diff --git a/CHANGELOG.md b/CHANGELOG.md index 8ac17698..72f7f9eb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased ### Changed -- The TUI runs **inline** by default (`alternate_screen` never). It does not enter the alternate screen on start; the real shell prompt stays in scrollback above the app. Opt in with `cortex --alternate-screen` or `[tui] alternate_screen = true`. +- The TUI enters the **alternate screen** by default (`alternate_screen` always). Interactive launch takes the full viewport. Opt out with `cortex --no-alternate-screen` or `[tui] alternate_screen = false` to stay inline. - Empty-session splash is `Welcome to Cortex, the coding agent CLI` plus `v{package version} · / commands · @ files · ! shell · & cloud`. After the first user turn the splash is dropped (composer + footer only). No mascot, no painted `> cortex` shell lines. - Composer lock: empty is `> ` + white block at input col 0 + dim `Plan, search, build anything` after that cell (never a white rect after the placeholder). Blink-off (~530ms) hides the block so the placeholder starts at col 0. Typed copy is `#F5F5F5` with the block at the caret. diff --git a/docs/configuration/config.md b/docs/configuration/config.md index 104eb072..c85e84eb 100644 --- a/docs/configuration/config.md +++ b/docs/configuration/config.md @@ -150,9 +150,9 @@ max_bytes = 10000000 [tui] animations = true notifications = true -# Stay inline in the host terminal (default / never). Set true only to take -# over the alternate screen buffer. Equivalent CLI flag: --alternate-screen -alternate_screen = false +# Enter the alternate screen buffer (default / always). Set false to stay +# inline in the host terminal. Equivalent CLI flag: --no-alternate-screen +alternate_screen = true [tui.theme] name = "dark" # dark, light, ocean_dark, monokai diff --git a/docs/guides/tui.md b/docs/guides/tui.md index e913beb4..961ef887 100644 --- a/docs/guides/tui.md +++ b/docs/guides/tui.md @@ -16,26 +16,26 @@ cortex --profile work # load a profile from config.toml The TUI needs a terminal on both stdin and stdout. If either is redirected it refuses to start and points you at [`cortex run` or `cortex exec`](exec.md). -The session runs **inline** in the host terminal (`alternate_screen` is -never on by default). Cortex does not enter the alternate screen buffer -on start, so your shell prompt and the typed command stay visible above -the app. The welcome splash is two lines: +The session enters the **alternate screen** on start (`alternate_screen` +is always on by default) so Cortex takes the full viewport. The welcome +splash is two lines: `Welcome to **Cortex**, the coding agent CLI` then `v{version} · / commands · @ files · ! shell · & cloud`. It does not paint a fake shell prompt or working directory. After the first user turn the splash is dropped; an empty session is composer and footer only. -To opt in to a full-screen alternate buffer: +To stay inline in the host terminal (shell prompt remains in scrollback +above the app): ```bash -cortex --alternate-screen +cortex --no-alternate-screen ``` or in `~/.cortex/config.toml`: ```toml [tui] -alternate_screen = true +alternate_screen = false ``` ## The session view diff --git a/docs/media/tui-lock/README.md b/docs/media/tui-lock/README.md index 723e6506..ebb4f088 100644 --- a/docs/media/tui-lock/README.md +++ b/docs/media/tui-lock/README.md @@ -39,7 +39,9 @@ word-level colour on a changed line. Splash copy in these frames is `Welcome to Cortex, the coding agent CLI` plus `v{CARGO_PKG_VERSION} · / commands · …`. After the first user turn, -`session_empty` is composer and footer only. +`session_empty` is composer and footer only. Captures are full-viewport +(matching the default alternate-screen launch); they do not paint a fake +`> cortex` prompt or cwd line. | File | Surface | |------|---------| diff --git a/docs/reference/cli.md b/docs/reference/cli.md index 96840b20..5ae56c6f 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -32,7 +32,8 @@ A positional prompt seeds that session. Without a terminal, use | `--add-dir ` | Extra writable directory; repeatable | | `-i`, `--image ` | Attach an image to the initial prompt | | `--search` | Enable web search | -| `--alternate-screen` | Opt in to the alternate screen buffer. Default is inline (`never`): the host shell prompt stays visible above the app. Same as `[tui] alternate_screen = true`. | +| `--alternate-screen` | Enter the alternate screen buffer. Default is always (`true`): Cortex takes the full viewport. Same as `[tui] alternate_screen = true`. | +| `--no-alternate-screen` | Stay inline in the host terminal. Same as `[tui] alternate_screen = false`. | | `--max-agent-threads ` | Concurrent agent threads | | `--max-tool-threads ` | Concurrent tool executions | | `--command-timeout ` | Shell command timeout | diff --git a/src/cortex-cli/src/cli/args.rs b/src/cortex-cli/src/cli/args.rs index ee9a7b72..219c81d2 100644 --- a/src/cortex-cli/src/cli/args.rs +++ b/src/cortex-cli/src/cli/args.rs @@ -208,16 +208,27 @@ pub struct InteractiveArgs { #[arg(long = "search", default_value_t = false, help_heading = "Features")] pub web_search: bool, - /// Opt in to the alternate screen buffer. Default is **never** (inline): - /// the host shell prompt stays visible above the app. Same as - /// `[tui] alternate_screen = true`. + /// Enter the alternate screen buffer. Default is **always** (full + /// viewport). Same as `[tui] alternate_screen = true`. Use + /// `--no-alternate-screen` to stay inline. #[arg( long = "alternate-screen", default_value_t = false, + conflicts_with = "no_alternate_screen", help_heading = "Features" )] pub alternate_screen: bool, + /// Stay inline in the host terminal (never alternate screen). Same as + /// `[tui] alternate_screen = false`. + #[arg( + long = "no-alternate-screen", + default_value_t = false, + conflicts_with = "alternate_screen", + help_heading = "Features" + )] + pub no_alternate_screen: bool, + /// Maximum number of concurrent agent threads #[arg( long = "max-agent-threads", @@ -974,6 +985,8 @@ mod tests { assert!(args.add_dir.is_empty()); assert!(args.images.is_empty()); assert!(!args.web_search); + assert!(!args.alternate_screen); + assert!(!args.no_alternate_screen); assert_eq!(args.log_level, LogLevel::Info); assert!(!args.debug); assert!(args.prompt.is_empty()); @@ -1194,15 +1207,39 @@ mod tests { } #[test] - fn test_cli_alternate_screen_flag_defaults_off() { + fn test_cli_alternate_screen_flag_defaults_unset() { let cli = Cli::try_parse_from(["cortex"]).expect("should parse"); assert!( !cli.interactive.alternate_screen, - "the flag is opt-in; default TUI is inline (never alt-screen)" + "the flag is unset unless passed; config default is always" + ); + assert!( + !cli.interactive.no_alternate_screen, + "inline opt-out is unset unless --no-alternate-screen is passed" ); let cli = Cli::try_parse_from(["cortex", "--alternate-screen"]) .expect("should parse --alternate-screen"); assert!(cli.interactive.alternate_screen); + assert!(!cli.interactive.no_alternate_screen); + let cli = Cli::try_parse_from(["cortex", "--no-alternate-screen"]) + .expect("should parse --no-alternate-screen"); + assert!(cli.interactive.no_alternate_screen); + assert!(!cli.interactive.alternate_screen); + } + + #[test] + fn test_cli_alternate_screen_conflicts_with_no_alternate_screen() { + let result = Cli::try_parse_from(["cortex", "--alternate-screen", "--no-alternate-screen"]); + match result { + Err(err) => { + let rendered = err.to_string(); + assert!( + rendered.contains("cannot be used with") || rendered.contains("conflict"), + "unexpected clap error: {rendered}" + ); + } + Ok(_) => panic!("--alternate-screen and --no-alternate-screen must conflict"), + } } #[test] diff --git a/src/cortex-cli/src/cli/handlers.rs b/src/cortex-cli/src/cli/handlers.rs index 5860af9d..4ecd15ec 100644 --- a/src/cortex-cli/src/cli/handlers.rs +++ b/src/cortex-cli/src/cli/handlers.rs @@ -109,12 +109,20 @@ async fn run_tui(args: InteractiveArgs) -> Result<()> { .as_ref() .map(|m| resolve_model_alias(m).to_string()), cwd: args.cwd.clone(), - alternate_screen: args.alternate_screen.then_some(true), + alternate_screen: if args.no_alternate_screen { + Some(false) + } else if args.alternate_screen { + Some(true) + } else { + None + }, ..Default::default() }) .unwrap_or_else(|_| cortex_engine::Config::default()); - if args.alternate_screen { + if args.no_alternate_screen { + config.alternate_screen = false; + } else if args.alternate_screen { config.alternate_screen = true; } diff --git a/src/cortex-engine/src/config/mod.rs b/src/cortex-engine/src/config/mod.rs index 199f8502..ba5ebb9a 100644 --- a/src/cortex-engine/src/config/mod.rs +++ b/src/cortex-engine/src/config/mod.rs @@ -79,7 +79,7 @@ pub struct Config { pub disable_paste_burst: bool, /// Enable TUI animations. pub animations: bool, - /// Opt in to the alternate screen buffer. Default is inline (`never`). + /// Enter the alternate screen buffer. Default is always (`true`). pub alternate_screen: bool, /// Current agent profile. pub current_agent: Option, @@ -121,7 +121,7 @@ impl Default for Config { check_for_update_on_startup: true, disable_paste_burst: false, animations: true, - alternate_screen: false, + alternate_screen: true, current_agent: None, permission: PermissionConfig::default(), small_model: None, // Auto-detected based on available providers @@ -246,7 +246,7 @@ impl Config { toml.tui .as_ref() .map(|t| t.alternate_screen) - .unwrap_or(false) + .unwrap_or_else(|| TuiConfig::default().alternate_screen) }), current_agent: toml.current_agent, permission: toml.permission, @@ -320,7 +320,8 @@ pub struct ConfigOverrides { pub additional_writable_roots: Vec, /// Temperature override from CLI (0.0-2.0). pub temperature: Option, - /// When `Some(true)`, enter the alternate screen. Default (None/false) is inline. + /// When `Some`, force alternate screen on (`true`) or inline (`false`). + /// `None` keeps the config default (always). pub alternate_screen: Option, } @@ -330,24 +331,24 @@ mod alternate_screen_tests { use types::ConfigToml; #[test] - fn from_toml_keeps_alternate_screen_off_by_default() { + fn from_toml_keeps_alternate_screen_on_by_default() { assert!( - !Config::default().alternate_screen, - "default must be inline (never alt-screen)" + Config::default().alternate_screen, + "default must enter the alternate screen (always)" ); let toml: ConfigToml = toml::from_str("").unwrap(); let cfg = Config::from_toml(toml, ConfigOverrides::default(), PathBuf::from("/tmp")); assert!( - !cfg.alternate_screen, - "default must be inline (never alt-screen)" + cfg.alternate_screen, + "default must enter the alternate screen (always)" ); } #[test] - fn from_toml_honors_tui_alternate_screen_opt_in() { - let toml: ConfigToml = toml::from_str("[tui]\nalternate_screen = true\n").unwrap(); + fn from_toml_honors_tui_alternate_screen_opt_out() { + let toml: ConfigToml = toml::from_str("[tui]\nalternate_screen = false\n").unwrap(); let cfg = Config::from_toml(toml, ConfigOverrides::default(), PathBuf::from("/tmp")); - assert!(cfg.alternate_screen); + assert!(!cfg.alternate_screen); } #[test] @@ -363,4 +364,18 @@ mod alternate_screen_tests { ); assert!(cfg.alternate_screen); } + + #[test] + fn cli_override_can_force_inline() { + let toml: ConfigToml = toml::from_str("[tui]\nalternate_screen = true\n").unwrap(); + let cfg = Config::from_toml( + toml, + ConfigOverrides { + alternate_screen: Some(false), + ..Default::default() + }, + PathBuf::from("/tmp"), + ); + assert!(!cfg.alternate_screen); + } } diff --git a/src/cortex-engine/src/config/types.rs b/src/cortex-engine/src/config/types.rs index 5b24b6d9..107a8455 100644 --- a/src/cortex-engine/src/config/types.rs +++ b/src/cortex-engine/src/config/types.rs @@ -327,8 +327,8 @@ pub enum ReasoningSummary { pub struct TuiConfig { #[serde(default = "default_animations")] pub animations: bool, - /// Default is **never** (inline). The host shell prompt stays visible - /// above the app. Set `true` only to take over the alternate screen. + /// Default is **always** (full viewport). Set `false` to stay inline + /// so the host shell prompt remains visible above the app. #[serde(default = "default_alternate_screen")] pub alternate_screen: bool, #[serde(default)] @@ -341,7 +341,7 @@ impl Default for TuiConfig { fn default() -> Self { Self { animations: true, - alternate_screen: false, + alternate_screen: true, notifications: NotificationsConfig::default(), theme: ThemeConfig::default(), } @@ -353,7 +353,7 @@ fn default_animations() -> bool { } fn default_alternate_screen() -> bool { - false + true } /// Theme configuration. @@ -488,20 +488,20 @@ mod tui_alternate_screen_tests { use super::*; #[test] - fn tui_alternate_screen_defaults_never() { + fn tui_alternate_screen_defaults_always() { assert!( - !TuiConfig::default().alternate_screen, - "default must be inline (never alt-screen)" + TuiConfig::default().alternate_screen, + "default must enter the alternate screen (always)" ); let parsed: ConfigToml = toml::from_str("").expect("empty config"); assert!(parsed.tui.is_none()); let parsed: ConfigToml = toml::from_str("[tui]\n").expect("empty tui table"); - assert!(!parsed.tui.expect("tui").alternate_screen); + assert!(parsed.tui.expect("tui").alternate_screen); let parsed: ConfigToml = - toml::from_str("[tui]\nalternate_screen = true\n").expect("opt-in"); - assert!(parsed.tui.expect("tui").alternate_screen); + toml::from_str("[tui]\nalternate_screen = false\n").expect("opt-out"); + assert!(!parsed.tui.expect("tui").alternate_screen); } } diff --git a/src/cortex-tui/src/lock_proof.rs b/src/cortex-tui/src/lock_proof.rs index c9a6fa41..3f8ce16e 100644 --- a/src/cortex-tui/src/lock_proof.rs +++ b/src/cortex-tui/src/lock_proof.rs @@ -2,7 +2,8 @@ //! //! Renders the real session, login, palette, and settings widgets through //! [`cortex_tui_capture::MockTerminal`] and writes ANSI frames a rasteriser -//! turns into PNGs. +//! turns into PNGs. Frames are full-viewport (40×12 and 120×40), matching +//! the default alternate-screen launch — no fake `> cortex` / cwd chrome. use std::path::{Path, PathBuf}; @@ -2297,6 +2298,11 @@ mod tests { ); } assert!(!frame.plain.contains("▄█▀▀▀▀█▄"), "{}", frame.plain); + assert!( + !frame.plain.contains("> cortex"), + "splash must not paint a fake shell prompt at {size:?}:\n{}", + frame.plain + ); assert_no_junk(&frame.plain); } let wide = render_lock_scene("splash", 120, 40).expect("splash wide"); diff --git a/src/cortex-tui/src/runner/app_runner/runner.rs b/src/cortex-tui/src/runner/app_runner/runner.rs index e9a2111e..49506186 100644 --- a/src/cortex-tui/src/runner/app_runner/runner.rs +++ b/src/cortex-tui/src/runner/app_runner/runner.rs @@ -79,9 +79,9 @@ impl AppRunner { /// Create a new app runner with the given configuration. /// - /// The TUI starts **inline** (never alternate screen) so the host shell - /// prompt stays visible above the app. Set `tui.alternate_screen = true` - /// to opt in to the alternate screen buffer. + /// The TUI starts on the **alternate screen** (always) so the session + /// takes the full viewport. Set `tui.alternate_screen = false` or pass + /// `--no-alternate-screen` to stay inline. /// /// # Arguments /// @@ -96,8 +96,6 @@ impl AppRunner { pub fn new(config: Config) -> Self { let terminal_options = if config.alternate_screen { TerminalOptions::default() - .alternate_screen(true) - .clear_on_start(true) } else { TerminalOptions::inline() }; @@ -1054,20 +1052,20 @@ mod tests { fn test_app_runner_terminal_options() { let config = Config::default(); - // Default: inline (never alternate screen) + // Default: alternate screen (always) let runner = AppRunner::new(config.clone()); assert!( - !runner.terminal_options.alternate_screen, - "default must be inline (never alt-screen)" + runner.terminal_options.alternate_screen, + "default must enter the alternate screen (always)" ); - assert!(!runner.terminal_options.clear_on_start); - - let mut fullscreen = config.clone(); - fullscreen.alternate_screen = true; - let runner = AppRunner::new(fullscreen); - assert!(runner.terminal_options.alternate_screen); assert!(runner.terminal_options.clear_on_start); + let mut inline = config.clone(); + inline.alternate_screen = false; + let runner = AppRunner::new(inline); + assert!(!runner.terminal_options.alternate_screen); + assert!(!runner.terminal_options.clear_on_start); + // Custom options let custom_options = TerminalOptions::new() .alternate_screen(false) diff --git a/src/cortex-tui/src/runner/terminal.rs b/src/cortex-tui/src/runner/terminal.rs index 5edb6626..6cd6a230 100644 --- a/src/cortex-tui/src/runner/terminal.rs +++ b/src/cortex-tui/src/runner/terminal.rs @@ -9,13 +9,13 @@ //! ```rust,ignore //! use cortex_tui::runner::terminal::{CortexTerminal, TerminalOptions}; //! -//! // Create with default options (inline, no alternate screen) +//! // Create with default options (alternate screen, full viewport) //! let mut terminal = CortexTerminal::new()?; //! //! // Or with custom options //! let mut terminal = CortexTerminal::with_options( //! TerminalOptions::new() -//! .alternate_screen(true) +//! .alternate_screen(false) //! .mouse_capture(false) //! .title("My App") //! )?; @@ -116,7 +116,7 @@ impl Drop for TerminalGuard { /// Configuration options for terminal initialization. /// /// This struct uses the builder pattern to allow flexible configuration -/// of terminal features. Alternate screen is **off** by default (inline). +/// of terminal features. Alternate screen is **on** by default (always). /// /// # Example /// @@ -146,13 +146,13 @@ pub struct TerminalOptions { impl Default for TerminalOptions { fn default() -> Self { - // Interactive start is inline: never enter the alternate screen. + // Interactive start takes the full viewport (always alternate screen). Self { - alternate_screen: false, + alternate_screen: true, mouse_capture: true, bracketed_paste: true, title: Some("Cortex".to_string()), - clear_on_start: false, + clear_on_start: true, } } } @@ -160,12 +160,12 @@ impl Default for TerminalOptions { impl TerminalOptions { /// Create a new `TerminalOptions` with default settings. /// - /// Default settings stay **inline** (never alternate screen): - /// - Alternate screen: off (set `[tui] alternate_screen = true` to opt in) + /// Default settings enter the **alternate screen** (always): + /// - Alternate screen: on (set `[tui] alternate_screen = false` to stay inline) /// - Mouse capture: enabled /// - Bracketed paste: enabled /// - Title: "Cortex" - /// - Clear on start: off + /// - Clear on start: on pub fn new() -> Self { Self::default() } @@ -265,10 +265,10 @@ pub struct CortexTerminal { } impl CortexTerminal { - /// Create a new terminal with default **inline** mode (no alternate screen). + /// Create a new terminal with default **alternate screen** mode. /// /// This initializes the terminal with: - /// - Inline buffer (host shell prompt stays visible above the app) + /// - Alternate screen buffer (full viewport) /// - Mouse capture /// - Bracketed paste mode /// - Hidden cursor @@ -987,13 +987,13 @@ mod tests { fn test_terminal_options_default() { let options = TerminalOptions::default(); assert!( - !options.alternate_screen, - "default must be inline (never alt-screen)" + options.alternate_screen, + "default must enter the alternate screen (always)" ); assert!(options.mouse_capture); assert!(options.bracketed_paste); assert_eq!(options.title, Some("Cortex".to_string())); - assert!(!options.clear_on_start); + assert!(options.clear_on_start); } #[test]