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