Conversation
…pete#3746) Claude renders `/usage` as a redrawing TUI. Its renderer diffs each frame against the previous one and jumps over unchanged cells with `ESC[<col>G`, so those characters are never retransmitted. Deleting the escape sequences and concatenating what is left therefore produces text with holes in it. In a real capture the Fable row arrived as `51%<ESC>[59Gus<ESC>[62Gd`: the `e` of "used" sat in a column where the previous frame had already painted the `e` of "does". The scraped text read `51%usd`, `percentFromLine` could not tell "used" from "left", returned nil, and `extractScopedWeeklyUsages` discarded the whole Fable panel. Whether a hole lands inside a keyword is down to which text the panel replaced, which is why the row appeared and disappeared at random. Word gaps were collapsed the same way, which is why reset descriptions read `ResetsSep23at3pm`. Replay the capture onto an in-memory screen and parse what the user would actually see. The screen is sized from `ClaudeCLISession.ptyRows/ptyColumns`, which is also what `openpty` is given, so the renderer and the PTY cannot drift apart: a wider PTY would clamp absolute column addressing and wrap in the wrong place, corrupting the text the same way again. `render` therefore takes the geometry explicitly rather than defaulting to a guess. An unexpected capture shape still degrades to the old ANSI strip rather than to no data. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
🦞👀 Pull request received. I will update this pull request when review starts. ClawSweeper review completeClawSweeper finished reviewing this revision. The review result is being finalized. |
|
Codex review: needs maintainer review before merge. Reviewed September 21, 2026, 5:40 AM ET / 09:40 UTC (Revision 2). ClawSweeper reviewWhat this changesReconstructs Claude’s terminal screen before parsing usage and account details, preserving characters and spaces omitted by differential redraws. Merge readiness✅ Ready for maintainer review This remains a useful fix absent from current main and the latest release. The previous display-width finding is addressed, the supplied real-behavior proof is sufficient, and no remaining blocking defect was identified. Priority: P2 Review scores
Verification
How this fits togetherCodexBar captures Claude CLI usage and status panels through a pseudo-terminal. The reconstructed text feeds existing quota and identity parsers, which supply the CLI output and menu-bar display. flowchart LR
A[Claude CLI panels] --> B[Pseudo-terminal capture]
B --> C[Replay screen updates]
C --> D{Percentages preserved?}
D -->|Yes| E[Usage and identity parsers]
D -->|No| F[Legacy ANSI stripping]
F --> E
E --> G[CLI output and menu]
Before mergeNone. Agent review detailsSecurityNone. Review metrics
Technical reviewBest possible solution: Reconstruct Claude’s captured terminal text at the actual PTY geometry, then retain the existing quota and identity parsing rules. Do we have a high-confidence way to reproduce the issue? Yes, from source: the committed real capture omits a previously painted letter in 'used', and current main strips the cursor instructions before requiring that keyword. No reviewer-side execution was performed. Is this the best way to solve the issue? Yes: replaying the terminal updates addresses the missing characters at their source, without relaxing percentage interpretation or adding competing provider behavior. AGENTS.md: found and applied where relevant. Codex review notes: model internal, reasoning medium; reviewed against d8d0f3394989. LabelsLabel justifications:
EvidenceWhat I checked:
Likely related people:
Rating scale
Overall follows the weaker of proof and patch quality. Workflow
HistoryReview history (1 earlier review cycle)
|
…erer Ink positions its `ESC[<col>G` jumps with `string-width`, so CJK, fullwidth and presentation emoji take two columns while `█` and other East Asian Ambiguous characters take one. The renderer stored every Swift `Character` in a single cell, which put every jump after a wide character one column too far and could overwrite the wrong cells on the next repaint. `/status` identity text (org and account names) is the most exposed path. Wide characters now occupy a head cell plus a continuation cell, overwriting or erasing either half blanks the other, and a wide character that does not fit at the end of a row wraps whole. Combining marks that Swift clusters onto an escape sequence's final byte are peeled off and rejoined to the glyph before the cursor. Remaining C0 controls and DEL are dropped instead of rendered. Also fixes the doc comment that had drifted onto `ptyRows`, removes a duplicated line in docs/claude.md, and adds a fallback test for `cleanCapture`. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
@clawsweeper re-review Addressed the P2 finding in a00e78d: the renderer now counts cells by display width the way Ink's |
|
🦞🧹 I asked ClawSweeper to review this item again. |
Fixes #3746.
Summary
Claude's
/usagepanel is a redrawing TUI. Its renderer diffs each frame against the previous one and jumps over cells whose content did not change, usingESC[<col>G. Those characters are never retransmitted.ClaudeStatusProbefed its PTY capture throughTextParsing.stripANSICodes, which deletes the escape sequences and concatenates the leftovers — so every skipped cell becomes a hole in the text.When a hole lands inside the word
used,percentFromLinecannot tell "used" from "left", returnsnil, andextractScopedWeeklyUsagesdrops the entire panel at itsguard let percentLeft. The Fable row vanishes; the main rows survive only because their holes happened to land elsewhere.Which data path is affected
Only the CLI PTY source. On my machine OAuth and Web are both unavailable, so
Autolands on the CLI scrape:Claude OAuth credentials not found.(Keychain access disabled, no~/.claude/.credentials.json)No available fetch strategy for claude.(nosessionKey)dataConfidence: percentOnly— Fable droppedBefore the fix: 5 consecutive
codexbar usage --provider claude --source cliruns, 0 of 5 returnedclaude-weekly-scoped-fable.Evidence
Captured with a temporary dump of the raw PTY bytes. The capture is committed as
Tests/CodexBarTests/Fixtures/Providers/Claude/usage-pty-differential-redraw.ansi.1. The bytes for the Fable row
usis written at columns 59-60, then the cursor jumps to column 62 to writed. Column 61 is never sent.2. What column 61 held, and why it was skipped
The Fable panel replaced the "Refreshing…" layout, so this screen row previously held
Approximate, based on local sessions on this machine — does not include other devices or claude.ai. Aligning both frames by terminal column:doesandusedhappen to carry anein the same column, so the diff marked that cell clean. Columns 58 and 63 were spaces in both frames and were skipped for the same reason.The same rule explains every other jump in the frame. The bar line's written spaces cover exactly the previous frame's words (
sessions31-38,on40-41,this43-46,machine48-54) and skip exactly the columns that were already blank (30, 39, 42, 47). Elsewhere in the same frame:every jump skips one already-correct space.
3. What the parser saw vs. what was on screen
Instrumented parser output, before the fix:
The label and the reset text were found. Only the percentage was lost.
Replaying the same bytes onto a terminal screen reconstructs the panel exactly:
Fix
Sources/CodexBarCore/TerminalScreenRenderer.swiftreplays a PTY stream onto an in-memory screen and returns the visible text, keeping scrolled-off lines as history. It implements the VT100/xterm subset CLI panels use — cursor motion, erase in line/display, insert/delete lines and characters, scroll regions, autowrap — and skips SGR and other presentation-only sequences. Text with noESCin it is returned untouched, so plain piped output and existing fixtures are unaffected.ClaudeStatusProbenow runs its/usageand/statuscaptures through it, and degrades to the old ANSI strip rather than to no data if a capture has an unexpected shape.rendertakes its geometry explicitly and has no defaults: the screen is sized fromClaudeCLISession.ptyRows/ptyColumns, which is also whatopenptyis given. A renderer narrower than the PTY would clamp absolute column addressing and wrap in the wrong place — corrupting the text in exactly the way this PR fixes — so the two must not be able to drift apart.A second, quieter bug goes away with it: the spaces between rendered segments were also transmitted as cursor jumps, which is why reset descriptions were reaching the menu as
ResetsSep23at3pm. They now readResets Sep 23 at 3pm (Europe/Stockholm).Commands run
swift test --filter TerminalScreenRendererTests— 8 new unit tests, including the differential-redraw reconstruction.swift test --filter ClaudeCLIPTYRedrawRegressionTests— 3 new tests parsing the committed real capture; one asserts that the plain strip still reads51%usd, documenting why the screen replay is needed.swift test --filter "TerminalScreenRendererTests|ClaudeCLIPTYRedrawRegressionTests|ClaudeCLIScopedWeeklyUsageTests|StatusProbeTests|ClaudeCLISessionTests|TextParsingTests"— 311 tests, all passing.swift test— no new failures. (The suite has pre-existing timing-sensitive flakes under parallel load; the threeClaudeOAuthPromptCoalescingTestsfailures reproduce onmainunchanged.)./Scripts/lint.sh lint— 0 violations../Scripts/compile_and_run.sh— packaged and relaunchedCodexBar.appfor bundle-level validation.Verification
Live, after the fix:
codexbar usage --provider claude --source clireturnedclaude-weekly-scoped-fableon 4 of 4 consecutive runs (0 of 5 before). The packaged bundle's own CLI agrees:Note for other providers
GeminiStatusProbeandCodexStatusProbestrip ANSI from their own PTY captures the same way. They are not touched here, but they are exposed to the same class of corruption if those CLIs repaint differentially.