diff --git a/ui-tui/src/__tests__/textInputFastEcho.test.ts b/ui-tui/src/__tests__/textInputFastEcho.test.ts
index 75ed282a8a0e..d0ccaada60f6 100644
--- a/ui-tui/src/__tests__/textInputFastEcho.test.ts
+++ b/ui-tui/src/__tests__/textInputFastEcho.test.ts
@@ -1,6 +1,11 @@
import { describe, expect, it } from 'vitest'
-import { canFastAppendShape, canFastBackspaceShape, supportsFastEchoTerminal } from '../components/textInput.js'
+import {
+ canFastAppendShape,
+ canFastBackspaceShape,
+ colorizeEcho,
+ supportsFastEchoTerminal
+} from '../components/textInput.js'
// The fast-echo path bypasses Ink and writes characters directly to stdout
// for the common case of typing plain English at the end of the line. These
@@ -173,6 +178,27 @@ describe('canFastBackspaceShape', () => {
})
})
+describe('colorizeEcho', () => {
+ // The fast-echo bypass writes raw cells past Ink, so a themed input must
+ // carry the theme fg explicitly — a default-fg glyph goes invisible when a
+ // skin repaints the background to the opposite polarity (dark skin on a
+ // light terminal ⇒ black-on-black).
+
+ it('wraps the write in truecolor fg + reset for a hex theme color', () => {
+ expect(colorizeEcho('x', '#ff2d95')).toBe('\x1b[38;2;255;45;149mx\x1b[39m')
+ })
+
+ it('passes through untouched without a color (unthemed keeps terminal default)', () => {
+ expect(colorizeEcho('x')).toBe('x')
+ expect(colorizeEcho('x', undefined)).toBe('x')
+ })
+
+ it('passes through on a non-hex color (never emit a garbage SGR)', () => {
+ expect(colorizeEcho('x', 'red')).toBe('x')
+ expect(colorizeEcho('x', '#fff')).toBe('x')
+ })
+})
+
describe('supportsFastEchoTerminal', () => {
it('disables fast-echo in Apple Terminal', () => {
expect(supportsFastEchoTerminal({ TERM_PROGRAM: 'Apple_Terminal' } as NodeJS.ProcessEnv)).toBe(false)
diff --git a/ui-tui/src/components/activeSessionSwitcher.tsx b/ui-tui/src/components/activeSessionSwitcher.tsx
index 52668c1d51f3..beb4683f62ec 100644
--- a/ui-tui/src/components/activeSessionSwitcher.tsx
+++ b/ui-tui/src/components/activeSessionSwitcher.tsx
@@ -866,7 +866,13 @@ export function ActiveSessionSwitcher({
<>
prompt ›
-
+
diff --git a/ui-tui/src/components/appLayout.tsx b/ui-tui/src/components/appLayout.tsx
index d6333eccd4a2..1b4ba084fba0 100644
--- a/ui-tui/src/components/appLayout.tsx
+++ b/ui-tui/src/components/appLayout.tsx
@@ -417,6 +417,7 @@ const ComposerPane = memo(function ComposerPane({
{/* Reserve the transcript scrollbar gutter too so typing never rewraps when the scrollbar column repaints. */}
Enter a custom amount:
{'$'}
-
+
{error && {error}}
@@ -852,6 +852,7 @@ function AutoReloadScreen({ ctx, onClose, onPatch, s, t }: ScreenProps) {
{'$'}
{'> '}
-
+
)
diff --git a/ui-tui/src/components/prompts.tsx b/ui-tui/src/components/prompts.tsx
index 614be012aaad..78fade0d2c1f 100644
--- a/ui-tui/src/components/prompts.tsx
+++ b/ui-tui/src/components/prompts.tsx
@@ -192,7 +192,13 @@ export function ClarifyPrompt({ cols = 80, onAnswer, onCancel, req, t }: Clarify
{'> '}
-
+
diff --git a/ui-tui/src/components/textInput.tsx b/ui-tui/src/components/textInput.tsx
index c65fc8c54419..9d35bbdf1331 100644
--- a/ui-tui/src/components/textInput.tsx
+++ b/ui-tui/src/components/textInput.tsx
@@ -58,6 +58,14 @@ const colorizeHint = (s: string, hex?: string) => {
return `${ESC}[38;2;${r};${g};${b}m${s}${ESC}[39m`
}
+// Typed-text fast-echo must carry the SAME explicit fg the Ink render uses:
+// the bypass writes raw cells, and a default-fg glyph goes invisible the
+// moment a skin repaints the background to the opposite polarity (a dark
+// skin on a light terminal ⇒ black-on-black). No color ⇒ passthrough, so
+// unthemed inputs keep the terminal default.
+export const colorizeEcho = (s: string, hex?: string) =>
+ /^#[0-9a-f]{6}$/i.test(hex ?? '') ? `${ESC}[38;2;${hintRgb(hex).join(';')}m${s}${ESC}[39m` : s
+
/** Synthetic placeholder cursor: a hint-colored chip with luminance-picked
* ink, standing in for the hidden hardware cursor (bubbles pattern). */
const hintCursorCell = (ch: string, hex?: string) => {
@@ -565,6 +573,7 @@ export function TextInput({
voiceRecordKey = DEFAULT_VOICE_RECORD_KEY,
placeholder = '',
placeholderColor,
+ color,
focus = true
}: TextInputProps) {
const [cur, setCur] = useState(value.length)
@@ -1306,7 +1315,9 @@ export function TextInput({
if (simpleAppend) {
const effect = fastAppendEffect(preInsertValue, preInsertCursor, text)
- stdout!.write(effect.write)
+ // Same explicit fg as the Ink render (see the ) —
+ // the bypass cell must not flash the terminal-default color.
+ stdout!.write(colorizeEcho(effect.write, color))
// ASCII-printable text advances the physical cursor by exactly
// text.length cells (canFastAppendShape rejects non-ASCII,
// wide chars, newlines). Notify Ink so the cached displayCursor
@@ -1395,7 +1406,14 @@ export function TextInput({
ref={boxRef}
width={columns}
>
- {rendered}
+ {/* Explicit theme color on the typed text — default fg tracks the HOST
+ terminal's polarity, not the skin's, so a live dark-skin repaint on a
+ light terminal would otherwise leave the input black-on-black. chalk
+ re-opens the outer color after embedded [39m closes (placeholder
+ chips), and INV cursor/selection cells don't touch fg. */}
+
+ {rendered}
+
)
}
@@ -1416,6 +1434,8 @@ export interface PasteEvent {
}
interface TextInputProps {
+ /** Hex color for typed text (theme text); terminal default when omitted. */
+ color?: string
columns?: number
focus?: boolean
mask?: string