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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions code-rs/tui/src/app/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1259,6 +1259,25 @@ async fn token_usage_update_refreshes_status_line_with_runtime_context_window()
);
}

#[tokio::test]
async fn token_usage_update_refreshes_status_line_with_prompt_cache_hit_rate() {
let mut app = make_test_app().await;
app.chat_widget.setup_status_line(
vec![crate::bottom_pane::StatusLineItem::PromptCacheHitRate],
/*use_theme_colors*/ true,
);

assert_eq!(app.chat_widget.status_line_text(), None);

app.handle_thread_event_now(ThreadBufferedEvent::Notification(token_usage_notification(
ThreadId::new(),
"turn-1",
Some(950_000),
)));

assert_eq!(app.chat_widget.status_line_text(), Some("cache 25%".into()));
}

#[tokio::test]
async fn open_agent_picker_keeps_missing_threads_for_replay() -> Result<()> {
let mut app = Box::pin(make_test_app()).await;
Expand Down
7 changes: 7 additions & 0 deletions code-rs/tui/src/bottom_pane/status_line_setup.rs
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,9 @@ pub(crate) enum StatusLineItem {
/// Total output tokens generated.
TotalOutputTokens,

/// Prompt-cache hit rate for reported cached input tokens.
PromptCacheHitRate,

/// Full session UUID.
SessionId,

Expand Down Expand Up @@ -162,6 +165,9 @@ impl StatusLineItem {
StatusLineItem::UsedTokens => "Total tokens used in session (omitted when zero)",
StatusLineItem::TotalInputTokens => "Total input tokens used in session",
StatusLineItem::TotalOutputTokens => "Total output tokens used in session",
StatusLineItem::PromptCacheHitRate => {
"Prompt-cache hit rate (omitted when no cached input is reported)"
}
StatusLineItem::SessionId => {
"Current session identifier (omitted until session starts)"
}
Expand Down Expand Up @@ -193,6 +199,7 @@ impl StatusLineItem {
StatusLineItem::UsedTokens => StatusSurfacePreviewItem::UsedTokens,
StatusLineItem::TotalInputTokens => StatusSurfacePreviewItem::TotalInputTokens,
StatusLineItem::TotalOutputTokens => StatusSurfacePreviewItem::TotalOutputTokens,
StatusLineItem::PromptCacheHitRate => StatusSurfacePreviewItem::PromptCacheHitRate,
StatusLineItem::SessionId => StatusSurfacePreviewItem::SessionId,
StatusLineItem::FastMode => StatusSurfacePreviewItem::FastMode,
StatusLineItem::RawOutput => StatusSurfacePreviewItem::RawOutput,
Expand Down
3 changes: 2 additions & 1 deletion code-rs/tui/src/bottom_pane/status_line_style.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@ impl StatusLineAccent {
| StatusLineItem::ContextWindowSize
| StatusLineItem::UsedTokens
| StatusLineItem::TotalInputTokens
| StatusLineItem::TotalOutputTokens => Self::Usage,
| StatusLineItem::TotalOutputTokens
| StatusLineItem::PromptCacheHitRate => Self::Usage,
StatusLineItem::FiveHourLimit | StatusLineItem::WeeklyLimit => Self::Limit,
StatusLineItem::CodexVersion | StatusLineItem::SessionId => Self::Metadata,
StatusLineItem::FastMode | StatusLineItem::RawOutput => Self::Mode,
Expand Down
3 changes: 3 additions & 0 deletions code-rs/tui/src/bottom_pane/status_surface_preview.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ pub(crate) enum StatusSurfacePreviewItem {
UsedTokens,
TotalInputTokens,
TotalOutputTokens,
PromptCacheHitRate,
SessionId,
FastMode,
RawOutput,
Expand Down Expand Up @@ -54,6 +55,7 @@ impl StatusSurfacePreviewItem {
StatusSurfacePreviewItem::UsedTokens => "0 used",
StatusSurfacePreviewItem::TotalInputTokens => "0 in",
StatusSurfacePreviewItem::TotalOutputTokens => "0 out",
StatusSurfacePreviewItem::PromptCacheHitRate => "cache 0%",
StatusSurfacePreviewItem::SessionId => "550e8400-e29b-41d4",
StatusSurfacePreviewItem::FastMode => "Fast on",
StatusSurfacePreviewItem::RawOutput => "raw output",
Expand Down Expand Up @@ -83,6 +85,7 @@ impl StatusSurfacePreviewItem {
Self::UsedTokens,
Self::TotalInputTokens,
Self::TotalOutputTokens,
Self::PromptCacheHitRate,
Self::SessionId,
Self::FastMode,
Self::RawOutput,
Expand Down
5 changes: 5 additions & 0 deletions code-rs/tui/src/chatwidget/status_surfaces.rs
Original file line number Diff line number Diff line change
Expand Up @@ -639,6 +639,10 @@ impl ChatWidget {
"{} out",
format_tokens_compact(self.status_line_total_usage().output_tokens)
)),
StatusLineItem::PromptCacheHitRate => self
.status_line_total_usage()
.prompt_cache_hit_rate_percent()
.map(|hit_rate| format!("cache {hit_rate}%")),
StatusLineItem::SessionId => self.thread_id.map(|id| id.to_string()),
StatusLineItem::FastMode => Some(
if matches!(self.current_service_tier(), Some(ServiceTier::Fast)) {
Expand Down Expand Up @@ -687,6 +691,7 @@ impl ChatWidget {
StatusSurfacePreviewItem::UsedTokens => StatusLineItem::UsedTokens,
StatusSurfacePreviewItem::TotalInputTokens => StatusLineItem::TotalInputTokens,
StatusSurfacePreviewItem::TotalOutputTokens => StatusLineItem::TotalOutputTokens,
StatusSurfacePreviewItem::PromptCacheHitRate => StatusLineItem::PromptCacheHitRate,
StatusSurfacePreviewItem::SessionId => StatusLineItem::SessionId,
StatusSurfacePreviewItem::FastMode => StatusLineItem::FastMode,
StatusSurfacePreviewItem::RawOutput => StatusLineItem::RawOutput,
Expand Down
13 changes: 11 additions & 2 deletions code-rs/tui/src/status/card.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ pub(crate) struct StatusTokenUsageData {
total: i64,
input: i64,
output: i64,
prompt_cache_hit_rate_percent: Option<i64>,
context_window: Option<StatusContextWindowData>,
}

Expand Down Expand Up @@ -316,6 +317,7 @@ impl StatusHistoryCell {
total: total_usage.blended_total(),
input: total_usage.non_cached_input(),
output: total_usage.output_tokens,
prompt_cache_hit_rate_percent: total_usage.prompt_cache_hit_rate_percent(),
context_window,
};
let rate_limits = if rate_limits.len() <= 1 {
Expand Down Expand Up @@ -354,7 +356,7 @@ impl StatusHistoryCell {
let input_fmt = format_tokens_compact(self.token_usage.input);
let output_fmt = format_tokens_compact(self.token_usage.output);

vec![
let mut spans = vec![
Span::from(total_fmt),
Span::from(" total "),
Span::from(" (").dim(),
Expand All @@ -364,7 +366,14 @@ impl StatusHistoryCell {
Span::from(output_fmt).dim(),
Span::from(" output").dim(),
Span::from(")").dim(),
]
];

if let Some(hit_rate) = self.token_usage.prompt_cache_hit_rate_percent {
spans.push(Span::from(" ").dim());
spans.push(Span::from(format!("cache {hit_rate}%")).dim());
}

spans
}

fn context_window_spans(&self) -> Option<Vec<Span<'static>>> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ expression: sanitized
│ Permissions: Custom (workspace with network access, on-request) │
│ Agents.md: <none> │
│ │
│ Token usage: 1.05K total (700 input + 350 output)
│ Token usage: 1.05K total (700 input + 350 output) cache 22%
│ Context window: 100% left (1.45K used / 272K) │
│ 5h limit: [████████░░░░░░░░░░░░] 40% left (resets 11:32) │
│ Weekly limit: [█████████████░░░░░░░] 65% left (resets 11:52) │
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ expression: sanitized
│ Permissions: Custom (workspace with network access, on-request) │
│ Agents.md: <none> │
│ │
│ Token usage: 2K total (1.4K input + 600 output)
│ Token usage: 2K total (1.4K input + 600 output) cache 7%
│ Context window: 100% left (2.2K used / 272K) │
│ 5h limit: [███████████░░░░░░░░░] 55% left (resets 09:25) │
│ Weekly limit: [██████████████░░░░░░] 70% left (resets 09:55) │
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ expression: sanitized
│ Permissions: Workspace (on-request) │
│ Agents.md: <none> │
│ │
│ Token usage: 1.9K total (1K input + 900 output)
│ Token usage: 1.9K total (1K input + 900 output) cache 17%
│ Context window: 100% left (2.25K used / 272K) │
│ 5h limit: [██████░░░░░░░░░░░░░░] 28% left (resets 03:14) │
│ Weekly limit: [███████████░░░░░░░░░] 55% left (resets 03:24) │
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ expression: sanitized
│ Permissions: Custom (workspace with network access, on-request) │
│ Agents.md: <none> │
│ │
│ Token usage: 1.9K total (1K input + 900 output)
│ Token usage: 1.9K total (1K input + 900 output) cache 17%
│ Context window: 100% left (2.25K used / 272K) │
│ 5h limit: [██████░░░░░░░░░░░░░░] 28% left (resets 03:14) │
│ Weekly limit: [████████████░░░░░░░░] 60% left (resets 03:34) │
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ expression: sanitized
│ Permissions: Custom (workspace with network access, on-reque │
│ Agents.md: <none> │
│ │
│ Token usage: 1.9K total (1K input + 900 output)
│ Token usage: 1.9K total (1K input + 900 output) cache 17%
│ Context window: 100% left (2.25K used / 272K) │
│ 5h limit: [██████░░░░░░░░░░░░░░] 28% left (resets 03:14) │
╰────────────────────────────────────────────────────────────────────╯
54 changes: 51 additions & 3 deletions code-rs/tui/src/status/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -981,7 +981,7 @@ async fn status_snapshot_hides_when_has_no_credits_flag() {
}

#[tokio::test]
async fn status_card_token_usage_excludes_cached_tokens() {
async fn status_card_token_usage_shows_prompt_cache_hit_rate() {
let temp_home = TempDir::new().expect("temp home");
let mut config = test_config(&temp_home).await;
config.model = Some("gpt-5.1-codex-max".to_string());
Expand Down Expand Up @@ -1021,8 +1021,56 @@ async fn status_card_token_usage_excludes_cached_tokens() {
let rendered = render_lines(&composite.display_lines(/*width*/ 120));

assert!(
rendered.iter().all(|line| !line.contains("cached")),
"cached tokens should not be displayed, got: {rendered:?}"
rendered
.iter()
.any(|line| line.contains("Token usage:") && line.contains("cache 17%")),
"expected prompt-cache hit rate in token usage, got: {rendered:?}"
);
}

#[tokio::test]
async fn status_card_token_usage_omits_prompt_cache_when_no_cached_tokens() {
let temp_home = TempDir::new().expect("temp home");
let mut config = test_config(&temp_home).await;
config.model = Some("gpt-5.1-codex-max".to_string());
config.cwd = test_path_buf("/workspace/tests").abs();

let account_display = test_status_account_display();
let usage = TokenUsage {
input_tokens: 1_200,
cached_input_tokens: 0,
output_tokens: 900,
reasoning_output_tokens: 0,
total_tokens: 2_100,
};

let now = chrono::Local
.with_ymd_and_hms(2024, 1, 1, 0, 0, 0)
.single()
.expect("timestamp");

let model_slug = crate::legacy_core::test_support::get_model_offline(config.model.as_deref());
let token_info = token_info_for(&model_slug, &config, &usage);
let composite = new_status_output(
&config,
account_display.as_ref(),
Some(&token_info),
&usage,
&None,
/*thread_name*/ None,
/*forked_from*/ None,
/*rate_limits*/ None,
None,
now,
&model_slug,
/*collaboration_mode*/ None,
/*reasoning_effort_override*/ None,
);
let rendered = render_lines(&composite.display_lines(/*width*/ 120));

assert!(
rendered.iter().all(|line| !line.contains("cache")),
"prompt-cache hit rate should be omitted without cached input, got: {rendered:?}"
);
}

Expand Down
10 changes: 10 additions & 0 deletions code-rs/tui/src/token_usage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,16 @@ impl TokenUsage {
(self.non_cached_input() + self.output_tokens.max(0)).max(0)
}

pub(crate) fn prompt_cache_hit_rate_percent(&self) -> Option<i64> {
let input = self.input_tokens.max(0);
let cached = self.cached_input();
if input == 0 || cached == 0 {
return None;
}

Some(((cached as f64 / input as f64) * 100.0).clamp(0.0, 100.0).round() as i64)
}

pub(crate) fn tokens_in_context_window(&self) -> i64 {
self.total_tokens
}
Expand Down
2 changes: 1 addition & 1 deletion docs/codex-fork-parity-ledger.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ preserving the old module boundary.
| App-server v1 compatibility and generated schemas | Deleted `code-rs/app-server-protocol/schema/json/v1/*`, root `EventMsg.json`, root TypeScript request/event models, `code-rs/app-server-protocol/src/protocol/v2.rs`, and removed remote skill schema files | Current protocol is Codex-shaped and Desktop-oriented, with PR #393 shimming narrow Desktop startup gaps. Some v1 conversation concepts may map to v2 threads; others may be obsolete. | Rewrite | Do not restore v1 wholesale. For each external client expectation, add a compatibility fixture or document the Codex v2 equivalent. |
| TUI settings, overlays, and approval UX | `code-rs/tui/src/bottom_pane/settings_overlay.rs`, many settings section views, `approval_modal_view.rs`, `approval_ui.rs`, `request_user_input_view.rs`, `model_selection_view.rs`, `undo_timeline_view.rs`, `validation_settings_view.rs`, `verbosity_selection_view.rs`, `user_approval_widget.rs`, `settings_overlay_agents.rs`, and settings snapshots | Codex-base has its own TUI app, settings, approval, and status surfaces. Every Code-specific settings sections for agents, GitHub, Auto Drive, review, validation, notifications, and planning need explicit decisions. | Rewrite | Split by feature domain. Preserve user workflows such as agent settings, approval clarity, and validation controls where still required. |
| TUI history cells and transcript rendering | Deleted `code-rs/tui/src/history_cell/*`, `code-rs/tui/src/history/compat.rs`, `code-rs/tui/src/chatwidget/*` modules for agents, terminal, tools, web search, rate limits, streaming, history render, plus `vt100_chatwidget_snapshot.rs` and many snapshots | Current TUI has Codex-native history/rendering modules and snapshots. Some Every Code-specific cells represented Auto Drive, browser sessions, agents, rate limits, context, and tool grouping. | Rewrite | Preserve snapshot coverage for required overlays. Avoid restoring old visual architecture unless it carries behavior current TUI cannot express. |
| Token, rate-limit, and prompt-cache diagnostics | `code-rs/core/src/token_data.rs`, `code-rs/core/src/account_usage.rs`, `code-rs/tui/src/chatwidget/limits_overlay.rs`, `code-rs/tui/src/rate_limits_view.rs`, `code-rs/tui/src/history_cell/rate_limits.rs`, `codex_tui__chatwidget__tests__limits_*.snap` | Codex-base has token usage replay, app-server token events, login token data, and TUI token usage. Prompt-cache hit-rate and old limits UI parity are not proven. | Rewrite | #401 owns the token/prompt-cache audit. Add missing prompt-cache/rate-limit fixtures before UI work. |
| Token, rate-limit, and prompt-cache diagnostics | `code-rs/core/src/token_data.rs`, `code-rs/core/src/account_usage.rs`, `code-rs/tui/src/chatwidget/limits_overlay.rs`, `code-rs/tui/src/rate_limits_view.rs`, `code-rs/tui/src/history_cell/rate_limits.rs`, `codex_tui__chatwidget__tests__limits_*.snap` | Codex-base has token usage replay, app-server token events, login token data, and TUI token usage. The first #401 slice restores prompt-cache hit-rate display from existing cached-token telemetry; schema-level telemetry-present semantics and old limits UI parity remain open. See [token-diagnostics-parity.md](token-diagnostics-parity.md). | Rewrite | #401 owns the token/prompt-cache audit. Add telemetry-present and old limits/rate diagnostics fixtures before deeper UI work. |
| Agents, multi-agent workflows, and selectors | `code-rs/core/src/agent_defaults.rs`, `code-rs/core/src/agent_tool.rs`, `code-rs/core/tests/agent_completion_wake.rs`, `code-rs/core/tests/antigravity_agent_spec.rs`, `code-rs/tui/src/chatwidget/agent*.rs`, `code-rs/tui/src/history_cell/agent.rs`, `vt100_chatwidget_snapshot__agent_*.snap`, `settings_overlay_agents.rs` | Docs still describe built-in multi-agent selectors. Current `main` may contain newer agent infrastructure, but old TUI/status/test parity needs confirmation. | Port | Create focused fixtures for selector resolution, agent run grouping, agent status errors, and settings UI before changing agent orchestration. |
| Config, identity, skills, and prompts | Deleted old `code-rs/core/src/config*`, `config_loader/*`, `custom_prompts.rs`, `slash_commands.rs`, `skills/*`, `external_agent_config.rs`, and tests such as `custom_prompts_discovery.rs`, `skill_command_policy.rs`, `prompt_context_dedup.rs`, `external_agent_config` snapshots | PR #391 and #394 restored important config compatibility, including `CODE_HOME` precedence and legacy `[tui] alternate_screen`. Current Codex-base has its own config, prompt, and skills systems. | Rewrite | Keep compatibility tests for `CODE_HOME`, legacy config shapes, prompts, skills, and external agent import. Mark individual probes covered only after linking current tests. |
| Exec, sandbox, patch, and validation harness | Deleted old `patch_harness.rs`, `workflow_validation.rs`, `dry_run_guard.rs`, `command_safety/*`, `git_worktree.rs`, `exec_command/*`, `tool_hooks.rs`, `git_mutation_guard.rs`, `stuck_exec.rs`, `exec_completion_test.rs`, `wayland_clipboard_feature_regression.rs`, `windows_altgr.rs` | Codex-base has richer exec/sandbox/apply-patch crates. Every Code-specific validator discovery and git safety behavior must be compared rather than restored wholesale. | Rewrite | Add fixtures for changed-file validation, git mutation safety, shell completion, and platform shortcut regressions only where Codex-base lacks equivalent tests. |
Expand Down
3 changes: 1 addition & 2 deletions docs/release-notes/RELEASE_NOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,7 @@ This release reduces prompt-cache churn so repeated Every Code turns are less li
### Changes

- Stable prompt content now stays ahead of volatile context such as CTX_UI timelines, environment snapshots, and Auto Review ledger details, helping providers reuse cached input across turns.
- The TUI footer now shows prompt-cache hit rate when the provider reports cached input token telemetry, including a token-weighted rolling average for recent turns.
- App-server protocol schemas now expose whether cached input token telemetry was reported, so missing cache data is not confused with a real 0% cache hit.
- `/status` and the configurable TUI footer status line now show prompt-cache hit rate when cached input tokens are non-zero.

### Install

Expand Down
Loading