feat(tui): highlight past user inputs with the original userMessageBackground band#691
Merged
Merged
Conversation
…ckground band Past user prompts rendered as bare '❯ text' — visually identical in weight to assistant prose, so earlier inputs were hard to find when scrolling a multi-turn transcript. The original Claude Code draws every past user input (and slash echo) on a full-row background band; port that: - theme.ts: new userMessageBackground token — dark rgb(55,55,55) / light rgb(240,240,240), the exact original utils/theme.ts values; skinnable via user_message_bg with a DEFAULT_THEME fallback. - messageLine.tsx: transcriptRowBand(msg, t) paints the inner row Box for user rows and slash echoes (UserPromptMessage.tsx:76 / UserCommandMessage.tsx:62 parity). Slash echoes borrow the user pointer and text color instead of the muted system dot, and the user glyph drops its non-original bold — the band, not bold text, carries the emphasis (HighlightedThinkingText renders the pointer un-bolded). - appLayout/useMainApp/virtualHeights: the '───' inter-turn dash above non-first user rows becomes a monochrome-only fallback. It existed as a crutch for exactly this findability problem; with color available the band replaces it. On NO_COLOR / FORCE_COLOR=0 / TERM=dumb terminals the band emits nothing (chalk level 0), so the textual separator returns — gated by domain/blockLayout.ts::showsInterTurnSeparator fed from config/env.ts::TRANSCRIPT_COLOR (process.stdout.hasColors), with the render gate and the height estimator sharing the same predicate. [Codex adversarial-review catch: the first cut removed the separator unconditionally, leaving no-color terminals with no turn boundary.] - tests: pin band routing (transcriptRowBand), slash pointer swap, theme values incl. skin fallback/override, separator gate matrix, band-mode same-height + monochrome +2 heights; refresh the stale compound-prompt expectation (glyph is the fixed '❯' since the re-theme; the compound brand prompt only widens the gutter). - theme.ts selectionBg: ported the original selection blues — dark rgb(38,79,120) / light rgb(180,213,255) (utils/theme.ts). The previous dark #373737 was the exact band color, so selecting text on a past user row painted band-on-band and vanished. [Critic catch #2.] - diffView.ts structuredDiffSupported(): gate the raw-ANSI ColorDiff path on TRANSCRIPT_COLOR as well as NO_COLOR. ColorDiff builds SGRs itself (not through chalk), so a FORCE_COLOR=0 / TERM=dumb session previously kept colored diff blocks inside an otherwise monochrome transcript; it now falls back to the chalk-suppressed markdown ```diff path on the same signals as transcript chrome. [Codex verification-pass catch.] Verified with the pyte harness (fake NDJSON agent-server): banded rows carry bg 373737 across the row for single-line, multi-line (both lines + indent), and slash echoes; assistant/system rows and the composer stay bandless; with NO_COLOR=1 the '───' separator renders above non-first user turns. ui-tui vitest: 8 failed vs 9 on main baseline (one stale test fixed, no new failures); tsc + eslint clean. - lib/forceTruecolor.ts: the bundled chalk predates NO_COLOR support (it emitted full color regardless — verified level 2 on a PTY with NO_COLOR=1), so the pre-chalk bootstrap now translates NO_COLOR into FORCE_COLOR=0, the channel chalk does honor, unless the user set FORCE_COLOR explicitly. This keeps TRANSCRIPT_COLOR (stdout.hasColors, which honors NO_COLOR) agreeing with what the renderer emits — critic caught the divergence: NO_COLOR previously showed the band AND the fallback separator. diffView already gated on NO_COLOR, so the app is now consistent. [Critic catch: hasColors vs chalk signal divergence under NO_COLOR.] Known pre-existing (NOT introduced here, repro'd on the unmodified build): the inline growth repaint can leave stale right-edge cells from shifted footer/rule rows (e.g. a stray '─' at 100x30 after 3 turns on main). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
After a few turns, previous user inputs are hard to find in the transcript — they rendered as bare
❯ text, visually identical in weight to assistant prose. The original Claude Code draws every past user input on a full-row background band, which is what makes prompts scannable when scrolling (user-supplied reference screenshots).Change (faithful port from the original source)
theme.ts— newuserMessageBackgroundtoken: darkrgb(55,55,55)/ lightrgb(240,240,240), the exact originalutils/theme.tsvalues. Skinnable viauser_message_bg,DEFAULT_THEMEfallback. Also ports the original selection blues (darkrgb(38,79,120)/ lightrgb(180,213,255)): the previous darkselectionBg#373737was byte-identical to the new band, so selecting text on a past user row would have been invisible.messageLine.tsx—transcriptRowBand(msg, t)paints the inner row Box for user rows and slash echoes (UserPromptMessage.tsx:76/UserCommandMessage.tsx:62parity). Multi-line inputs band every row as one solid block (continuation indent and empty lines included — verified per-cell). Slash echoes borrow the user pointer +textcolor (previously muted under the system·dot). The user gutter glyph drops its non-originalbold— the band, not bold text, carries the emphasis.appLayout.tsx/useMainApp.ts/lib/virtualHeights.ts/domain/blockLayout.ts— the───inter-turn dash becomes a monochrome-only fallback: it renders (with its 2-row height accounting) only when color is disabled, via the sharedshowsInterTurnSeparator(...)predicate fed fromTRANSCRIPT_COLOR(process.stdout.hasColors). With color available the band replaces it — the original has no dash. Render gate and height estimator share one predicate, so virtualization stays in lockstep.lib/forceTruecolor.ts— the bundled chalk predatesNO_COLOR(verified: level 2–3 on a PTY withNO_COLOR=1), so the pre-chalk bootstrap now translatesNO_COLOR→FORCE_COLOR=0(the channel chalk honors) unless the user setFORCE_COLORexplicitly. The app is now genuinely monochrome underNO_COLOR, and the fallback signal agrees with what the renderer emits.diffView.tsx—structuredDiffSupported()now gates the raw-ANSI ColorDiff path onTRANSCRIPT_COLORtoo (ColorDiff builds SGRs without chalk, soFORCE_COLOR=0/TERM=dumbsessions previously kept colored diffs inside an otherwise monochrome transcript; they now fall back to the chalk-suppressed markdown path).NO_COLOR→FORCE_COLOR=0translation + explicit-FORCE_COLOR-wins, structured-diff no-color gating; refreshed one stale pre-existing expectation.Adversarial review loop (all catches fixed in this PR)
/codex:adversarial-reviewAdd initial project source, docs, tests, and config #1 — removing the separator unconditionally left no-color terminals with no turn boundary → restored as the monochrome fallback.TRANSCRIPT_COLORdiverged from the renderer's chalk underNO_COLOR(band + dash double emphasis) → root-caused to chalk predatingNO_COLOR; fixed via the bootstrap translation; verified on the full env matrix incl.NO_COLOR+COLORTERM=truecolor.selectionBg== band → original selection blues ported + regression test.FORCE_COLOR=0/TERM=dumb→structuredDiffSupported()aligned to the same signals + tests.Final verdicts: Critic APPROVE; Codex separator no-ship lifted, its remaining recommendation implemented verbatim.
Evidence
📸 Visual captures (before / after / multiline+slash / NO_COLOR): https://claude.ai/code/artifact/09bee85d-b365-4258-a84d-13a9f3bb5378
pyte PTY harness (fake NDJSON agent-server, per-cell bg audit):
default×100373737across the row·system style❯+textcolor on band───above turn 2+NO_COLOR(±COLORTERM=truecolor)───fallbacktsc --noEmitclean; eslint clean on touched files.!input band (bashMessageBackgroundColor), memory#input highlight, queued-prompt band reuse.🤖 Generated with Claude Code