mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-04-25 00:51:20 +00:00
feat: show thinking indicator while inferencing
This commit is contained in:
parent
0fd33a98cd
commit
a2c0597ae4
2 changed files with 81 additions and 2 deletions
|
|
@ -58,6 +58,7 @@ const STARTUP_RESUME_ID = (process.env.HERMES_TUI_RESUME ?? '').trim()
|
|||
const LARGE_PASTE = { chars: 8000, lines: 80 }
|
||||
const EXCERPT = { chars: 1200, lines: 14 }
|
||||
const MAX_HISTORY = 800
|
||||
const REASONING_PULSE_MS = 700
|
||||
|
||||
const SECRET_PATTERNS = [
|
||||
/AKIA[0-9A-Z]{16}/g,
|
||||
|
|
@ -337,6 +338,7 @@ export function App({ gw }: { gw: GatewayClient }) {
|
|||
const [secret, setSecret] = useState<SecretReq | null>(null)
|
||||
const [picker, setPicker] = useState(false)
|
||||
const [reasoning, setReasoning] = useState('')
|
||||
const [reasoningStreaming, setReasoningStreaming] = useState(false)
|
||||
const [statusBar, setStatusBar] = useState(true)
|
||||
const [lastUserMsg, setLastUserMsg] = useState('')
|
||||
const [pastes, setPastes] = useState<PendingPaste[]>([])
|
||||
|
|
@ -370,6 +372,7 @@ export function App({ gw }: { gw: GatewayClient }) {
|
|||
const colsRef = useRef(cols)
|
||||
const turnToolsRef = useRef<string[]>([])
|
||||
const persistedToolLabelsRef = useRef<Set<string>>(new Set())
|
||||
const reasoningStreamingTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
||||
const statusTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
||||
const busyRef = useRef(busy)
|
||||
const onEventRef = useRef<(ev: GatewayEvent) => void>(() => {})
|
||||
|
|
@ -386,6 +389,36 @@ export function App({ gw }: { gw: GatewayClient }) {
|
|||
const { historyRef, historyIdx, setHistoryIdx, historyDraftRef, pushHistory } = useInputHistory()
|
||||
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() {
|
||||
return !!(clarify || approval || pasteReview || picker || secret || sudo || pager)
|
||||
}
|
||||
|
|
@ -1356,6 +1389,7 @@ export function App({ gw }: { gw: GatewayClient }) {
|
|||
case 'reasoning.delta':
|
||||
if (p?.text) {
|
||||
setReasoning(prev => prev + p.text)
|
||||
pulseReasoningStreaming()
|
||||
}
|
||||
|
||||
break
|
||||
|
|
@ -1382,6 +1416,7 @@ export function App({ gw }: { gw: GatewayClient }) {
|
|||
|
||||
case 'tool.start':
|
||||
pruneTransient()
|
||||
clearReasoningStreaming()
|
||||
setTools(prev => [
|
||||
...prev,
|
||||
{ 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':
|
||||
pruneTransient()
|
||||
clearReasoningStreaming()
|
||||
|
||||
if (p?.text && !interruptedRef.current) {
|
||||
buf.current = p.rendered ?? buf.current + p.text
|
||||
|
|
@ -1492,6 +1528,7 @@ export function App({ gw }: { gw: GatewayClient }) {
|
|||
|
||||
idle()
|
||||
setReasoning('')
|
||||
clearReasoningStreaming()
|
||||
setStreaming('')
|
||||
|
||||
if (inflightPasteIdsRef.current.length) {
|
||||
|
|
@ -2447,6 +2484,7 @@ export function App({ gw }: { gw: GatewayClient }) {
|
|||
activity={busy ? activity : []}
|
||||
busy={busy && !streaming}
|
||||
reasoning={reasoning}
|
||||
reasoningStreaming={reasoningStreaming}
|
||||
t={theme}
|
||||
thinkingMode={hasReasoning ? thinkingMode : 'truncated'}
|
||||
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) ─────────────────────────────────────
|
||||
|
||||
export const Thinking = memo(function Thinking({
|
||||
mode = 'truncated',
|
||||
reasoning,
|
||||
streaming = false,
|
||||
t
|
||||
}: {
|
||||
mode?: ThinkingMode
|
||||
reasoning: string
|
||||
streaming?: boolean
|
||||
t: Theme
|
||||
}) {
|
||||
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 dimColor>└ </Text>
|
||||
{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>
|
||||
) : null}
|
||||
</Box>
|
||||
|
|
@ -102,6 +124,7 @@ export const ToolTrail = memo(function ToolTrail({
|
|||
busy = false,
|
||||
thinkingMode = 'truncated',
|
||||
reasoning = '',
|
||||
reasoningStreaming = false,
|
||||
t,
|
||||
tools = [],
|
||||
trail = [],
|
||||
|
|
@ -110,6 +133,7 @@ export const ToolTrail = memo(function ToolTrail({
|
|||
busy?: boolean
|
||||
thinkingMode?: ThinkingMode
|
||||
reasoning?: string
|
||||
reasoningStreaming?: boolean
|
||||
t: Theme
|
||||
tools?: ActiveTool[]
|
||||
trail?: string[]
|
||||
|
|
@ -214,7 +238,24 @@ export const ToolTrail = memo(function ToolTrail({
|
|||
}
|
||||
|
||||
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 ─────────────────────────────────────────────
|
||||
|
|
@ -230,7 +271,7 @@ export const ToolTrail = memo(function ToolTrail({
|
|||
|
||||
return (
|
||||
<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 && (
|
||||
<Detail color={t.color.dim} content={reasoningTail} dimColor key="cot" />
|
||||
)}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue