From 10fcd620d274ebcc073efbdb1d15a5efe175d522 Mon Sep 17 00:00:00 2001 From: Brooklyn Nicholson Date: Wed, 29 Apr 2026 15:25:06 -0500 Subject: [PATCH] fix(tui): render explicit prompt gap Reserve the composer prompt gap as layout instead of relying on terminal handling of trailing spaces. --- ui-tui/src/__tests__/textInputWrap.test.ts | 8 +++- ui-tui/src/components/appLayout.tsx | 47 ++++++++++++++++++---- ui-tui/src/lib/inputMetrics.ts | 4 ++ 3 files changed, 50 insertions(+), 9 deletions(-) diff --git a/ui-tui/src/__tests__/textInputWrap.test.ts b/ui-tui/src/__tests__/textInputWrap.test.ts index 5521012e9c..e3ab5d0b32 100644 --- a/ui-tui/src/__tests__/textInputWrap.test.ts +++ b/ui-tui/src/__tests__/textInputWrap.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it } from 'vitest' import { offsetFromPosition } from '../components/textInput.js' -import { cursorLayout, inputVisualHeight, stableComposerColumns } from '../lib/inputMetrics.js' +import { composerPromptWidth, cursorLayout, inputVisualHeight, stableComposerColumns } from '../lib/inputMetrics.js' describe('cursorLayout — char-wrap parity with wrap-ansi', () => { it('places cursor mid-line at its column', () => { @@ -42,6 +42,12 @@ describe('input metrics helpers', () => { expect(inputVisualHeight('one\ntwo', 40)).toBe(2) }) + it('counts the prompt gap as its own cell', () => { + expect(composerPromptWidth('>')).toBe(2) + expect(composerPromptWidth('❯')).toBe(2) + expect(composerPromptWidth('Ψ >')).toBe(4) + }) + it('reserves gutters on wide panes without starving narrow composer width', () => { expect(stableComposerColumns(100, 3)).toBe(93) expect(stableComposerColumns(100, 5)).toBe(91) diff --git a/ui-tui/src/components/appLayout.tsx b/ui-tui/src/components/appLayout.tsx index f97cc17e60..bd78c31f47 100644 --- a/ui-tui/src/components/appLayout.tsx +++ b/ui-tui/src/components/appLayout.tsx @@ -9,7 +9,7 @@ import { $uiState } from '../app/uiStore.js' import { INLINE_MODE, SHOW_FPS } from '../config/env.js' import { FULL_RENDER_TAIL_ITEMS } from '../config/limits.js' import { PLACEHOLDER } from '../content/placeholders.js' -import { inputVisualHeight, stableComposerColumns } from '../lib/inputMetrics.js' +import { composerPromptWidth, inputVisualHeight, stableComposerColumns } from '../lib/inputMetrics.js' import { PerfPane } from '../lib/perfPane.js' import { AgentsOverlay } from './agentsOverlay.js' @@ -22,6 +22,33 @@ import { QueuedMessages } from './queuedMessages.js' import { LiveTodoPanel, StreamingAssistant } from './streamingAssistant.js' import { TextInput, type TextInputMouseApi } from './textInput.js' +const PROMPT_GAP_WIDTH = 1 + +const PromptPrefix = memo(function PromptPrefix({ + bold = false, + color, + promptText, + width +}: { + bold?: boolean + color: string + promptText: string + width: number +}) { + const glyphWidth = Math.max(1, stringWidth(promptText)) + + return ( + + + + {promptText} + + + + + ) +}) + const TranscriptPane = memo(function TranscriptPane({ actions, composer, @@ -125,8 +152,8 @@ const ComposerPane = memo(function ComposerPane({ const isBlocked = useStore($isBlocked) const sh = (composer.inputBuf[0] ?? composer.input).startsWith('!') const promptText = sh ? '$' : ui.theme.brand.prompt - const promptLabel = `${promptText} ` - const promptWidth = Math.max(1, stringWidth(promptLabel)) + const promptWidth = composerPromptWidth(promptText) + const promptBlank = ' '.repeat(promptWidth) const inputColumns = stableComposerColumns(composer.cols, promptWidth) const inputHeight = inputVisualHeight(composer.input, inputColumns) const inputMouseRef = useRef(null) @@ -217,7 +244,11 @@ const ComposerPane = memo(function ComposerPane({ {composer.inputBuf.map((line, i) => ( - {i === 0 ? promptLabel : ' '.repeat(promptWidth)} + {i === 0 ? ( + + ) : ( + {promptBlank} + )} {line || ' '} @@ -232,11 +263,11 @@ const ComposerPane = memo(function ComposerPane({ > {sh ? ( - {promptLabel} + + ) : composer.inputBuf.length ? ( + {promptBlank} ) : ( - - {composer.inputBuf.length ? ' '.repeat(promptWidth) : promptLabel} - + )} diff --git a/ui-tui/src/lib/inputMetrics.ts b/ui-tui/src/lib/inputMetrics.ts index d54f963709..f110221a56 100644 --- a/ui-tui/src/lib/inputMetrics.ts +++ b/ui-tui/src/lib/inputMetrics.ts @@ -53,6 +53,10 @@ export function inputVisualHeight(value: string, columns: number) { return cursorLayout(value, value.length, columns).line + 1 } +export function composerPromptWidth(promptText: string) { + return Math.max(1, stringWidth(promptText)) + 1 +} + export function stableComposerColumns(totalCols: number, promptWidth: number) { // Physical render/wrap width. Always reserve outer composer padding and // prompt prefix. Only reserve the transcript scrollbar gutter when the