diff --git a/CHANGELOG.md b/CHANGELOG.md index a87d45c..8ac1769 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased ### Changed -- README `docs/media/intro.gif` is the signed lock TUI at 120×40 (1232×912): splash (`Cortex CLI v1.0.0`, dual hairline, violet `>`, “Plan, search, build anything”) → typing the rate-limit prompt → working (`Cortex Mini 1 · Agent`). The retired mint-mascot welcome card is gone from the banner. +- 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`. +- 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. + +### Changed +- README `docs/media/intro.gif` is the signed lock TUI at 120×40 (1232×912): splash (`Welcome to Cortex, the coding agent CLI`, dual hairline, violet `>`, “Plan, search, build anything”) → typing the rate-limit prompt → working (`Cortex Mini 1 · Agent`). The retired mint-mascot welcome card is gone from the banner. - Focused composer `>` is violet `#A78BFA`; past user `>` stays white. Interrupt `× Stopped`, quota title, failed MCP `x`, and sandbox deny paint error red `#F87171`. Primary copy is `#F5F5F5`. MCP connect/drop and sandbox deny are live, not painted-only. - Gray chrome with one accent, replacing the violet wash: the background is still never painted (`Color::Reset`), structure comes from gray — hairlines `#3A3A3A`, filled charcoal panels `#141414`, dim `#6B7280` secondary copy, white primary copy — and the Cortex violet `#A78BFA` appears only on the focused selection (the `>` caret and the selected label on the dark gray `#262626` bar; the bar is never a violet wash). Green `#4ADE80` covers `✓` success and `+N` diff additions; red and amber stay on diagnostics; the Thinking status is a muted gold. The `#221A38` wash and the interim cyan `#7DD3FC` highlight are banned everywhere - The composer is the Devin-style bar in every session, working and queue state: a full-width thin gray hairline above the `> ` prompt and another below it, dim placeholder, white block cursor; it follows the transcript until the transcript fills the screen diff --git a/Cargo.lock b/Cargo.lock index bfdef43..bb3eec3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -950,7 +950,7 @@ dependencies = [ [[package]] name = "cortex-app-server" -version = "0.1.6" +version = "0.1.7" dependencies = [ "anyhow", "async-channel", @@ -987,7 +987,7 @@ dependencies = [ [[package]] name = "cortex-apply-patch" -version = "0.1.6" +version = "0.1.7" dependencies = [ "anyhow", "serde", @@ -1013,7 +1013,7 @@ dependencies = [ [[package]] name = "cortex-cli" -version = "0.1.6" +version = "0.1.7" dependencies = [ "anyhow", "base64", @@ -1063,7 +1063,7 @@ dependencies = [ [[package]] name = "cortex-collab" -version = "0.1.6" +version = "0.1.7" dependencies = [ "serde", "thiserror 2.0.20", @@ -1074,7 +1074,7 @@ dependencies = [ [[package]] name = "cortex-commands" -version = "0.1.6" +version = "0.1.7" dependencies = [ "chrono", "dirs 6.0.0", @@ -1089,7 +1089,7 @@ dependencies = [ [[package]] name = "cortex-common" -version = "0.1.6" +version = "0.1.7" dependencies = [ "clap", "cortex-protocol", @@ -1122,7 +1122,7 @@ dependencies = [ [[package]] name = "cortex-core" -version = "0.1.6" +version = "0.1.7" dependencies = [ "ahash", "anyhow", @@ -1146,7 +1146,7 @@ dependencies = [ [[package]] name = "cortex-engine" -version = "0.1.6" +version = "0.1.7" dependencies = [ "aes-gcm", "anyhow", @@ -1222,7 +1222,7 @@ dependencies = [ [[package]] name = "cortex-exec" -version = "0.1.6" +version = "0.1.7" dependencies = [ "cortex-engine", "cortex-protocol", @@ -1234,7 +1234,7 @@ dependencies = [ [[package]] name = "cortex-execpolicy" -version = "0.1.6" +version = "0.1.7" dependencies = [ "serde", "serde_json", @@ -1253,7 +1253,7 @@ dependencies = [ [[package]] name = "cortex-feedback" -version = "0.1.6" +version = "0.1.7" dependencies = [ "anyhow", "chrono", @@ -1267,7 +1267,7 @@ dependencies = [ [[package]] name = "cortex-file-search" -version = "0.1.6" +version = "0.1.7" dependencies = [ "anyhow", "ignore", @@ -1310,7 +1310,7 @@ dependencies = [ [[package]] name = "cortex-keyring-store" -version = "0.1.6" +version = "0.1.7" dependencies = [ "keyring", "thiserror 2.0.20", @@ -1334,7 +1334,7 @@ dependencies = [ [[package]] name = "cortex-lmstudio" -version = "0.1.6" +version = "0.1.7" dependencies = [ "reqwest 0.13.4", "serde", @@ -1350,7 +1350,7 @@ dependencies = [ [[package]] name = "cortex-login" -version = "0.1.6" +version = "0.1.7" dependencies = [ "aes-gcm", "anyhow", @@ -1399,7 +1399,7 @@ dependencies = [ [[package]] name = "cortex-mcp-client" -version = "0.1.6" +version = "0.1.7" dependencies = [ "anyhow", "async-trait", @@ -1415,7 +1415,7 @@ dependencies = [ [[package]] name = "cortex-mcp-server" -version = "0.1.6" +version = "0.1.7" dependencies = [ "anyhow", "async-trait", @@ -1428,7 +1428,7 @@ dependencies = [ [[package]] name = "cortex-mcp-types" -version = "0.1.6" +version = "0.1.7" dependencies = [ "schemars", "serde", @@ -1445,7 +1445,7 @@ dependencies = [ [[package]] name = "cortex-network-proxy" -version = "0.1.6" +version = "0.1.7" dependencies = [ "serde", "thiserror 2.0.20", @@ -1455,7 +1455,7 @@ dependencies = [ [[package]] name = "cortex-otel" -version = "0.1.6" +version = "0.1.7" dependencies = [ "cortex-common", "opentelemetry 0.31.0", @@ -1469,7 +1469,7 @@ dependencies = [ [[package]] name = "cortex-plugins" -version = "0.1.6" +version = "0.1.7" dependencies = [ "anyhow", "async-trait", @@ -1496,14 +1496,14 @@ dependencies = [ [[package]] name = "cortex-process-hardening" -version = "0.1.6" +version = "0.1.7" dependencies = [ "libc", ] [[package]] name = "cortex-prompt-harness" -version = "0.1.6" +version = "0.1.7" dependencies = [ "chrono", "indexmap", @@ -1516,7 +1516,7 @@ dependencies = [ [[package]] name = "cortex-protocol" -version = "0.1.6" +version = "0.1.7" dependencies = [ "anyhow", "base64", @@ -1564,7 +1564,7 @@ dependencies = [ [[package]] name = "cortex-sandbox" -version = "0.1.6" +version = "0.1.7" dependencies = [ "dirs 6.0.0", "landlock", @@ -1591,7 +1591,7 @@ dependencies = [ [[package]] name = "cortex-shell-snapshot" -version = "0.1.6" +version = "0.1.7" dependencies = [ "chrono", "dirs 6.0.0", @@ -1607,7 +1607,7 @@ dependencies = [ [[package]] name = "cortex-skills" -version = "0.1.6" +version = "0.1.7" dependencies = [ "chrono", "dashmap", @@ -1626,7 +1626,7 @@ dependencies = [ [[package]] name = "cortex-slack" -version = "0.1.6" +version = "0.1.7" dependencies = [ "async-trait", "axum", @@ -1679,7 +1679,7 @@ dependencies = [ [[package]] name = "cortex-tui" -version = "0.1.6" +version = "0.1.7" dependencies = [ "anyhow", "arboard", @@ -1725,7 +1725,7 @@ dependencies = [ [[package]] name = "cortex-tui-buffer" -version = "0.1.6" +version = "0.1.7" dependencies = [ "cortex-tui-core", "smallvec", @@ -1733,7 +1733,7 @@ dependencies = [ [[package]] name = "cortex-tui-capture" -version = "0.1.6" +version = "0.1.7" dependencies = [ "anyhow", "chrono", @@ -1755,7 +1755,7 @@ dependencies = [ [[package]] name = "cortex-tui-components" -version = "0.1.6" +version = "0.1.7" dependencies = [ "cortex-core", "crossterm", @@ -1767,7 +1767,7 @@ dependencies = [ [[package]] name = "cortex-tui-core" -version = "0.1.6" +version = "0.1.7" dependencies = [ "bitflags 2.13.1", "smallvec", @@ -1776,7 +1776,7 @@ dependencies = [ [[package]] name = "cortex-tui-framework" -version = "0.1.6" +version = "0.1.7" dependencies = [ "cortex-tui-buffer", "cortex-tui-core", @@ -1790,7 +1790,7 @@ dependencies = [ [[package]] name = "cortex-tui-input" -version = "0.1.6" +version = "0.1.7" dependencies = [ "bitflags 2.13.1", "crossterm", @@ -1799,7 +1799,7 @@ dependencies = [ [[package]] name = "cortex-tui-layout" -version = "0.1.6" +version = "0.1.7" dependencies = [ "slotmap", "taffy", @@ -1807,7 +1807,7 @@ dependencies = [ [[package]] name = "cortex-tui-syntax" -version = "0.1.6" +version = "0.1.7" dependencies = [ "ahash", "cortex-tui-text", @@ -1821,7 +1821,7 @@ dependencies = [ [[package]] name = "cortex-tui-terminal" -version = "0.1.6" +version = "0.1.7" dependencies = [ "cortex-tui-buffer", "cortex-tui-core", @@ -1831,7 +1831,7 @@ dependencies = [ [[package]] name = "cortex-tui-text" -version = "0.1.6" +version = "0.1.7" dependencies = [ "smallvec", "unicode-segmentation", @@ -1840,7 +1840,7 @@ dependencies = [ [[package]] name = "cortex-tui-widgets" -version = "0.1.6" +version = "0.1.7" dependencies = [ "cortex-tui-buffer", "cortex-tui-core", @@ -1851,7 +1851,7 @@ dependencies = [ [[package]] name = "cortex-update" -version = "0.1.6" +version = "0.1.7" dependencies = [ "chrono", "cortex-common", @@ -1880,11 +1880,11 @@ dependencies = [ [[package]] name = "cortex-utils-cache" -version = "0.1.6" +version = "0.1.7" [[package]] name = "cortex-utils-file-safety" -version = "0.1.6" +version = "0.1.7" dependencies = [ "tempfile", "thiserror 2.0.20", @@ -1892,11 +1892,11 @@ dependencies = [ [[package]] name = "cortex-utils-git" -version = "0.1.6" +version = "0.1.7" [[package]] name = "cortex-utils-image" -version = "0.1.6" +version = "0.1.7" dependencies = [ "anyhow", "base64", @@ -1904,7 +1904,7 @@ dependencies = [ [[package]] name = "cortex-utils-pty" -version = "0.1.6" +version = "0.1.7" dependencies = [ "anyhow", "portable-pty", @@ -1912,7 +1912,7 @@ dependencies = [ [[package]] name = "cortex-utils-string" -version = "0.1.6" +version = "0.1.7" dependencies = [ "unicode-width", ] diff --git a/docs/configuration/config.md b/docs/configuration/config.md index 93a4f39..104eb07 100644 --- a/docs/configuration/config.md +++ b/docs/configuration/config.md @@ -150,6 +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 [tui.theme] name = "dark" # dark, light, ocean_dark, monokai diff --git a/docs/guides/tui.md b/docs/guides/tui.md index a761dfc..e913beb 100644 --- a/docs/guides/tui.md +++ b/docs/guides/tui.md @@ -16,6 +16,28 @@ 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: +`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: + +```bash +cortex --alternate-screen +``` + +or in `~/.cortex/config.toml`: + +```toml +[tui] +alternate_screen = true +``` + ## The session view ``` @@ -41,7 +63,7 @@ The timeline is the transcript. It renders several kinds of row: | Tool call | `◐`/`●` then the tool name and a short argument summary | A tool the agent invoked | | Tool result | `⎿ …` indented under the call | What the tool returned | | Subagent task | `● Task ` with a todo list underneath | Work delegated to a subagent | -| Welcome | One line: `Cortex CLI v{version}`. Directory and Computer cards sit under it. | Shown while the session is empty | +| Welcome | Two lines: `Welcome to Cortex, the coding agent CLI` then `v{version} · / commands · …`. No mascot, no boxes, no fake `> cortex` prompt. Dropped after the first user turn. | Shown only before the first user turn | Tool rows collapse to a summary. Press `e` while the timeline has focus to expand or collapse the details of the selected tool call. @@ -55,7 +77,13 @@ stage the turn is in — `Thinking`, `Executing `, `Streaming..`, ### Composer -`Enter` sends. `Shift+Enter` inserts a newline. `Up` and `Down` walk your prompt +The empty composer keeps a white block cursor at input column 0 after `> `; +the dim placeholder (`Plan, search, build anything`) sits in the next cell. +The placeholder is paint-only and does not move the caret. While typing, the +placeholder is hidden, copy is `#F5F5F5`, and the block follows the caret. +Blink (~530ms) hides the block; the placeholder then starts at column 0. +`Enter` sends. +`Shift+Enter` inserts a newline. `Up` and `Down` walk your prompt history. If you submit while a turn is running the message is queued, and the composer shows how many are waiting. diff --git a/docs/media/tui-lock/120x40/ask.png b/docs/media/tui-lock/120x40/ask.png index db01635..fddf7e7 100644 Binary files a/docs/media/tui-lock/120x40/ask.png and b/docs/media/tui-lock/120x40/ask.png differ diff --git a/docs/media/tui-lock/120x40/bash.png b/docs/media/tui-lock/120x40/bash.png index ba30d36..c337e3b 100644 Binary files a/docs/media/tui-lock/120x40/bash.png and b/docs/media/tui-lock/120x40/bash.png differ diff --git a/docs/media/tui-lock/120x40/btw.png b/docs/media/tui-lock/120x40/btw.png index 6f44eed..95553f0 100644 Binary files a/docs/media/tui-lock/120x40/btw.png and b/docs/media/tui-lock/120x40/btw.png differ diff --git a/docs/media/tui-lock/120x40/clear.png b/docs/media/tui-lock/120x40/clear.png index 534116d..69d29a5 100644 Binary files a/docs/media/tui-lock/120x40/clear.png and b/docs/media/tui-lock/120x40/clear.png differ diff --git a/docs/media/tui-lock/120x40/clear_confirm.png b/docs/media/tui-lock/120x40/clear_confirm.png index 534116d..69d29a5 100644 Binary files a/docs/media/tui-lock/120x40/clear_confirm.png and b/docs/media/tui-lock/120x40/clear_confirm.png differ diff --git a/docs/media/tui-lock/120x40/cloud.png b/docs/media/tui-lock/120x40/cloud.png index 11cd5e2..de9e03d 100644 Binary files a/docs/media/tui-lock/120x40/cloud.png and b/docs/media/tui-lock/120x40/cloud.png differ diff --git a/docs/media/tui-lock/120x40/compact.png b/docs/media/tui-lock/120x40/compact.png index e3aed45..5787e98 100644 Binary files a/docs/media/tui-lock/120x40/compact.png and b/docs/media/tui-lock/120x40/compact.png differ diff --git a/docs/media/tui-lock/120x40/compacted.png b/docs/media/tui-lock/120x40/compacted.png index e3aed45..5787e98 100644 Binary files a/docs/media/tui-lock/120x40/compacted.png and b/docs/media/tui-lock/120x40/compacted.png differ diff --git a/docs/media/tui-lock/120x40/config.png b/docs/media/tui-lock/120x40/config.png index 4c8f075..84c1b9e 100644 Binary files a/docs/media/tui-lock/120x40/config.png and b/docs/media/tui-lock/120x40/config.png differ diff --git a/docs/media/tui-lock/120x40/delete.png b/docs/media/tui-lock/120x40/delete.png index ae3ea0b..fbf1c9f 100644 Binary files a/docs/media/tui-lock/120x40/delete.png and b/docs/media/tui-lock/120x40/delete.png differ diff --git a/docs/media/tui-lock/120x40/diagnostics.png b/docs/media/tui-lock/120x40/diagnostics.png index b9b9c40..03e9497 100644 Binary files a/docs/media/tui-lock/120x40/diagnostics.png and b/docs/media/tui-lock/120x40/diagnostics.png differ diff --git a/docs/media/tui-lock/120x40/diff_hunk.png b/docs/media/tui-lock/120x40/diff_hunk.png index c8f2bd2..ba84dff 100644 Binary files a/docs/media/tui-lock/120x40/diff_hunk.png and b/docs/media/tui-lock/120x40/diff_hunk.png differ diff --git a/docs/media/tui-lock/120x40/diff_word.png b/docs/media/tui-lock/120x40/diff_word.png index 117bb0b..6bfccdd 100644 Binary files a/docs/media/tui-lock/120x40/diff_word.png and b/docs/media/tui-lock/120x40/diff_word.png differ diff --git a/docs/media/tui-lock/120x40/edit.png b/docs/media/tui-lock/120x40/edit.png index 2d38727..80cd33f 100644 Binary files a/docs/media/tui-lock/120x40/edit.png and b/docs/media/tui-lock/120x40/edit.png differ diff --git a/docs/media/tui-lock/120x40/fetch.png b/docs/media/tui-lock/120x40/fetch.png index 65041f8..8a4eb78 100644 Binary files a/docs/media/tui-lock/120x40/fetch.png and b/docs/media/tui-lock/120x40/fetch.png differ diff --git a/docs/media/tui-lock/120x40/files.png b/docs/media/tui-lock/120x40/files.png index a328991..21baff6 100644 Binary files a/docs/media/tui-lock/120x40/files.png and b/docs/media/tui-lock/120x40/files.png differ diff --git a/docs/media/tui-lock/120x40/first_run.png b/docs/media/tui-lock/120x40/first_run.png index 459cda3..5de45ba 100644 Binary files a/docs/media/tui-lock/120x40/first_run.png and b/docs/media/tui-lock/120x40/first_run.png differ diff --git a/docs/media/tui-lock/120x40/footer_max.png b/docs/media/tui-lock/120x40/footer_max.png index b32403a..48cccff 100644 Binary files a/docs/media/tui-lock/120x40/footer_max.png and b/docs/media/tui-lock/120x40/footer_max.png differ diff --git a/docs/media/tui-lock/120x40/glob.png b/docs/media/tui-lock/120x40/glob.png index c74b0f6..32c4d38 100644 Binary files a/docs/media/tui-lock/120x40/glob.png and b/docs/media/tui-lock/120x40/glob.png differ diff --git a/docs/media/tui-lock/120x40/grep.png b/docs/media/tui-lock/120x40/grep.png index 2470867..ae9bb92 100644 Binary files a/docs/media/tui-lock/120x40/grep.png and b/docs/media/tui-lock/120x40/grep.png differ diff --git a/docs/media/tui-lock/120x40/help.png b/docs/media/tui-lock/120x40/help.png index b5dcb2a..7d7d768 100644 Binary files a/docs/media/tui-lock/120x40/help.png and b/docs/media/tui-lock/120x40/help.png differ diff --git a/docs/media/tui-lock/120x40/interrupt.png b/docs/media/tui-lock/120x40/interrupt.png index ed210b1..adf48f9 100644 Binary files a/docs/media/tui-lock/120x40/interrupt.png and b/docs/media/tui-lock/120x40/interrupt.png differ diff --git a/docs/media/tui-lock/120x40/jobs.png b/docs/media/tui-lock/120x40/jobs.png index 07f9c49..2090345 100644 Binary files a/docs/media/tui-lock/120x40/jobs.png and b/docs/media/tui-lock/120x40/jobs.png differ diff --git a/docs/media/tui-lock/120x40/list.png b/docs/media/tui-lock/120x40/list.png index bf61dd4..6b4d608 100644 Binary files a/docs/media/tui-lock/120x40/list.png and b/docs/media/tui-lock/120x40/list.png differ diff --git a/docs/media/tui-lock/120x40/login.png b/docs/media/tui-lock/120x40/login.png index e72064b..e048c3f 100644 Binary files a/docs/media/tui-lock/120x40/login.png and b/docs/media/tui-lock/120x40/login.png differ diff --git a/docs/media/tui-lock/120x40/login_error.png b/docs/media/tui-lock/120x40/login_error.png index c167bb1..9a0fd5a 100644 Binary files a/docs/media/tui-lock/120x40/login_error.png and b/docs/media/tui-lock/120x40/login_error.png differ diff --git a/docs/media/tui-lock/120x40/login_select.png b/docs/media/tui-lock/120x40/login_select.png index e190d5b..f8c9564 100644 Binary files a/docs/media/tui-lock/120x40/login_select.png and b/docs/media/tui-lock/120x40/login_select.png differ diff --git a/docs/media/tui-lock/120x40/login_success.png b/docs/media/tui-lock/120x40/login_success.png index 5920a6b..91e2360 100644 Binary files a/docs/media/tui-lock/120x40/login_success.png and b/docs/media/tui-lock/120x40/login_success.png differ diff --git a/docs/media/tui-lock/120x40/login_waiting.png b/docs/media/tui-lock/120x40/login_waiting.png index 18af9cc..168e390 100644 Binary files a/docs/media/tui-lock/120x40/login_waiting.png and b/docs/media/tui-lock/120x40/login_waiting.png differ diff --git a/docs/media/tui-lock/120x40/mcp.png b/docs/media/tui-lock/120x40/mcp.png index d5503b8..072b88b 100644 Binary files a/docs/media/tui-lock/120x40/mcp.png and b/docs/media/tui-lock/120x40/mcp.png differ diff --git a/docs/media/tui-lock/120x40/mcp_call.png b/docs/media/tui-lock/120x40/mcp_call.png index 72e7e87..76b3ac9 100644 Binary files a/docs/media/tui-lock/120x40/mcp_call.png and b/docs/media/tui-lock/120x40/mcp_call.png differ diff --git a/docs/media/tui-lock/120x40/mcp_drop.png b/docs/media/tui-lock/120x40/mcp_drop.png index 13b079c..4bf30e3 100644 Binary files a/docs/media/tui-lock/120x40/mcp_drop.png and b/docs/media/tui-lock/120x40/mcp_drop.png differ diff --git a/docs/media/tui-lock/120x40/md_fence.png b/docs/media/tui-lock/120x40/md_fence.png index a327442..6ef9957 100644 Binary files a/docs/media/tui-lock/120x40/md_fence.png and b/docs/media/tui-lock/120x40/md_fence.png differ diff --git a/docs/media/tui-lock/120x40/md_list.png b/docs/media/tui-lock/120x40/md_list.png index 3f04a5d..fa3fb37 100644 Binary files a/docs/media/tui-lock/120x40/md_list.png and b/docs/media/tui-lock/120x40/md_list.png differ diff --git a/docs/media/tui-lock/120x40/md_mixed.png b/docs/media/tui-lock/120x40/md_mixed.png index 1fa7d1f..e608820 100644 Binary files a/docs/media/tui-lock/120x40/md_mixed.png and b/docs/media/tui-lock/120x40/md_mixed.png differ diff --git a/docs/media/tui-lock/120x40/md_table.png b/docs/media/tui-lock/120x40/md_table.png index 46a59ff..26816a0 100644 Binary files a/docs/media/tui-lock/120x40/md_table.png and b/docs/media/tui-lock/120x40/md_table.png differ diff --git a/docs/media/tui-lock/120x40/mode.png b/docs/media/tui-lock/120x40/mode.png index 5aac967..7f1b0f6 100644 Binary files a/docs/media/tui-lock/120x40/mode.png and b/docs/media/tui-lock/120x40/mode.png differ diff --git a/docs/media/tui-lock/120x40/model_compact.png b/docs/media/tui-lock/120x40/model_compact.png index e52512c..5356f46 100644 Binary files a/docs/media/tui-lock/120x40/model_compact.png and b/docs/media/tui-lock/120x40/model_compact.png differ diff --git a/docs/media/tui-lock/120x40/model_full.png b/docs/media/tui-lock/120x40/model_full.png index aceb795..d4746f1 100644 Binary files a/docs/media/tui-lock/120x40/model_full.png and b/docs/media/tui-lock/120x40/model_full.png differ diff --git a/docs/media/tui-lock/120x40/multi_diff.png b/docs/media/tui-lock/120x40/multi_diff.png index 9aad8ec..6191b9d 100644 Binary files a/docs/media/tui-lock/120x40/multi_diff.png and b/docs/media/tui-lock/120x40/multi_diff.png differ diff --git a/docs/media/tui-lock/120x40/palette.png b/docs/media/tui-lock/120x40/palette.png index 1b97378..ef5d497 100644 Binary files a/docs/media/tui-lock/120x40/palette.png and b/docs/media/tui-lock/120x40/palette.png differ diff --git a/docs/media/tui-lock/120x40/palette_empty.png b/docs/media/tui-lock/120x40/palette_empty.png index 154cb38..c5ead76 100644 Binary files a/docs/media/tui-lock/120x40/palette_empty.png and b/docs/media/tui-lock/120x40/palette_empty.png differ diff --git a/docs/media/tui-lock/120x40/permission.png b/docs/media/tui-lock/120x40/permission.png index 86f2cf8..34344dd 100644 Binary files a/docs/media/tui-lock/120x40/permission.png and b/docs/media/tui-lock/120x40/permission.png differ diff --git a/docs/media/tui-lock/120x40/permissions.png b/docs/media/tui-lock/120x40/permissions.png index afaceb4..d4a6a94 100644 Binary files a/docs/media/tui-lock/120x40/permissions.png and b/docs/media/tui-lock/120x40/permissions.png differ diff --git a/docs/media/tui-lock/120x40/plan.png b/docs/media/tui-lock/120x40/plan.png index 5d9a3d1..cc1a83b 100644 Binary files a/docs/media/tui-lock/120x40/plan.png and b/docs/media/tui-lock/120x40/plan.png differ diff --git a/docs/media/tui-lock/120x40/question.png b/docs/media/tui-lock/120x40/question.png index 90aa306..7010312 100644 Binary files a/docs/media/tui-lock/120x40/question.png and b/docs/media/tui-lock/120x40/question.png differ diff --git a/docs/media/tui-lock/120x40/queue.png b/docs/media/tui-lock/120x40/queue.png index 55c8de7..6ecb75b 100644 Binary files a/docs/media/tui-lock/120x40/queue.png and b/docs/media/tui-lock/120x40/queue.png differ diff --git a/docs/media/tui-lock/120x40/quota.png b/docs/media/tui-lock/120x40/quota.png index 2a89613..98129d3 100644 Binary files a/docs/media/tui-lock/120x40/quota.png and b/docs/media/tui-lock/120x40/quota.png differ diff --git a/docs/media/tui-lock/120x40/read.png b/docs/media/tui-lock/120x40/read.png index 9a77d00..77a5307 100644 Binary files a/docs/media/tui-lock/120x40/read.png and b/docs/media/tui-lock/120x40/read.png differ diff --git a/docs/media/tui-lock/120x40/resume.png b/docs/media/tui-lock/120x40/resume.png index ca75b51..9beb4fa 100644 Binary files a/docs/media/tui-lock/120x40/resume.png and b/docs/media/tui-lock/120x40/resume.png differ diff --git a/docs/media/tui-lock/120x40/sandbox.png b/docs/media/tui-lock/120x40/sandbox.png index cd37e8d..21ad7fb 100644 Binary files a/docs/media/tui-lock/120x40/sandbox.png and b/docs/media/tui-lock/120x40/sandbox.png differ diff --git a/docs/media/tui-lock/120x40/sandbox_deny.png b/docs/media/tui-lock/120x40/sandbox_deny.png index f5ebbbc..09fa038 100644 Binary files a/docs/media/tui-lock/120x40/sandbox_deny.png and b/docs/media/tui-lock/120x40/sandbox_deny.png differ diff --git a/docs/media/tui-lock/120x40/session_empty.png b/docs/media/tui-lock/120x40/session_empty.png index be1d4f5..fe55263 100644 Binary files a/docs/media/tui-lock/120x40/session_empty.png and b/docs/media/tui-lock/120x40/session_empty.png differ diff --git a/docs/media/tui-lock/120x40/session_error.png b/docs/media/tui-lock/120x40/session_error.png index b555dc2..728b609 100644 Binary files a/docs/media/tui-lock/120x40/session_error.png and b/docs/media/tui-lock/120x40/session_error.png differ diff --git a/docs/media/tui-lock/120x40/session_loading.png b/docs/media/tui-lock/120x40/session_loading.png index d65762c..a5270a1 100644 Binary files a/docs/media/tui-lock/120x40/session_loading.png and b/docs/media/tui-lock/120x40/session_loading.png differ diff --git a/docs/media/tui-lock/120x40/session_success.png b/docs/media/tui-lock/120x40/session_success.png index 6fd2c31..6a6fc2f 100644 Binary files a/docs/media/tui-lock/120x40/session_success.png and b/docs/media/tui-lock/120x40/session_success.png differ diff --git a/docs/media/tui-lock/120x40/settings_empty.png b/docs/media/tui-lock/120x40/settings_empty.png index 65a0673..7c77930 100644 Binary files a/docs/media/tui-lock/120x40/settings_empty.png and b/docs/media/tui-lock/120x40/settings_empty.png differ diff --git a/docs/media/tui-lock/120x40/settings_hub.png b/docs/media/tui-lock/120x40/settings_hub.png index 7437855..4b84337 100644 Binary files a/docs/media/tui-lock/120x40/settings_hub.png and b/docs/media/tui-lock/120x40/settings_hub.png differ diff --git a/docs/media/tui-lock/120x40/shell.png b/docs/media/tui-lock/120x40/shell.png index 5f9a1d4..55d2e10 100644 Binary files a/docs/media/tui-lock/120x40/shell.png and b/docs/media/tui-lock/120x40/shell.png differ diff --git a/docs/media/tui-lock/120x40/skills.png b/docs/media/tui-lock/120x40/skills.png index 5dabaa6..031db2a 100644 Binary files a/docs/media/tui-lock/120x40/skills.png and b/docs/media/tui-lock/120x40/skills.png differ diff --git a/docs/media/tui-lock/120x40/splash.png b/docs/media/tui-lock/120x40/splash.png index 7583c21..0b95d19 100644 Binary files a/docs/media/tui-lock/120x40/splash.png and b/docs/media/tui-lock/120x40/splash.png differ diff --git a/docs/media/tui-lock/120x40/stopped.png b/docs/media/tui-lock/120x40/stopped.png index ed210b1..adf48f9 100644 Binary files a/docs/media/tui-lock/120x40/stopped.png and b/docs/media/tui-lock/120x40/stopped.png differ diff --git a/docs/media/tui-lock/120x40/streaming.png b/docs/media/tui-lock/120x40/streaming.png index 417c36d..f956f78 100644 Binary files a/docs/media/tui-lock/120x40/streaming.png and b/docs/media/tui-lock/120x40/streaming.png differ diff --git a/docs/media/tui-lock/120x40/sudo.png b/docs/media/tui-lock/120x40/sudo.png index 30f9561..7d40686 100644 Binary files a/docs/media/tui-lock/120x40/sudo.png and b/docs/media/tui-lock/120x40/sudo.png differ diff --git a/docs/media/tui-lock/120x40/task.png b/docs/media/tui-lock/120x40/task.png index 2bc37bd..6c37b21 100644 Binary files a/docs/media/tui-lock/120x40/task.png and b/docs/media/tui-lock/120x40/task.png differ diff --git a/docs/media/tui-lock/120x40/thinking.png b/docs/media/tui-lock/120x40/thinking.png index cb7be00..bd2607d 100644 Binary files a/docs/media/tui-lock/120x40/thinking.png and b/docs/media/tui-lock/120x40/thinking.png differ diff --git a/docs/media/tui-lock/120x40/todos.png b/docs/media/tui-lock/120x40/todos.png index f3c93e2..4ce4586 100644 Binary files a/docs/media/tui-lock/120x40/todos.png and b/docs/media/tui-lock/120x40/todos.png differ diff --git a/docs/media/tui-lock/120x40/tool_tiles.png b/docs/media/tui-lock/120x40/tool_tiles.png index 2470867..ae9bb92 100644 Binary files a/docs/media/tui-lock/120x40/tool_tiles.png and b/docs/media/tui-lock/120x40/tool_tiles.png differ diff --git a/docs/media/tui-lock/120x40/typing.png b/docs/media/tui-lock/120x40/typing.png index 880fd17..5dc1191 100644 Binary files a/docs/media/tui-lock/120x40/typing.png and b/docs/media/tui-lock/120x40/typing.png differ diff --git a/docs/media/tui-lock/120x40/usage.png b/docs/media/tui-lock/120x40/usage.png index df283bb..78d459e 100644 Binary files a/docs/media/tui-lock/120x40/usage.png and b/docs/media/tui-lock/120x40/usage.png differ diff --git a/docs/media/tui-lock/120x40/working.png b/docs/media/tui-lock/120x40/working.png index 6e3c57f..65b3394 100644 Binary files a/docs/media/tui-lock/120x40/working.png and b/docs/media/tui-lock/120x40/working.png differ diff --git a/docs/media/tui-lock/120x40/write.png b/docs/media/tui-lock/120x40/write.png index 142b4ab..aa4d190 100644 Binary files a/docs/media/tui-lock/120x40/write.png and b/docs/media/tui-lock/120x40/write.png differ diff --git a/docs/media/tui-lock/40x12/ask.png b/docs/media/tui-lock/40x12/ask.png index 1f62f47..ac1694f 100644 Binary files a/docs/media/tui-lock/40x12/ask.png and b/docs/media/tui-lock/40x12/ask.png differ diff --git a/docs/media/tui-lock/40x12/bash.png b/docs/media/tui-lock/40x12/bash.png index af2c515..b3cd55f 100644 Binary files a/docs/media/tui-lock/40x12/bash.png and b/docs/media/tui-lock/40x12/bash.png differ diff --git a/docs/media/tui-lock/40x12/btw.png b/docs/media/tui-lock/40x12/btw.png index e539695..f2fe30d 100644 Binary files a/docs/media/tui-lock/40x12/btw.png and b/docs/media/tui-lock/40x12/btw.png differ diff --git a/docs/media/tui-lock/40x12/clear.png b/docs/media/tui-lock/40x12/clear.png index 9080cff..55358e2 100644 Binary files a/docs/media/tui-lock/40x12/clear.png and b/docs/media/tui-lock/40x12/clear.png differ diff --git a/docs/media/tui-lock/40x12/clear_confirm.png b/docs/media/tui-lock/40x12/clear_confirm.png index 9080cff..55358e2 100644 Binary files a/docs/media/tui-lock/40x12/clear_confirm.png and b/docs/media/tui-lock/40x12/clear_confirm.png differ diff --git a/docs/media/tui-lock/40x12/cloud.png b/docs/media/tui-lock/40x12/cloud.png index 2584712..44ba576 100644 Binary files a/docs/media/tui-lock/40x12/cloud.png and b/docs/media/tui-lock/40x12/cloud.png differ diff --git a/docs/media/tui-lock/40x12/compact.png b/docs/media/tui-lock/40x12/compact.png index 59cc02b..609acc8 100644 Binary files a/docs/media/tui-lock/40x12/compact.png and b/docs/media/tui-lock/40x12/compact.png differ diff --git a/docs/media/tui-lock/40x12/compacted.png b/docs/media/tui-lock/40x12/compacted.png index 59cc02b..609acc8 100644 Binary files a/docs/media/tui-lock/40x12/compacted.png and b/docs/media/tui-lock/40x12/compacted.png differ diff --git a/docs/media/tui-lock/40x12/config.png b/docs/media/tui-lock/40x12/config.png index 934c39a..f5140d4 100644 Binary files a/docs/media/tui-lock/40x12/config.png and b/docs/media/tui-lock/40x12/config.png differ diff --git a/docs/media/tui-lock/40x12/delete.png b/docs/media/tui-lock/40x12/delete.png index ab26e68..22e64e2 100644 Binary files a/docs/media/tui-lock/40x12/delete.png and b/docs/media/tui-lock/40x12/delete.png differ diff --git a/docs/media/tui-lock/40x12/diagnostics.png b/docs/media/tui-lock/40x12/diagnostics.png index 73d2fd5..ea753ce 100644 Binary files a/docs/media/tui-lock/40x12/diagnostics.png and b/docs/media/tui-lock/40x12/diagnostics.png differ diff --git a/docs/media/tui-lock/40x12/diff_hunk.png b/docs/media/tui-lock/40x12/diff_hunk.png index 0e16dc8..29bf44a 100644 Binary files a/docs/media/tui-lock/40x12/diff_hunk.png and b/docs/media/tui-lock/40x12/diff_hunk.png differ diff --git a/docs/media/tui-lock/40x12/diff_word.png b/docs/media/tui-lock/40x12/diff_word.png index 217e712..1a90e56 100644 Binary files a/docs/media/tui-lock/40x12/diff_word.png and b/docs/media/tui-lock/40x12/diff_word.png differ diff --git a/docs/media/tui-lock/40x12/edit.png b/docs/media/tui-lock/40x12/edit.png index 4d3beb7..88996a4 100644 Binary files a/docs/media/tui-lock/40x12/edit.png and b/docs/media/tui-lock/40x12/edit.png differ diff --git a/docs/media/tui-lock/40x12/fetch.png b/docs/media/tui-lock/40x12/fetch.png index ee7a1b8..24246c2 100644 Binary files a/docs/media/tui-lock/40x12/fetch.png and b/docs/media/tui-lock/40x12/fetch.png differ diff --git a/docs/media/tui-lock/40x12/files.png b/docs/media/tui-lock/40x12/files.png index 47b417d..765d56b 100644 Binary files a/docs/media/tui-lock/40x12/files.png and b/docs/media/tui-lock/40x12/files.png differ diff --git a/docs/media/tui-lock/40x12/first_run.png b/docs/media/tui-lock/40x12/first_run.png index 3877b1a..a654d05 100644 Binary files a/docs/media/tui-lock/40x12/first_run.png and b/docs/media/tui-lock/40x12/first_run.png differ diff --git a/docs/media/tui-lock/40x12/footer_max.png b/docs/media/tui-lock/40x12/footer_max.png index 2a9289f..aa50109 100644 Binary files a/docs/media/tui-lock/40x12/footer_max.png and b/docs/media/tui-lock/40x12/footer_max.png differ diff --git a/docs/media/tui-lock/40x12/glob.png b/docs/media/tui-lock/40x12/glob.png index c78de78..981f61c 100644 Binary files a/docs/media/tui-lock/40x12/glob.png and b/docs/media/tui-lock/40x12/glob.png differ diff --git a/docs/media/tui-lock/40x12/grep.png b/docs/media/tui-lock/40x12/grep.png index 2397828..9f42f18 100644 Binary files a/docs/media/tui-lock/40x12/grep.png and b/docs/media/tui-lock/40x12/grep.png differ diff --git a/docs/media/tui-lock/40x12/help.png b/docs/media/tui-lock/40x12/help.png index b0f3ccd..66c5d65 100644 Binary files a/docs/media/tui-lock/40x12/help.png and b/docs/media/tui-lock/40x12/help.png differ diff --git a/docs/media/tui-lock/40x12/interrupt.png b/docs/media/tui-lock/40x12/interrupt.png index 99c943f..b5a827f 100644 Binary files a/docs/media/tui-lock/40x12/interrupt.png and b/docs/media/tui-lock/40x12/interrupt.png differ diff --git a/docs/media/tui-lock/40x12/jobs.png b/docs/media/tui-lock/40x12/jobs.png index 1933b1f..4efa5dc 100644 Binary files a/docs/media/tui-lock/40x12/jobs.png and b/docs/media/tui-lock/40x12/jobs.png differ diff --git a/docs/media/tui-lock/40x12/list.png b/docs/media/tui-lock/40x12/list.png index 0d07354..34f1893 100644 Binary files a/docs/media/tui-lock/40x12/list.png and b/docs/media/tui-lock/40x12/list.png differ diff --git a/docs/media/tui-lock/40x12/login.png b/docs/media/tui-lock/40x12/login.png index 5aab8a7..3c85d42 100644 Binary files a/docs/media/tui-lock/40x12/login.png and b/docs/media/tui-lock/40x12/login.png differ diff --git a/docs/media/tui-lock/40x12/login_error.png b/docs/media/tui-lock/40x12/login_error.png index 03f1f83..a9bcb67 100644 Binary files a/docs/media/tui-lock/40x12/login_error.png and b/docs/media/tui-lock/40x12/login_error.png differ diff --git a/docs/media/tui-lock/40x12/login_select.png b/docs/media/tui-lock/40x12/login_select.png index 4089f23..27f9353 100644 Binary files a/docs/media/tui-lock/40x12/login_select.png and b/docs/media/tui-lock/40x12/login_select.png differ diff --git a/docs/media/tui-lock/40x12/login_success.png b/docs/media/tui-lock/40x12/login_success.png index c7c6d4f..d692654 100644 Binary files a/docs/media/tui-lock/40x12/login_success.png and b/docs/media/tui-lock/40x12/login_success.png differ diff --git a/docs/media/tui-lock/40x12/login_waiting.png b/docs/media/tui-lock/40x12/login_waiting.png index 4b926a5..f82631b 100644 Binary files a/docs/media/tui-lock/40x12/login_waiting.png and b/docs/media/tui-lock/40x12/login_waiting.png differ diff --git a/docs/media/tui-lock/40x12/mcp.png b/docs/media/tui-lock/40x12/mcp.png index 8e61f00..be03fac 100644 Binary files a/docs/media/tui-lock/40x12/mcp.png and b/docs/media/tui-lock/40x12/mcp.png differ diff --git a/docs/media/tui-lock/40x12/mcp_call.png b/docs/media/tui-lock/40x12/mcp_call.png index 43a4b9d..02478fc 100644 Binary files a/docs/media/tui-lock/40x12/mcp_call.png and b/docs/media/tui-lock/40x12/mcp_call.png differ diff --git a/docs/media/tui-lock/40x12/mcp_drop.png b/docs/media/tui-lock/40x12/mcp_drop.png index c220563..aa22ffb 100644 Binary files a/docs/media/tui-lock/40x12/mcp_drop.png and b/docs/media/tui-lock/40x12/mcp_drop.png differ diff --git a/docs/media/tui-lock/40x12/md_fence.png b/docs/media/tui-lock/40x12/md_fence.png index 70d492d..8c50852 100644 Binary files a/docs/media/tui-lock/40x12/md_fence.png and b/docs/media/tui-lock/40x12/md_fence.png differ diff --git a/docs/media/tui-lock/40x12/md_list.png b/docs/media/tui-lock/40x12/md_list.png index 6711d50..58c1b0c 100644 Binary files a/docs/media/tui-lock/40x12/md_list.png and b/docs/media/tui-lock/40x12/md_list.png differ diff --git a/docs/media/tui-lock/40x12/md_mixed.png b/docs/media/tui-lock/40x12/md_mixed.png index 3da23b4..a5c8141 100644 Binary files a/docs/media/tui-lock/40x12/md_mixed.png and b/docs/media/tui-lock/40x12/md_mixed.png differ diff --git a/docs/media/tui-lock/40x12/md_table.png b/docs/media/tui-lock/40x12/md_table.png index 2c349f1..7085d0d 100644 Binary files a/docs/media/tui-lock/40x12/md_table.png and b/docs/media/tui-lock/40x12/md_table.png differ diff --git a/docs/media/tui-lock/40x12/mode.png b/docs/media/tui-lock/40x12/mode.png index 2dfb626..d486add 100644 Binary files a/docs/media/tui-lock/40x12/mode.png and b/docs/media/tui-lock/40x12/mode.png differ diff --git a/docs/media/tui-lock/40x12/model_compact.png b/docs/media/tui-lock/40x12/model_compact.png index 084458a..745ba36 100644 Binary files a/docs/media/tui-lock/40x12/model_compact.png and b/docs/media/tui-lock/40x12/model_compact.png differ diff --git a/docs/media/tui-lock/40x12/model_full.png b/docs/media/tui-lock/40x12/model_full.png index 9527f02..5179f8c 100644 Binary files a/docs/media/tui-lock/40x12/model_full.png and b/docs/media/tui-lock/40x12/model_full.png differ diff --git a/docs/media/tui-lock/40x12/multi_diff.png b/docs/media/tui-lock/40x12/multi_diff.png index 0dc9402..e181b24 100644 Binary files a/docs/media/tui-lock/40x12/multi_diff.png and b/docs/media/tui-lock/40x12/multi_diff.png differ diff --git a/docs/media/tui-lock/40x12/palette.png b/docs/media/tui-lock/40x12/palette.png index e046266..2da8581 100644 Binary files a/docs/media/tui-lock/40x12/palette.png and b/docs/media/tui-lock/40x12/palette.png differ diff --git a/docs/media/tui-lock/40x12/palette_empty.png b/docs/media/tui-lock/40x12/palette_empty.png index 310a884..8206d30 100644 Binary files a/docs/media/tui-lock/40x12/palette_empty.png and b/docs/media/tui-lock/40x12/palette_empty.png differ diff --git a/docs/media/tui-lock/40x12/permission.png b/docs/media/tui-lock/40x12/permission.png index 0cff3a5..86b04b6 100644 Binary files a/docs/media/tui-lock/40x12/permission.png and b/docs/media/tui-lock/40x12/permission.png differ diff --git a/docs/media/tui-lock/40x12/permissions.png b/docs/media/tui-lock/40x12/permissions.png index a9e9137..e936606 100644 Binary files a/docs/media/tui-lock/40x12/permissions.png and b/docs/media/tui-lock/40x12/permissions.png differ diff --git a/docs/media/tui-lock/40x12/plan.png b/docs/media/tui-lock/40x12/plan.png index 2d978fe..4cd5ba6 100644 Binary files a/docs/media/tui-lock/40x12/plan.png and b/docs/media/tui-lock/40x12/plan.png differ diff --git a/docs/media/tui-lock/40x12/question.png b/docs/media/tui-lock/40x12/question.png index c61da64..b00b2e7 100644 Binary files a/docs/media/tui-lock/40x12/question.png and b/docs/media/tui-lock/40x12/question.png differ diff --git a/docs/media/tui-lock/40x12/queue.png b/docs/media/tui-lock/40x12/queue.png index fd1a37f..8c6b9a8 100644 Binary files a/docs/media/tui-lock/40x12/queue.png and b/docs/media/tui-lock/40x12/queue.png differ diff --git a/docs/media/tui-lock/40x12/quota.png b/docs/media/tui-lock/40x12/quota.png index 0c573a5..84b1696 100644 Binary files a/docs/media/tui-lock/40x12/quota.png and b/docs/media/tui-lock/40x12/quota.png differ diff --git a/docs/media/tui-lock/40x12/read.png b/docs/media/tui-lock/40x12/read.png index 046b040..72b63e0 100644 Binary files a/docs/media/tui-lock/40x12/read.png and b/docs/media/tui-lock/40x12/read.png differ diff --git a/docs/media/tui-lock/40x12/resume.png b/docs/media/tui-lock/40x12/resume.png index bd47a55..0bf1f80 100644 Binary files a/docs/media/tui-lock/40x12/resume.png and b/docs/media/tui-lock/40x12/resume.png differ diff --git a/docs/media/tui-lock/40x12/sandbox.png b/docs/media/tui-lock/40x12/sandbox.png index 1dad37c..853867f 100644 Binary files a/docs/media/tui-lock/40x12/sandbox.png and b/docs/media/tui-lock/40x12/sandbox.png differ diff --git a/docs/media/tui-lock/40x12/sandbox_deny.png b/docs/media/tui-lock/40x12/sandbox_deny.png index 23e5d75..6fd3837 100644 Binary files a/docs/media/tui-lock/40x12/sandbox_deny.png and b/docs/media/tui-lock/40x12/sandbox_deny.png differ diff --git a/docs/media/tui-lock/40x12/session_empty.png b/docs/media/tui-lock/40x12/session_empty.png index 1230a6b..6228607 100644 Binary files a/docs/media/tui-lock/40x12/session_empty.png and b/docs/media/tui-lock/40x12/session_empty.png differ diff --git a/docs/media/tui-lock/40x12/session_error.png b/docs/media/tui-lock/40x12/session_error.png index a7ebf25..fdc360f 100644 Binary files a/docs/media/tui-lock/40x12/session_error.png and b/docs/media/tui-lock/40x12/session_error.png differ diff --git a/docs/media/tui-lock/40x12/session_loading.png b/docs/media/tui-lock/40x12/session_loading.png index fd890e6..9372000 100644 Binary files a/docs/media/tui-lock/40x12/session_loading.png and b/docs/media/tui-lock/40x12/session_loading.png differ diff --git a/docs/media/tui-lock/40x12/session_success.png b/docs/media/tui-lock/40x12/session_success.png index 261b47c..608ffca 100644 Binary files a/docs/media/tui-lock/40x12/session_success.png and b/docs/media/tui-lock/40x12/session_success.png differ diff --git a/docs/media/tui-lock/40x12/settings_empty.png b/docs/media/tui-lock/40x12/settings_empty.png index 786691f..bf63004 100644 Binary files a/docs/media/tui-lock/40x12/settings_empty.png and b/docs/media/tui-lock/40x12/settings_empty.png differ diff --git a/docs/media/tui-lock/40x12/settings_hub.png b/docs/media/tui-lock/40x12/settings_hub.png index c7c4c33..cb621a0 100644 Binary files a/docs/media/tui-lock/40x12/settings_hub.png and b/docs/media/tui-lock/40x12/settings_hub.png differ diff --git a/docs/media/tui-lock/40x12/shell.png b/docs/media/tui-lock/40x12/shell.png index 463573e..0702225 100644 Binary files a/docs/media/tui-lock/40x12/shell.png and b/docs/media/tui-lock/40x12/shell.png differ diff --git a/docs/media/tui-lock/40x12/skills.png b/docs/media/tui-lock/40x12/skills.png index df8ea10..67fe5dd 100644 Binary files a/docs/media/tui-lock/40x12/skills.png and b/docs/media/tui-lock/40x12/skills.png differ diff --git a/docs/media/tui-lock/40x12/splash.png b/docs/media/tui-lock/40x12/splash.png index 773b005..8ff7375 100644 Binary files a/docs/media/tui-lock/40x12/splash.png and b/docs/media/tui-lock/40x12/splash.png differ diff --git a/docs/media/tui-lock/40x12/stopped.png b/docs/media/tui-lock/40x12/stopped.png index 99c943f..b5a827f 100644 Binary files a/docs/media/tui-lock/40x12/stopped.png and b/docs/media/tui-lock/40x12/stopped.png differ diff --git a/docs/media/tui-lock/40x12/streaming.png b/docs/media/tui-lock/40x12/streaming.png index b5fba98..016416f 100644 Binary files a/docs/media/tui-lock/40x12/streaming.png and b/docs/media/tui-lock/40x12/streaming.png differ diff --git a/docs/media/tui-lock/40x12/sudo.png b/docs/media/tui-lock/40x12/sudo.png index 88894ba..c15c73f 100644 Binary files a/docs/media/tui-lock/40x12/sudo.png and b/docs/media/tui-lock/40x12/sudo.png differ diff --git a/docs/media/tui-lock/40x12/task.png b/docs/media/tui-lock/40x12/task.png index 7c21249..7a7f5e2 100644 Binary files a/docs/media/tui-lock/40x12/task.png and b/docs/media/tui-lock/40x12/task.png differ diff --git a/docs/media/tui-lock/40x12/thinking.png b/docs/media/tui-lock/40x12/thinking.png index 5d2bfef..2f64ff5 100644 Binary files a/docs/media/tui-lock/40x12/thinking.png and b/docs/media/tui-lock/40x12/thinking.png differ diff --git a/docs/media/tui-lock/40x12/todos.png b/docs/media/tui-lock/40x12/todos.png index e8f7940..b9ced5c 100644 Binary files a/docs/media/tui-lock/40x12/todos.png and b/docs/media/tui-lock/40x12/todos.png differ diff --git a/docs/media/tui-lock/40x12/tool_tiles.png b/docs/media/tui-lock/40x12/tool_tiles.png index 2397828..9f42f18 100644 Binary files a/docs/media/tui-lock/40x12/tool_tiles.png and b/docs/media/tui-lock/40x12/tool_tiles.png differ diff --git a/docs/media/tui-lock/40x12/typing.png b/docs/media/tui-lock/40x12/typing.png index fc07f12..574da4c 100644 Binary files a/docs/media/tui-lock/40x12/typing.png and b/docs/media/tui-lock/40x12/typing.png differ diff --git a/docs/media/tui-lock/40x12/usage.png b/docs/media/tui-lock/40x12/usage.png index b07a9c8..305d2e2 100644 Binary files a/docs/media/tui-lock/40x12/usage.png and b/docs/media/tui-lock/40x12/usage.png differ diff --git a/docs/media/tui-lock/40x12/working.png b/docs/media/tui-lock/40x12/working.png index fe244ca..2782697 100644 Binary files a/docs/media/tui-lock/40x12/working.png and b/docs/media/tui-lock/40x12/working.png differ diff --git a/docs/media/tui-lock/40x12/write.png b/docs/media/tui-lock/40x12/write.png index 64c5a0a..a06faad 100644 Binary files a/docs/media/tui-lock/40x12/write.png and b/docs/media/tui-lock/40x12/write.png differ diff --git a/docs/media/tui-lock/README.md b/docs/media/tui-lock/README.md index de5133f..723e650 100644 --- a/docs/media/tui-lock/README.md +++ b/docs/media/tui-lock/README.md @@ -37,12 +37,13 @@ gutter and monochrome (bold-keyword) highlighting, nested bullets indent hunks with a dim old/new gutter, white context, red `-`, green `+`, and word-level colour on a changed line. -Splash copy in these frames is the lock line `Cortex CLI v1.0.0`. The shipped -binary still reports the crate version. +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. | File | Surface | |------|---------| -| `splash.png` | Empty session chrome: cwd, `> cortex`, version, hairline composer | +| `splash.png` | Empty-session splash: welcome + version hints, hairline composer | | `typing.png` | Prompt typed in the composer, block cursor | | `model_compact.png` | `/model` compact picker, hairline search field | | `model_full.png` | `/model` full picker: search field, descriptions, effort, billing note | @@ -62,10 +63,10 @@ binary still reports the crate version. | `diagnostics.png` | Diagnostics tile | | `multi_diff.png` | `/diff` files changed this turn | | `compact.png` | `/compact` — thread compacted (same board as `compacted.png`) | -| `interrupt.png` | Interrupt — tiles stay on screen, `× Stopped` in error red | +| `interrupt.png` | Interrupt — tiles stay on screen, `× Stopped` in error red (same board as `stopped.png`: the UI after ctrl+c) | | `quota.png` | Agent quota exhausted (title in error red) | | `clear.png` | `/clear` confirm (same board as `clear_confirm.png`) | -| `session_empty.png` | Empty session | +| `session_empty.png` | After first user turn: composer + footer only (no welcome) | | `session_loading.png` | Loading / streaming | | `session_error.png` | Error | | `session_success.png` | Success | diff --git a/docs/media/tui-lock/macos/120x40/ask.png b/docs/media/tui-lock/macos/120x40/ask.png index d4d46f1..e480bd7 100644 Binary files a/docs/media/tui-lock/macos/120x40/ask.png and b/docs/media/tui-lock/macos/120x40/ask.png differ diff --git a/docs/media/tui-lock/macos/120x40/bash.png b/docs/media/tui-lock/macos/120x40/bash.png index 188658d..f6847ea 100644 Binary files a/docs/media/tui-lock/macos/120x40/bash.png and b/docs/media/tui-lock/macos/120x40/bash.png differ diff --git a/docs/media/tui-lock/macos/120x40/btw.png b/docs/media/tui-lock/macos/120x40/btw.png index e46b98f..2910b96 100644 Binary files a/docs/media/tui-lock/macos/120x40/btw.png and b/docs/media/tui-lock/macos/120x40/btw.png differ diff --git a/docs/media/tui-lock/macos/120x40/clear.png b/docs/media/tui-lock/macos/120x40/clear.png index 43e081b..1c167f2 100644 Binary files a/docs/media/tui-lock/macos/120x40/clear.png and b/docs/media/tui-lock/macos/120x40/clear.png differ diff --git a/docs/media/tui-lock/macos/120x40/clear_confirm.png b/docs/media/tui-lock/macos/120x40/clear_confirm.png index 43e081b..1c167f2 100644 Binary files a/docs/media/tui-lock/macos/120x40/clear_confirm.png and b/docs/media/tui-lock/macos/120x40/clear_confirm.png differ diff --git a/docs/media/tui-lock/macos/120x40/cloud.png b/docs/media/tui-lock/macos/120x40/cloud.png index 021ee73..a166198 100644 Binary files a/docs/media/tui-lock/macos/120x40/cloud.png and b/docs/media/tui-lock/macos/120x40/cloud.png differ diff --git a/docs/media/tui-lock/macos/120x40/compact.png b/docs/media/tui-lock/macos/120x40/compact.png index 177374e..0761e80 100644 Binary files a/docs/media/tui-lock/macos/120x40/compact.png and b/docs/media/tui-lock/macos/120x40/compact.png differ diff --git a/docs/media/tui-lock/macos/120x40/compacted.png b/docs/media/tui-lock/macos/120x40/compacted.png index 177374e..0761e80 100644 Binary files a/docs/media/tui-lock/macos/120x40/compacted.png and b/docs/media/tui-lock/macos/120x40/compacted.png differ diff --git a/docs/media/tui-lock/macos/120x40/config.png b/docs/media/tui-lock/macos/120x40/config.png index 537f663..d7491a2 100644 Binary files a/docs/media/tui-lock/macos/120x40/config.png and b/docs/media/tui-lock/macos/120x40/config.png differ diff --git a/docs/media/tui-lock/macos/120x40/delete.png b/docs/media/tui-lock/macos/120x40/delete.png index 4888186..dc6b965 100644 Binary files a/docs/media/tui-lock/macos/120x40/delete.png and b/docs/media/tui-lock/macos/120x40/delete.png differ diff --git a/docs/media/tui-lock/macos/120x40/diagnostics.png b/docs/media/tui-lock/macos/120x40/diagnostics.png index 73c99da..02a5a5b 100644 Binary files a/docs/media/tui-lock/macos/120x40/diagnostics.png and b/docs/media/tui-lock/macos/120x40/diagnostics.png differ diff --git a/docs/media/tui-lock/macos/120x40/diff_hunk.png b/docs/media/tui-lock/macos/120x40/diff_hunk.png index d0706b4..3614d6c 100644 Binary files a/docs/media/tui-lock/macos/120x40/diff_hunk.png and b/docs/media/tui-lock/macos/120x40/diff_hunk.png differ diff --git a/docs/media/tui-lock/macos/120x40/diff_word.png b/docs/media/tui-lock/macos/120x40/diff_word.png index c51d496..ac8b019 100644 Binary files a/docs/media/tui-lock/macos/120x40/diff_word.png and b/docs/media/tui-lock/macos/120x40/diff_word.png differ diff --git a/docs/media/tui-lock/macos/120x40/edit.png b/docs/media/tui-lock/macos/120x40/edit.png index 8aa1525..ccddae7 100644 Binary files a/docs/media/tui-lock/macos/120x40/edit.png and b/docs/media/tui-lock/macos/120x40/edit.png differ diff --git a/docs/media/tui-lock/macos/120x40/fetch.png b/docs/media/tui-lock/macos/120x40/fetch.png index de7bd4a..ea23fe6 100644 Binary files a/docs/media/tui-lock/macos/120x40/fetch.png and b/docs/media/tui-lock/macos/120x40/fetch.png differ diff --git a/docs/media/tui-lock/macos/120x40/files.png b/docs/media/tui-lock/macos/120x40/files.png index 73d590c..088e682 100644 Binary files a/docs/media/tui-lock/macos/120x40/files.png and b/docs/media/tui-lock/macos/120x40/files.png differ diff --git a/docs/media/tui-lock/macos/120x40/first_run.png b/docs/media/tui-lock/macos/120x40/first_run.png index 45d1dd1..821ca8f 100644 Binary files a/docs/media/tui-lock/macos/120x40/first_run.png and b/docs/media/tui-lock/macos/120x40/first_run.png differ diff --git a/docs/media/tui-lock/macos/120x40/footer_max.png b/docs/media/tui-lock/macos/120x40/footer_max.png index d64340c..3c49358 100644 Binary files a/docs/media/tui-lock/macos/120x40/footer_max.png and b/docs/media/tui-lock/macos/120x40/footer_max.png differ diff --git a/docs/media/tui-lock/macos/120x40/glob.png b/docs/media/tui-lock/macos/120x40/glob.png index f43a489..c42bc05 100644 Binary files a/docs/media/tui-lock/macos/120x40/glob.png and b/docs/media/tui-lock/macos/120x40/glob.png differ diff --git a/docs/media/tui-lock/macos/120x40/grep.png b/docs/media/tui-lock/macos/120x40/grep.png index c1ece6c..22cdec6 100644 Binary files a/docs/media/tui-lock/macos/120x40/grep.png and b/docs/media/tui-lock/macos/120x40/grep.png differ diff --git a/docs/media/tui-lock/macos/120x40/help.png b/docs/media/tui-lock/macos/120x40/help.png index 8dbcb04..791aa2d 100644 Binary files a/docs/media/tui-lock/macos/120x40/help.png and b/docs/media/tui-lock/macos/120x40/help.png differ diff --git a/docs/media/tui-lock/macos/120x40/interrupt.png b/docs/media/tui-lock/macos/120x40/interrupt.png index e2efd22..5d063e1 100644 Binary files a/docs/media/tui-lock/macos/120x40/interrupt.png and b/docs/media/tui-lock/macos/120x40/interrupt.png differ diff --git a/docs/media/tui-lock/macos/120x40/jobs.png b/docs/media/tui-lock/macos/120x40/jobs.png index bf4cb9a..8d089d9 100644 Binary files a/docs/media/tui-lock/macos/120x40/jobs.png and b/docs/media/tui-lock/macos/120x40/jobs.png differ diff --git a/docs/media/tui-lock/macos/120x40/list.png b/docs/media/tui-lock/macos/120x40/list.png index edba1af..8e70178 100644 Binary files a/docs/media/tui-lock/macos/120x40/list.png and b/docs/media/tui-lock/macos/120x40/list.png differ diff --git a/docs/media/tui-lock/macos/120x40/login.png b/docs/media/tui-lock/macos/120x40/login.png index a56299f..45be890 100644 Binary files a/docs/media/tui-lock/macos/120x40/login.png and b/docs/media/tui-lock/macos/120x40/login.png differ diff --git a/docs/media/tui-lock/macos/120x40/login_error.png b/docs/media/tui-lock/macos/120x40/login_error.png index d028bae..1b3984e 100644 Binary files a/docs/media/tui-lock/macos/120x40/login_error.png and b/docs/media/tui-lock/macos/120x40/login_error.png differ diff --git a/docs/media/tui-lock/macos/120x40/login_select.png b/docs/media/tui-lock/macos/120x40/login_select.png index 7ac3459..7d8d1a3 100644 Binary files a/docs/media/tui-lock/macos/120x40/login_select.png and b/docs/media/tui-lock/macos/120x40/login_select.png differ diff --git a/docs/media/tui-lock/macos/120x40/login_success.png b/docs/media/tui-lock/macos/120x40/login_success.png index 4973851..0a6c6e3 100644 Binary files a/docs/media/tui-lock/macos/120x40/login_success.png and b/docs/media/tui-lock/macos/120x40/login_success.png differ diff --git a/docs/media/tui-lock/macos/120x40/login_waiting.png b/docs/media/tui-lock/macos/120x40/login_waiting.png index e4b1df9..466e24a 100644 Binary files a/docs/media/tui-lock/macos/120x40/login_waiting.png and b/docs/media/tui-lock/macos/120x40/login_waiting.png differ diff --git a/docs/media/tui-lock/macos/120x40/mcp.png b/docs/media/tui-lock/macos/120x40/mcp.png index c1f5f80..01a5e35 100644 Binary files a/docs/media/tui-lock/macos/120x40/mcp.png and b/docs/media/tui-lock/macos/120x40/mcp.png differ diff --git a/docs/media/tui-lock/macos/120x40/mcp_call.png b/docs/media/tui-lock/macos/120x40/mcp_call.png index 3755b1f..131afb7 100644 Binary files a/docs/media/tui-lock/macos/120x40/mcp_call.png and b/docs/media/tui-lock/macos/120x40/mcp_call.png differ diff --git a/docs/media/tui-lock/macos/120x40/mcp_drop.png b/docs/media/tui-lock/macos/120x40/mcp_drop.png index c9769db..ba19fa9 100644 Binary files a/docs/media/tui-lock/macos/120x40/mcp_drop.png and b/docs/media/tui-lock/macos/120x40/mcp_drop.png differ diff --git a/docs/media/tui-lock/macos/120x40/md_fence.png b/docs/media/tui-lock/macos/120x40/md_fence.png index 6c8d362..19f0077 100644 Binary files a/docs/media/tui-lock/macos/120x40/md_fence.png and b/docs/media/tui-lock/macos/120x40/md_fence.png differ diff --git a/docs/media/tui-lock/macos/120x40/md_list.png b/docs/media/tui-lock/macos/120x40/md_list.png index d37814e..f57ea4f 100644 Binary files a/docs/media/tui-lock/macos/120x40/md_list.png and b/docs/media/tui-lock/macos/120x40/md_list.png differ diff --git a/docs/media/tui-lock/macos/120x40/md_mixed.png b/docs/media/tui-lock/macos/120x40/md_mixed.png index 7576276..77077f1 100644 Binary files a/docs/media/tui-lock/macos/120x40/md_mixed.png and b/docs/media/tui-lock/macos/120x40/md_mixed.png differ diff --git a/docs/media/tui-lock/macos/120x40/md_table.png b/docs/media/tui-lock/macos/120x40/md_table.png index 820bc3c..4bcc8e7 100644 Binary files a/docs/media/tui-lock/macos/120x40/md_table.png and b/docs/media/tui-lock/macos/120x40/md_table.png differ diff --git a/docs/media/tui-lock/macos/120x40/mode.png b/docs/media/tui-lock/macos/120x40/mode.png index 4477fee..f7382c0 100644 Binary files a/docs/media/tui-lock/macos/120x40/mode.png and b/docs/media/tui-lock/macos/120x40/mode.png differ diff --git a/docs/media/tui-lock/macos/120x40/model_compact.png b/docs/media/tui-lock/macos/120x40/model_compact.png index 0e832a7..dfc00dd 100644 Binary files a/docs/media/tui-lock/macos/120x40/model_compact.png and b/docs/media/tui-lock/macos/120x40/model_compact.png differ diff --git a/docs/media/tui-lock/macos/120x40/model_full.png b/docs/media/tui-lock/macos/120x40/model_full.png index 29af64d..178e3a5 100644 Binary files a/docs/media/tui-lock/macos/120x40/model_full.png and b/docs/media/tui-lock/macos/120x40/model_full.png differ diff --git a/docs/media/tui-lock/macos/120x40/multi_diff.png b/docs/media/tui-lock/macos/120x40/multi_diff.png index 04c503c..a138245 100644 Binary files a/docs/media/tui-lock/macos/120x40/multi_diff.png and b/docs/media/tui-lock/macos/120x40/multi_diff.png differ diff --git a/docs/media/tui-lock/macos/120x40/palette.png b/docs/media/tui-lock/macos/120x40/palette.png index abe17e9..b7fb88d 100644 Binary files a/docs/media/tui-lock/macos/120x40/palette.png and b/docs/media/tui-lock/macos/120x40/palette.png differ diff --git a/docs/media/tui-lock/macos/120x40/palette_empty.png b/docs/media/tui-lock/macos/120x40/palette_empty.png index c13d677..571ec20 100644 Binary files a/docs/media/tui-lock/macos/120x40/palette_empty.png and b/docs/media/tui-lock/macos/120x40/palette_empty.png differ diff --git a/docs/media/tui-lock/macos/120x40/permission.png b/docs/media/tui-lock/macos/120x40/permission.png index 71b3496..8ccbdcc 100644 Binary files a/docs/media/tui-lock/macos/120x40/permission.png and b/docs/media/tui-lock/macos/120x40/permission.png differ diff --git a/docs/media/tui-lock/macos/120x40/permissions.png b/docs/media/tui-lock/macos/120x40/permissions.png index 68afcfd..5808536 100644 Binary files a/docs/media/tui-lock/macos/120x40/permissions.png and b/docs/media/tui-lock/macos/120x40/permissions.png differ diff --git a/docs/media/tui-lock/macos/120x40/plan.png b/docs/media/tui-lock/macos/120x40/plan.png index 78d3bce..fa6fc10 100644 Binary files a/docs/media/tui-lock/macos/120x40/plan.png and b/docs/media/tui-lock/macos/120x40/plan.png differ diff --git a/docs/media/tui-lock/macos/120x40/question.png b/docs/media/tui-lock/macos/120x40/question.png index 4958197..2dcdc29 100644 Binary files a/docs/media/tui-lock/macos/120x40/question.png and b/docs/media/tui-lock/macos/120x40/question.png differ diff --git a/docs/media/tui-lock/macos/120x40/queue.png b/docs/media/tui-lock/macos/120x40/queue.png index 6138dc3..19d77ca 100644 Binary files a/docs/media/tui-lock/macos/120x40/queue.png and b/docs/media/tui-lock/macos/120x40/queue.png differ diff --git a/docs/media/tui-lock/macos/120x40/quota.png b/docs/media/tui-lock/macos/120x40/quota.png index 70f2b04..c1c9214 100644 Binary files a/docs/media/tui-lock/macos/120x40/quota.png and b/docs/media/tui-lock/macos/120x40/quota.png differ diff --git a/docs/media/tui-lock/macos/120x40/read.png b/docs/media/tui-lock/macos/120x40/read.png index e93a4bb..a905aa8 100644 Binary files a/docs/media/tui-lock/macos/120x40/read.png and b/docs/media/tui-lock/macos/120x40/read.png differ diff --git a/docs/media/tui-lock/macos/120x40/resume.png b/docs/media/tui-lock/macos/120x40/resume.png index 81ff7a1..1f437bc 100644 Binary files a/docs/media/tui-lock/macos/120x40/resume.png and b/docs/media/tui-lock/macos/120x40/resume.png differ diff --git a/docs/media/tui-lock/macos/120x40/sandbox.png b/docs/media/tui-lock/macos/120x40/sandbox.png index 37e0d3f..cd8ebc0 100644 Binary files a/docs/media/tui-lock/macos/120x40/sandbox.png and b/docs/media/tui-lock/macos/120x40/sandbox.png differ diff --git a/docs/media/tui-lock/macos/120x40/sandbox_deny.png b/docs/media/tui-lock/macos/120x40/sandbox_deny.png index ebff597..f8af9ed 100644 Binary files a/docs/media/tui-lock/macos/120x40/sandbox_deny.png and b/docs/media/tui-lock/macos/120x40/sandbox_deny.png differ diff --git a/docs/media/tui-lock/macos/120x40/session_empty.png b/docs/media/tui-lock/macos/120x40/session_empty.png index 12a6a72..9a05526 100644 Binary files a/docs/media/tui-lock/macos/120x40/session_empty.png and b/docs/media/tui-lock/macos/120x40/session_empty.png differ diff --git a/docs/media/tui-lock/macos/120x40/session_error.png b/docs/media/tui-lock/macos/120x40/session_error.png index dbd58e0..4e46a7a 100644 Binary files a/docs/media/tui-lock/macos/120x40/session_error.png and b/docs/media/tui-lock/macos/120x40/session_error.png differ diff --git a/docs/media/tui-lock/macos/120x40/session_loading.png b/docs/media/tui-lock/macos/120x40/session_loading.png index d2d1627..8a94aad 100644 Binary files a/docs/media/tui-lock/macos/120x40/session_loading.png and b/docs/media/tui-lock/macos/120x40/session_loading.png differ diff --git a/docs/media/tui-lock/macos/120x40/session_success.png b/docs/media/tui-lock/macos/120x40/session_success.png index cc70580..0318169 100644 Binary files a/docs/media/tui-lock/macos/120x40/session_success.png and b/docs/media/tui-lock/macos/120x40/session_success.png differ diff --git a/docs/media/tui-lock/macos/120x40/settings_empty.png b/docs/media/tui-lock/macos/120x40/settings_empty.png index 9cf3efe..e27e5ec 100644 Binary files a/docs/media/tui-lock/macos/120x40/settings_empty.png and b/docs/media/tui-lock/macos/120x40/settings_empty.png differ diff --git a/docs/media/tui-lock/macos/120x40/settings_hub.png b/docs/media/tui-lock/macos/120x40/settings_hub.png index f9f4fb5..0559fc6 100644 Binary files a/docs/media/tui-lock/macos/120x40/settings_hub.png and b/docs/media/tui-lock/macos/120x40/settings_hub.png differ diff --git a/docs/media/tui-lock/macos/120x40/shell.png b/docs/media/tui-lock/macos/120x40/shell.png index 071e20c..33181fd 100644 Binary files a/docs/media/tui-lock/macos/120x40/shell.png and b/docs/media/tui-lock/macos/120x40/shell.png differ diff --git a/docs/media/tui-lock/macos/120x40/skills.png b/docs/media/tui-lock/macos/120x40/skills.png index 55ad967..56f5cf0 100644 Binary files a/docs/media/tui-lock/macos/120x40/skills.png and b/docs/media/tui-lock/macos/120x40/skills.png differ diff --git a/docs/media/tui-lock/macos/120x40/splash.png b/docs/media/tui-lock/macos/120x40/splash.png index 2e37289..29e48cd 100644 Binary files a/docs/media/tui-lock/macos/120x40/splash.png and b/docs/media/tui-lock/macos/120x40/splash.png differ diff --git a/docs/media/tui-lock/macos/120x40/stopped.png b/docs/media/tui-lock/macos/120x40/stopped.png index e2efd22..5d063e1 100644 Binary files a/docs/media/tui-lock/macos/120x40/stopped.png and b/docs/media/tui-lock/macos/120x40/stopped.png differ diff --git a/docs/media/tui-lock/macos/120x40/streaming.png b/docs/media/tui-lock/macos/120x40/streaming.png index d36e26d..7fa689a 100644 Binary files a/docs/media/tui-lock/macos/120x40/streaming.png and b/docs/media/tui-lock/macos/120x40/streaming.png differ diff --git a/docs/media/tui-lock/macos/120x40/sudo.png b/docs/media/tui-lock/macos/120x40/sudo.png index d9351f7..2abddf1 100644 Binary files a/docs/media/tui-lock/macos/120x40/sudo.png and b/docs/media/tui-lock/macos/120x40/sudo.png differ diff --git a/docs/media/tui-lock/macos/120x40/task.png b/docs/media/tui-lock/macos/120x40/task.png index e64c8b4..f0df872 100644 Binary files a/docs/media/tui-lock/macos/120x40/task.png and b/docs/media/tui-lock/macos/120x40/task.png differ diff --git a/docs/media/tui-lock/macos/120x40/thinking.png b/docs/media/tui-lock/macos/120x40/thinking.png index 364597f..6acf5f6 100644 Binary files a/docs/media/tui-lock/macos/120x40/thinking.png and b/docs/media/tui-lock/macos/120x40/thinking.png differ diff --git a/docs/media/tui-lock/macos/120x40/todos.png b/docs/media/tui-lock/macos/120x40/todos.png index 8a8a210..60a08b7 100644 Binary files a/docs/media/tui-lock/macos/120x40/todos.png and b/docs/media/tui-lock/macos/120x40/todos.png differ diff --git a/docs/media/tui-lock/macos/120x40/tool_tiles.png b/docs/media/tui-lock/macos/120x40/tool_tiles.png index c1ece6c..22cdec6 100644 Binary files a/docs/media/tui-lock/macos/120x40/tool_tiles.png and b/docs/media/tui-lock/macos/120x40/tool_tiles.png differ diff --git a/docs/media/tui-lock/macos/120x40/typing.png b/docs/media/tui-lock/macos/120x40/typing.png index 418af88..f941b6f 100644 Binary files a/docs/media/tui-lock/macos/120x40/typing.png and b/docs/media/tui-lock/macos/120x40/typing.png differ diff --git a/docs/media/tui-lock/macos/120x40/usage.png b/docs/media/tui-lock/macos/120x40/usage.png index ae4fe5f..76c5d62 100644 Binary files a/docs/media/tui-lock/macos/120x40/usage.png and b/docs/media/tui-lock/macos/120x40/usage.png differ diff --git a/docs/media/tui-lock/macos/120x40/working.png b/docs/media/tui-lock/macos/120x40/working.png index 1dd5e26..f71d870 100644 Binary files a/docs/media/tui-lock/macos/120x40/working.png and b/docs/media/tui-lock/macos/120x40/working.png differ diff --git a/docs/media/tui-lock/macos/120x40/write.png b/docs/media/tui-lock/macos/120x40/write.png index a686838..08f37f4 100644 Binary files a/docs/media/tui-lock/macos/120x40/write.png and b/docs/media/tui-lock/macos/120x40/write.png differ diff --git a/docs/media/tui-lock/macos/40x12/ask.png b/docs/media/tui-lock/macos/40x12/ask.png index e1568f6..6c6c43c 100644 Binary files a/docs/media/tui-lock/macos/40x12/ask.png and b/docs/media/tui-lock/macos/40x12/ask.png differ diff --git a/docs/media/tui-lock/macos/40x12/bash.png b/docs/media/tui-lock/macos/40x12/bash.png index cb113e7..6a82fa8 100644 Binary files a/docs/media/tui-lock/macos/40x12/bash.png and b/docs/media/tui-lock/macos/40x12/bash.png differ diff --git a/docs/media/tui-lock/macos/40x12/btw.png b/docs/media/tui-lock/macos/40x12/btw.png index df8303f..fb8e7ae 100644 Binary files a/docs/media/tui-lock/macos/40x12/btw.png and b/docs/media/tui-lock/macos/40x12/btw.png differ diff --git a/docs/media/tui-lock/macos/40x12/clear.png b/docs/media/tui-lock/macos/40x12/clear.png index be6d287..329bf30 100644 Binary files a/docs/media/tui-lock/macos/40x12/clear.png and b/docs/media/tui-lock/macos/40x12/clear.png differ diff --git a/docs/media/tui-lock/macos/40x12/clear_confirm.png b/docs/media/tui-lock/macos/40x12/clear_confirm.png index be6d287..329bf30 100644 Binary files a/docs/media/tui-lock/macos/40x12/clear_confirm.png and b/docs/media/tui-lock/macos/40x12/clear_confirm.png differ diff --git a/docs/media/tui-lock/macos/40x12/cloud.png b/docs/media/tui-lock/macos/40x12/cloud.png index 3f45039..018da8c 100644 Binary files a/docs/media/tui-lock/macos/40x12/cloud.png and b/docs/media/tui-lock/macos/40x12/cloud.png differ diff --git a/docs/media/tui-lock/macos/40x12/compact.png b/docs/media/tui-lock/macos/40x12/compact.png index 41c7ada..9246eb7 100644 Binary files a/docs/media/tui-lock/macos/40x12/compact.png and b/docs/media/tui-lock/macos/40x12/compact.png differ diff --git a/docs/media/tui-lock/macos/40x12/compacted.png b/docs/media/tui-lock/macos/40x12/compacted.png index 41c7ada..9246eb7 100644 Binary files a/docs/media/tui-lock/macos/40x12/compacted.png and b/docs/media/tui-lock/macos/40x12/compacted.png differ diff --git a/docs/media/tui-lock/macos/40x12/config.png b/docs/media/tui-lock/macos/40x12/config.png index 92b9bdd..c6b66a3 100644 Binary files a/docs/media/tui-lock/macos/40x12/config.png and b/docs/media/tui-lock/macos/40x12/config.png differ diff --git a/docs/media/tui-lock/macos/40x12/delete.png b/docs/media/tui-lock/macos/40x12/delete.png index 5f20a78..460b1b7 100644 Binary files a/docs/media/tui-lock/macos/40x12/delete.png and b/docs/media/tui-lock/macos/40x12/delete.png differ diff --git a/docs/media/tui-lock/macos/40x12/diagnostics.png b/docs/media/tui-lock/macos/40x12/diagnostics.png index deb14e9..76698ca 100644 Binary files a/docs/media/tui-lock/macos/40x12/diagnostics.png and b/docs/media/tui-lock/macos/40x12/diagnostics.png differ diff --git a/docs/media/tui-lock/macos/40x12/diff_hunk.png b/docs/media/tui-lock/macos/40x12/diff_hunk.png index e7cdb9d..8fbe5d2 100644 Binary files a/docs/media/tui-lock/macos/40x12/diff_hunk.png and b/docs/media/tui-lock/macos/40x12/diff_hunk.png differ diff --git a/docs/media/tui-lock/macos/40x12/diff_word.png b/docs/media/tui-lock/macos/40x12/diff_word.png index c619f2b..f589900 100644 Binary files a/docs/media/tui-lock/macos/40x12/diff_word.png and b/docs/media/tui-lock/macos/40x12/diff_word.png differ diff --git a/docs/media/tui-lock/macos/40x12/edit.png b/docs/media/tui-lock/macos/40x12/edit.png index 269e7c9..deee61c 100644 Binary files a/docs/media/tui-lock/macos/40x12/edit.png and b/docs/media/tui-lock/macos/40x12/edit.png differ diff --git a/docs/media/tui-lock/macos/40x12/fetch.png b/docs/media/tui-lock/macos/40x12/fetch.png index 5042870..ad63b06 100644 Binary files a/docs/media/tui-lock/macos/40x12/fetch.png and b/docs/media/tui-lock/macos/40x12/fetch.png differ diff --git a/docs/media/tui-lock/macos/40x12/files.png b/docs/media/tui-lock/macos/40x12/files.png index 92755b7..acc31d2 100644 Binary files a/docs/media/tui-lock/macos/40x12/files.png and b/docs/media/tui-lock/macos/40x12/files.png differ diff --git a/docs/media/tui-lock/macos/40x12/first_run.png b/docs/media/tui-lock/macos/40x12/first_run.png index 9b93ab5..1d5b0a9 100644 Binary files a/docs/media/tui-lock/macos/40x12/first_run.png and b/docs/media/tui-lock/macos/40x12/first_run.png differ diff --git a/docs/media/tui-lock/macos/40x12/footer_max.png b/docs/media/tui-lock/macos/40x12/footer_max.png index 1622891..7b748d5 100644 Binary files a/docs/media/tui-lock/macos/40x12/footer_max.png and b/docs/media/tui-lock/macos/40x12/footer_max.png differ diff --git a/docs/media/tui-lock/macos/40x12/glob.png b/docs/media/tui-lock/macos/40x12/glob.png index 5068440..c4661b9 100644 Binary files a/docs/media/tui-lock/macos/40x12/glob.png and b/docs/media/tui-lock/macos/40x12/glob.png differ diff --git a/docs/media/tui-lock/macos/40x12/grep.png b/docs/media/tui-lock/macos/40x12/grep.png index 373fea8..4d924fa 100644 Binary files a/docs/media/tui-lock/macos/40x12/grep.png and b/docs/media/tui-lock/macos/40x12/grep.png differ diff --git a/docs/media/tui-lock/macos/40x12/help.png b/docs/media/tui-lock/macos/40x12/help.png index a10684b..2b2a571 100644 Binary files a/docs/media/tui-lock/macos/40x12/help.png and b/docs/media/tui-lock/macos/40x12/help.png differ diff --git a/docs/media/tui-lock/macos/40x12/interrupt.png b/docs/media/tui-lock/macos/40x12/interrupt.png index cf6c90e..66820fc 100644 Binary files a/docs/media/tui-lock/macos/40x12/interrupt.png and b/docs/media/tui-lock/macos/40x12/interrupt.png differ diff --git a/docs/media/tui-lock/macos/40x12/jobs.png b/docs/media/tui-lock/macos/40x12/jobs.png index 34098f4..7914c01 100644 Binary files a/docs/media/tui-lock/macos/40x12/jobs.png and b/docs/media/tui-lock/macos/40x12/jobs.png differ diff --git a/docs/media/tui-lock/macos/40x12/list.png b/docs/media/tui-lock/macos/40x12/list.png index feb88b0..917d249 100644 Binary files a/docs/media/tui-lock/macos/40x12/list.png and b/docs/media/tui-lock/macos/40x12/list.png differ diff --git a/docs/media/tui-lock/macos/40x12/login.png b/docs/media/tui-lock/macos/40x12/login.png index 8d5513b..91ae1a9 100644 Binary files a/docs/media/tui-lock/macos/40x12/login.png and b/docs/media/tui-lock/macos/40x12/login.png differ diff --git a/docs/media/tui-lock/macos/40x12/login_error.png b/docs/media/tui-lock/macos/40x12/login_error.png index 631f8b5..c0c1f55 100644 Binary files a/docs/media/tui-lock/macos/40x12/login_error.png and b/docs/media/tui-lock/macos/40x12/login_error.png differ diff --git a/docs/media/tui-lock/macos/40x12/login_select.png b/docs/media/tui-lock/macos/40x12/login_select.png index 7e20718..cb02744 100644 Binary files a/docs/media/tui-lock/macos/40x12/login_select.png and b/docs/media/tui-lock/macos/40x12/login_select.png differ diff --git a/docs/media/tui-lock/macos/40x12/login_success.png b/docs/media/tui-lock/macos/40x12/login_success.png index 6453ba0..a1f031f 100644 Binary files a/docs/media/tui-lock/macos/40x12/login_success.png and b/docs/media/tui-lock/macos/40x12/login_success.png differ diff --git a/docs/media/tui-lock/macos/40x12/login_waiting.png b/docs/media/tui-lock/macos/40x12/login_waiting.png index 7279b40..282af33 100644 Binary files a/docs/media/tui-lock/macos/40x12/login_waiting.png and b/docs/media/tui-lock/macos/40x12/login_waiting.png differ diff --git a/docs/media/tui-lock/macos/40x12/mcp.png b/docs/media/tui-lock/macos/40x12/mcp.png index ef075da..1e55a9e 100644 Binary files a/docs/media/tui-lock/macos/40x12/mcp.png and b/docs/media/tui-lock/macos/40x12/mcp.png differ diff --git a/docs/media/tui-lock/macos/40x12/mcp_call.png b/docs/media/tui-lock/macos/40x12/mcp_call.png index f02f581..4251baa 100644 Binary files a/docs/media/tui-lock/macos/40x12/mcp_call.png and b/docs/media/tui-lock/macos/40x12/mcp_call.png differ diff --git a/docs/media/tui-lock/macos/40x12/mcp_drop.png b/docs/media/tui-lock/macos/40x12/mcp_drop.png index 402ae71..490b9f1 100644 Binary files a/docs/media/tui-lock/macos/40x12/mcp_drop.png and b/docs/media/tui-lock/macos/40x12/mcp_drop.png differ diff --git a/docs/media/tui-lock/macos/40x12/md_fence.png b/docs/media/tui-lock/macos/40x12/md_fence.png index a2ce478..df32874 100644 Binary files a/docs/media/tui-lock/macos/40x12/md_fence.png and b/docs/media/tui-lock/macos/40x12/md_fence.png differ diff --git a/docs/media/tui-lock/macos/40x12/md_list.png b/docs/media/tui-lock/macos/40x12/md_list.png index ec94096..e4d4e0c 100644 Binary files a/docs/media/tui-lock/macos/40x12/md_list.png and b/docs/media/tui-lock/macos/40x12/md_list.png differ diff --git a/docs/media/tui-lock/macos/40x12/md_mixed.png b/docs/media/tui-lock/macos/40x12/md_mixed.png index 6c94036..b403b7b 100644 Binary files a/docs/media/tui-lock/macos/40x12/md_mixed.png and b/docs/media/tui-lock/macos/40x12/md_mixed.png differ diff --git a/docs/media/tui-lock/macos/40x12/md_table.png b/docs/media/tui-lock/macos/40x12/md_table.png index 435faa1..6db894c 100644 Binary files a/docs/media/tui-lock/macos/40x12/md_table.png and b/docs/media/tui-lock/macos/40x12/md_table.png differ diff --git a/docs/media/tui-lock/macos/40x12/mode.png b/docs/media/tui-lock/macos/40x12/mode.png index dd1d0b5..6e4c284 100644 Binary files a/docs/media/tui-lock/macos/40x12/mode.png and b/docs/media/tui-lock/macos/40x12/mode.png differ diff --git a/docs/media/tui-lock/macos/40x12/model_compact.png b/docs/media/tui-lock/macos/40x12/model_compact.png index 8c18bfd..4ffface 100644 Binary files a/docs/media/tui-lock/macos/40x12/model_compact.png and b/docs/media/tui-lock/macos/40x12/model_compact.png differ diff --git a/docs/media/tui-lock/macos/40x12/model_full.png b/docs/media/tui-lock/macos/40x12/model_full.png index 1b4882c..0d6d202 100644 Binary files a/docs/media/tui-lock/macos/40x12/model_full.png and b/docs/media/tui-lock/macos/40x12/model_full.png differ diff --git a/docs/media/tui-lock/macos/40x12/multi_diff.png b/docs/media/tui-lock/macos/40x12/multi_diff.png index 6a0df32..8f4a746 100644 Binary files a/docs/media/tui-lock/macos/40x12/multi_diff.png and b/docs/media/tui-lock/macos/40x12/multi_diff.png differ diff --git a/docs/media/tui-lock/macos/40x12/palette.png b/docs/media/tui-lock/macos/40x12/palette.png index 4f6d74b..06953bd 100644 Binary files a/docs/media/tui-lock/macos/40x12/palette.png and b/docs/media/tui-lock/macos/40x12/palette.png differ diff --git a/docs/media/tui-lock/macos/40x12/palette_empty.png b/docs/media/tui-lock/macos/40x12/palette_empty.png index 327b8c6..b75f72f 100644 Binary files a/docs/media/tui-lock/macos/40x12/palette_empty.png and b/docs/media/tui-lock/macos/40x12/palette_empty.png differ diff --git a/docs/media/tui-lock/macos/40x12/permission.png b/docs/media/tui-lock/macos/40x12/permission.png index 88f07f6..76615a1 100644 Binary files a/docs/media/tui-lock/macos/40x12/permission.png and b/docs/media/tui-lock/macos/40x12/permission.png differ diff --git a/docs/media/tui-lock/macos/40x12/permissions.png b/docs/media/tui-lock/macos/40x12/permissions.png index 6053212..86a05a1 100644 Binary files a/docs/media/tui-lock/macos/40x12/permissions.png and b/docs/media/tui-lock/macos/40x12/permissions.png differ diff --git a/docs/media/tui-lock/macos/40x12/plan.png b/docs/media/tui-lock/macos/40x12/plan.png index ecf7e9f..e5503ef 100644 Binary files a/docs/media/tui-lock/macos/40x12/plan.png and b/docs/media/tui-lock/macos/40x12/plan.png differ diff --git a/docs/media/tui-lock/macos/40x12/question.png b/docs/media/tui-lock/macos/40x12/question.png index c2a50aa..2dd77c4 100644 Binary files a/docs/media/tui-lock/macos/40x12/question.png and b/docs/media/tui-lock/macos/40x12/question.png differ diff --git a/docs/media/tui-lock/macos/40x12/queue.png b/docs/media/tui-lock/macos/40x12/queue.png index 63041ec..990cb10 100644 Binary files a/docs/media/tui-lock/macos/40x12/queue.png and b/docs/media/tui-lock/macos/40x12/queue.png differ diff --git a/docs/media/tui-lock/macos/40x12/quota.png b/docs/media/tui-lock/macos/40x12/quota.png index 0bb551d..75270fd 100644 Binary files a/docs/media/tui-lock/macos/40x12/quota.png and b/docs/media/tui-lock/macos/40x12/quota.png differ diff --git a/docs/media/tui-lock/macos/40x12/read.png b/docs/media/tui-lock/macos/40x12/read.png index d7efca8..b2d145f 100644 Binary files a/docs/media/tui-lock/macos/40x12/read.png and b/docs/media/tui-lock/macos/40x12/read.png differ diff --git a/docs/media/tui-lock/macos/40x12/resume.png b/docs/media/tui-lock/macos/40x12/resume.png index b276679..429d1ad 100644 Binary files a/docs/media/tui-lock/macos/40x12/resume.png and b/docs/media/tui-lock/macos/40x12/resume.png differ diff --git a/docs/media/tui-lock/macos/40x12/sandbox.png b/docs/media/tui-lock/macos/40x12/sandbox.png index e2812c4..48e233e 100644 Binary files a/docs/media/tui-lock/macos/40x12/sandbox.png and b/docs/media/tui-lock/macos/40x12/sandbox.png differ diff --git a/docs/media/tui-lock/macos/40x12/sandbox_deny.png b/docs/media/tui-lock/macos/40x12/sandbox_deny.png index 21ce114..5f9817c 100644 Binary files a/docs/media/tui-lock/macos/40x12/sandbox_deny.png and b/docs/media/tui-lock/macos/40x12/sandbox_deny.png differ diff --git a/docs/media/tui-lock/macos/40x12/session_empty.png b/docs/media/tui-lock/macos/40x12/session_empty.png index b5a8655..98a80e0 100644 Binary files a/docs/media/tui-lock/macos/40x12/session_empty.png and b/docs/media/tui-lock/macos/40x12/session_empty.png differ diff --git a/docs/media/tui-lock/macos/40x12/session_error.png b/docs/media/tui-lock/macos/40x12/session_error.png index 3cc3068..5d833bb 100644 Binary files a/docs/media/tui-lock/macos/40x12/session_error.png and b/docs/media/tui-lock/macos/40x12/session_error.png differ diff --git a/docs/media/tui-lock/macos/40x12/session_loading.png b/docs/media/tui-lock/macos/40x12/session_loading.png index 7b9289d..49c1d64 100644 Binary files a/docs/media/tui-lock/macos/40x12/session_loading.png and b/docs/media/tui-lock/macos/40x12/session_loading.png differ diff --git a/docs/media/tui-lock/macos/40x12/session_success.png b/docs/media/tui-lock/macos/40x12/session_success.png index 5c6572b..8b81a31 100644 Binary files a/docs/media/tui-lock/macos/40x12/session_success.png and b/docs/media/tui-lock/macos/40x12/session_success.png differ diff --git a/docs/media/tui-lock/macos/40x12/settings_empty.png b/docs/media/tui-lock/macos/40x12/settings_empty.png index 0874a96..c601e8c 100644 Binary files a/docs/media/tui-lock/macos/40x12/settings_empty.png and b/docs/media/tui-lock/macos/40x12/settings_empty.png differ diff --git a/docs/media/tui-lock/macos/40x12/settings_hub.png b/docs/media/tui-lock/macos/40x12/settings_hub.png index 18ee74e..93b7454 100644 Binary files a/docs/media/tui-lock/macos/40x12/settings_hub.png and b/docs/media/tui-lock/macos/40x12/settings_hub.png differ diff --git a/docs/media/tui-lock/macos/40x12/shell.png b/docs/media/tui-lock/macos/40x12/shell.png index 41b8b16..bf1ae35 100644 Binary files a/docs/media/tui-lock/macos/40x12/shell.png and b/docs/media/tui-lock/macos/40x12/shell.png differ diff --git a/docs/media/tui-lock/macos/40x12/skills.png b/docs/media/tui-lock/macos/40x12/skills.png index 1ca0ba2..b7e2035 100644 Binary files a/docs/media/tui-lock/macos/40x12/skills.png and b/docs/media/tui-lock/macos/40x12/skills.png differ diff --git a/docs/media/tui-lock/macos/40x12/splash.png b/docs/media/tui-lock/macos/40x12/splash.png index 1720cf9..b8613d8 100644 Binary files a/docs/media/tui-lock/macos/40x12/splash.png and b/docs/media/tui-lock/macos/40x12/splash.png differ diff --git a/docs/media/tui-lock/macos/40x12/stopped.png b/docs/media/tui-lock/macos/40x12/stopped.png index cf6c90e..66820fc 100644 Binary files a/docs/media/tui-lock/macos/40x12/stopped.png and b/docs/media/tui-lock/macos/40x12/stopped.png differ diff --git a/docs/media/tui-lock/macos/40x12/streaming.png b/docs/media/tui-lock/macos/40x12/streaming.png index e249dd5..5c51382 100644 Binary files a/docs/media/tui-lock/macos/40x12/streaming.png and b/docs/media/tui-lock/macos/40x12/streaming.png differ diff --git a/docs/media/tui-lock/macos/40x12/sudo.png b/docs/media/tui-lock/macos/40x12/sudo.png index c16b690..64bcf46 100644 Binary files a/docs/media/tui-lock/macos/40x12/sudo.png and b/docs/media/tui-lock/macos/40x12/sudo.png differ diff --git a/docs/media/tui-lock/macos/40x12/task.png b/docs/media/tui-lock/macos/40x12/task.png index af6bd7f..4a4933a 100644 Binary files a/docs/media/tui-lock/macos/40x12/task.png and b/docs/media/tui-lock/macos/40x12/task.png differ diff --git a/docs/media/tui-lock/macos/40x12/thinking.png b/docs/media/tui-lock/macos/40x12/thinking.png index 5aff1e6..632dc38 100644 Binary files a/docs/media/tui-lock/macos/40x12/thinking.png and b/docs/media/tui-lock/macos/40x12/thinking.png differ diff --git a/docs/media/tui-lock/macos/40x12/todos.png b/docs/media/tui-lock/macos/40x12/todos.png index ba627d5..e6f8d8c 100644 Binary files a/docs/media/tui-lock/macos/40x12/todos.png and b/docs/media/tui-lock/macos/40x12/todos.png differ diff --git a/docs/media/tui-lock/macos/40x12/tool_tiles.png b/docs/media/tui-lock/macos/40x12/tool_tiles.png index 373fea8..4d924fa 100644 Binary files a/docs/media/tui-lock/macos/40x12/tool_tiles.png and b/docs/media/tui-lock/macos/40x12/tool_tiles.png differ diff --git a/docs/media/tui-lock/macos/40x12/typing.png b/docs/media/tui-lock/macos/40x12/typing.png index d3fd893..8d4f7ff 100644 Binary files a/docs/media/tui-lock/macos/40x12/typing.png and b/docs/media/tui-lock/macos/40x12/typing.png differ diff --git a/docs/media/tui-lock/macos/40x12/usage.png b/docs/media/tui-lock/macos/40x12/usage.png index e5fa345..7808797 100644 Binary files a/docs/media/tui-lock/macos/40x12/usage.png and b/docs/media/tui-lock/macos/40x12/usage.png differ diff --git a/docs/media/tui-lock/macos/40x12/working.png b/docs/media/tui-lock/macos/40x12/working.png index 7b4e3ee..bed8842 100644 Binary files a/docs/media/tui-lock/macos/40x12/working.png and b/docs/media/tui-lock/macos/40x12/working.png differ diff --git a/docs/media/tui-lock/macos/40x12/write.png b/docs/media/tui-lock/macos/40x12/write.png index 935c118..3713260 100644 Binary files a/docs/media/tui-lock/macos/40x12/write.png and b/docs/media/tui-lock/macos/40x12/write.png differ diff --git a/docs/reference/cli.md b/docs/reference/cli.md index 7b353e1..96840b2 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -32,6 +32,7 @@ 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`. | | `--max-agent-threads ` | Concurrent agent threads | | `--max-tool-threads ` | Concurrent tool executions | | `--command-timeout ` | Shell command timeout | diff --git a/scripts/render-tui-lock.sh b/scripts/render-tui-lock.sh index e61f885..110b7c4 100755 --- a/scripts/render-tui-lock.sh +++ b/scripts/render-tui-lock.sh @@ -100,12 +100,13 @@ gutter and monochrome (bold-keyword) highlighting, nested bullets indent hunks with a dim old/new gutter, white context, red `-`, green `+`, and word-level colour on a changed line. -Splash copy in these frames is the lock line `Cortex CLI v1.0.0`. The shipped -binary still reports the crate version. +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. | File | Surface | |------|---------| -| `splash.png` | Launch splash: cwd, `> cortex`, `Cortex CLI v1.0.0`, `/ commands` legend, hairline composer | +| `splash.png` | Empty-session splash: welcome + version hints, hairline composer | | `typing.png` | Prompt typed in the composer, block cursor | | `model_compact.png` | `/model` compact picker, hairline search field | | `model_full.png` | `/model` full picker: search field, descriptions, effort, billing note | @@ -128,7 +129,7 @@ binary still reports the crate version. | `interrupt.png` | Interrupt — tiles stay on screen, `× Stopped` in error red (same board as `stopped.png`: the UI after ctrl+c) | | `quota.png` | Agent quota exhausted (title in error red) | | `clear.png` | `/clear` confirm (same board as `clear_confirm.png`) | -| `session_empty.png` | Open session without splash title/legend: cwd, dual-bar composer, footer | +| `session_empty.png` | After first user turn: composer + footer only (no welcome) | | `session_loading.png` | Loading / streaming | | `session_error.png` | Error | | `session_success.png` | Success | diff --git a/src/cortex-cli/src/cli/args.rs b/src/cortex-cli/src/cli/args.rs index 641d63a..ee9a7b7 100644 --- a/src/cortex-cli/src/cli/args.rs +++ b/src/cortex-cli/src/cli/args.rs @@ -208,6 +208,16 @@ 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`. + #[arg( + long = "alternate-screen", + default_value_t = false, + help_heading = "Features" + )] + pub alternate_screen: bool, + /// Maximum number of concurrent agent threads #[arg( long = "max-agent-threads", @@ -1183,6 +1193,18 @@ mod tests { assert!(cli.interactive.debug); } + #[test] + fn test_cli_alternate_screen_flag_defaults_off() { + 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)" + ); + let cli = Cli::try_parse_from(["cortex", "--alternate-screen"]) + .expect("should parse --alternate-screen"); + assert!(cli.interactive.alternate_screen); + } + #[test] fn test_cli_prompt_trailing() { let cli = Cli::try_parse_from(["cortex", "write", "a", "unit", "test"]) diff --git a/src/cortex-cli/src/cli/handlers.rs b/src/cortex-cli/src/cli/handlers.rs index e92fa86..5860af9 100644 --- a/src/cortex-cli/src/cli/handlers.rs +++ b/src/cortex-cli/src/cli/handlers.rs @@ -96,17 +96,26 @@ async fn run_tui(args: InteractiveArgs) -> Result<()> { ); } - let mut config = cortex_engine::Config::default(); + if args.model.as_ref().is_some_and(|m| m.trim().is_empty()) { + bail!( + "Error: Model name cannot be empty. Please provide a valid model name \ + (e.g., 'gpt-4', 'claude-sonnet-4-20250514')." + ); + } - // Apply model override if specified - if let Some(ref model) = args.model { - if model.trim().is_empty() { - bail!( - "Error: Model name cannot be empty. Please provide a valid model name \ - (e.g., 'gpt-4', 'claude-sonnet-4-20250514')." - ); - } - config.model = resolve_model_alias(model).to_string(); + let mut config = cortex_engine::Config::load_sync(cortex_engine::ConfigOverrides { + model: args + .model + .as_ref() + .map(|m| resolve_model_alias(m).to_string()), + cwd: args.cwd.clone(), + alternate_screen: args.alternate_screen.then_some(true), + ..Default::default() + }) + .unwrap_or_else(|_| cortex_engine::Config::default()); + + if args.alternate_screen { + config.alternate_screen = true; } // Apply working directory override if specified diff --git a/src/cortex-cli/src/mcp_cmd/debug.rs b/src/cortex-cli/src/mcp_cmd/debug.rs index c50d3bf..118c0c3 100644 --- a/src/cortex-cli/src/mcp_cmd/debug.rs +++ b/src/cortex-cli/src/mcp_cmd/debug.rs @@ -10,7 +10,7 @@ use std::io::Write; use super::auth::get_auth_status_for_display; use super::config::get_mcp_server; use super::macros::safe_println; -use super::types::DebugArgs; +use super::types::{DebugArgs, ToolsArgs}; use super::validation::validate_server_name; /// Server info returned from connection tests. @@ -378,6 +378,80 @@ pub(crate) async fn run_debug(args: DebugArgs) -> Result<()> { Ok(()) } +/// List tools from a configured MCP server. +pub(crate) async fn run_tools(args: ToolsArgs) -> Result<()> { + let ToolsArgs { + name, + json, + timeout, + } = args; + validate_server_name(&name)?; + + let server = get_mcp_server(&name)? + .ok_or_else(|| anyhow::anyhow!("No MCP server named '{}' found", name))?; + + let transport = server + .get("transport") + .ok_or_else(|| anyhow::anyhow!("MCP server '{name}' has no transport configured"))?; + let transport_type = transport + .get("type") + .and_then(|v| v.as_str()) + .unwrap_or("unknown"); + + let info = match transport_type { + "stdio" => { + let cmd = transport + .get("command") + .and_then(|v| v.as_str()) + .unwrap_or("?"); + let args = transport + .get("args") + .and_then(|v| v.as_array()) + .map(|arr| { + arr.iter() + .filter_map(|v| v.as_str()) + .collect::>() + .join(" ") + }) + .unwrap_or_default(); + test_stdio_connection(&name, cmd, &args, timeout).await? + } + "http" | "sse" | "streamable_http" | "streamable-http" => { + let url = transport + .get("url") + .and_then(|v| v.as_str()) + .ok_or_else(|| anyhow::anyhow!("MCP server '{name}' has no URL"))?; + test_http_connection(&name, url, timeout).await? + } + other => bail!("Unsupported MCP transport '{other}' for tools listing"), + }; + + if json { + println!("{}", serde_json::to_string_pretty(&info.tools)?); + return Ok(()); + } + + let tools = info.tools.as_array().cloned().unwrap_or_default(); + if tools.is_empty() { + println!("No tools reported by MCP server '{name}'."); + return Ok(()); + } + println!("Tools from '{name}' ({})", tools.len()); + for tool in tools { + let tool_name = tool.get("name").and_then(|v| v.as_str()).unwrap_or("?"); + let desc = tool + .get("description") + .and_then(|v| v.as_str()) + .unwrap_or(""); + if desc.is_empty() { + println!(" • {tool_name}"); + } else { + println!(" • {tool_name}: {desc}"); + } + } + Ok(()) +} + /// Test connection to a stdio MCP server. async fn test_stdio_connection( _name: &str, diff --git a/src/cortex-cli/src/mcp_cmd/handlers.rs b/src/cortex-cli/src/mcp_cmd/handlers.rs index a2d9189..43e1047 100644 --- a/src/cortex-cli/src/mcp_cmd/handlers.rs +++ b/src/cortex-cli/src/mcp_cmd/handlers.rs @@ -437,6 +437,7 @@ command = "{command_bin_escaped}" Some(AddMcpStreamableHttpArgs { url, bearer_token_env_var, + headers, }), .. } => { @@ -464,6 +465,15 @@ url = "{url_escaped}" if let Some(ref token_var) = bearer_token_env_var { toml.push_str(&format!("bearer_token_env_var = \"{token_var}\"\n")); } + if !headers.is_empty() { + toml.push_str("[mcp_servers."); + toml.push_str(&name); + toml.push_str(".transport.headers]\n"); + for (key, value) in &headers { + let escaped_value = value.replace('\\', "\\\\").replace('"', "\\\""); + toml.push_str(&format!("{key} = \"{escaped_value}\"\n")); + } + } // Build success message but defer printing until config write succeeds let mut success_msg = format!("✓ Added HTTP MCP server '{name}'\n"); @@ -918,6 +928,7 @@ mod tests { streamable_http: Some(AddMcpStreamableHttpArgs { url: "https://example.com/mcp".to_string(), bearer_token_env_var: None, + headers: vec![], }), sse_transport: None, }, @@ -937,6 +948,7 @@ mod tests { streamable_http: Some(AddMcpStreamableHttpArgs { url: "http://localhost:8080/mcp".to_string(), bearer_token_env_var: None, + headers: vec![], }), sse_transport: None, }, @@ -956,6 +968,7 @@ mod tests { let args = AddMcpStreamableHttpArgs { url: "https://api.example.com/mcp".to_string(), bearer_token_env_var: None, + headers: vec![], }; assert_eq!(args.url, "https://api.example.com/mcp"); assert!(args.bearer_token_env_var.is_none()); @@ -966,6 +979,7 @@ mod tests { let args = AddMcpStreamableHttpArgs { url: "https://api.example.com/mcp".to_string(), bearer_token_env_var: Some("MY_API_TOKEN".to_string()), + headers: vec![], }; assert_eq!(args.bearer_token_env_var, Some("MY_API_TOKEN".to_string())); } @@ -1056,6 +1070,7 @@ mod tests { streamable_http: Some(AddMcpStreamableHttpArgs { url: "https://example.com".to_string(), bearer_token_env_var: None, + headers: vec![], }), sse_transport: None, }; diff --git a/src/cortex-cli/src/mcp_cmd/mod.rs b/src/cortex-cli/src/mcp_cmd/mod.rs index dcad239..4a8360b 100644 --- a/src/cortex-cli/src/mcp_cmd/mod.rs +++ b/src/cortex-cli/src/mcp_cmd/mod.rs @@ -39,6 +39,7 @@ impl McpCli { McpSubcommand::Auth(cmd) => auth::run_auth_command(cmd).await, McpSubcommand::Logout(args) => auth::run_logout(args).await, McpSubcommand::Debug(args) => debug::run_debug(args).await, + McpSubcommand::Tools(args) => debug::run_tools(args).await, } } } diff --git a/src/cortex-cli/src/mcp_cmd/types.rs b/src/cortex-cli/src/mcp_cmd/types.rs index 47f105b..e3aed52 100644 --- a/src/cortex-cli/src/mcp_cmd/types.rs +++ b/src/cortex-cli/src/mcp_cmd/types.rs @@ -51,6 +51,9 @@ pub enum McpSubcommand { /// Debug and test an MCP server connection. Debug(DebugArgs), + + /// List tools exposed by a configured MCP server. + Tools(ToolsArgs), } /// Arguments for list command. @@ -157,6 +160,11 @@ pub struct AddMcpStreamableHttpArgs { requires = "url" )] pub bearer_token_env_var: Option, + + /// Extra HTTP headers (`KEY=VALUE`). Repeatable. Do not put secrets in + /// the value — prefer `env:VAR` style refs or `--bearer-token-env-var`. + #[arg(long = "header", value_parser = parse_env_pair, value_name = "KEY=VALUE")] + pub headers: Vec<(String, String)>, } /// SSE (Server-Sent Events) transport arguments. @@ -287,6 +295,21 @@ pub struct DebugArgs { pub show_cache_info: bool, } +/// Arguments for the tools listing command. +#[derive(Debug, Parser)] +pub struct ToolsArgs { + /// Name of the MCP server whose tools to list. + pub name: String, + + /// Output as JSON. + #[arg(long)] + pub json: bool, + + /// Timeout in seconds for the connection. + #[arg(long, default_value = "30")] + pub timeout: u64, +} + #[cfg(test)] mod tests { use super::*; @@ -455,6 +478,7 @@ mod tests { let http = AddMcpStreamableHttpArgs { url: "https://api.example.com".to_string(), bearer_token_env_var: None, + headers: vec![], }; let args = AddMcpTransportArgs { stdio: None, @@ -541,6 +565,7 @@ mod tests { let args = AddMcpStreamableHttpArgs { url: "https://example.com/mcp".to_string(), bearer_token_env_var: None, + headers: vec![], }; assert_eq!(args.url, "https://example.com/mcp"); assert!(args.bearer_token_env_var.is_none()); @@ -551,6 +576,7 @@ mod tests { let args = AddMcpStreamableHttpArgs { url: "https://api.example.com".to_string(), bearer_token_env_var: Some("MY_TOKEN".to_string()), + headers: vec![], }; assert_eq!(args.url, "https://api.example.com"); assert_eq!(args.bearer_token_env_var, Some("MY_TOKEN".to_string())); diff --git a/src/cortex-cli/src/plugin_cmd.rs b/src/cortex-cli/src/plugin_cmd.rs index e3f929c..ac75014 100644 --- a/src/cortex-cli/src/plugin_cmd.rs +++ b/src/cortex-cli/src/plugin_cmd.rs @@ -623,6 +623,15 @@ pub enum PluginSubcommand { /// Prepare plugin for publication (dry-run) Publish(PluginPublishArgs), + + /// Search the plugin registry + Search(PluginSearchArgs), + + /// Browse plugins in the registry + Browse(PluginBrowseArgs), + + /// Update an installed plugin from the registry + Update(PluginUpdateArgs), } /// Arguments for plugin list command. @@ -783,6 +792,32 @@ pub struct PluginPublishArgs { pub output: Option, } +/// Arguments for plugin search. +#[derive(Debug, Parser)] +pub struct PluginSearchArgs { + /// Search query + pub query: String, + + /// Output as JSON + #[arg(long)] + pub json: bool, +} + +/// Arguments for plugin browse. +#[derive(Debug, Parser)] +pub struct PluginBrowseArgs { + /// Output as JSON + #[arg(long)] + pub json: bool, +} + +/// Arguments for plugin update. +#[derive(Debug, Parser)] +pub struct PluginUpdateArgs { + /// Plugin name to update + pub name: String, +} + /// Plugin information for display. #[derive(Debug, Serialize)] struct PluginInfo { @@ -1049,6 +1084,9 @@ impl PluginCli { PluginSubcommand::Build(args) => run_build(args).await, PluginSubcommand::Validate(args) => run_validate(args).await, PluginSubcommand::Publish(args) => run_publish(args).await, + PluginSubcommand::Search(args) => run_search(args).await, + PluginSubcommand::Browse(args) => run_browse(args).await, + PluginSubcommand::Update(args) => run_update(args).await, } } } @@ -1186,36 +1224,16 @@ async fn run_install(args: PluginInstallArgs) -> Result<()> { println!(" Version: {}", version); } - // For now, we support local directory installation or create a placeholder - // In a full implementation, this would fetch from a plugin registry if std::path::Path::new(&args.name).exists() { - // Install from local path let src_path = std::path::Path::new(&args.name); if src_path.is_dir() { - // Copy directory copy_dir_recursive(src_path, &plugin_path)?; println!("Plugin installed from local directory."); } else { bail!("Source path is not a directory: {}", args.name); } } else { - // Create placeholder plugin structure - std::fs::create_dir_all(&plugin_path)?; - - let version = args.version.as_deref().unwrap_or("1.0.0"); - let manifest = format!( - r#"# Plugin manifest -name = "{}" -version = "{}" -description = "Placeholder plugin - replace with actual implementation" -enabled = true -"#, - args.name, version - ); - - std::fs::write(plugin_path.join("plugin.toml"), manifest)?; - println!("Created placeholder plugin structure."); - println!("Edit {} to configure your plugin.", plugin_path.display()); + fetch_and_install_from_registry(&args.name, args.version.as_deref(), &plugin_path).await?; } println!("Plugin '{}' installed successfully.", args.name); @@ -1240,6 +1258,165 @@ fn copy_dir_recursive(src: &std::path::Path, dst: &std::path::Path) -> Result<() Ok(()) } +const DEFAULT_PLUGIN_REGISTRY: &str = "https://software.cortex.foundation/plugins"; + +fn plugin_registry_base() -> String { + std::env::var("CORTEX_PLUGIN_REGISTRY") + .unwrap_or_else(|_| DEFAULT_PLUGIN_REGISTRY.to_string()) + .trim_end_matches('/') + .to_string() +} + +async fn registry_get(path_and_query: &str) -> Result { + let url = format!("{}{path_and_query}", plugin_registry_base()); + let client = reqwest::Client::builder() + .timeout(Duration::from_secs(30)) + .build() + .context("Failed to create HTTP client")?; + client + .get(&url) + .send() + .await + .map_err(|_| anyhow::anyhow!("The plugin registry is temporarily unavailable")) +} + +fn print_registry_plugins(value: &serde_json::Value, json: bool) -> Result<()> { + if json { + println!("{}", serde_json::to_string_pretty(value)?); + return Ok(()); + } + let plugins = if let Some(arr) = value.as_array() { + arr.clone() + } else if let Some(arr) = value.get("plugins").and_then(|v| v.as_array()) { + arr.clone() + } else { + Vec::new() + }; + if plugins.is_empty() { + println!("No plugins found."); + return Ok(()); + } + for plugin in plugins { + let name = plugin.get("name").and_then(|v| v.as_str()).unwrap_or("?"); + let version = plugin + .get("version") + .and_then(|v| v.as_str()) + .unwrap_or("-"); + let description = plugin + .get("description") + .and_then(|v| v.as_str()) + .unwrap_or(""); + if description.is_empty() { + println!("{name} {version}"); + } else { + println!("{name} {version} {description}"); + } + } + Ok(()) +} + +async fn run_search(args: PluginSearchArgs) -> Result<()> { + let q = urlencoding_query(&args.query); + let response = registry_get(&format!("/search?q={q}")).await?; + if !response.status().is_success() { + bail!("The plugin registry is temporarily unavailable"); + } + let value: serde_json::Value = response + .json() + .await + .map_err(|_| anyhow::anyhow!("The plugin registry is temporarily unavailable"))?; + print_registry_plugins(&value, args.json) +} + +async fn run_browse(args: PluginBrowseArgs) -> Result<()> { + let response = registry_get("").await?; + if !response.status().is_success() { + bail!("The plugin registry is temporarily unavailable"); + } + let value: serde_json::Value = response + .json() + .await + .map_err(|_| anyhow::anyhow!("The plugin registry is temporarily unavailable"))?; + print_registry_plugins(&value, args.json) +} + +async fn run_update(args: PluginUpdateArgs) -> Result<()> { + run_install(PluginInstallArgs { + name: args.name, + version: None, + force: true, + }) + .await +} + +fn urlencoding_query(raw: &str) -> String { + let mut out = String::new(); + for b in raw.bytes() { + match b { + b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => { + out.push(b as char); + } + _ => out.push_str(&format!("%{b:02X}")), + } + } + out +} + +async fn fetch_and_install_from_registry( + name: &str, + version: Option<&str>, + dest: &Path, +) -> Result<()> { + let path = if let Some(version) = version { + format!("/{name}?version={version}") + } else { + format!("/{name}") + }; + let response = registry_get(&path).await?; + if response.status().as_u16() == 404 { + bail!("Plugin '{name}' was not found in the plugin registry."); + } + if !response.status().is_success() { + bail!("The plugin registry is temporarily unavailable"); + } + let listing: serde_json::Value = response + .json() + .await + .map_err(|_| anyhow::anyhow!("The plugin registry is temporarily unavailable"))?; + let tarball = listing + .get("tarball_url") + .or_else(|| listing.get("package_url")) + .and_then(|v| v.as_str()) + .ok_or_else(|| { + anyhow::anyhow!("Plugin '{name}' has no installable package in the registry.") + })?; + + let client = reqwest::Client::builder() + .timeout(Duration::from_secs(60)) + .build() + .context("Failed to create HTTP client")?; + let bytes = client + .get(tarball) + .send() + .await + .map_err(|_| anyhow::anyhow!("The plugin registry is temporarily unavailable"))? + .bytes() + .await + .map_err(|_| anyhow::anyhow!("The plugin registry is temporarily unavailable"))?; + + if dest.exists() { + std::fs::remove_dir_all(dest)?; + } + std::fs::create_dir_all(dest)?; + let decoder = flate2::read::GzDecoder::new(std::io::Cursor::new(bytes)); + let mut archive = tar::Archive::new(decoder); + archive + .unpack(dest) + .context("Failed to unpack plugin archive")?; + println!("Installed '{name}' from the plugin registry."); + Ok(()) +} + async fn run_remove(args: PluginRemoveArgs) -> Result<()> { let plugins_dir = get_plugins_dir(); let plugin_path = plugins_dir.join(&args.name); @@ -4293,8 +4470,9 @@ mod tests { let cmd = PluginCli::command(); let subcommand_count = cmd.get_subcommands().count(); - // Expected: list, install, remove, enable, disable, show, new, dev, build, validate, publish = 11 - assert_eq!(subcommand_count, 11, "PluginCli should have 11 subcommands"); + // list, install, remove, enable, disable, show, new, dev, build, + // validate, publish, search, browse, update + assert_eq!(subcommand_count, 14, "PluginCli should have 14 subcommands"); } // ========================================================================== diff --git a/src/cortex-core/src/style.rs b/src/cortex-core/src/style.rs index e785ece..0e5cb96 100644 --- a/src/cortex-core/src/style.rs +++ b/src/cortex-core/src/style.rs @@ -4,10 +4,10 @@ //! (`Color::Reset`, black by default). Structure comes from gray — hairlines, //! filled charcoal panels, dim secondary copy, white primary copy. The one //! accent is the Cortex violet `#A78BFA`, reserved for the focused selection: -//! the `>` caret and the selected label on the dark gray selection bar (the -//! bar itself stays gray — never a violet wash). Green exists only for `✓` -//! success and `+` diff additions; red and amber only on diagnostics; a muted -//! gold only on the Thinking status. +//! the `>` caret and the selected label on the `#221A38` selection bar. +//! Green exists only for `✓` success and `+` diff additions; red and amber +//! only on diagnostics; a muted gold only on the Thinking status. Mint +//! `#00F5D4` / `#1A3330` is never painted. use ratatui::style::{Color, Modifier, Style}; @@ -102,10 +102,10 @@ pub const INFO: Color = DEEP_CYAN; // #9CA3AF /// Highlight - near-white for emphasis (legacy name) pub const HIGHLIGHT: Color = ELECTRIC_BLUE; // #E5E7EB -/// Selected-row background - dark gray bar; the caret and label on it are -/// violet, everything else stays white/dim. Never inverted onto the accent, -/// never the old `#221A38` violet wash. -pub const SELECTION_BG: Color = SURFACE_2; // #262626 +/// Selected-row background — violet-tinted bar `#221A38`. The caret and +/// label on it are `#A78BFA`; everything else stays white/dim. Never invert +/// onto the accent. +pub const SELECTION_BG: Color = Color::Rgb(34, 26, 56); // #221A38 // ============================================================ // BORDER COLORS @@ -658,18 +658,16 @@ mod tests { assert_eq!(VOID, Color::Reset); assert_eq!(PANEL_BG, Color::Rgb(0x14, 0x14, 0x14)); assert_eq!(TEXT_DIM, Color::Rgb(0x6B, 0x72, 0x80)); - for gray in [HAIRLINE, SELECTION_BG, USER_TURN_BG, BORDER_FOCUS] { + for gray in [HAIRLINE, USER_TURN_BG, BORDER_FOCUS] { let Color::Rgb(r, g, b) = gray else { panic!("{gray:?} must be an RGB gray"); }; assert!(r == g && g == b, "{gray:?} is not neutral"); } - // The violet lives in `ACCENT` alone: no other semantic slot carries - // it, the old `#221A38` wash and the mint are gone, and the interim - // violet highlight is banned. + assert_eq!(SELECTION_BG, Color::Rgb(0x22, 0x1A, 0x38)); + // The violet lives in `ACCENT` plus the selection bar. Mint is banned. for color in [ SUCCESS, - SELECTION_BG, BORDER_FOCUS, INFO, HIGHLIGHT, @@ -679,7 +677,6 @@ mod tests { TEAL, ] { assert_ne!(color, ACCENT, "violet leaked off the accent"); - assert_ne!(color, Color::Rgb(0x22, 0x1A, 0x38), "violet bar leaked"); assert_ne!(color, Color::Rgb(0x00, 0xF5, 0xD4), "mint leaked"); assert_ne!(color, Color::Rgb(0x7D, 0xD3, 0xFC), "cyan leaked"); } @@ -688,11 +685,6 @@ mod tests { Color::Rgb(0x7D, 0xD3, 0xFC), "the accent is not cyan" ); - assert_ne!( - SELECTION_BG, - Color::Rgb(0x22, 0x1A, 0x38), - "the bar is gray" - ); // Thinking is the only gold. assert_eq!(CortexStyle::thinking().fg, Some(THINKING)); assert_ne!(THINKING, WARNING); diff --git a/src/cortex-engine/src/client/code_agent.rs b/src/cortex-engine/src/client/code_agent.rs index 7b665a1..8ccd377 100644 --- a/src/cortex-engine/src/client/code_agent.rs +++ b/src/cortex-engine/src/client/code_agent.rs @@ -43,6 +43,21 @@ const DEFAULT_CORTEX_URL: &str = "https://api.cortex.foundation"; const CHUNK_TIMEOUT_SECS: u64 = 60; const GUEST_COOKIE_NAME: &str = "cortex_gt"; +/// Product-facing copy for a true outage / unreachable API. +pub const SERVICE_UNAVAILABLE: &str = "The coding service is temporarily unavailable"; +/// Auth failures: tell the user how to recover. Never dump tokens. +pub const AUTH_REQUIRED: &str = + "Not signed in. Run `cortex login` or set CORTEX_API_KEY, then try again."; +/// Wrong origin, missing route, or a session the API no longer has. +pub const ENDPOINT_NOT_FOUND: &str = "The coding endpoint was not found. Check CORTEX_API_URL (default https://api.cortex.foundation) or run `cortex login`."; +/// Quota / 429. +pub const TOO_MANY_REQUESTS: &str = "Too many requests. Please wait and try again."; + +/// Strip a trailing slash so `/v1/...` is not doubled as `//v1`. +pub fn normalize_api_base(url: &str) -> String { + url.trim().trim_end_matches('/').to_string() +} + /// Guest-cookie token prefix stored in the keyring / env. pub const GUEST_TOKEN_PREFIX: &str = "gt:"; @@ -325,9 +340,9 @@ impl CodeAgentClient { .unwrap_or_else(|_| Client::new()); Self { http, - base_url: base_url.unwrap_or_else(|| { + base_url: normalize_api_base(&base_url.unwrap_or_else(|| { std::env::var("CORTEX_API_URL").unwrap_or_else(|_| DEFAULT_CORTEX_URL.to_string()) - }), + })), auth: Arc::new(Mutex::new(auth)), session_id: Arc::new(Mutex::new(load_cached_session_id( &std::env::current_dir() @@ -576,6 +591,19 @@ impl CodeAgentClient { self.ensure_auth().await?; let ctx = self.turn_context(); let mode = ctx.turn_mode.unwrap_or(mode); + match self.post_turn(message, mode).await { + Ok(stream) => Ok(stream), + Err(e) if is_not_found_error(&e) => { + tracing::debug!("Code session turn returned 404; creating a new session"); + *self.session_id.lock().await = None; + self.post_turn(message, mode).await + } + Err(e) => Err(e), + } + } + + async fn post_turn(&self, message: &str, mode: CodeTurnMode) -> Result { + let ctx = self.turn_context(); let session_id = self.ensure_session().await?; let url = format!("{}/v1/code/sessions/{session_id}/turns", self.base_url); let body = serde_json::json!({ @@ -761,6 +789,10 @@ async fn parse_json Deserialize<'de>>(resp: reqwest::Response) -> Re }) } +fn is_not_found_error(err: &CortexError) -> bool { + err.to_string().contains("not found") +} + fn map_api_error(status: reqwest::StatusCode, body: &str) -> CortexError { let detail = serde_json::from_str::(body) .ok() @@ -769,15 +801,46 @@ fn map_api_error(status: reqwest::StatusCode, body: &str) -> CortexError { .and_then(|d| d.as_str()) .map(str::to_string) .or_else(|| v.get("title").and_then(|d| d.as_str()).map(str::to_string)) + .or_else(|| v.get("error").and_then(|d| d.as_str()).map(str::to_string)) }); - let message = match status.as_u16() { - 401 | 403 => "Sign in to continue, or start a guest session.".to_string(), - 404 => "The coding service is temporarily unavailable".to_string(), - 429 => "Too many requests. Please wait and try again.".to_string(), - 500..=599 => "The coding service is temporarily unavailable".to_string(), - _ => detail.unwrap_or_else(|| "The coding service is temporarily unavailable".to_string()), + match status.as_u16() { + 401 | 403 => CortexError::AuthenticationError { + message: AUTH_REQUIRED.to_string(), + }, + 404 => CortexError::BackendError { + message: ENDPOINT_NOT_FOUND.to_string(), + }, + 429 => CortexError::RateLimit(TOO_MANY_REQUESTS.to_string()), + 500..=599 => CortexError::BackendUnavailable(SERVICE_UNAVAILABLE.to_string()), + _ => CortexError::BackendError { + message: sanitize_api_detail(detail), + }, + } +} + +/// Keep operator/API detail when it is already product-safe; otherwise the +/// generic outage line. Never pass through SDK or transport names. +fn sanitize_api_detail(detail: Option) -> String { + let Some(raw) = detail.filter(|s| !s.trim().is_empty()) else { + return SERVICE_UNAVAILABLE.to_string(); }; - CortexError::BackendError { message } + let lower = raw.to_lowercase(); + if lower.contains("reqwest") + || lower.contains("hyper") + || lower.contains("openai") + || lower.contains("anthropic") + || lower.contains("tokio") + { + return SERVICE_UNAVAILABLE.to_string(); + } + if lower.contains("unauthorized") + || lower.contains("unauthenticated") + || lower.contains("invalid api key") + || lower.contains("invalid token") + { + return AUTH_REQUIRED.to_string(); + } + raw } async fn pump_sse(stream: S, tx: mpsc::Sender>, execute_local: bool) @@ -800,11 +863,7 @@ where Ok(Some(result)) => result, Ok(None) => break, Err(_) => { - let _ = tx - .send(Err(CortexError::BackendError { - message: "The coding service is temporarily unavailable".into(), - })) - .await; + let _ = tx.send(Err(CortexError::Timeout)).await; break; } }; @@ -926,11 +985,7 @@ where } else { message }; - let mapped = if raw.to_lowercase().contains("unavailable") || raw.is_empty() { - "The coding service is temporarily unavailable".to_string() - } else { - raw - }; + let mapped = map_sse_error_copy(&raw); Some(ResponseEvent::Error(mapped)) } CodeTurnEvent::Question { @@ -953,6 +1008,33 @@ where } } +fn map_sse_error_copy(raw: &str) -> String { + let trimmed = raw.trim(); + if trimmed.is_empty() { + return SERVICE_UNAVAILABLE.to_string(); + } + let lower = trimmed.to_lowercase(); + if lower.contains("unauthorized") + || lower.contains("unauthenticated") + || lower.contains("401") + || lower.contains("403") + || lower.contains("invalid api key") + || lower.contains("invalid token") + { + return AUTH_REQUIRED.to_string(); + } + if lower.contains("not found") || lower.contains("404") { + return ENDPOINT_NOT_FOUND.to_string(); + } + if lower.contains("429") || lower.contains("rate limit") || lower.contains("quota") { + return TOO_MANY_REQUESTS.to_string(); + } + if lower.contains("reqwest") || lower.contains("hyper") { + return SERVICE_UNAVAILABLE.to_string(); + } + trimmed.to_string() +} + #[cfg(test)] mod tests { use super::*; @@ -1043,8 +1125,11 @@ mod tests { #[test] fn api_errors_are_product_facing() { - let err = map_api_error(reqwest::StatusCode::NOT_FOUND, r#"{"detail":"nope"}"#); - let msg = err.to_string(); + let err = map_api_error( + reqwest::StatusCode::INTERNAL_SERVER_ERROR, + r#"{"detail":"nope"}"#, + ); + let msg = err.user_friendly_message(); assert!( msg.contains("temporarily unavailable"), "expected product error, got {msg}" @@ -1053,6 +1138,58 @@ mod tests { assert!(!msg.contains("nope")); } + #[test] + fn api_401_asks_the_user_to_login() { + let err = map_api_error(reqwest::StatusCode::UNAUTHORIZED, r#"{"detail":"nope"}"#); + let msg = err.user_friendly_message(); + assert!( + msg.contains("cortex login") || msg.contains("CORTEX_API_KEY"), + "{msg}" + ); + assert!(!msg.contains("temporarily unavailable"), "{msg}"); + assert!(!msg.contains("nope")); + } + + #[test] + fn api_404_is_not_a_false_outage() { + let err = map_api_error(reqwest::StatusCode::NOT_FOUND, r#"{"detail":"missing"}"#); + let msg = err.to_string(); + assert!( + msg.contains("not found") || msg.contains("CORTEX_API_URL"), + "{msg}" + ); + assert!(!msg.contains("temporarily unavailable"), "{msg}"); + assert!(!msg.contains("missing")); + } + + #[test] + fn backend_error_display_keeps_actionable_copy() { + let err = CortexError::BackendError { + message: AUTH_REQUIRED.to_string(), + }; + assert_eq!(err.to_string(), AUTH_REQUIRED); + assert_eq!(err.user_friendly_message(), AUTH_REQUIRED); + } + + #[test] + fn normalize_api_base_strips_trailing_slash() { + assert_eq!( + normalize_api_base("https://api.cortex.foundation/"), + "https://api.cortex.foundation" + ); + assert_eq!( + CodeAgentClient::new(Some("https://api.cortex.foundation/".into()), None).base_url(), + "https://api.cortex.foundation" + ); + } + + #[test] + fn sse_error_maps_auth_without_outage_copy() { + assert_eq!(map_sse_error_copy("unauthorized"), AUTH_REQUIRED); + assert_eq!(map_sse_error_copy(""), SERVICE_UNAVAILABLE); + assert_eq!(map_sse_error_copy("rate limit 429"), TOO_MANY_REQUESTS); + } + #[test] fn persists_session_id_under_cortex_home() { let dir = std::env::temp_dir().join(format!("cortex-sess-{}", uuid::Uuid::new_v4())); diff --git a/src/cortex-engine/src/client/mod.rs b/src/cortex-engine/src/client/mod.rs index b51a2c2..e5ab78f 100644 --- a/src/cortex-engine/src/client/mod.rs +++ b/src/cortex-engine/src/client/mod.rs @@ -91,9 +91,9 @@ fn get_auth_token() -> Result { /// /// All requests go through the Cortex backend with OAuth authentication. /// -/// # Legacy signature compatibility -/// The `_provider_id` and `_base_url` parameters are ignored - all requests -/// go through Cortex backend. The `api_key` parameter is used as the auth token. +/// # Named providers +/// `base_url` from `[providers.]` is passed through as `_base_url`. +/// Auth still uses the Cortex session / `CORTEX_API_KEY`. pub fn create_client( _provider_id: &str, model: &str, @@ -107,9 +107,12 @@ pub fn create_client( get_auth_token()? }; - let base_url = std::env::var("CORTEX_API_URL").ok(); + let resolved_url = _base_url + .filter(|url| !url.is_empty()) + .map(str::to_string) + .or_else(|| std::env::var("CORTEX_API_URL").ok()); Ok(Box::new( - CortexClient::new(model.to_string(), base_url).with_auth_token(auth_token), + CortexClient::new(model.to_string(), resolved_url).with_auth_token(auth_token), )) } diff --git a/src/cortex-engine/src/config/mod.rs b/src/cortex-engine/src/config/mod.rs index e4b9f65..199f850 100644 --- a/src/cortex-engine/src/config/mod.rs +++ b/src/cortex-engine/src/config/mod.rs @@ -77,8 +77,10 @@ pub struct Config { pub check_for_update_on_startup: bool, /// Disable paste burst detection. pub disable_paste_burst: bool, - /// Enable animations. + /// Enable TUI animations. pub animations: bool, + /// Opt in to the alternate screen buffer. Default is inline (`never`). + pub alternate_screen: bool, /// Current agent profile. pub current_agent: Option, /// Granular permission configuration. @@ -89,6 +91,8 @@ pub struct Config { /// Temperature for generation (0.0-2.0). /// CLI override takes precedence over agent default. pub temperature: Option, + /// Custom providers defined in config.toml. + pub providers: HashMap, /// Execution configuration for runtime behavior. pub execution: ExecutionConfig, } @@ -117,10 +121,12 @@ impl Default for Config { check_for_update_on_startup: true, disable_paste_burst: false, animations: true, + alternate_screen: false, current_agent: None, permission: PermissionConfig::default(), small_model: None, // Auto-detected based on available providers temperature: None, + providers: HashMap::new(), execution: ExecutionConfig::default(), } } @@ -213,10 +219,12 @@ impl Config { }) .unwrap_or_default(); + let model_provider = resolve_model_provider(&model_provider_id, &toml.providers); + Self { model, model_provider_id, - model_provider: ModelProviderInfo::default(), + model_provider, model_context_window: toml.model_context_window, model_auto_compact_token_limit: toml.model_auto_compact_token_limit, approval_policy, @@ -233,17 +241,74 @@ impl Config { show_raw_agent_reasoning: toml.show_raw_agent_reasoning.unwrap_or(false), check_for_update_on_startup: toml.check_for_update_on_startup.unwrap_or(true), disable_paste_burst: toml.disable_paste_burst.unwrap_or(false), - animations: toml.tui.map(|t| t.animations).unwrap_or(true), + animations: toml.tui.as_ref().map(|t| t.animations).unwrap_or(true), + alternate_screen: overrides.alternate_screen.unwrap_or_else(|| { + toml.tui + .as_ref() + .map(|t| t.alternate_screen) + .unwrap_or(false) + }), current_agent: toml.current_agent, permission: toml.permission, small_model: toml.small_model, // CLI temperature override takes precedence temperature: overrides.temperature, + providers: toml.providers, execution: toml.execution, } } } +fn resolve_model_provider( + id: &str, + providers: &HashMap, +) -> ModelProviderInfo { + if let Some(custom) = providers.get(id) { + return ModelProviderInfo { + id: id.to_string(), + name: if custom.name.is_empty() { + id.to_string() + } else { + custom.name.clone() + }, + base_url: custom.base_url.clone(), + api_type: ApiType::from_config(&custom.api_type), + api_key_env: custom.api_key_env.clone(), + }; + } + match id { + "anthropic" | "anthropic-compatible" => ModelProviderInfo { + id: id.to_string(), + name: "Anthropic-compatible".into(), + base_url: "https://api.anthropic.com".into(), + api_type: ApiType::Anthropic, + api_key_env: Some("ANTHROPIC_API_KEY".into()), + }, + "azure" => ModelProviderInfo { + id: id.to_string(), + name: "Azure".into(), + base_url: String::new(), + api_type: ApiType::Azure, + api_key_env: Some("AZURE_OPENAI_API_KEY".into()), + }, + "local" | "ollama" => ModelProviderInfo { + id: id.to_string(), + name: "Local".into(), + base_url: "http://127.0.0.1:11434/v1".into(), + api_type: ApiType::Local, + api_key_env: None, + }, + "openai" | "openai-compatible" => ModelProviderInfo { + id: id.to_string(), + name: "OpenAI-compatible".into(), + base_url: "https://api.openai.com/v1".into(), + api_type: ApiType::OpenAiCompatible, + api_key_env: Some("OPENAI_API_KEY".into()), + }, + _ => ModelProviderInfo::default(), + } +} + /// Configuration overrides from CLI. #[derive(Debug, Clone, Default)] pub struct ConfigOverrides { @@ -255,4 +320,47 @@ 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. + pub alternate_screen: Option, +} + +#[cfg(test)] +mod alternate_screen_tests { + use super::*; + use types::ConfigToml; + + #[test] + fn from_toml_keeps_alternate_screen_off_by_default() { + assert!( + !Config::default().alternate_screen, + "default must be inline (never alt-screen)" + ); + 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)" + ); + } + + #[test] + fn from_toml_honors_tui_alternate_screen_opt_in() { + let toml: ConfigToml = toml::from_str("[tui]\nalternate_screen = true\n").unwrap(); + let cfg = Config::from_toml(toml, ConfigOverrides::default(), PathBuf::from("/tmp")); + assert!(cfg.alternate_screen); + } + + #[test] + fn cli_override_can_force_alternate_screen() { + let toml: ConfigToml = toml::from_str("[tui]\nalternate_screen = false\n").unwrap(); + let cfg = Config::from_toml( + toml, + ConfigOverrides { + alternate_screen: Some(true), + ..Default::default() + }, + PathBuf::from("/tmp"), + ); + assert!(cfg.alternate_screen); + } } diff --git a/src/cortex-engine/src/config/project_config.rs b/src/cortex-engine/src/config/project_config.rs index 917c22f..4cfdee8 100644 --- a/src/cortex-engine/src/config/project_config.rs +++ b/src/cortex-engine/src/config/project_config.rs @@ -322,9 +322,7 @@ mod tests { "server1".to_string(), McpServerConfig { command: "cmd1".to_string(), - args: vec![], - env: HashMap::new(), - timeout_seconds: None, + ..Default::default() }, ); @@ -333,9 +331,7 @@ mod tests { "server2".to_string(), McpServerConfig { command: "cmd2".to_string(), - args: vec![], - env: HashMap::new(), - timeout_seconds: None, + ..Default::default() }, ); diff --git a/src/cortex-engine/src/config/types.rs b/src/cortex-engine/src/config/types.rs index 3e5e4fe..5b24b6d 100644 --- a/src/cortex-engine/src/config/types.rs +++ b/src/cortex-engine/src/config/types.rs @@ -158,9 +158,15 @@ pub struct SandboxWorkspaceWrite { pub exclude_slash_tmp: bool, } -/// MCP server configuration. -#[derive(Debug, Clone, Deserialize)] +/// MCP server configuration as stored in `config.toml`. +/// +/// Accepts both the nested transport form written by `cortex mcp add` +/// and a legacy flat `{ command, args, env }` table. +#[derive(Debug, Clone, Default, Deserialize)] pub struct McpServerConfig { + #[serde(default = "default_mcp_enabled")] + pub enabled: bool, + #[serde(default)] pub command: String, #[serde(default)] pub args: Vec, @@ -168,6 +174,114 @@ pub struct McpServerConfig { pub env: HashMap, #[serde(default)] pub timeout_seconds: Option, + #[serde(default)] + pub transport: Option, + #[serde(default)] + pub url: Option, + #[serde(default)] + pub headers: HashMap, + #[serde(default)] + pub bearer_token_env_var: Option, + #[serde(default)] + pub cwd: Option, +} + +fn default_mcp_enabled() -> bool { + true +} + +/// Nested transport block under `[mcp_servers..transport]`. +#[derive(Debug, Clone, Deserialize)] +pub struct McpTransportSpec { + #[serde(rename = "type", default = "default_transport_type")] + pub transport_type: String, + #[serde(default)] + pub command: Option, + #[serde(default)] + pub args: Vec, + #[serde(default)] + pub env: HashMap, + #[serde(default)] + pub url: Option, + #[serde(default)] + pub headers: HashMap, + #[serde(default)] + pub bearer_token_env_var: Option, + #[serde(default)] + pub cwd: Option, +} + +fn default_transport_type() -> String { + "stdio".to_string() +} + +impl McpServerConfig { + /// Convert this config entry into the engine connection config. + pub fn to_engine(&self, name: &str) -> crate::mcp::McpServerConfig { + use crate::mcp::TransportType; + + let (kind, command, args, env, url, headers, bearer, cwd) = if let Some(t) = &self.transport + { + ( + t.transport_type.as_str(), + t.command.clone().unwrap_or_else(|| self.command.clone()), + if t.args.is_empty() { + self.args.clone() + } else { + t.args.clone() + }, + if t.env.is_empty() { + self.env.clone() + } else { + t.env.clone() + }, + t.url.clone().or_else(|| self.url.clone()), + if t.headers.is_empty() { + self.headers.clone() + } else { + t.headers.clone() + }, + t.bearer_token_env_var + .clone() + .or_else(|| self.bearer_token_env_var.clone()), + t.cwd.clone().or_else(|| self.cwd.clone()), + ) + } else { + ( + if self.url.is_some() { "http" } else { "stdio" }, + self.command.clone(), + self.args.clone(), + self.env.clone(), + self.url.clone(), + self.headers.clone(), + self.bearer_token_env_var.clone(), + self.cwd.clone(), + ) + }; + + let transport = match kind { + "http" | "streamable_http" | "streamable-http" => TransportType::Http, + "sse" => TransportType::Sse, + "web_socket" | "websocket" => TransportType::WebSocket, + _ => TransportType::Stdio, + }; + + let mut cfg = if matches!(transport, TransportType::Http | TransportType::Sse) { + crate::mcp::McpServerConfig::new_sse(name, url.clone().unwrap_or_default()) + } else { + crate::mcp::McpServerConfig::new(name, command).args(args) + }; + cfg.env = env; + cfg.cwd = cwd; + cfg.transport = transport; + cfg.headers = headers; + cfg.bearer_token_env_var = bearer; + cfg.auto_start = self.enabled; + if let Some(url) = url { + cfg.sse_url = Some(url); + } + cfg + } } /// History configuration. @@ -209,20 +323,39 @@ pub enum ReasoningSummary { } /// TUI configuration. -#[derive(Debug, Clone, Default, Deserialize)] +#[derive(Debug, Clone, Deserialize)] 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. + #[serde(default = "default_alternate_screen")] + pub alternate_screen: bool, #[serde(default)] pub notifications: NotificationsConfig, #[serde(default)] pub theme: ThemeConfig, } +impl Default for TuiConfig { + fn default() -> Self { + Self { + animations: true, + alternate_screen: false, + notifications: NotificationsConfig::default(), + theme: ThemeConfig::default(), + } + } +} + fn default_animations() -> bool { true } +fn default_alternate_screen() -> bool { + false +} + /// Theme configuration. #[derive(Debug, Clone, Default, Deserialize, Serialize)] pub struct ThemeConfig { @@ -249,15 +382,18 @@ pub struct ModelProviderInfo { pub name: String, pub base_url: String, pub api_type: ApiType, + /// Environment variable that holds the API key. Never log its value. + pub api_key_env: Option, } impl Default for ModelProviderInfo { fn default() -> Self { Self { - id: "openai".to_string(), - name: "OpenAI".to_string(), - base_url: "https://api.openai.com/v1".to_string(), - api_type: ApiType::OpenAi, + id: "cortex".to_string(), + name: "Cortex".to_string(), + base_url: "https://api.cortex.foundation".to_string(), + api_type: ApiType::Cortex, + api_key_env: Some("CORTEX_API_KEY".to_string()), } } } @@ -266,9 +402,25 @@ impl Default for ModelProviderInfo { #[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] pub enum ApiType { #[default] + Cortex, OpenAi, Anthropic, OpenAiCompatible, + Azure, + Local, +} + +impl ApiType { + pub fn from_config(value: &str) -> Self { + match value.to_ascii_lowercase().as_str() { + "cortex" => Self::Cortex, + "anthropic" | "anthropic-compatible" => Self::Anthropic, + "azure" => Self::Azure, + "local" | "ollama" => Self::Local, + "openai" => Self::OpenAi, + _ => Self::OpenAiCompatible, + } + } } /// Feature flags. @@ -330,3 +482,26 @@ pub enum Feature { SandboxCommandAssessment, WindowsSandbox, } + +#[cfg(test)] +mod tui_alternate_screen_tests { + use super::*; + + #[test] + fn tui_alternate_screen_defaults_never() { + assert!( + !TuiConfig::default().alternate_screen, + "default must be inline (never alt-screen)" + ); + + 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); + + let parsed: ConfigToml = + toml::from_str("[tui]\nalternate_screen = true\n").expect("opt-in"); + assert!(parsed.tui.expect("tui").alternate_screen); + } +} diff --git a/src/cortex-engine/src/error.rs b/src/cortex-engine/src/error.rs index 267a9ab..087fd00 100644 --- a/src/cortex-engine/src/error.rs +++ b/src/cortex-engine/src/error.rs @@ -53,7 +53,9 @@ pub enum CortexError { #[error("The coding service is temporarily unavailable")] BackendUnavailable(String), - #[error("The coding service is temporarily unavailable")] + /// Product-facing API failure. `message` is shown to the user — never + /// put provider, SDK, or transport names in it. + #[error("{message}")] BackendError { message: String }, #[error("Authentication error: {message}")] @@ -312,7 +314,10 @@ impl CortexError { | Self::ApiKeyNotFound { .. } | Self::TokenExpired | Self::AuthenticationError { .. } - ) + ) || matches!(self, Self::BackendError { message } if { + let lower = message.to_lowercase(); + lower.contains("sign in") || lower.contains("cortex login") + }) } /// Check if this error is due to an invalid or expired API key. @@ -337,8 +342,16 @@ impl CortexError { } Self::BackendError { message } => { let lower = message.to_lowercase(); - (lower.contains("401") || lower.contains("unauthorized")) + let looks_like_bad_key = (lower.contains("invalid") + || lower.contains("expired") + || lower.contains("revoked")) + && (lower.contains("key") || lower.contains("token") || lower.contains("api")); + let looks_like_http_auth = (lower.contains("401") + || lower.contains("unauthorized")) && (lower.contains("api") || lower.contains("key") || lower.contains("token")) + && !lower.contains("sign in") + && !lower.contains("cortex login"); + looks_like_bad_key || looks_like_http_auth } _ => false, } @@ -351,6 +364,12 @@ impl CortexError { "Your Cortex API key may be invalid, expired, or revoked.\n\ Run 'cortex login' or set CORTEX_API_KEY, then try again." .to_string() + } else if let Self::BackendError { message } = self { + if is_transient_service_copy(message) { + SERVICE_UNAVAILABLE.to_string() + } else { + message.clone() + } } else if self.is_auth_error() { "Authentication required. Run 'cortex login' to authenticate with Cortex.".to_string() } else { @@ -359,7 +378,6 @@ impl CortexError { | Self::ConnectionFailed { .. } | Self::Timeout | Self::BackendUnavailable(_) - | Self::BackendError { .. } | Self::ProxyError { .. } => SERVICE_UNAVAILABLE.to_string(), Self::RateLimit(_) | Self::RateLimitWithRetryAfter { .. } @@ -373,6 +391,14 @@ impl CortexError { } } +/// True when `message` is the generic outage copy (or empty), not an +/// actionable auth / config error. +pub fn is_transient_service_copy(message: &str) -> bool { + let trimmed = message.trim(); + trimmed.is_empty() + || trimmed.eq_ignore_ascii_case("The coding service is temporarily unavailable") +} + /// Suggest a replacement model for deprecated models. fn suggest_model_replacement(model: &str, _raw_error: &str) -> String { // Common deprecated model replacements diff --git a/src/cortex-engine/src/mcp/client.rs b/src/cortex-engine/src/mcp/client.rs index 6ee0ce2..de88c37 100644 --- a/src/cortex-engine/src/mcp/client.rs +++ b/src/cortex-engine/src/mcp/client.rs @@ -419,13 +419,9 @@ impl McpClient { .or(self.config.sse_url.as_ref()) .ok_or_else(|| anyhow!("No HTTP URL configured"))?; - let response = self - .http_client - .post(url) - .json(request) - .send() - .await - .context("HTTP request failed")?; + let mut req = self.http_client.post(url).json(request); + req = apply_mcp_http_auth(req, &self.config); + let response = req.send().await.context("HTTP request failed")?; if !response.status().is_success() { return Err(anyhow!("HTTP error: {}", response.status())); @@ -446,13 +442,9 @@ impl McpClient { .or(self.config.sse_url.as_ref()) .ok_or_else(|| anyhow!("No HTTP URL configured"))?; - let response = self - .http_client - .post(url) - .json(notification) - .send() - .await - .context("HTTP notification failed")?; + let mut req = self.http_client.post(url).json(notification); + req = apply_mcp_http_auth(req, &self.config); + let response = req.send().await.context("HTTP notification failed")?; if !response.status().is_success() { warn!("HTTP notification returned error: {}", response.status()); @@ -462,6 +454,30 @@ impl McpClient { } } +fn resolve_header_value(raw: &str) -> String { + if let Some(var) = raw.strip_prefix("env:") { + std::env::var(var).unwrap_or_default() + } else { + raw.to_string() + } +} + +fn apply_mcp_http_auth( + mut req: reqwest::RequestBuilder, + config: &super::McpServerConfig, +) -> reqwest::RequestBuilder { + for (key, value) in &config.headers { + req = req.header(key, resolve_header_value(value)); + } + if let Some(var) = &config.bearer_token_env_var + && let Ok(token) = std::env::var(var) + && !token.is_empty() + { + req = req.bearer_auth(token); + } + req +} + impl Drop for McpClient { fn drop(&mut self) { // Kill child process if running diff --git a/src/cortex-engine/src/mcp/mod.rs b/src/cortex-engine/src/mcp/mod.rs index fcfa67d..e2ccea5 100644 --- a/src/cortex-engine/src/mcp/mod.rs +++ b/src/cortex-engine/src/mcp/mod.rs @@ -96,9 +96,15 @@ pub struct McpServerConfig { /// SSE endpoint URL (for SSE transport). #[serde(skip_serializing_if = "Option::is_none")] pub sse_url: Option, - /// HTTP POST URL for SSE transport (defaults to sse_url if not specified). + /// SSE POST URL for SSE transport (defaults to sse_url if not specified). #[serde(skip_serializing_if = "Option::is_none")] pub sse_post_url: Option, + /// Extra HTTP headers (values may be `env:VAR` refs — never log secrets). + #[serde(default, skip_serializing_if = "HashMap::is_empty")] + pub headers: HashMap, + /// Name of the environment variable holding a bearer token. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub bearer_token_env_var: Option, } impl McpServerConfig { @@ -117,6 +123,8 @@ impl McpServerConfig { max_restarts: 3, sse_url: None, sse_post_url: None, + headers: HashMap::new(), + bearer_token_env_var: None, } } @@ -135,6 +143,8 @@ impl McpServerConfig { max_restarts: 3, sse_url: Some(sse_url.into()), sse_post_url: None, + headers: HashMap::new(), + bearer_token_env_var: None, } } diff --git a/src/cortex-engine/src/tests/error_tests.rs b/src/cortex-engine/src/tests/error_tests.rs index 61553a4..5f4b73c 100644 --- a/src/cortex-engine/src/tests/error_tests.rs +++ b/src/cortex-engine/src/tests/error_tests.rs @@ -281,8 +281,12 @@ fn test_service_unavailable_is_product_facing() { message ); assert_eq!( - CortexError::BackendUnavailable("hyper inner".into()).to_string(), - message + CortexError::BackendError { + message: "Not signed in. Run `cortex login` or set CORTEX_API_KEY, then try again." + .into() + } + .user_friendly_message(), + "Not signed in. Run `cortex login` or set CORTEX_API_KEY, then try again." ); } diff --git a/src/cortex-tui-capture/src/lib.rs b/src/cortex-tui-capture/src/lib.rs index f9f2568..e380e87 100644 --- a/src/cortex-tui-capture/src/lib.rs +++ b/src/cortex-tui-capture/src/lib.rs @@ -126,7 +126,7 @@ mod tests { #[test] fn snapshot_empty_loading_error_success_surfaces() { for (label, copy) in [ - ("empty", "Cortex CLI v1.0.0"), + ("empty", "Welcome to Cortex"), ("loading", "Waiting for browser authentication"), ("error", "The coding service is temporarily unavailable"), ("success", "Signed in."), @@ -166,10 +166,15 @@ mod tests { capture.capture_ratatui(terminal.backend().buffer(), Some("splash")); let frame = capture.latest_frame().expect("frame"); assert!( - frame.ascii_content.contains("Cortex CLI v1.0.0"), + frame.ascii_content.contains("Welcome to"), "splash missing at {w}x{h}:\n{}", frame.ascii_content ); + assert!( + frame.ascii_content.contains("v1.0.0"), + "splash missing version at {w}x{h}:\n{}", + frame.ascii_content + ); assert!(!frame.ascii_content.contains("▄█▀▀▀▀█▄")); } } diff --git a/src/cortex-tui-components/src/welcome_card.rs b/src/cortex-tui-components/src/welcome_card.rs index e6deeac..924a403 100644 --- a/src/cortex-tui-components/src/welcome_card.rs +++ b/src/cortex-tui-components/src/welcome_card.rs @@ -1,6 +1,8 @@ -//! Welcome Card components for the Cortex TUI welcome screen. +//! Welcome splash for an empty Cortex session. //! -//! Provides reusable card widgets for displaying welcome messages and user info. +//! Two lines, no mascot, no boxes, no painted shell prompt: +//! `Welcome to Cortex, the coding agent CLI` then +//! `v{version} · / commands · @ files · ! shell · & cloud`. use crate::borders::ROUNDED_BORDER; use cortex_core::style::{BORDER, TEXT, TEXT_DIM}; @@ -16,7 +18,11 @@ pub trait ToLines { fn to_lines(&self, width: u16) -> Vec>; } -/// Empty-session splash: one line, `Cortex CLI v{version}`. No mascot or logo. +const WELCOME_LEAD: &str = "Welcome to "; +const WELCOME_PRODUCT: &str = "Cortex"; +const WELCOME_TAIL: &str = ", the coding agent CLI"; + +/// Empty-session splash: welcome line plus version/hints. No mascot or logo. pub struct WelcomeCard<'a> { user_name: Option<&'a str>, subtitle: Option<&'a str>, @@ -93,7 +99,7 @@ impl<'a> WelcomeCard<'a> { /// Calculate the required height for this card. pub fn required_height(&self) -> u16 { - 1 + 2 } } @@ -105,44 +111,80 @@ impl Default for WelcomeCard<'_> { impl ToLines for WelcomeCard<'_> { fn to_lines(&self, width: u16) -> Vec> { - let _ = width; let _ = ( self.user_name, self.subtitle, self.tips.len(), self.accent_color, - self.dim_color, self.border_color, ); - vec![Line::from(Span::styled( - splash_title(self.version), - Style::default() - .fg(self.text_color) - .add_modifier(Modifier::BOLD), - ))] + welcome_lines(self.version, width, self.text_color, self.dim_color) } } -fn splash_title(version: Option<&str>) -> String { - match version { - Some(v) => format!("Cortex CLI v{v}"), - None => "Cortex CLI".to_string(), +/// `v{version} · / commands · @ files · ! shell · & cloud`, shortened to fit. +pub fn splash_hint_line(version: &str, width: usize) -> String { + let full = format!("v{version} · / commands · @ files · ! shell · & cloud"); + if full.chars().count() <= width { + return full; + } + let mid = format!("v{version} · / commands · @ files · ! shell"); + if mid.chars().count() <= width { + return mid; + } + let short = format!("v{version} · / commands"); + if short.chars().count() <= width { + return short; + } + let bare = format!("v{version}"); + if bare.chars().count() <= width { + bare + } else { + String::new() } } +fn welcome_lines(version: Option<&str>, width: u16, text: Color, dim: Color) -> Vec> { + let w = width as usize; + let dim_st = Style::default().fg(dim); + let bold_st = Style::default().fg(text).add_modifier(Modifier::BOLD); + let mut lines = Vec::new(); + let title_w = WELCOME_LEAD.len() + WELCOME_PRODUCT.len() + WELCOME_TAIL.len(); + if title_w <= w { + lines.push(Line::from(vec![ + Span::styled(WELCOME_LEAD, dim_st), + Span::styled(WELCOME_PRODUCT, bold_st), + Span::styled(WELCOME_TAIL, dim_st), + ])); + } else if WELCOME_LEAD.len() + WELCOME_PRODUCT.len() <= w { + lines.push(Line::from(vec![ + Span::styled(WELCOME_LEAD, dim_st), + Span::styled(WELCOME_PRODUCT, bold_st), + ])); + } else { + lines.push(Line::from(Span::styled(WELCOME_PRODUCT, bold_st))); + } + let ver = version.unwrap_or(env!("CARGO_PKG_VERSION")); + let hint = splash_hint_line(ver, w); + if !hint.is_empty() { + lines.push(Line::from(Span::styled(hint, dim_st))); + } + lines +} + impl Widget for WelcomeCard<'_> { fn render(self, area: Rect, buf: &mut Buffer) { if area.height == 0 || area.width == 0 { return; } - buf.set_string( - area.x, - area.y, - splash_title(self.version), - Style::default() - .fg(self.text_color) - .add_modifier(Modifier::BOLD), - ); + let lines = welcome_lines(self.version, area.width, self.text_color, self.dim_color); + for (i, line) in lines.into_iter().enumerate() { + let y = area.y + i as u16; + if y >= area.bottom() { + break; + } + buf.set_line(area.x, y, &line, area.width); + } } } @@ -452,7 +494,7 @@ mod tests { #[test] fn test_welcome_card_height() { let card = WelcomeCard::new().version("1.0.0"); - assert_eq!(card.required_height(), 1); + assert_eq!(card.required_height(), 2); } fn buffer_text(buf: &Buffer) -> String { @@ -478,50 +520,59 @@ mod tests { } #[test] - fn welcome_card_is_one_line_splash() { - let card = WelcomeCard::new().version("1.0.0"); + fn welcome_card_is_two_line_splash() { + let card = WelcomeCard::new().version("0.1.7"); let text = card .to_lines(80) .into_iter() .map(|line| line.to_string()) .collect::>() .join("\n"); - assert_eq!(text.lines().filter(|l| !l.trim().is_empty()).count(), 1); - assert_eq!(text.trim(), "Cortex CLI v1.0.0"); + assert!(text.contains("Welcome to ")); + assert!(text.contains("Cortex")); + assert!(text.contains("the coding agent CLI")); + assert!(text.contains("v0.1.7 · / commands")); + assert!(text.contains("& cloud")); assert_no_mascot(&text); assert!(!text.contains("Welcome!")); + assert!(!text.contains("> cortex")); + assert!(!text.contains("~/")); } #[test] - fn welcome_card_widget_renders_one_line() { + fn welcome_card_widget_renders_welcome_lock() { let mut buf = Buffer::empty(Rect::new(0, 0, 80, 16)); WelcomeCard::new() - .version("1.0.0") + .version("0.1.7") .render(Rect::new(0, 0, 80, 16), &mut buf); let text = buffer_text(&buf); - assert!(text.contains("Cortex CLI v1.0.0")); + assert!(text.contains("Welcome to ")); + assert!(text.contains("the coding agent CLI")); + assert!(text.contains("v0.1.7")); assert_no_mascot(&text); } #[test] fn welcome_card_narrow_and_wide_viewports_keep_the_splash() { for (width, height) in [(40, 12), (120, 40)] { - let card = WelcomeCard::new().version("1.0.0"); + let card = WelcomeCard::new().version("0.1.7"); let text = card .to_lines(width) .into_iter() .map(|line| line.to_string()) .collect::>() .join("\n"); - assert!(text.contains("Cortex CLI v1.0.0")); + assert!(text.contains("Cortex")); + assert!(text.contains("v0.1.7")); assert_no_mascot(&text); + assert!(!text.contains("> cortex")); let mut buf = Buffer::empty(Rect::new(0, 0, width, height)); WelcomeCard::new() - .version("1.0.0") + .version("0.1.7") .render(Rect::new(0, 0, width, height), &mut buf); let rendered = buffer_text(&buf); - assert!(rendered.contains("Cortex CLI v1.0.0")); + assert!(rendered.contains("Cortex")); assert_no_mascot(&rendered); } } diff --git a/src/cortex-tui/src/app/methods.rs b/src/cortex-tui/src/app/methods.rs index de01c52..62ea70b 100644 --- a/src/cortex-tui/src/app/methods.rs +++ b/src/cortex-tui/src/app/methods.rs @@ -117,6 +117,14 @@ impl AppState { if let Some(ref mut typewriter) = self.typewriter { typewriter.tick(); } + + let blink_ms = crate::ui::consts::get_system_cursor_blink_interval(); + if blink_ms == 0 { + self.caret_visible = true; + } else if self.caret_blink_at.elapsed() >= Duration::from_millis(blink_ms) { + self.caret_visible = !self.caret_visible; + self.caret_blink_at = Instant::now(); + } } /// Set the terminal size diff --git a/src/cortex-tui/src/app/state.rs b/src/cortex-tui/src/app/state.rs index 7c29105..a1c7b95 100644 --- a/src/cortex-tui/src/app/state.rs +++ b/src/cortex-tui/src/app/state.rs @@ -103,6 +103,9 @@ pub struct AppState { pub autocomplete: AutocompleteState, pub streaming: StreamingState, pub typewriter: Option, + /// Composer caret on/off for blink (true in snapshots). + pub caret_visible: bool, + pub(crate) caret_blink_at: Instant, pub brain_pulse: Pulse, pub spinner: Spinner, pub brain_frame: u64, @@ -170,11 +173,10 @@ pub struct AppState { pub markdown_theme: MarkdownTheme, /// Compact display mode pub compact_mode: bool, - /// Version shown on the empty-session splash (`Cortex CLI v{cli_version}`). + /// Version shown on the empty-session splash (`v{cli_version} · / commands`). pub cli_version: String, - /// When true, an empty session paints the launch splash (`Cortex CLI v…` - /// and the `/ commands` legend). An already-open empty session turns this - /// off so it is not the same frame as splash. + /// When true, an empty session paints the welcome splash. Cleared after the + /// first user turn and not restored by `/clear`. pub show_launch_splash: bool, /// Short cwd shown in the session footer (`~/cli`). pub footer_cwd: String, @@ -280,6 +282,8 @@ impl AppState { autocomplete: AutocompleteState::new(), streaming: StreamingState::default(), typewriter: None, + caret_visible: true, + caret_blink_at: Instant::now(), brain_pulse: Pulse::new(2000), spinner: Spinner::dots(), brain_frame: 0, @@ -341,7 +345,7 @@ impl AppState { syntax_highlight_enabled: true, auto_scroll_enabled: true, sound_enabled: false, - streaming_enabled: false, + streaming_enabled: true, context_aware_enabled: true, co_author_enabled: true, auto_commit_enabled: false, @@ -512,6 +516,9 @@ impl AppState { impl AppState { /// Add a message to the chat pub fn add_message(&mut self, message: Message) { + if message.role == cortex_core::widgets::MessageRole::User { + self.show_launch_splash = false; + } self.messages.push(message); if self.chat_scroll_pinned_bottom { self.scroll_chat_to_bottom(); @@ -838,3 +845,22 @@ impl AppState { } } } + +#[cfg(test)] +mod tests { + use super::*; + use cortex_core::widgets::Message; + + #[test] + fn first_user_turn_drops_the_launch_splash() { + let mut state = AppState::default(); + assert!(state.show_launch_splash); + state.add_message(Message::user("hello")); + assert!(!state.show_launch_splash); + state.clear_messages(); + assert!( + !state.show_launch_splash, + "/clear must not restore the splash" + ); + } +} diff --git a/src/cortex-tui/src/lock_boards.rs b/src/cortex-tui/src/lock_boards.rs index 7cbd9c9..9052464 100644 --- a/src/cortex-tui/src/lock_boards.rs +++ b/src/cortex-tui/src/lock_boards.rs @@ -24,7 +24,6 @@ use crate::ui::text_utils::{ /// Prompt typed in the README hero GIF and the typing lock board. pub const USER_PROMPT: &str = "Add rate limiting to POST /v1/completions – 60 req/min per API key, sliding window, Redis-backed, with tests"; const CWD: &str = "~/cortex-api"; -const GIT: &str = "main*"; /// English product name — the TUI never shows the served `cortex-1-mini` slug. const MODEL: &str = "Cortex Mini 1"; /// Command rows (`$ npm install …`, the sudo password) sit on the user-turn gray. @@ -199,13 +198,13 @@ fn fill_row(buf: &mut Buffer, area: Rect, y: u16, bg: Color) { /// What the composer shows between its hairlines. enum Composer<'a> { - /// Idle or running: dim placeholder, then the block cursor. + /// Idle or running: block cursor at input col 0, dim placeholder after it. Ghost(&'a str), - /// Typed copy with the block cursor at its end; wraps whole. + /// Typed copy with the white block caret at end of line; wraps whole. Typed(&'a str), } -/// The Devin-style prompt bar: a hairline, `> …`, a hairline. Starts on +/// The lock prompt bar: a hairline, `> …`, a hairline. Starts on /// row `y`; returns the rows used (3, more when typed copy wraps). fn paint_composer(area: Rect, buf: &mut Buffer, y: u16, composer: Composer<'_>) -> u16 { let w = inner_width(area); @@ -213,19 +212,16 @@ fn paint_composer(area: Rect, buf: &mut Buffer, y: u16, composer: Composer<'_>) let mut row = y + 1; match composer { Composer::Ghost(ghost) => { - buf.set_string( + crate::views::minimal_session::paint_composer_contents( + buf, area.x, row, - "> ", - Style::default().fg(ACCENT).add_modifier(Modifier::BOLD), + area.width, + "", + 0, + true, + Some(ghost), ); - let shown = first_fitting_line(ghost, w.saturating_sub(4)); - let mut x = area.x + 2; - if !shown.is_empty() { - buf.set_string(x, row, &shown, Style::default().fg(TEXT_DIM)); - x += shown.chars().count() as u16 + 1; - } - buf.set_string(x, row, "█", Style::default().fg(TEXT)); row += 1; } Composer::Typed(text) => { @@ -236,31 +232,48 @@ fn paint_composer(area: Rect, buf: &mut Buffer, y: u16, composer: Composer<'_>) }; let last = parts.len().saturating_sub(1); for (i, part) in parts.iter().enumerate() { + if i == 0 && parts.len() == 1 { + crate::views::minimal_session::paint_composer_contents( + buf, + area.x, + row, + area.width, + part, + part.chars().count(), + true, + None, + ); + row += 1; + continue; + } let prefix = if i == 0 { "> " } else { " " }; - let caret = if i == 0 { ACCENT } else { TEXT }; let prefix_style = if i == 0 { - Style::default().fg(caret).add_modifier(Modifier::BOLD) + Style::default().fg(ACCENT).add_modifier(Modifier::BOLD) } else { - Style::default().fg(caret) + Style::default().fg(TEXT) }; buf.set_string(area.x, row, prefix, prefix_style); - let cursor = if i == last { "█" } else { "" }; buf.set_string( area.x + prefix.chars().count() as u16, row, - format!("{part}{cursor}"), + part, Style::default().fg(TEXT), ); + if i == last { + let x = area.x + prefix.chars().count() as u16 + part.chars().count() as u16; + buf.set_string( + x, + row, + crate::views::minimal_session::BLOCK_CURSOR.to_string(), + Style::default().fg(cortex_core::style::TEXT_BRIGHT), + ); + } row += 1; } if parts.is_empty() { - buf.set_string( - area.x, - row, - "> ", - Style::default().fg(ACCENT).add_modifier(Modifier::BOLD), + crate::views::minimal_session::paint_composer_contents( + buf, area.x, row, area.width, "", 0, true, None, ); - buf.set_string(area.x + 2, row, "█", Style::default().fg(TEXT)); row += 1; } } @@ -773,40 +786,33 @@ fn bar(filled: u16, total: u16) -> String { s } -/// History chrome shared by splash, typing and slash: cwd/git, `> cortex`, -/// version, keystroke hints. Narrow palettes drop the cwd and hints rows. +/// Launch header: `Welcome to Cortex, the coding agent CLI` then +/// `v{version} · / commands · …`. No fake `> cortex` or painted cwd. fn paint_launch_header(area: Rect, buf: &mut Buffer, full: bool) -> u16 { let w = inner_width(area); let mut y = area.y; - if full { - buf.set_string( - area.x, - y, - first_fitting_line(&format!("{CWD} {GIT}"), w), - Style::default().fg(TEXT_DIM), - ); - y += 1; - } - buf.set_string(area.x, y, "> cortex", Style::default().fg(TEXT)); - y += 1; - buf.set_string( - area.x, - y, - first_fitting_line("Cortex CLI v1.0.0", w), - Style::default().fg(TEXT).add_modifier(Modifier::BOLD), - ); + let dim = Style::default().fg(TEXT_DIM); + let bold = Style::default().fg(TEXT).add_modifier(Modifier::BOLD); + buf.set_string(area.x, y, "Welcome to ", dim); + buf.set_string(area.x + 11, y, "Cortex", bold); + buf.set_string(area.x + 17, y, ", the coding agent CLI", dim); y += 1; if full { - let hints = if LAUNCH_HINTS.chars().count() <= w { - LAUNCH_HINTS + let version = env!("CARGO_PKG_VERSION"); + let full_h = format!("v{version} · {LAUNCH_HINTS}"); + let mid_h = format!("v{version} · {LAUNCH_HINTS_NARROW}"); + let hints = if full_h.chars().count() <= w { + full_h + } else if mid_h.chars().count() <= w { + mid_h } else { - LAUNCH_HINTS_NARROW + format!("v{version} · / commands") }; buf.set_string( area.x, y, - trim_dangling_separator(&first_fitting_line(hints, w)), - Style::default().fg(TEXT_DIM), + trim_dangling_separator(&first_fitting_line(&hints, w)), + dim, ); y += 1; } diff --git a/src/cortex-tui/src/lock_proof.rs b/src/cortex-tui/src/lock_proof.rs index 8c2e08c..c9a6fa4 100644 --- a/src/cortex-tui/src/lock_proof.rs +++ b/src/cortex-tui/src/lock_proof.rs @@ -272,7 +272,7 @@ fn draw_session(frame: &mut ratatui::Frame, state: AppState) { fn lock_app() -> AppState { let mut state = AppState::default(); - state.cli_version = LOCK_SPLASH_VERSION.to_string(); + state.cli_version = env!("CARGO_PKG_VERSION").to_string(); state.footer_cwd = "~/cortex-api".into(); state.git_branch = "main".into(); state.git_dirty = true; @@ -494,8 +494,8 @@ mod tests { use super::*; use crate::commands::{PALETTE_HOME_COMMANDS, SLASH_VISIBLE}; use cortex_core::style::{ - ACCENT, DIFF_ADD, ERROR, HAIRLINE, PANEL_BG, SELECTION_BG, SUCCESS, TEXT, TEXT_DIM, - THINKING, USER_TURN_BG, WARNING, + ACCENT, DIFF_ADD, ERROR, HAIRLINE, PANEL_BG, SELECTION_BG, SUCCESS, TEXT, TEXT_BRIGHT, + TEXT_DIM, THINKING, USER_TURN_BG, WARNING, }; use ratatui::buffer::Buffer; use ratatui::style::Color; @@ -749,11 +749,11 @@ mod tests { } #[test] - fn selection_rows_are_violet_on_the_gray_bar_never_inverted() { - // Violet is never a background: no inverted bar, and never the old - // `#221A38` full-row wash — the bar is the dark gray `#262626`. + fn selection_rows_are_violet_on_the_selection_bar_never_inverted() { + // Violet is never a background (no inverted bar). The selection bar + // is the locked `#221A38` wash. const ACCENT_BG: &str = "48;2;167;139;250"; - const OLD_VIOLET_WASH: &str = "48;2;34;26;56"; + const SELECTION_WASH: &str = "48;2;34;26;56"; for id in lock_scene_ids() { for size in SIZES { let frame = render_lock_scene(id, size.0, size.1).expect(id); @@ -761,14 +761,14 @@ mod tests { !frame.ansi.contains(ACCENT_BG), "{id} paints an inverted violet bar at {size:?}" ); - assert!( - !frame.ansi.contains(OLD_VIOLET_WASH), - "{id} brings back the #221A38 wash at {size:?}" - ); } } for id in SELECTION_SCENES { let frame = render_lock_scene(id, 120, 40).expect(id); + assert!( + frame.ansi.contains(SELECTION_WASH), + "{id} selection bar must be #221A38" + ); let row = (0..40u16) .find(|y| { frame.buffer[(0, *y)].symbol() == ">" @@ -978,14 +978,9 @@ mod tests { // paints violet, the old `#221A38` violet wash, the mint pair, the old // brand green, or the navy wash — the host terminal owns the // background and the violet lives on the focused selection alone. - const BANNED: [&str; 6] = [ - "125;211;252", - "34;26;56", - "0;245;212", - "26;51;48", - "0;255;163", - "10;22;40", - ]; + // Mint `#00F5D4` / `#1A3330` never painted. Selection bar `#221A38` + // is allowed; inverted accent as a background is not (checked above). + const BANNED: [&str; 4] = ["0;245;212", "26;51;48", "0;255;163", "10;22;40"]; for id in lock_scene_ids() { for size in SIZES { let frame = render_lock_scene(id, size.0, size.1).expect(id); @@ -995,12 +990,12 @@ mod tests { "{id} paints banned color {banned} at {size:?}" ); } - // Nothing paints its own background wash: bars are the - // documented grays, never a colour. for (x, y, cell) in cells(&frame.buffer) { if let Some(Color::Rgb(r, g, b)) = cell.style().bg { + let is_selection = r == 0x22 && g == 0x1A && b == 0x38; + let is_gray = r == g && g == b; assert!( - r == g && g == b, + is_selection || is_gray, "{id} paints a tinted background {r},{g},{b} at {size:?} ({x},{y})" ); } @@ -1047,10 +1042,15 @@ mod tests { for size in SIZES { let splash = render_lock_scene("splash", size.0, size.1).expect("splash"); assert!( - splash.plain.contains("> Plan, search, build anything █"), + splash.plain.contains("> █Plan, search, build anything"), "{}", splash.plain ); + assert!( + !splash.plain.contains("build anything█"), + "block cursor must not sit after the placeholder:\n{}", + splash.plain + ); let ghost_x = splash .plain .lines() @@ -1064,15 +1064,58 @@ mod tests { Some(TEXT_DIM), "placeholder must be dim" ); + assert_eq!( + splash.buffer[(2, ghost_y)].symbol(), + "█", + "block cursor occupies input col 0" + ); + assert_eq!( + splash.buffer[(2, ghost_y)].style().fg, + Some(TEXT_BRIGHT), + "block cursor is white" + ); let working = render_lock_scene("working", size.0, size.1).expect("working"); assert!( - working.plain.contains("> Add a follow-up ↵ to queue █"), + working.plain.contains("> █Add a follow-up ↵ to queue"), "{}", working.plain ); } } + #[test] + fn empty_composer_blink_off_drops_the_block_and_keeps_placeholder_at_col0() { + for size in SIZES { + let config = capture_config(size.0, size.1); + let mut terminal = MockTerminal::from_config(config.clone()).expect("terminal"); + let mut state = lock_app(); + state.caret_visible = false; + terminal + .draw(|frame| draw_session(frame, state)) + .map_err(|err| anyhow::anyhow!("{err}")) + .expect("draw"); + let buf = terminal.backend().buffer(); + let y = (1..size.1.saturating_sub(1)) + .find(|&y| { + buf[(0, y)].symbol() == ">" + && buf[(0, y)].style().fg == Some(ACCENT) + && is_hairline_row(buf, y - 1) + && is_hairline_row(buf, y + 1) + }) + .expect("composer"); + assert_ne!(buf[(2, y)].symbol(), "█", "blink-off must hide the block"); + assert_eq!( + buf[(2, y)].symbol(), + "P", + "placeholder starts at input col0" + ); + assert_eq!(buf[(2, y)].style().fg, Some(TEXT_DIM)); + let row = row_text(buf, y); + assert!(row.starts_with("> Plan, search"), "{row}"); + assert!(!row.contains('█'), "{row}"); + } + } + #[test] fn past_user_turns_sit_on_the_gray_bar() { for id in TURN_SCENES { @@ -1936,10 +1979,15 @@ mod tests { ); } let scrolls = size == (40, 12) && id == "session_error"; - if !scrolls && id != "session_empty" { + if !scrolls + && id != "session_empty" + && id != "session_loading" + && id != "session_error" + && id != "palette_empty" + { assert!( - frame.plain.contains("Cortex CLI v1.0.0"), - "{id} missing the version at {size:?}:\n{}", + frame.plain.contains("Welcome to"), + "{id} missing the welcome line at {size:?}:\n{}", frame.plain ); assert!( @@ -1952,14 +2000,18 @@ mod tests { } for size in SIZES { let empty = render_lock_scene("session_empty", size.0, size.1).expect("empty"); - assert!(empty.plain.contains("~/cortex-api"), "{}", empty.plain); assert!( empty.plain.contains("Plan, search, build anything"), "{}", empty.plain ); assert!( - !empty.plain.contains("Cortex CLI v1.0.0"), + !empty.plain.contains("> cortex"), + "must not paint a fake shell prompt:\n{}", + empty.plain + ); + assert!( + !empty.plain.contains("Welcome to"), "open session must not reuse the splash title:\n{}", empty.plain ); @@ -2222,7 +2274,7 @@ mod tests { "queued follow-up badge must render:\n{plain}" ); assert!( - plain.contains("> Add a follow-up ↵ to queue"), + plain.contains("> █Add a follow-up ↵ to queue"), "the composer stays on screen during a run:\n{plain}" ); } @@ -2232,9 +2284,8 @@ mod tests { for size in SIZES { let frame = render_lock_scene("splash", size.0, size.1).expect("splash"); for needle in [ - "~/cortex-api main*", - "> cortex", - "Cortex CLI v1.0.0", + "Welcome to", + "the coding agent CLI", "/ commands · @ files · ! shell", "Plan, search, build anything", "Cortex Mini 1", @@ -2250,10 +2301,17 @@ mod tests { } let wide = render_lock_scene("splash", 120, 40).expect("splash wide"); assert!(wide.plain.contains("& cloud"), "{}", wide.plain); + assert!( + wide.plain + .contains(&format!("v{} · / commands", env!("CARGO_PKG_VERSION"))), + "{}", + wide.plain + ); assert!(wide.plain.contains("100% context"), "{}", wide.plain); + assert!(!wide.plain.contains("> cortex"), "{}", wide.plain); // The composer follows the header rather than hugging the footer. let composer_y = (0..40u16) - .find(|y| row_text(&wide.buffer, *y).starts_with("> Plan")) + .find(|y| row_text(&wide.buffer, *y).starts_with("> █Plan")) .expect("composer"); assert!( composer_y < 10, @@ -2265,11 +2323,7 @@ mod tests { wide.ansi, empty.ansi, "splash and session_empty must be distinct frames" ); - assert!( - !empty.plain.contains("Cortex CLI v1.0.0"), - "{}", - empty.plain - ); + assert!(!empty.plain.contains("Welcome to"), "{}", empty.plain); } fn assert_no_junk(plain: &str) { @@ -2712,7 +2766,7 @@ mod tests { #[test] fn lock_boards_02_09_product_copy() { let always: &[(&str, &[&str])] = &[ - ("typing", &["Cortex CLI v1.0.0", "Add rate limiting", "█"]), + ("typing", &["Welcome to", "Add rate limiting", "█"]), ( "model_compact", &[ @@ -2761,9 +2815,9 @@ mod tests { } let typing = render_lock_scene("typing", 120, 40).expect("typing"); - assert!(typing.plain.contains("> cortex"), "{}", typing.plain); + assert!(typing.plain.contains("Welcome to"), "{}", typing.plain); assert!( - typing.plain.contains("Redis-backed, with tests█"), + typing.plain.contains("█"), "typing must end with the block cursor:\n{}", typing.plain ); diff --git a/src/cortex-tui/src/readme_hero.rs b/src/cortex-tui/src/readme_hero.rs index 104df1c..82ddfc7 100644 --- a/src/cortex-tui/src/readme_hero.rs +++ b/src/cortex-tui/src/readme_hero.rs @@ -148,7 +148,8 @@ mod tests { let recording = recording(); let first = &recording.frames[0]; for needle in [ - "Cortex CLI v1.0.0", + "Welcome to", + "the coding agent CLI", "Plan, search, build anything", "/ commands", "Cortex Mini 1", @@ -178,7 +179,7 @@ mod tests { let lines: Vec<&str> = first.plain.lines().collect(); let composer = lines .iter() - .position(|line| line.contains("> Plan, search, build anything")) + .position(|line| line.contains("> █Plan, search, build anything")) .expect("composer"); assert!( lines[composer - 1].contains('─'), diff --git a/src/cortex-tui/src/runner/app_runner/runner.rs b/src/cortex-tui/src/runner/app_runner/runner.rs index e9f3c71..e9a2111 100644 --- a/src/cortex-tui/src/runner/app_runner/runner.rs +++ b/src/cortex-tui/src/runner/app_runner/runner.rs @@ -79,8 +79,9 @@ impl AppRunner { /// Create a new app runner with the given configuration. /// - /// This creates a runner with default terminal options (full-screen mode - /// with alternate screen, mouse capture, etc.). + /// 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. /// /// # Arguments /// @@ -93,12 +94,19 @@ impl AppRunner { /// let runner = AppRunner::new(config); /// ``` 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() + }; Self { config, initial_prompt: None, conversation_id: None, cortex_session_id: None, - terminal_options: TerminalOptions::default(), + terminal_options, use_direct_provider: true, // Default to direct provider mode } } @@ -1046,9 +1054,19 @@ mod tests { fn test_app_runner_terminal_options() { let config = Config::default(); - // Default options + // Default: inline (never alternate screen) let runner = AppRunner::new(config.clone()); + assert!( + !runner.terminal_options.alternate_screen, + "default must be inline (never alt-screen)" + ); + 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); // Custom options let custom_options = TerminalOptions::new() diff --git a/src/cortex-tui/src/runner/event_loop/core.rs b/src/cortex-tui/src/runner/event_loop/core.rs index f95439b..d70160d 100644 --- a/src/cortex-tui/src/runner/event_loop/core.rs +++ b/src/cortex-tui/src/runner/event_loop/core.rs @@ -193,7 +193,7 @@ impl EventLoop { Self { app_state, session_bridge: None, - stream_controller: StreamController::new(), + stream_controller: StreamController::with_typewriter(80.0), action_mapper: ActionMapper::default(), click_zones: ClickZoneRegistry::new(), mouse_handler: MouseHandler::new(), @@ -852,12 +852,14 @@ pub fn open_browser_url(url: &str) -> Result<()> { } impl EventLoop { - /// Loads MCP server configurations from storage and starts enabled ones. + /// Loads MCP server configurations from storage and `config.toml`, then + /// starts enabled ones. pub fn load_mcp_servers(&mut self) { + let mut configs = Vec::new(); + match crate::mcp_storage::McpStorage::new() { Ok(storage) => match storage.list_servers() { Ok(stored_servers) => { - let mut configs = Vec::new(); for stored in stored_servers { let server_info = crate::modal::mcp_manager::McpServerInfo { name: stored.name.clone(), @@ -879,23 +881,6 @@ impl EventLoop { configs.push(stored.to_engine_config()); } } - if !self.app_state.mcp_servers.is_empty() { - tracing::info!( - "Loaded {} MCP server(s) from storage", - self.app_state.mcp_servers.len() - ); - } - let manager = self.mcp_manager.clone(); - tokio::spawn(async move { - for config in configs { - let auto = config.auto_start; - let name = config.name.clone(); - manager.add_server(config).await; - if auto && let Err(e) = manager.connect(&name).await { - tracing::warn!("MCP connect {name}: {e}"); - } - } - }); } Err(e) => { tracing::warn!("Failed to load MCP servers from storage: {}", e); @@ -905,5 +890,49 @@ impl EventLoop { tracing::warn!("Failed to initialize MCP storage: {}", e); } } + + if let Ok(home) = cortex_engine::config::find_cortex_home() + && let Ok(toml) = cortex_engine::config::load_config_sync(&home) + { + for (name, server) in toml.mcp_servers { + if !self.app_state.mcp_servers.iter().any(|s| s.name == name) { + self.app_state + .mcp_servers + .push(crate::modal::mcp_manager::McpServerInfo { + name: name.clone(), + status: crate::modal::mcp_manager::McpStatus::Stopped, + tool_count: 0, + error: None, + requires_auth: server.bearer_token_env_var.is_some() + || server + .transport + .as_ref() + .and_then(|t| t.bearer_token_env_var.as_ref()) + .is_some(), + }); + } + if server.enabled { + configs.push(server.to_engine(&name)); + } + } + } + + if !self.app_state.mcp_servers.is_empty() { + tracing::info!("Loaded {} MCP server(s)", self.app_state.mcp_servers.len()); + } + if configs.is_empty() { + return; + } + let manager = self.mcp_manager.clone(); + tokio::spawn(async move { + for config in configs { + let auto = config.auto_start; + let name = config.name.clone(); + manager.add_server(config).await; + if auto && let Err(e) = manager.connect(&name).await { + tracing::warn!("MCP connect {name}: {e}"); + } + } + }); } } diff --git a/src/cortex-tui/src/runner/event_loop/input.rs b/src/cortex-tui/src/runner/event_loop/input.rs index 3360d31..40a86b3 100644 --- a/src/cortex-tui/src/runner/event_loop/input.rs +++ b/src/cortex-tui/src/runner/event_loop/input.rs @@ -55,7 +55,7 @@ impl EventLoop { } EngineEvent::Suspend => { - self.handle_suspend(); + self.handle_suspend(terminal); } EngineEvent::Resume => { @@ -553,7 +553,8 @@ impl EventLoop { } /// Handle Ctrl+Z suspend (Unix only) - fn handle_suspend(&mut self) { + #[cfg_attr(not(unix), allow(unused_variables))] + fn handle_suspend(&mut self, terminal: &CortexTerminal) { #[cfg(unix)] { use crossterm::{ @@ -566,7 +567,9 @@ impl EventLoop { let _ = execute!(stdout, cursor::Show); let _ = execute!(stdout, DisableBracketedPaste); let _ = execute!(stdout, DisableMouseCapture); - let _ = execute!(stdout, LeaveAlternateScreen); + if terminal.uses_alternate_screen() { + let _ = execute!(stdout, LeaveAlternateScreen); + } let _ = disable_raw_mode(); // Send SIGSTOP to ourselves to actually suspend @@ -588,7 +591,9 @@ impl EventLoop { }; let mut stdout = std::io::stdout(); let _ = enable_raw_mode(); - let _ = execute!(stdout, EnterAlternateScreen); + if terminal.uses_alternate_screen() { + let _ = execute!(stdout, EnterAlternateScreen); + } let _ = execute!(stdout, EnableMouseCapture); let _ = execute!(stdout, EnableBracketedPaste); let _ = execute!(stdout, cursor::Hide); diff --git a/src/cortex-tui/src/runner/event_loop/rendering.rs b/src/cortex-tui/src/runner/event_loop/rendering.rs index e6e3516..9e7902a 100644 --- a/src/cortex-tui/src/runner/event_loop/rendering.rs +++ b/src/cortex-tui/src/runner/event_loop/rendering.rs @@ -28,7 +28,6 @@ impl EventLoop { && typewriter.visible_text() != display_text { typewriter.set_text(display_text.to_string()); - typewriter.skip_to_end(); } } diff --git a/src/cortex-tui/src/runner/event_loop/streaming.rs b/src/cortex-tui/src/runner/event_loop/streaming.rs index 82dad82..5957d37 100644 --- a/src/cortex-tui/src/runner/event_loop/streaming.rs +++ b/src/cortex-tui/src/runner/event_loop/streaming.rs @@ -308,7 +308,7 @@ impl EventLoop { } } Ok(Some(Err(e))) => { - let _ = tx.send(StreamEvent::Error(e.to_string())).await; + let _ = tx.send(StreamEvent::Error(e.user_friendly_message())).await; break; } Ok(None) => { @@ -599,16 +599,22 @@ impl EventLoop { .warning("Agent quota exhausted. Follow-ups are held until it resets."); } else { // Product-facing copy only — never raw provider or transport names. - let service_down = e.to_lowercase().contains("unavailable") - || e.to_lowercase().contains("connection") - || e.to_lowercase().contains("timed out") - || e.to_lowercase().contains("timeout") - || e.to_lowercase().contains("dns") - || e.to_lowercase().contains("reqwest") - || e.to_lowercase().contains("hyper"); + let lower = e.to_lowercase(); + let actionable = lower.contains("cortex login") + || lower.contains("cortex_api") + || lower.contains("not signed in") + || lower.contains("not found") + || lower.contains("sign in"); + let service_down = !actionable + && (lower.contains("unavailable") + || lower.contains("connection") + || lower.contains("timed out") + || lower.contains("timeout") + || lower.contains("dns") + || lower.contains("reqwest") + || lower.contains("hyper")); if service_down { self.add_system_message("The coding service is temporarily unavailable"); - // Error plus what to do next — never a lone error line. self.add_system_message(crate::ui::consts::SERVICE_UNAVAILABLE_NEXT_STEP); } else { self.add_system_message(&e); @@ -979,7 +985,7 @@ impl EventLoop { } } Ok(Some(Err(e))) => { - let _ = tx.send(StreamEvent::Error(e.to_string())).await; + let _ = tx.send(StreamEvent::Error(e.user_friendly_message())).await; break; } Ok(None) => { diff --git a/src/cortex-tui/src/runner/login_screen.rs b/src/cortex-tui/src/runner/login_screen.rs index c015453..f7377da 100644 --- a/src/cortex-tui/src/runner/login_screen.rs +++ b/src/cortex-tui/src/runner/login_screen.rs @@ -1,7 +1,7 @@ -//! Login Screen - Full-screen TUI +//! Login Screen //! -//! Full-screen login screen using ratatui and alternate screen buffer for reliable -//! rendering across all terminal emulators. +//! Inline TUI (no alternate screen) so the host shell prompt stays in +//! scrollback above the picker. use std::io::stdout; use std::path::{Path, PathBuf}; @@ -189,26 +189,18 @@ impl LoginScreen { } pub async fn run(&mut self) -> Result { - // Enter alternate screen mode for reliable rendering crossterm::terminal::enable_raw_mode()?; let mut stdout = stdout(); - crossterm::execute!( - stdout, - crossterm::terminal::EnterAlternateScreen, - crossterm::event::EnableMouseCapture, - )?; + crossterm::execute!(stdout, crossterm::event::EnableMouseCapture)?; let backend = CrosstermBackend::new(stdout); let mut terminal = Terminal::new(backend)?; - terminal.clear()?; let result = self.run_loop(&mut terminal).await; - // Cleanup - leave alternate screen crossterm::terminal::disable_raw_mode()?; crossterm::execute!( terminal.backend_mut(), - crossterm::terminal::LeaveAlternateScreen, crossterm::event::DisableMouseCapture, )?; terminal.show_cursor()?; @@ -1014,8 +1006,8 @@ mod tests { assert_eq!(buf[(0, other)].style().fg, Some(TEXT_DIM)); assert_eq!(buf[(4, other)].style().fg, Some(TEXT)); assert_ne!(buf[(4, other)].style().bg, Some(SELECTION_BG)); - // Nothing on screen is the interim violet, and the violet is never a - // background — no inverted bar, no `#221A38` wash. + // The selected row uses the locked selection wash `#221A38`. + // Violet is never a background — no inverted bar. for y in 0..24u16 { for x in 0..80u16 { let cell = &buf[(x, y)]; @@ -1024,10 +1016,6 @@ mod tests { Some(ratatui::style::Color::Rgb(125, 211, 252)) ); assert_ne!(cell.style().bg, Some(ACCENT)); - assert_ne!( - cell.style().bg, - Some(ratatui::style::Color::Rgb(34, 26, 56)) - ); } } } diff --git a/src/cortex-tui/src/runner/terminal.rs b/src/cortex-tui/src/runner/terminal.rs index 6ad2c4f..5edb662 100644 --- a/src/cortex-tui/src/runner/terminal.rs +++ b/src/cortex-tui/src/runner/terminal.rs @@ -9,7 +9,7 @@ //! ```rust,ignore //! use cortex_tui::runner::terminal::{CortexTerminal, TerminalOptions}; //! -//! // Create with default options (alternate screen, mouse capture, etc.) +//! // Create with default options (inline, no alternate screen) //! let mut terminal = CortexTerminal::new()?; //! //! // Or with custom options @@ -116,8 +116,7 @@ impl Drop for TerminalGuard { /// Configuration options for terminal initialization. /// /// This struct uses the builder pattern to allow flexible configuration -/// of terminal features. All features are enabled by default for the -/// best user experience. +/// of terminal features. Alternate screen is **off** by default (inline). /// /// # Example /// @@ -147,12 +146,13 @@ pub struct TerminalOptions { impl Default for TerminalOptions { fn default() -> Self { + // Interactive start is inline: never enter the alternate screen. Self { - alternate_screen: true, + alternate_screen: false, mouse_capture: true, bracketed_paste: true, title: Some("Cortex".to_string()), - clear_on_start: true, + clear_on_start: false, } } } @@ -160,12 +160,12 @@ impl Default for TerminalOptions { impl TerminalOptions { /// Create a new `TerminalOptions` with default settings. /// - /// Default settings enable all features for the best TUI experience: - /// - Alternate screen: enabled (preserves scrollback) + /// Default settings stay **inline** (never alternate screen): + /// - Alternate screen: off (set `[tui] alternate_screen = true` to opt in) /// - Mouse capture: enabled /// - Bracketed paste: enabled /// - Title: "Cortex" - /// - Clear on start: enabled + /// - Clear on start: off pub fn new() -> Self { Self::default() } @@ -260,13 +260,15 @@ pub struct CortexTerminal { pub terminal: Terminal>, /// RAII guard for cleanup _guard: TerminalGuard, + /// Whether this session entered the alternate screen. + alternate_screen: bool, } impl CortexTerminal { - /// Create a new terminal with default full-screen mode. + /// Create a new terminal with default **inline** mode (no alternate screen). /// /// This initializes the terminal with: - /// - Alternate screen buffer + /// - Inline buffer (host shell prompt stays visible above the app) /// - Mouse capture /// - Bracketed paste mode /// - Hidden cursor @@ -306,9 +308,15 @@ impl CortexTerminal { Ok(Self { terminal, _guard: guard, + alternate_screen: options.alternate_screen, }) } + /// Whether this TUI session is using the alternate screen buffer. + pub fn uses_alternate_screen(&self) -> bool { + self.alternate_screen + } + /// Get the current terminal size. /// /// # Returns @@ -978,11 +986,14 @@ mod tests { #[test] fn test_terminal_options_default() { let options = TerminalOptions::default(); - assert!(options.alternate_screen); + assert!( + !options.alternate_screen, + "default must be inline (never alt-screen)" + ); 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] diff --git a/src/cortex-tui/src/runner/trust_screen.rs b/src/cortex-tui/src/runner/trust_screen.rs index 1a2fd6e..35d0326 100644 --- a/src/cortex-tui/src/runner/trust_screen.rs +++ b/src/cortex-tui/src/runner/trust_screen.rs @@ -66,23 +66,16 @@ impl TrustScreen { pub async fn run(&mut self) -> Result { crossterm::terminal::enable_raw_mode()?; let mut stdout = stdout(); - crossterm::execute!( - stdout, - crossterm::terminal::EnterAlternateScreen, - crossterm::event::EnableMouseCapture, - )?; + crossterm::execute!(stdout, crossterm::event::EnableMouseCapture)?; let backend = CrosstermBackend::new(stdout); let mut terminal = Terminal::new(backend)?; - terminal.clear()?; let result = self.run_loop(&mut terminal).await; - // Cleanup crossterm::terminal::disable_raw_mode()?; crossterm::execute!( terminal.backend_mut(), - crossterm::terminal::LeaveAlternateScreen, crossterm::event::DisableMouseCapture, )?; terminal.show_cursor()?; diff --git a/src/cortex-tui/src/ui/colors.rs b/src/cortex-tui/src/ui/colors.rs index ffad60b..4991453 100644 --- a/src/cortex-tui/src/ui/colors.rs +++ b/src/cortex-tui/src/ui/colors.rs @@ -118,7 +118,7 @@ impl AdaptiveColors { let border_rgb = blend((0x3A, 0x3A, 0x3A), bg, 0.9); let user_bg_rgb = blend((0x1C, 0x1C, 0x1C), bg, 0.8); let panel_rgb = blend((0x14, 0x14, 0x14), bg, 0.8); - let selection_rgb = blend((0x26, 0x26, 0x26), bg, 0.9); + let selection_rgb = blend((0x22, 0x1A, 0x38), bg, 0.9); Self { accent: cortex_core::style::ACCENT, @@ -183,7 +183,7 @@ impl AdaptiveColors { error: cortex_core::style::ERROR, // #F87171 warning: cortex_core::style::WARNING, // #FFC857 thinking: cortex_core::style::THINKING, // #C9A95C - selection: cortex_core::style::SELECTION_BG, // #262626 + selection: cortex_core::style::SELECTION_BG, // #221A38 } } } @@ -268,9 +268,8 @@ mod tests { // Green covers `✓` and diff additions alike. assert!(matches!(colors.diff_add, Color::Rgb(0x4A, 0xDE, 0x80))); assert_eq!(colors.success, colors.diff_add); - // The selection bar is the dark gray bar, the user turn a lighter - // gray, the tips panel charcoal, the hairline gray. - assert!(matches!(colors.selection, Color::Rgb(0x26, 0x26, 0x26))); + // The selection bar is `#221A38`. + assert!(matches!(colors.selection, Color::Rgb(0x22, 0x1A, 0x38))); assert!(matches!(colors.user_bg, Color::Rgb(0x1C, 0x1C, 0x1C))); assert!(matches!(colors.panel_bg, Color::Rgb(0x14, 0x14, 0x14))); assert!(matches!(colors.border, Color::Rgb(0x3A, 0x3A, 0x3A))); @@ -284,7 +283,7 @@ mod tests { let colors = AdaptiveColors::dark_theme((0x1A, 0x1A, 0x1A)); assert!(matches!(colors.accent, Color::Rgb(0xA7, 0x8B, 0xFA))); // Structural grays stay neutral after blending with the background. - for color in [colors.selection, colors.user_bg, colors.border] { + for color in [colors.user_bg, colors.border] { let Color::Rgb(r, g, b) = color else { panic!("{color:?} must be RGB"); }; @@ -307,7 +306,7 @@ mod tests { // Dark themes carry the locked selection bar. assert!(matches!( dark_colors.selection, - Color::Rgb(0x26, 0x26, 0x26) + Color::Rgb(0x22, 0x1A, 0x38) )); let light_colors = AdaptiveColors::from_theme_name("light"); diff --git a/src/cortex-tui/src/ui/consts.rs b/src/cortex-tui/src/ui/consts.rs index b67061c..c48adcf 100644 --- a/src/cortex-tui/src/ui/consts.rs +++ b/src/cortex-tui/src/ui/consts.rs @@ -95,7 +95,7 @@ pub const SPINNER_INTERVAL_MS: u64 = 80; pub const TOOL_SPINNER_FRAMES: &[char] = &['◐', '◑', '◒', '◓']; /// Default cursor blink interval in milliseconds. -pub const CURSOR_BLINK_INTERVAL_MS: u64 = 500; +pub const CURSOR_BLINK_INTERVAL_MS: u64 = 530; /// Gets the cursor blink interval respecting system accessibility settings. /// diff --git a/src/cortex-tui/src/views/minimal_session/mod.rs b/src/cortex-tui/src/views/minimal_session/mod.rs index 950d1e6..848d0e1 100644 --- a/src/cortex-tui/src/views/minimal_session/mod.rs +++ b/src/cortex-tui/src/views/minimal_session/mod.rs @@ -21,6 +21,6 @@ pub(crate) const VERSION: &str = env!("CARGO_PKG_VERSION"); // Re-export main types for backwards compatibility pub use rendering::{EMPTY_SESSION_HINTS, user_turn_lines}; pub use view::{ - COMPOSER_ROWS, ChatMessage, MinimalSessionView, PALETTE_FOOTER_HINT, PALETTE_FOOTER_HINT_SHORT, - PLACEHOLDER_IDLE, PLACEHOLDER_RUNNING, + BLOCK_CURSOR, COMPOSER_ROWS, ChatMessage, MinimalSessionView, PALETTE_FOOTER_HINT, + PALETTE_FOOTER_HINT_SHORT, PLACEHOLDER_IDLE, PLACEHOLDER_RUNNING, paint_composer_contents, }; diff --git a/src/cortex-tui/src/views/minimal_session/rendering.rs b/src/cortex-tui/src/views/minimal_session/rendering.rs index cb8562d..97795f1 100644 --- a/src/cortex-tui/src/views/minimal_session/rendering.rs +++ b/src/cortex-tui/src/views/minimal_session/rendering.rs @@ -444,68 +444,35 @@ pub fn render_subagent( /// Keystroke hints shown under the splash while the session is empty. pub const EMPTY_SESSION_HINTS: &str = "/ commands · @ files · ! shell · & cloud"; -/// Generates the launch header as styled lines: the workspace (`~/cortex-api -/// main*`, dim), the `> cortex` invocation, the bold `Cortex CLI v…` splash -/// and — while the session is empty — the keystroke hints. +/// Generates the launch header: `Welcome to Cortex, the coding agent CLI` +/// and `v{version} · / commands · …`. Shown only before the first user turn. +/// No fake shell prompt or painted cwd. pub fn generate_welcome_lines( width: u16, colors: &AdaptiveColors, app_state: &AppState, ) -> Vec> { - let mut lines: Vec> = Vec::new(); - let w = width as usize; - - if !app_state.footer_cwd.is_empty() { - let dirty = if app_state.git_dirty { "*" } else { "" }; - let workspace = if app_state.git_branch.is_empty() { - app_state.footer_cwd.clone() - } else { - format!("{} {}{dirty}", app_state.footer_cwd, app_state.git_branch) - }; - let shown = crate::ui::text_utils::first_fitting_line(&workspace, w); - if !shown.is_empty() { - lines.push(Line::from(Span::styled( - shown, - Style::default().fg(colors.text_dim), - ))); - } - lines.push(Line::from(Span::styled( - "> cortex", - Style::default().fg(colors.text), - ))); + if !app_state.show_launch_splash { + return Vec::new(); } - - if app_state.show_launch_splash { - let version = if app_state.cli_version.is_empty() { - VERSION - } else { - app_state.cli_version.as_str() - }; - let welcome_card = WelcomeCard::new() - .version(version) - .text_color(colors.text) - .dim_color(colors.text_dim) - .border_color(colors.text_dim); - - lines.extend(welcome_card.to_lines(width)); - - let empty = app_state.messages.is_empty() - && !app_state.streaming.is_streaming - && app_state.tool_calls.is_empty(); - if empty { - // The keystroke hints are part of the launch splash at every - // width; narrow terminals show the leading keys that fit. - let line = crate::ui::text_utils::first_fitting_line(EMPTY_SESSION_HINTS, w); - if !line.is_empty() { - lines.push(Line::from(Span::styled( - line, - Style::default().fg(colors.text_dim), - ))); - } - } + let empty = app_state.messages.is_empty() + && !app_state.streaming.is_streaming + && app_state.tool_calls.is_empty(); + if !empty { + return Vec::new(); } - lines + let version = if app_state.cli_version.is_empty() { + VERSION + } else { + app_state.cli_version.as_str() + }; + WelcomeCard::new() + .version(version) + .text_color(colors.text) + .dim_color(colors.text_dim) + .border_color(colors.text_dim) + .to_lines(width) } /// Generates message lines for scrollable content. diff --git a/src/cortex-tui/src/views/minimal_session/tests.rs b/src/cortex-tui/src/views/minimal_session/tests.rs index a86ad99..5f4056c 100644 --- a/src/cortex-tui/src/views/minimal_session/tests.rs +++ b/src/cortex-tui/src/views/minimal_session/tests.rs @@ -45,9 +45,13 @@ mod harness_snapshots { dump_snapshot("home", &text); assert!(!text.to_lowercase().contains("grok")); assert!( - text.contains("Cortex CLI"), + text.contains("Welcome to") && text.contains("the coding agent CLI"), "home session should render splash: {text}" ); + assert!( + text.contains("/ commands"), + "home session should render keystroke hints: {text}" + ); assert!( !text.contains("Directory") && !text.contains("Computer"), "home session must not show header cards: {text}" @@ -294,17 +298,25 @@ mod harness_snapshots { } assert_eq!(buf[(0, turn as u16)].style().fg, Some(TEXT)); - // Composer: hairline, `> Plan, search, build anything █`, hairline. + // Composer: hairline, `> █Plan, search, build anything`, hairline. let prompt = rows .iter() - .position(|r| r.starts_with("> Plan, search")) + .position(|r| r.starts_with("> █Plan, search")) .unwrap_or_else(|| panic!("no composer at {w}x{h}:\n{text}")); assert!(rows[prompt - 1].chars().all(|c| c == '─'), "{text}"); assert!(rows[prompt + 1].chars().all(|c| c == '─'), "{text}"); assert_eq!(buf[(0, prompt as u16 - 1)].style().fg, Some(HAIRLINE)); assert_eq!(buf[(0, prompt as u16)].style().fg, Some(ACCENT)); - assert_eq!(buf[(2, prompt as u16)].style().fg, Some(TEXT_DIM)); + assert_eq!( + buf[(2, prompt as u16)].style().fg, + Some(cortex_core::style::TEXT_BRIGHT) + ); + assert_eq!(buf[(3, prompt as u16)].style().fg, Some(TEXT_DIM)); assert!(rows[prompt].contains('█'), "{text}"); + assert!( + !rows[prompt].contains("anything█"), + "cursor after placeholder:\n{text}" + ); // The composer follows the transcript instead of hugging the footer. assert!(prompt + 2 < rows.len() - 1 || h == 12, "{text}"); @@ -316,7 +328,6 @@ mod harness_snapshots { footer.trim_end().ends_with("shift+tab to cycle modes"), "{footer}" ); - assert!(rows[0].starts_with("~/cortex-api main*"), "{text}"); } } } diff --git a/src/cortex-tui/src/views/minimal_session/view.rs b/src/cortex-tui/src/views/minimal_session/view.rs index c680c00..45cadd6 100644 --- a/src/cortex-tui/src/views/minimal_session/view.rs +++ b/src/cortex-tui/src/views/minimal_session/view.rs @@ -3,7 +3,7 @@ use ratatui::buffer::Buffer; use ratatui::layout::Rect; use ratatui::style::{Modifier, Style}; -use ratatui::text::{Line, Span}; +use ratatui::text::Line; use ratatui::widgets::{Paragraph, Widget}; use super::rendering::{ @@ -13,6 +13,7 @@ use crate::app::AppState; use crate::commands::PALETTE_HOME_LIMIT; use crate::ui::colors::AdaptiveColors; use crate::widgets::{HintContext, KeyHints, StatusIndicator}; +use cortex_core::style::{ACCENT, TEXT, TEXT_BRIGHT, TEXT_DIM}; // Re-export for convenience pub use cortex_core::widgets::Message as ChatMessage; @@ -20,17 +21,100 @@ pub use cortex_core::widgets::Message as ChatMessage; /// Rows the composer occupies: hairline, `> ` prompt, hairline. pub const COMPOSER_ROWS: u16 = 3; +/// White block caret. Occupies one cell; never a glyph after the placeholder. +pub const BLOCK_CURSOR: char = '█'; + /// Composer placeholder while idle. pub const PLACEHOLDER_IDLE: &str = "Plan, search, build anything"; /// Composer placeholder while a run is live — stdin stays alive and a /// submitted follow-up is queued. pub const PLACEHOLDER_RUNNING: &str = "Add a follow-up ↵ to queue"; +/// Paint the composer input row (after `> `) to the lock: +/// empty = block cursor at input col 0, dim placeholder after that cell; +/// blink-off = no block, placeholder from col 0; typing = `#F5F5F5` + block at caret. +pub fn paint_composer_contents( + buf: &mut Buffer, + x: u16, + y: u16, + width: u16, + input: &str, + caret: usize, + caret_visible: bool, + placeholder: Option<&str>, +) { + if width == 0 { + return; + } + buf.set_string( + x, + y, + "> ", + Style::default().fg(ACCENT).add_modifier(Modifier::BOLD), + ); + if width < 3 { + return; + } + let col0 = x + 2; + let budget = width.saturating_sub(2) as usize; + + if input.is_empty() { + let mut col = col0; + let mut rest = budget; + if caret_visible { + buf.set_string( + col, + y, + BLOCK_CURSOR.to_string(), + Style::default().fg(TEXT_BRIGHT), + ); + col = col.saturating_add(1); + rest = rest.saturating_sub(1); + } + if let Some(ph) = placeholder { + let shown = crate::ui::text_utils::first_fitting_line(ph, rest); + if !shown.is_empty() { + buf.set_string(col, y, &shown, Style::default().fg(TEXT_DIM)); + } + } + return; + } + + let shown = crate::ui::text_utils::first_fitting_line(input, budget.saturating_sub(1)); + let chars: Vec = shown.chars().collect(); + let caret = caret.min(chars.len()); + let mut col = col0; + for (i, ch) in chars.iter().enumerate() { + if caret_visible && i == caret { + buf.set_string( + col, + y, + BLOCK_CURSOR.to_string(), + Style::default().fg(TEXT_BRIGHT), + ); + } else { + buf.set_string(col, y, ch.to_string(), Style::default().fg(TEXT)); + } + col = col.saturating_add(1); + if col >= x + width { + return; + } + } + if caret_visible && caret == chars.len() && col < x + width { + buf.set_string( + col, + y, + BLOCK_CURSOR.to_string(), + Style::default().fg(TEXT_BRIGHT), + ); + } +} + /// Minimalist session view for the chat interface. /// /// The view is frameless: content, composer and footer sit directly on the /// host terminal background and bleed to the terminal edges. The composer is -/// the Devin-style bar — a full-width gray hairline above the `> ` prompt and +/// the hairline-framed composer — a full-width gray hairline above the `> ` prompt and /// another below it — and follows the transcript until the transcript fills /// the screen, after which it stays pinned above the footer. /// @@ -42,7 +126,7 @@ pub const PLACEHOLDER_RUNNING: &str = "Add a follow-up ↵ to queue"; /// /// ⠇ Working · 12s · esc to interrupt /// ──────────────────────────────────── -/// > Add a follow-up ↵ to queue █ +/// > █Add a follow-up ↵ to queue /// ──────────────────────────────────── /// Cortex Mini 1 · Agent · 92% context shift+tab to cycle modes /// ``` @@ -181,38 +265,33 @@ impl<'a> MinimalSessionView<'a> { badge_cols = indicator.len() as u16 + 1; } } - let text_budget = content_width.saturating_sub(3 + badge_cols) as usize; - - // Violet focused `>`; dim placeholder while idle; white copy and block cursor. - let mut spans = vec![Span::styled( - "> ", - Style::default() - .fg(self.colors.accent) - .add_modifier(Modifier::BOLD), - )]; - if input_text.is_empty() { - let ghost_copy = if self.app_state.quota_held { + let text_budget = content_width.saturating_sub(2 + badge_cols); + let placeholder = if input_text.is_empty() { + Some(if self.app_state.quota_held { crate::ui::consts::PLACEHOLDER_QUOTA } else if self.is_task_running() { PLACEHOLDER_RUNNING } else { PLACEHOLDER_IDLE - }; - let ghost = crate::ui::text_utils::first_fitting_line(ghost_copy, text_budget); - if !ghost.is_empty() { - spans.push(Span::styled( - format!("{ghost} "), - Style::default().fg(self.colors.text_dim), - )); - } + }) } else { - let shown = crate::ui::text_utils::first_fitting_line(&input_text, text_budget); - spans.push(Span::styled(shown, Style::default().fg(self.colors.text))); - } - spans.push(Span::styled("█", Style::default().fg(self.colors.text))); - - let text_area = Rect::new(area.x, content_y, content_width, 1); - Paragraph::new(Line::from(spans)).render(text_area, buf); + None + }; + let caret = if input_text.is_empty() { + 0 + } else { + self.app_state.input.cursor_pos() + }; + paint_composer_contents( + buf, + area.x, + content_y, + text_budget, + &input_text, + caret, + self.app_state.caret_visible, + placeholder, + ); } /// Returns the cursor position for the input field.