Skip to content

Improve terminal input status layout#152

Merged
Desperado merged 2 commits into
mainfrom
Desperado/fix-window-spacing-input
Jul 18, 2026
Merged

Improve terminal input status layout#152
Desperado merged 2 commits into
mainfrom
Desperado/fix-window-spacing-input

Conversation

@Desperado

Copy link
Copy Markdown
Contributor

Summary

  • Separate the terminal input from the agent stream with a bordered input panel.
  • Show live session metrics, backend/mode details, and the latest task in the input status area.
  • Capture available Claude Code usage and render the status consistently in normal and slash-command modes.

Why

The prompt input visually blended into agent output, and session context/timing information was either absent or misleading. The status panel now uses the latest request's input usage for context-window estimation and refreshes the session timer while the prompt is open.

Validation

  • go test ./...
  • git diff --check

@sigilix

sigilix Bot commented Jul 17, 2026

Copy link
Copy Markdown

Sigilix review resumed

This PR is ready for review. The earlier draft-skip notice has been superseded by the normal review run.

Current commit: 3ef820c

@Desperado
Desperado marked this pull request as ready for review July 17, 2026 22:29
@sigilix

sigilix Bot commented Jul 17, 2026

Copy link
Copy Markdown

Sigilix Overview

Effort: 4/5 (large)

Quality gates

  • ⚠️ PR title follows convention — Title doesn't match repo convention: ^(?:feat|fix|chore|docs|refactor|test|perf|style|build|ci|revert)(?:\([^)]+\))?!?: .+
  • ✅ PR description is complete
  • ℹ️ PR is linked to an issue — No Closes #N / Closes SIG-N keyword found in PR body or commit messages.

Summary — latest push

Introduces a bordered input panel and live status bar to the terminal REPL, surfacing session metrics (context window fill, token counts, turn duration, session timer) and backend/mode details alongside the current task. The change threads a new StatusInfo struct through the input loop, clears per-turn context estimates at turn boundaries, and captures CLI backend usage via a TurnStatsProvider interface. A specialist review flagged logic errors in context token tracking, unhandled edge cases in duration formatting, and security concerns around unbounded task strings that must be addressed before merge.

Important files

File Score Notes Next step
internal/repl/repl.go 5/5 Wires session state (timers, token counts, task name) into the TUI input loop and clears per-turn context estimates at turn boundaries, including for CLI backends via the new TurnStatsProvider interface. Fix the logic error where lastContextTokens is unconditionally set to 0 before CLI backends run, preventing the TurnStatsProvider from ever reporting a non-zero value, and guard the cleanInput assignment against empty or whitespace-only inputs.
internal/tui/input.go 5/5 Implements the StatusInfo struct, bordered input panel rendering, and the live-updating status bar with metrics and mode/task information, including a 1-second tick for the session timer. Sanitize the Task string in renderStatus to strip ANSI escape sequences or control characters before rendering, and fix the compactDuration function to handle zero or negative durations without producing malformed output.
internal/agent/agent.go 4/5 Adds the LastContextTokens field to the Agent struct and resets it to 0 at turn boundaries and on history clear, ensuring stale context estimates don't persist across turns or backends. Verify that LastContextTokens is correctly set in all streaming and non-streaming code paths, including error paths where the API call fails before reporting usage.
internal/agent/cc_agent.go 4/5 Implements TurnStatsProvider for CCAgent, parsing usage from the result event and storing lastTurnIn/lastTurnOut under mutex. Verify that resetting lastTurnOK to false at the start of Run() doesn't cause a race with a concurrent LastTurnStats() call if the previous turn's parseStream hasn't finished.
internal/tui/input_test.go 3/5 Adds unit tests verifying the bordered input panel, status metrics rendering, slash-menu status, and live session duration calculation. Add edge-case tests for compactTokens and compactDuration with zero, negative, and boundary values, and test that an empty or nil StatusInfo produces no status output.

Confidence: 2/5

Unconditional zeroing of lastContextTokens before CLI backend execution creates a logic error that hides context metrics, and unescaped task strings pose a terminal injection risk.

  • Fix the logic error at internal/repl/repl.go line 1098 where lastContextTokens = 0 is set immediately before the CLI backend runs, guaranteeing the subsequent TurnStatsProvider assignment overwrites a zero instead of accumulating correctly.
  • Sanitize the Task string in internal/tui/input.go renderStatus to strip ANSI escape sequences and control characters before rendering to prevent terminal injection or layout corruption.
  • Fix compactDuration in internal/tui/input.go to handle zero and negative durations gracefully instead of producing potentially malformed strings like "0s" or negative seconds.
  • Verify that LastContextTokens in internal/agent/agent.go is reset to 0 on error paths where the API call fails before usage is reported, preventing stale values from persisting.
  • Guard the lastTask = cleanInput assignment at internal/repl/repl.go line 1200 against empty or whitespace-only inputs to avoid setting the task to an empty string.

Suggested labels: bug ui


Posted · a5f7766 · 13 findings — View review
Proof: 13 model-only
runner-verified = CI receipt · reproduced = sandbox observed diff · grounded = deterministic detector/worker-token · model-only = model judgment only
Dismiss @sigilix dismiss <reason> (not-a-bug | bad-anchor | already-covered | too-minor | wrong-context) · Re-run /sigilix review · Review #2
Sigilix · 0 of 50 reviews used in past 5h

@sigilix sigilix Bot added the enhancement New feature or request label Jul 17, 2026
Comment thread internal/agent/agent.go
// ClearHistory resets conversation history.
func (a *Agent) ClearHistory() {
a.History = []api.Message{}
a.LastContextTokens = 0

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 LOGICGROUNDED Ollama backend does not update LastContextTokens after a turn

The diff establishes a convention: every backend that reports per-turn input tokens sets Agent.LastContextTokens so the TUI can estimate context-window fill. The Anthropic streaming path (line 662), the non-streaming API path (line 822), and the Cerebras path (cerebras_agent.go line 71) all set a.LastContextTokens = <per-turn input tokens>. The Ollama path in agent.go does not, so after an Ollama turn LastContextTokens stays stale (zero or the previous backend's value), and the status bar shows a misleading context-fill percentage. Add a.LastContextTokens = <ollama per-turn input tokens> in the Ollama response-handling branch.

Example:

input: Ollama backend active, a turn completes with 5000 input tokens
actual: `LastContextTokens` remains 0 (or the previous backend's value)
expected: `LastContextTokens` == 5000, status bar shows correct context fill

Suggested fix:

a.LastContextTokens = ollamaResp.Usage.InputTokens

Why this wasn't caught: No test covers LastContextTokens being set after an Ollama turn; the existing TestContextWindow only tests the window-size helper.

More Info
  • Threat model: Users on the Ollama backend see a stale or zero context-fill percentage in the input status bar, misleading them about how full the context window is.
  • Specific code citations: Convention sites: agent.go line 662 (a.LastContextTokens = ev.Message.Usage.InputTokens), line 822 (a.LastContextTokens = apiResp.Usage.InputTokens), cerebras_agent.go line 71 (a.LastContextTokens = resp.Usage.PromptTokens). Violation site: the Ollama response-handling code in agent.go (the callOllama or equivalent function) — no LastContextTokens assignment.
  • Existing protections: None. LastContextTokens is only set in the three paths listed above; the Ollama path is a fourth code path that handles its own response and never touches the field.
  • Proposed mitigation: In the Ollama response handler, after extracting the per-turn input token count, assign a.LastContextTokens = <that count>.
  • Alternative mitigations considered: A default-to-zero approach would hide the metric for Ollama users, but the diff's intent is to show it for every backend that can report it. Ollama responses do carry usage info, so the metric is available.
  • Severity calibration: Score 3: the bug is real and user-visible (wrong context-fill display), but it only affects Ollama users and does not cause data loss or crashes.
Prompt To Fix With AI
This is a comment left during a code review.
Path: internal/agent/agent.go
Line: 133

Comment:
**Ollama backend does not update `LastContextTokens` after a turn**

The diff establishes a convention: every backend that reports per-turn input tokens sets `Agent.LastContextTokens` so the TUI can estimate context-window fill. The Anthropic streaming path (line 662), the non-streaming API path (line 822), and the Cerebras path (`cerebras_agent.go` line 71) all set `a.LastContextTokens = <per-turn input tokens>`. The Ollama path in `agent.go` does not, so after an Ollama turn `LastContextTokens` stays stale (zero or the previous backend's value), and the status bar shows a misleading context-fill percentage. Add `a.LastContextTokens = <ollama per-turn input tokens>` in the Ollama response-handling branch.

Example:
input: Ollama backend active, a turn completes with 5000 input tokens
actual: `LastContextTokens` remains 0 (or the previous backend's value)
expected: `LastContextTokens` == 5000, status bar shows correct context fill

Threat model:
Users on the Ollama backend see a stale or zero context-fill percentage in the input status bar, misleading them about how full the context window is.

Specific code citations:
Convention sites: `agent.go` line 662 (`a.LastContextTokens = ev.Message.Usage.InputTokens`), line 822 (`a.LastContextTokens = apiResp.Usage.InputTokens`), `cerebras_agent.go` line 71 (`a.LastContextTokens = resp.Usage.PromptTokens`). Violation site: the Ollama response-handling code in `agent.go` (the `callOllama` or equivalent function) — no `LastContextTokens` assignment.

Existing protections:
None. `LastContextTokens` is only set in the three paths listed above; the Ollama path is a fourth code path that handles its own response and never touches the field.

Proposed mitigation:
In the Ollama response handler, after extracting the per-turn input token count, assign `a.LastContextTokens = <that count>`.

Alternative mitigations considered:
A default-to-zero approach would hide the metric for Ollama users, but the diff's intent is to show it for every backend that can report it. Ollama responses do carry usage info, so the metric is available.

Severity calibration:
Score 3: the bug is real and user-visible (wrong context-fill display), but it only affects Ollama users and does not cause data loss or crashes.

Suggested fix shape:
a.LastContextTokens = ollamaResp.Usage.InputTokens

Why this wasn't caught:
No test covers `LastContextTokens` being set after an Ollama turn; the existing `TestContextWindow` only tests the window-size helper.

How can I resolve this? If you propose a fix, please make it concise.

Comment thread internal/repl/repl.go
llmResult, err = cliAgent.Run(cleanInput, term)
term.StopThinking()
if err == nil {
if stats, ok := cliAgent.(agent.TurnStatsProvider); ok {

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 LOGICGROUNDED Direct-API backend silently omits per-turn status metrics that CC/Codex backends report

The TurnStatsProvider interface is only implemented by CCAgent. The REPL's post-turn block (line ~1164) calls cliAgent.(agent.TurnStatsProvider) only when cliAgent != nil, which is true for CC/Codex but false for the direct API path. The direct-API Agent struct has a LastContextTokens field populated during API calls, but it does not implement TurnStatsProvider, so the status bar's per-turn token counts and context-window estimate are silently absent when using the API backend. The inputStatus closure already reads ag.LastContextTokens directly, so the data is available — the gap is that the REPL never updates lastContextTokens from ag.LastContextTokens for the API path in the same structured way it does for CLI backends. Fix: either have the direct-API Agent implement TurnStatsProvider (returning a.LastContextTokens, a.Usage.OutputTokens - previousOutput, true), or add an explicit API-path branch in the REPL that reads ag.LastContextTokens and updates lastContextTokens.

More Info
  • Threat model: Users on the direct API backend see no per-turn context-window fill or token counts in the status bar, while CC/Codex users do. This is a UX inconsistency, not a correctness bug — no data is lost, but the feature is silently gated on backend choice.
  • Specific code citations: TurnStatsProvider interface defined in internal/agent/cc_agent.go:32-35. Implemented only by CCAgent at cc_agent.go:631-635. REPL calls it at repl.go:1164-1172 inside if cliAgent != nil. Direct-API Agent has LastContextTokens at agent.go:66-69 but no LastTurnStats() method.
  • Existing protections: None. The inputStatus closure reads ag.LastContextTokens directly, but the REPL's lastContextTokens local is only updated via the TurnStatsProvider path (CLI backends) or the else branch at line ~1193 (which sets it from ag.LastContextTokens when cliAgent == nil). The else branch at line 1193 does update lastContextTokens from ag.LastContextTokens, so the status bar WILL show context usage for the API path — but only after the turn completes, and only via the lastContextTokens local, not via the TurnStatsProvider interface. The asymmetry is that the API path has no TurnStatsProvider implementation, making the interface a partial abstraction.
  • Proposed mitigation: Either implement TurnStatsProvider on *Agent (returning a.LastContextTokens, a.Usage.OutputTokens - previousOutput, true) so the REPL's single cliAgent.(agent.TurnStatsProvider) branch covers all backends, or add an explicit else branch in the REPL that reads ag.LastContextTokens and updates lastContextTokens for the API path. The latter already exists at line 1193, so the actual gap is narrower than it first appears.
  • Alternative mitigations considered: Remove TurnStatsProvider and have the REPL always read ag.LastContextTokens directly. This is simpler but loses the CC-specific output-token reporting.
  • Severity calibration: Score 3: the API path does update lastContextTokens at line 1193, so the status bar is not completely broken — but the TurnStatsProvider interface is a partial abstraction that only one backend implements, which is a design smell that will cause confusion when adding new backends.
Prompt To Fix With AI
This is a comment left during a code review.
Path: internal/repl/repl.go
Line: 1164

Comment:
**Direct-API backend silently omits per-turn status metrics that CC/Codex backends report**

The `TurnStatsProvider` interface is only implemented by `CCAgent`. The REPL's post-turn block (line ~1164) calls `cliAgent.(agent.TurnStatsProvider)` only when `cliAgent != nil`, which is true for CC/Codex but false for the direct API path. The direct-API `Agent` struct has a `LastContextTokens` field populated during API calls, but it does not implement `TurnStatsProvider`, so the status bar's per-turn token counts and context-window estimate are silently absent when using the API backend. The `inputStatus` closure already reads `ag.LastContextTokens` directly, so the data is available — the gap is that the REPL never updates `lastContextTokens` from `ag.LastContextTokens` for the API path in the same structured way it does for CLI backends. Fix: either have the direct-API `Agent` implement `TurnStatsProvider` (returning `a.LastContextTokens, a.Usage.OutputTokens - previousOutput, true`), or add an explicit API-path branch in the REPL that reads `ag.LastContextTokens` and updates `lastContextTokens`.

Threat model:
Users on the direct API backend see no per-turn context-window fill or token counts in the status bar, while CC/Codex users do. This is a UX inconsistency, not a correctness bug — no data is lost, but the feature is silently gated on backend choice.

Specific code citations:
`TurnStatsProvider` interface defined in `internal/agent/cc_agent.go:32-35`. Implemented only by `CCAgent` at `cc_agent.go:631-635`. REPL calls it at `repl.go:1164-1172` inside `if cliAgent != nil`. Direct-API `Agent` has `LastContextTokens` at `agent.go:66-69` but no `LastTurnStats()` method.

Existing protections:
None. The `inputStatus` closure reads `ag.LastContextTokens` directly, but the REPL's `lastContextTokens` local is only updated via the `TurnStatsProvider` path (CLI backends) or the `else` branch at line ~1193 (which sets it from `ag.LastContextTokens` when `cliAgent == nil`). The `else` branch at line 1193 does update `lastContextTokens` from `ag.LastContextTokens`, so the status bar WILL show context usage for the API path — but only after the turn completes, and only via the `lastContextTokens` local, not via the `TurnStatsProvider` interface. The asymmetry is that the API path has no `TurnStatsProvider` implementation, making the interface a partial abstraction.

Proposed mitigation:
Either implement `TurnStatsProvider` on `*Agent` (returning `a.LastContextTokens, a.Usage.OutputTokens - previousOutput, true`) so the REPL's single `cliAgent.(agent.TurnStatsProvider)` branch covers all backends, or add an explicit `else` branch in the REPL that reads `ag.LastContextTokens` and updates `lastContextTokens` for the API path. The latter already exists at line 1193, so the actual gap is narrower than it first appears.

Alternative mitigations considered:
Remove `TurnStatsProvider` and have the REPL always read `ag.LastContextTokens` directly. This is simpler but loses the CC-specific output-token reporting.

Severity calibration:
Score 3: the API path does update `lastContextTokens` at line 1193, so the status bar is not completely broken — but the `TurnStatsProvider` interface is a partial abstraction that only one backend implements, which is a design smell that will cause confusion when adding new backends.

How can I resolve this? If you propose a fix, please make it concise.

Comment thread internal/agent/agent.go
var ev sseMessageStart
if err := json.Unmarshal([]byte(rawData), &ev); err == nil {
a.Usage.InputTokens += ev.Message.Usage.InputTokens
a.LastContextTokens = ev.Message.Usage.InputTokens

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 LOGICGROUNDED LastContextTokens is not reset at turn start in the streaming API path, leaking the previous turn's value on failure

The CC backend (CCAgent.Run) resets lastTurnIn, lastTurnOut, and lastTurnOK to zero at the top of every turn, so a failed turn shows no stale context usage. The streaming API path (callStreamingAPI) only sets a.LastContextTokens when the SSE message_start event arrives (line 662). If the request fails before that event — or if the event never carries usage — the status bar retains the previous turn's LastContextTokens, misrepresenting the current context fill. The fix is to zero a.LastContextTokens at the start of callStreamingAPI, matching the CC backend's contract.

Example:

Turn 1: API succeeds, LastContextTokens = 12000. Turn 2: API fails before message_start. Status bar shows 'ctx 12.0k/200.0k (6%)' — the stale value from Turn 1.

Suggested fix:

a.LastContextTokens = 0

Why this wasn't caught: No test exercises the LastContextTokens value after a failed API call.

More Info
  • Threat model: A user sees a stale context-window percentage in the TUI status bar after a failed API call, leading to incorrect assumptions about how full the context window is.
  • Specific code citations: CCAgent.Run resets lastTurnIn/Out/OK at line 361 of cc_agent.go. callStreamingAPI sets LastContextTokens at line 662 of agent.go but never resets it at the top of the function.
  • Existing protections: The REPL's lastContextTokens variable is only updated on success (repl.go line 1195), but the Agent field itself is not cleared on failure, so a subsequent success that doesn't carry usage would still show the stale value.
  • Proposed mitigation: Add a.LastContextTokens = 0 at the top of callStreamingAPI, before the request is sent.
  • Alternative mitigations considered: Resetting in the REPL after every turn would work but would miss the case where the Agent is used outside the REPL. Resetting in the Agent is the correct single point of truth.
  • Severity calibration: Score 3: a stale UI metric, not a correctness or security bug. The blast radius is limited to the status bar display, and the likelihood is moderate (any failed API call triggers it).
Prompt To Fix With AI
This is a comment left during a code review.
Path: internal/agent/agent.go
Line: 662

Comment:
**`LastContextTokens` is not reset at turn start in the streaming API path, leaking the previous turn's value on failure**

The CC backend (`CCAgent.Run`) resets `lastTurnIn`, `lastTurnOut`, and `lastTurnOK` to zero at the top of every turn, so a failed turn shows no stale context usage. The streaming API path (`callStreamingAPI`) only sets `a.LastContextTokens` when the SSE `message_start` event arrives (line 662). If the request fails before that event — or if the event never carries usage — the status bar retains the previous turn's `LastContextTokens`, misrepresenting the current context fill. The fix is to zero `a.LastContextTokens` at the start of `callStreamingAPI`, matching the CC backend's contract.

Example:
Turn 1: API succeeds, LastContextTokens = 12000. Turn 2: API fails before message_start. Status bar shows 'ctx 12.0k/200.0k (6%)' — the stale value from Turn 1.

Threat model:
A user sees a stale context-window percentage in the TUI status bar after a failed API call, leading to incorrect assumptions about how full the context window is.

Specific code citations:
CCAgent.Run resets lastTurnIn/Out/OK at line 361 of cc_agent.go. callStreamingAPI sets LastContextTokens at line 662 of agent.go but never resets it at the top of the function.

Existing protections:
The REPL's `lastContextTokens` variable is only updated on success (repl.go line 1195), but the Agent field itself is not cleared on failure, so a subsequent success that doesn't carry usage would still show the stale value.

Proposed mitigation:
Add `a.LastContextTokens = 0` at the top of `callStreamingAPI`, before the request is sent.

Alternative mitigations considered:
Resetting in the REPL after every turn would work but would miss the case where the Agent is used outside the REPL. Resetting in the Agent is the correct single point of truth.

Severity calibration:
Score 3: a stale UI metric, not a correctness or security bug. The blast radius is limited to the status bar display, and the likelihood is moderate (any failed API call triggers it).

Suggested fix shape:
a.LastContextTokens = 0

Why this wasn't caught:
No test exercises the LastContextTokens value after a failed API call.

How can I resolve this? If you propose a fix, please make it concise.

Comment thread internal/agent/agent.go

// Track usage
a.Usage.InputTokens += apiResp.Usage.InputTokens
a.LastContextTokens = apiResp.Usage.InputTokens

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 LOGICGROUNDED LastContextTokens is not reset at turn start in the direct API path, leaking the previous turn's value on failure

The CC backend resets its per-turn usage counters at the top of Run(). The direct API path (callAPI) sets a.LastContextTokens only after a successful response (line 822). If the HTTP request fails, times out, or returns a non-2xx status, LastContextTokens retains the previous turn's value. The fix is to zero a.LastContextTokens at the start of callAPI, matching the CC backend's contract.

Example:

Turn 1: direct API succeeds, LastContextTokens = 8000. Turn 2: HTTP 502. Status bar shows 'ctx 8.0k/200.0k (4%)' — stale.

Suggested fix:

a.LastContextTokens = 0

Why this wasn't caught: No test exercises the LastContextTokens value after a failed direct API call.

More Info
  • Threat model: Same as the streaming path: a stale context-window percentage in the TUI after a failed direct API call.
  • Specific code citations: CCAgent.Run resets at cc_agent.go line 361. callAPI sets LastContextTokens at agent.go line 822 but never resets it at the top.
  • Existing protections: The REPL only reads LastContextTokens on success (repl.go line 1195), but the field itself is not cleared on failure.
  • Proposed mitigation: Add a.LastContextTokens = 0 at the top of callAPI, before the HTTP request.
  • Alternative mitigations considered: Same as the streaming path — resetting in the REPL is less robust than resetting in the Agent.
  • Severity calibration: Score 3: stale UI metric, moderate likelihood on any failed direct API call.
Prompt To Fix With AI
This is a comment left during a code review.
Path: internal/agent/agent.go
Line: 822

Comment:
**`LastContextTokens` is not reset at turn start in the direct API path, leaking the previous turn's value on failure**

The CC backend resets its per-turn usage counters at the top of `Run()`. The direct API path (`callAPI`) sets `a.LastContextTokens` only after a successful response (line 822). If the HTTP request fails, times out, or returns a non-2xx status, `LastContextTokens` retains the previous turn's value. The fix is to zero `a.LastContextTokens` at the start of `callAPI`, matching the CC backend's contract.

Example:
Turn 1: direct API succeeds, LastContextTokens = 8000. Turn 2: HTTP 502. Status bar shows 'ctx 8.0k/200.0k (4%)' — stale.

Threat model:
Same as the streaming path: a stale context-window percentage in the TUI after a failed direct API call.

Specific code citations:
CCAgent.Run resets at cc_agent.go line 361. callAPI sets LastContextTokens at agent.go line 822 but never resets it at the top.

Existing protections:
The REPL only reads LastContextTokens on success (repl.go line 1195), but the field itself is not cleared on failure.

Proposed mitigation:
Add `a.LastContextTokens = 0` at the top of `callAPI`, before the HTTP request.

Alternative mitigations considered:
Same as the streaming path — resetting in the REPL is less robust than resetting in the Agent.

Severity calibration:
Score 3: stale UI metric, moderate likelihood on any failed direct API call.

Suggested fix shape:
a.LastContextTokens = 0

Why this wasn't caught:
No test exercises the LastContextTokens value after a failed direct API call.

How can I resolve this? If you propose a fix, please make it concise.


choice := resp.Choices[0]
a.Usage.InputTokens += resp.Usage.PromptTokens
a.LastContextTokens = resp.Usage.PromptTokens

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 LOGICGROUNDED LastContextTokens is not reset at turn start in the Cerebras path, leaking the previous turn's value on failure

The CC backend resets its per-turn usage counters at the top of Run(). The Cerebras path (RunCerebrasAgent) sets a.LastContextTokens only after a successful response (line 71). If the Cerebras request fails, LastContextTokens retains the previous turn's value. The fix is to zero a.LastContextTokens at the start of RunCerebrasAgent, matching the CC backend's contract.

Example:

Turn 1: Cerebras succeeds, LastContextTokens = 5000. Turn 2: Cerebras API error. Status bar shows 'ctx 5.0k/200.0k (2%)' — stale.

Suggested fix:

a.LastContextTokens = 0

Why this wasn't caught: No test exercises the LastContextTokens value after a failed Cerebras call.

More Info
  • Threat model: Stale context-window percentage in the TUI after a failed Cerebras call.
  • Specific code citations: CCAgent.Run resets at cc_agent.go line 361. RunCerebrasAgent sets LastContextTokens at cerebras_agent.go line 71 but never resets it at the top.
  • Existing protections: The REPL only reads LastContextTokens on success (repl.go line 1195), but the field itself is not cleared on failure.
  • Proposed mitigation: Add a.LastContextTokens = 0 at the top of RunCerebrasAgent, before the request loop.
  • Alternative mitigations considered: Resetting in the REPL is less robust than resetting in the Agent.
  • Severity calibration: Score 3: stale UI metric, moderate likelihood on any failed Cerebras call.
Prompt To Fix With AI
This is a comment left during a code review.
Path: internal/agent/cerebras_agent.go
Line: 71

Comment:
**`LastContextTokens` is not reset at turn start in the Cerebras path, leaking the previous turn's value on failure**

The CC backend resets its per-turn usage counters at the top of `Run()`. The Cerebras path (`RunCerebrasAgent`) sets `a.LastContextTokens` only after a successful response (line 71). If the Cerebras request fails, `LastContextTokens` retains the previous turn's value. The fix is to zero `a.LastContextTokens` at the start of `RunCerebrasAgent`, matching the CC backend's contract.

Example:
Turn 1: Cerebras succeeds, LastContextTokens = 5000. Turn 2: Cerebras API error. Status bar shows 'ctx 5.0k/200.0k (2%)' — stale.

Threat model:
Stale context-window percentage in the TUI after a failed Cerebras call.

Specific code citations:
CCAgent.Run resets at cc_agent.go line 361. RunCerebrasAgent sets LastContextTokens at cerebras_agent.go line 71 but never resets it at the top.

Existing protections:
The REPL only reads LastContextTokens on success (repl.go line 1195), but the field itself is not cleared on failure.

Proposed mitigation:
Add `a.LastContextTokens = 0` at the top of `RunCerebrasAgent`, before the request loop.

Alternative mitigations considered:
Resetting in the REPL is less robust than resetting in the Agent.

Severity calibration:
Score 3: stale UI metric, moderate likelihood on any failed Cerebras call.

Suggested fix shape:
a.LastContextTokens = 0

Why this wasn't caught:
No test exercises the LastContextTokens value after a failed Cerebras call.

How can I resolve this? If you propose a fix, please make it concise.

Comment thread internal/repl/repl.go

var inputHistory []string
var lastCtrlC time.Time
sessionStarted := time.Now()

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 LOGICGROUNDED LastContextTokens is set from different sources in API vs CLI paths, breaking the declared contract

The Agent.LastContextTokens field is documented as 'the input-token count reported by the most recent model request' and is set directly in callStreamingAPI, callAPI, and RunCerebrasAgent. However, in the CLI-agent path (CC/Codex), lastContextTokens is instead set from TurnStatsProvider.LastTurnStats(), which returns the input tokens from the CLI subprocess's result event. This means ag.LastContextTokens is stale (zero or from a prior API call) when a CLI backend is active, while the REPL's local lastContextTokens variable holds the correct value. The inputStatus closure reads lastContextTokens into StatusInfo.ContextUsed, so the display is correct, but the Agent field's contract is violated — it does not reflect the most recent model request when a CLI backend is used.

More Info
  • Threat model: Any code that reads ag.LastContextTokens expecting it to reflect the most recent request (as documented) will get a stale or zero value when a CLI backend is active. This is a latent correctness bug for future consumers of the field.
  • Specific code citations: internal/agent/agent.go:66-68 declares LastContextTokens with the contract 'input-token count reported by the most recent model request'. internal/agent/agent.go:662 and internal/agent/agent.go:822 set it in the API paths. internal/repl/repl.go:1164-1170 sets the local lastContextTokens from TurnStatsProvider but never updates ag.LastContextTokens.
  • Existing protections: The REPL's inputStatus closure reads the local lastContextTokens variable, not ag.LastContextTokens, so the TUI display is correct. No other code in this diff reads ag.LastContextTokens.
  • Proposed mitigation: After the TurnStatsProvider block in the REPL, also set ag.LastContextTokens = inputTokens so the field stays consistent with its documented contract.
  • Alternative mitigations considered: Remove the LastContextTokens field from Agent and rely solely on the REPL-local variable, but that would break the API path's direct usage. Keeping the field and updating it in both paths is the minimal fix.
  • Severity calibration: Score 3 because the TUI display is correct (the local variable is used), but the documented field contract is broken, creating a latent bug for any future code that reads ag.LastContextTokens.
Prompt To Fix With AI
This is a comment left during a code review.
Path: internal/repl/repl.go
Line: 165

Comment:
**`LastContextTokens` is set from different sources in API vs CLI paths, breaking the declared contract**

The `Agent.LastContextTokens` field is documented as 'the input-token count reported by the most recent model request' and is set directly in `callStreamingAPI`, `callAPI`, and `RunCerebrasAgent`. However, in the CLI-agent path (CC/Codex), `lastContextTokens` is instead set from `TurnStatsProvider.LastTurnStats()`, which returns the input tokens from the CLI subprocess's result event. This means `ag.LastContextTokens` is stale (zero or from a prior API call) when a CLI backend is active, while the REPL's local `lastContextTokens` variable holds the correct value. The `inputStatus` closure reads `lastContextTokens` into `StatusInfo.ContextUsed`, so the display is correct, but the `Agent` field's contract is violated — it does not reflect the most recent model request when a CLI backend is used.

Threat model:
Any code that reads `ag.LastContextTokens` expecting it to reflect the most recent request (as documented) will get a stale or zero value when a CLI backend is active. This is a latent correctness bug for future consumers of the field.

Specific code citations:
`internal/agent/agent.go:66-68` declares `LastContextTokens` with the contract 'input-token count reported by the most recent model request'. `internal/agent/agent.go:662` and `internal/agent/agent.go:822` set it in the API paths. `internal/repl/repl.go:1164-1170` sets the local `lastContextTokens` from `TurnStatsProvider` but never updates `ag.LastContextTokens`.

Existing protections:
The REPL's `inputStatus` closure reads the local `lastContextTokens` variable, not `ag.LastContextTokens`, so the TUI display is correct. No other code in this diff reads `ag.LastContextTokens`.

Proposed mitigation:
After the `TurnStatsProvider` block in the REPL, also set `ag.LastContextTokens = inputTokens` so the field stays consistent with its documented contract.

Alternative mitigations considered:
Remove the `LastContextTokens` field from `Agent` and rely solely on the REPL-local variable, but that would break the API path's direct usage. Keeping the field and updating it in both paths is the minimal fix.

Severity calibration:
Score 3 because the TUI display is correct (the local variable is used), but the documented field contract is broken, creating a latent bug for any future code that reads `ag.LastContextTokens`.

How can I resolve this? If you propose a fix, please make it concise.

Comment thread internal/tui/input.go Outdated
ContextWindow int // assumed context window of the active model
LastTurnDur time.Duration // wall-clock duration of the previous agent turn
SessionStarted time.Time // used to keep the session timer live while typing
SessionDur time.Duration // fallback when SessionStarted is unavailable

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P3 STYLEGROUNDED StatusInfo.SessionDur is declared as a fallback but never populated by the REPL

The StatusInfo struct declares SessionDur with the comment 'fallback when SessionStarted is unavailable', and sessionDuration() checks SessionStarted first, falling back to SessionDur. However, the REPL's inputStatus closure never sets SessionDur — it only sets SessionStarted. This means the fallback path is dead code: if SessionStarted were ever zero, sessionDuration() would return the zero value of SessionDur (0), producing no session timer display. Either the field should be removed, or the REPL should populate it when SessionStarted is unavailable.

More Info
  • Threat model: If a future code path creates a StatusInfo without SessionStarted (e.g., a non-interactive mode), the session timer would silently show nothing instead of a meaningful duration.
  • Specific code citations: internal/tui/input.go:31 declares SessionDur with the fallback comment. internal/tui/input.go:607-611 implements the fallback logic. internal/repl/repl.go:169-200 constructs StatusInfo and sets SessionStarted but never SessionDur.
  • Existing protections: The REPL always sets SessionStarted, so the fallback is never exercised in the current code paths.
  • Proposed mitigation: Either remove SessionDur from StatusInfo (since it's unused) or populate it in the REPL when SessionStarted is unavailable. Removing it is simpler and avoids dead code.
  • Alternative mitigations considered: Keep the field and document it as reserved for future use, but dead code with a misleading comment is worse than no code.
  • Severity calibration: Score 2 because the current code paths always set SessionStarted, so the bug is latent. The dead code and misleading comment are a minor maintainability issue.
Prompt To Fix With AI
This is a comment left during a code review.
Path: internal/tui/input.go
Line: 32

Comment:
**`StatusInfo.SessionDur` is declared as a fallback but never populated by the REPL**

The `StatusInfo` struct declares `SessionDur` with the comment 'fallback when SessionStarted is unavailable', and `sessionDuration()` checks `SessionStarted` first, falling back to `SessionDur`. However, the REPL's `inputStatus` closure never sets `SessionDur` — it only sets `SessionStarted`. This means the fallback path is dead code: if `SessionStarted` were ever zero, `sessionDuration()` would return the zero value of `SessionDur` (0), producing no session timer display. Either the field should be removed, or the REPL should populate it when `SessionStarted` is unavailable.

Threat model:
If a future code path creates a `StatusInfo` without `SessionStarted` (e.g., a non-interactive mode), the session timer would silently show nothing instead of a meaningful duration.

Specific code citations:
`internal/tui/input.go:31` declares `SessionDur` with the fallback comment. `internal/tui/input.go:607-611` implements the fallback logic. `internal/repl/repl.go:169-200` constructs `StatusInfo` and sets `SessionStarted` but never `SessionDur`.

Existing protections:
The REPL always sets `SessionStarted`, so the fallback is never exercised in the current code paths.

Proposed mitigation:
Either remove `SessionDur` from `StatusInfo` (since it's unused) or populate it in the REPL when `SessionStarted` is unavailable. Removing it is simpler and avoids dead code.

Alternative mitigations considered:
Keep the field and document it as reserved for future use, but dead code with a misleading comment is worse than no code.

Severity calibration:
Score 2 because the current code paths always set `SessionStarted`, so the bug is latent. The dead code and misleading comment are a minor maintainability issue.

How can I resolve this? If you propose a fix, please make it concise.

@qualitymaxapp

qualitymaxapp Bot commented Jul 17, 2026

Copy link
Copy Markdown

✅ QualityMax Pipeline

Gate Result
🔍 AI Review ✅ Clean
🧪 Repo Tests ✅ 477/477 passed (go)
🤖 AI Tests ✅ 47/51 passed

Powered by QualityMax — AI-Powered Test Automation

@sigilix sigilix Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

2 findings outside the diff
File Scope Finding
internal/repl/repl.go:1115-1123 file-scope Missing authorization check before updating agent usage from CLI backend stats
internal/agent/agent.go file-scope New LastContextTokens field and its reset logic are untested

Comment thread internal/agent/agent.go
// All backends share this top-level turn boundary, including Ollama. Ollama's
// streaming protocol does not expose usage, so leaving this at zero hides the
// context-fill metric instead of carrying a stale value from another backend.
a.LastContextTokens = 0

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 LOGICGROUNDED Ollama streaming path never updates LastContextTokens

runStreamingLoop sets a.LastContextTokens = 0 at the top and the comment says 'Ollama's streaming protocol does not expose usage, so leaving this at zero hides the context-fill metric'. However, the Ollama path inside runStreamingLoop (line 338-344) calls a.runOllamaLoop(term) which returns without ever setting LastContextTokens. This is intentional per the comment, but the REPL's inputStatus closure reads lastContextTokens (which is set from ag.LastContextTokens only on success). If the Ollama turn succeeds, lastContextTokens stays at the zero reset from the top of the turn, so the status panel shows no context-fill metric — which is the desired behavior. No bug here.

More Info
  • Threat model: N/A — this is intentional behavior.
  • Specific code citations: internal/agent/agent.go lines 335-344.
  • Existing protections: The comment on line 338 explicitly documents the intent.
  • Proposed mitigation: No fix needed.
  • Alternative mitigations considered: N/A
  • Severity calibration: Score 4 is too high; this is intentional.
Prompt To Fix With AI
This is a comment left during a code review.
Path: internal/agent/agent.go
Line: 341

Comment:
**Ollama streaming path never updates LastContextTokens**

`runStreamingLoop` sets `a.LastContextTokens = 0` at the top and the comment says 'Ollama's streaming protocol does not expose usage, so leaving this at zero hides the context-fill metric'. However, the Ollama path inside `runStreamingLoop` (line 338-344) calls `a.runOllamaLoop(term)` which returns without ever setting `LastContextTokens`. This is intentional per the comment, but the REPL's `inputStatus` closure reads `lastContextTokens` (which is set from `ag.LastContextTokens` only on success). If the Ollama turn succeeds, `lastContextTokens` stays at the zero reset from the top of the turn, so the status panel shows no context-fill metric — which is the desired behavior. No bug here.

Threat model:
N/A — this is intentional behavior.

Specific code citations:
`internal/agent/agent.go` lines 335-344.

Existing protections:
The comment on line 338 explicitly documents the intent.

Proposed mitigation:
No fix needed.

Alternative mitigations considered:
N/A

Severity calibration:
Score 4 is too high; this is intentional.

How can I resolve this? If you propose a fix, please make it concise.

Comment thread internal/repl/repl.go
Comment on lines +1098 to +1100
// Clear the UI's per-turn estimate before starting any backend. This also
// covers CLI backends, whose usage is reported outside Agent.RunStreaming.
lastContextTokens = 0

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 LOGICGROUNDED Race on Agent.LastContextTokens between REPL and agent goroutines

ag.LastContextTokens is written by the agent's streaming goroutine (in callStreamingAPI when processing SSE message_start events) and read by the REPL goroutine (in the inputStatus closure and on line 1200). There is no synchronization — no mutex, no atomic, no channel. In Go, concurrent reads and writes to a plain int field are a data race. The fix is to protect LastContextTokens with a mutex or use atomic.Int64.

More Info
  • Threat model: Under the Go memory model, concurrent read/write of a non-atomic int is undefined behavior. The race detector will flag it, and in practice the TUI could read a torn or stale value.
  • Specific code citations: Write: internal/agent/agent.go line 670 (a.LastContextTokens = ev.Message.Usage.InputTokens) inside callStreamingAPI. Read: internal/repl/repl.go line 1200 (lastContextTokens = ag.LastContextTokens) and line 192 (ContextUsed: lastContextTokens).
  • Existing protections: None — no mutex, no atomic, no channel.
  • Proposed mitigation: Change LastContextTokens to atomic.Int64 in the Agent struct, or wrap accesses with a mutex.
  • Alternative mitigations considered: Could pass the value through a channel, but an atomic is simpler for a single scalar.
  • Severity calibration: Score 4 — a real data race that the Go race detector will catch. In practice the impact is a stale/zero display, but it's a correctness bug in concurrent code.
Prompt To Fix With AI
This is a comment left during a code review.
Path: internal/repl/repl.go
Line: 1098-1100

Comment:
**Race on Agent.LastContextTokens between REPL and agent goroutines**

`ag.LastContextTokens` is written by the agent's streaming goroutine (in `callStreamingAPI` when processing SSE `message_start` events) and read by the REPL goroutine (in the `inputStatus` closure and on line 1200). There is no synchronization — no mutex, no atomic, no channel. In Go, concurrent reads and writes to a plain `int` field are a data race. The fix is to protect `LastContextTokens` with a mutex or use `atomic.Int64`.

Threat model:
Under the Go memory model, concurrent read/write of a non-atomic int is undefined behavior. The race detector will flag it, and in practice the TUI could read a torn or stale value.

Specific code citations:
Write: `internal/agent/agent.go` line 670 (`a.LastContextTokens = ev.Message.Usage.InputTokens`) inside `callStreamingAPI`. Read: `internal/repl/repl.go` line 1200 (`lastContextTokens = ag.LastContextTokens`) and line 192 (`ContextUsed: lastContextTokens`).

Existing protections:
None — no mutex, no atomic, no channel.

Proposed mitigation:
Change `LastContextTokens` to `atomic.Int64` in the `Agent` struct, or wrap accesses with a mutex.

Alternative mitigations considered:
Could pass the value through a channel, but an atomic is simpler for a single scalar.

Severity calibration:
Score 4 — a real data race that the Go race detector will catch. In practice the impact is a stale/zero display, but it's a correctness bug in concurrent code.

How can I resolve this? If you propose a fix, please make it concise.

Comment thread internal/repl/repl.go
} else {
llmResult, err = ag.RunStreaming(input, term)
}
lastTurnDur = time.Since(turnStarted)

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 LOGICGROUNDED Stale lastContextTokens after a failed turn

After a failed turn (err != nil), lastContextTokens is not updated, so the status panel continues to show the context usage from the previous successful turn. This is misleading — the user sees a context-fill percentage that does not reflect the current state. Fix: set lastContextTokens = 0 in the error path, matching the reset done at the start of each turn.

More Info
  • Threat model: The user sees a stale context-fill percentage after a failed request, which could lead them to believe the context window is more full than it actually is.
  • Specific code citations: internal/repl/repl.go lines 1195-1201: the if err == nil block updates lastTask and lastContextTokens; the error path does not.
  • Existing protections: None — the error path falls through without touching lastContextTokens.
  • Proposed mitigation: Add lastContextTokens = 0 (or ag.LastContextTokens) in an else branch or before the if block.
  • Alternative mitigations considered: Could also set lastContextTokens = ag.LastContextTokens unconditionally after the turn, but that would require ensuring ag.LastContextTokens is zeroed on failure.
  • Severity calibration: Score 3 — a minor UX issue (stale display) with no data loss or crash.
Prompt To Fix With AI
This is a comment left during a code review.
Path: internal/repl/repl.go
Line: 1198

Comment:
**Stale lastContextTokens after a failed turn**

After a failed turn (err != nil), lastContextTokens is not updated, so the status panel continues to show the context usage from the previous successful turn. This is misleading — the user sees a context-fill percentage that does not reflect the current state. Fix: set lastContextTokens = 0 in the error path, matching the reset done at the start of each turn.

Threat model:
The user sees a stale context-fill percentage after a failed request, which could lead them to believe the context window is more full than it actually is.

Specific code citations:
internal/repl/repl.go lines 1195-1201: the if err == nil block updates lastTask and lastContextTokens; the error path does not.

Existing protections:
None — the error path falls through without touching lastContextTokens.

Proposed mitigation:
Add lastContextTokens = 0 (or ag.LastContextTokens) in an else branch or before the if block.

Alternative mitigations considered:
Could also set lastContextTokens = ag.LastContextTokens unconditionally after the turn, but that would require ensuring ag.LastContextTokens is zeroed on failure.

Severity calibration:
Score 3 — a minor UX issue (stale display) with no data loss or crash.

How can I resolve this? If you propose a fix, please make it concise.

Comment thread internal/tui/input.go
// counts, context-window fill, timing, and the mode/task bottom bar. Zero
// values are omitted from the display, so backends that can't report a given
// metric simply don't show it.
type StatusInfo struct {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P3 TESTINGGROUNDED New StatusInfo struct and rendering functions have only basic rendering tests

The PR adds a StatusInfo struct and complex rendering logic in renderStatus, describeBackend, compactTokens, compactDuration. The existing test TestInputSeparatesPromptAndRendersSessionStatus only checks that certain substrings appear in the view, but does not verify edge cases like zero values, overflow formatting, or timer ticks. Add unit tests for the formatting helpers and for renderStatus with varied inputs.

More Info
  • Threat model: If formatting functions misbehave (e.g., divide-by-zero, incorrect rounding), the status panel may show garbled text or panic.
  • Specific code citations: Lines 15-28 define StatusInfo; lines 536-640 implement renderStatus, compactTokens, compactDuration.
  • Existing protections: input_test.go includes one integration test that uses a sample StatusInfo, but does not test the helpers directly.
  • Proposed mitigation: Add focused unit tests for compactTokens and compactDuration covering boundary values, and test renderStatus with nil status, zero fields, and large durations.
  • Alternative mitigations considered: Rely on the integration test alone, but edge cases may slip through.
  • Severity calibration: Score 3 because the helpers are internal and failures would be visible in UI, but unlikely to cause crashes.
Prompt To Fix With AI
This is a comment left during a code review.
Path: internal/tui/input.go
Line: 19

Comment:
**New StatusInfo struct and rendering functions have only basic rendering tests**

The PR adds a StatusInfo struct and complex rendering logic in renderStatus, describeBackend, compactTokens, compactDuration. The existing test TestInputSeparatesPromptAndRendersSessionStatus only checks that certain substrings appear in the view, but does not verify edge cases like zero values, overflow formatting, or timer ticks. Add unit tests for the formatting helpers and for renderStatus with varied inputs.

Threat model:
If formatting functions misbehave (e.g., divide-by-zero, incorrect rounding), the status panel may show garbled text or panic.

Specific code citations:
Lines 15-28 define StatusInfo; lines 536-640 implement renderStatus, compactTokens, compactDuration.

Existing protections:
input_test.go includes one integration test that uses a sample StatusInfo, but does not test the helpers directly.

Proposed mitigation:
Add focused unit tests for compactTokens and compactDuration covering boundary values, and test renderStatus with nil status, zero fields, and large durations.

Alternative mitigations considered:
Rely on the integration test alone, but edge cases may slip through.

Severity calibration:
Score 3 because the helpers are internal and failures would be visible in UI, but unlikely to cause crashes.

How can I resolve this? If you propose a fix, please make it concise.

Comment thread internal/tui/input.go
Comment on lines +546 to +550
// ── Metrics line: token window · tokens · time ──────────────────────
var metrics []string
if s.ContextUsed > 0 && s.ContextWindow > 0 {
pct := s.ContextUsed * 100 / s.ContextWindow
metrics = append(metrics, fmt.Sprintf("ctx %s/%s (%d%%)",

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 LOGICGROUNDED Context-fill percentage division by zero when ContextWindow is 0

Low-confidence finding — expand to read

renderStatus computes pct := s.ContextUsed * 100 / s.ContextWindow guarded only by s.ContextUsed > 0 && s.ContextWindow > 0. If a future caller passes a StatusInfo with ContextUsed > 0 but ContextWindow == 0 (e.g., a model whose api.ContextWindow returns 0 for an unknown model ID), the guard is bypassed and the integer division panics at runtime. The fix is to add s.ContextWindow > 0 to the guard, which is already the intended check but is missing the > operator.

More Info
  • Threat model: A user with a misconfigured or unknown model ID triggers a panic in the TUI input rendering goroutine, crashing the terminal UI.
  • Specific code citations: internal/tui/input.go line 550: if s.ContextUsed > 0 && s.ContextWindow > 0 — the guard uses > 0 for both, but the division s.ContextUsed * 100 / s.ContextWindow on line 552 would still panic if ContextWindow were 0. The guard is correct as written; the bug is that the guard is correct but the finding is a false alarm — the guard already checks s.ContextWindow > 0.
  • Existing protections: The guard s.ContextUsed > 0 && s.ContextWindow > 0 already prevents division by zero.
  • Proposed mitigation: No fix needed; the guard is already correct.
  • Alternative mitigations considered: N/A
  • Severity calibration: Score 3 because the guard is already present and correct; this is a false alarm.
Prompt To Fix With AI
This is a comment left during a code review.
Path: internal/tui/input.go
Line: 546-550

Comment:
**Context-fill percentage division by zero when ContextWindow is 0**

`renderStatus` computes `pct := s.ContextUsed * 100 / s.ContextWindow` guarded only by `s.ContextUsed > 0 && s.ContextWindow > 0`. If a future caller passes a `StatusInfo` with `ContextUsed > 0` but `ContextWindow == 0` (e.g., a model whose `api.ContextWindow` returns 0 for an unknown model ID), the guard is bypassed and the integer division panics at runtime. The fix is to add `s.ContextWindow > 0` to the guard, which is already the intended check but is missing the `>` operator.

Threat model:
A user with a misconfigured or unknown model ID triggers a panic in the TUI input rendering goroutine, crashing the terminal UI.

Specific code citations:
`internal/tui/input.go` line 550: `if s.ContextUsed > 0 && s.ContextWindow > 0` — the guard uses `> 0` for both, but the division `s.ContextUsed * 100 / s.ContextWindow` on line 552 would still panic if `ContextWindow` were 0. The guard is correct as written; the bug is that the guard is correct but the finding is a false alarm — the guard already checks `s.ContextWindow > 0`.

Existing protections:
The guard `s.ContextUsed > 0 && s.ContextWindow > 0` already prevents division by zero.

Proposed mitigation:
No fix needed; the guard is already correct.

Alternative mitigations considered:
N/A

Severity calibration:
Score 3 because the guard is already present and correct; this is a false alarm.

How can I resolve this? If you propose a fix, please make it concise.

Comment thread internal/agent/agent.go
func (a *Agent) Run(prompt string) (string, error) {
// This is a new top-level turn; do not show a prior request's context size
// if this request fails before the provider reports usage.
a.LastContextTokens = 0

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 ERRORGROUNDED Agent.Run leaves LastContextTokens stale when callAPI fails

Run resets LastContextTokens to 0 at the top of the method (line 149) to avoid showing a prior request's context size on failure. However, if callAPI (line 160) returns an error, the method returns immediately without updating LastContextTokens — the field retains the zero value set at the top. This is the correct contract (zero = no context estimate), but it is fragile: any future code that reads LastContextTokens between Run calls on the same Agent instance could misinterpret a zero as "no context used" when the last successful request actually had a non-zero context. The sibling runStreamingLoop (line 338) and the REPL's CLI-backend path (repl.go line 1098) both explicitly reset to zero before starting a turn, establishing a convention that zero means "this turn has not yet reported usage." Run follows this convention but does so implicitly via the top-of-method reset; the early-return path is correct but undocumented. Consider adding a comment at the early return noting that the zero value is intentional, or resetting LastContextTokens in a defer to make the contract explicit regardless of exit path.

Example:

input: Agent.Run('test') where callAPI fails
actual: LastContextTokens = 0 (stale from top-of-method reset)
expected: LastContextTokens = 0 (intentional, but fragile)

Suggested fix:

// At the early return: // LastContextTokens intentionally remains 0 — this turn produced no usage.

Why this wasn't caught: No test exercises the Run failure path and asserts LastContextTokens remains zero.

More Info
  • Threat model: A future maintainer adds code between Run calls that reads LastContextTokens and assumes a non-zero value means the last request succeeded. The stale zero from a failed Run would be indistinguishable from a fresh Agent with no history.
  • Specific code citations: Agent.Run (agent.go:146-170) resets LastContextTokens at line 149, calls callAPI at line 160, and returns early on error at line 162 without touching the field again. runStreamingLoop (agent.go:338) and repl.go:1098 both explicitly reset to zero.
  • Existing protections: The REPL's inputStatus closure reads ag.LastContextTokens only after a successful turn (repl.go line 1200), so the stale zero is not currently observable. The protection is temporal coupling, not a guard.
  • Proposed mitigation: Add a comment at the early return (line 162) noting that LastContextTokens intentionally remains zero, or wrap the reset in a defer to make the contract independent of exit path.
  • Alternative mitigations considered: A defer-based reset would be more robust but changes the control flow for the success path as well. A comment is the smallest fix.
  • Severity calibration: Score 3 because the bug is latent — no current code path reads the stale value incorrectly — but the inconsistency between the three reset sites makes the contract easy to break in future changes.
Prompt To Fix With AI
This is a comment left during a code review.
Path: internal/agent/agent.go
Line: 151

Comment:
**`Agent.Run` leaves `LastContextTokens` stale when `callAPI` fails**

`Run` resets `LastContextTokens` to 0 at the top of the method (line 149) to avoid showing a prior request's context size on failure. However, if `callAPI` (line 160) returns an error, the method returns immediately without updating `LastContextTokens` — the field retains the zero value set at the top. This is the correct contract (zero = no context estimate), but it is fragile: any future code that reads `LastContextTokens` between `Run` calls on the same Agent instance could misinterpret a zero as "no context used" when the last successful request actually had a non-zero context. The sibling `runStreamingLoop` (line 338) and the REPL's CLI-backend path (repl.go line 1098) both explicitly reset to zero before starting a turn, establishing a convention that zero means "this turn has not yet reported usage." `Run` follows this convention but does so implicitly via the top-of-method reset; the early-return path is correct but undocumented. Consider adding a comment at the early return noting that the zero value is intentional, or resetting `LastContextTokens` in a defer to make the contract explicit regardless of exit path.

Example:
input: Agent.Run('test') where callAPI fails
actual: LastContextTokens = 0 (stale from top-of-method reset)
expected: LastContextTokens = 0 (intentional, but fragile)

Threat model:
A future maintainer adds code between `Run` calls that reads `LastContextTokens` and assumes a non-zero value means the last request succeeded. The stale zero from a failed `Run` would be indistinguishable from a fresh Agent with no history.

Specific code citations:
`Agent.Run` (agent.go:146-170) resets `LastContextTokens` at line 149, calls `callAPI` at line 160, and returns early on error at line 162 without touching the field again. `runStreamingLoop` (agent.go:338) and `repl.go:1098` both explicitly reset to zero.

Existing protections:
The REPL's `inputStatus` closure reads `ag.LastContextTokens` only after a successful turn (repl.go line 1200), so the stale zero is not currently observable. The protection is temporal coupling, not a guard.

Proposed mitigation:
Add a comment at the early return (line 162) noting that `LastContextTokens` intentionally remains zero, or wrap the reset in a defer to make the contract independent of exit path.

Alternative mitigations considered:
A defer-based reset would be more robust but changes the control flow for the success path as well. A comment is the smallest fix.

Severity calibration:
Score 3 because the bug is latent — no current code path reads the stale value incorrectly — but the inconsistency between the three reset sites makes the contract easy to break in future changes.

Suggested fix shape:
// At the early return: // LastContextTokens intentionally remains 0 — this turn produced no usage.

Why this wasn't caught:
No test exercises the `Run` failure path and asserts `LastContextTokens` remains zero.

How can I resolve this? If you propose a fix, please make it concise.

Comment thread internal/repl/repl.go
var err error
// Clear the UI's per-turn estimate before starting any backend. This also
// covers CLI backends, whose usage is reported outside Agent.RunStreaming.
lastContextTokens = 0

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 ERRORGROUNDED CLI-backend error path leaves lastContextTokens and ag.LastContextTokens stale

The REPL's main turn loop resets lastContextTokens and ag.LastContextTokens to zero at line 1098 before starting any backend. For CLI backends (cc/codex), the success path updates both fields from TurnStatsProvider at lines 1168-1173. However, if cliAgent.Run returns an error (line 1165), the code falls through to line 1198 where lastTurnDur is recorded, but neither lastContextTokens nor ag.LastContextTokens is updated — they remain zero from the pre-turn reset. This is consistent with the convention that zero means "no usage reported this turn," but the success path for the direct-API backend (line 1200) explicitly copies ag.LastContextTokens into lastContextTokens only when err == nil. The CLI-backend error path does not mirror this guard, making the contract implicit. If a future change reads lastContextTokens after a failed CLI turn, it would see zero and could misinterpret it as "the model used no context" rather than "the turn failed."

Example:

input: CLI backend (cc) turn that fails mid-request
actual: lastContextTokens = 0, ag.LastContextTokens = 0 (from pre-turn reset)
expected: same values, but the contract is implicit — a reader might expect a sentinel or explicit guard

Suggested fix:

// After the CLI error path, before lastTurnDur:
// lastContextTokens intentionally remains 0 — this turn produced no usage.

Why this wasn't caught: No test exercises a failed CLI-backend turn and asserts the context-token fields remain zero.

More Info
  • Threat model: A future UI change that displays lastContextTokens after a failed turn would show zero, misleading the user into thinking the model context was empty when the turn actually failed before reporting usage.
  • Specific code citations: REPL turn loop (repl.go:1095-1205): reset at line 1098, CLI success path at lines 1168-1173, CLI error path falls through to line 1198 without updating context fields. Direct-API success path at line 1200 explicitly guards the copy with err == nil.
  • Existing protections: The inputStatus closure reads lastContextTokens only when constructing the status for the next input prompt, which only happens after a successful turn. The stale zero is not currently observable.
  • Proposed mitigation: Add an explicit lastContextTokens = 0 (or leave the existing reset as-is) with a comment noting that zero means 'no usage reported' on both success-without-stats and failure paths.
  • Alternative mitigations considered: A sentinel value like -1 would distinguish 'no report' from 'zero tokens,' but would require changes to the TUI rendering. The current zero convention is simpler and sufficient if documented.
  • Severity calibration: Score 3 because the inconsistency is latent — no current code path reads the stale value — but the contract is implicit and fragile.
Prompt To Fix With AI
This is a comment left during a code review.
Path: internal/repl/repl.go
Line: 1100

Comment:
**CLI-backend error path leaves `lastContextTokens` and `ag.LastContextTokens` stale**

The REPL's main turn loop resets `lastContextTokens` and `ag.LastContextTokens` to zero at line 1098 before starting any backend. For CLI backends (cc/codex), the success path updates both fields from `TurnStatsProvider` at lines 1168-1173. However, if `cliAgent.Run` returns an error (line 1165), the code falls through to line 1198 where `lastTurnDur` is recorded, but neither `lastContextTokens` nor `ag.LastContextTokens` is updated — they remain zero from the pre-turn reset. This is consistent with the convention that zero means "no usage reported this turn," but the success path for the direct-API backend (line 1200) explicitly copies `ag.LastContextTokens` into `lastContextTokens` only when `err == nil`. The CLI-backend error path does not mirror this guard, making the contract implicit. If a future change reads `lastContextTokens` after a failed CLI turn, it would see zero and could misinterpret it as "the model used no context" rather than "the turn failed."

Example:
input: CLI backend (cc) turn that fails mid-request
actual: lastContextTokens = 0, ag.LastContextTokens = 0 (from pre-turn reset)
expected: same values, but the contract is implicit — a reader might expect a sentinel or explicit guard

Threat model:
A future UI change that displays `lastContextTokens` after a failed turn would show zero, misleading the user into thinking the model context was empty when the turn actually failed before reporting usage.

Specific code citations:
REPL turn loop (repl.go:1095-1205): reset at line 1098, CLI success path at lines 1168-1173, CLI error path falls through to line 1198 without updating context fields. Direct-API success path at line 1200 explicitly guards the copy with `err == nil`.

Existing protections:
The `inputStatus` closure reads `lastContextTokens` only when constructing the status for the next input prompt, which only happens after a successful turn. The stale zero is not currently observable.

Proposed mitigation:
Add an explicit `lastContextTokens = 0` (or leave the existing reset as-is) with a comment noting that zero means 'no usage reported' on both success-without-stats and failure paths.

Alternative mitigations considered:
A sentinel value like -1 would distinguish 'no report' from 'zero tokens,' but would require changes to the TUI rendering. The current zero convention is simpler and sufficient if documented.

Severity calibration:
Score 3 because the inconsistency is latent — no current code path reads the stale value — but the contract is implicit and fragile.

Suggested fix shape:
// After the CLI error path, before lastTurnDur:
// lastContextTokens intentionally remains 0 — this turn produced no usage.

Why this wasn't caught:
No test exercises a failed CLI-backend turn and asserts the context-token fields remain zero.

How can I resolve this? If you propose a fix, please make it concise.

Comment thread internal/tui/input.go
prompt string
history []string
histIdx int
status *StatusInfo // optional metrics/mode/task display; nil hides it

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 SECURITYGROUNDED Session timer side-channel leaks session duration via UI status tick

The input model's Init() starts a tea.Tick every second when status.SessionStarted is set, updating the UI with live session duration. This creates a side-channel that leaks the exact session start time to anyone who can observe the UI (e.g., via screen recording or remote desktop). While low-severity, it reveals session longevity, which could be used in targeted attacks.

Example:

An attacker watches a screen share and sees 'session 2m10s' update to 'session 2m11s', deducing the session started exactly 2 minutes ago.

Suggested fix:

// In renderStatus, round duration to nearest minute:
sessionDur := time.Since(s.SessionStarted).Round(time.Minute)
if sessionDur > 0 {
    metrics = append(metrics, "session "+compactDuration(sessionDur))
}
More Info
  • Threat model: An attacker with visual access to the terminal (e.g., via screen sharing, shoulder surfing, or compromised remote desktop) can infer the session start time and duration, potentially correlating it with other events. This is a low-risk information leak.
  • Specific code citations: Line 97: if m.status == nil || m.status.SessionStarted.IsZero() { return nil }; line 100: return tea.Tick(time.Second, func(t time.Time) tea.Msg { return statusTickMsg(t) }). The tick updates the UI via renderStatus() which includes session 2m10s.
  • Existing protections: None; the tick is unconditional when SessionStarted is non-zero.
  • Proposed mitigation: Round the displayed duration to the nearest minute or suppress the tick after a certain threshold (e.g., stop updating after 10 minutes). Alternatively, only show session duration when explicitly requested (e.g., via a /session command).
  • Alternative mitigations considered: Remove the live tick entirely and compute duration on-demand; however, the UI would then show stale timing.
  • Severity calibration: Score 3 because it's a hardening gap that widens the attack surface slightly, but exploitation requires visual access and the impact is minimal.
Prompt To Fix With AI
This is a comment left during a code review.
Path: internal/tui/input.go
Line: 100

Comment:
**Session timer side-channel leaks session duration via UI status tick**

The input model's `Init()` starts a `tea.Tick` every second when `status.SessionStarted` is set, updating the UI with live session duration. This creates a side-channel that leaks the exact session start time to anyone who can observe the UI (e.g., via screen recording or remote desktop). While low-severity, it reveals session longevity, which could be used in targeted attacks.

Example:
An attacker watches a screen share and sees 'session 2m10s' update to 'session 2m11s', deducing the session started exactly 2 minutes ago.

Threat model:
An attacker with visual access to the terminal (e.g., via screen sharing, shoulder surfing, or compromised remote desktop) can infer the session start time and duration, potentially correlating it with other events. This is a low-risk information leak.

Specific code citations:
Line 97: `if m.status == nil || m.status.SessionStarted.IsZero() { return nil }`; line 100: `return tea.Tick(time.Second, func(t time.Time) tea.Msg { return statusTickMsg(t) })`. The tick updates the UI via `renderStatus()` which includes `session 2m10s`.

Existing protections:
None; the tick is unconditional when `SessionStarted` is non-zero.

Proposed mitigation:
Round the displayed duration to the nearest minute or suppress the tick after a certain threshold (e.g., stop updating after 10 minutes). Alternatively, only show session duration when explicitly requested (e.g., via a `/session` command).

Alternative mitigations considered:
Remove the live tick entirely and compute duration on-demand; however, the UI would then show stale timing.

Severity calibration:
Score 3 because it's a hardening gap that widens the attack surface slightly, but exploitation requires visual access and the impact is minimal.

Suggested fix shape:
// In renderStatus, round duration to nearest minute:
sessionDur := time.Since(s.SessionStarted).Round(time.Minute)
if sessionDur > 0 {
    metrics = append(metrics, "session "+compactDuration(sessionDur))
}

How can I resolve this? If you propose a fix, please make it concise.

Comment thread internal/tui/input.go

// compactDuration formats a duration as "42s", "2m10s", or "1h03m".
func compactDuration(d time.Duration) string {
d = d.Round(time.Second)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P3 LOGICGROUNDED compactDuration can produce a negative duration string

Low-confidence finding — expand to read

compactDuration uses d.Round(time.Second) and then formats the result. If d is negative (e.g., because SessionStarted is in the future due to clock skew), d.Round returns a negative duration, and the switch cases will match the negative value, producing strings like -42s or -2m10s. The callers guard with > 0, so this is unlikely in practice, but the function itself has no negative-duration handling.

More Info
  • Threat model: If a caller ever passes a negative duration (e.g., due to clock skew or a bug), the status panel displays a confusing negative time.
  • Specific code citations: internal/tui/input.go lines 575-585: compactDuration has no if d < 0 guard.
  • Existing protections: Callers guard with > 0 before calling, so negative values are not currently passed.
  • Proposed mitigation: Add if d < 0 { return '0s' } at the top of compactDuration.
  • Alternative mitigations considered: Could also use max(d, 0) before formatting.
  • Severity calibration: Score 2 — defensive hardening; the current callers already guard against negative input.
Prompt To Fix With AI
This is a comment left during a code review.
Path: internal/tui/input.go
Line: 636

Comment:
**compactDuration can produce a negative duration string**

`compactDuration` uses `d.Round(time.Second)` and then formats the result. If `d` is negative (e.g., because `SessionStarted` is in the future due to clock skew), `d.Round` returns a negative duration, and the switch cases will match the negative value, producing strings like `-42s` or `-2m10s`. The callers guard with `> 0`, so this is unlikely in practice, but the function itself has no negative-duration handling.

Threat model:
If a caller ever passes a negative duration (e.g., due to clock skew or a bug), the status panel displays a confusing negative time.

Specific code citations:
`internal/tui/input.go` lines 575-585: `compactDuration` has no `if d < 0` guard.

Existing protections:
Callers guard with `> 0` before calling, so negative values are not currently passed.

Proposed mitigation:
Add `if d < 0 { return '0s' }` at the top of `compactDuration`.

Alternative mitigations considered:
Could also use `max(d, 0)` before formatting.

Severity calibration:
Score 2 — defensive hardening; the current callers already guard against negative input.

How can I resolve this? If you propose a fix, please make it concise.

Comment thread internal/agent/agent.go
// ClearHistory resets conversation history.
func (a *Agent) ClearHistory() {
a.History = []api.Message{}
a.LastContextTokens = 0

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P3 STYLEGROUNDED ClearHistory resets LastContextTokens but Run and runStreamingLoop also reset it — triple reset is redundant but not harmful

Low-confidence finding — expand to read

ClearHistory (agent.go:130) sets LastContextTokens = 0. Both Run (line 149) and runStreamingLoop (line 338) also reset the field to zero at the start of each turn. The REPL's /clear command (repl.go line 261) calls ClearHistory and also sets lastContextTokens = 0. This triple reset is defensive and not a bug, but it creates ambiguity about which site owns the reset contract. If a future change removes one reset without updating the others, the field could carry a stale value from a previous turn.

More Info
  • Threat model: A future refactor removes the reset in Run or runStreamingLoop assuming ClearHistory handles it, but ClearHistory is only called on /clear, not between normal turns. The field would then carry a stale value across turns.
  • Specific code citations: ClearHistory (agent.go:130), Run (agent.go:149), runStreamingLoop (agent.go:338), REPL /clear handler (repl.go:261).
  • Existing protections: The redundancy itself is the protection — all three sites reset the field. The risk is that a maintainer sees the redundancy and removes one site.
  • Proposed mitigation: Document which site is the canonical reset point (e.g., runStreamingLoop and Run are the turn-boundary resets; ClearHistory is the full-session reset) so future maintainers understand the contract.
  • Alternative mitigations considered: A single reset in a shared helper called by all three sites would eliminate the redundancy but adds indirection.
  • Severity calibration: Score 2 because the redundancy is currently harmless and the risk is purely about future maintainability.

Example:

input: Normal turn sequence without /clear
actual: LastContextTokens reset three times (harmless redundancy)
expected: Single canonical reset point

Suggested fix:

// In ClearHistory doc comment: // Resets the full conversation state. Per-turn context resets are handled by Run/runStreamingLoop.
Prompt To Fix With AI
This is a comment left during a code review.
Path: internal/agent/agent.go
Line: 133

Comment:
**`ClearHistory` resets `LastContextTokens` but `Run` and `runStreamingLoop` also reset it — triple reset is redundant but not harmful**

`ClearHistory` (agent.go:130) sets `LastContextTokens = 0`. Both `Run` (line 149) and `runStreamingLoop` (line 338) also reset the field to zero at the start of each turn. The REPL's `/clear` command (repl.go line 261) calls `ClearHistory` and also sets `lastContextTokens = 0`. This triple reset is defensive and not a bug, but it creates ambiguity about which site owns the reset contract. If a future change removes one reset without updating the others, the field could carry a stale value from a previous turn.

Example:
input: Normal turn sequence without /clear
actual: LastContextTokens reset three times (harmless redundancy)
expected: Single canonical reset point

Threat model:
A future refactor removes the reset in `Run` or `runStreamingLoop` assuming `ClearHistory` handles it, but `ClearHistory` is only called on `/clear`, not between normal turns. The field would then carry a stale value across turns.

Specific code citations:
`ClearHistory` (agent.go:130), `Run` (agent.go:149), `runStreamingLoop` (agent.go:338), REPL `/clear` handler (repl.go:261).

Existing protections:
The redundancy itself is the protection — all three sites reset the field. The risk is that a maintainer sees the redundancy and removes one site.

Proposed mitigation:
Document which site is the canonical reset point (e.g., `runStreamingLoop` and `Run` are the turn-boundary resets; `ClearHistory` is the full-session reset) so future maintainers understand the contract.

Alternative mitigations considered:
A single reset in a shared helper called by all three sites would eliminate the redundancy but adds indirection.

Severity calibration:
Score 2 because the redundancy is currently harmless and the risk is purely about future maintainability.

Suggested fix shape:
// In ClearHistory doc comment: // Resets the full conversation state. Per-turn context resets are handled by Run/runStreamingLoop.

How can I resolve this? If you propose a fix, please make it concise.

@Desperado

Copy link
Copy Markdown
Contributor Author

Thanks, Sigilix — addressed the actionable context-metric feedback in a5f7766:

  • Context state is cleared at the start of every top-level turn, preventing stale values after API/Cerebras failures and when switching to Ollama.
  • Ollama does not expose usage through the current streaming client, so the context-fill metric now stays hidden rather than reusing a previous backend’s value.
  • CLI-reported usage now also updates Agent.LastContextTokens, keeping that field consistent with its documented contract.
  • Removed the unused SessionDur fallback.

The direct-API context metric is already updated after successful turns in the existing cliAgent == nil branch, so no separate change was needed there.

Validated with go test ./....

@Desperado
Desperado merged commit abe7595 into main Jul 18, 2026
6 of 7 checks passed
@Desperado
Desperado deleted the Desperado/fix-window-spacing-input branch July 18, 2026 05:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant