feat(tui): keep performance readings visible and individually configurable

Compact mode retains measured TTFT and average output rate when space
allows, shedding help and secondary counts first. Reuse existing metrics
and statusline settings, support separate toggles with legacy migration,
and add mouse selection and scrolling to the picker.

Refresh release notes and derived website facts for the combined candidate.

Validation: 157 focused Rust tests passed; all 8 full-frame regression tests
passed after updating 3 expectations for the new compact behavior. The
StepFun follow-up cases also passed. npm test: 551 passed. check:web:
821 routes passed. Clippy lib/tests passed with repository CI allowances;
only presentation-test assertions changed after that check. Real PTY
capture of the final rebuilt binary follows this source commit.

Signed-off-by: CodeWhale Bot <[email protected]>
This commit is contained in:
CodeWhale Bot committed 2026-09-19 21:57:41 -07:00
1 parent 5bbc10979f
commit a7033c4bec
19 files changed
+325 -78

No files matched your search

+8 -1
View File
@@ -16,7 +16,7 @@ tag, packages, checksums and release assets exist.
### Contributors
- **[@AdityaVG13](https://github.com/AdityaVG13)** — fixed composer wrapping, tab/caret placement, pasted and editor-returned draft history, painted-column transcript copying, explicit terminal foregrounds, and headless user-input tool availability ([#6363](https://github.com/Hmbown/Codewhale/pull/6363), [#6365](https://github.com/Hmbown/Codewhale/pull/6365)).
- **[@AdityaVG13](https://github.com/AdityaVG13)** — fixed composer wrapping, tab/caret placement, pasted and editor-returned draft history, painted-column transcript copying, explicit terminal foregrounds, headless user-input tool availability, and engine synchronization after importing foreign sessions ([#6369](https://github.com/Hmbown/Codewhale/pull/6369), [#6363](https://github.com/Hmbown/Codewhale/pull/6363), [#6365](https://github.com/Hmbown/Codewhale/pull/6365)).
- **[@aboimpinto](https://github.com/aboimpinto)** — moved the TUI session-export slice onto shared command contracts (FEAT-025): a session-export contract facet with one shared sanitizer, `/export` routed through the facet, pinned with baseline-captured goldens and gates ([#6096](https://github.com/Hmbown/Codewhale/pull/6096)).
- **[@BX166](https://github.com/BX166)** — contributed the AICraft provider template and its documentation ([#6171](https://github.com/Hmbown/Codewhale/pull/6171)). It was closed unmerged, but it is what surfaced the decision to stop special-casing named OpenAI-compatible hosts ([#6289](https://github.com/Hmbown/Codewhale/issues/6289)).
- **[@7jrxt42BxFZo4iAnN4CX](https://github.com/7jrxt42BxFZo4iAnN4CX)** — reported the session-retention defects behind archive-past-the-cap and empty-session cap occupancy ([#6136](https://github.com/Hmbown/Codewhale/issues/6136), [#6137](https://github.com/Hmbown/Codewhale/issues/6137)), the resume-failure design behind durable transcript errors ([#6138](https://github.com/Hmbown/Codewhale/issues/6138)), and the gaps behind the opt-in approval timeout ([#6101](https://github.com/Hmbown/Codewhale/issues/6101)), `codewhale exec --hooks` ([#6099](https://github.com/Hmbown/Codewhale/issues/6099)), Markdown drag-copy ([#6156](https://github.com/Hmbown/Codewhale/issues/6156)), and the browsable, current-aware session picker ([#6014](https://github.com/Hmbown/Codewhale/issues/6014)); the goal token-budget hard stop ([#6013](https://github.com/Hmbown/Codewhale/issues/6013)) and the fleet no-progress guard shared with child workers ([#6015](https://github.com/Hmbown/Codewhale/issues/6015)) landed as first slices of two larger proposals, and the runtime-store session refusal ([#6207](https://github.com/Hmbown/Codewhale/issues/6207)).
@@ -41,6 +41,13 @@ tag, packages, checksums and release assets exist.
### Added
- TTFT and average output rate stay visible in the compact working footer when
space allows. `/statusline` now controls them separately; existing
`session_metrics` settings still work.
- StepFun’s current coding catalog includes Step 5 Preview, Step 3.7 Flash,
Step 3.5 Flash and its 2603 version, with published reasoning controls and
PAYG rates. Existing Step Plan routing remains subscription-based.
- `read` responses now always report the file's byte size, line count, and
whether output was truncated, and truncation footers name the total size
alongside the continuation offset — so paging through a large file is
+5 -4
View File
@@ -1178,9 +1178,10 @@ osc8_links = true # emit OSC 8 escapes around URLs (Cmd+click in iTer
# interactively with `/statusline`.
# Supported keys: mode, model, context_percent, cost,
# balance (prepaid providers only: DeepSeek, DeepSeekCN, OpenRouter,
# SiliconFlow), cache, tokens, session_metrics.
# SiliconFlow), cache, tokens, ttft, output_rate, workspace, git_branch.
# Legacy session_metrics enables both ttft and output_rate.
# Retired in 0.9.13: status, agents, reasoning_replay, prefix_stability,
# git_branch, last_tool_elapsed, rate_limit — they drove nothing. Old files
# 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
@@ -1191,8 +1192,8 @@ osc8_links = true # emit OSC 8 escapes around URLs (Cmd+click in iTer
# # hidden gives the row to the transcript.
# metrics_line = "compact" # full | compact | hidden (default compact)
# # compact keeps the route, context reading, cost
# # and balance and drops the telemetry and the
# # help hint; hidden gives the row to the transcript.
# # and balance plus selected TTFT/rate when they fit.
# # It drops secondary counts and help; 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
+8 -1
View File
@@ -16,7 +16,7 @@ tag, packages, checksums and release assets exist.
### Contributors
- **[@AdityaVG13](https://github.com/AdityaVG13)** — fixed composer wrapping, tab/caret placement, pasted and editor-returned draft history, painted-column transcript copying, explicit terminal foregrounds, and headless user-input tool availability ([#6363](https://github.com/Hmbown/Codewhale/pull/6363), [#6365](https://github.com/Hmbown/Codewhale/pull/6365)).
- **[@AdityaVG13](https://github.com/AdityaVG13)** — fixed composer wrapping, tab/caret placement, pasted and editor-returned draft history, painted-column transcript copying, explicit terminal foregrounds, headless user-input tool availability, and engine synchronization after importing foreign sessions ([#6369](https://github.com/Hmbown/Codewhale/pull/6369), [#6363](https://github.com/Hmbown/Codewhale/pull/6363), [#6365](https://github.com/Hmbown/Codewhale/pull/6365)).
- **[@aboimpinto](https://github.com/aboimpinto)** — moved the TUI session-export slice onto shared command contracts (FEAT-025): a session-export contract facet with one shared sanitizer, `/export` routed through the facet, pinned with baseline-captured goldens and gates ([#6096](https://github.com/Hmbown/Codewhale/pull/6096)).
- **[@BX166](https://github.com/BX166)** — contributed the AICraft provider template and its documentation ([#6171](https://github.com/Hmbown/Codewhale/pull/6171)). It was closed unmerged, but it is what surfaced the decision to stop special-casing named OpenAI-compatible hosts ([#6289](https://github.com/Hmbown/Codewhale/issues/6289)).
- **[@7jrxt42BxFZo4iAnN4CX](https://github.com/7jrxt42BxFZo4iAnN4CX)** — reported the session-retention defects behind archive-past-the-cap and empty-session cap occupancy ([#6136](https://github.com/Hmbown/Codewhale/issues/6136), [#6137](https://github.com/Hmbown/Codewhale/issues/6137)), the resume-failure design behind durable transcript errors ([#6138](https://github.com/Hmbown/Codewhale/issues/6138)), and the gaps behind the opt-in approval timeout ([#6101](https://github.com/Hmbown/Codewhale/issues/6101)), `codewhale exec --hooks` ([#6099](https://github.com/Hmbown/Codewhale/issues/6099)), Markdown drag-copy ([#6156](https://github.com/Hmbown/Codewhale/issues/6156)), and the browsable, current-aware session picker ([#6014](https://github.com/Hmbown/Codewhale/issues/6014)); the goal token-budget hard stop ([#6013](https://github.com/Hmbown/Codewhale/issues/6013)) and the fleet no-progress guard shared with child workers ([#6015](https://github.com/Hmbown/Codewhale/issues/6015)) landed as first slices of two larger proposals, and the runtime-store session refusal ([#6207](https://github.com/Hmbown/Codewhale/issues/6207)).
@@ -41,6 +41,13 @@ tag, packages, checksums and release assets exist.
### Added
- TTFT and average output rate stay visible in the compact working footer when
space allows. `/statusline` now controls them separately; existing
`session_metrics` settings still work.
- StepFun’s current coding catalog includes Step 5 Preview, Step 3.7 Flash,
Step 3.5 Flash and its 2603 version, with published reasoning controls and
PAYG rates. Existing Step Plan routing remains subscription-based.
- `read` responses now always report the file's byte size, line count, and
whether output was truncated, and truncation footers name the total size
alongside the continuation offset — so paging through a large file is
@@ -969,7 +969,7 @@ fn config_editability_audit(app: &App) -> CommandResult {
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.",
"Writes [tui].metrics_line; hidden gives the row to the transcript, compact drops secondary counts and help while keeping selected TTFT/rate readings when they fit. Choose readings with /statusline.",
),
(
"subagents.enabled",
+20 -5
View File
@@ -2145,9 +2145,13 @@ pub enum StatusItem {
Tokens,
/// Prepaid remaining credit, refreshed once per turn completion.
Balance,
/// The metrics line's latency pair: `ttft NNNms` and `NN tok/s`, from
/// the same engine timings and provider usage `/status` prints in full.
/// Legacy configuration alias enabling both TTFT and output rate.
/// The picker expands it into independently editable readings.
SessionMetrics,
/// Mean measured time from request dispatch to the first token.
Ttft,
/// Provider output tokens divided by measured request time.
OutputRate,
/// Leaf directory of the session workspace, left-truncated when long.
/// Opt-in (#6112); off the default footer.
Workspace,
@@ -2171,7 +2175,8 @@ impl StatusItem {
StatusItem::Cost,
StatusItem::Cache,
StatusItem::Tokens,
StatusItem::SessionMetrics,
StatusItem::Ttft,
StatusItem::OutputRate,
]
}
@@ -2187,6 +2192,8 @@ impl StatusItem {
StatusItem::Tokens => "tokens",
StatusItem::Balance => "balance",
StatusItem::SessionMetrics => "session_metrics",
StatusItem::Ttft => "ttft",
StatusItem::OutputRate => "output_rate",
StatusItem::Workspace => "workspace",
StatusItem::GitBranch => "git_branch",
}
@@ -2210,6 +2217,8 @@ impl StatusItem {
| "rate_limit" => None,
"balance" => Some(Self::Balance),
"session_metrics" => Some(Self::SessionMetrics),
"ttft" => Some(Self::Ttft),
"output_rate" => Some(Self::OutputRate),
"workspace" => Some(Self::Workspace),
// Revived in #6112 as an opt-in metrics-line chip; it parses
// again, so a config written between its #5950 retirement and
@@ -2231,6 +2240,8 @@ impl StatusItem {
StatusItem::Tokens => "Output tokens",
StatusItem::Balance => "Account balance",
StatusItem::SessionMetrics => "Session metrics",
StatusItem::Ttft => "Time to first token",
StatusItem::OutputRate => "Output rate",
StatusItem::Workspace => "Workspace",
StatusItem::GitBranch => "Git branch",
}
@@ -2249,12 +2260,15 @@ impl StatusItem {
StatusItem::Tokens => "output tokens of the live or last turn",
StatusItem::Balance => "remaining prepaid credit from the active provider",
StatusItem::SessionMetrics => "time to first token and output rate",
StatusItem::Ttft => "average wait for the first token",
StatusItem::OutputRate => "average tok/s, including first-token wait",
StatusItem::Workspace => "directory this session writes to",
StatusItem::GitBranch => "branch the next commit lands on",
}
}
/// Every variant in display order — used by the picker to enumerate rows.
/// Editable items in display order. Legacy combined metrics parse but
/// expand to the two individual controls instead of appearing twice.
#[must_use]
pub fn all() -> &'static [StatusItem] {
&[
@@ -2265,7 +2279,8 @@ impl StatusItem {
StatusItem::Balance,
StatusItem::Cache,
StatusItem::Tokens,
StatusItem::SessionMetrics,
StatusItem::Ttft,
StatusItem::OutputRate,
StatusItem::Workspace,
StatusItem::GitBranch,
]
+15
View File
@@ -12351,6 +12351,21 @@ fn status_items_scenario() {
assert_eq!(StatusItem::Workspace.key(), "workspace");
assert_eq!(StatusItem::GitBranch.key(), "git_branch");
}
{
let tui: TuiConfig =
toml::from_str(r#"status_items = ["ttft", "output_rate", "session_metrics"]"#)
.expect("new and legacy metrics keys should parse");
assert_eq!(
tui.status_items,
Some(vec![
StatusItem::Ttft,
StatusItem::OutputRate,
StatusItem::SessionMetrics
])
);
assert_eq!(StatusItem::Ttft.key(), "ttft");
assert_eq!(StatusItem::OutputRate.key(), "output_rate");
}
// from status_items_deser_allows_missing_field
{
let toml_str = r#"
+17 -17
View File
@@ -19,16 +19,16 @@
//! inspector. Both are the floor and never shed. Everything else is a
//! metric: the session cost (the same number `/cost`, the roster and the
//! price widget print), time to first token, output rate and output tokens —
//! live while a turn streams, the last turn's figures when idle, and never
//! blank between turns once a turn has reported them.
//! measured latency/rate averages persist between receipts; the output count
//! updates during streaming. Missing measurements remain absent.
//!
//! The context reading is painted here and only here — the posture bar above
//! used to print the same percentage a second time from the same snapshot —
//! and at every fullness, not only from 50% up (#5950).
//!
//! Shed order as width drops: `tok/s`, `ttft`, `↓ tokens`, the help hint,
//! then the cost, then the balance ([`InfoSegmentId::shed_priority`]). The
//! model and `ctx NN%` never shed; below that floor the row clips at its
//! Shed order as width drops: cache, output count and billing tier, the help
//! hint, then rate and TTFT, then cost and balance
//! ([`InfoSegmentId::shed_priority`]). The model and `ctx NN%` never shed; below that floor the row clips at its
//! right edge.
//!
//! Which segments exist at all is the user's call: `/statusline` and
@@ -36,7 +36,8 @@
//! builds only the ones that are on. Shedding decides what survives the
//! width that is left. `tui.metrics_line` sizes the row (#5950): `hidden`
//! gives the line back to the transcript, and `compact` starts the shed
//! pass with the telemetry and the help hint already gone
//! pass with secondary counts and the help hint already gone; TTFT and rate
//! remain when selected and space allows
//! ([`InfoLine::compact`]).
//!
//! Interaction: segment geometry is recorded for parity tests, but only the
@@ -112,25 +113,25 @@ impl InfoSegmentId {
#[must_use]
pub fn shed_priority(self) -> u8 {
match self {
Self::Rate => 9,
Self::Cache => 8,
Self::Ttft => 8,
// Performance readings outlive help and secondary counts.
Self::Rate | Self::Ttft => 6,
Self::OutputTokens => 7,
// The tier is a reading about the cost, not the cost: it sheds
// with the telemetry, ahead of the number it annotates.
Self::BillingTier => 7,
Self::Cost => 6,
Self::Cost => 5,
// The balance outlives the cost: it is off by default, so a row
// that shows one is a row whose owner asked for it by name.
Self::Balance => 5,
Self::Balance => 4,
// An active goal is the session's deliberate long-running mode:
// its reading outlives every telemetry segment and sheds only
// ahead of the route and context readings.
Self::Goal => 4,
Self::Goal => 3,
// Workspace and branch are opt-in like the balance: a row that
// shows them is a row whose owner asked by name, so they shed
// with it, ahead of telemetry but behind the goal.
Self::Workspace | Self::GitBranch => 5,
Self::Workspace | Self::GitBranch => 4,
Self::Model | Self::Context => 0,
}
}
@@ -190,10 +191,9 @@ pub struct InfoLine<'a> {
/// [`glyphs::ascii_fallback`].
pub ascii_safe: bool,
/// `tui.metrics_line = "compact"` (#5950): the shed pass starts with
/// the telemetry (everything at or above
/// [`InfoSegmentId::SHED_BEFORE_HELP`]) and the help hint already gone,
/// so the row states the route, the context reading, the cost and the
/// balance. Width sheds the rest exactly as it always did.
/// secondary counts (everything at or above
/// [`InfoSegmentId::SHED_BEFORE_HELP`]) and the help hint already gone.
/// Selected TTFT and rate readings remain; width sheds the rest.
pub compact: bool,
}
@@ -265,7 +265,7 @@ fn shed_pass<'t>(info: &'t InfoLine<'_>, area: Rect) -> ShedRow<'t> {
let help = sym(info.help_hint, ascii);
let join_w = sym(ITEM_JOIN, ascii).width();
// A compact row is the full row after its first shed rungs: the
// telemetry and the help hint go before width is consulted.
// secondary counts and the help hint go before width is consulted.
let mut kept: Vec<&InfoSegment> = info
.segments
.iter()
+12 -12
View File
@@ -180,10 +180,9 @@ fn infoline_is_model_context_and_metrics_only() {
assert!(work.trim_end().ends_with("/help"), "{work:?}");
}
/// Declared shed order: `tok/s`, `ttft`, `↓ tokens`, the help hint, then
/// the cost. The model and `ctx NN%` are the floor at every width.
/// Secondary counts and help yield before performance readings and cost. The model and `ctx NN%` are the floor at every width.
#[test]
fn infoline_sheds_rate_then_ttft_then_tokens_then_help_then_cost() {
fn infoline_sheds_tokens_then_help_then_rate_then_ttft_then_cost() {
let segments = work_segments();
// The narrowest row that still shows a thing. A thing that sheds earlier
// needs a wider row to survive, so these strictly decrease down the
@@ -200,7 +199,7 @@ fn infoline_sheds_rate_then_ttft_then_tokens_then_help_then_cost() {
let help = narrowest_showing("help");
let cost = narrowest_showing("$0.42");
assert!(
rate > ttft && ttft > tokens && tokens > help && help > cost,
tokens > help && help > rate && rate > ttft && ttft > cost,
"shed order broke: rate@{rate} ttft@{ttft} tokens@{tokens} help@{help} cost@{cost}"
);
for w in 24..=180u16 {
@@ -213,12 +212,11 @@ fn infoline_sheds_rate_then_ttft_then_tokens_then_help_then_cost() {
}
/// `tui.metrics_line = "compact"` (#5950) is the row after its first shed
/// rungs, at any width: the telemetry (`tok/s`, `ttft`, `↓ tokens`) and
/// the help hint are gone before width is consulted, the route, the
/// context reading and the cost stay, and the hitboxes follow the same
/// rungs, at any width: output counts and help are gone before width is
/// consulted; selected TTFT/rate survive when they fit. Hitboxes follow the same
/// pass so a click still lands on what painted.
#[test]
fn infoline_compact_drops_the_telemetry_and_help_before_width_does() {
fn infoline_compact_keeps_performance_readings_without_extra_rows() {
let segments = work_segments();
let hint = help_hint();
let compact_row = |width: u16| -> (String, Vec<InfoSegmentId>) {
@@ -248,20 +246,22 @@ fn infoline_compact_drops_the_telemetry_and_help_before_width_does() {
let (wide, ids) = compact_row(160);
assert_eq!(
wide.trim_end(),
"deepseek-v4 · ctx 61% · $0.42",
"compact keeps the route, the reading and the price: {wide:?}"
"deepseek-v4 · ctx 61% · $0.42 · ttft 400ms · 38 tok/s",
"compact keeps performance, route, context and price: {wide:?}"
);
assert_eq!(
ids,
vec![
InfoSegmentId::Model,
InfoSegmentId::Context,
InfoSegmentId::Cost
InfoSegmentId::Cost,
InfoSegmentId::Ttft,
InfoSegmentId::Rate,
]
);
for w in 24..=180u16 {
let (row, _) = compact_row(w);
for gone in ["tok/s", "ttft", "1.2K", "help"] {
for gone in ["1.2K", "help"] {
assert!(
!row.contains(gone),
"{w}: compact never paints {gone}: {row:?}"
+6 -4
View File
@@ -404,8 +404,9 @@ mod tests {
#[test]
fn session_metrics_strip_is_on_by_default() {
assert!(
crate::config::StatusItem::default_footer()
.contains(&crate::config::StatusItem::SessionMetrics)
crate::config::StatusItem::default_footer().contains(&crate::config::StatusItem::Ttft)
&& crate::config::StatusItem::default_footer()
.contains(&crate::config::StatusItem::OutputRate)
);
assert_eq!(
crate::config::StatusItem::from_key("session_metrics"),
@@ -1366,8 +1367,9 @@ mod neutrality_tests {
#[test]
fn session_metrics_strip_is_on_by_default() {
assert!(
crate::config::StatusItem::default_footer()
.contains(&crate::config::StatusItem::SessionMetrics)
crate::config::StatusItem::default_footer().contains(&crate::config::StatusItem::Ttft)
&& crate::config::StatusItem::default_footer()
.contains(&crate::config::StatusItem::OutputRate)
);
assert_eq!(
crate::config::StatusItem::from_key("session_metrics"),
+49 -2
View File
@@ -271,7 +271,7 @@ pub(crate) fn info_segments(app: &App, width: u16) -> Vec<InfoSegment> {
// The DeepSeek-harness session metrics, from the same accumulators
// `/cost` prints: nothing here is estimated except the live stream's
// running token count, which the provider's receipt replaces.
if shows(StatusItem::SessionMetrics)
if (shows(StatusItem::SessionMetrics) || shows(StatusItem::Ttft))
&& let Some(ttft) = app.session_metrics.ttft_average()
{
segments.push(InfoSegment::new(
@@ -281,7 +281,7 @@ pub(crate) fn info_segments(app: &App, width: u16) -> Vec<InfoSegment> {
ChromeInk::MetadataValue,
));
}
if shows(StatusItem::SessionMetrics)
if (shows(StatusItem::SessionMetrics) || shows(StatusItem::OutputRate))
&& let Some(rate) = app.session_metrics.tokens_per_second()
{
segments.push(InfoSegment::new(
@@ -2729,6 +2729,53 @@ mod tests {
assert_eq!(rate(&app), None, "the existing status toggle still owns it");
}
#[test]
fn default_compact_footer_keeps_measured_performance_at_working_widths() {
use ratatui::{Terminal, backend::TestBackend};
let mut app = app_with_context_percent(60);
app.ui_locale = codewhale_localization::Locale::En;
app.status_items = StatusItem::default_footer();
app.metrics_line = crate::config::ChromeRowPreset::Compact;
app.session_metrics
.record_model_call(120, 4_800, Some(1_000), Some(5_000));
for width in [80, 100, 140] {
let mut terminal = Terminal::new(TestBackend::new(width, 1)).unwrap();
terminal
.draw(|frame| {
super::render_info_row(frame, &mut app, frame.area(), false);
})
.unwrap();
let row: String = terminal
.backend()
.buffer()
.content()
.iter()
.map(|cell| cell.symbol())
.collect();
assert!(row.contains("ttft 1.0s"), "{width}: {row}");
assert!(row.contains("24 avg tok/s"), "{width}: {row}");
assert!(!row.contains("/help"), "{width}: {row}");
}
}
#[test]
fn performance_readings_can_be_selected_independently() {
let mut app = app_with_context_percent(60);
app.session_metrics
.record_model_call(120, 4_800, Some(1_000), Some(5_000));
for (item, expected) in [
(StatusItem::Ttft, InfoSegmentId::Ttft),
(StatusItem::OutputRate, InfoSegmentId::Rate),
] {
app.status_items = vec![item];
let ids: Vec<_> = super::info_segments(&app, 80)
.into_iter()
.map(|s| s.id)
.collect();
assert_eq!(ids, vec![expected]);
}
}
/// Every remaining status item owns a segment, and an empty list leaves
/// the row with nothing but the help hint — no toggle in `/statusline`
/// paints something no toggle can remove.
+17 -12
View File
@@ -162,18 +162,15 @@ fn composed_frame_paints_each_fact_in_exactly_one_row() {
("model", model),
("cost", super::session_cost_label(&app)),
("agent count", "2 agents".to_string()),
(
"help hint",
crate::tui::shell_key_routing::info_help_hint(app.ui_locale),
),
("ttft", "ttft 400ms".to_string()),
];
facts.push(("context reading", format!("ctx {pct}%")));
facts.push(("output rate", "40 avg tok/s".to_string()));
if width >= 120 {
facts.push(("output rate", "40 avg tok/s".to_string()));
} else {
// The billing tier takes priority over rate at narrow widths.
assert_eq!(count_rows_containing(&rows, "40 avg tok/s"), 0);
facts.push((
"help hint",
crate::tui::shell_key_routing::info_help_hint(app.ui_locale),
));
}
for (name, needle) in facts {
if needle.is_empty() {
@@ -418,7 +415,7 @@ fn row_presets_reclaim_rows_and_quiet_them_in_the_composed_frame() {
assert_eq!(count_rows_containing(&rows, "deepseek-v4-pro"), 0);
// Compact both: the rows are back, quieter — the posture and the
// route/reading/price, none of the live facts or telemetry.
// route/reading/price and measured performance, without secondary counts.
app.posture_bar = ChromeRowPreset::Compact;
app.metrics_line = ChromeRowPreset::Compact;
let rows = draw(&mut app, width, height);
@@ -446,9 +443,10 @@ fn row_presets_reclaim_rows_and_quiet_them_in_the_composed_frame() {
"{:?}",
rows[metrics]
);
assert!(rows[metrics].contains("ttft 400ms"), "{}", rows[metrics]);
assert!(rows[metrics].contains("40 avg tok/s"), "{}", rows[metrics]);
for gone in [
"tok/s",
"ttft",
"↓ 1.2K",
crate::tui::shell_key_routing::info_help_hint(app.ui_locale).as_str(),
] {
assert!(
@@ -647,7 +645,14 @@ fn statusline_full_frame_presets_preserve_transcript_composer_and_hitboxes() {
assert_eq!(count_rows_containing(&rows, "ctx 0%"), 1, "{evidence}");
}
if metrics == ChromeRowPreset::Compact {
for shed in ["tok/s", "ttft", "Ctrl+/ help"] {
if width >= 60 {
assert!(rows[usize::from(height - 1)].contains("ttft"), "{evidence}");
assert!(
rows[usize::from(height - 1)].contains("avg tok/s"),
"{evidence}"
);
}
for shed in ["↓ 1.2K", "Ctrl+/ help"] {
assert!(!rows[usize::from(height - 1)].contains(shed), "{evidence}");
}
}
+100 -2
View File
@@ -12,7 +12,9 @@
//! read, so the checklist was decoration — a new variant belongs in
//! `crates/tui/src/config.rs` only once something paints it.
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use std::cell::RefCell;
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers, MouseButton, MouseEvent, MouseEventKind};
use ratatui::{
buffer::Buffer,
layout::Rect,
@@ -45,6 +47,7 @@ pub struct StatusPickerView {
/// Snapshot of `app.status_items` at open time so Esc reverts cleanly.
original: Vec<StatusItem>,
locale: Locale,
row_hitboxes: RefCell<Vec<(usize, Rect)>>,
}
impl StatusPickerView {
@@ -55,13 +58,21 @@ impl StatusPickerView {
.filter(|item| item.is_available_for(provider))
.copied()
.collect();
let selected: Vec<bool> = rows.iter().map(|item| active.contains(item)).collect();
let selected: Vec<bool> = rows
.iter()
.map(|item| {
active.contains(item)
|| (active.contains(&StatusItem::SessionMetrics)
&& matches!(item, StatusItem::Ttft | StatusItem::OutputRate))
})
.collect();
Self {
rows,
selected,
cursor: 0,
original: active.to_vec(),
locale,
row_hitboxes: RefCell::new(Vec::new()),
}
}
@@ -168,6 +179,30 @@ impl ModalView for StatusPickerView {
}
}
fn handle_mouse(&mut self, mouse: MouseEvent) -> ViewAction {
match mouse.kind {
MouseEventKind::ScrollUp => {
self.apply_motion(crate::tui::list_nav::Motion::Prev);
}
MouseEventKind::ScrollDown => {
self.apply_motion(crate::tui::list_nav::Motion::Next);
}
MouseEventKind::Down(MouseButton::Left) => {
let clicked = self.row_hitboxes.borrow().iter().find_map(|(index, rect)| {
rect.contains(ratatui::layout::Position::new(mouse.column, mouse.row))
.then_some(*index)
});
if let Some(index) = clicked {
self.cursor = index;
self.toggle_current();
return ViewAction::Emit(self.live_preview_event());
}
}
_ => {}
}
ViewAction::None
}
fn render(&self, area: Rect, buf: &mut Buffer) {
// Two header lines + one row per StatusItem + the wrapping action
// footer that now lives inside the body (one row more than the old
@@ -208,6 +243,7 @@ impl ModalView for StatusPickerView {
],
);
self.row_hitboxes.borrow_mut().clear();
let visible_rows = content.height.saturating_sub(2) as usize;
let row_start = visible_row_start(self.rows.len(), self.cursor, visible_rows);
@@ -225,6 +261,15 @@ impl ModalView for StatusPickerView {
.skip(row_start)
.take(visible_rows)
{
self.row_hitboxes.borrow_mut().push((
idx,
Rect::new(
content.x,
content.y + 2 + (idx - row_start) as u16,
content.width,
1,
),
));
let checked = *self.selected.get(idx).unwrap_or(&false);
let is_cursor = idx == self.cursor;
let mark = if checked { "[✓]" } else { "[ ]" };
@@ -306,6 +351,59 @@ mod tests {
assert_eq!(view.current_selection(), active);
}
#[test]
fn legacy_metrics_can_be_split_and_cancel_restores_the_saved_pair() {
let original = vec![StatusItem::SessionMetrics];
let mut view = StatusPickerView::new(&original, ApiProvider::Stepfun, Locale::En);
assert_eq!(
view.current_selection(),
vec![StatusItem::Ttft, StatusItem::OutputRate]
);
view.cursor = view
.rows
.iter()
.position(|item| *item == StatusItem::OutputRate)
.unwrap();
view.handle_key(KeyEvent::new(KeyCode::Char(' '), KeyModifiers::NONE));
assert_eq!(view.current_selection(), vec![StatusItem::Ttft]);
match view.handle_key(KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE)) {
ViewAction::EmitAndClose(ViewEvent::StatusItemsUpdated { items, final_save }) => {
assert_eq!(items, original);
assert!(!final_save);
}
action => panic!("unexpected cancel: {action:?}"),
}
}
#[test]
fn mouse_toggles_the_painted_row_after_scrolling_a_short_picker() {
let mut view = StatusPickerView::new(
&StatusItem::default_footer(),
ApiProvider::Stepfun,
Locale::En,
);
view.handle_key(KeyEvent::new(KeyCode::End, KeyModifiers::NONE));
let area = Rect::new(0, 0, 40, 12);
view.render(area, &mut Buffer::empty(area));
let (index, rect) = *view.row_hitboxes.borrow().last().expect("visible row");
let was_selected = view.selected[index];
let action = view.handle_mouse(MouseEvent {
kind: MouseEventKind::Down(MouseButton::Left),
column: rect.x + 2,
row: rect.y,
modifiers: KeyModifiers::NONE,
});
assert!(matches!(
action,
ViewAction::Emit(ViewEvent::StatusItemsUpdated {
final_save: false,
..
})
));
assert_eq!(view.cursor, index);
assert_eq!(view.selected[index], !was_selected);
}
#[test]
fn space_toggles_current_row_and_emits_live_preview() {
let active = StatusItem::default_footer();
@@ -218,6 +218,7 @@ fn workbench_settings_visual_evidence() {
("/fleet", "Coordinator", "fleet"),
("/plugin", "Extensions", "plugins"),
("/config", "Config", "settings"),
("/statusline", "Status", "statusline"),
] {
for (rows, cols) in SIZES {
let (_workspace, mut tui) = start(rows, cols, true);
+9 -6
View File
@@ -236,7 +236,7 @@ visible, or set `[tui].status_items` in `config.toml`. Each key owns exactly
one thing on screen: `mode` is the posture bar's plan/act/operate chip, and
`model`, `context_percent`, `cost`, `balance` (prepaid providers only:
DeepSeek, DeepSeekCN, OpenRouter, SiliconFlow), `cache`, `tokens` and
`session_metrics`, `workspace` and `git_branch` are segments of the metrics line below it. Omit
`ttft`, `output_rate`, `workspace` and `git_branch` are segments of the metrics line below it. Omit
`status_items` to keep the built-in default; set it to `[]` to strip the
metrics line down to the help hint.
@@ -260,19 +260,22 @@ ignored with a warning in the log.
`status_items` composes the rows; two size presets decide how much of each
row paints. `[tui].posture_bar` and `[tui].metrics_line` each take `full`,
`compact`, or `hidden`. The posture bar defaults to `full` so active controls
stay visible; the metrics line defaults to `compact` to keep routine telemetry
out of the working surface. These are also settable at runtime with
stay visible; the metrics line defaults to `compact` to keep selected performance
readings while removing secondary counts and help. These are also settable at runtime with
`/config posture_bar compact`. TOML values must be lowercase; `/config`
accepts either case. `compact` is the row after its first shed
rungs: the posture bar keeps its permission and mode chips — and the cap
warning, which is advice, not decoration — and drops the clocks, counts and
hint; the metrics line keeps the route, the context reading, the cost and
the balance, and drops the telemetry and the help hint. `hidden` gives the
the balance, plus selected TTFT and output rate when space allows, and drops
secondary counts and the help hint. `hidden` gives the
row back to the transcript. A small tmux pane can hide both rows without
touching what `/statusline` composes.
With `metrics_line = "full"`, `session_metrics` (on by default) paints the latency pair on the metrics
line: `ttft 1.5s` — the mean time to first streamed token — and `120 avg tok/s`,
Both `ttft` and `output_rate` are on by default and work in full or compact
rows. `/statusline` lets you toggle them separately; Space previews, Enter saves,
and Esc restores your previous settings. Legacy `session_metrics` still enables
both readings. The pair shows: `ttft 1.5s` — the mean time to first streamed token — and `120 avg tok/s`,
the session's provider-reported output tokens divided by the measured request
seconds for those same calls. The rate includes connection setup, time to first
token and pauses within a response, and excludes tools and idle time between
+18
View File
@@ -243,3 +243,21 @@ record exact build and installation receipts separately.
implement a visual treatment.
- **Don't** describe reconstructed cells as host screenshots or static captures
as motion proof, and don't imply every settings subview gained mouse parity.
## Working-screen performance readings (2026-09-20)
TTFT and output rate reuse the existing session accumulator. The compact footer
keeps selected performance readings when they fit, shedding secondary counts
and help first. `/statusline` offers separate Time to first token and Output
rate controls with immediate preview, Enter to save and Esc to restore. Old
`session_metrics` settings continue to enable both and become separate choices
when edited. Full, compact and hidden row settings remain in `/config`.
The motion focal point remains the shared activity marker: request progress,
verification and completion use one cadence and the existing bounded completion
settle. Numbers stay still between measured receipts, preserving legibility on
video. There is no synthetic live speed counter, new timer or extra footer row.
Reduced and still motion retain the same readings and explicit phase words.
TTFT is a session average; throughput includes first-token wait and stream
pauses, but excludes tool/idle gaps. Missing measurements stay absent.
+4 -2
View File
@@ -169,7 +169,7 @@ JSON 把凭据的 `source`(来源)与字面的 `availability`(可用性)
- 工作栏(Work bar):转录区上方的一条(或可选的侧栏),承载活动目标、待办列表和子智能体。行会保持整个会话——已完成的工作显示为"已完成"而不是消失——点击某一行(或对它按 `Enter`)会打开它的详情。
- 状态与底部区域:实时活动、排队的后续动作和简短命令提示。
底部区域可配置。运行 `/statusline` 选择哪些内容可见,或在 `config.toml` 里设置 `[tui].status_items`。每个键只对应屏幕上的一样东西:`mode` 是姿态栏的 plan/act/operate 片区,而 `model`、`context_percent`、`cost`、`balance`(仅限预付费提供商:DeepSeek、DeepSeekCN、OpenRouter、SiliconFlow)、`cache`、`tokens` 、`session_metrics`、`workspace` 和 `git_branch` 是它下方指标行的片区。
底部区域可配置。运行 `/statusline` 选择哪些内容可见,或在 `config.toml` 里设置 `[tui].status_items`。每个键只对应屏幕上的一样东西:`mode` 是姿态栏的 plan/act/operate 片区,而 `model`、`context_percent`、`cost`、`balance`(仅限预付费提供商:DeepSeek、DeepSeekCN、OpenRouter、SiliconFlow)、`cache`、`tokens` 、`ttft`、`output_rate`、`workspace` 和 `git_branch` 是它下方指标行的片区。
省略 `status_items` 以保持内置默认;把它设为 `[]` 只保留帮助提示。
`workspace` 和 `git_branch` 默认关闭。工作区片区显示文件夹名称;链接工作树会包含父目录以区分同名文件夹。分支片区显示当前分支或游离 HEAD 的短 SHA,并用 `(wt)` 标记链接工作树。名称过长时,保留末尾并限制为 24 个显示列。Git 信息沿用每 15 秒的后台刷新机制,也可按需刷新;无法取得 Git 信息时省略分支片区。这些信息对应当前会话的工作区,完整路径仍可在 `/status` 查看。
@@ -178,7 +178,7 @@ JSON 把凭据的 `source`(来源)与字面的 `availability`(可用性)
`status`、`agents`、`reasoning_replay`、`prefix_stability`、`last_tool_elapsed` 和 `rate_limit` 这些键在 0.9.13 中已退役:它们不驱动任何东西。旧的配置文件仍可加载——已退役的键会被忽略并在日志中给出警告。
`status_items` 负责组合这两行;另有两个尺寸预设决定每行绘制多少。`[tui].posture_bar` 和 `[tui].metrics_line` 各接受 `full`、`compact` 或 `hidden`。姿态栏默认使用 `full` 以保留操作提示;指标行默认使用 `compact`,减少常驻遥测信息,也可以在运行时用 `/config posture_bar compact` 设置。TOML 中的值必须使用小写;`/config` 命令不区分大小写。`compact` 是该行走完最初几级舍弃阶梯后的样子:姿态栏保留权限与模式片区——以及属于建议而非装饰的容量警示——并舍弃时钟、计数和提示;指标行保留路由、上下文读数、成本和余额,并舍弃遥测与帮助提示。`hidden` 把该行交还给转录区。狭小的 tmux 面板可以隐藏两行而不动 `/statusline` 的组合。
`status_items` 负责组合这两行;另有两个尺寸预设决定每行绘制多少。`[tui].posture_bar` 和 `[tui].metrics_line` 各接受 `full`、`compact` 或 `hidden`。姿态栏默认使用 `full` 以保留操作提示;指标行默认使用 `compact`,减少常驻遥测信息,也可以在运行时用 `/config posture_bar compact` 设置。TOML 中的值必须使用小写;`/config` 命令不区分大小写。`compact` 是该行走完最初几级舍弃阶梯后的样子:姿态栏保留权限与模式片区——以及属于建议而非装饰的容量警示——并舍弃时钟、计数和提示;指标行保留路由、上下文读数、成本和余额,并在空间足够时保留已选的 TTFT 和输出速率,舍弃次要计数与帮助提示。`hidden` 把该行交还给转录区。狭小的 tmux 面板可以隐藏两行而不动 `/statusline` 的组合。
`session_metrics`(默认开启)在指标行上显示 `ttft 1.5s`(到首个流式 token 的平均时间)和 `120 平均 tok/s`(本次会话中提供商报告的输出 token 总数,除以同一批调用的实测请求总秒数)。速率包含连接建立、首 token 等待以及响应过程中的停顿,不包含工具执行和调用之间的空闲时间;它衡量请求的实际吞吐量,而非模型解码速度。流式和非流式调用使用相同规则;没有独立请求计时的回执,其 token 和时间都不计入。新请求进行时保留上次实测平均值。两项读数与 `/status` 共用累加器,缺少证据时省略而不估算。在窄行上,这一对会先于成本和上下文读数被舍弃。
@@ -479,3 +479,5 @@ Codewhale 会保存会话。用 README 和模式指南里讲到的会话选择
读与你正在改动的东西相关的重点参考。对大多数用户,接下来的页面是安装、配置、提供商、模式、快捷键、工具和子智能体。
下一步:[INSTALL.md](INSTALL.md)、[CONFIGURATION.md](CONFIGURATION.md)、[PROVIDERS.md](PROVIDERS.md)、[MODES.md](MODES.md) 和 [TOOL_SURFACE.md](../TOOL_SURFACE.md)。
`/statusline` 可分别切换首 token 等待时间(`ttft`)和平均输出速率(`output_rate`)。Space 预览,Enter 保存,Esc 撤销。旧的 `session_metrics` 配置仍会启用两项读数。
+5 -5
View File
@@ -37,7 +37,7 @@ export const CHANGELOG: ChangelogRelease[] = [
{
"heading": "Contributors",
"items": [
"@AdityaVG13 — fixed composer wrapping, tab/caret placement, pasted and editor-returned draft history, painted-column transcript copying, explicit terminal foregrounds, and headless user-input tool availability (#6363, #6365).",
"@AdityaVG13 — fixed composer wrapping, tab/caret placement, pasted and editor-returned draft history, painted-column transcript copying, explicit terminal foregrounds, headless user-input tool availability, and engine synchronization after importing foreign sessions (#6369, #6363, #6365).",
"@aboimpinto — moved the TUI session-export slice onto shared command contracts (FEAT-025): a session-export contract facet with one shared sanitizer, /export routed through the facet, pinned with baseline-captured goldens and gates (#6096).",
"@BX166 — contributed the AICraft provider template and its documentation (#6171). It was closed unmerged, but it is what surfaced the decision to stop special-casing named OpenAI-compatible hosts (#6289).",
"@7jrxt42BxFZo4iAnN4CX — reported the session-retention defects behind archive-past-the-cap and empty-session cap occupancy (#6136, #6137), the resume-failure design behind durable transcript errors (#6138), and the gaps behind the opt-in approval timeout (#6101), codewhale exec --hooks (#6099), Markdown drag-copy (#6156), and the browsable, current-aware session picker (#6014); the goal token-budget hard stop (#6013) and the fleet no-progress guard shared with child workers…",
@@ -59,6 +59,8 @@ export const CHANGELOG: ChangelogRelease[] = [
{
"heading": "Added",
"items": [
"TTFT and average output rate stay visible in the compact working footer when space allows. /statusline now controls them separately; existing session_metrics settings still work.",
"StepFun’s current coding catalog includes Step 5 Preview, Step 3.7 Flash, Step 3.5 Flash and its 2603 version, with published reasoning controls and PAYG rates. Existing Step Plan routing remains subscription-based.",
"read responses now always report the file's byte size, line count, and whether output was truncated, and truncation footers name the total size alongside the continuation offset — so paging through a large file is deliberate instead of a surprise (#6283).",
"File edits are parse-gated before the write lands: Rust goes through syn::parse_file for a grammar-exact line:column, and .toml / .json through the parsers already vendored. An edit is refused only when the file parsed *before* and would not parse *after* — repairing an already-broken file is the commonest reason to edit source at all, so pre-existing breakage and new files fail open. The check precedes the write, so a rejection leaves the file untouched and apply_patch…",
"Rust files that were already rustfmt-clean are re-normalized after an edit, so the next patch's anchors still match. Hand-formatted files are never rewritten, and every failure path skips and lets the edit land (#6205, #6151).",
@@ -68,11 +70,9 @@ export const CHANGELOG: ChangelogRelease[] = [
"The Runtime API serves the workspace files a native client browses and edits: GET /v1/workspace/files lists one directory, GET /v1/workspace/files/read returns a bounded byte window with a whole-file SHA-256 revision, and PUT /v1/workspace/files writes atomically through the confined opener with revision-checked overwrites (409 on drift). .git is never served and symlinks are never followed. A saved session's oversized tool outputs are served as artifacts at GET…",
"A session that ended mid-turn is no longer invisible to the model. The newest workspace-scoped session still holding a crash-recovery checkpoint is surfaced as a one-line ## Prior Session notice in the session-pinned prompt prefix — metadata reads only, excluding the live session and any session this process instance created. Clean sessions get no block, so their prefix bytes are unchanged. Two bounded read-only tools, session_search and session_get, give the model…",
"codewhale exec --hooks opts a headless run into the same HookExecutor the TUI builds — global config, reviewed plugin snapshots, and trusted project hooks.toml. Headless runs previously fired no hooks at all. tool_call_before can still deny and shell_env still applies; a hook ask resolves fail-closed without a terminal. Fleet worker subprocesses never opt in, and permissions.toml typed rules are unchanged (#6099).",
"codewhale doctor flags fleet and profile model pins that the provider's own roster no longer offers. A pin is reported only when a *fresh* cached live roster for that exact route exists and omits it — stale, failed, or absent rosters prove nothing and are counted as unverifiable rather than raising a false warning. Each row names the route and every owner of the pin; the pin is surfaced, never rewritten (#6035).",
"Background-capable clients can enumerate owned work and watch TUI-visible conditions: GET /v1/threads/running lists threads with queued or in-progress turns in one call (one turns scan grouped by thread), and GET /v1/threads/{id}/notices serves active subagent-terminal, elevation-needed, and model-notify notices with thread/turn identity, cleared by ack or — for elevation — when the tool call completes (#6180, #3757).",
"Sub-agent launches adapt to provider throttling: a DynamicGate replaces the fixed semaphore so launch capacity adjusts at runtime, and a RateLimitGovernor halves capacity on 429 pressure, pauses admissions past the threshold, and recovers additively; 429 retries honor Retry-After with jittered backoff, and quota exhaustion keeps the failure path (#6055)."
"codewhale doctor flags fleet and profile model pins that the provider's own roster no longer offers. A pin is reported only when a *fresh* cached live roster for that exact route exists and omits it — stale, failed, or absent rosters prove nothing and are counted as unverifiable rather than raising a false warning. Each row names the route and every owner of the pin; the pin is surfaced, never rewritten (#6035)."
],
"itemCount": 20
"itemCount": 22
},
{
"heading": "Changed",
+27 -3
View File
@@ -37,7 +37,7 @@ export interface RepoFacts {
}
export const FACTS: RepoFacts = {
"generatedAt": "2026-09-20T01:09:49.496Z",
"generatedAt": "2026-09-20T04:56:43.746Z",
"sourceRevision": null,
"sourceCommittedAt": null,
"version": "0.10.0",
@@ -326,6 +326,30 @@ export const FACTS: RepoFacts = {
}
],
"models": [
{
"id": "step-3.5-flash",
"provider": "StepFun",
"contextWindow": 256000,
"maxOutput": null,
"reasoning": true,
"addedAt": "2026-09-19"
},
{
"id": "step-3.5-flash-2603",
"provider": "StepFun",
"contextWindow": 256000,
"maxOutput": null,
"reasoning": true,
"addedAt": "2026-09-19"
},
{
"id": "step-5-preview",
"provider": "StepFun",
"contextWindow": 1000000,
"maxOutput": 1000000,
"reasoning": true,
"addedAt": "2026-09-19"
},
{
"id": "qwen3.5-flash",
"provider": "Qwen",
@@ -776,10 +800,10 @@ export const FACTS: RepoFacts = {
},
{
"id": "step-3.7-flash",
"provider": null,
"provider": "StepFun",
"contextWindow": 256000,
"maxOutput": 256000,
"reasoning": false,
"reasoning": true,
"addedAt": "2026-06-12"
},
{
+3 -1
View File
@@ -227,6 +227,7 @@ const MODEL_PROVIDER_LABELS = {
Moonshot: "Moonshot/Kimi",
Zai: "Z.ai",
Minimax: "MiniMax",
Stepfun: "StepFun",
Qwen: "Qwen",
Arcee: "Arcee",
Together: "Together",
@@ -250,7 +251,7 @@ const MODEL_PREFIX_PROVIDERS = {
moonshotai: "Moonshot", minimax: "Minimax", qwen: "Qwen",
"arcee-ai": "Arcee", together: "Together", nvidia: "Other",
"opencode-go": "Moonshot", openai: "OpenAi", google: "Google",
xai: "Xai", mistral: "Mistral", stepfun: "Other", meta: "Meta",
xai: "Xai", mistral: "Mistral", stepfun: "Stepfun", meta: "Meta",
huggingface: "Other",
};
@@ -273,6 +274,7 @@ const MODEL_NAME_FAMILIES = [
[/^(kimi|moonshot)/i, "Moonshot"],
[/^(glm-|zai)/i, "Zai"],
[/^(minimax|abab)/i, "Minimax"],
[/^step(?:-|audio)/i, "Stepfun"],
[/^(qwen|qwq)/i, "Qwen"],
[/^(arcee|trinity|afm|virtuoso|maestro|spotlight|blitz)/i, "Arcee"],
[/^(mimo|xiaomi)/i, "XiaomiMimo"],