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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
label as a child that asked a question. Parked work sorts below live and
answerable work and no longer inflates the `blocked` chip; the receipts
roster and the wire `state` gain `parked` (#5906, #5921).
- The bottom chrome no longer prints facts it cannot stand behind. A route
that cannot prove its effective reasoning tier — typically a custom
OpenAI-compatible gateway with no endpoint receipt — states no effort
field in the metrics line rather than the placeholder
`high→effective unavailable`; `/status` still tells the full story. The
cost segment is omitted only where the route itself cannot be priced
(`cost: unknown` stays on metered routes that merely lack a reading this
session, per #5578) (#5950).
Comment on lines +32 to +39

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 Remove branch changelog edits

The contribution rules reserve both changelogs for batched updates on main. This PR modifies both files and creates avoidable merge conflicts.

Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.


- `codewhale account keys set|remove|list` no longer carry a hardcoded
eight-provider list. Provider ids come from the control plane's public
Expand Down Expand Up @@ -153,6 +161,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
per-World session container before each command — full tree first, then
only changes and deletions — so remote builds and tests run on the files
just edited locally and their outputs persist across commands.
- `[tui].posture_bar` and `[tui].metrics_line` size the two rows under the
composer: `full` (the default), `compact`, or `hidden`, also settable at
runtime with `/config posture_bar compact`. `compact` is the row after its
first shed rungs — the posture bar keeps its permission and mode chips and
the cap warning but drops the clocks, counts and hint; the metrics line
keeps the route, context reading, cost and balance but drops the telemetry
and the help hint — and `hidden` gives the row back to the transcript.
Composition stays in `tui.status_items`; these presets only decide how
much of a row paints, reusing the rows' existing shed ladders rather than
a second renderer (#5950).

## [0.9.12] - 2026-09-03

Expand Down
11 changes: 11 additions & 0 deletions config.example.toml
Original file line number Diff line number Diff line change
Expand Up @@ -1126,6 +1126,17 @@ osc8_links = true # emit OSC 8 escapes around URLs (Cmd+click in iTer
# git_branch, last_tool_elapsed, rate_limit — they drove nothing. Old files
# keep loading; the retired keys are ignored.
# status_items = ["mode", "model", "context_percent", "cost", "tokens"]
# Size presets for the two rows themselves (#5950) — composition stays in
# status_items; these only decide how much of a row paints:
# posture_bar = "full" # full | compact | hidden (default full)
# # compact keeps the posture chips (and the cap
# # warning) and drops the clocks, counts and hint;
# # hidden gives the row to the transcript.
# metrics_line = "full" # full | compact | hidden (default full)
# # compact keeps the route, context reading, cost
# # and balance and drops the telemetry and the
# # help hint; hidden gives the row to the transcript.
# # Also settable at runtime: /config posture_bar compact
# notification_condition = "unfocused" # unfocused | always | never
# "unfocused" = notify only after this terminal has been
# in the background for two seconds (default);
Expand Down
18 changes: 18 additions & 0 deletions crates/tui/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
label as a child that asked a question. Parked work sorts below live and
answerable work and no longer inflates the `blocked` chip; the receipts
roster and the wire `state` gain `parked` (#5906, #5921).
- The bottom chrome no longer prints facts it cannot stand behind. A route
that cannot prove its effective reasoning tier — typically a custom
OpenAI-compatible gateway with no endpoint receipt — states no effort
field in the metrics line rather than the placeholder
`high→effective unavailable`; `/status` still tells the full story. The
cost segment is omitted only where the route itself cannot be priced
(`cost: unknown` stays on metered routes that merely lack a reading this
session, per #5578) (#5950).

- `codewhale account keys set|remove|list` no longer carry a hardcoded
eight-provider list. Provider ids come from the control plane's public
Expand Down Expand Up @@ -153,6 +161,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
per-World session container before each command — full tree first, then
only changes and deletions — so remote builds and tests run on the files
just edited locally and their outputs persist across commands.
- `[tui].posture_bar` and `[tui].metrics_line` size the two rows under the
composer: `full` (the default), `compact`, or `hidden`, also settable at
runtime with `/config posture_bar compact`. `compact` is the row after its
first shed rungs — the posture bar keeps its permission and mode chips and
the cap warning but drops the clocks, counts and hint; the metrics line
keeps the route, context reading, cost and balance but drops the telemetry
and the help hint — and `hidden` gives the row back to the transcript.
Composition stays in `tui.status_items`; these presets only decide how
much of a row paints, reusing the rows' existing shed ladders rather than
a second renderer (#5950).

## [0.9.12] - 2026-09-03

Expand Down
112 changes: 112 additions & 0 deletions crates/tui/src/commands/groups/config/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,8 @@ fn show_single_setting(app: &App, key: &str) -> CommandResult {
app.active_context_window_source.display_label(),
)),
"stream_chunk_timeout_secs" => Some(app.stream_chunk_timeout_secs.to_string()),
"posture_bar" => Some(app.posture_bar.as_setting().to_string()),
"metrics_line" => Some(app.metrics_line.as_setting().to_string()),
"locale" | "language" => Some(locale_display(app.ui_locale).to_string()),
"theme" | "ui_theme" => Some(
if app
Expand Down Expand Up @@ -867,6 +869,20 @@ fn config_editability_audit(app: &App) -> CommandResult {
"/config stream_chunk_timeout_secs <0|1..3600> --save",
"Writes [tui].stream_chunk_timeout_secs and updates the running stream timeout.",
),
(
"posture_bar",
app.posture_bar.as_setting().to_string(),
"runtime+persisted",
"/config posture_bar <full|compact|hidden> --save",
"Writes [tui].posture_bar; hidden gives the row to the transcript, compact keeps the posture chips only.",
),
(
"metrics_line",
app.metrics_line.as_setting().to_string(),
"runtime+persisted",
"/config metrics_line <full|compact|hidden> --save",
"Writes [tui].metrics_line; hidden gives the row to the transcript, compact drops the telemetry and help hint.",
),
(
"subagents.enabled",
subagents_config_display_value(&config, "enabled"),
Expand Down Expand Up @@ -2364,6 +2380,40 @@ pub fn set_config_value(app: &mut App, key: &str, value: &str, persist: bool) ->
"provider_url must be saved with --save; client base URL is loaded from config on startup. Restart and re-open your session after saving.",
);
}
// The two bottom-chrome rows' size presets (`tui.posture_bar`,
// `tui.metrics_line`, #5950). Live on the next frame; `--save`
// writes the `[tui]` key. `/statusline` composes what is in a row;
// this only decides whether and how much of it paints.
row_key @ ("posture_bar" | "metrics_line") => {
let Some(preset) = crate::config::ChromeRowPreset::from_setting(value) else {
return CommandResult::error(format!(
"{row_key} must be one of: {}",
crate::config::ChromeRowPreset::SETTINGS.join(", ")
Comment on lines +2389 to +2391

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Route the new preset feedback through localization

For every non-English UI locale, invalid preset feedback—and the success/session-only responses later in this branch—remains hard-coded English. These user-visible command messages should use tr(locale, MessageId::...), leaving only setting names and command tokens composed in code.

AGENTS.md reference: crates/tui/AGENTS.md:L25-L26

Useful? React with 👍 / 👎.

));
};
if row_key == "posture_bar" {
app.posture_bar = preset;
} else {
app.metrics_line = preset;
}
Comment on lines +2394 to +2398

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Apply runtime state only after a successful save

When /config posture_bar ... --save or /config metrics_line ... --save targets an unwritable or malformed config file, these assignments occur before persist_table_string_key can fail. The command therefore reports an error while silently applying the requested preset for the current session. Persist first and update the app only on success, or restore the previous value on failure.

Useful? React with 👍 / 👎.

app.needs_redraw = true;
let value = preset.as_setting();
if persist {
return match persist_table_string_key(
app.config_path.as_deref(),
"tui",
row_key,
value,
Comment on lines +2402 to +2406

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Persist row presets into the active profile

When Codewhale is launched with --profile work and that profile defines any [profiles.work.tui] table, merge_config selects the profile's entire TuiConfig over the base table. This call nevertheless writes the preset to the base [tui] table and reports success, so the live change works but the profile shadows it on the next launch and the supposedly saved preset reverts. Write through the active profile path or reject the save with an explicit scope message.

Useful? React with 👍 / 👎.

) {
Ok(path) => CommandResult::message(format!(
"{row_key} = {value} (saved to {})",
path.display()
)),
Err(err) => CommandResult::error(format!("Failed to save: {err}")),
};
}
return CommandResult::message(format!("{row_key} = {value} (session only)"));
}
"stream_chunk_timeout_secs" => {
let raw = match value.trim().parse::<u64>() {
Ok(value) => value,
Expand Down Expand Up @@ -4822,6 +4872,68 @@ context_window = 262144
));
}

/// The bottom-chrome row presets (#5950) apply on the next frame and
/// `--save` writes the `[tui]` key; an unknown preset names the three.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[INFO] Only metrics_line --save persistence is covered; posture_bar save path lacks direct test

The new config test exercises live /config posture_bar compact and saving metrics_line HIDDEN --save, but never saves posture_bar with --save. The two branches share a helper, but a regression in the posture_bar persist path or key name would not be caught.

#[test]
fn config_command_row_presets_apply_live_and_persist_to_tui_table() {
use crate::config::ChromeRowPreset;
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_nanos();
let temp_root = env::temp_dir().join(format!(
"codewhale-tui-row-presets-test-{}-{}",
std::process::id(),
nanos
));
fs::create_dir_all(&temp_root).unwrap();
let _guard = EnvGuard::new(&temp_root);
let config_path = temp_root.join("custom-config.toml");
let mut app = create_test_app();
app.config_path = Some(config_path.clone());
assert_eq!(app.posture_bar, ChromeRowPreset::Full);
assert_eq!(app.metrics_line, ChromeRowPreset::Full);

let live = config_command(&mut app, Some("posture_bar compact"));
assert!(!live.is_error, "{live:?}");
assert_eq!(app.posture_bar, ChromeRowPreset::Compact);
assert_eq!(
live.message.as_deref(),
Some("posture_bar = compact (session only)")
);
assert_eq!(
config_command(&mut app, Some("posture_bar"))
.message
.as_deref(),
Some("posture_bar = compact")
);

let saved = config_command(&mut app, Some("metrics_line HIDDEN --save"));
assert!(!saved.is_error, "{saved:?}");
assert_eq!(app.metrics_line, ChromeRowPreset::Hidden);
let body = fs::read_to_string(&config_path).unwrap();
assert!(body.contains("[tui]"), "{body}");
assert!(body.contains("metrics_line = \"hidden\""), "{body}");
assert!(
!body.contains("posture_bar"),
"session-only value must not be saved: {body}"
);

let bad = config_command(&mut app, Some("metrics_line tiny"));
assert!(bad.is_error);
assert!(
bad.message
.as_deref()
.is_some_and(|m| m.contains("metrics_line must be one of: full, compact, hidden")),
"{bad:?}"
);
assert_eq!(
app.metrics_line,
ChromeRowPreset::Hidden,
"a bad value changes nothing"
);
}

#[test]
fn config_command_stream_chunk_timeout_rejects_invalid_input() {
let _lock = lock_test_env();
Expand Down
60 changes: 60 additions & 0 deletions crates/tui/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1811,6 +1811,20 @@ pub struct TuiConfig {
/// in `~/.deepseek/config.toml`.
#[serde(default, deserialize_with = "deser_status_items")]
pub status_items: Option<Vec<StatusItem>>,
/// How much of the posture bar — the first row under the composer — to
/// paint: `full` (default), `compact`, or `hidden`. `hidden` gives the
/// row back to the transcript; `compact` keeps the row and starts its
/// shed ladder past the clocks, counts and hints (#5950).
///
/// `status_items` still composes what is *in* the row; this only decides
/// the row's size. Absent from an older `config.toml` means `full`.
#[serde(default)]
pub posture_bar: Option<ChromeRowPreset>,
/// The same three settings for the metrics line under the posture bar.
/// `compact` keeps the route, the context reading, the cost and the
/// balance and drops the telemetry and the help hint (#5950).
#[serde(default)]
pub metrics_line: Option<ChromeRowPreset>,
/// Ordered list of optional header items the user wants visible.
///
/// `None` (the field missing from `config.toml`) preserves the built-in
Expand Down Expand Up @@ -1860,6 +1874,52 @@ pub struct TuiConfig {
pub composer_arrows_scroll: Option<bool>,
}

/// How much of one bottom-chrome row to paint (#5950). One value for each
/// of the two rows under the composer — [`TuiConfig::posture_bar`] and
/// [`TuiConfig::metrics_line`] — so a small tmux pane can give one or both
/// rows back to the transcript without touching `status_items`.
///
/// `compact` is not a second renderer: it starts the row's existing shed
/// ladder at a fixed rung and lets width shed the rest, so what it keeps is

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[INFO] Case sensitivity differs between /config and config.toml

ChromeRowPreset::from_setting trims and lowercases, so /config posture_bar COMPACT works. However the serde-derived Deserialize with rename_all = "snake_case" only accepts the exact lowercase values, so a hand-edited posture_bar = "COMPACT" in config.toml is rejected. Consider adding serde aliases/custom deserialization or explicitly documenting that TOML values must be lowercase.

/// exactly what a narrow row keeps.
#[derive(Debug, Clone, Copy, Default, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum ChromeRowPreset {
/// Every fact the row owns, shed only by width.
#[default]
Full,
/// The row's shed ladder started past its most expendable rungs.
Compact,
/// No row: the transcript takes the line.
Hidden,
}

impl ChromeRowPreset {
/// Every setting value, in the order `/config` names them.
pub const SETTINGS: [&'static str; 3] = ["full", "compact", "hidden"];

/// Stable name used in `config.toml` and `/config`.
#[must_use]
pub const fn as_setting(self) -> &'static str {
match self {
Self::Full => "full",
Self::Compact => "compact",
Self::Hidden => "hidden",
}
}

/// Reverse of [`Self::as_setting`]; `None` for anything else.
#[must_use]
pub fn from_setting(value: &str) -> Option<Self> {
match value.trim().to_ascii_lowercase().as_str() {
"full" => Some(Self::Full),
"compact" => Some(Self::Compact),
"hidden" => Some(Self::Hidden),
_ => None,
}
}
}

/// High-level notification trigger override. See
/// [`TuiConfig::notification_condition`].
#[derive(Debug, Clone, Copy, Deserialize, PartialEq, Eq)]
Expand Down
46 changes: 46 additions & 0 deletions crates/tui/src/config/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -942,6 +942,52 @@ webhook_token = "secret-token"
assert!(absent.base.lifecycle_outbox.is_none());
}

/// `tui.posture_bar` / `tui.metrics_line` (#5950): absent means full — an
/// older `config.toml` keeps loading unchanged — and each key takes one of
/// the three presets.
#[test]
fn tui_config_parses_bottom_chrome_row_presets() {
let raw = r#"
[tui]
posture_bar = "compact"
metrics_line = "hidden"
"#;
let parsed: ConfigFile = toml::from_str(raw).expect("parse row presets");
let tui = parsed.base.tui.expect("tui table");
assert_eq!(tui.posture_bar, Some(ChromeRowPreset::Compact));
assert_eq!(tui.metrics_line, Some(ChromeRowPreset::Hidden));

let absent: ConfigFile = toml::from_str("[tui]\nmouse_capture = true\n").expect("old file");
let tui = absent.base.tui.expect("tui table");
assert_eq!(tui.posture_bar, None);
assert_eq!(tui.metrics_line, None);
assert_eq!(
tui.posture_bar.unwrap_or_default(),
ChromeRowPreset::Full,
"absent means the full row"
);

let bad: Result<ConfigFile, _> = toml::from_str("[tui]\nposture_bar = \"tiny\"\n");
assert!(
bad.is_err(),
"a preset this build does not know is refused, not guessed"
);

for (setting, preset) in [
("full", ChromeRowPreset::Full),
("compact", ChromeRowPreset::Compact),
("hidden", ChromeRowPreset::Hidden),
] {
assert_eq!(ChromeRowPreset::from_setting(setting), Some(preset));
assert_eq!(
ChromeRowPreset::from_setting(&setting.to_uppercase()),
Some(preset)
);
assert_eq!(preset.as_setting(), setting);
}
assert_eq!(ChromeRowPreset::from_setting("tiny"), None);
}

#[test]
fn tui_config_parses_control_socket_table() {
let raw = r#"
Expand Down
8 changes: 8 additions & 0 deletions crates/tui/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17372,6 +17372,8 @@ api_key = "test-only-key"
stream_max_content_mb: None,
stream_max_duration_secs: None,
status_items: None,
posture_bar: None,
metrics_line: None,
osc8_links: None,
composer_arrows_scroll: None,
notification_condition: None,
Expand Down Expand Up @@ -17471,6 +17473,8 @@ api_key = "test-only-key"
stream_max_content_mb: None,
stream_max_duration_secs: None,
status_items: None,
posture_bar: None,
metrics_line: None,
osc8_links: None,
composer_arrows_scroll: None,
notification_condition: None,
Expand Down Expand Up @@ -17508,6 +17512,8 @@ api_key = "test-only-key"
stream_max_content_mb: None,
stream_max_duration_secs: None,
status_items: None,
posture_bar: None,
metrics_line: None,
osc8_links: None,
composer_arrows_scroll: None,
notification_condition: None,
Expand Down Expand Up @@ -17599,6 +17605,8 @@ api_key = "test-only-key"
stream_max_content_mb: None,
stream_max_duration_secs: None,
status_items: None,
posture_bar: None,
metrics_line: None,
osc8_links: None,
composer_arrows_scroll: None,
notification_condition: None,
Expand Down
Loading
Loading