fix(tui): Tighten message and tool spacing#4595
Conversation
📋 Review SummaryThis PR implements focused TUI spacing and density improvements by removing unnecessary blank rows between UI elements. The changes are well-scoped to three core components ( 🔍 General Feedback
🎯 Specific Feedback🟢 Medium
🔵 Low
✅ Highlights
|
Code Coverage Summary
CLI Package - Full Text ReportCore Package - Full Text ReportFor detailed HTML reports, please see the 'coverage-reports-22.x-ubuntu-latest' artifact from the main CI run. |
|
Added the 34s local visual verification GIF generated with Artifacts:
|
|
|
||
| const output = lastFrame() ?? ''; | ||
| expect(output.startsWith('\n')).toBe(false); | ||
| expect(output).toContain('Read txt files'); |
There was a problem hiding this comment.
[Suggestion] The two new tests confirm gemini and tool_use_summary types produce no leading spacer — but the inverse contract is untested. No test verifies that user / user_shell types still produce marginTop=1 (the leading spacer row). This is the highest-risk regression path: someone adding 'user' to the zero-margin switch cases in getHistoryItemMarginTop would silently remove the turn separator with no test failure.
| expect(output).toContain('Read txt files'); | |
| expect(output.startsWith('\n')).toBe(false); | |
| expect(output).toContain('Read txt files'); | |
| }); | |
| it('renders user prompts with a leading spacer row', () => { | |
| const item: HistoryItem = { | |
| id: 1, | |
| type: 'user', | |
| text: 'Hello', | |
| }; | |
| const { lastFrame } = renderWithProviders( | |
| <HistoryItemDisplay item={item} terminalWidth={100} isPending={false} />, | |
| ); | |
| const output = lastFrame() ?? ''; | |
| expect(output.startsWith('\n')).toBe(true); | |
| }); |
— qwen3.7-max via Qwen Code /review
There was a problem hiding this comment.
Not applicable — user type is intentionally marginTop=0 in this PR (the half-line band provides visual separation). Adding a test asserting marginTop=1 would contradict the design.
- Set marginTop=0 for user and gemini message types to eliminate blank lines between Q&A turns and tool calls - Add half-block (▄) separator line above Composer input area, replacing the full blank row with a subtle blended color line - Add color-utils helpers: interpolateColor, supportsTrueColor - Add HalfLinePaddedBox component (reusable, currently used by Composer only) - Add design doc for TUI spacing density PR2 Generated with AI Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
| <Box flexDirection="column" marginTop={1}> | ||
| <Box flexDirection="column" marginTop={separatorColor ? 0 : 1}> | ||
| {separatorColor && ( | ||
| <Text color={separatorColor}> |
There was a problem hiding this comment.
[Critical] The decorative ▄ separator renders unconditionally when truecolor is supported, without checking isScreenReaderEnabled. Screen reader users will hear a row of meaningless half-block characters announced as noise.
The hook is already imported at line 7 and used elsewhere in this file (e.g., <Footer /> gate). HalfLinePaddedBox in the same PR also correctly bails out for screen readers.
| <Text color={separatorColor}> | |
| {separatorColor && !isScreenReaderEnabled && ( |
— qwen3.7-max via Qwen Code /review
There was a problem hiding this comment.
Resolved — the Composer separator line has been removed entirely in later commits.
| * themes. The content area has no backgroundColor — it inherits the terminal's | ||
| * real background. | ||
| */ | ||
| export const HalfLinePaddedBox: React.FC<HalfLinePaddedBoxProps> = (props) => { |
There was a problem hiding this comment.
[Suggestion] HalfLinePaddedBox is exported but never imported anywhere in the codebase — it's dead code in this PR. The 89-line component adds maintenance burden without serving any current functionality.
Either remove it from this PR (re-introduce alongside its first consumer), or wire it into Composer.tsx to replace the inline separator logic that duplicates the same pattern (supportsTrueColor() + interpolateColor() + '▄'.repeat()).
— qwen3.7-max via Qwen Code /review
There was a problem hiding this comment.
Fixed in 6bf2aa4 — HalfLinePaddedBox.tsx has been deleted.
| color2: string, | ||
| factor: number, | ||
| ): string { | ||
| if (factor <= 0) { |
There was a problem hiding this comment.
[Suggestion] Two new exported functions (91 lines combined) have no test coverage. The existing color-utils.test.ts doesn't reference either function.
interpolateColor has untested branches: boundary factors (0, 1, negative, >1), mid-blend correctness, unparseable input returning '', 3-digit hex expansion, and Ink name lookup. supportsTrueColor has untested env-var paths and the getColorDepth() fallback.
Being pure functions, these are trivially testable with vi.stubEnv and standard assertions.
— qwen3.7-max via Qwen Code /review
There was a problem hiding this comment.
Fixed in 6bf2aa4 — added 10 unit tests covering interpolateColor (boundary factors, blend correctness, 3-digit hex, Ink names, unparseable input), subtleBandColor (dark/light detection, invalid input), and supportsTrueColor.
Replace the interpolated purple band color with terminal-native dimColor rendering for ▄/▀ half-line characters. This avoids theme-dependent color mismatches while preserving the visual spacing effect. Generated with AI Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
| } | ||
|
|
||
| return ( | ||
| <Box flexDirection="column" width={width}> |
There was a problem hiding this comment.
[Critical] New a11y regression: the decorative ▄/▀ half-line separators render unconditionally whenever width is defined (which is always, via HistoryItemDisplay passing contentWidth). There is no useIsScreenReaderEnabled() guard, so screen-reader users will hear two rows of repeated block characters announced around every user message.
The project already has the right pattern in packages/cli/src/ui/components/shared/HalfLinePaddedBox.tsx:42-47 — it bails out with if (... || isScreenReaderEnabled) return <>{props.children}</>; and also handles the truecolor fallback and background-blended color. This component was introduced earlier in the same PR but is currently unused; reusing it here both fixes the regression and removes the dead-code concern from R2.
| <Box flexDirection="column" width={width}> | |
| export const UserMessage: React.FC<UserMessageProps> = ({ text, width }) => { | |
| const content = ( | |
| <PrefixedTextMessage | |
| text={text} | |
| prefix=">" | |
| prefixColor={theme.text.accent} | |
| textColor={theme.text.accent} | |
| ariaLabel={SCREEN_READER_USER_PREFIX} | |
| alignSelf="flex-start" | |
| /> | |
| ); | |
| if (width === undefined) { | |
| return content; | |
| } | |
| return ( | |
| <HalfLinePaddedBox bandColor={theme.text.accent} bandOpacity={0.15} width={width}> | |
| {content} | |
| </HalfLinePaddedBox> | |
| ); | |
| }; |
— qwen3.7-max via Qwen Code /review
There was a problem hiding this comment.
Fixed in 6bf2aa4 — UserMessage now checks useIsScreenReaderEnabled() and falls back to plain PrefixedTextMessage for screen reader users.
| ariaLabel={SCREEN_READER_USER_PREFIX} | ||
| alignSelf="flex-start" | ||
| /> | ||
| ); |
There was a problem hiding this comment.
[Suggestion] '▄'.repeat(width) and '▀'.repeat(width) throw RangeError: Invalid count value when width is negative. width here is contentWidth = terminalWidth - 4 (HistoryItemDisplay.tsx:140), so during extremely narrow terminal resize (or transient init states before size detection), this becomes a hard unhandled exception. Previous code only fed contentWidth into ink <Box width> which clamps gracefully; this is the first path that hands it to String.prototype.repeat.
| ); | |
| export const UserMessage: React.FC<UserMessageProps> = ({ text, width }) => { | |
| const content = ( | |
| <PrefixedTextMessage | |
| text={text} | |
| prefix=">" | |
| prefixColor={theme.text.accent} | |
| textColor={theme.text.accent} | |
| ariaLabel={SCREEN_READER_USER_PREFIX} | |
| alignSelf="flex-start" | |
| /> | |
| ); | |
| if (width === undefined) { | |
| return content; | |
| } | |
| const safeWidth = Math.max(0, width); | |
| return ( | |
| <Box flexDirection="column" width={safeWidth}> | |
| <Text dimColor>{'▄'.repeat(safeWidth)}</Text> | |
| {content} | |
| <Text dimColor>{'▀'.repeat(safeWidth)}</Text> | |
| </Box> | |
| ); | |
| }; |
If the Critical above is addressed by switching to HalfLinePaddedBox, this guard belongs inside that component (it also calls repeat(width)) rather than here.
— qwen3.7-max via Qwen Code /review
There was a problem hiding this comment.
Fixed in 6bf2aa4 — width <= 0 guard added to useBand condition, prevents repeat() from receiving negative values.
| ); | ||
|
|
||
| const output = lastFrame() ?? ''; | ||
| expect(output.startsWith('\n')).toBe(false); |
There was a problem hiding this comment.
[Suggestion] The two new tests cover the no-leading-spacer contract for gemini and tool_use_summary, but the incremental change in this PR — UserMessage now rendering ▄/▀ half-lines when width is provided — has no dedicated test coverage. The existing user-type tests (lines 37-59) only assert text content via toContain('Hello') and would pass even if the half-lines rendered incorrectly or not at all.
Worth adding:
width=positive→ half-lines rendered at correct count (expect(lastFrame()).toContain('▄'.repeat(76))withterminalWidth=80).width=undefined→ no half-lines rendered (negative assertion against▄/▀).- Screen-reader mode (
useIsScreenReaderEnabledmockedtrue) → half-lines suppressed. This test would currently fail, driving the fix for the Critical above. - Narrow terminal (
terminalWidth=3,contentWidth=-1) → no crash, text still rendered.
— qwen3.7-max via Qwen Code /review
There was a problem hiding this comment.
Partially addressed — unit tests added for interpolateColor/subtleBandColor/supportsTrueColor. Full integration tests for half-line rendering require mocking Ink's useIsScreenReaderEnabled and terminal color depth, tracked for follow-up.
Dismissing — comments posted with line:null (Create Review API anchor failure). Re-posting individually via PR Comments API with side=RIGHT.
| } | ||
|
|
||
| return ( | ||
| <Box flexDirection="column" width={width}> |
There was a problem hiding this comment.
[Critical] New a11y regression: the decorative ▄/▀ half-line separators render unconditionally whenever width is defined (which is always, via HistoryItemDisplay passing contentWidth). There is no useIsScreenReaderEnabled() guard, so screen-reader users will hear two rows of repeated block characters announced around every user message.
The project already has the right pattern in packages/cli/src/ui/components/shared/HalfLinePaddedBox.tsx:42-47 — it bails out with if (... || isScreenReaderEnabled) return <>{props.children}</>; and also handles the truecolor fallback and background-blended color. This component was introduced earlier in the same PR but is currently unused; reusing it here both fixes the regression and removes the dead-code concern from R2.
| <Box flexDirection="column" width={width}> | |
| export const UserMessage: React.FC<UserMessageProps> = ({ text, width }) => { | |
| const content = ( | |
| <PrefixedTextMessage | |
| text={text} | |
| prefix=">" | |
| prefixColor={theme.text.accent} | |
| textColor={theme.text.accent} | |
| ariaLabel={SCREEN_READER_USER_PREFIX} | |
| alignSelf="flex-start" | |
| /> | |
| ); | |
| if (width === undefined) { | |
| return content; | |
| } | |
| return ( | |
| <HalfLinePaddedBox bandColor={theme.text.accent} bandOpacity={0.15} width={width}> | |
| {content} | |
| </HalfLinePaddedBox> | |
| ); | |
| }; |
— qwen3.7-max via Qwen Code /review
| ariaLabel={SCREEN_READER_USER_PREFIX} | ||
| alignSelf="flex-start" | ||
| /> | ||
| ); |
There was a problem hiding this comment.
[Suggestion] '▄'.repeat(width) and '▀'.repeat(width) throw RangeError: Invalid count value when width is negative. width here is contentWidth = terminalWidth - 4 (HistoryItemDisplay.tsx:140), so during extremely narrow terminal resize (or transient init states before size detection), this becomes a hard unhandled exception. Previous code only fed contentWidth into ink <Box width> which clamps gracefully; this is the first path that hands it to String.prototype.repeat.
| ); | |
| export const UserMessage: React.FC<UserMessageProps> = ({ text, width }) => { | |
| const content = ( | |
| <PrefixedTextMessage | |
| text={text} | |
| prefix=">" | |
| prefixColor={theme.text.accent} | |
| textColor={theme.text.accent} | |
| ariaLabel={SCREEN_READER_USER_PREFIX} | |
| alignSelf="flex-start" | |
| /> | |
| ); | |
| if (width === undefined) { | |
| return content; | |
| } | |
| const safeWidth = Math.max(0, width); | |
| return ( | |
| <Box flexDirection="column" width={safeWidth}> | |
| <Text dimColor>{'▄'.repeat(safeWidth)}</Text> | |
| {content} | |
| <Text dimColor>{'▀'.repeat(safeWidth)}</Text> | |
| </Box> | |
| ); | |
| }; |
If the Critical above is addressed by switching to HalfLinePaddedBox, this guard belongs inside that component (it also calls repeat(width)) rather than here.
— qwen3.7-max via Qwen Code /review
| ); | ||
|
|
||
| const output = lastFrame() ?? ''; | ||
| expect(output.startsWith('\n')).toBe(false); |
There was a problem hiding this comment.
[Suggestion] The two new tests cover the no-leading-spacer contract for gemini and tool_use_summary, but the incremental change in this PR — UserMessage now rendering ▄/▀ half-lines when width is provided — has no dedicated test coverage. The existing user-type tests (lines 37-59) only assert text content via toContain('Hello') and would pass even if the half-lines rendered incorrectly or not at all.
Worth adding:
width=positive→ half-lines rendered at correct count (expect(lastFrame()).toContain('▄'.repeat(76))withterminalWidth=80).width=undefined→ no half-lines rendered (negative assertion against▄/▀).- Screen-reader mode (
useIsScreenReaderEnabledmockedtrue) → half-lines suppressed. This test would currently fail, driving the fix for the Critical above. - Narrow terminal (
terminalWidth=3,contentWidth=-1) → no crash, text still rendered.
— qwen3.7-max via Qwen Code /review
There was a problem hiding this comment.
Duplicate of above — partially addressed.
| magenta: '#ff00ff', | ||
| white: '#ffffff', | ||
| gray: '#808080', | ||
| grey: '#808080', |
There was a problem hiding this comment.
[Suggestion] INK_NAME_TO_HEX maps only the 10 basic Ink color names (black, red, green, yellow, blue, cyan, magenta, white, gray, grey) but omits all 8 bright variants (blackbright, redbright, greenbright, yellowbright, bluebright, cyanbright, magentabright, whitebright) that INK_SUPPORTED_NAMES accepts. toHex() therefore returns undefined for bright names, and interpolateColor() silently returns '' whenever either input is a bright Ink color name.
This is not currently triggered by the bundled themes (theme.text.secondary resolves to gray or a hex value), but ansi.ts actively uses bluebright for syntax colors (lines 123, 126, 129). Any future caller of interpolateColor with a bright-named color gets a silent empty-string failure. resolveColor accepts bright names (they're in INK_SUPPORTED_NAMES), so the caller has no signal that the color was rejected downstream.
| grey: '#808080', | |
| grey: '#808080', | |
| blackbright: '#555555', | |
| redbright: '#ff5555', | |
| greenbright: '#55ff55', | |
| yellowbright: '#ffff55', | |
| bluebright: '#5555ff', | |
| cyanbright: '#55ffff', | |
| magentabright: '#ff55ff', | |
| whitebright: '#ffffff', | |
| }; |
(Hex values above are standard ANSI bright colors; adjust to match the terminal's bright palette if needed.) Alternatively, derive the map from INK_SUPPORTED_NAMES to prevent future drift.
— qwen3.7-max via Qwen Code /review
There was a problem hiding this comment.
Not applicable to current usage — subtleBandColor only receives theme.background.primary which is always a hex value or basic name, never a bright variant. The function returns '' gracefully for unknown names and callers fall back. Low risk, defer to follow-up if a caller with bright names is added.
| * Blend factor (0–1) from terminal background toward bandColor. | ||
| * Lower = more subtle. Default 0.35. | ||
| */ | ||
| bandOpacity?: number; |
There was a problem hiding this comment.
[Suggestion] Doc/code mismatch: the JSDoc for bandOpacity declares "Default 0.35" but the destructuring default at line 56 is bandOpacity = 0.15 — more than a 2x difference. Future callers relying on the documented default will get a much subtler line than expected.
| bandOpacity?: number; | |
| * Blend factor (0–1) from terminal background toward bandColor. | |
| * Lower = more subtle. Default 0.15. | |
| */ | |
| bandOpacity?: number; |
— qwen3.7-max via Qwen Code /review
There was a problem hiding this comment.
Resolved — HalfLinePaddedBox.tsx has been deleted in 6bf2aa4, so the mismatched JSDoc no longer exists.
| ); | ||
|
|
||
| const composerWidth = uiState.terminalWidth - 4; | ||
| const separatorColor = supportsTrueColor() |
There was a problem hiding this comment.
[Suggestion] separatorColor is computed inline on every render, calling supportsTrueColor(), two resolveColor() lookups, and interpolateColor() (which parses hex and does arithmetic). All inputs (theme.background.primary, theme.text.secondary, env COLORTERM) are stable across renders. Composer re-renders frequently during streaming, so this is unnecessary work on the hot path.
| const separatorColor = supportsTrueColor() | |
| const composerWidth = uiState.terminalWidth - 4; | |
| const separatorColor = useMemo(() => { | |
| if (!supportsTrueColor()) return undefined; | |
| return interpolateColor( | |
| resolveColor(theme.background.primary || 'black') || 'black', | |
| resolveColor(theme.text.secondary) || theme.text.secondary, | |
| 0.15, | |
| ); | |
| }, []); |
— qwen3.7-max via Qwen Code /review
There was a problem hiding this comment.
Resolved — the Composer separator line has been removed entirely. No inline color computation remains.
| * Detects whether the terminal supports 24-bit (true) color, required for the | ||
| * blended half-line background band. | ||
| */ | ||
| export function supportsTrueColor(): boolean { |
There was a problem hiding this comment.
[Suggestion] supportsTrueColor() reads process.env['COLORTERM'] and calls process.stdout.getColorDepth() on every invocation. The terminal's color capability does not change during the process lifetime. It is called from Composer (frequent re-renders during streaming) and from every HalfLinePaddedBoxInternal render. Cache at module scope:
| export function supportsTrueColor(): boolean { | |
| let _supportsTrueColor: boolean | undefined; | |
| export function supportsTrueColor(): boolean { | |
| if (_supportsTrueColor !== undefined) return _supportsTrueColor; | |
| const colorterm = process.env['COLORTERM']; | |
| if ( | |
| colorterm === 'truecolor' || | |
| colorterm === '24bit' || | |
| colorterm === 'kmscon' | |
| ) { | |
| return (_supportsTrueColor = true); | |
| } | |
| if (process.stdout.getColorDepth && process.stdout.getColorDepth() >= 24) { | |
| return (_supportsTrueColor = true); | |
| } | |
| return (_supportsTrueColor = false); | |
| } |
— qwen3.7-max via Qwen Code /review
There was a problem hiding this comment.
Fixed in 6bf2aa4 — supportsTrueColor() now caches its result at module scope via _supportsTrueColor.
| <Box flexDirection="column" width={width}> | ||
| <Text dimColor>{'▄'.repeat(width)}</Text> | ||
| {content} | ||
| <Text dimColor>{'▀'.repeat(width)}</Text> |
There was a problem hiding this comment.
[Suggestion] UserMessage renders ▄/▀ half-block characters unconditionally when width is provided, without checking supportsTrueColor(). Both Composer (line 100) and HalfLinePaddedBox (line 72) guard with supportsTrueColor() and silently skip the separator on non-trueColor terminals. This inconsistency means UserMessage's dimColor half-blocks appear on terminals where the Composer and HalfLinePaddedBox separators are absent — a visual mismatch.
Additionally, three divergent implementations of the same half-block separator pattern now exist (Composer inline, UserMessage inline, HalfLinePaddedBox component), each with different guards and color approaches. Consider consolidating on a single approach — use HalfLinePaddedBox in both places, or extract a shared helper.
— qwen3.7-max via Qwen Code /review
There was a problem hiding this comment.
Fixed — UserMessage now checks supportsTrueColor() in the useBand guard. Composer separator has been removed, so only one rendering pattern remains (UserMessage inline).
| return 1; | ||
| } | ||
| } | ||
|
|
There was a problem hiding this comment.
[Suggestion] default: return 1 breaks TypeScript exhaustiveness checking. When a new HistoryItem type is added to the union, it silently inherits marginTop: 1 without any compiler warning. The new type might be a conversation-flow message that should have marginTop: 0 (matching the PR's density goal), but there's no compile-time enforcement.
Consider adding an exhaustiveness guard:
| default: { | |
| const _exhaustive: never = item; | |
| return 1; | |
| } |
Or invert the logic: list the types that need marginTop: 1 (currently ~16 types) and default to 0, which matches the PR's density intent and gives new types the correct default.
— qwen3.7-max via Qwen Code /review
There was a problem hiding this comment.
Intentional — marginTop=1 is a safe default for unknown/new types (about, stats, help, etc. are dialog-like items that need spacing). Inverting to default: return 0 would risk breaking layout for these types. The switch explicitly lists all density-critical types.
DragonnZhang
left a comment
There was a problem hiding this comment.
Review Summary
Independent review of the TUI spacing and density changes. The PR is well-scoped with thorough evidence (side-by-side GIF, tmux captures, row-count measurements).
Deterministic checks: tsc and eslint pass with 0 findings from this PR's changes.
Code review findings: After reviewing all 15 changed files, I found no additional issues beyond the 16 existing inline comments from @wenshao. The existing comments are comprehensive and cover the key concerns:
- Accessibility (Critical): The two screen reader regressions in
Composer.tsxandConversationMessages.tsxare legitimate. Decorative half-block characters (▄/▀) needuseIsScreenReaderEnabled()guards to avoid announcing meaningless noise to assistive technology users. These should be addressed before merge. - Negative width guard: The
RangeErrorrisk from'▄'.repeat(width)with negativecontentWidthon very narrow terminals is a real edge case worth fixing (Composer already usesMath.max(0, ...)but UserMessage does not). - Dead code:
HalfLinePaddedBoxis exported but unused in this PR. If it's preparation for PR2, consider deferring it to that PR to keep this one minimal. - JSDoc/code mismatch: The
bandOpacitydefault documented as 0.35 vs actual 0.15 should be corrected. - Test coverage: The missing inverse tests (verifying
user/user_shelltypes still getmarginTop=1), half-line rendering tests, andcolor-utilstests are reasonable asks.
The core spacing logic changes (getHistoryItemMarginTop, gap: 0, marginTop removal) are clean and well-tested via snapshot updates. The getHistoryItemMarginTop switch correctly categorizes conversation-flow types (marginTop=0) vs standalone command outputs (marginTop=1 via default).
— qwen-code via Qwen Code /review
|
@qwen-code /review |
|
Qwen Code review did not complete successfully: Qwen review timed out after 55 minutes. See workflow logs. |
DragonnZhang
left a comment
There was a problem hiding this comment.
Independent review: no novel findings beyond the 16 existing inline comments. Agreed with the Critical accessibility issues (screen reader guards needed for decorative half-block characters in Composer.tsx and ConversationMessages.tsx). Core spacing logic changes are clean, well-scoped, and covered by new tests. — Claude (Sonnet 4) via Qwen Code /review
DragonnZhang
left a comment
There was a problem hiding this comment.
Incremental Review (commits since last review)
The two new code commits (c2242b6e4, 59300704c) make positive changes:
- Composer separator now properly guards with
supportsTrueColor()andMath.max(0, ...)width clamp - UserMessage switches to
dimColorfor half-line separators, which is simpler and gracefully degrades on non-truecolor terminals usertype correctly added tomarginTop=0group to complement the new separator rendering
Deterministic checks: tsc (0 code errors) and eslint (0 findings) pass on all changed files.
Prior Critical issues (not resolved by these commits): The screen reader accessibility concerns (existing comments on ConversationMessages.tsx and Composer.tsx) remain — UserMessage still lacks useIsScreenReaderEnabled() guard, while the new HalfLinePaddedBox correctly includes one.
One new suggestion below regarding an inconsistency introduced by the incremental changes.
— qwen-code via Qwen Code /review
| <Text dimColor>{'▄'.repeat(width)}</Text> | ||
| {content} | ||
| <Text dimColor>{'▀'.repeat(width)}</Text> | ||
| </Box> |
There was a problem hiding this comment.
[Suggestion] Three different separator-rendering patterns now coexist in this PR, each with different guards and color logic:
| Component | Color approach | supportsTrueColor() guard |
useIsScreenReaderEnabled() guard |
|---|---|---|---|
UserMessage (here) |
dimColor |
No | No |
Composer |
interpolateColor() with explicit blend |
Yes | No |
HalfLinePaddedBox |
interpolateColor() with explicit blend |
Yes | Yes |
dimColor is simpler and gracefully degrades on non-truecolor terminals, but produces a different visual result than the explicit color blending used in Composer and HalfLinePaddedBox. This means the user-message separator and the composer separator will look different in the same session.
Consider consolidating: either adopt dimColor in Composer too (for simplicity), or use HalfLinePaddedBox / interpolateColor here (for visual consistency). The current mix creates two code paths for the same visual element.
— qwen-code via Qwen Code /review
There was a problem hiding this comment.
Resolved — Composer separator removed, HalfLinePaddedBox deleted. Only one pattern remains: UserMessage inline rendering with subtleBandColor, guarded by supportsTrueColor(), useIsScreenReaderEnabled(), theme.background.primary, and width > 0.
- User message: three-layer seamless band using subtleBandColor (6% brightness shift, no hue change) with ▄/content-bg/▀ - gemini type: restore marginTop=1 for thinking→output gap - Thinking text: trimEnd() to avoid double blank lines - Composer: restore marginTop=1, remove separator line - Add subtleBandColor() helper to color-utils Generated with AI Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
- Add useIsScreenReaderEnabled() guard in UserMessage to skip decorative half-block characters for screen reader users - Add width <= 0 guard to prevent RangeError on narrow terminals - Remove unused HalfLinePaddedBox component (dead code) - Cache supportsTrueColor() result at module scope - Add unit tests for interpolateColor, subtleBandColor, supportsTrueColor Generated with AI Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
| {/* Render standard message types */} | ||
| {itemForDisplay.type === 'user' && ( | ||
| <UserMessage text={itemForDisplay.text} /> | ||
| <UserMessage text={itemForDisplay.text} width={contentWidth} /> |
There was a problem hiding this comment.
[Critical] UserMessage receives width={contentWidth} (terminalWidth - 4, uncapped), while every other width-aware bordered child in this component uses boxWidth (mainAreaWidth || contentWidth, where mainAreaWidth = Math.min(terminalWidth - 4, 100)). On terminals wider than 104 columns, the accent band extends past the 100-column boundary that all other boxed components (tool groups, about box, stats displays, etc.) respect.
| <UserMessage text={itemForDisplay.text} width={contentWidth} /> | |
| <UserMessage text={itemForDisplay.text} width={boxWidth} /> |
— qwen3.7-max via Qwen Code /review
There was a problem hiding this comment.
Not applicable — the accent band is a content-level decoration (like AssistantMessage's contentWidth), not a bordered container (like AboutBox/StatsDisplay which use boxWidth). All text-rendering components in this file (AssistantMessage, ThinkMessage, BtwMessage, etc.) use contentWidth, not boxWidth. Using boxWidth would make the band narrower than the text it wraps.
| !supportsTrueColor() | ||
| ) { | ||
| return fallback; | ||
| } |
There was a problem hiding this comment.
[Suggestion] When this fallback path is taken (non-truecolor terminal, screen reader enabled, invalid width, or unresolvable background color), UserMessage renders without any top spacing. But getHistoryItemMarginTop returns 0 for user type (relying on the band for visual separation). So on non-truecolor terminals, user messages become directly adjacent to the preceding content — a regression from the pre-PR marginTop: 1.
Add marginTop to both fallback return paths:
| } | |
| return <Box marginTop={1}>{fallback}</Box>; |
And similarly at line 221 for the !bandColor fallback.
— qwen3.7-max via Qwen Code /review
There was a problem hiding this comment.
Fixed in bfa3305 — fallback path now renders with marginTop={1} when the band is unavailable, preventing user messages from becoming adjacent to preceding content.
When the half-line band is unavailable (non-truecolor terminal, screen reader, invalid width), restore marginTop=1 on the plain PrefixedTextMessage so user messages don't become adjacent to preceding content. Generated with AI Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
DragonnZhang
left a comment
There was a problem hiding this comment.
All previously reported Critical findings (screen-reader a11y guards, negative-width crash, Composer separator a11y) have been properly addressed across recent iterations. The latest commit cleanly adds a marginTop fallback for non-truecolor user messages via a well-structured useBand flag. HalfLinePaddedBox dead code is removed, 10 new unit tests cover the color utilities, and the UserMessage band rendering has proper guards for width, screen reader, and truecolor support. No new issues found. — claude-sonnet-4-20250514 via Qwen Code /review
| return fallback; | ||
| } | ||
|
|
||
| const bg = resolveColor(theme.background.primary || 'black') || 'black'; |
There was a problem hiding this comment.
[Suggestion] resolveColor(theme.background.primary || 'black') || 'black' bypasses the NoColorTheme. When NO_COLOR is set, NoColorTheme sets background.primary to '', but '' || 'black' evaluates to 'black', causing subtleBandColor to compute a real hex color. The useBand guard checks supportsTrueColor() and isScreenReaderEnabled but never checks for NO_COLOR. Users who set NO_COLOR=1 on a truecolor terminal would still see decorative half-block characters (▄/▀) framing their messages.
| const bg = resolveColor(theme.background.primary || 'black') || 'black'; | |
| if (!theme.background.primary) { | |
| return fallback; | |
| } | |
| const bg = resolveColor(theme.background.primary) || 'black'; |
— qwen3.7-max via Qwen Code /review
There was a problem hiding this comment.
Fixed in ddf9a47 — useBand guard now checks !!theme.background.primary, which is '' (falsy) under NoColorTheme. The || 'black' fallback in resolveColor call has also been removed.
Merge origin/main into feat/tui-spacing-density-pr1. Conflict in HistoryItemDisplay.tsx resolved by keeping our getHistoryItemMarginTop() while restoring main's compactMode/isHiddenInCompact logic. Generated with AI Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Remove .dataworks/ and .qwen/skills/data-consistency-analysis/ that were accidentally included in the merge commit via git add -A. Generated with AI Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
| blue: '#0000ff', | ||
| cyan: '#00ffff', | ||
| magenta: '#ff00ff', | ||
| white: '#ffffff', |
There was a problem hiding this comment.
[Suggestion] INK_NAME_TO_HEX maps only the 10 basic Ink color names but excludes the 8 bright variants (blackbright, redbright, greenbright, yellowbright, bluebright, cyanbright, magentabright, whitebright) that are listed in the exported INK_SUPPORTED_NAMES set. This causes toHex() to return undefined for bright names, making interpolateColor() silently return '' for valid Ink colors.
If a custom theme sets background.primary to a bright name, the band feature silently degrades.
| white: '#ffffff', | |
| gray: '#808080', | |
| grey: '#808080', | |
| blackbright: '#555555', | |
| redbright: '#ff5555', | |
| greenbright: '#55ff55', | |
| yellowbright: '#ffff55', | |
| bluebright: '#5555ff', | |
| cyanbright: '#55ffff', | |
| magentabright: '#ff55ff', | |
| whitebright: '#ffffff', |
— qwen3.7-max via Qwen Code /review
There was a problem hiding this comment.
Fixed in 3b0f4c9 — added all 8 bright variants.
| * Automatically detects dark/light from the background color luminance. | ||
| * Returns '' if the background color cannot be resolved. | ||
| */ | ||
| export function subtleBandColor(bgColor: string, factor = 0.06): string { |
There was a problem hiding this comment.
[Suggestion] subtleBandColor() uses a 6% brightness shift toward white/black, but the checked-in PR2 design doc (.qwen/design/tui-user-message-half-line-pr2.md, section 1) describes "混入 15% 主题强调色" (15% accent color blend). These are fundamentally different operations — achromatic brightness nudge vs chromatic accent blend. Either update the design doc to match the implementation, or align the code to the spec.
— qwen3.7-max via Qwen Code /review
There was a problem hiding this comment.
Fixed in 3b0f4c9 — design doc now correctly describes 6% brightness shift, not 15% accent blend.
|
|
||
| // Basic Ink color names that resolveColor passes through as names rather than | ||
| // hex. Needed so interpolateColor can blend them numerically. | ||
| const INK_NAME_TO_HEX: Readonly<Record<string, string>> = { |
There was a problem hiding this comment.
[Suggestion] INK_NAME_TO_HEX only maps 10 basic Ink color names, but INK_SUPPORTED_NAMES (line 155) includes 8 bright variants (blackbright, redbright, greenbright, yellowbright, bluebright, cyanbright, magentabright, whitebright). toHex() returns undefined for bright names, causing subtleBandColor to silently return ''. No built-in theme uses bright names for backgrounds today, but this is a clear completeness gap.
| const INK_NAME_TO_HEX: Readonly<Record<string, string>> = { | |
| const INK_NAME_TO_HEX: Readonly<Record<string, string>> = { | |
| black: '#000000', | |
| red: '#ff0000', | |
| green: '#00ff00', | |
| yellow: '#ffff00', | |
| blue: '#0000ff', | |
| cyan: '#00ffff', | |
| magenta: '#ff00ff', | |
| white: '#ffffff', | |
| gray: '#808080', | |
| grey: '#808080', | |
| blackbright: '#808080', | |
| redbright: '#ff8080', | |
| greenbright: '#80ff80', | |
| yellowbright: '#ffff80', | |
| bluebright: '#8080ff', | |
| cyanbright: '#80ffff', | |
| magentabright: '#ff80ff', | |
| whitebright: '#ffffff', | |
| }; |
— qwen3.7-max via Qwen Code /review
There was a problem hiding this comment.
Fixed in 3b0f4c9 — added all 8 bright variants to INK_NAME_TO_HEX.
|
|
||
| 同一轮对话内的"回复 → 工具调用 → 回复"序列不再有多余空行,信息更紧凑连贯。 | ||
|
|
||
| ### 3. 输入区域分隔线 |
There was a problem hiding this comment.
[Suggestion] Section 3 ("输入区域分隔线") describes replacing the input area blank line with a half-height separator, but the PR contains zero changes to the Composer/input area. The design doc is committed as part of this PR, so it should reflect what's actually delivered. Consider marking this section as deferred/out-of-scope.
— qwen3.7-max via Qwen Code /review
There was a problem hiding this comment.
Fixed in 3b0f4c9 — design doc updated: removed Composer separator section (not in PR), corrected color description to 6% brightness shift.
Local runtime verification report (Linux)Verdict: works as described — all five reviewer-checklist items verified on real built bundles. LGTM, with a few non-blocking cosmetic notes below. This fills in the 🐧 Linux row of the author's test matrix. MethodTwo throwaway worktrees, full real build chain (
A scripted mock OpenAI provider (streaming Reviewer test plan → results
A/B evidence (120-col terminal, same conversation)BEFORE ( AFTER (merged) — 25 rows (~22% fewer for this conversation): Band color math checks out exactlyCaptured SGR sequences (Qwen Dark,
Additional runtime checks
Tests
Merge mechanics
Non-blocking notes
Windows remains untested (per the author's matrix); the degradation guards make that low-risk. 🇨🇳 中文版报告(点击展开)本地运行时验证报告(Linux)结论:与描述一致 — Reviewer 清单五项全部在真实构建产物上验证通过。LGTM,文末附几条不阻塞合并的外观类备注。 本报告可补上作者测试矩阵中的 🐧 Linux 一行。 方法建两棵一次性 worktree,各自走完整真实构建链(
用脚本化的 mock OpenAI provider 驱动真实 TUI(流式 Reviewer 测试计划 → 结果
A/B 证据同一段对话(120 列终端):BEFORE 占 32 行,AFTER 占 25 行(本场景约省 22%)。具体逐行对比见英文版代码块。 Band 颜色数学精确吻合捕获的 SGR:band 上下行前景色与文本行背景色均为 额外运行时检查
测试
合并事项
不阻塞合并的备注
Windows 仍未测试(见作者矩阵);有降级守卫,风险较低。 |
- Update PR2 design doc to match implementation: 6% brightness shift (not 15% accent blend), remove Composer separator section - Add 8 bright Ink color variants to INK_NAME_TO_HEX for completeness Generated with AI Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
| ariaLabel={SCREEN_READER_USER_PREFIX} | ||
| alignSelf="flex-start" | ||
| marginTop={useBand ? 0 : 1} | ||
| /> |
There was a problem hiding this comment.
[Suggestion] When useBand is true but subtleBandColor() returns '' (e.g., theme background is truthy but unparseable to hex like rgb(...)), this fallback has marginTop={useBand ? 0 : 1} which evaluates to 0. The user message ends up with no top margin AND no accent band — flush against preceding content.
The fallback is always the non-band rendering path, so it should always carry the traditional spacing:
| /> | |
| marginTop={1} |
— qwen3.7-max via Qwen Code /review
There was a problem hiding this comment.
Fixed in 33d9903 — fallback path now always uses marginTop={1}, regardless of useBand flag.
| alignSelf="flex-start" | ||
| /> | ||
| ); | ||
| export const UserMessage: React.FC<UserMessageProps> = ({ text, width }) => { |
There was a problem hiding this comment.
[Suggestion] The accent band rendering path (half-line ▄/▀ characters, backgroundColor, multi-line padding with prefix alternation) has zero component-level test coverage. Existing tests run without truecolor support, so only the fallback PrefixedTextMessage path is exercised.
Consider adding tests that mock supportsTrueColor() to return true and assert: (a) ▄/▀ rows render, (b) background color is applied, (c) multi-line messages get '> ' on line 0 and ' ' on subsequent lines.
— qwen3.7-max via Qwen Code /review
There was a problem hiding this comment.
Acknowledged — component-level tests for the truecolor band path require mocking supportsTrueColor() and useIsScreenReaderEnabled(). The core color functions are covered by unit tests. Band rendering integration tests tracked for follow-up.
When subtleBandColor() fails to compute a color, the fallback PrefixedTextMessage now always gets marginTop=1 regardless of the useBand flag, preventing user messages from becoming flush against preceding content. Generated with AI Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
DragonnZhang
left a comment
There was a problem hiding this comment.
LGTM. Clean, well-tested spacing tightening. The centralized getHistoryItemMarginTop function correctly handles all HistoryItem types (unhandled types fall through to the original default of 1). The removal of isHiddenInCompact is safe because the JSX guards below still suppress content for compact-mode thought/summary items. The UserMessage band rendering has proper fallbacks for non-true-color terminals, screen readers, and missing width/color. Snapshots are consistent with the intended changes.
Local runtime verification on Linux (real TUI under tmux) — PASS ✅I built this PR's head ( Setup: Linux (Debian, kernel 6.12), isolated tmux socket at 200×50, fresh Reviewer test plan checklist
Band color, verified at the byte levelTheme Band width is exactly 196 = 200 (terminal) − 4, matching the PR vs main, same prompt (captures trimmed to ~100 cols)PR build: main build: Measured on this conversation: −5 rows inside a 5-entry tool group, −1 above the group, −1 above each thinking block. (The two runs batched tool calls differently — 5-in-1 vs 2+3, normal model nondeterminism — so I compared the structural anchors rather than total row counts.) Probes beyond the test plan
Observations for the merge decision (non-blocking)
Full step-labeled tmux transcripts (startup, both builds, every probe) were captured per 中文版(点击展开)Linux 本地真实 TUI 运行验证(tmux)— 通过 ✅我分别构建了本 PR 的最新提交( 环境: Linux(Debian,内核 6.12),独立 tmux socket,200×50;每个 worktree 独立执行 Test Plan 核对表
色带颜色的字节级验证主题 色带宽度恰为 196 = 200(终端宽)− 4,与 PR vs main 同提示词对比(截取约 100 列)PR 构建: main 构建: 本轮对话实测:5 条目工具组内部 −5 行,工具组上方 −1 行,每个思考块上方 −1 行。(两次运行的工具批次不同——5 合 1 vs 2+3,属正常的模型非确定性——因此对比的是结构锚点而非总行数。) Test Plan 之外的探测
供合并决策参考的观察(不阻塞)
完整的分步 tmux 截屏日志(启动、两个构建、全部探测)按 |
* fix(tui): Tighten message and tool spacing * docs(tui): Add spacing density evidence * docs(tui): Use upstream spacing evidence references * fix(tui): tighten inter-block spacing and add composer separator - Set marginTop=0 for user and gemini message types to eliminate blank lines between Q&A turns and tool calls - Add half-block (▄) separator line above Composer input area, replacing the full blank row with a subtle blended color line - Add color-utils helpers: interpolateColor, supportsTrueColor - Add HalfLinePaddedBox component (reusable, currently used by Composer only) - Add design doc for TUI spacing density PR2 Generated with AI Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(tui): use dimColor for user message half-line separators Replace the interpolated purple band color with terminal-native dimColor rendering for ▄/▀ half-line characters. This avoids theme-dependent color mismatches while preserving the visual spacing effect. Generated with AI Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(tui): refine spacing and add subtle user message band - User message: three-layer seamless band using subtleBandColor (6% brightness shift, no hue change) with ▄/content-bg/▀ - gemini type: restore marginTop=1 for thinking→output gap - Thinking text: trimEnd() to avoid double blank lines - Composer: restore marginTop=1, remove separator line - Add subtleBandColor() helper to color-utils Generated with AI Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(tui): address review findings for spacing PR - Add useIsScreenReaderEnabled() guard in UserMessage to skip decorative half-block characters for screen reader users - Add width <= 0 guard to prevent RangeError on narrow terminals - Remove unused HalfLinePaddedBox component (dead code) - Cache supportsTrueColor() result at module scope - Add unit tests for interpolateColor, subtleBandColor, supportsTrueColor Generated with AI Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(tui): add marginTop fallback for non-truecolor user messages When the half-line band is unavailable (non-truecolor terminal, screen reader, invalid width), restore marginTop=1 on the plain PrefixedTextMessage so user messages don't become adjacent to preceding content. Generated with AI Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(tui): respect NO_COLOR for user message band Skip half-line band rendering when theme.background.primary is empty (NoColorTheme sets it to ''), preventing decorative characters from appearing in NO_COLOR environments. Generated with AI Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * chore: remove accidentally committed local data files Remove .dataworks/ and .qwen/skills/data-consistency-analysis/ that were accidentally included in the merge commit via git add -A. Generated with AI Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(tui): update design doc and add bright color variants - Update PR2 design doc to match implementation: 6% brightness shift (not 15% accent blend), remove Composer separator section - Add 8 bright Ink color variants to INK_NAME_TO_HEX for completeness Generated with AI Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(tui): always use marginTop=1 on UserMessage fallback path When subtleBandColor() fails to compute a color, the fallback PrefixedTextMessage now always gets marginTop=1 regardless of the useBand flag, preventing user messages from becoming flush against preceding content. Generated with AI Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> --------- Co-authored-by: 秦奇 <gary.gq@alibaba-inc.com> Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
The TUI paints no background of its own and relies on the terminal's own background. Two elements broke that and rendered as off-colour blocks that could not be made consistent across terminals and themes: - The input box (QwenLM#5568) flood-filled theme.background.primary. Even when the theme's light/dark bucket matched the terminal (the QwenLM#5746 gate), the exact colour usually differed from the terminal's real background, so the prompt rendered as a distinct block — worst over SSH/remote where brightness detection is unreliable and defaults to dark. - The user-message half-line band (QwenLM#4595) painted a subtleBandColor band behind each user message, gated on the same theme/terminal match. Because history is rendered through Ink <Static> (committed rows are never repainted) and the gate only fires when the active theme matches the terminal, the band showed on some messages but not others across a theme switch — it cannot be made consistent. Stop painting both so the input area and user messages blend into the terminal background everywhere. User messages fall back to marginTop=1 for separation. The software cursor now derives its contrast from the terminal's detected brightness (getEffectiveTerminalBackground) so it stays visible with no fill painted. Fixes QwenLM#5771. Generated with AI Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
The TUI paints no background of its own and relies on the terminal's own background. Two elements broke that and rendered as off-colour blocks that could not be made consistent across terminals and themes: - The input box (QwenLM#5568) flood-filled theme.background.primary. Even when the theme's light/dark bucket matched the terminal (the QwenLM#5746 gate), the exact colour usually differed from the terminal's real background, so the prompt rendered as a distinct block — worst over SSH/remote where brightness detection is unreliable and defaults to dark. - The user-message half-line band (QwenLM#4595) painted a subtleBandColor band behind each user message, gated on the same theme/terminal match. Because history is rendered through Ink <Static> (committed rows are never repainted) and the gate only fires when the active theme matches the terminal, the band showed on some messages but not others across a theme switch — it cannot be made consistent. Stop painting both so the input area and user messages blend into the terminal background everywhere. User messages fall back to marginTop=1 for separation. The software cursor now derives its contrast from the terminal's detected brightness (getEffectiveTerminalBackground) so it stays visible with no fill painted. Also remove the band's now-unused helpers — subtleBandColor and supportsTrueColor (added by QwenLM#4595) and the dead UserMessageProps.width prop. Fixes QwenLM#5771. Generated with AI Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
The TUI paints no background of its own and relies on the terminal's own background. Two elements broke that and rendered as off-colour blocks that could not be made consistent across terminals and themes: - The input box (#5568) flood-filled theme.background.primary. Even when the theme's light/dark bucket matched the terminal (the #5746 gate), the exact colour usually differed from the terminal's real background, so the prompt rendered as a distinct block — worst over SSH/remote where brightness detection is unreliable and defaults to dark. - The user-message half-line band (#4595) painted a subtleBandColor band behind each user message, gated on the same theme/terminal match. Because history is rendered through Ink <Static> (committed rows are never repainted) and the gate only fires when the active theme matches the terminal, the band showed on some messages but not others across a theme switch — it cannot be made consistent. Stop painting both so the input area and user messages blend into the terminal background everywhere. User messages fall back to marginTop=1 for separation. The software cursor now derives its contrast from the terminal's detected brightness (getEffectiveTerminalBackground) so it stays visible with no fill painted. Also remove the band's now-unused helpers — subtleBandColor and supportsTrueColor (added by #4595) and the dead UserMessageProps.width prop. Fixes #5771. Generated with AI Co-authored-by: 秦奇 <gary.gq@alibaba-inc.com> Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

What this PR does
Tighten TUI vertical spacing and add a subtle user-message accent band, making the conversation denser while keeping turn boundaries scannable.
1. Spacing tightening
getHistoryItemMarginTop()— most message types returnmarginTop=0; onlygemini(model output) returns1for a thinking→output gapgap={1}insideToolGroupMessage(tool-to-tool separator)marginTop={1}on tool result containers inToolMessagetrimEnd()thinking text to prevent trailing newlines from creating double blank lines2. User message half-line accent band
On truecolor terminals, user messages render inside a three-layer band:
Band color is computed by
subtleBandColor(): a 6% brightness shift from the terminal background toward white (dark themes) or black (light themes). No hue change — just a brightness nudge, so the band is nearly invisible.Degradation:
> text(no band)> text(no decorative glyphs)> text(no crash)3. Infrastructure
color-utils.ts:interpolateColor(),subtleBandColor(),supportsTrueColor()(module-level cached)Comparision
Before(main branch) VS After (current branch)
Changed files
HistoryItemDisplay.tsxgetHistoryItemMarginTop(), thinkingtrimEnd(), passwidthto UserMessageConversationMessages.tsxToolGroupMessage.tsxgap={1}→gap={0}ToolMessage.tsxmarginTop={1}on result containerComposer.tsxmarginTop={1}(unchanged from main)color-utils.tsinterpolateColor,subtleBandColor,supportsTrueColor+ cachecolor-utils.test.tsHistoryItemDisplay.test.tsxWhy it's needed
Common sessions spend extra terminal rows on blank separators before assistant output, between tool blocks, and inside expanded tool groups. This makes Q&A, file lists, shell output, and streaming harder to scan.
Ref: QwenLM/qwen-code#4588
Reviewer Test Plan
Run the CLI and send a multi-step prompt that triggers tool calls + thinking + model output:
Verify:
>(non-truecolor)▄/▀characters renderedTested on