diff --git a/ui-tui/src/app.tsx b/ui-tui/src/app.tsx
index eae4043714..2efaca53ea 100644
--- a/ui-tui/src/app.tsx
+++ b/ui-tui/src/app.tsx
@@ -13,7 +13,7 @@ import { MessageLine } from './components/messageLine.js'
import { ApprovalPrompt, ClarifyPrompt } from './components/prompts.js'
import { QueuedMessages } from './components/queuedMessages.js'
import { SessionPicker } from './components/sessionPicker.js'
-import { TextInput } from './components/textInput.js'
+import { type PasteEvent, TextInput } from './components/textInput.js'
import { Thinking } from './components/thinking.js'
import { HOTKEYS, INTERPOLATION_RE, PLACEHOLDERS, TOOL_VERBS, ZERO } from './constants.js'
import { type GatewayClient, type GatewayEvent } from './gatewayClient.js'
@@ -229,18 +229,24 @@ export function App({ gw }: { gw: GatewayClient }) {
const [cols, setCols] = useState(stdout?.columns ?? 80)
useEffect(() => {
- if (!stdout) {return}
+ if (!stdout) {
+ return
+ }
const sync = () => setCols(stdout.columns ?? 80)
stdout.on('resize', sync)
// Enable bracketed paste so image-only clipboard paste reaches the app
- if (stdout.isTTY) {stdout.write('\x1b[?2004h')}
+ if (stdout.isTTY) {
+ stdout.write('\x1b[?2004h')
+ }
return () => {
stdout.off('resize', sync)
- if (stdout.isTTY) {stdout.write('\x1b[?2004l')}
+ if (stdout.isTTY) {
+ stdout.write('\x1b[?2004l')
+ }
}
}, [stdout])
@@ -513,14 +519,20 @@ export function App({ gw }: { gw: GatewayClient }) {
)
const handleTextPaste = useCallback(
- ({ bracketed, cursor, hotkey, text, value }: import('./components/textInput.js').PasteEvent) => {
- if (hotkey) { void paste(false);
+ ({ bracketed, cursor, hotkey, text, value }: PasteEvent) => {
+ if (hotkey) {
+ void paste(false)
- return null }
+ return null
+ }
- if (bracketed) {void paste(true)}
+ if (bracketed) {
+ void paste(true)
+ }
- if (!text) {return null}
+ if (!text) {
+ return null
+ }
const lineCount = text.split('\n').length
@@ -770,23 +782,38 @@ export function App({ gw }: { gw: GatewayClient }) {
// ── Input handling ───────────────────────────────────────────────
- const ctrl = (key: { ctrl: boolean }, ch: string, target: string) =>
- key.ctrl && ch.toLowerCase() === target
+ const ctrl = (key: { ctrl: boolean }, ch: string, target: string) => key.ctrl && ch.toLowerCase() === target
useInput((ch, key) => {
if (isBlocked) {
if (pasteReview) {
- if (key.return) { setPasteReview(null); dispatchSubmission(pasteReview.text, true) }
- else if (key.escape || ctrl(key, ch, 'c')) { setPasteReview(null); setStatus('ready') }
+ if (key.return) {
+ setPasteReview(null)
+ dispatchSubmission(pasteReview.text, true)
+ } else if (key.escape || ctrl(key, ch, 'c')) {
+ setPasteReview(null)
+ setStatus('ready')
+ }
return
}
if (ctrl(key, ch, 'c')) {
- if (approval) { gw.request('approval.respond', { choice: 'deny', session_id: sid }).catch(() => {}); setApproval(null); sys('denied') }
- else if (sudo) { gw.request('sudo.respond', { request_id: sudo.requestId, password: '' }).catch(() => {}); setSudo(null); sys('sudo cancelled') }
- else if (secret) { gw.request('secret.respond', { request_id: secret.requestId, value: '' }).catch(() => {}); setSecret(null); sys('secret entry cancelled') }
- else if (picker) {setPicker(false)}
+ if (approval) {
+ gw.request('approval.respond', { choice: 'deny', session_id: sid }).catch(() => {})
+ setApproval(null)
+ sys('denied')
+ } else if (sudo) {
+ gw.request('sudo.respond', { request_id: sudo.requestId, password: '' }).catch(() => {})
+ setSudo(null)
+ sys('sudo cancelled')
+ } else if (secret) {
+ gw.request('secret.respond', { request_id: secret.requestId, value: '' }).catch(() => {})
+ setSecret(null)
+ sys('secret entry cancelled')
+ } else if (picker) {
+ setPicker(false)
+ }
} else if (key.escape && picker) {
setPicker(false)
}
@@ -803,7 +830,9 @@ export function App({ gw }: { gw: GatewayClient }) {
if (!inputBuf.length && key.tab && completions.length) {
const row = completions[compIdx]
- if (row) {setInput(input.slice(0, compReplace) + row.text)}
+ if (row) {
+ setInput(input.slice(0, compReplace) + row.text)
+ }
return
}
@@ -811,12 +840,19 @@ export function App({ gw }: { gw: GatewayClient }) {
if (key.upArrow && !inputBuf.length) {
if (queueRef.current.length) {
const idx = queueEditIdx === null ? 0 : (queueEditIdx + 1) % queueRef.current.length
- setQueueEdit(idx); setHistoryIdx(null); setInput(queueRef.current[idx] ?? '')
+ setQueueEdit(idx)
+ setHistoryIdx(null)
+ setInput(queueRef.current[idx] ?? '')
} else if (historyRef.current.length) {
const idx = historyIdx === null ? historyRef.current.length - 1 : Math.max(0, historyIdx - 1)
- if (historyIdx === null) {historyDraftRef.current = input}
- setHistoryIdx(idx); setQueueEdit(null); setInput(historyRef.current[idx] ?? '')
+ if (historyIdx === null) {
+ historyDraftRef.current = input
+ }
+
+ setHistoryIdx(idx)
+ setQueueEdit(null)
+ setInput(historyRef.current[idx] ?? '')
}
return
@@ -824,16 +860,24 @@ export function App({ gw }: { gw: GatewayClient }) {
if (key.downArrow && !inputBuf.length) {
if (queueRef.current.length) {
- const idx = queueEditIdx === null
- ? queueRef.current.length - 1
- : (queueEditIdx - 1 + queueRef.current.length) % queueRef.current.length
+ const idx =
+ queueEditIdx === null
+ ? queueRef.current.length - 1
+ : (queueEditIdx - 1 + queueRef.current.length) % queueRef.current.length
- setQueueEdit(idx); setHistoryIdx(null); setInput(queueRef.current[idx] ?? '')
+ setQueueEdit(idx)
+ setHistoryIdx(null)
+ setInput(queueRef.current[idx] ?? '')
} else if (historyIdx !== null) {
const next = historyIdx + 1
- if (next >= historyRef.current.length) { setHistoryIdx(null); setInput(historyDraftRef.current) }
- else { setHistoryIdx(next); setInput(historyRef.current[next] ?? '') }
+ if (next >= historyRef.current.length) {
+ setHistoryIdx(null)
+ setInput(historyDraftRef.current)
+ } else {
+ setHistoryIdx(next)
+ setInput(historyRef.current[next] ?? '')
+ }
}
return
@@ -846,10 +890,20 @@ export function App({ gw }: { gw: GatewayClient }) {
const partial = (streaming || buf.current).trimStart()
partial ? appendMessage({ role: 'assistant', text: partial + '\n\n*[interrupted]*' }) : sys('interrupted')
- idle(); setReasoning(''); setActivity([]); turnToolsRef.current = []; setStatus('interrupted')
+ idle()
+ setReasoning('')
+ setActivity([])
+ turnToolsRef.current = []
+ setStatus('interrupted')
- if (statusTimerRef.current) {clearTimeout(statusTimerRef.current)}
- statusTimerRef.current = setTimeout(() => { statusTimerRef.current = null; setStatus('ready') }, 1500)
+ if (statusTimerRef.current) {
+ clearTimeout(statusTimerRef.current)
+ }
+
+ statusTimerRef.current = setTimeout(() => {
+ statusTimerRef.current = null
+ setStatus('ready')
+ }, 1500)
} else if (input || inputBuf.length) {
clearIn()
} else {
@@ -859,13 +913,20 @@ export function App({ gw }: { gw: GatewayClient }) {
return
}
- if (ctrl(key, ch, 'd')) {return die()}
+ if (ctrl(key, ch, 'd')) {
+ return die()
+ }
- if (ctrl(key, ch, 'l')) { setStatus('forging session…'); newSession();
+ if (ctrl(key, ch, 'l')) {
+ setStatus('forging session…')
+ newSession()
- return }
+ return
+ }
- if (ctrl(key, ch, 'g')) {return openEditor()}
+ if (ctrl(key, ch, 'g')) {
+ return openEditor()
+ }
})
// ── Gateway events ───────────────────────────────────────────────
diff --git a/ui-tui/src/components/textInput.tsx b/ui-tui/src/components/textInput.tsx
index 6ec5cc5fca..9ec083c9dd 100644
--- a/ui-tui/src/components/textInput.tsx
+++ b/ui-tui/src/components/textInput.tsx
@@ -4,9 +4,13 @@ import { useEffect, useRef, useState } from 'react'
function wordLeft(s: string, p: number) {
let i = p - 1
- while (i > 0 && /\s/.test(s[i]!)) {i--}
+ while (i > 0 && /\s/.test(s[i]!)) {
+ i--
+ }
- while (i > 0 && !/\s/.test(s[i - 1]!)) {i--}
+ while (i > 0 && !/\s/.test(s[i - 1]!)) {
+ i--
+ }
return Math.max(0, i)
}
@@ -14,9 +18,13 @@ function wordLeft(s: string, p: number) {
function wordRight(s: string, p: number) {
let i = p
- while (i < s.length && !/\s/.test(s[i]!)) {i++}
+ while (i < s.length && !/\s/.test(s[i]!)) {
+ i++
+ }
- while (i < s.length && /\s/.test(s[i]!)) {i++}
+ while (i < s.length && /\s/.test(s[i]!)) {
+ i++
+ }
return i
}
@@ -77,7 +85,14 @@ export function TextInput({ value, onChange, onPaste, onSubmit, placeholder = ''
}
}, [value])
- useEffect(() => () => { if (pasteTimer.current) {clearTimeout(pasteTimer.current)} }, [])
+ useEffect(
+ () => () => {
+ if (pasteTimer.current) {
+ clearTimeout(pasteTimer.current)
+ }
+ },
+ []
+ )
// ── Buffer ops (synchronous, ref-based) ─────────────────────────
@@ -88,7 +103,10 @@ export function TextInput({ value, onChange, onPaste, onSubmit, placeholder = ''
if (track && next !== prev) {
undoStack.current.push({ cursor: curRef.current, value: prev })
- if (undoStack.current.length > 200) {undoStack.current.shift()}
+ if (undoStack.current.length > 200) {
+ undoStack.current.shift()
+ }
+
redoStack.current = []
}
@@ -105,7 +123,10 @@ export function TextInput({ value, onChange, onPaste, onSubmit, placeholder = ''
const swap = (from: typeof undoStack, to: typeof redoStack) => {
const entry = from.current.pop()
- if (!entry) {return}
+ if (!entry) {
+ return
+ }
+
to.current.push({ cursor: curRef.current, value: vRef.current })
commit(entry.value, entry.cursor, false)
}
@@ -113,7 +134,9 @@ export function TextInput({ value, onChange, onPaste, onSubmit, placeholder = ''
const emitPaste = (e: PasteEvent) => {
const handled = onPasteRef.current?.(e)
- if (handled) {commit(handled.value, handled.cursor)}
+ if (handled) {
+ commit(handled.value, handled.cursor)
+ }
return !!handled
}
@@ -124,7 +147,9 @@ export function TextInput({ value, onChange, onPaste, onSubmit, placeholder = ''
pasteBuf.current = ''
pasteTimer.current = null
- if (!text) {return}
+ if (!text) {
+ return
+ }
if (!emitPaste({ cursor: at, text, value: vRef.current }) && PRINTABLE.test(text)) {
commit(vRef.current.slice(0, at) + text + vRef.current.slice(at), at + text.length)
@@ -146,15 +171,20 @@ export function TextInput({ value, onChange, onPaste, onSubmit, placeholder = ''
// Keys handled by App.useInput
if (
- k.upArrow || k.downArrow ||
+ k.upArrow ||
+ k.downArrow ||
(k.ctrl && inp === 'c') ||
- k.tab || (k.shift && k.tab) ||
- k.pageUp || k.pageDown ||
+ k.tab ||
+ (k.shift && k.tab) ||
+ k.pageUp ||
+ k.pageDown ||
k.escape
- ) {return}
+ ) {
+ return
+ }
if (k.return) {
- ;(k.shift || k.meta)
+ k.shift || k.meta
? commit(insert(vRef.current, curRef.current, '\n'), curRef.current + 1)
: onSubmitRef.current?.(vRef.current)
@@ -165,46 +195,85 @@ export function TextInput({ value, onChange, onPaste, onSubmit, placeholder = ''
let v = vRef.current
const mod = k.ctrl || k.meta
- if (k.ctrl && inp === 'z') {return swap(undoStack, redoStack)}
-
- if ((k.ctrl && inp === 'y') || (k.meta && k.shift && inp === 'z')) {return swap(redoStack, undoStack)}
-
- if (k.home || (k.ctrl && inp === 'a')) {c = 0}
- else if (k.end || (k.ctrl && inp === 'e')) {c = v.length}
- else if (k.leftArrow) {c = mod ? wordLeft(v, c) : Math.max(0, c - 1)}
- else if (k.rightArrow) {c = mod ? wordRight(v, c) : Math.min(v.length, c + 1)}
- else if ((k.backspace || k.delete) && c > 0) {
- if (mod) { const t = wordLeft(v, c); v = v.slice(0, t) + v.slice(c); c = t }
- else { v = v.slice(0, c - 1) + v.slice(c); c-- }
+ if (k.ctrl && inp === 'z') {
+ return swap(undoStack, redoStack)
}
- else if (k.ctrl && inp === 'w' && c > 0) { const t = wordLeft(v, c); v = v.slice(0, t) + v.slice(c); c = t }
- else if (k.ctrl && inp === 'u') { v = v.slice(c); c = 0 }
- else if (k.ctrl && inp === 'k') {v = v.slice(0, c)}
- else if (k.meta && inp === 'b') {c = wordLeft(v, c)}
- else if (k.meta && inp === 'f') {c = wordRight(v, c)}
- else if (inp.length > 0) {
+
+ if ((k.ctrl && inp === 'y') || (k.meta && k.shift && inp === 'z')) {
+ return swap(redoStack, undoStack)
+ }
+
+ if (k.home || (k.ctrl && inp === 'a')) {
+ c = 0
+ } else if (k.end || (k.ctrl && inp === 'e')) {
+ c = v.length
+ } else if (k.leftArrow) {
+ c = mod ? wordLeft(v, c) : Math.max(0, c - 1)
+ } else if (k.rightArrow) {
+ c = mod ? wordRight(v, c) : Math.min(v.length, c + 1)
+ } else if ((k.backspace || k.delete) && c > 0) {
+ if (mod) {
+ const t = wordLeft(v, c)
+ v = v.slice(0, t) + v.slice(c)
+ c = t
+ } else {
+ v = v.slice(0, c - 1) + v.slice(c)
+ c--
+ }
+ } else if (k.ctrl && inp === 'w' && c > 0) {
+ const t = wordLeft(v, c)
+ v = v.slice(0, t) + v.slice(c)
+ c = t
+ } else if (k.ctrl && inp === 'u') {
+ v = v.slice(c)
+ c = 0
+ } else if (k.ctrl && inp === 'k') {
+ v = v.slice(0, c)
+ } else if (k.meta && inp === 'b') {
+ c = wordLeft(v, c)
+ } else if (k.meta && inp === 'f') {
+ c = wordRight(v, c)
+ } else if (inp.length > 0) {
const bracketed = inp.includes('[200~')
const raw = inp.replace(BRACKET_PASTE, '').replace(/\r\n/g, '\n').replace(/\r/g, '\n')
- if (bracketed && emitPaste({ bracketed: true, cursor: c, text: raw, value: v })) {return}
+ if (bracketed && emitPaste({ bracketed: true, cursor: c, text: raw, value: v })) {
+ return
+ }
- if (!raw) {return}
+ if (!raw) {
+ return
+ }
- if (raw === '\n') {return commit(insert(v, c, '\n'), c + 1)}
+ if (raw === '\n') {
+ return commit(insert(v, c, '\n'), c + 1)
+ }
if (raw.length > 1 || raw.includes('\n')) {
- if (!pasteBuf.current) {pastePos.current = c}
+ if (!pasteBuf.current) {
+ pastePos.current = c
+ }
+
pasteBuf.current += raw
- if (pasteTimer.current) {clearTimeout(pasteTimer.current)}
+ if (pasteTimer.current) {
+ clearTimeout(pasteTimer.current)
+ }
+
pasteTimer.current = setTimeout(flushPaste, 50)
return
}
- if (PRINTABLE.test(raw)) { v = v.slice(0, c) + raw + v.slice(c); c += raw.length }
- else {return}
- } else {return}
+ if (PRINTABLE.test(raw)) {
+ v = v.slice(0, c) + raw + v.slice(c)
+ c += raw.length
+ } else {
+ return
+ }
+ } else {
+ return
+ }
commit(v, c)
},
@@ -213,7 +282,9 @@ export function TextInput({ value, onChange, onPaste, onSubmit, placeholder = ''
// ── Render ──────────────────────────────────────────────────────
- if (!focus) {return {value || (placeholder ? DIM + placeholder + DIM_OFF : '')}}
+ if (!focus) {
+ return {value || (placeholder ? DIM + placeholder + DIM_OFF : '')}
+ }
if (!value && placeholder) {
return {INV + (placeholder[0] ?? ' ') + INV_OFF + DIM + placeholder.slice(1) + DIM_OFF}