fix(tui): render explicit prompt gap
Reserve the composer prompt gap as layout instead of relying on terminal handling of trailing spaces.
This commit is contained in:
parent
456955c2e4
commit
10fcd620d2
@ -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)
|
||||
|
||||
@ -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 (
|
||||
<Box width={width}>
|
||||
<Box width={glyphWidth}>
|
||||
<Text bold={bold} color={color}>
|
||||
{promptText}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box width={PROMPT_GAP_WIDTH} />
|
||||
</Box>
|
||||
)
|
||||
})
|
||||
|
||||
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 | TextInputMouseApi>(null)
|
||||
@ -217,7 +244,11 @@ const ComposerPane = memo(function ComposerPane({
|
||||
{composer.inputBuf.map((line, i) => (
|
||||
<Box key={i}>
|
||||
<Box width={promptWidth}>
|
||||
<Text color={ui.theme.color.muted}>{i === 0 ? promptLabel : ' '.repeat(promptWidth)}</Text>
|
||||
{i === 0 ? (
|
||||
<PromptPrefix color={ui.theme.color.muted} promptText={promptText} width={promptWidth} />
|
||||
) : (
|
||||
<Text color={ui.theme.color.muted}>{promptBlank}</Text>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<Text color={ui.theme.color.text}>{line || ' '}</Text>
|
||||
@ -232,11 +263,11 @@ const ComposerPane = memo(function ComposerPane({
|
||||
>
|
||||
<Box width={promptWidth}>
|
||||
{sh ? (
|
||||
<Text color={ui.theme.color.shellDollar}>{promptLabel}</Text>
|
||||
<PromptPrefix color={ui.theme.color.shellDollar} promptText={promptText} width={promptWidth} />
|
||||
) : composer.inputBuf.length ? (
|
||||
<Text color={ui.theme.color.prompt}>{promptBlank}</Text>
|
||||
) : (
|
||||
<Text bold color={ui.theme.color.prompt}>
|
||||
{composer.inputBuf.length ? ' '.repeat(promptWidth) : promptLabel}
|
||||
</Text>
|
||||
<PromptPrefix bold color={ui.theme.color.prompt} promptText={promptText} width={promptWidth} />
|
||||
)}
|
||||
</Box>
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user