feat: keep startup prompts interactive
This commit is contained in:
+14
-2
@@ -2958,11 +2958,15 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_top_level_prompt_flag_for_canonical_one_shot() {
|
||||
fn parses_top_level_prompt_flag_for_interactive_startup_prompt() {
|
||||
let cli = parse_ok(&["deepseek", "-p", "Reply with exactly OK."]);
|
||||
|
||||
assert_eq!(cli.prompt_flag.as_deref(), Some("Reply with exactly OK."));
|
||||
assert!(cli.prompt.is_empty());
|
||||
assert_eq!(
|
||||
root_tui_passthrough(&cli).unwrap(),
|
||||
vec!["--prompt".to_string(), "Reply with exactly OK.".to_string()]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -2976,7 +2980,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn top_level_continue_rejects_one_shot_prompt() {
|
||||
fn top_level_continue_rejects_startup_prompt() {
|
||||
let cli = parse_ok(&["codewhale", "--continue", "-p", "follow up"]);
|
||||
|
||||
let err = root_tui_passthrough(&cli).expect_err("prompted continue should be rejected");
|
||||
@@ -2992,6 +2996,10 @@ mod tests {
|
||||
|
||||
assert_eq!(cli.prompt, vec!["hello", "world"]);
|
||||
assert!(cli.command.is_none());
|
||||
assert_eq!(
|
||||
root_tui_passthrough(&cli).unwrap(),
|
||||
vec!["--prompt".to_string(), "hello world".to_string()]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -3000,6 +3008,10 @@ mod tests {
|
||||
|
||||
assert_eq!(cli.prompt_flag.as_deref(), Some("hello"));
|
||||
assert_eq!(cli.prompt, vec!["world"]);
|
||||
assert_eq!(
|
||||
root_tui_passthrough(&cli).unwrap(),
|
||||
vec!["--prompt".to_string(), "hello world".to_string()]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
+28
-8
@@ -121,7 +121,7 @@ struct Cli {
|
||||
#[command(flatten)]
|
||||
feature_toggles: FeatureToggles,
|
||||
|
||||
/// Send a one-shot prompt (non-interactive)
|
||||
/// Initial prompt to submit in the interactive TUI. Use `exec` for non-interactive runs.
|
||||
#[arg(short, long, value_name = "PROMPT", num_args = 1..)]
|
||||
prompt: Vec<String>,
|
||||
|
||||
@@ -427,6 +427,10 @@ fn join_prompt_parts(parts: &[String]) -> String {
|
||||
parts.join(" ")
|
||||
}
|
||||
|
||||
fn top_level_prompt_initial_input(parts: &[String]) -> Option<tui::InitialInput> {
|
||||
(!parts.is_empty()).then(|| tui::InitialInput::Submit(join_prompt_parts(parts)))
|
||||
}
|
||||
|
||||
fn resolve_exec_resume_session_id(args: &ExecArgs, workspace: &Path) -> Result<Option<String>> {
|
||||
if let Some(id) = args.resume.as_ref().or(args.session_id.as_ref()) {
|
||||
return Ok(Some(id.clone()));
|
||||
@@ -975,12 +979,12 @@ async fn main() -> Result<()> {
|
||||
};
|
||||
}
|
||||
|
||||
// One-shot prompt mode
|
||||
// Top-level prompt mode: submit the initial prompt, then keep the TUI alive
|
||||
// for follow-up messages. Use `codewhale exec` for explicit non-interactive
|
||||
// one-shot behavior (#2370).
|
||||
let config = load_config_from_cli(&cli)?;
|
||||
if !cli.prompt.is_empty() {
|
||||
let prompt = join_prompt_parts(&cli.prompt);
|
||||
let model = config.default_model();
|
||||
return run_one_shot(&config, &model, &prompt).await;
|
||||
if let Some(initial_input) = top_level_prompt_initial_input(&cli.prompt) {
|
||||
return run_interactive(&cli, &config, None, Some(initial_input)).await;
|
||||
}
|
||||
|
||||
// Handle session resume. Plain `codewhale` starts fresh: interrupted
|
||||
@@ -3783,7 +3787,13 @@ async fn run_pr(
|
||||
} else {
|
||||
cli.resume.clone()
|
||||
};
|
||||
run_interactive(cli, config, resume_session_id, Some(prompt)).await
|
||||
run_interactive(
|
||||
cli,
|
||||
config,
|
||||
resume_session_id,
|
||||
Some(tui::InitialInput::Prefill(prompt)),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Return true if `name` resolves to an executable on the current `PATH`.
|
||||
@@ -4795,7 +4805,7 @@ async fn run_interactive(
|
||||
cli: &Cli,
|
||||
config: &Config,
|
||||
resume_session_id: Option<String>,
|
||||
initial_input: Option<String>,
|
||||
initial_input: Option<tui::InitialInput>,
|
||||
) -> Result<()> {
|
||||
let workspace = cli
|
||||
.workspace
|
||||
@@ -5838,6 +5848,16 @@ mod terminal_mode_tests {
|
||||
assert_eq!(cli.prompt, vec!["hello", "world"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prompt_flag_starts_interactive_submit_input() {
|
||||
let cli = parse_cli(&["codewhale", "-p", "read", "the", "project"]);
|
||||
|
||||
assert_eq!(
|
||||
top_level_prompt_initial_input(&cli.prompt),
|
||||
Some(tui::InitialInput::Submit("read the project".to_string()))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn companion_binary_reports_its_own_name() {
|
||||
assert_eq!(Cli::command().get_name(), "codewhale-tui");
|
||||
|
||||
+61
-12
@@ -814,7 +814,19 @@ pub struct TuiOptions {
|
||||
/// Used by `deepseek pr <N>` (#451) to drop the model into a
|
||||
/// session with the PR context already typed — the user can edit
|
||||
/// before sending or hit Enter to fire as-is.
|
||||
pub initial_input: Option<String>,
|
||||
pub initial_input: Option<InitialInput>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum InitialInput {
|
||||
/// Pre-populate the composer and wait for the user to press Enter.
|
||||
///
|
||||
/// Used by `codewhale pr <N>` (#451) to drop the model into a session
|
||||
/// with the PR context already typed so the user can edit before sending.
|
||||
Prefill(String),
|
||||
/// Pre-populate the composer, submit it once startup is ready, then keep
|
||||
/// the interactive session open for follow-up messages (#2370).
|
||||
Submit(String),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
@@ -1444,6 +1456,8 @@ pub struct App {
|
||||
/// Most recent user prompt accepted for an active engine turn. Ctrl+C can
|
||||
/// restore this into an empty composer after cancelling that turn.
|
||||
pub last_submitted_prompt: Option<String>,
|
||||
/// Startup prompt should be submitted automatically after the engine is ready.
|
||||
pub auto_submit_initial_input: bool,
|
||||
/// Two-tap quit confirmation. When set, a prior Ctrl+C in idle state has
|
||||
/// armed the quit shortcut; a second Ctrl+C before this `Instant` exits
|
||||
/// the app, while expiry silently re-arms the prompt for next time.
|
||||
@@ -1803,17 +1817,22 @@ impl App {
|
||||
let cached_skills = Self::discover_cached_skills(&workspace, &skills_dir);
|
||||
|
||||
let input_history = crate::composer_history::load_history();
|
||||
let (initial_input_text, initial_input_cursor) = match initial_input {
|
||||
// #451: pre-populate the composer when invoked via
|
||||
// `deepseek pr <N>` (or any future caller that wants to
|
||||
// drop the model into a session with context already
|
||||
// typed). Cursor lands at the end so Enter sends as-is.
|
||||
Some(text) if !text.is_empty() => {
|
||||
let cursor = text.len();
|
||||
(text, cursor)
|
||||
}
|
||||
_ => (String::new(), 0),
|
||||
};
|
||||
let (initial_input_text, initial_input_cursor, auto_submit_initial_input) =
|
||||
match initial_input {
|
||||
// #451: pre-populate the composer when invoked via
|
||||
// `deepseek pr <N>` (or any future caller that wants to
|
||||
// drop the model into a session with context already
|
||||
// typed). Cursor lands at the end so Enter sends as-is.
|
||||
Some(InitialInput::Prefill(text)) if !text.is_empty() => {
|
||||
let cursor = text.len();
|
||||
(text, cursor, false)
|
||||
}
|
||||
Some(InitialInput::Submit(text)) if !text.is_empty() => {
|
||||
let cursor = text.len();
|
||||
(text, cursor, true)
|
||||
}
|
||||
_ => (String::new(), 0, false),
|
||||
};
|
||||
Self {
|
||||
mode: initial_mode,
|
||||
composer: ComposerState {
|
||||
@@ -2004,6 +2023,7 @@ impl App {
|
||||
coherence_state: CoherenceState::default(),
|
||||
last_send_at: None,
|
||||
last_submitted_prompt: None,
|
||||
auto_submit_initial_input,
|
||||
quit_armed_until: None,
|
||||
cycle_count: 0,
|
||||
cycle_briefings: Vec::new(),
|
||||
@@ -4855,6 +4875,35 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn initial_input_prefill_waits_for_manual_submit() {
|
||||
let mut options = test_options(false);
|
||||
options.initial_input = Some(InitialInput::Prefill("review this PR".to_string()));
|
||||
|
||||
let app = App::new(options, &Config::default());
|
||||
|
||||
assert_eq!(app.input, "review this PR");
|
||||
assert_eq!(app.cursor_position, "review this PR".chars().count());
|
||||
assert!(!app.auto_submit_initial_input);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn initial_input_submit_marks_startup_dispatch() {
|
||||
let mut options = test_options(false);
|
||||
options.initial_input = Some(InitialInput::Submit(
|
||||
"Read the project and wait for instructions".to_string(),
|
||||
));
|
||||
|
||||
let app = App::new(options, &Config::default());
|
||||
|
||||
assert_eq!(app.input, "Read the project and wait for instructions");
|
||||
assert_eq!(
|
||||
app.cursor_position,
|
||||
"Read the project and wait for instructions".chars().count()
|
||||
);
|
||||
assert!(app.auto_submit_initial_input);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn composer_arrows_scroll_default_is_true_without_mouse_capture() {
|
||||
assert!(default_composer_arrows_scroll_for_platform(false, false));
|
||||
|
||||
@@ -76,5 +76,5 @@ pub mod workspace_context;
|
||||
|
||||
// === Re-exports ===
|
||||
|
||||
pub use app::TuiOptions;
|
||||
pub use app::{InitialInput, TuiOptions};
|
||||
pub use ui::run_tui;
|
||||
|
||||
@@ -530,6 +530,19 @@ pub async fn run_tui(config: &Config, options: TuiOptions) -> Result<()> {
|
||||
persistence_actor::init_actor(handle);
|
||||
}
|
||||
|
||||
if app.auto_submit_initial_input {
|
||||
app.auto_submit_initial_input = false;
|
||||
if app.onboarding == OnboardingState::None {
|
||||
if let Some(input) = app.submit_input() {
|
||||
let queued = build_queued_message(&mut app, input);
|
||||
dispatch_user_message(&mut app, config, &engine_handle, queued).await?;
|
||||
}
|
||||
} else if app.status_message.is_none() && !app.input.trim().is_empty() {
|
||||
app.status_message =
|
||||
Some("Initial prompt ready; complete setup, then press Enter".to_string());
|
||||
}
|
||||
}
|
||||
|
||||
let result = run_event_loop(
|
||||
&mut terminal,
|
||||
&mut app,
|
||||
|
||||
Reference in New Issue
Block a user