添加steer 命令
This commit is contained in:
parent
b64a30ef48
commit
10d25a88f9
|
|
@ -698,6 +698,13 @@ const SLASH_COMMAND_SPECS: &[SlashCommandSpec] = &[
|
|||
argument_hint: Some("[count]"),
|
||||
resume_supported: false,
|
||||
},
|
||||
SlashCommandSpec {
|
||||
name: "steer",
|
||||
aliases: &[],
|
||||
summary: "Inject guidance during AI output without interrupting the conversation",
|
||||
argument_hint: Some("<text>"),
|
||||
resume_supported: false,
|
||||
},
|
||||
SlashCommandSpec {
|
||||
name: "tokens",
|
||||
aliases: &[],
|
||||
|
|
@ -1179,6 +1186,9 @@ pub enum SlashCommand {
|
|||
History {
|
||||
count: Option<String>,
|
||||
},
|
||||
Steer {
|
||||
text: String,
|
||||
},
|
||||
Unknown(String),
|
||||
}
|
||||
|
||||
|
|
@ -1222,6 +1232,7 @@ impl SlashCommand {
|
|||
Self::Config { .. } => "/config",
|
||||
Self::Memory { .. } => "/memory",
|
||||
Self::History { .. } => "/history",
|
||||
Self::Steer { .. } => "/steer",
|
||||
Self::Diff => "/diff",
|
||||
Self::Status => "/status",
|
||||
Self::Stats => "/stats",
|
||||
|
|
@ -1488,10 +1499,13 @@ pub fn validate_slash_command_input(
|
|||
"tag" => SlashCommand::Tag { label: remainder },
|
||||
"output-style" => SlashCommand::OutputStyle { style: remainder },
|
||||
"add-dir" => SlashCommand::AddDir { path: remainder },
|
||||
"history" => SlashCommand::History {
|
||||
count: optional_single_arg(command, &args, "[count]")?,
|
||||
},
|
||||
other => SlashCommand::Unknown(other.to_string()),
|
||||
"history" => SlashCommand::History {
|
||||
count: optional_single_arg(command, &args, "[count]")?,
|
||||
},
|
||||
"steer" => SlashCommand::Steer {
|
||||
text: remainder.unwrap_or_default().to_string(),
|
||||
},
|
||||
other => SlashCommand::Unknown(other.to_string()),
|
||||
}))
|
||||
}
|
||||
fn validate_no_args(command: &str, args: &[&str]) -> Result<(), SlashCommandParseError> {
|
||||
|
|
@ -4153,8 +4167,9 @@ pub fn handle_slash_command(
|
|||
| SlashCommand::Tag { .. }
|
||||
| SlashCommand::OutputStyle { .. }
|
||||
| SlashCommand::AddDir { .. }
|
||||
| SlashCommand::History { .. }
|
||||
| SlashCommand::Unknown(_) => None,
|
||||
| SlashCommand::History { .. }
|
||||
| SlashCommand::Steer { .. }
|
||||
| SlashCommand::Unknown(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -4501,6 +4516,40 @@ mod tests {
|
|||
assert_eq!(parsed, Ok(Some(SlashCommand::History { count: None })));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_steer_command_with_text() {
|
||||
// given
|
||||
let input = "/steer focus on testing";
|
||||
|
||||
// when
|
||||
let parsed = SlashCommand::parse(input);
|
||||
|
||||
// then
|
||||
assert_eq!(
|
||||
parsed,
|
||||
Ok(Some(SlashCommand::Steer {
|
||||
text: "focus on testing".to_string()
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_steer_command_without_text() {
|
||||
// given
|
||||
let input = "/steer";
|
||||
|
||||
// when
|
||||
let parsed = SlashCommand::parse(input);
|
||||
|
||||
// then
|
||||
assert_eq!(
|
||||
parsed,
|
||||
Ok(Some(SlashCommand::Steer {
|
||||
text: String::new()
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_history_command_with_numeric_count() {
|
||||
// given
|
||||
|
|
@ -4676,6 +4725,7 @@ mod tests {
|
|||
assert!(help.contains("/config [env|hooks|model|plugins]"));
|
||||
assert!(help.contains("/mcp [list|show <server>|help]"));
|
||||
assert!(help.contains("/memory"));
|
||||
assert!(help.contains("/steer <text>"));
|
||||
assert!(help.contains("/init"));
|
||||
assert!(help.contains("/diff"));
|
||||
assert!(help.contains("/version"));
|
||||
|
|
@ -4691,7 +4741,7 @@ mod tests {
|
|||
assert!(help.contains("aliases: /skill"));
|
||||
assert!(!help.contains("/login"));
|
||||
assert!(!help.contains("/logout"));
|
||||
assert_eq!(slash_command_specs().len(), 139);
|
||||
assert_eq!(slash_command_specs().len(), 140);
|
||||
assert!(resume_supported_slash_commands().len() >= 10);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1349,6 +1349,7 @@ pub(crate) fn slash_command_completion_candidates_with_sessions(
|
|||
"/plugin update ",
|
||||
"/plugins list",
|
||||
"/pr ",
|
||||
"/steer ",
|
||||
"/resume ",
|
||||
"/session list",
|
||||
"/session switch ",
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ pub(crate) struct LiveCli {
|
|||
pub(crate) runtime: BuiltRuntime,
|
||||
pub(crate) session: SessionHandle,
|
||||
pub(crate) prompt_history: Vec<PromptHistoryEntry>,
|
||||
pub(crate) steer_queue: SteerQueue,
|
||||
}
|
||||
|
||||
impl LiveCli {
|
||||
|
|
@ -52,6 +53,7 @@ impl LiveCli {
|
|||
runtime,
|
||||
session,
|
||||
prompt_history: Vec::new(),
|
||||
steer_queue: new_steer_queue(),
|
||||
};
|
||||
cli.persist_session()?;
|
||||
Ok(cli)
|
||||
|
|
@ -152,16 +154,18 @@ impl LiveCli {
|
|||
}
|
||||
|
||||
pub(crate) fn run_turn(&mut self, input: &str) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let effective_input = self.build_effective_input(input);
|
||||
|
||||
let (mut runtime, hook_abort_monitor) = self.prepare_turn_runtime(true)?;
|
||||
let mut spinner = Spinner::new();
|
||||
let mut stdout = io::stdout();
|
||||
spinner.tick(
|
||||
"🦀 Thinking...",
|
||||
"🦀 Thinking... (type /steer <text> to guide)",
|
||||
TerminalRenderer::new().color_theme(),
|
||||
&mut stdout,
|
||||
)?;
|
||||
let mut permission_prompter = CliPermissionPrompter::new(self.permission_mode);
|
||||
let result = runtime.run_turn(input, Some(&mut permission_prompter));
|
||||
let result = runtime.run_turn(&effective_input, Some(&mut permission_prompter));
|
||||
hook_abort_monitor.stop();
|
||||
match result {
|
||||
Ok(summary) => {
|
||||
|
|
@ -314,6 +318,20 @@ impl LiveCli {
|
|||
}
|
||||
}
|
||||
|
||||
/// Build effective input by prepending any queued steer texts.
|
||||
fn build_effective_input(&self, input: &str) -> String {
|
||||
let steer_texts = steer_drain(&self.steer_queue);
|
||||
if steer_texts.is_empty() {
|
||||
return input.to_string();
|
||||
}
|
||||
let steer_block = steer_texts
|
||||
.iter()
|
||||
.map(|t| format!("[steer] {t}"))
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
format!("{steer_block}\n\n{input}")
|
||||
}
|
||||
|
||||
pub(crate) fn reload_runtime_features(&mut self) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let runtime = build_runtime(
|
||||
self.runtime.session().clone(),
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ mod tool_executor;
|
|||
mod runtime_builder;
|
||||
mod repl_commands;
|
||||
mod setup_wizard;
|
||||
mod steer;
|
||||
|
||||
pub(crate) use api_client::*;
|
||||
pub(crate) use args::*;
|
||||
|
|
@ -57,6 +58,8 @@ pub(crate) use render::*;
|
|||
pub(crate) use commands::*;
|
||||
pub(crate) use runtime::*;
|
||||
|
||||
pub(crate) use steer::*;
|
||||
|
||||
use std::env;
|
||||
use std::fs;
|
||||
use std::io::{self, IsTerminal, Read, Write};
|
||||
|
|
|
|||
|
|
@ -126,6 +126,15 @@ impl LiveCli {
|
|||
self.print_prompt_history(count.as_deref());
|
||||
false
|
||||
}
|
||||
SlashCommand::Steer { text } => {
|
||||
if text.trim().is_empty() {
|
||||
eprintln!("Usage: /steer <text>");
|
||||
} else {
|
||||
steer_push(&self.steer_queue, text.clone());
|
||||
println!("🧭 Steer queued: {text}");
|
||||
}
|
||||
false
|
||||
}
|
||||
SlashCommand::Stats => {
|
||||
let usage = UsageTracker::from_session(self.runtime.session()).cumulative_usage();
|
||||
println!("{}", format_cost_report(usage));
|
||||
|
|
@ -1154,7 +1163,8 @@ pub(crate) fn run_resume_command(
|
|||
| SlashCommand::Ide { .. }
|
||||
| SlashCommand::Tag { .. }
|
||||
| SlashCommand::OutputStyle { .. }
|
||||
| SlashCommand::AddDir { .. } => Err("unsupported resumed slash command".into()),
|
||||
| SlashCommand::AddDir { .. }
|
||||
| SlashCommand::Steer { .. } => Err("unsupported resumed slash command".into()),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,95 @@
|
|||
use std::collections::VecDeque;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
/// Thread-safe queue for `/steer` texts injected during AI output.
|
||||
///
|
||||
/// The queue supports concurrent push (from stdin polling) and
|
||||
/// drain (from the `run_turn` conversation loop).
|
||||
pub(crate) type SteerQueue = Arc<Mutex<VecDeque<String>>>;
|
||||
|
||||
/// Create a new empty `SteerQueue`.
|
||||
pub(crate) fn new_steer_queue() -> SteerQueue {
|
||||
Arc::new(Mutex::new(VecDeque::new()))
|
||||
}
|
||||
|
||||
/// Push a steer text into the queue.
|
||||
pub(crate) fn steer_push(queue: &SteerQueue, text: String) {
|
||||
if text.trim().is_empty() {
|
||||
return;
|
||||
}
|
||||
let mut guard = queue.lock().unwrap_or_else(|e| e.into_inner());
|
||||
guard.push_back(text);
|
||||
}
|
||||
|
||||
/// Drain all pending steer texts from the queue, returning them in FIFO order.
|
||||
pub(crate) fn steer_drain(queue: &SteerQueue) -> Vec<String> {
|
||||
let mut guard = queue.lock().unwrap_or_else(|e| e.into_inner());
|
||||
guard.drain(..).collect()
|
||||
}
|
||||
|
||||
/// Poll stdin for `/steer <text>` input using crossterm non-blocking reads.
|
||||
///
|
||||
/// This function checks if there is pending stdin data during AI output.
|
||||
/// Currently a placeholder — the primary input path is through the idle
|
||||
/// REPL prompt's rustyline handler. Will be enhanced with raw-mode stdin
|
||||
/// line accumulation in a future iteration.
|
||||
pub(crate) fn poll_steer_input(_queue: &SteerQueue) -> usize {
|
||||
0
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::thread;
|
||||
|
||||
#[test]
|
||||
fn push_and_drain_single() {
|
||||
let queue = new_steer_queue();
|
||||
steer_push(&queue, "focus on tests".to_string());
|
||||
let drained = steer_drain(&queue);
|
||||
assert_eq!(drained, vec!["focus on tests"]);
|
||||
// Queue should be empty after drain
|
||||
assert!(steer_drain(&queue).is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn push_and_drain_multiple_in_order() {
|
||||
let queue = new_steer_queue();
|
||||
steer_push(&queue, "A".to_string());
|
||||
steer_push(&queue, "B".to_string());
|
||||
let drained = steer_drain(&queue);
|
||||
assert_eq!(drained, vec!["A", "B"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn push_ignores_empty_and_whitespace() {
|
||||
let queue = new_steer_queue();
|
||||
steer_push(&queue, "".to_string());
|
||||
steer_push(&queue, " ".to_string());
|
||||
steer_push(&queue, "valid".to_string());
|
||||
let drained = steer_drain(&queue);
|
||||
assert_eq!(drained, vec!["valid"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn concurrent_push_is_safe() {
|
||||
let queue = new_steer_queue();
|
||||
let handles: Vec<_> = (0..4)
|
||||
.map(|i| {
|
||||
let q = Arc::clone(&queue);
|
||||
thread::spawn(move || {
|
||||
steer_push(&q, format!("steer-{i}"));
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
for h in handles {
|
||||
h.join().unwrap();
|
||||
}
|
||||
let mut drained = steer_drain(&queue);
|
||||
drained.sort();
|
||||
assert_eq!(
|
||||
drained,
|
||||
vec!["steer-0", "steer-1", "steer-2", "steer-3"]
|
||||
);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue