Skip to content

Commit eb34f4d

Browse files
committed
Feat: Add accept all flag and command to chat, and change verbage around consent
1 parent cca6b4f commit eb34f4d

File tree

6 files changed

+68
-33
lines changed

6 files changed

+68
-33
lines changed

crates/q_cli/src/cli/chat/command.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ pub enum Command {
55
Execute { command: String },
66
Clear,
77
Help,
8+
AcceptAll,
89
Quit,
910
}
1011

@@ -16,6 +17,7 @@ impl Command {
1617
return Ok(match command.to_lowercase().as_str() {
1718
"clear" => Self::Clear,
1819
"help" => Self::Help,
20+
"acceptall" => Self::AcceptAll,
1921
"q" | "exit" | "quit" => Self::Quit,
2022
_ => return Err(format!("Unknown command: {}", input)),
2123
});

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

Lines changed: 44 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -105,9 +105,12 @@ const HELP_TEXT: &str = color_print::cstr! {"
105105
106106
<em>!{command}</em> <black!>Quickly execute a command in your current session</black!>
107107
108+
<black!>Use the following dangerous command at your own discretion.</black!>
109+
<em>/acceptall</em> <black!>Disables acceptance prompting for the session.</black!>
110+
108111
"};
109112

110-
pub async fn chat(input: Option<String>) -> Result<ExitCode> {
113+
pub async fn chat(input: Option<String>, accept_all: bool) -> Result<ExitCode> {
111114
if !fig_util::system_info::in_cloudshell() && !fig_auth::is_logged_in().await {
112115
bail!(
113116
"You are not logged in, please log in with {}",
@@ -136,9 +139,16 @@ pub async fn chat(input: Option<String>) -> Result<ExitCode> {
136139
_ => StreamingClient::new().await?,
137140
};
138141

139-
let mut chat = ChatContext::new(ctx, output, input, InputSource::new()?, interactive, client, || {
140-
terminal::window_size().map(|s| s.columns.into()).ok()
141-
})?;
142+
let mut chat = ChatContext::new(
143+
ctx,
144+
output,
145+
input,
146+
InputSource::new()?,
147+
interactive,
148+
client,
149+
|| terminal::window_size().map(|s| s.columns.into()).ok(),
150+
accept_all,
151+
)?;
142152

143153
let result = chat.try_chat().await.map(|_| ExitCode::SUCCESS);
144154
drop(chat); // Explicit drop for clarity
@@ -191,9 +201,11 @@ pub struct ChatContext<W: Write> {
191201
tool_use_telemetry_events: HashMap<String, ToolUseEventBuilder>,
192202
/// State used to keep track of tool use relation
193203
tool_use_status: ToolUseStatus,
204+
accept_all: bool,
194205
}
195206

196207
impl<W: Write> ChatContext<W> {
208+
#[allow(clippy::too_many_arguments)]
197209
pub fn new(
198210
ctx: Arc<Context>,
199211
output: W,
@@ -202,6 +214,7 @@ impl<W: Write> ChatContext<W> {
202214
interactive: bool,
203215
client: StreamingClient,
204216
terminal_width_provider: fn() -> Option<usize>,
217+
accept_all: bool,
205218
) -> Result<Self> {
206219
Ok(Self {
207220
ctx,
@@ -215,6 +228,7 @@ impl<W: Write> ChatContext<W> {
215228
conversation_state: ConversationState::new(load_tools()?),
216229
tool_use_telemetry_events: HashMap::new(),
217230
tool_use_status: ToolUseStatus::Idle,
231+
accept_all,
218232
})
219233
}
220234
}
@@ -429,7 +443,7 @@ where
429443
style::Print("y"),
430444
style::SetForegroundColor(Color::DarkGrey),
431445
style::Print(format!(
432-
" to run {}, otherwise continue without.\n\n",
446+
" to run {}, otherwise continue chatting.\n\n",
433447
match tool_uses.len() == 1 {
434448
true => "this tool",
435449
false => "these tools",
@@ -499,7 +513,7 @@ where
499513
execute!(
500514
self.output,
501515
style::SetForegroundColor(Color::Green),
502-
style::Print("\nConversation history cleared\n\n"),
516+
style::Print("\nConversation history cleared.\n\n"),
503517
style::SetForegroundColor(Color::Reset)
504518
)?;
505519

@@ -509,6 +523,21 @@ where
509523
execute!(self.output, style::Print(HELP_TEXT))?;
510524
ChatState::PromptUser { tool_uses: None }
511525
},
526+
Command::AcceptAll => {
527+
self.accept_all = !self.accept_all;
528+
529+
execute!(
530+
self.output,
531+
style::SetForegroundColor(Color::Green),
532+
style::Print(format!("\n{} acceptance prompting.\n\n", match self.accept_all {
533+
true => "Disabled",
534+
false => "Enabled",
535+
})),
536+
style::SetForegroundColor(Color::Reset)
537+
)?;
538+
539+
ChatState::PromptUser { tool_uses: None }
540+
},
512541
Command::Quit => ChatState::Exit,
513542
})
514543
}
@@ -851,20 +880,16 @@ where
851880
return Ok(ChatState::HandleResponseStream(response));
852881
}
853882

854-
let skip_consent = self
855-
.ctx
856-
.env()
857-
.get("Q_CHAT_SKIP_TOOL_CONSENT")
858-
.is_ok_and(|s| !s.is_empty() && !queued_tools.is_empty())
859-
|| queued_tools.iter().all(|tool| !tool.1.requires_consent(&self.ctx));
883+
let skip_acceptance = self.accept_all || queued_tools.iter().all(|tool| !tool.1.requires_acceptance(&self.ctx));
860884

861-
if skip_consent {
862-
self.print_tool_descriptions(&queued_tools)?;
863-
Ok(ChatState::ExecuteTools(queued_tools))
864-
} else {
865-
Ok(ChatState::PromptUser {
885+
match skip_acceptance {
886+
true => {
887+
self.print_tool_descriptions(&queued_tools)?;
888+
Ok(ChatState::ExecuteTools(queued_tools))
889+
},
890+
false => Ok(ChatState::PromptUser {
866891
tool_uses: Some(queued_tools),
867-
})
892+
}),
868893
}
869894
}
870895

@@ -1080,6 +1105,7 @@ mod tests {
10801105
true,
10811106
test_client,
10821107
|| Some(80),
1108+
false,
10831109
)
10841110
.unwrap()
10851111
.try_chat()

crates/q_cli/src/cli/chat/tools/execute_bash.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ pub struct ExecuteBash {
3232
}
3333

3434
impl ExecuteBash {
35-
pub fn requires_consent(&self) -> bool {
35+
pub fn requires_acceptance(&self) -> bool {
3636
let Some(args) = shlex::split(&self.command) else {
3737
return true;
3838
};
@@ -228,7 +228,7 @@ mod tests {
228228
}
229229

230230
#[test]
231-
fn test_requires_consent_for_readonly_commands() {
231+
fn test_requires_acceptance_for_readonly_commands() {
232232
let cmds = &[
233233
// Safe commands
234234
("ls ~", false),
@@ -259,9 +259,9 @@ mod tests {
259259
}))
260260
.unwrap();
261261
assert_eq!(
262-
tool.requires_consent(),
262+
tool.requires_acceptance(),
263263
*expected,
264-
"expected command: `{}` to have requires_consent: `{}`",
264+
"expected command: `{}` to have requires_acceptance: `{}`",
265265
cmd,
266266
expected
267267
);

crates/q_cli/src/cli/chat/tools/mod.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -80,13 +80,13 @@ impl Tool {
8080
.to_owned()
8181
}
8282

83-
/// Whether or not the tool should prompt the user for consent before [Self::invoke] is called.
84-
pub fn requires_consent(&self, _ctx: &Context) -> bool {
83+
/// Whether or not the tool should prompt the user to accept before [Self::invoke] is called.
84+
pub fn requires_acceptance(&self, _ctx: &Context) -> bool {
8585
match self {
8686
Tool::FsRead(_) => false,
8787
Tool::FsWrite(_) => true,
88-
Tool::ExecuteBash(execute_bash) => execute_bash.requires_consent(),
89-
Tool::UseAws(use_aws) => use_aws.requires_consent(),
88+
Tool::ExecuteBash(execute_bash) => execute_bash.requires_acceptance(),
89+
Tool::UseAws(use_aws) => use_aws.requires_acceptance(),
9090
}
9191
}
9292

crates/q_cli/src/cli/chat/tools/use_aws.rs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ pub struct UseAws {
3939
}
4040

4141
impl UseAws {
42-
pub fn requires_consent(&self) -> bool {
42+
pub fn requires_acceptance(&self) -> bool {
4343
!READONLY_OPS.iter().any(|op| self.operation_name.starts_with(op))
4444
}
4545

@@ -160,31 +160,31 @@ mod tests {
160160
}
161161

162162
#[test]
163-
fn test_requires_consent() {
163+
fn test_requires_acceptance() {
164164
let cmd = use_aws! {{
165165
"service_name": "ecs",
166166
"operation_name": "list-task-definitions",
167167
"region": "us-west-2",
168168
"profile_name": "default",
169169
"label": ""
170170
}};
171-
assert!(!cmd.requires_consent());
171+
assert!(!cmd.requires_acceptance());
172172
let cmd = use_aws! {{
173173
"service_name": "lambda",
174174
"operation_name": "list-functions",
175175
"region": "us-west-2",
176176
"profile_name": "default",
177177
"label": ""
178178
}};
179-
assert!(!cmd.requires_consent());
179+
assert!(!cmd.requires_acceptance());
180180
let cmd = use_aws! {{
181181
"service_name": "s3",
182182
"operation_name": "put-object",
183183
"region": "us-west-2",
184184
"profile_name": "default",
185185
"label": ""
186186
}};
187-
assert!(cmd.requires_consent());
187+
assert!(cmd.requires_acceptance());
188188
}
189189

190190
#[test]

crates/q_cli/src/cli/mod.rs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,10 @@ pub enum CliRootCommands {
182182
/// AI assistant in your terminal
183183
#[command(alias("q"))]
184184
Chat {
185+
/// Enabling this flag allows the model to execute all commands without first accepting
186+
/// them.
187+
#[arg(short, long)]
188+
accept_all: bool,
185189
/// The first question to ask
186190
input: Option<String>,
187191
},
@@ -328,7 +332,7 @@ impl Cli {
328332
CliRootCommands::Telemetry(subcommand) => subcommand.execute().await,
329333
CliRootCommands::Version => Self::print_version(),
330334
CliRootCommands::Dashboard => launch_dashboard(false).await,
331-
CliRootCommands::Chat { input } => chat::chat(input).await,
335+
CliRootCommands::Chat { accept_all, input } => chat::chat(input, accept_all).await,
332336
CliRootCommands::Inline(subcommand) => subcommand.execute(&cli_context).await,
333337
},
334338
// Root command
@@ -447,7 +451,10 @@ mod test {
447451
});
448452

449453
assert_eq!(Cli::parse_from([CLI_BINARY_NAME, "chat", "-vv"]), Cli {
450-
subcommand: Some(CliRootCommands::Chat { input: None },),
454+
subcommand: Some(CliRootCommands::Chat {
455+
accept_all: false,
456+
input: None
457+
},),
451458
verbose: 2,
452459
help_all: false,
453460
});

0 commit comments

Comments
 (0)