You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
fix(tui): move ACP session usage into footer context (#441)
## Summary
- route ACP `SessionUpdateInfo::Usage` updates into the footer context
segment instead of rendering the verbose `Session usage: ...` history
line
- use structured ACP session usage as the primary context source and
only fall back to provider-specific transcript parsing when no ACP usage
updates have arrived
- update transcript replay, view-only transcript behavior, docs, and
snapshots so the footer shows values like `Context 16% (42.6K)`
consistently
## Why
The new ACP usage history render was technically correct but too noisy
in the transcript and interrupted more important output. The footer
already has a dedicated context surface, so this change keeps the usage
signal visible without adding extra transcript clutter.
## Impact
Users on the ACP-backed Nori CLI now see context usage in the footer
instead of as a standalone history item, and replayed sessions restore
that footer state through the structured protocol path.
## Test Plan
- `just fmt`
- `cargo test -p nori-protocol`
- `cargo test -p codex-acp`
- `cargo test -p nori-tui`
- `cargo test`
- `cargo build --bin nori`
- `cargo test -p tui-pty-e2e`
- manual tmux verification with `elizacp`: launch `nori`, assert prompt
appears, submit `hello`, assert prompt returns
## Notes
- originally developed as a short stacked follow-up, then rebased onto
the latest `main` after the parent work landed
Copy file name to clipboardExpand all lines: codex-rs/acp/docs.md
+3-3Lines changed: 3 additions & 3 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -89,7 +89,7 @@ The live backend path in `user_input.rs`, `submit_and_ops.rs`, `spawn_and_relay.
89
89
90
90
Metadata notifications that ACP permits while idle are treated as session-owned rather than request-owned. `AvailableCommandsUpdate`, `CurrentModeUpdate`, `ConfigOptionUpdate`, `SessionInfoUpdate`, and `UsageUpdate` no longer produce "no request is active" warnings; instead the reducer persists the latest values and forwards normalized `ClientEvent`s downstream.
91
91
92
-
`session/load` replay also preserves more session context than before. User-side `MessageDelta { stream: User, .. }` values are reassembled into `ReplayEntry::UserMessage`, while `SessionUpdateInfo` notes pass through unchanged so restored transcripts can show lightweight session metadata alongside the loaded conversation.
92
+
`session/load` replay also preserves more session context than before. User-side `MessageDelta { stream: User, .. }` values are reassembled into `ReplayEntry::UserMessage`, while `SessionUpdateInfo` notes pass through unchanged. For usage updates, that replay path now restores the structured footer context state without needing to re-render the verbose message in history.
@@ -494,9 +494,9 @@ Codex `token_count` events contain two token usage objects with different semant
494
494
| Object | Meaning | Used For |
495
495
|--------|---------|----------|
496
496
|`total_token_usage`| Cumulative billing counter across ALL API calls in the session; grows unboundedly |`input_tokens`, `output_tokens`, `cached_tokens` fields (the "Tokens" footer segment) |
497
-
|`last_token_usage`| Tokens from the most recent API call only; represents actual context window fill |`last_context_tokens` field (the "Context: XK (Y%)" footer segment)|
497
+
|`last_token_usage`| Tokens from the most recent API call only; represents actual context window fill |`last_context_tokens` field used by transcript-discovery fallback for the "Context Y% (XK)" footer segment |
498
498
499
-
Using `total_token_usage.input_tokens` for context window percentage would produce nonsensical results (e.g., 995K tokens for a 258K context window) because the cumulative counter sums across all turns. The `last_token_usage.input_tokens` correctly reflects how full the context window is for the current turn. When `last_token_usage` is absent (older transcript formats), `last_context_tokens` is `None` and the context percentage is not displayed.
499
+
Using `total_token_usage.input_tokens` for context window percentage would produce nonsensical results (e.g., 995K tokens for a 258K context window) because the cumulative counter sums across all turns. The `last_token_usage.input_tokens` correctly reflects how full the context window is for the current turn. When ACP `UsageUpdate` events are present, the TUI prefers those live session values for the footer; transcript parsing remains a fallback for older agents/sessions where `UsageUpdate` is absent. When `last_token_usage` is absent (older transcript formats), `last_context_tokens` is `None` and the transcript fallback does not display a context percentage.
Copy file name to clipboardExpand all lines: codex-rs/nori-protocol/docs.md
+4-3Lines changed: 4 additions & 3 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -6,7 +6,7 @@ Path: @/codex-rs/nori-protocol
6
6
7
7
- Defines the normalized `ClientEvent` protocol that sits between raw ACP session updates (from `agent-client-protocol-schema`) and the TUI rendering layer. All ACP tool calls, messages, plans, approvals, and replay entries are transformed into this crate's types before reaching the TUI.
8
8
- The `ClientEventNormalizer` is the stateful entry point: it accepts `acp::SessionUpdate` and `acp::RequestPermissionRequest` values and emits `Vec<ClientEvent>`.
9
-
- Session-scoped ACP metadata that does not deserve bespoke widgets yet is normalized into `ClientEvent::SessionUpdateInfo`, giving the rest of the stack one minimal rendering/replay path for mode, config, session-info, and usage updates.
9
+
- Session-scoped ACP metadata is normalized into `ClientEvent::SessionUpdateInfo`, giving the rest of the stack one minimal rendering/replay path for mode, config, and session-info updates while still letting usage updates carry structured footer state.
10
10
- Single-file crate (`lib.rs`) with no submodules.
-**`SessionRuntime` support types** in `session_runtime.rs` define the reducer-owned ACP runtime model used by `codex-acp`: `SessionPhase`, `PersistedSessionState`, `ActiveRequestState`, `OpenMessage`, and `QueuedPrompt`. These types let the backend treat prompt turns, `session/load`, queued prompts, and ownership of tool/approval updates as one ordered state machine instead of reconstructing turn state from racing tasks.
30
30
-**Session update normalization** keeps the first pass intentionally small:
31
31
-`UserMessageChunk` becomes `MessageDelta { stream: User, .. }`, which lets replay paths reconstruct visible user history during `session/load`.
32
-
-`CurrentModeUpdate`, `ConfigOptionUpdate`, `SessionInfoUpdate`, and `UsageUpdate` become `SessionUpdateInfo`, a display-oriented summary with a stable `kind`.
32
+
-`CurrentModeUpdate`, `ConfigOptionUpdate`, and `SessionInfoUpdate` become lightweight `SessionUpdateInfo` summaries.
33
+
-`UsageUpdate` also becomes `SessionUpdateInfo`, but the usage variant additionally carries `SessionUsageState` so the TUI can update footer context without reparsing the display string.
33
34
-**Persisted session metadata** now includes `session_info` and `session_usage` alongside available commands, current mode, and config options. `codex-acp` owns persistence, but these structs live here so the reducer and replay pipeline share one runtime model.
34
35
-**`is_generic_tool_call()`** gates initial `ToolCall` emission: tool calls with no `raw_input`, no `locations`, empty `content`, and no `/` in the title are suppressed (return empty `Vec`). The normalizer still records them internally so that later attributed `ToolCallUpdate` messages can refine the existing call without forcing the TUI to render a placeholder cell first.
35
36
-**Invocation priority cascade** in `invocation_from_tool_call()` resolves what the tool is doing, in priority order:
- The `is_generic_tool_call()` filter means the normalizer is not 1:1 with incoming events. Initial `ToolCall` messages that are sufficiently sparse are silently dropped, but later `ToolCallUpdate` messages still become visible `ToolSnapshot`s even if no initial `ToolCall` ever arrived.
51
-
-`SessionUpdateInfo`is intentionally lossy. It exists to stop valid ACP session updates from disappearing while the UI remains lightweight; richer dedicated widgets can still be layered on later without changing ACP reducer semantics.
52
+
-`SessionUpdateInfo`stays intentionally lightweight, but it is no longer fully lossy: the `Usage` variant also carries structured `SessionUsageState` so replay and live footer updates can share the same path.
52
53
- The location fallback (tier 4) only handles `Read` and `Search` kinds. Edit/Delete/Move with locations but no `raw_input` return `None` from the normalizer and fall through to the TUI's location-path display fallback, avoiding creation of empty-diff `FileOperations` that would route to `PatchHistoryCell`.
53
54
-`sanitize_title()` is a two-pass operation: first strips the `[current working directory ...]` bracket, then strips trailing `(description)` parenthetical. The parenthetical strip only fires after a cwd bracket was found, because Gemini appends descriptions after the cwd metadata.
54
55
- Shell wrapper detection (`is_shell_wrapper()`) recognizes `bash`, `sh`, `zsh`, `fish`, `pwsh`, and `powershell` with `-c` or `-lc` flags. When a 3-element command array matches this pattern, only the script portion is extracted as the command string.
Copy file name to clipboardExpand all lines: codex-rs/tui/docs.md
+4-3Lines changed: 4 additions & 3 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -47,7 +47,7 @@ The main event loop in `app/mod.rs` processes:
47
47
2.**Backend events** from ACP: `BackendEvent::Client` carries normalized `nori_protocol::ClientEvent` session data, while `BackendEvent::Control` carries shared control-plane events
48
48
3.**App events** for state changes (agent selection, config updates)
49
49
50
-
The client-event stream now also includes lightweight ACP session metadata summaries. Rather than adding dedicated UI chrome for every ACP session update, the first-pass TUI behavior renders `ClientEvent::SessionUpdateInfo` as ordinary info/history cells and includes the same text in the view-only transcript.
50
+
The client-event stream now also includes lightweight ACP session metadata summaries. Most `ClientEvent::SessionUpdateInfo` values still render as ordinary info/history cells, but usage updates are handled specially: they update the footer context segment and are omitted from both live history cells and the view-only transcript.
51
51
52
52
The chat interface is managed by the `chatwidget/` module (`chatwidget/mod.rs` + submodules), which handles:
| Context line |`context_window_percent` present, with or without token data | "Context: 77.0K (27%)" or just "42%" |
418
+
| Context line |`context_window_percent` present, with or without token data | "Context 27% (77.0K)" or just "Context 42%" |
419
419
| Token totals |`token_breakdown` has non-zero total | "Tokens: 123K total (32.0K cached)" |
420
420
421
421
The Tokens section renders if either `token_breakdown` has a non-zero total OR `context_window_percent` is present. This means context window percentage from the live API (`TokenUsageInfo`) can appear even before transcript token data is available.
@@ -598,7 +598,7 @@ The footer displays configurable segments, each of which can be enabled/disabled
598
598
| Git Branch |`git_branch`| Current branch name with ⎇ symbol (yellow for main repo, orange for worktree) |
599
599
| Worktree Name |`worktree_name`| "Worktree: {name}" (light red) when running in an auto-worktree session -- the immutable directory name, distinct from the git branch which gets renamed after the first prompt |
600
600
| Git Stats |`git_stats`| Lines added/removed in current session |
601
-
| Context Window |`context`| "Context: 34K (27%)" when running within an agent environment |
601
+
| Context Window |`context`| "Context27% (34K)" when running within an agent environment |
| Nori Profile |`nori_profile`| "Skillset: name" for one active skillset, "Skillsets: a, b" for multiple, hidden when none are active. Uses `active_skillsets` from `SystemInfo` (populated by `nori-skillsets list-active`). |
604
604
| Nori Version |`nori_version`| "Skillsets v<version>" |
@@ -614,6 +614,7 @@ git_stats = false
614
614
All segments are enabled by default. The order of segments in the footer is fixed (cannot be reordered via config).
615
615
616
616
Token data flows from `TranscriptLocation.token_breakdown` (provided by `codex_acp::discover_transcript_for_agent_with_message()`) through `FooterProps` to the footer renderer. The breakdown includes separate input, output, and cached token counts for accurate usage reporting.
617
+
Footer context usage is sourced in priority order: ACP `SessionUpdateInfo { kind: Usage, usage: Some(..) }` updates drive the footer when available, while `TranscriptLocation.token_breakdown` remains the provider-specific fallback for older sessions or agents that do not emit ACP usage updates.
617
618
618
619
The prompt summary flows from the ACP backend as an `EventMsg::PromptSummary` event, handled by `ChatWidget::on_prompt_summary()`, which propagates it down: `ChatWidget` -> `BottomPane::set_prompt_summary()` -> `ChatComposer::set_prompt_summary()` -> `FooterProps.prompt_summary` -> `footer_segments()` renderer.
0 commit comments