Skip to content

Commit b709854

Browse files
author
kiran-garre
committed
Merge branch 'main' of github.com:aws/amazon-q-developer-cli into kiran-garre/git-checkpoints
2 parents bc23993 + 22783a8 commit b709854

File tree

13 files changed

+308
-68
lines changed

13 files changed

+308
-68
lines changed

crates/chat-cli/src/cli/chat/cli/profile.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ use crate::util::{
5555
5656
Notes
5757
• Launch q chat with a specific agent with --agent
58-
• Construct an agent under ~/.aws/amazonq/cli-agents/ (accessible globally) or cwd/.aws/amazonq/cli-agents (accessible in workspace)
58+
• Construct an agent under ~/.aws/amazonq/cli-agents/ (accessible globally) or cwd/.amazonq/cli-agents (accessible in workspace)
5959
• See example config under global directory
6060
• Set default agent to assume with settings by running \"q settings chat.defaultAgent agent_name\"
6161
• Each agent maintains its own set of context and customizations"

crates/chat-cli/src/cli/chat/cli/tangent.rs

Lines changed: 107 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1-
use clap::Args;
1+
use clap::{
2+
Args,
3+
Subcommand,
4+
};
25
use crossterm::execute;
36
use crossterm::style::{
47
self,
@@ -14,9 +17,33 @@ use crate::database::settings::Setting;
1417
use crate::os::Os;
1518

1619
#[derive(Debug, PartialEq, Args)]
17-
pub struct TangentArgs;
20+
pub struct TangentArgs {
21+
#[command(subcommand)]
22+
pub subcommand: Option<TangentSubcommand>,
23+
}
24+
25+
#[derive(Debug, PartialEq, Subcommand)]
26+
pub enum TangentSubcommand {
27+
/// Exit tangent mode and keep the last conversation entry (user question + assistant response)
28+
Tail,
29+
}
1830

1931
impl TangentArgs {
32+
async fn send_tangent_telemetry(os: &Os, session: &ChatSession, duration_seconds: i64) {
33+
if let Err(err) = os
34+
.telemetry
35+
.send_tangent_mode_session(
36+
&os.database,
37+
session.conversation.conversation_id().to_string(),
38+
crate::telemetry::TelemetryResult::Succeeded,
39+
crate::telemetry::core::TangentModeSessionArgs { duration_seconds },
40+
)
41+
.await
42+
{
43+
tracing::warn!(?err, "Failed to send tangent mode session telemetry");
44+
}
45+
}
46+
2047
pub async fn execute(self, os: &Os, session: &mut ChatSession) -> Result<ChatState, ChatError> {
2148
// Check if tangent mode is enabled
2249
if !os
@@ -35,69 +62,86 @@ impl TangentArgs {
3562
skip_printing_tools: true,
3663
});
3764
}
38-
if session.conversation.is_in_tangent_mode() {
39-
// Get duration before exiting tangent mode
40-
let duration_seconds = session.conversation.get_tangent_duration_seconds().unwrap_or(0);
41-
42-
session.conversation.exit_tangent_mode();
43-
44-
// Send telemetry for tangent mode session
45-
if let Err(err) = os
46-
.telemetry
47-
.send_tangent_mode_session(
48-
&os.database,
49-
session.conversation.conversation_id().to_string(),
50-
crate::telemetry::TelemetryResult::Succeeded,
51-
crate::telemetry::core::TangentModeSessionArgs { duration_seconds },
52-
)
53-
.await
54-
{
55-
tracing::warn!(?err, "Failed to send tangent mode session telemetry");
56-
}
5765

58-
execute!(
59-
session.stderr,
60-
style::SetForegroundColor(Color::DarkGrey),
61-
style::Print("Restored conversation from checkpoint ("),
62-
style::SetForegroundColor(Color::Yellow),
63-
style::Print("↯"),
64-
style::SetForegroundColor(Color::DarkGrey),
65-
style::Print("). - Returned to main conversation.\n"),
66-
style::SetForegroundColor(Color::Reset)
67-
)?;
68-
} else {
69-
session.conversation.enter_tangent_mode();
70-
71-
// Get the configured tangent mode key for display
72-
let tangent_key_char = match os
73-
.database
74-
.settings
75-
.get_string(crate::database::settings::Setting::TangentModeKey)
76-
{
77-
Some(key) if key.len() == 1 => key.chars().next().unwrap_or('t'),
78-
_ => 't', // Default to 't' if setting is missing or invalid
79-
};
80-
let tangent_key_display = format!("ctrl + {}", tangent_key_char.to_lowercase());
66+
match self.subcommand {
67+
Some(TangentSubcommand::Tail) => {
68+
if session.conversation.is_in_tangent_mode() {
69+
let duration_seconds = session.conversation.get_tangent_duration_seconds().unwrap_or(0);
70+
session.conversation.exit_tangent_mode_with_tail();
71+
Self::send_tangent_telemetry(os, session, duration_seconds).await;
8172

82-
execute!(
83-
session.stderr,
84-
style::SetForegroundColor(Color::DarkGrey),
85-
style::Print("Created a conversation checkpoint ("),
86-
style::SetForegroundColor(Color::Yellow),
87-
style::Print("↯"),
88-
style::SetForegroundColor(Color::DarkGrey),
89-
style::Print("). Use "),
90-
style::SetForegroundColor(Color::Green),
91-
style::Print(&tangent_key_display),
92-
style::SetForegroundColor(Color::DarkGrey),
93-
style::Print(" or "),
94-
style::SetForegroundColor(Color::Green),
95-
style::Print("/tangent"),
96-
style::SetForegroundColor(Color::DarkGrey),
97-
style::Print(" to restore the conversation later.\n"),
98-
style::Print("Note: this functionality is experimental and may change or be removed in the future.\n"),
99-
style::SetForegroundColor(Color::Reset)
100-
)?;
73+
execute!(
74+
session.stderr,
75+
style::SetForegroundColor(Color::DarkGrey),
76+
style::Print("Restored conversation from checkpoint ("),
77+
style::SetForegroundColor(Color::Yellow),
78+
style::Print("↯"),
79+
style::SetForegroundColor(Color::DarkGrey),
80+
style::Print(") with last conversation entry preserved.\n"),
81+
style::SetForegroundColor(Color::Reset)
82+
)?;
83+
} else {
84+
execute!(
85+
session.stderr,
86+
style::SetForegroundColor(Color::Red),
87+
style::Print("You need to be in tangent mode to use tail.\n"),
88+
style::SetForegroundColor(Color::Reset)
89+
)?;
90+
}
91+
},
92+
None => {
93+
if session.conversation.is_in_tangent_mode() {
94+
let duration_seconds = session.conversation.get_tangent_duration_seconds().unwrap_or(0);
95+
session.conversation.exit_tangent_mode();
96+
Self::send_tangent_telemetry(os, session, duration_seconds).await;
97+
98+
execute!(
99+
session.stderr,
100+
style::SetForegroundColor(Color::DarkGrey),
101+
style::Print("Restored conversation from checkpoint ("),
102+
style::SetForegroundColor(Color::Yellow),
103+
style::Print("↯"),
104+
style::SetForegroundColor(Color::DarkGrey),
105+
style::Print("). - Returned to main conversation.\n"),
106+
style::SetForegroundColor(Color::Reset)
107+
)?;
108+
} else {
109+
session.conversation.enter_tangent_mode();
110+
111+
// Get the configured tangent mode key for display
112+
let tangent_key_char = match os
113+
.database
114+
.settings
115+
.get_string(crate::database::settings::Setting::TangentModeKey)
116+
{
117+
Some(key) if key.len() == 1 => key.chars().next().unwrap_or('t'),
118+
_ => 't', // Default to 't' if setting is missing or invalid
119+
};
120+
let tangent_key_display = format!("ctrl + {}", tangent_key_char.to_lowercase());
121+
122+
execute!(
123+
session.stderr,
124+
style::SetForegroundColor(Color::DarkGrey),
125+
style::Print("Created a conversation checkpoint ("),
126+
style::SetForegroundColor(Color::Yellow),
127+
style::Print("↯"),
128+
style::SetForegroundColor(Color::DarkGrey),
129+
style::Print("). Use "),
130+
style::SetForegroundColor(Color::Green),
131+
style::Print(&tangent_key_display),
132+
style::SetForegroundColor(Color::DarkGrey),
133+
style::Print(" or "),
134+
style::SetForegroundColor(Color::Green),
135+
style::Print("/tangent"),
136+
style::SetForegroundColor(Color::DarkGrey),
137+
style::Print(" to restore the conversation later.\n"),
138+
style::Print(
139+
"Note: this functionality is experimental and may change or be removed in the future.\n"
140+
),
141+
style::SetForegroundColor(Color::Reset)
142+
)?;
143+
}
144+
},
101145
}
102146

103147
Ok(ChatState::PromptUser {

crates/chat-cli/src/cli/chat/conversation.rs

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,27 @@ impl ConversationState {
273273
}
274274
}
275275

276+
/// Exit tangent mode and preserve the last conversation entry (user + assistant)
277+
pub fn exit_tangent_mode_with_tail(&mut self) {
278+
if let Some(checkpoint) = self.tangent_state.take() {
279+
// Capture the last history entry from tangent conversation if it exists
280+
// and if it's different from what was in the main conversation
281+
let last_entry = if self.history.len() > checkpoint.main_history.len() {
282+
self.history.back().cloned()
283+
} else {
284+
None // No new entries in tangent mode
285+
};
286+
287+
// Restore from checkpoint
288+
self.restore_from_checkpoint(checkpoint);
289+
290+
// Add the last entry if it exists
291+
if let Some(entry) = last_entry {
292+
self.history.push_back(entry);
293+
}
294+
}
295+
}
296+
276297
/// Appends a collection prompts into history and returns the last message in the collection.
277298
/// It asserts that the collection ends with a prompt that assumes the role of user.
278299
pub fn append_prompts(&mut self, mut prompts: VecDeque<PromptMessage>) -> Option<String> {
@@ -1577,4 +1598,99 @@ mod tests {
15771598
// No duration when not in tangent mode
15781599
assert!(conversation.get_tangent_duration_seconds().is_none());
15791600
}
1601+
1602+
#[tokio::test]
1603+
async fn test_tangent_mode_with_tail() {
1604+
let mut os = Os::new().await.unwrap();
1605+
let agents = Agents::default();
1606+
let mut tool_manager = ToolManager::default();
1607+
let mut conversation = ConversationState::new(
1608+
"test_conv_id",
1609+
agents,
1610+
tool_manager.load_tools(&mut os, &mut vec![]).await.unwrap(),
1611+
tool_manager,
1612+
None,
1613+
&os,
1614+
false,
1615+
)
1616+
.await;
1617+
1618+
// Add main conversation
1619+
conversation.set_next_user_message("main question".to_string()).await;
1620+
conversation.push_assistant_message(
1621+
&mut os,
1622+
AssistantMessage::new_response(None, "main response".to_string()),
1623+
None,
1624+
);
1625+
1626+
let main_history_len = conversation.history.len();
1627+
1628+
// Enter tangent mode
1629+
conversation.enter_tangent_mode();
1630+
assert!(conversation.is_in_tangent_mode());
1631+
1632+
// Add tangent conversation
1633+
conversation.set_next_user_message("tangent question".to_string()).await;
1634+
conversation.push_assistant_message(
1635+
&mut os,
1636+
AssistantMessage::new_response(None, "tangent response".to_string()),
1637+
None,
1638+
);
1639+
1640+
// Exit tangent mode with tail
1641+
conversation.exit_tangent_mode_with_tail();
1642+
assert!(!conversation.is_in_tangent_mode());
1643+
1644+
// Should have main conversation + last assistant message from tangent
1645+
assert_eq!(conversation.history.len(), main_history_len + 1);
1646+
1647+
// Check that the last message is the tangent response
1648+
if let Some(entry) = conversation.history.back() {
1649+
assert_eq!(entry.assistant.content(), "tangent response");
1650+
} else {
1651+
panic!("Expected history entry at the end");
1652+
}
1653+
}
1654+
1655+
#[tokio::test]
1656+
async fn test_tangent_mode_with_tail_edge_cases() {
1657+
let mut os = Os::new().await.unwrap();
1658+
let agents = Agents::default();
1659+
let mut tool_manager = ToolManager::default();
1660+
let mut conversation = ConversationState::new(
1661+
"test_conv_id",
1662+
agents,
1663+
tool_manager.load_tools(&mut os, &mut vec![]).await.unwrap(),
1664+
tool_manager,
1665+
None,
1666+
&os,
1667+
false,
1668+
)
1669+
.await;
1670+
1671+
// Add main conversation
1672+
conversation.set_next_user_message("main question".to_string()).await;
1673+
conversation.push_assistant_message(
1674+
&mut os,
1675+
AssistantMessage::new_response(None, "main response".to_string()),
1676+
None,
1677+
);
1678+
1679+
let main_history_len = conversation.history.len();
1680+
1681+
// Test: Enter tangent mode but don't add any new conversation
1682+
conversation.enter_tangent_mode();
1683+
assert!(conversation.is_in_tangent_mode());
1684+
1685+
// Exit tangent mode with tail (should not add anything since no new entries)
1686+
conversation.exit_tangent_mode_with_tail();
1687+
assert!(!conversation.is_in_tangent_mode());
1688+
1689+
// Should have same length as before (no new entries added)
1690+
assert_eq!(conversation.history.len(), main_history_len);
1691+
1692+
// Test: Call exit_tangent_mode_with_tail when not in tangent mode (should do nothing)
1693+
conversation.exit_tangent_mode_with_tail();
1694+
assert_eq!(conversation.history.len(), main_history_len);
1695+
}
15801696
}

crates/chat-cli/src/cli/chat/tool_manager.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1923,7 +1923,7 @@ fn queue_failure_message(
19231923
style::Print(fail_load_msg),
19241924
style::Print("\n"),
19251925
style::Print(format!(
1926-
" - run with Q_LOG_LEVEL=trace and see $TMPDIR/{CHAT_BINARY_NAME} for detail\n"
1926+
" - run with Q_LOG_LEVEL=trace and see $TMPDIR/qlog/{CHAT_BINARY_NAME}.log for detail\n"
19271927
)),
19281928
style::ResetColor,
19291929
)?)

crates/chat-cli/src/cli/chat/tools/execute/mod.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ impl ExecuteCommand {
7070
let Some(args) = shlex::split(&self.command) else {
7171
return true;
7272
};
73-
const DANGEROUS_PATTERNS: &[&str] = &["<(", "$(", "`", ">", "&&", "||", "&", ";", "${", "\n", "\r", "IFS"];
73+
const DANGEROUS_PATTERNS: &[&str] = &["<(", "$(", "`", ">", "&&", "||", "&", ";", "$", "\n", "\r", "IFS"];
7474

7575
if args
7676
.iter()
@@ -328,6 +328,7 @@ mod tests {
328328
(r#"find / -fprintf "/path/to/file" <data-to-write> -quit"#, true),
329329
(r"find . -${t}exec touch asdf \{\} +", true),
330330
(r"find . -${t:=exec} touch asdf2 \{\} +", true),
331+
(r#"find /tmp -name "*" -exe$9c touch /tmp/find_result {} +"#, true),
331332
// `grep` command arguments
332333
("echo 'test data' | grep -P '(?{system(\"date\")})'", true),
333334
("echo 'test data' | grep --perl-regexp '(?{system(\"date\")})'", true),

crates/chat-cli/src/cli/mod.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,11 @@ impl RootSubcommand {
144144
);
145145
}
146146

147+
// Daily heartbeat check
148+
if os.database.should_send_heartbeat() && os.telemetry.send_daily_heartbeat().is_ok() {
149+
os.database.record_heartbeat_sent().ok();
150+
}
151+
147152
// Send executed telemetry.
148153
if self.valid_for_telemetry() {
149154
os.telemetry

0 commit comments

Comments
 (0)