diff --git a/CHANGELOG.md b/CHANGELOG.md index 82d47e04..a7edffdf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.59.1] - 2026-10-01 + ### Fixed - **`rm` no longer counts a commit and the commit that reverts it** (#664). Neither has a @@ -19,6 +21,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 and so does a second commit that grew from it, such as a branch or the stash. A merge, a root commit, an empty commit and a revert of a commit that another rule cleared still count, and so does an earlier draft of a commit that was edited later. +- **`aid ` goes straight to the prompt editor again** (#666). 0.59.0 asked for the + agent, the model and the effort on every promptless launch on a terminal. Only a line + with no workspace, such as a bare `aid` or `aid --codex`, is asked now. A line that + names its workspace starts the agent on its defaults and the line's flags, as 0.58.0 did. ## [0.59.0] - 2026-09-30 diff --git a/README.md b/README.md index 24abda66..6d4fc203 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ one argument instead of a clone, a config file and a build command. [![GitHub pull-requests merged](https://badgen.net/github/merged-prs/blooop/devlaunch)](https://github.com/blooop/devlaunch/pulls?q=is%3Amerged) [![GitHub release](https://img.shields.io/github/release/blooop/devlaunch.svg)](https://GitHub.com/blooop/devlaunch/releases/) [![PyPI](https://img.shields.io/pypi/v/devlaunch)](https://pypi.org/project/devlaunch/) -[![Conda](https://img.shields.io/badge/conda-v0.59.0-brightgreen?logo=anaconda)](https://prefix.dev/channels/blooop/packages/devlaunch) +[![Conda](https://img.shields.io/badge/conda-v0.59.1-brightgreen?logo=anaconda)](https://prefix.dev/channels/blooop/packages/devlaunch) [![License](https://img.shields.io/github/license/blooop/devlaunch)](https://opensource.org/license/mit/) [![Platform](https://img.shields.io/badge/platform-linux--64-blue)](https://github.com/blooop/devlaunch/releases) [![Pixi Badge](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/prefix-dev/pixi/main/assets/badge/v0.json)](https://pixi.sh) @@ -341,7 +341,7 @@ clone, and [docs/cleanup.md](docs/cleanup.md) says what it carries one past and ```bash $ dl --version -dl 0.59.0 +dl 0.59.1 ``` `--devcontainer ` picks a non-default `devcontainer.json`. A bare name means @@ -411,10 +411,11 @@ With no workspace on the line, `aid` on a terminal lets you pick one of your wor does. With no prompt on the line, `aid` starts the container booting and asks for the prompt while it does. Type it free of shell quoting, with no escaping and no history expansion eating a `!`. A paste keeps its line breaks and can be any length, Alt-Enter or Ctrl-J adds a line, and an empty -Enter starts the agent's plain session. Before the prompt it asks for the agent (one row per Claude -login, then `codex` and `gemini`), the model and the effort, each in a picker that lists your -recent choices first, so one Enter repeats the last launch. A flag on the line skips its picker, -and Esc stops the boot. See [docs/cli.md](docs/cli.md#the-pickers-ahead-of-the-prompt). Piping +Enter starts the agent's plain session. A bare `aid`, which picks its workspace, also asks before +the prompt for the agent (one row per Claude login, then `codex` and `gemini`), the model and the +effort, each in a picker that lists your recent choices first, so one Enter repeats the last +launch. A flag on the line skips its picker, and Esc stops the boot. `aid ` asks for +none of them and goes straight to the prompt. See [docs/cli.md](docs/cli.md#the-pickers-ahead-of-the-prompt). Piping stdin or setting `DEVLAUNCH_NO_TTY=1` skips the question and launches one-shot, so scripts behave as they always have. diff --git a/docs/cli.md b/docs/cli.md index b6ce99b2..a4ef87c0 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -759,8 +759,14 @@ Remote Control. A value that is missing, empty or starts with `-` is refused too ## The pickers ahead of the prompt -`aid ` with no prompt on a terminal asks for up to three settings before -it opens the prompt editor. The workspace boots in the background the whole time. +`aid` with no workspace and no prompt on a terminal asks for up to three settings +after the workspace picker and before it opens the prompt editor. The workspace +boots in the background the whole time. A line of flags with no workspace, such as +`aid --codex`, is asked the same way. + +`aid ` asks for none of them. A line that names its workspace said what +it wanted, so it goes straight to the prompt editor, and every setting its flags +left open stays at the agent's default. 1. **The agent**, with one row per Claude login and one row for each other agent: @@ -797,7 +803,7 @@ Where the name is part of a listed row, such as `gpt-5.5` beside `gpt-5.5-codex` Alt-Enter uses the text as typed instead of the row. A setting that a flag on the line already gave is not asked for, so -`aid --model opus --effort max ` goes straight to the prompt. A line with a +`aid --model opus --effort max` asks for the workspace and the agent only. A line with a prompt on it asks for nothing, as before. **Esc cancels the launch.** In a picker Esc and Ctrl-C are keys, not signals, so diff --git a/rust/Cargo.lock b/rust/Cargo.lock index e03578d1..fc05ab07 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -13,7 +13,7 @@ dependencies = [ [[package]] name = "aid" -version = "0.59.0" +version = "0.59.1" dependencies = [ "devlaunch-test-support", "dl", @@ -438,7 +438,7 @@ dependencies = [ [[package]] name = "devlaunch-core" -version = "0.59.0" +version = "0.59.1" dependencies = [ "devlaunch-runner", "devlaunch-test-support", @@ -457,7 +457,7 @@ dependencies = [ [[package]] name = "devlaunch-runner" -version = "0.59.0" +version = "0.59.1" dependencies = [ "libc", "portable-pty", @@ -466,7 +466,7 @@ dependencies = [ [[package]] name = "devlaunch-test-support" -version = "0.59.0" +version = "0.59.1" dependencies = [ "devlaunch-runner", "serde", @@ -508,7 +508,7 @@ dependencies = [ [[package]] name = "dl" -version = "0.59.0" +version = "0.59.1" dependencies = [ "clap", "devlaunch-core", diff --git a/rust/Cargo.toml b/rust/Cargo.toml index b8166321..be05a0a9 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -11,7 +11,7 @@ members = [ # The single source of the version (docs/rust-rewrite-plan.md: cutover ships # 0.1.0, version read from Cargo.toml). [workspace.package] -version = "0.59.0" +version = "0.59.1" edition = "2024" license = "MIT" repository = "https://github.com/blooop/devlaunch" diff --git a/rust/aid/src/interactive.rs b/rust/aid/src/interactive.rs index ac807701..583ea0b2 100644 --- a/rust/aid/src/interactive.rs +++ b/rust/aid/src/interactive.rs @@ -162,6 +162,17 @@ impl BootChild { } } +/// Whether the agent, model and effort pickers run before the editor. +/// +/// Only a bare `aid`, or a line of flags with no workspace, asks. A line that +/// names its workspace already said what it wanted, so it gets the editor alone +/// and every setting it left open stays the agent's default. +#[derive(Clone, Copy)] +pub(crate) enum Pickers { + Ask, + Skip, +} + /// How the interactive flow ended. pub(crate) enum Collected { /// Launch this line, once the boot beside it, if any, has finished. @@ -187,10 +198,12 @@ pub(crate) enum Collected { /// /// `argv` and `environment` are what `parsed` was parsed from: the agent picker /// builds its rows by parsing the line again with each row's flags in front. +/// `pickers` says whether those pickers run at all. pub(crate) fn collect_prompt( parsed: AidArgs, argv: &[String], environment: Environment<'_>, + pickers: Pickers, ) -> Collected { let promptless_agent = matches!( &parsed.task, @@ -216,9 +229,15 @@ pub(crate) fn collect_prompt( // The pickers come after the boot has started, so the minute they take is // also spent booting. Nothing they choose reaches the boot: `up` takes no // model, and `--claude-profile` is read when the session starts, not at `up`. - let Some(parsed) = settle(parsed, argv, environment) else { - boot.cancel(); - return Collected::Cancelled; + let parsed = match pickers { + Pickers::Skip => parsed, + Pickers::Ask => { + let Some(parsed) = settle(parsed, argv, environment) else { + boot.cancel(); + return Collected::Cancelled; + }; + parsed + } }; banner(&parsed); match dl::read_prompt() { diff --git a/rust/aid/src/main.rs b/rust/aid/src/main.rs index fc7ac574..a6fa7c22 100644 --- a/rust/aid/src/main.rs +++ b/rust/aid/src/main.rs @@ -150,14 +150,22 @@ fn run(argv: &[String]) -> i32 { // id is added to the end of the line, after the leading flags, which is where a // typed spec goes, and from here on the line is that longer one, because the // agent picker parses it again. + // + // Only that last pick, a workspace for a line that named none, earns the agent, + // model and effort pickers: a line that names its workspace goes straight to + // the editor. let with_pick: Vec; - let (argv, parsed) = match rewrite::parse_aid_args(argv, environment) { - Ok(rewrite::Line::Ready(parsed)) => (argv, parsed), + let (argv, parsed, pickers) = match rewrite::parse_aid_args(argv, environment) { + Ok(rewrite::Line::Ready(parsed)) => (argv, parsed, interactive::Pickers::Skip), // `aid resume` with no workspace. The pick comes before everything below, // which is all about one named workspace, so from here on this line is an // `aid resume ` like any other. Ok(rewrite::Line::Unpicked(unpicked)) => match dl::pick_workspace() { - Ok(workspace_id) => (argv, unpicked.picked(workspace_id)), + Ok(workspace_id) => ( + argv, + unpicked.picked(workspace_id), + interactive::Pickers::Skip, + ), Err(code) => return code, }, Err(UsageError::NoWorkspace) @@ -169,7 +177,9 @@ fn run(argv: &[String]) -> i32 { }; with_pick = argv.iter().cloned().chain([spec]).collect(); match rewrite::parse_aid_args(&with_pick, environment) { - Ok(rewrite::Line::Ready(parsed)) => (with_pick.as_slice(), parsed), + Ok(rewrite::Line::Ready(parsed)) => { + (with_pick.as_slice(), parsed, interactive::Pickers::Ask) + } // A line that names its workspace is never unpicked. Ok(rewrite::Line::Unpicked(_)) => { eprintln!("{}", refusal(&UsageError::NoWorkspace)); @@ -199,7 +209,7 @@ fn run(argv: &[String]) -> i32 { Ok(spec) => parsed.with_spec(spec), Err(code) => return code, }; - let (parsed, boot) = match interactive::collect_prompt(parsed, argv, environment) { + let (parsed, boot) = match interactive::collect_prompt(parsed, argv, environment, pickers) { interactive::Collected::Launch(parsed, boot) => (*parsed, boot), // 130, the code a Ctrl-C at the prompt editor ends with, because a cancel // in a picker is the same request made with a different key. @@ -408,14 +418,15 @@ resume words: claude and codex open their session picker, and gemini reopens its latest session. With no workspace on a terminal, aid lets you pick one of your workspaces, as -dl does. +dl does. Then it asks for each setting the line left open: the agent (one row +per Claude login, then each other agent), the model and the effort. Each +picker lists your recent choices first, so one Enter repeats the last launch. +Type a name that is not listed to use it. Alt-Enter uses the text as typed +where it is part of a listed name. A line that names its workspace asks for +none of these, and the agent starts on its defaults and the line's flags. With no prompt on a terminal, aid boots the workspace in the background and -asks for the prompt while it does. First it asks for each setting the line -left open: the agent (one row per Claude login, then each other agent), the -model and the effort. Each picker lists your recent choices first, so one -Enter repeats the last launch. Type a name that is not listed to use it. -Alt-Enter uses the text as typed where it is part of a listed name. +asks for the prompt while it does. Then type the prompt free of shell quoting and press Enter to launch. A paste keeps its line breaks, and Alt-Enter or Ctrl-J adds a line. An empty Enter diff --git a/rust/aid/tests/interactive.rs b/rust/aid/tests/interactive.rs index 0467d877..81509362 100644 --- a/rust/aid/tests/interactive.rs +++ b/rust/aid/tests/interactive.rs @@ -244,10 +244,23 @@ impl PtyAid { self.press("\r"); } + /// Wait for the prompt editor. A line that names its workspace reaches it with + /// no picker in the way. + fn reach_the_editor(&self) { + self.expect(BANNER); + } + + /// Take the first row of dl's workspace picker, which is how a line with no + /// workspace names one. In the scenario that row is [`MAIN`], and only a line + /// that took this way in is asked for the agent, the model and the effort. + fn pick_the_workspace(&mut self) { + self.answer(WORKSPACE_PICKER, "\r"); + } + /// Take the first row of the agent, model and effort pickers, then wait for - /// the prompt editor. On a first run that is claude with every default, which - /// is every launch's way in when nothing is chosen. - fn reach_the_editor(&mut self) { + /// the prompt editor. On a first run that is claude with every default. Only a + /// line with no workspace meets these pickers. + fn take_the_defaults(&mut self) { self.answer(AGENT_PICKER, "\r"); self.answer(MODEL_PICKER, "\r"); self.answer(EFFORT_PICKER, "\r"); @@ -431,6 +444,53 @@ fn a_typed_prompt_reaches_the_agent_with_no_shell_in_the_way() { ); } +#[test] +fn a_named_workspace_goes_straight_to_the_editor_with_no_picker() { + // The pickers belong to a bare `aid`. A line that names its workspace said + // what it wanted, so it gets the editor alone and the agent's defaults. + let world = World::with(&["--warm"]); + let mut session = PtyAid::spawn(&world, &[MAIN], &[]); + session.expect(BANNER); + for picker in [AGENT_PICKER, MODEL_PICKER, EFFORT_PICKER] { + assert!( + !session.text().contains(picker), + "{picker:?} was drawn for a named workspace; the pty said:\n{}", + session.text() + ); + } + session.send_line("go"); + assert_eq!(session.wait(), 0); + assert_eq!( + &without_session_id(world.devpod_calls().last().expect("a session")), + &claude_session("", "go") + ); +} + +#[test] +fn a_choice_a_bare_aid_remembered_stays_out_of_aid_workspace() { + // The cache remembers what a bare `aid` chose, but only to put it first in + // the next picker. A line that names its workspace draws no picker, so it + // gets the agent's defaults, not the last choice. + let world = World::with(&["--warm"]); + let mut first = PtyAid::spawn(&world, &[], &[]); + first.pick_the_workspace(); + first.answer(AGENT_PICKER, "\r"); + first.answer_typed(MODEL_PICKER, "sonnet"); + first.answer_typed(EFFORT_PICKER, "low"); + first.expect(BANNER); + first.send_line("one"); + assert_eq!(first.wait(), 0); + + let mut second = PtyAid::spawn(&world, &[MAIN], &[]); + second.reach_the_editor(); + second.send_line("two"); + assert_eq!(second.wait(), 0); + assert_eq!( + &without_session_id(world.devpod_calls().last().expect("a session")), + &claude_session("", "two") + ); +} + #[test] fn a_lone_esc_at_the_editor_does_not_swallow_the_next_key() { // The pickers teach that Esc cancels, so a person presses it here too. On its @@ -727,7 +787,8 @@ fn a_model_that_is_not_listed_is_typed_and_reaches_the_agent() { // Nothing lists `claude-opus-5-5`, so the query matches no row and Enter takes // the query itself. The effort is picked from the list by filtering to it. let world = World::with(&["--warm"]); - let mut session = PtyAid::spawn(&world, &[MAIN], &[]); + let mut session = PtyAid::spawn(&world, &[], &[]); + session.pick_the_workspace(); session.answer(AGENT_PICKER, "\r"); session.answer_typed(MODEL_PICKER, "claude-opus-5-5"); session.answer_typed(EFFORT_PICKER, "max"); @@ -746,7 +807,8 @@ fn the_last_choice_is_the_first_row_of_the_next_launch() { // One world, two launches: the second takes the first row of each picker and // gets what the first launch chose, because the cache remembered it. let world = World::with(&["--warm"]); - let mut first = PtyAid::spawn(&world, &[MAIN], &[]); + let mut first = PtyAid::spawn(&world, &[], &[]); + first.pick_the_workspace(); first.answer(AGENT_PICKER, "\r"); first.answer_typed(MODEL_PICKER, "sonnet"); first.answer_typed(EFFORT_PICKER, "low"); @@ -754,8 +816,9 @@ fn the_last_choice_is_the_first_row_of_the_next_launch() { first.send_line("one"); assert_eq!(first.wait(), 0); - let mut second = PtyAid::spawn(&world, &[MAIN], &[]); - second.reach_the_editor(); + let mut second = PtyAid::spawn(&world, &[], &[]); + second.pick_the_workspace(); + second.take_the_defaults(); second.send_line("two"); assert_eq!(second.wait(), 0); assert_eq!( @@ -767,7 +830,8 @@ fn the_last_choice_is_the_first_row_of_the_next_launch() { #[test] fn a_setting_a_flag_gave_is_not_asked_for() { let world = World::with(&["--warm"]); - let mut session = PtyAid::spawn(&world, &["--model", "opus", MAIN], &[]); + let mut session = PtyAid::spawn(&world, &["--model", "opus"], &[]); + session.pick_the_workspace(); session.answer(AGENT_PICKER, "\r"); session.answer(EFFORT_PICKER, "\r"); session.expect(BANNER); @@ -789,7 +853,8 @@ fn with_no_terminal_type_the_pickers_are_drawn_as_the_fallback_terminal() { // set. The pickers take the workspace picker's fallback and say so, and each // one is still answered with Enter. let world = World::with(&["--warm"]); - let mut session = PtyAid::spawn(&world, &[MAIN], &[("TERM", "")]); + let mut session = PtyAid::spawn(&world, &[], &[("TERM", "")]); + session.pick_the_workspace(); session.answer(AGENT_PICKER, "\r"); session.answer(MODEL_PICKER, "\r"); session.answer(EFFORT_PICKER, "\r"); @@ -818,12 +883,13 @@ fn a_named_claude_login_is_a_row_of_the_agent_picker_and_reaches_dl() { std::fs::write(profiles.join("work/.credentials.json"), "{}").expect("a credential"); let mut session = PtyAid::spawn( &world, - &[MAIN], + &[], &[( "DEVLAUNCH_CLAUDE_PROFILES_DIR", &profiles.display().to_string(), )], ); + session.pick_the_workspace(); session.answer_typed(AGENT_PICKER, "work"); session.answer(MODEL_PICKER, "\r"); session.answer(EFFORT_PICKER, "\r"); @@ -839,7 +905,8 @@ fn another_agent_is_a_row_of_the_same_picker() { // codex is chosen by name, and its own pickers follow. Remote Control was only // claude's default, so codex starts without it and nothing refuses. let world = World::with(&["--warm"]); - let mut session = PtyAid::spawn(&world, &[MAIN], &[]); + let mut session = PtyAid::spawn(&world, &[], &[]); + session.pick_the_workspace(); session.answer_typed(AGENT_PICKER, "codex"); session.answer(MODEL_PICKER.replace("claude", "codex").as_str(), "\r"); session.answer_typed(EFFORT_PICKER.replace("claude", "codex").as_str(), "high"); @@ -863,14 +930,16 @@ fn alt_enter_takes_a_model_that_is_a_prefix_of_a_listed_one() { let world = World::with(&["--warm"]); let codex_model = MODEL_PICKER.replace("claude", "codex"); let codex_effort = EFFORT_PICKER.replace("claude", "codex"); - let mut first = PtyAid::spawn(&world, &["--codex", MAIN], &[]); + let mut first = PtyAid::spawn(&world, &["--codex"], &[]); + first.pick_the_workspace(); first.answer_typed(&codex_model, "gpt-5.5-codex"); first.answer(&codex_effort, "\r"); first.expect(BANNER); first.send_line("one"); assert_eq!(first.wait(), 0); - let mut second = PtyAid::spawn(&world, &["--codex", MAIN], &[]); + let mut second = PtyAid::spawn(&world, &["--codex"], &[]); + second.pick_the_workspace(); second.answer(&codex_model, "gpt-5.5"); std::thread::sleep(Duration::from_millis(400)); second.press("\x1b\r"); @@ -886,7 +955,8 @@ fn alt_enter_takes_a_model_that_is_a_prefix_of_a_listed_one() { #[test] fn a_typed_agent_with_one_login_is_not_asked_which_agent() { let world = World::with(&["--warm"]); - let mut session = PtyAid::spawn(&world, &["--claude", MAIN], &[]); + let mut session = PtyAid::spawn(&world, &["--claude"], &[]); + session.pick_the_workspace(); session.answer(MODEL_PICKER, "\r"); session.answer(EFFORT_PICKER, "\r"); session.expect(BANNER); @@ -897,10 +967,12 @@ fn a_typed_agent_with_one_login_is_not_asked_which_agent() { #[test] fn esc_in_a_picker_stops_the_boot_and_launches_nothing() { - // The same world as the Ctrl-C test above: an `up` that blocks with the token - // staged. In a picker Ctrl-C and Esc are keys, not signals, so nothing reaches - // the boot unless aid sends it. The cleanup is the proof that it did. - let world = World::with(&["--gh"]); + // The same blocking `up` as the Ctrl-C test above, with the token staged. In a + // picker Ctrl-C and Esc are keys, not signals, so nothing reaches the boot + // unless aid sends it. The cleanup is the proof that it did. Only a line with + // no workspace gets the pickers, so the workspace is the stopped one the + // workspace picker lists, which the boot has to `up`. + let world = World::with(&["--gh", "--stopped"]); let devpod = world.root.join("bin/devpod"); let original = std::fs::read_to_string(&devpod).expect("the scenario's devpod"); let delegate = original @@ -927,13 +999,14 @@ fn esc_in_a_picker_stops_the_boot_and_launches_nothing() { let mut session = PtyAid::spawn( &world, - &["blooop/devlaunch@cold"], + &[], &[ ("TMPDIR", &tmpdir.display().to_string()), ("DL_UP_PID", &up_pid.display().to_string()), ("DL_UP_STARTED", &up_started.display().to_string()), ], ); + session.pick_the_workspace(); session.answer(AGENT_PICKER, "\r"); session.answer(MODEL_PICKER, ""); assert!( @@ -990,7 +1063,8 @@ fn the_agent_chosen_last_is_the_first_row_of_the_next_launch() { // The first run lists claude first, so an Enter on the second launch's agent // picker takes codex only if the first launch's choice moved it up. let world = World::with(&["--warm"]); - let mut first = PtyAid::spawn(&world, &[MAIN], &[]); + let mut first = PtyAid::spawn(&world, &[], &[]); + first.pick_the_workspace(); first.answer_typed(AGENT_PICKER, "codex"); first.answer("Model for codex", "\r"); first.answer("Effort for codex", "\r"); @@ -998,7 +1072,8 @@ fn the_agent_chosen_last_is_the_first_row_of_the_next_launch() { first.send_line("one"); assert_eq!(first.wait(), 0); - let mut second = PtyAid::spawn(&world, &[MAIN], &[]); + let mut second = PtyAid::spawn(&world, &[], &[]); + second.pick_the_workspace(); second.answer(AGENT_PICKER, "\r"); // Waited for by the words every agent's model picker shares, so a claude row // fails here at once rather than at the deadline. @@ -1022,7 +1097,8 @@ fn the_agent_chosen_last_is_the_first_row_of_the_next_launch() { #[test] fn a_typed_agent_that_is_no_row_is_asked_for_again() { let world = World::with(&["--warm"]); - let mut session = PtyAid::spawn(&world, &[MAIN], &[]); + let mut session = PtyAid::spawn(&world, &[], &[]); + session.pick_the_workspace(); session.answer_typed(AGENT_PICKER, "nonesuch"); // `answer` counts the header, so it waits for the picker drawn a second time. session.answer(AGENT_PICKER, "\x1b"); @@ -1044,7 +1120,8 @@ fn a_typed_agent_that_is_no_row_is_asked_for_again() { #[test] fn esc_at_the_agent_picker_launches_nothing() { let world = World::with(&["--warm"]); - let mut session = PtyAid::spawn(&world, &[MAIN], &[]); + let mut session = PtyAid::spawn(&world, &[], &[]); + session.pick_the_workspace(); session.answer(AGENT_PICKER, "\x1b"); session.expect("cancelled"); let seen = Arc::clone(&session.seen); @@ -1065,15 +1142,17 @@ fn esc_at_the_agent_picker_launches_nothing() { fn a_setting_a_flag_gave_is_the_first_row_of_the_next_launch() { // Remembered as a choice although no picker asked for it. let world = World::with(&["--warm"]); - let mut first = PtyAid::spawn(&world, &["--model", "opus", MAIN], &[]); + let mut first = PtyAid::spawn(&world, &["--model", "opus"], &[]); + first.pick_the_workspace(); first.answer(AGENT_PICKER, "\r"); first.answer(EFFORT_PICKER, "\r"); first.expect(BANNER); first.send_line("one"); assert_eq!(first.wait(), 0); - let mut second = PtyAid::spawn(&world, &[MAIN], &[]); - second.reach_the_editor(); + let mut second = PtyAid::spawn(&world, &[], &[]); + second.pick_the_workspace(); + second.take_the_defaults(); second.send_line("two"); assert_eq!(second.wait(), 0); assert_eq!( @@ -1186,7 +1265,7 @@ fn a_bare_aid_picks_a_workspace_as_a_bare_dl_does() { let mut session = PtyAid::spawn(&world, &[], &[]); session.answer(WORKSPACE_PICKER, "\r"); session.expect(&format!("-> {MAIN}")); - session.reach_the_editor(); + session.take_the_defaults(); session.send_line("go"); assert_eq!(session.wait(), 0); assert_eq!( diff --git a/test/e2e/test_interactive_session.py b/test/e2e/test_interactive_session.py index b34cc7c1..6dae1b51 100644 --- a/test/e2e/test_interactive_session.py +++ b/test/e2e/test_interactive_session.py @@ -33,7 +33,6 @@ import re import shlex import subprocess -import time from dataclasses import dataclass from pathlib import Path from typing import Dict, Iterator @@ -392,27 +391,13 @@ def test_aid_leaves_an_interactive_agent_running(self, workspace): session = workspace.aid() with session: - # On a terminal a promptless `aid` asks for the agent (one row per - # Claude login, then the other agents), the model and the effort, then - # the prompt, while the workspace boots. The agent picker is filtered - # to claude, since a recent choice could have put another agent first; - # Enter takes the first row of the rest. `\r`, not a newline: skim - # holds the terminal in raw mode, where a newline is Ctrl-J and moves - # the cursor down a row. With TERM=dumb there are no pickers. - session.expect(r"Agent for this launch|press Enter") - if "Agent for this launch" in session.text: - time.sleep(0.2) - session.send("claude", newline=False) - time.sleep(0.5) - session.send("\r", newline=False) - for picker in ("Model for claude", "Effort for claude"): - session.expect(picker) - time.sleep(0.2) - session.send("\r", newline=False) - # An empty Enter is the plain session this test has always been about. + # `aid ` names its workspace, so it asks for no agent, model + # or effort and goes straight to the prompt editor with the agent's + # defaults, while the workspace boots. An empty Enter is the plain + # session this test has always been about. session.expect(r"press Enter") - # The prompt editor is raw mode too: Enter is `\r`, and a newline - # would add a line to the prompt instead of submitting it. + # The prompt editor holds the terminal in raw mode: Enter is `\r`, and + # a newline (Ctrl-J) would add a line to the prompt instead of submitting. session.send("\r", newline=False) # Claude Code prints its banner once the TUI is up; without a # terminal it exits before ever getting there.