feat: show thinking indicator while inferencing
This commit is contained in:
@@ -58,6 +58,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 MAX_HISTORY = 800
|
||||||
|
const REASONING_PULSE_MS = 700
|
||||||
|
|
||||||
const SECRET_PATTERNS = [
|
const SECRET_PATTERNS = [
|
||||||
/AKIA[0-9A-Z]{16}/g,
|
/AKIA[0-9A-Z]{16}/g,
|
||||||
@@ -337,6 +338,7 @@ export function App({ gw }: { gw: GatewayClient }) {
|
|||||||
const [secret, setSecret] = useState<SecretReq | null>(null)
|
const [secret, setSecret] = useState<SecretReq | null>(null)
|
||||||
const [picker, setPicker] = useState(false)
|
const [picker, setPicker] = useState(false)
|
||||||
const [reasoning, setReasoning] = useState('')
|
const [reasoning, setReasoning] = useState('')
|
||||||
|
const [reasoningStreaming, setReasoningStreaming] = useState(false)
|
||||||
const [statusBar, setStatusBar] = useState(true)
|
const [statusBar, setStatusBar] = useState(true)
|
||||||
const [lastUserMsg, setLastUserMsg] = useState('')
|
const [lastUserMsg, setLastUserMsg] = useState('')
|
||||||
const [pastes, setPastes] = useState<PendingPaste[]>([])
|
const [pastes, setPastes] = useState<PendingPaste[]>([])
|
||||||
@@ -370,6 +372,7 @@ export function App({ gw }: { gw: GatewayClient }) {
|
|||||||
const colsRef = useRef(cols)
|
const colsRef = useRef(cols)
|
||||||
const turnToolsRef = useRef<string[]>([])
|
const turnToolsRef = useRef<string[]>([])
|
||||||
const persistedToolLabelsRef = useRef<Set<string>>(new Set())
|
const persistedToolLabelsRef = useRef<Set<string>>(new Set())
|
||||||
|
const reasoningStreamingTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
||||||
const statusTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
const statusTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
||||||
const busyRef = useRef(busy)
|
const busyRef = useRef(busy)
|
||||||
const onEventRef = useRef<(ev: GatewayEvent) => void>(() => {})
|
const onEventRef = useRef<(ev: GatewayEvent) => void>(() => {})
|
||||||
@@ -386,6 +389,36 @@ export function App({ gw }: { gw: GatewayClient }) {
|
|||||||
const { historyRef, historyIdx, setHistoryIdx, historyDraftRef, pushHistory } = useInputHistory()
|
const { historyRef, historyIdx, setHistoryIdx, historyDraftRef, pushHistory } = useInputHistory()
|
||||||
const { completions, compIdx, setCompIdx, compReplace } = useCompletion(input, blocked(), gw)
|
const { completions, compIdx, setCompIdx, compReplace } = useCompletion(input, blocked(), gw)
|
||||||
|
|
||||||
|
const pulseReasoningStreaming = useCallback(() => {
|
||||||
|
if (reasoningStreamingTimerRef.current) {
|
||||||
|
clearTimeout(reasoningStreamingTimerRef.current)
|
||||||
|
}
|
||||||
|
|
||||||
|
setReasoningStreaming(true)
|
||||||
|
reasoningStreamingTimerRef.current = setTimeout(() => {
|
||||||
|
reasoningStreamingTimerRef.current = null
|
||||||
|
setReasoningStreaming(false)
|
||||||
|
}, REASONING_PULSE_MS)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const clearReasoningStreaming = useCallback(() => {
|
||||||
|
if (reasoningStreamingTimerRef.current) {
|
||||||
|
clearTimeout(reasoningStreamingTimerRef.current)
|
||||||
|
reasoningStreamingTimerRef.current = null
|
||||||
|
}
|
||||||
|
|
||||||
|
setReasoningStreaming(false)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
useEffect(
|
||||||
|
() => () => {
|
||||||
|
if (reasoningStreamingTimerRef.current) {
|
||||||
|
clearTimeout(reasoningStreamingTimerRef.current)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
|
||||||
function blocked() {
|
function blocked() {
|
||||||
return !!(clarify || approval || pasteReview || picker || secret || sudo || pager)
|
return !!(clarify || approval || pasteReview || picker || secret || sudo || pager)
|
||||||
}
|
}
|
||||||
@@ -1356,6 +1389,7 @@ export function App({ gw }: { gw: GatewayClient }) {
|
|||||||
case 'reasoning.delta':
|
case 'reasoning.delta':
|
||||||
if (p?.text) {
|
if (p?.text) {
|
||||||
setReasoning(prev => prev + p.text)
|
setReasoning(prev => prev + p.text)
|
||||||
|
pulseReasoningStreaming()
|
||||||
}
|
}
|
||||||
|
|
||||||
break
|
break
|
||||||
@@ -1382,6 +1416,7 @@ export function App({ gw }: { gw: GatewayClient }) {
|
|||||||
|
|
||||||
case 'tool.start':
|
case 'tool.start':
|
||||||
pruneTransient()
|
pruneTransient()
|
||||||
|
clearReasoningStreaming()
|
||||||
setTools(prev => [
|
setTools(prev => [
|
||||||
...prev,
|
...prev,
|
||||||
{ id: p.tool_id, name: p.name, context: (p.context as string) || '', startedAt: Date.now() }
|
{ id: p.tool_id, name: p.name, context: (p.context as string) || '', startedAt: Date.now() }
|
||||||
@@ -1472,6 +1507,7 @@ export function App({ gw }: { gw: GatewayClient }) {
|
|||||||
|
|
||||||
case 'message.delta':
|
case 'message.delta':
|
||||||
pruneTransient()
|
pruneTransient()
|
||||||
|
clearReasoningStreaming()
|
||||||
|
|
||||||
if (p?.text && !interruptedRef.current) {
|
if (p?.text && !interruptedRef.current) {
|
||||||
buf.current = p.rendered ?? buf.current + p.text
|
buf.current = p.rendered ?? buf.current + p.text
|
||||||
@@ -1492,6 +1528,7 @@ export function App({ gw }: { gw: GatewayClient }) {
|
|||||||
|
|
||||||
idle()
|
idle()
|
||||||
setReasoning('')
|
setReasoning('')
|
||||||
|
clearReasoningStreaming()
|
||||||
setStreaming('')
|
setStreaming('')
|
||||||
|
|
||||||
if (inflightPasteIdsRef.current.length) {
|
if (inflightPasteIdsRef.current.length) {
|
||||||
@@ -2447,6 +2484,7 @@ export function App({ gw }: { gw: GatewayClient }) {
|
|||||||
activity={busy ? activity : []}
|
activity={busy ? activity : []}
|
||||||
busy={busy && !streaming}
|
busy={busy && !streaming}
|
||||||
reasoning={reasoning}
|
reasoning={reasoning}
|
||||||
|
reasoningStreaming={reasoningStreaming}
|
||||||
t={theme}
|
t={theme}
|
||||||
thinkingMode={hasReasoning ? thinkingMode : 'truncated'}
|
thinkingMode={hasReasoning ? thinkingMode : 'truncated'}
|
||||||
tools={tools}
|
tools={tools}
|
||||||
|
|||||||
@@ -56,15 +56,31 @@ function Detail({ color, content, dimColor }: DetailRow) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Streaming cursor ─────────────────────────────────────────────────
|
||||||
|
|
||||||
|
function StreamCursor({ active = false, color, dimColor }: { active?: boolean; color: string; dimColor?: boolean }) {
|
||||||
|
const [on, setOn] = useState(true)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const id = setInterval(() => setOn(v => !v), 420)
|
||||||
|
|
||||||
|
return () => clearInterval(id)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return <Text color={color} dimColor={dimColor}>{active && on ? '▍' : ' '}</Text>
|
||||||
|
}
|
||||||
|
|
||||||
// ── Thinking (pre-tool fallback) ─────────────────────────────────────
|
// ── Thinking (pre-tool fallback) ─────────────────────────────────────
|
||||||
|
|
||||||
export const Thinking = memo(function Thinking({
|
export const Thinking = memo(function Thinking({
|
||||||
mode = 'truncated',
|
mode = 'truncated',
|
||||||
reasoning,
|
reasoning,
|
||||||
|
streaming = false,
|
||||||
t
|
t
|
||||||
}: {
|
}: {
|
||||||
mode?: ThinkingMode
|
mode?: ThinkingMode
|
||||||
reasoning: string
|
reasoning: string
|
||||||
|
streaming?: boolean
|
||||||
t: Theme
|
t: Theme
|
||||||
}) {
|
}) {
|
||||||
const [tick, setTick] = useState(0)
|
const [tick, setTick] = useState(0)
|
||||||
@@ -88,6 +104,12 @@ export const Thinking = memo(function Thinking({
|
|||||||
<Text color={t.color.dim} dimColor {...(mode !== 'full' ? { wrap: 'truncate-end' as const } : {})}>
|
<Text color={t.color.dim} dimColor {...(mode !== 'full' ? { wrap: 'truncate-end' as const } : {})}>
|
||||||
<Text dimColor>└ </Text>
|
<Text dimColor>└ </Text>
|
||||||
{preview}
|
{preview}
|
||||||
|
<StreamCursor active={streaming} color={t.color.dim} dimColor />
|
||||||
|
</Text>
|
||||||
|
) : streaming ? (
|
||||||
|
<Text color={t.color.dim} dimColor>
|
||||||
|
<Text dimColor>└ </Text>
|
||||||
|
<StreamCursor active={streaming} color={t.color.dim} dimColor />
|
||||||
</Text>
|
</Text>
|
||||||
) : null}
|
) : null}
|
||||||
</Box>
|
</Box>
|
||||||
@@ -102,6 +124,7 @@ export const ToolTrail = memo(function ToolTrail({
|
|||||||
busy = false,
|
busy = false,
|
||||||
thinkingMode = 'truncated',
|
thinkingMode = 'truncated',
|
||||||
reasoning = '',
|
reasoning = '',
|
||||||
|
reasoningStreaming = false,
|
||||||
t,
|
t,
|
||||||
tools = [],
|
tools = [],
|
||||||
trail = [],
|
trail = [],
|
||||||
@@ -110,6 +133,7 @@ export const ToolTrail = memo(function ToolTrail({
|
|||||||
busy?: boolean
|
busy?: boolean
|
||||||
thinkingMode?: ThinkingMode
|
thinkingMode?: ThinkingMode
|
||||||
reasoning?: string
|
reasoning?: string
|
||||||
|
reasoningStreaming?: boolean
|
||||||
t: Theme
|
t: Theme
|
||||||
tools?: ActiveTool[]
|
tools?: ActiveTool[]
|
||||||
trail?: string[]
|
trail?: string[]
|
||||||
@@ -214,7 +238,24 @@ export const ToolTrail = memo(function ToolTrail({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (reasoningTail && groups.length) {
|
if (reasoningTail && groups.length) {
|
||||||
detail({ color: t.color.dim, content: reasoningTail, dimColor: true, key: 'cot' })
|
detail({
|
||||||
|
color: t.color.dim,
|
||||||
|
content: (
|
||||||
|
<>
|
||||||
|
{reasoningTail}
|
||||||
|
<StreamCursor active={reasoningStreaming} color={t.color.dim} dimColor />
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
dimColor: true,
|
||||||
|
key: 'cot'
|
||||||
|
})
|
||||||
|
} else if (reasoningStreaming && groups.length && thinkingMode === 'collapsed') {
|
||||||
|
detail({
|
||||||
|
color: t.color.dim,
|
||||||
|
content: <StreamCursor active={reasoningStreaming} color={t.color.dim} dimColor />,
|
||||||
|
dimColor: true,
|
||||||
|
key: 'cot'
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── activity → meta ─────────────────────────────────────────────
|
// ── activity → meta ─────────────────────────────────────────────
|
||||||
@@ -230,7 +271,7 @@ export const ToolTrail = memo(function ToolTrail({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Box flexDirection="column">
|
<Box flexDirection="column">
|
||||||
{busy && !groups.length && <Thinking mode={thinkingMode} reasoning={reasoning} t={t} />}
|
{busy && !groups.length && <Thinking mode={thinkingMode} reasoning={reasoning} streaming={reasoningStreaming} t={t} />}
|
||||||
{!busy && !groups.length && reasoningTail && (
|
{!busy && !groups.length && reasoningTail && (
|
||||||
<Detail color={t.color.dim} content={reasoningTail} dimColor key="cot" />
|
<Detail color={t.color.dim} content={reasoningTail} dimColor key="cot" />
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user