fix(ui-tui): ref-based input buffer, gateway listener stability, usage display, and 6 correctness bugs
This commit is contained in:
@@ -232,8 +232,13 @@ def _resolve_model() -> str:
|
|||||||
def _get_usage(agent) -> dict:
|
def _get_usage(agent) -> dict:
|
||||||
g = lambda k, fb=None: getattr(agent, k, 0) or (getattr(agent, fb, 0) if fb else 0)
|
g = lambda k, fb=None: getattr(agent, k, 0) or (getattr(agent, fb, 0) if fb else 0)
|
||||||
usage = {
|
usage = {
|
||||||
|
"model": getattr(agent, "model", "") or "",
|
||||||
"input": g("session_input_tokens", "session_prompt_tokens"),
|
"input": g("session_input_tokens", "session_prompt_tokens"),
|
||||||
"output": g("session_output_tokens", "session_completion_tokens"),
|
"output": g("session_output_tokens", "session_completion_tokens"),
|
||||||
|
"cache_read": g("session_cache_read_tokens"),
|
||||||
|
"cache_write": g("session_cache_write_tokens"),
|
||||||
|
"prompt": g("session_prompt_tokens"),
|
||||||
|
"completion": g("session_completion_tokens"),
|
||||||
"total": g("session_total_tokens"),
|
"total": g("session_total_tokens"),
|
||||||
"calls": g("session_api_calls"),
|
"calls": g("session_api_calls"),
|
||||||
}
|
}
|
||||||
@@ -245,6 +250,25 @@ def _get_usage(agent) -> dict:
|
|||||||
usage["context_used"] = ctx_used
|
usage["context_used"] = ctx_used
|
||||||
usage["context_max"] = ctx_max
|
usage["context_max"] = ctx_max
|
||||||
usage["context_percent"] = max(0, min(100, round(ctx_used / ctx_max * 100)))
|
usage["context_percent"] = max(0, min(100, round(ctx_used / ctx_max * 100)))
|
||||||
|
usage["compressions"] = getattr(comp, "compression_count", 0) or 0
|
||||||
|
try:
|
||||||
|
from agent.usage_pricing import CanonicalUsage, estimate_usage_cost
|
||||||
|
cost = estimate_usage_cost(
|
||||||
|
usage["model"],
|
||||||
|
CanonicalUsage(
|
||||||
|
input_tokens=usage["input"],
|
||||||
|
output_tokens=usage["output"],
|
||||||
|
cache_read_tokens=usage["cache_read"],
|
||||||
|
cache_write_tokens=usage["cache_write"],
|
||||||
|
),
|
||||||
|
provider=getattr(agent, "provider", None),
|
||||||
|
base_url=getattr(agent, "base_url", None),
|
||||||
|
)
|
||||||
|
usage["cost_status"] = cost.status
|
||||||
|
if cost.amount_usd is not None:
|
||||||
|
usage["cost_usd"] = float(cost.amount_usd)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
return usage
|
return usage
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ const STARTUP_RESUME_ID = (process.env.HERMES_TUI_RESUME ?? '').trim()
|
|||||||
|
|
||||||
const LARGE_PASTE = { chars: 8000, lines: 80 }
|
const LARGE_PASTE = { chars: 8000, lines: 80 }
|
||||||
const EXCERPT = { chars: 1200, lines: 14 }
|
const EXCERPT = { chars: 1200, lines: 14 }
|
||||||
|
const MAX_HISTORY = 800
|
||||||
|
|
||||||
const SECRET_PATTERNS = [
|
const SECRET_PATTERNS = [
|
||||||
/AKIA[0-9A-Z]{16}/g,
|
/AKIA[0-9A-Z]{16}/g,
|
||||||
@@ -286,6 +287,8 @@ export function App({ gw }: { gw: GatewayClient }) {
|
|||||||
const pasteCounterRef = useRef(0)
|
const pasteCounterRef = useRef(0)
|
||||||
const colsRef = useRef(cols)
|
const colsRef = useRef(cols)
|
||||||
const turnToolsRef = useRef<string[]>([])
|
const turnToolsRef = useRef<string[]>([])
|
||||||
|
const statusTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
||||||
|
const onEventRef = useRef<(ev: GatewayEvent) => void>(() => {})
|
||||||
colsRef.current = cols
|
colsRef.current = cols
|
||||||
reasoningRef.current = reasoning
|
reasoningRef.current = reasoning
|
||||||
|
|
||||||
@@ -322,8 +325,15 @@ export function App({ gw }: { gw: GatewayClient }) {
|
|||||||
// ── Core actions ─────────────────────────────────────────────────
|
// ── Core actions ─────────────────────────────────────────────────
|
||||||
|
|
||||||
const appendMessage = useCallback((msg: Msg) => {
|
const appendMessage = useCallback((msg: Msg) => {
|
||||||
setMessages(prev => [...prev, msg])
|
const cap = (items: Msg[]) =>
|
||||||
setHistoryItems(prev => [...prev, msg])
|
items.length <= MAX_HISTORY
|
||||||
|
? items
|
||||||
|
: items[0]?.kind === 'intro'
|
||||||
|
? [items[0]!, ...items.slice(-(MAX_HISTORY - 1))]
|
||||||
|
: items.slice(-MAX_HISTORY)
|
||||||
|
|
||||||
|
setMessages(prev => cap([...prev, msg]))
|
||||||
|
setHistoryItems(prev => cap([...prev, msg]))
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const sys = useCallback((text: string) => appendMessage({ role: 'system' as const, text }), [appendMessage])
|
const sys = useCallback((text: string) => appendMessage({ role: 'system' as const, text }), [appendMessage])
|
||||||
@@ -378,6 +388,8 @@ export function App({ gw }: { gw: GatewayClient }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const resetSession = () => {
|
const resetSession = () => {
|
||||||
|
idle()
|
||||||
|
setReasoning('')
|
||||||
setSid(null as any) // will be set by caller
|
setSid(null as any) // will be set by caller
|
||||||
setHistoryItems([])
|
setHistoryItems([])
|
||||||
setMessages([])
|
setMessages([])
|
||||||
@@ -385,6 +397,7 @@ export function App({ gw }: { gw: GatewayClient }) {
|
|||||||
setActivity([])
|
setActivity([])
|
||||||
setBgTasks(new Set())
|
setBgTasks(new Set())
|
||||||
setUsage(ZERO)
|
setUsage(ZERO)
|
||||||
|
turnToolsRef.current = []
|
||||||
lastStatusNoteRef.current = ''
|
lastStatusNoteRef.current = ''
|
||||||
protocolWarnedRef.current = false
|
protocolWarnedRef.current = false
|
||||||
}
|
}
|
||||||
@@ -541,6 +554,11 @@ export function App({ gw }: { gw: GatewayClient }) {
|
|||||||
pushActivity(`redacted ${payload.redactions} secret-like value(s)`, 'warn')
|
pushActivity(`redacted ${payload.redactions} secret-like value(s)`, 'warn')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (statusTimerRef.current) {
|
||||||
|
clearTimeout(statusTimerRef.current)
|
||||||
|
statusTimerRef.current = null
|
||||||
|
}
|
||||||
|
|
||||||
inflightPasteIdsRef.current = payload.usedIds
|
inflightPasteIdsRef.current = payload.usedIds
|
||||||
setLastUserMsg(text)
|
setLastUserMsg(text)
|
||||||
appendMessage({ role: 'user', text })
|
appendMessage({ role: 'user', text })
|
||||||
@@ -855,7 +873,15 @@ export function App({ gw }: { gw: GatewayClient }) {
|
|||||||
setActivity([])
|
setActivity([])
|
||||||
turnToolsRef.current = []
|
turnToolsRef.current = []
|
||||||
setStatus('interrupted')
|
setStatus('interrupted')
|
||||||
setTimeout(() => setStatus('ready'), 1500)
|
|
||||||
|
if (statusTimerRef.current) {
|
||||||
|
clearTimeout(statusTimerRef.current)
|
||||||
|
}
|
||||||
|
|
||||||
|
statusTimerRef.current = setTimeout(() => {
|
||||||
|
statusTimerRef.current = null
|
||||||
|
setStatus('ready')
|
||||||
|
}, 1500)
|
||||||
} else if (input || inputBuf.length) {
|
} else if (input || inputBuf.length) {
|
||||||
clearIn()
|
clearIn()
|
||||||
} else {
|
} else {
|
||||||
@@ -1077,7 +1103,7 @@ export function App({ gw }: { gw: GatewayClient }) {
|
|||||||
case 'btw.complete':
|
case 'btw.complete':
|
||||||
setBgTasks(prev => {
|
setBgTasks(prev => {
|
||||||
const next = new Set(prev)
|
const next = new Set(prev)
|
||||||
next.delete(`btw:${p.task_id ?? 'x'}`)
|
next.delete('btw:x')
|
||||||
|
|
||||||
return next
|
return next
|
||||||
})
|
})
|
||||||
@@ -1096,6 +1122,8 @@ export function App({ gw }: { gw: GatewayClient }) {
|
|||||||
const wasInterrupted = interruptedRef.current
|
const wasInterrupted = interruptedRef.current
|
||||||
const savedReasoning = reasoningRef.current.trim()
|
const savedReasoning = reasoningRef.current.trim()
|
||||||
const savedTools = [...turnToolsRef.current]
|
const savedTools = [...turnToolsRef.current]
|
||||||
|
const finalText = (p?.rendered ?? p?.text ?? buf.current).trimStart()
|
||||||
|
|
||||||
idle()
|
idle()
|
||||||
setReasoning('')
|
setReasoning('')
|
||||||
setStreaming('')
|
setStreaming('')
|
||||||
@@ -1108,7 +1136,7 @@ export function App({ gw }: { gw: GatewayClient }) {
|
|||||||
if (!wasInterrupted) {
|
if (!wasInterrupted) {
|
||||||
appendMessage({
|
appendMessage({
|
||||||
role: 'assistant',
|
role: 'assistant',
|
||||||
text: (p?.rendered ?? p?.text ?? buf.current).trimStart(),
|
text: finalText,
|
||||||
thinking: savedReasoning || undefined,
|
thinking: savedReasoning || undefined,
|
||||||
tools: savedTools.length ? savedTools : undefined
|
tools: savedTools.length ? savedTools : undefined
|
||||||
})
|
})
|
||||||
@@ -1152,20 +1180,24 @@ export function App({ gw }: { gw: GatewayClient }) {
|
|||||||
[appendMessage, dequeue, newSession, pushActivity, send, sys]
|
[appendMessage, dequeue, newSession, pushActivity, send, sys]
|
||||||
)
|
)
|
||||||
|
|
||||||
const onExit = useCallback(() => {
|
onEventRef.current = onEvent
|
||||||
setStatus('gateway exited')
|
|
||||||
exit()
|
|
||||||
}, [exit])
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
gw.on('event', onEvent)
|
const handler = (ev: GatewayEvent) => onEventRef.current(ev)
|
||||||
gw.on('exit', onExit)
|
|
||||||
|
const exitHandler = () => {
|
||||||
|
setStatus('gateway exited')
|
||||||
|
exit()
|
||||||
|
}
|
||||||
|
|
||||||
|
gw.on('event', handler)
|
||||||
|
gw.on('exit', exitHandler)
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
gw.off('event', onEvent)
|
gw.off('event', handler)
|
||||||
gw.off('exit', onExit)
|
gw.off('exit', exitHandler)
|
||||||
}
|
}
|
||||||
}, [gw, onEvent, onExit])
|
}, [gw, exit])
|
||||||
|
|
||||||
// ── Slash commands ───────────────────────────────────────────────
|
// ── Slash commands ───────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -1505,8 +1537,36 @@ export function App({ gw }: { gw: GatewayClient }) {
|
|||||||
setUsage({ input: r.input ?? 0, output: r.output ?? 0, total: r.total ?? 0, calls: r.calls ?? 0 })
|
setUsage({ input: r.input ?? 0, output: r.output ?? 0, total: r.total ?? 0, calls: r.calls ?? 0 })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!r?.calls) {
|
||||||
|
sys('no API calls yet')
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const f = (v: number) => (v ?? 0).toLocaleString()
|
||||||
|
const ln = (k: string, v: string) => ` ${k.padEnd(26)}${v.padStart(10)}`
|
||||||
|
const hr = ` ${'─'.repeat(36)}`
|
||||||
|
|
||||||
|
const cost =
|
||||||
|
r.cost_usd != null ? `${r.cost_status === 'estimated' ? '~' : ''}$${r.cost_usd.toFixed(4)}` : null
|
||||||
|
|
||||||
sys(
|
sys(
|
||||||
`${fmtK(r?.input ?? 0)} in · ${fmtK(r?.output ?? 0)} out · ${fmtK(r?.total ?? 0)} total · ${r?.calls ?? 0} calls`
|
[
|
||||||
|
hr,
|
||||||
|
ln('Model:', r.model ?? ''),
|
||||||
|
ln('Input tokens:', f(r.input)),
|
||||||
|
ln('Cache read tokens:', f(r.cache_read)),
|
||||||
|
ln('Cache write tokens:', f(r.cache_write)),
|
||||||
|
ln('Output tokens:', f(r.output)),
|
||||||
|
ln('Total tokens:', f(r.total)),
|
||||||
|
ln('API calls:', f(r.calls)),
|
||||||
|
cost && ln('Cost:', cost),
|
||||||
|
hr,
|
||||||
|
r.context_max && ` Context: ${f(r.context_used)} / ${f(r.context_max)} (${r.context_percent}%)`,
|
||||||
|
r.compressions && ` Compressions: ${r.compressions}`
|
||||||
|
]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join('\n')
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -1634,7 +1694,15 @@ export function App({ gw }: { gw: GatewayClient }) {
|
|||||||
setActivity([])
|
setActivity([])
|
||||||
turnToolsRef.current = []
|
turnToolsRef.current = []
|
||||||
setStatus('interrupted')
|
setStatus('interrupted')
|
||||||
setTimeout(() => setStatus('ready'), 1500)
|
|
||||||
|
if (statusTimerRef.current) {
|
||||||
|
clearTimeout(statusTimerRef.current)
|
||||||
|
}
|
||||||
|
|
||||||
|
statusTimerRef.current = setTimeout(() => {
|
||||||
|
statusTimerRef.current = null
|
||||||
|
setStatus('ready')
|
||||||
|
}, 1500)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,16 +48,22 @@ interface Props {
|
|||||||
|
|
||||||
export function TextInput({ value, onChange, onPaste, onSubmit, placeholder = '', focus = true }: Props) {
|
export function TextInput({ value, onChange, onPaste, onSubmit, placeholder = '', focus = true }: Props) {
|
||||||
const [cur, setCur] = useState(value.length)
|
const [cur, setCur] = useState(value.length)
|
||||||
|
|
||||||
const curRef = useRef(cur)
|
const curRef = useRef(cur)
|
||||||
const vRef = useRef(value)
|
const vRef = useRef(value)
|
||||||
const selfChange = useRef(false)
|
const selfChange = useRef(false)
|
||||||
const pasteBuf = useRef('')
|
const pasteBuf = useRef('')
|
||||||
const pasteTimer = useRef<ReturnType<typeof setTimeout> | null>(null)
|
const pasteTimer = useRef<ReturnType<typeof setTimeout> | null>(null)
|
||||||
const pastePos = useRef(0)
|
const pastePos = useRef(0)
|
||||||
const undo = useRef<Array<{ cursor: number; value: string }>>([])
|
const undoStack = useRef<Array<{ cursor: number; value: string }>>([])
|
||||||
const redo = useRef<Array<{ cursor: number; value: string }>>([])
|
const redoStack = useRef<Array<{ cursor: number; value: string }>>([])
|
||||||
curRef.current = cur
|
|
||||||
vRef.current = value
|
const onChangeRef = useRef(onChange)
|
||||||
|
const onSubmitRef = useRef(onSubmit)
|
||||||
|
const onPasteRef = useRef(onPaste)
|
||||||
|
onChangeRef.current = onChange
|
||||||
|
onSubmitRef.current = onSubmit
|
||||||
|
onPasteRef.current = onPaste
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (selfChange.current) {
|
if (selfChange.current) {
|
||||||
@@ -65,36 +71,58 @@ export function TextInput({ value, onChange, onPaste, onSubmit, placeholder = ''
|
|||||||
} else {
|
} else {
|
||||||
setCur(value.length)
|
setCur(value.length)
|
||||||
curRef.current = value.length
|
curRef.current = value.length
|
||||||
undo.current = []
|
vRef.current = value
|
||||||
redo.current = []
|
undoStack.current = []
|
||||||
|
redoStack.current = []
|
||||||
}
|
}
|
||||||
}, [value])
|
}, [value])
|
||||||
|
|
||||||
const commit = (nextValue: string, nextCursor: number, track = true) => {
|
useEffect(
|
||||||
const currentValue = vRef.current
|
() => () => {
|
||||||
const currentCursor = curRef.current
|
if (pasteTimer.current) {
|
||||||
const c = Math.max(0, Math.min(nextCursor, nextValue.length))
|
clearTimeout(pasteTimer.current)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
|
||||||
if (track && nextValue !== currentValue) {
|
// ── Buffer ops (synchronous, ref-based — no stale closures) ─────
|
||||||
undo.current.push({ cursor: currentCursor, value: currentValue })
|
|
||||||
|
|
||||||
if (undo.current.length > 200) {
|
const commit = (next: string, nextCur: number, track = true) => {
|
||||||
undo.current.shift()
|
const prev = vRef.current
|
||||||
|
const c = Math.max(0, Math.min(nextCur, next.length))
|
||||||
|
|
||||||
|
if (track && next !== prev) {
|
||||||
|
undoStack.current.push({ cursor: curRef.current, value: prev })
|
||||||
|
|
||||||
|
if (undoStack.current.length > 200) {
|
||||||
|
undoStack.current.shift()
|
||||||
}
|
}
|
||||||
|
|
||||||
redo.current = []
|
redoStack.current = []
|
||||||
}
|
}
|
||||||
|
|
||||||
setCur(c)
|
setCur(c)
|
||||||
curRef.current = c
|
curRef.current = c
|
||||||
vRef.current = nextValue
|
vRef.current = next
|
||||||
|
|
||||||
if (nextValue !== currentValue) {
|
if (next !== prev) {
|
||||||
selfChange.current = true
|
selfChange.current = true
|
||||||
onChange(nextValue)
|
onChangeRef.current(next)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const swap = (from: typeof undoStack, to: typeof redoStack) => {
|
||||||
|
const entry = from.current.pop()
|
||||||
|
|
||||||
|
if (!entry) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
to.current.push({ cursor: curRef.current, value: vRef.current })
|
||||||
|
commit(entry.value, entry.cursor, false)
|
||||||
|
}
|
||||||
|
|
||||||
const flushPaste = () => {
|
const flushPaste = () => {
|
||||||
const pasted = pasteBuf.current
|
const pasted = pasteBuf.current
|
||||||
const at = pastePos.current
|
const at = pastePos.current
|
||||||
@@ -105,20 +133,20 @@ export function TextInput({ value, onChange, onPaste, onSubmit, placeholder = ''
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentValue = vRef.current
|
const v = vRef.current
|
||||||
const handled = onPaste?.({ cursor: at, text: pasted, value: currentValue })
|
const handled = onPasteRef.current?.({ cursor: at, text: pasted, value: v })
|
||||||
|
|
||||||
if (handled) {
|
if (handled) {
|
||||||
commit(handled.value, handled.cursor)
|
return commit(handled.value, handled.cursor)
|
||||||
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pasted.length && PRINTABLE.test(pasted)) {
|
if (PRINTABLE.test(pasted)) {
|
||||||
commit(currentValue.slice(0, at) + pasted + currentValue.slice(at), at + pasted.length)
|
commit(v.slice(0, at) + pasted + v.slice(at), at + pasted.length)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Input handler (reads only from refs) ────────────────────────
|
||||||
|
|
||||||
useInput(
|
useInput(
|
||||||
(inp, k) => {
|
(inp, k) => {
|
||||||
if (
|
if (
|
||||||
@@ -136,42 +164,24 @@ export function TextInput({ value, onChange, onPaste, onSubmit, placeholder = ''
|
|||||||
|
|
||||||
if (k.return) {
|
if (k.return) {
|
||||||
if (k.shift || k.meta) {
|
if (k.shift || k.meta) {
|
||||||
commit(value.slice(0, cur) + '\n' + value.slice(cur), cur + 1)
|
commit(vRef.current.slice(0, curRef.current) + '\n' + vRef.current.slice(curRef.current), curRef.current + 1)
|
||||||
} else {
|
} else {
|
||||||
onSubmit?.(value)
|
onSubmitRef.current?.(vRef.current)
|
||||||
}
|
}
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let c = cur
|
let c = curRef.current
|
||||||
let v = value
|
let v = vRef.current
|
||||||
const mod = k.ctrl || k.meta
|
const mod = k.ctrl || k.meta
|
||||||
|
|
||||||
if (k.ctrl && inp === 'z') {
|
if (k.ctrl && inp === 'z') {
|
||||||
const prev = undo.current.pop()
|
return swap(undoStack, redoStack)
|
||||||
|
|
||||||
if (!prev) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
redo.current.push({ cursor: curRef.current, value: vRef.current })
|
|
||||||
commit(prev.value, prev.cursor, false)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((k.ctrl && inp === 'y') || (k.meta && k.shift && inp === 'z')) {
|
if ((k.ctrl && inp === 'y') || (k.meta && k.shift && inp === 'z')) {
|
||||||
const next = redo.current.pop()
|
return swap(redoStack, undoStack)
|
||||||
|
|
||||||
if (!next) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
undo.current.push({ cursor: curRef.current, value: vRef.current })
|
|
||||||
commit(next.value, next.cursor, false)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (k.home || (k.ctrl && inp === 'a')) {
|
if (k.home || (k.ctrl && inp === 'a')) {
|
||||||
@@ -212,22 +222,18 @@ export function TextInput({ value, onChange, onPaste, onSubmit, placeholder = ''
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (raw === '\n') {
|
if (raw === '\n') {
|
||||||
commit(v.slice(0, c) + '\n' + v.slice(c), c + 1)
|
return commit(v.slice(0, c) + '\n' + v.slice(c), c + 1)
|
||||||
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (raw.length > 1 || raw.includes('\n')) {
|
if (raw.length > 1 || raw.includes('\n')) {
|
||||||
if (!pasteBuf.current) {
|
if (!pasteBuf.current) {
|
||||||
pastePos.current = c
|
pastePos.current = c
|
||||||
}
|
}
|
||||||
|
|
||||||
pasteBuf.current += raw
|
pasteBuf.current += raw
|
||||||
|
|
||||||
if (pasteTimer.current) {
|
if (pasteTimer.current) {
|
||||||
clearTimeout(pasteTimer.current)
|
clearTimeout(pasteTimer.current)
|
||||||
}
|
}
|
||||||
|
|
||||||
pasteTimer.current = setTimeout(flushPaste, 50)
|
pasteTimer.current = setTimeout(flushPaste, 50)
|
||||||
|
|
||||||
return
|
return
|
||||||
@@ -248,6 +254,8 @@ export function TextInput({ value, onChange, onPaste, onSubmit, placeholder = ''
|
|||||||
{ isActive: focus }
|
{ isActive: focus }
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// ── Render ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
if (!focus) {
|
if (!focus) {
|
||||||
return <Text>{value || (placeholder ? DIM + placeholder + DIM_OFF : '')}</Text>
|
return <Text>{value || (placeholder ? DIM + placeholder + DIM_OFF : '')}</Text>
|
||||||
}
|
}
|
||||||
@@ -256,15 +264,9 @@ export function TextInput({ value, onChange, onPaste, onSubmit, placeholder = ''
|
|||||||
return <Text>{INV + (placeholder[0] ?? ' ') + INV_OFF + DIM + placeholder.slice(1) + DIM_OFF}</Text>
|
return <Text>{INV + (placeholder[0] ?? ' ') + INV_OFF + DIM + placeholder.slice(1) + DIM_OFF}</Text>
|
||||||
}
|
}
|
||||||
|
|
||||||
let r = ''
|
const rendered =
|
||||||
|
[...value].map((ch, i) => (i === cur ? INV + ch + INV_OFF : ch)).join('') +
|
||||||
|
(cur === value.length ? INV + ' ' + INV_OFF : '')
|
||||||
|
|
||||||
for (let i = 0; i < value.length; i++) {
|
return <Text>{rendered}</Text>
|
||||||
r += i === cur ? INV + value[i] + INV_OFF : value[i]
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cur === value.length) {
|
|
||||||
r += INV + ' ' + INV_OFF
|
|
||||||
}
|
|
||||||
|
|
||||||
return <Text>{r}</Text>
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,4 +11,4 @@ if (!process.stdin.isTTY) {
|
|||||||
|
|
||||||
const gw = new GatewayClient()
|
const gw = new GatewayClient()
|
||||||
gw.start()
|
gw.start()
|
||||||
render(<App gw={gw} />, { exitOnCtrlC: false })
|
render(<App gw={gw} />, { exitOnCtrlC: false, maxFps: 60 })
|
||||||
|
|||||||
Reference in New Issue
Block a user