diff --git a/ui-tui/src/app.tsx b/ui-tui/src/app.tsx index 45feafff5b..bdb1c82798 100644 --- a/ui-tui/src/app.tsx +++ b/ui-tui/src/app.tsx @@ -455,11 +455,16 @@ export function App({ gw }: { gw: GatewayClient }) { }) }, []) - const setTrail = (next: string[]) => { turnToolsRef.current = next; return next } + const setTrail = (next: string[]) => { + turnToolsRef.current = next + + return next + } const pruneTransient = useCallback(() => { setTurnTrail(prev => { const next = prev.filter(l => !isTransientTrailLine(l)) + return next.length === prev.length ? prev : setTrail(next) }) }, []) @@ -480,7 +485,9 @@ export function App({ gw }: { gw: GatewayClient }) { const answerClarify = useCallback( (answer: string) => { - if (!clarify) return + if (!clarify) { + return + } const label = TOOL_VERBS.clarify ?? 'clarify' @@ -491,7 +498,12 @@ export function App({ gw }: { gw: GatewayClient }) { if (answer) { persistedToolLabelsRef.current.add(label) - appendMessage({ role: 'system', text: '', kind: 'trail', tools: [buildToolTrailLine('clarify', clarify.question)] }) + appendMessage({ + role: 'system', + text: '', + kind: 'trail', + tools: [buildToolTrailLine('clarify', clarify.question)] + }) appendMessage({ role: 'user', text: answer }) } else { sys('prompt cancelled') @@ -1457,8 +1469,11 @@ export function App({ gw }: { gw: GatewayClient }) { const wasInterrupted = interruptedRef.current const savedReasoning = reasoningRef.current.trim() const persisted = persistedToolLabelsRef.current - const savedTools = turnToolsRef.current - .filter(l => isToolTrailResultLine(l) && ![...persisted].some(p => sameToolTrailGroup(p, l))) + + const savedTools = turnToolsRef.current.filter( + l => isToolTrailResultLine(l) && ![...persisted].some(p => sameToolTrailGroup(p, l)) + ) + const finalText = (p?.rendered ?? p?.text ?? buf.current).trimStart() idle() diff --git a/ui-tui/src/components/markdown.tsx b/ui-tui/src/components/markdown.tsx index 8d5cf888fe..6a59227734 100644 --- a/ui-tui/src/components/markdown.tsx +++ b/ui-tui/src/components/markdown.tsx @@ -10,11 +10,11 @@ const FOOTNOTE_RE = /^\[\^([^\]]+)\]:\s*(.*)$/ const DEF_RE = /^\s*:\s+(.+)$/ const TABLE_DIVIDER_CELL_RE = /^:?-{3,}:?$/ const MD_URL_RE = '((?:[^\\s()]|\\([^\\s()]*\\))+?)' -const INLINE_RE = - new RegExp( - `(!\\[(.*?)\\]\\(${MD_URL_RE}\\)|\\[(.+?)\\]\\(${MD_URL_RE}\\)|<((?:https?:\\/\\/|mailto:)[^>\\s]+|[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,})>|~~(.+?)~~|\`([^\\\`]+)\`|\\*\\*(.+?)\\*\\*|__(.+?)__|\\*(.+?)\\*|_(.+?)_|==(.+?)==|\\[\\^([^\\]]+)\\]|\\^([^^\\s][^^]*?)\\^|~([^~\\s][^~]*?)~|(https?:\\/\\/[^\\s<]+))`, - 'g' - ) + +const INLINE_RE = new RegExp( + `(!\\[(.*?)\\]\\(${MD_URL_RE}\\)|\\[(.+?)\\]\\(${MD_URL_RE}\\)|<((?:https?:\\/\\/|mailto:)[^>\\s]+|[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,})>|~~(.+?)~~|\`([^\\\`]+)\`|\\*\\*(.+?)\\*\\*|__(.+?)__|\\*(.+?)\\*|_(.+?)_|==(.+?)==|\\[\\^([^\\]]+)\\]|\\^([^^\\s][^^]*?)\\^|~([^~\\s][^~]*?)~|(https?:\\/\\/[^\\s<]+))`, + 'g' +) type Fence = { char: '`' | '~' @@ -171,11 +171,7 @@ function MdInline({ t, text }: { t: Theme; text: string }) { parts.push(renderAutolink(parts.length, t, url)) if (tail) { - parts.push( - - {tail} - - ) + parts.push({tail}) } } @@ -193,16 +189,8 @@ export function Md({ compact, t, text }: { compact?: boolean; t: Theme; text: st const lines = text.split('\n') const nodes: ReactNode[] = [] let i = 0 - let prevKind: - | 'blank' - | 'code' - | 'heading' - | 'list' - | 'paragraph' - | 'quote' - | 'rule' - | 'table' - | null = null + + let prevKind: 'blank' | 'code' | 'heading' | 'list' | 'paragraph' | 'quote' | 'rule' | 'table' | null = null const gap = () => { if (nodes.length && prevKind !== 'blank') { @@ -400,7 +388,7 @@ export function Md({ compact, t, text }: { compact?: boolean; t: Theme; text: st nodes.push( - · + · ) diff --git a/ui-tui/src/components/maskedPrompt.tsx b/ui-tui/src/components/maskedPrompt.tsx index c9e1100995..f159cc681f 100644 --- a/ui-tui/src/components/maskedPrompt.tsx +++ b/ui-tui/src/components/maskedPrompt.tsx @@ -2,6 +2,7 @@ import { Box, Text } from '@hermes/ink' import { useState } from 'react' import type { Theme } from '../theme.js' + import { TextInput } from './textInput.js' export function MaskedPrompt({ diff --git a/ui-tui/src/components/prompts.tsx b/ui-tui/src/components/prompts.tsx index 69a2bb8a85..4e546f3d8b 100644 --- a/ui-tui/src/components/prompts.tsx +++ b/ui-tui/src/components/prompts.tsx @@ -3,6 +3,7 @@ import { useState } from 'react' import type { Theme } from '../theme.js' import type { ApprovalReq, ClarifyReq } from '../types.js' + import { TextInput } from './textInput.js' export function ApprovalPrompt({ onChoice, req, t }: { onChoice: (s: string) => void; req: ApprovalReq; t: Theme }) { @@ -88,20 +89,31 @@ export function ClarifyPrompt({ useInput((ch, key) => { if (key.escape) { typing && choices.length ? setTyping(false) : onCancel() + return } - if (typing) return + if (typing) { + return + } - if (key.upArrow && sel > 0) setSel(s => s - 1) - if (key.downArrow && sel < choices.length) setSel(s => s + 1) + if (key.upArrow && sel > 0) { + setSel(s => s - 1) + } + + if (key.downArrow && sel < choices.length) { + setSel(s => s + 1) + } if (key.return) { sel === choices.length ? setTyping(true) : choices[sel] && onAnswer(choices[sel]!) } const n = parseInt(ch) - if (n >= 1 && n <= choices.length) onAnswer(choices[n - 1]!) + + if (n >= 1 && n <= choices.length) { + onAnswer(choices[n - 1]!) + } }) if (typing || !choices.length) { @@ -126,7 +138,9 @@ export function ClarifyPrompt({ {[...choices, 'Other (type your answer)'].map((c, i) => ( {sel === i ? '▸ ' : ' '} - {i + 1}. {c} + + {i + 1}. {c} + ))} diff --git a/ui-tui/src/lib/text.ts b/ui-tui/src/lib/text.ts index 3c5ccbcc7d..82a87c91f2 100644 --- a/ui-tui/src/lib/text.ts +++ b/ui-tui/src/lib/text.ts @@ -54,8 +54,7 @@ export const buildToolTrailLine = (name: string, context: string, error?: boolea export const isToolTrailResultLine = (line: string) => line.endsWith(' ✓') || line.endsWith(' ✗') /** Ephemeral status lines that should vanish once the next phase starts. */ -export const isTransientTrailLine = (line: string) => - line.startsWith('drafting ') || line === 'analyzing tool output…' +export const isTransientTrailLine = (line: string) => line.startsWith('drafting ') || line === 'analyzing tool output…' /** Whether a persisted/activity tool line belongs to the same tool label as a newer line. */ export const sameToolTrailGroup = (label: string, entry: string) =>