Skip to content

feat(tui): highlight past user inputs with the original userMessageBackground band#691

Merged
ericleepi314 merged 1 commit into
mainfrom
worktree-user-input-highlight
Jul 10, 2026
Merged

feat(tui): highlight past user inputs with the original userMessageBackground band#691
ericleepi314 merged 1 commit into
mainfrom
worktree-user-input-highlight

Conversation

@ericleepi314

Copy link
Copy Markdown
Collaborator

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 — 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, DEFAULT_THEME fallback. Also ports the original selection blues (dark rgb(38,79,120) / light rgb(180,213,255)): the previous dark selectionBg #373737 was byte-identical to the new band, so selecting text on a past user row would have been invisible.
  • messageLine.tsxtranscriptRowBand(msg, t) paints the inner row Box for user rows and slash echoes (UserPromptMessage.tsx:76 / UserCommandMessage.tsx:62 parity). 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 + text color (previously muted under the system · dot). The user gutter glyph drops its non-original bold — 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 shared showsInterTurnSeparator(...) predicate fed from TRANSCRIPT_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 predates NO_COLOR (verified: level 2–3 on a PTY with NO_COLOR=1), so the pre-chalk bootstrap now translates NO_COLORFORCE_COLOR=0 (the channel chalk honors) unless the user set FORCE_COLOR explicitly. The app is now genuinely monochrome under NO_COLOR, and the fallback signal agrees with what the renderer emits.
  • diffView.tsxstructuredDiffSupported() now gates the raw-ANSI ColorDiff path on TRANSCRIPT_COLOR too (ColorDiff builds SGRs without chalk, so FORCE_COLOR=0/TERM=dumb sessions previously kept colored diffs inside an otherwise monochrome transcript; they now fall back to the chalk-suppressed markdown path).
  • Tests — band routing, slash pointer swap, theme value pins + skin fallback/override, selection≠band distinctness guard, separator gate matrix, band-mode same-height + monochrome +2 heights, NO_COLORFORCE_COLOR=0 translation + explicit-FORCE_COLOR-wins, structured-diff no-color gating; refreshed one stale pre-existing expectation.

Adversarial review loop (all catches fixed in this PR)

  1. Codex /codex:adversarial-review Add 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.
  2. CriticTRANSCRIPT_COLOR diverged from the renderer's chalk under NO_COLOR (band + dash double emphasis) → root-caused to chalk predating NO_COLOR; fixed via the bootstrap translation; verified on the full env matrix incl. NO_COLOR+COLORTERM=truecolor.
  3. Critic — dark selectionBg == band → original selection blues ported + regression test.
  4. Codex verification pass — structured diffs still colored under FORCE_COLOR=0/TERM=dumbstructuredDiffSupported() 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):

surface before after
past user row bg default×100 bg 373737 across the row
multi-line input (incl. empty line) no band every row banded, one solid block
slash echo muted · system style + text color on band
assistant/system/composer no band no band (unchanged)
color mode separators ─── above turn 2+ none (band replaces)
NO_COLORCOLORTERM=truecolor) full color anyway (chalk bug) genuinely monochrome + ─── fallback
  • ui-tui vitest: 8 failed = main baseline exactly (fixed one stale test, zero new failures). tsc --noEmit clean; eslint clean on touched files.
  • Known pre-existing artifact (reproduced on the unmodified build): inline growth repaint can leave stale right-edge cells from shifted footer/rule rows at some geometries. Renderer follow-up, out of scope.
  • Future parity candidates: bash ! input band (bashMessageBackgroundColor), memory # input highlight, queued-prompt band reuse.

🤖 Generated with Claude Code

…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>
@ericleepi314 ericleepi314 merged commit dc35e33 into main Jul 10, 2026
2 checks passed
@ericleepi314 ericleepi314 deleted the worktree-user-input-highlight branch July 10, 2026 07:26
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