Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
6 changes: 3 additions & 3 deletions docs/configuration/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
14 changes: 7 additions & 7 deletions docs/guides/tui.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 3 additions & 1 deletion docs/media/tui-lock/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
|------|---------|
Expand Down
3 changes: 2 additions & 1 deletion docs/reference/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ A positional prompt seeds that session. Without a terminal, use
| `--add-dir <DIR>` | Extra writable directory; repeatable |
| `-i`, `--image <PATH>` | 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 <N>` | Concurrent agent threads |
| `--max-tool-threads <N>` | Concurrent tool executions |
| `--command-timeout <SECONDS>` | Shell command timeout |
Expand Down
47 changes: 42 additions & 5 deletions src/cortex-cli/src/cli/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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());
Expand Down Expand Up @@ -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]
Expand Down
12 changes: 10 additions & 2 deletions src/cortex-cli/src/cli/handlers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down
39 changes: 27 additions & 12 deletions src/cortex-engine/src/config/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String>,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -320,7 +320,8 @@ pub struct ConfigOverrides {
pub additional_writable_roots: Vec<PathBuf>,
/// Temperature override from CLI (0.0-2.0).
pub temperature: Option<f32>,
/// 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<bool>,
}

Expand All @@ -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]
Expand All @@ -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);
}
}
20 changes: 10 additions & 10 deletions src/cortex-engine/src/config/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand All @@ -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(),
}
Expand All @@ -353,7 +353,7 @@ fn default_animations() -> bool {
}

fn default_alternate_screen() -> bool {
false
true
}

/// Theme configuration.
Expand Down Expand Up @@ -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);
}
}
8 changes: 7 additions & 1 deletion src/cortex-tui/src/lock_proof.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};

Expand Down Expand Up @@ -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");
Expand Down
Loading
Loading