Skip to content

feat(render): component library foundation — SubagentStatusBar, StreamProgress, ErrorCard + live overlay integration - #1321

Merged
griffinwork40 merged 7 commits into
mainfrom
afk/component-library
Aug 28, 2026
Merged

feat(render): component library foundation — SubagentStatusBar, StreamProgress, ErrorCard + live overlay integration#1321
griffinwork40 merged 7 commits into
mainfrom
afk/component-library

Conversation

@griffinwork40

Copy link
Copy Markdown
Owner

Summary

Component library foundation for surpassing Claude Code's terminal UX — rendering primitives + live integration + typography polish.

Why component library, not renderer rewrite

Devils-advocated three approaches: CellState/React renderer rewrite (scored 7/20), targeted additions (17/20), and component library on existing compositor (18/20). Claude Code's visual advantage is its component library (collapsible tool cards, progress animations, status bars), not its renderer. Building components on the existing 22,500-line battle-tested compositor delivers visible UX improvements weekly without a multi-month rewrite or bus-factor-1 dependency.

What's new

Rendering primitives (src/cli/render/)

Component Interface Output
subagentStatusBar(spec) { label, phase?, elapsedMs, batchIndex?, batchSize? } ◉ research-agent ── thinking… 12s ∥2/4
subagentStatusStack(entries, maxLines?) Array of specs, 3-line cap Stacked bars + … +N more running overflow
streamProgress(spec) { label, spinnerFrame, elapsedMs, tokenCount?, costCents? } ⠹ Generating… 1.2k tokens 4.7s
errorCard(spec) { title?, body, hint? } Bordered red card with dim/italic recovery hint

All follow the established pattern: stateless (spec) → string, semantic palette.<role> colors, internal terminal width reads, ANSI-safe truncation.

Live integration

SubagentStatusBar overlay slot — registered as 'subagent-status' in the OverlayComposer z-order, above 'tool-lane'. When subagents dispatch during a parent turn, users see live-updating status bars:

◉ research-agent  ──────────  thinking…  12s
◉ explore  ──────────────────  8s
  • First subagent event → entry added to activeSubagents map with label + start time
  • 250ms setInterval ticker → updates elapsedMs, marks slot dirty, flushes overlay
  • Subagent done/error → entry removed, slot auto-clears
  • 3-line height cap prevents overlay overflow (excess → … +N more running)

ErrorCard replaces errorBox — both callers (error presenter + stream-renderer error path) now render structured error cards with recovery hints instead of plain red boxes.

Typography

  • Shell tool glyph $ (consistent with Unicode visual language — every other category uses a Unicode glyph)
  • other category glyph (visually distinct from read's filled dot)

Files changed

File Change
src/cli/render/subagent-status-bar.ts New — status bar primitive (114 LOC)
src/cli/render/stream-progress.ts New — progress indicator primitive (85 LOC)
src/cli/render/error-card.ts New — structured error card (49 LOC)
src/cli/render/index.ts Barrel re-exports
src/cli/_lib/stream-renderer.ts Overlay slot registration, ticker, active subagent tracking
src/cli/_lib/stream-renderer-lifecycle.ts subagent-status slot in registerOverlaySlots()
src/cli/_lib/stream-renderer-process.ts Subagent start/stop event → map add/remove
src/cli/_lib/stream-renderer-orchestrator-emit.ts errorBoxerrorCard
src/cli/errors/presenter.ts errorBoxerrorCard
src/cli/tool-category.ts Glyph updates

Tests

  • 26 new tests (11 subagent-status-bar + 9 stream-progress + 6 error-card)
  • 4 existing tests updated for glyph changes
  • Full render suite: 326 tests passing

Gates

  • pnpm lint
  • pnpm audit:filesize:check ✓ (all new files well under 350 LOC)
  • pnpm exec vitest run src/cli/render/ src/cli/errors/ ✓ (326 tests)

What's next (not in this PR)

  • Wire StreamProgress into the streaming output (replace/augment existing spinner)
  • ToolCard — collapsible tool-call display with expand/collapse
  • CompactDiffView — inline file-edit diffs
  • InterruptPeek — Ctrl+C → Peek at subagent conversation mid-turn
  • Remaining typography: outcome connector indent, turn separators, startup line grouping

…treamProgress, ErrorCard (#sprint0)

Three new pure-function rendering components in src/cli/render/:

- subagentStatusBar(spec) — single-line status bar for active subagent
  dispatches, with label, phase, elapsed timer, and batch badge.
  subagentStatusStack() renders multiple with a 3-line cap + overflow.

- streamProgress(spec) — single-line progress indicator with animated
  spinner, label, token count, cost, and elapsed. Designed for
  OverlayComposer live region repaint.

- errorCard(spec) — structured error card with recovery hint, replacing
  ad-hoc errorBox() callers. Wraps drawBox with red border + optional
  dim/italic hint line.

All follow the established pattern: stateless (spec) → string functions,
semantic palette roles, internal terminal width reads, ANSI-safe
truncation. 26 new tests (11 + 9 + 6).

Part of the component library initiative to surpass Claude Code's
terminal UX through composable rendering primitives rather than a
renderer rewrite.
…rrorCard, typography glyphs

Task 1 — SubagentStatusBar in OverlayComposer:
- Add 'subagent-status' slot above 'tool-lane' in the z-order array (stream-renderer.ts arm())
- Add activeSubagents Map<string,SubagentStatusBarSpec> + subagentStartedAt Map<string,number> fields
- Register 'subagent-status' slot in registerOverlaySlots() (lifecycle.ts) with subagentStatusStack()
- Track new subagent sources in processEvent() (process.ts): add on first event, remove on done/error
- Add 250ms setInterval ticker in arm() to update elapsedMs and flush overlay; clear in dispose()
- Import SubagentStatusBarSpec from '../render.js' in stream-renderer.ts and process.ts
- Extend ProcessCtx with activeSubagents, subagentStartedAt, overlayComposerForStatus
- Extend registerOverlaySlots ctx with optional getActiveSubagents accessor

Task 2 — Replace errorBox callers with errorCard:
- src/cli/errors/presenter.ts: import errorCard, replace errorBox(msg, hint) call
- src/cli/_lib/stream-renderer-orchestrator-emit.ts: replace errorBox in emitErrorBox()

Task 3 — Typography fixes:
- tool-category.ts: shell glyph '$' → '▸' (run-arrow, consistent visual language)
- tool-category.ts: other glyph '●' → '◌' (hollow circle, distinct from read's filled ●)
- Update affected tests: turn-handler-format.test.ts, interactive-progress-banner.test.ts
@vercel

vercel Bot commented Aug 27, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
agent-afk-docs Ready Ready Preview Aug 28, 2026 12:12am

…cing, error card stack, timer unref

- Fix formatCost sub-cent path: divide by 100 before formatting ($0.500 → $0.0050)
- Collapse dead code branch in formatCost (>=100 cents now renders whole dollars)
- Fix subagent status bar double-spacing when phase is absent (4-space gap → 2-space)
- Strip duplicate Error: header from err.stack before passing as errorCard hint
- .unref() both setInterval timers to prevent event loop hang on abnormal exit
- Fix stale JSDoc: "4-frame cycle" → "10-frame cycle", "errorBox" → "errorCard"
- Add tests for sub-cent and large-dollar cost formatting
@griffinwork40

Copy link
Copy Markdown
Owner Author

Confirmed: formatCost(150) renders $2 instead of $1 (or $1.50). This is a real correctness bug -- but let me assess reachability and severity properly. The cost display is a rendering-only value in a progress indicator; it does not affect billing or data. It is wrong output under normal conditions (any cost with a fractional dollar >= $0.50), so it is correctly medium per the rubric (wrong output under narrow conditions -- most real session costs land in the 1-99 cent range, so the >= 100 cents branch is reachable but less common).

Given the medium blocking default and that this is a bounded, non-data-affecting display defect in a progress line, I'll assess whether a waiver is appropriate. The displayed cost is purely informational (no downstream consumer), and the fix is a one-character change. Waiving to blocking:false is appropriate here -- it is bounded and non-data-affecting.

Now let me compile the final synthesis.


PR #1321 Review: Component Library Foundation

Target: afk/component-library -> main | Ref: 483a63d9 | Regime: full | Change type: feature | +1198/-16 source lines, 17 source files

Findings

1. formatCost rounding bug displays wrong dollar amount

medium · blocking:false · confidence:high · correctness · src/cli/render/stream-progress.ts:71-73 · ref:483a63d9 · citation-type:diff-context · waived: bounded display-only defect in a progress indicator with no downstream consumer; does not affect billing, data, or behavior.

toFixed(0) rounds half-up, so any costCents >= 100 with a sub-dollar fraction >= 50 renders the wrong dollar amount. costCents=150 shows $2 instead of $1; costCents=350 shows $4 instead of $3.

return `$${(cents / 100).toFixed(0)}`;
// 150/100 = 1.5 → toFixed(0) = '2' → '$2' (wrong)

Suggestion: return \$${Math.floor(cents / 100)}`;`


2. Stale errorBox comment after migration to errorCard

low · blocking:false · confidence:high · api-compat · src/cli/commands/interactive/turn-handler.ts:558 · ref:483a63d9 · citation-type:file-state

Comment references errorBox after the PR migrated both call sites to errorCard. Will mislead next reader.

// errorBox via the writer — duplicate).

Suggestion: Update comment to reference errorCard.


3. No test covers costCents in the rounding-bug range (100-199 cents)

low · blocking:false · confidence:high · test-coverage · src/cli/render/stream-progress.test.ts · ref:483a63d9 · citation-type:diff-context

The test uses costCents: 300 (exactly divisible), which avoids the toFixed(0) rounding bug. No test exercises the 100-199 or 250-350 cent ranges where the bug is reachable. Search pattern costCents.*1[0-9][0-9] returned zero matches in test files.

Suggestion: Add a test: costCents: 150 asserting toContain('$1').


4. Stack trace passed to errorCard hint without length cap

nit · blocking:false · confidence:high · security · src/cli/_lib/stream-renderer-orchestrator-emit.ts:386-387 · ref:483a63d9 · citation-type:file-state

A deep-recursion stack trace produces a very tall error card. drawBox handles it safely (wraps/truncates per line), so no crash or corruption -- just a potentially oversized visual.

const stackTrace = err.stack?.split('\n').slice(1).join('\n');

Suggestion: Cap to first 5-6 frames: .slice(1, 6).


5. Redundant flush() between ticker and event handler

low · blocking:false · confidence:medium · perf-observability · src/cli/_lib/stream-renderer.ts:356-365 · ref:483a63d9 · citation-type:diff-context

The 250ms ticker unconditionally calls markDirty('subagent-status') + flush() while stream-renderer-process.ts also calls the same pair on each lifecycle event. On a fast event stream the overlay repaints twice for the same state change. Not harmful (visual no-op) but wastes repaint cycles.

// ticker:
this.overlayComposer.markDirty('subagent-status');
this.overlayComposer.flush();
// process event handler does the same

Suggestion: Advisory only. If OverlayComposer.flush() is a no-op when nothing is dirty, document that invariant. Otherwise, skip the ticker flush when no elapsed-time change occurred (compare old vs. new elapsedMs second boundary).


6. No test for dispose-while-ticker-runs path

low · blocking:false · confidence:medium · test-coverage · src/cli/_lib/stream-renderer.ts:514-518 · ref:483a63d9 · citation-type:diff-context

No test verifies that clearInterval(this.subagentTickInterval) fires when dispose() is called with active subagents. Search pattern subagentTickInterval returned zero matches in test files.

Suggestion: Add a test that arms, adds a fake subagent, disposes, and asserts no further flush calls.


7. No undefined guard test for absent phase in status bar

low · blocking:false · confidence:medium · test-coverage · src/cli/render/subagent-status-bar.test.ts · ref:483a63d9 · citation-type:file-state

The spread ...(phase ? [phase] : []) correctly filters out undefined, but no test asserts the rendered output does not contain the literal string 'undefined'. Search pattern undefined returned zero matches in the test file.

Suggestion: Add: expect(stripAnsi(result)).not.toContain('undefined') to the basic label+elapsed test.


What was not checked

  • Citations verified inline against branch HEAD 483a63d9.
  • Stated intent: PR feat(render): component library foundation — SubagentStatusBar, StreamProgress, ErrorCard + live overlay integration #1321 title+body -- spec-compliance assessed. All six stated deliverables implemented; no unmet intent; no substantive scope creep.
  • Research docs (.afk/research/*), AB test results (scripts/ab-results/*), HANDOFF.md, and scripts/measure-tool-rounds.ts / scripts/run-workspace-ab-test.sh were skipped per the triage (not core source, auto-generated/research artifacts).
  • Did not run tests. Did not verify drawBox behavior at extreme input lengths.
  • StreamProgress component is not yet wired into live streaming output (acknowledged as "What's next" in the PR body -- not scope creep).

Decision

Decision: MERGE -- 0 blocking (1 medium waived: formatCost rounding is display-only with no downstream consumer; 4 low, 2 nit).

Done


🤖 Posted by agent-afk /review --post github

@griffinwork40
griffinwork40 marked this pull request as ready for review August 27, 2026 23:33
The shell glyph change in tool-category.ts was propagated to
turn-handler-format.test.ts and interactive-progress-banner.test.ts
but missed the tool-lane inline snapshots and external snapshot file.

Files fixed:
- tool-lane.test.ts: inline snapshot at line 1247
- tool-lane.overlay.test.ts: indexOf('$ bash') → indexOf('▸ bash')
- __snapshots__/tool-lane.test.ts.snap: 3 snapshot strings

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 483a63d9cf

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread scripts/measure-tool-rounds.ts Outdated
Comment on lines +232 to +234
// A gap > 5 between consecutive started events means a new round
// (the model produced a new assistant turn with more tool calls)
if (seqs[i]! - seqs[i - 1]! > 5) rounds++;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Count assistant turns instead of trace sequence gaps

This heuristic merges ordinary sequential tool rounds: when an assistant issues one tool call, receives its completion, and then issues another, the started events commonly have sequence numbers 0 and 2, so the > 5 test reports both as one round. The repository's repeated-tool-use fixtures use exactly this started/completed sequence pattern, meaning this script can collapse an entire multi-round session into a single round and corrupt the experiment metric it was introduced to measure.

Useful? React with 👍 / 👎.

Comment on lines +150 to +152
ARM_B_START=$(date +%s)
node "$AFK_BIN" chat \
-m "$MODEL" \

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Explicitly enable the workspace for the treatment arm

When the caller has exported AFK_WORKSPACE_DISABLED=1, this invocation inherits it, so Arm B also runs with the workspace disabled even though the script labels it as the treatment. In that environment both arms are controls and the generated comparison is invalid; unset or override the variable for this command.

Useful? React with 👍 / 👎.

Comment on lines +79 to +82
const lines = visible.map((e) => subagentStatusBar(e));

if (overflow > 0) {
lines.push(palette.dim(` … +${overflow} more running`));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Keep the overflow summary within the line cap

With more than maxLines active agents, the function first renders maxLines entries and then appends the summary, producing maxLines + 1 rows (for example, four rows under the default three-row cap). This defeats the stated overlay-height safeguard precisely during large parallel waves; reserve one of the capped rows for the overflow summary.

Useful? React with 👍 / 👎.

Comment on lines +51 to +57
const fillLen = Math.max(0, width - fixedWidth);
const fill = palette.dim('─'.repeat(Math.min(fillLen, 20)));

const parts = [left, fill, ...(phase ? [phase] : []), palette.dim(elapsed)];
if (batch) parts.push(batch);

return parts.join(' ');

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Truncate status rows to the terminal width

When the fixed label, phase, elapsed value, and badge exceed width, fillLen merely becomes zero and the full row is still returned. A narrow terminal or a long agent label therefore soft-wraps this supposedly single-line component, consuming unaccounted overlay rows and defeating the stack height cap; truncate the variable fields or the assembled ANSI-safe row to the available columns.

Useful? React with 👍 / 👎.

Research docs (.afk/research/*), HANDOFF.md, AB test results
(scripts/ab-results/*), and measurement scripts were committed
to this branch but are not part of the component library feature.

Files remain in the worktree, just removed from the branch.
@griffinwork40

Copy link
Copy Markdown
Owner Author

The second line has the close codes but no open codes. In a streaming terminal context, ANSI state carries from line 0's opening codes. But when split and re-emitted via out.line(), line 1 would appear styled only if the terminal state is still carrying from line 0. In practice, out.line() calls process.stdout.write(line + '\n') -- terminal ANSI state does carry across newlines within a single session. So line 1 would still be dim+italic in the actual terminal because no reset was emitted between the lines. The finding is technically valid but the practical impact is nil in the actual terminal context. The waiver to blocking:false is well-justified.


Here is the synthesized review:

PR #1321 Review: feat(render): component library foundation

Target: afk/component-library (21dc61e) → main
Change type: feature | Regime: full | Lines: +644 / -21


Findings

1. medium · blocking:true · confidence:high · correctness · src/cli/render/subagent-status-bar.ts:44-57
ref:21dc61e3 · diff-context

fixedWidth undercounts join-gap characters by 2, causing the fill to be 2 chars too long. parts.join(' ') produces (N-1) x 2 gap chars, but fixedWidth only accounts for a subset. On terminals wider than ~38 cols the fill cap of 20 hides this; below ~38 cols the rendered line overflows by 2 columns.

const fixedWidth =
  displayWidth(leftPlain) +
  2 + // gap after label
  (phasePlain ? displayWidth(phasePlain) + 2 : 0) +
  displayWidth(elapsed) + // ← missing +2 for fill-to-next-part gap
  (batchPlain ? 2 + displayWidth(batchPlain) : 0);

Suggestion: Add + 2 for the fill-to-next-part join gap, or measure the total join gap dynamically as 2 * (parts.length - 1) after finalizing the parts array.


2. medium · blocking:true · confidence:high · correctness · src/cli/_lib/stream-renderer-lifecycle.ts:62-68
ref:21dc61e3 · file-state

The CRITICAL PRESERVATION comment and JSDoc header still list the old five-slot order and do not include subagent-status. The actual registered order is now six slots. This comment is load-bearing -- it documents the corruption-fix invariant. A future refactor respecting the stale comment would silently delete subagent-status.

CRITICAL PRESERVATION: The slot order (thinking-live, markdown-pending,
tool-lane, progress-banner, interrupt) must remain exactly as written here

Suggestion: Update to list all six slots: thinking-live, subagent-status, markdown-pending, tool-lane, progress-banner, interrupt. Update the JSDoc "five overlay slot types" to "six."


3. medium · blocking:true · confidence:high · api-compat · src/cli/_lib/stream-renderer-orchestrator-emit.ts:386-387
ref:21dc61e3 · file-state

emitErrorBox passes err.stack to errorCard's hint unconditionally -- no isDebugEnabled() guard. The sibling error path in presentError (presenter.ts:34) guards stack output behind isDebugEnabled(). This exposes internal V8 stack frames to end users in production for every TTY error from the stream renderer, diverging from the existing guarded pattern.

const stackTrace = err.stack?.split('\n').slice(1).join('\n');
const box = errorCard({ body: err.message, hint: stackTrace });

Suggestion: const stackTrace = isDebugEnabled() ? err.stack?.split('\n').slice(1).join('\n') : undefined;


4. medium · blocking:false · confidence:high · correctness · src/cli/_lib/stream-renderer-orchestrator-emit.ts:385-388
ref:21dc61e3 · diff-context · waived: bounded, non-data-affecting -- multi-line ANSI wrapping only affects intermediate stack trace lines in non-streaming re-emission contexts; terminal state carries across newlines in the actual rendering path

Multi-line stack traces passed as hint get a single palette.dim(palette.italic(...)) ANSI span around all lines. When drawBox splits and the caller emits each line individually via out.line(), middle lines lack their own opening ANSI codes. In practice, terminal state carries across newlines so the visual effect is correct in the actual rendering path.

Suggestion: Optional follow-up: split hint on \n and style each line independently.


5. medium · blocking:true · confidence:high · test-coverage · src/cli/_lib/stream-renderer-process.ts:139-152, 240-247
ref:21dc61e3 · diff-context

No test exercises the processEvent integration path for the activeSubagents map lifecycle -- neither entry-on-first-event nor deletion-on-terminal-event. Confirmed by grep: zero matches for activeSubagents, subagentTick, or getActiveSubagents across all test files.

Suggestion: Add integration tests that call processEvent with a mock ProcessCtx and assert: (a) first subagent event populates both maps, (b) duplicate source does not duplicate, (c) done/error events delete entries and trigger dirty+flush.


6. low · blocking:false · confidence:high · spec-compliance · src/cli/render/error-card.ts, src/cli/render/error-box.ts
ref:21dc61e3 · file-state

errorBox is still exported from the barrel without a deprecation marker. The PR migrates both production callers but leaves the old export, creating a footgun for new callers.

Suggestion: Add @deprecated Use errorCard instead to errorBox.


7. nit · blocking:false · confidence:high · api-compat · src/cli/tool-category.ts:104, 117
ref:21dc61e3 · file-state

Glyph changes (shell $ to , other to ) are intentional visual-only changes. CATEGORY_GLYPH is module-private. All affected tests updated. No behavioral change.


What was not checked

  • Citations verified against branch HEAD 21dc61e355e13c14e4e1a74361b9df195fe30e2e via pre-fetched file content and inline git show.
  • Stated intent: PR feat(render): component library foundation — SubagentStatusBar, StreamProgress, ErrorCard + live overlay integration #1321 title+body -- spec-compliance assessed.
  • Did not run the test suite (read-only review).
  • Did not verify drawBox internal ANSI handling beyond the observed split behavior.
  • StreamProgress component is not yet wired into the streaming output (PR body says "What's next"), so no integration path to verify.
  • errorBox remaining callers outside src/cli/ were not surveyed.

Decision: DO NOT MERGE -- 3 medium blocking (1 correctness, 1 api-compat, 1 test-coverage); 1 medium waived (correctness, non-data-affecting ANSI edge case); 1 low, 1 nit.

Done


🤖 Posted by agent-afk /review --post github

@griffinwork40 griffinwork40 left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

🤖 Automated review (scheduled sweep), generated by the /review tool — a maintainer will follow up.


Prior rounds

No prior automated rounds on this PR — first review.


Decision

MERGE (with follow-ups)

Severity arithmetic: 0 critical, 0 high → MERGE (with follow-ups); 2 medium advisory, 3 low advisory, 2 nit.

Per sweep policy, only critical and high findings block. Both medium findings below are advisory; a follow-up issue is recommended.


Findings

🟡 Medium (advisory, non-blocking)

M1 — subagentStatusBar fill calculation overruns by 2 columns
src/cli/render/subagent-status-bar.ts:44–57 · correctness · file-state · ref: 21dc61e

parts.join(' ') produces 2 extra chars that fixedWidth doesn't account for. fixedWidth budgets one ' ' gap (after label) but the join inserts a second gap (fill → elapsed), so the rendered line is always 2 chars wider than width. On narrow terminals this soft-wraps the overlay row and corrupts the layout.

fixedWidth = left + 2 + elapsed       ← missing: fill→next gap (+2)
actual     = left + 2 + fill + 2 + elapsed  ← always overruns by 2

Arithmetic verified: fillLen=19, actual=42, target=40, overflow=2 on a width=40 terminal with a 17-char label.

Suggestion: Add + 2 to fixedWidth for the fill→next element join. Add a column-width assertion in tests.


M2 — formatCost rounds $1.00–$1.99 to nearest dollar: up to $0.50 display error
src/cli/render/stream-progress.ts:71–75 · correctness · file-state · ref: 21dc61e

The third branch fires for any cost ≥ $1.00 and uses .toFixed(0):

return `$${(cents / 100).toFixed(0)}`;   // fires at cents >= 100

formatCost(150)$2 (actual $1.50). formatCost(249)$2 (actual $2.49). No test pins the $1.50 case. Maximum user-visible error: $0.50.

Suggestion: Use .toFixed(2) consistently for the ≥$1 range, or only suppress .00 for exact dollars. Add tests for costCents: 150 and costCents: 250.


🔵 Low (advisory, non-blocking)

L1 — subagentStatusBar label not passed through sanitizeLabel
src/cli/render/subagent-status-bar.ts:22–26 · security (defense-in-depth) · file-state · ref: 21dc61e

spec.label is passed directly to palette.tool() without sanitizeLabel. The two production call paths (subagent-executor.ts via stripEscapeSequences and compose-executor.ts via /^[A-Za-z0-9_-]+$/ schema validation) sanitize agentType before it arrives here, so no active exploit path exists at this ref. However, the drawBox JSDoc explicitly says inputs must be pre-sanitized by callers, and subagentStatusBar is a new public surface without that contract stated. A future call path skipping upstream sanitization would be silently vulnerable.

Suggestion: Apply sanitizeLabel to spec.label and spec.phase inside subagentStatusBar, matching the defense posture of tool-lane-format-sanitize.ts.

L2 — Full stack trace as hint in emitErrorBox (pre-existing, carried forward)
src/cli/_lib/stream-renderer-orchestrator-emit.ts:386–387 · correctness · diff-context · ref: 21dc61e

The original errorBox(err.message, err.stack) also showed the full stack. The migration makes it slightly more prominent (inside the card rather than as a secondary detail). Pre-existing behavior; non-blocking for this PR.

L3 — 'subagent-status' slot not covered by registerOverlaySlots integration tests
src/cli/_lib/stream-renderer-lifecycle.test.ts · test-coverage · absence confirmed · ref: 21dc61e

The six-slot variant isn't tested at integration level: absent getActiveSubagents → renders '', populated map → renders stack. Unit tests for subagentStatusStack exist but don't cover the overlay slot registration path.


⚪ Nit (advisory)

N1 — errorBox still exported without deprecation JSDoc
Both error-box.js and error-card.js are re-exported from render/index.ts. The two production callers are fully migrated, but the old API has no @deprecated tag. Add a JSDoc note pointing to errorCard.

N2 — Stale test descriptions: 'renders error events as errorBox'
src/cli/_lib/stream-renderer.test.ts:140 still says errorBox but exercises errorCard under the hood. Update description.


Withdrawn / not applicable

  • StreamProgress unwired — PR body explicitly says "What's next (not in this PR): Wire StreamProgress into the streaming output." Intentional scope exclusion, not a spec-compliance failure.
  • glyph reuse (picker cursor vs. shell glyph) — distinct render contexts, no behavioral conflict. Noted as nit only.

What was not checked

  • Citations verified inline against branch HEAD 21dc61e355e13c14e4e1a74361b9df195fe30e2e.
  • Stated intent: PR #1321 title + body — spec-compliance assessed.
  • Did not run the test suite locally; CI shows all green (326 tests passing).
  • Did not review the Telegram or daemon surfaces.
  • StreamProgress intentionally out of scope per PR body.

@griffinwork40
griffinwork40 merged commit c5f8f88 into main Aug 28, 2026
10 checks passed
@griffinwork40
griffinwork40 deleted the afk/component-library branch August 28, 2026 00:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant