feat: add inline token count etc and fix venv
This commit is contained in:
@@ -865,6 +865,7 @@ def _launch_tui(resume_session_id: Optional[str] = None, tui_dev: bool = False):
|
|||||||
|
|
||||||
env = os.environ.copy()
|
env = os.environ.copy()
|
||||||
env["HERMES_PYTHON_SRC_ROOT"] = os.environ.get("HERMES_PYTHON_SRC_ROOT", str(PROJECT_ROOT))
|
env["HERMES_PYTHON_SRC_ROOT"] = os.environ.get("HERMES_PYTHON_SRC_ROOT", str(PROJECT_ROOT))
|
||||||
|
env.setdefault("HERMES_PYTHON", sys.executable)
|
||||||
env.setdefault("HERMES_CWD", os.getcwd())
|
env.setdefault("HERMES_CWD", os.getcwd())
|
||||||
if resume_session_id:
|
if resume_session_id:
|
||||||
env["HERMES_TUI_RESUME"] = resume_session_id
|
env["HERMES_TUI_RESUME"] = resume_session_id
|
||||||
|
|||||||
@@ -619,9 +619,13 @@ def _on_tool_progress(
|
|||||||
_args: dict | None = None,
|
_args: dict | None = None,
|
||||||
**_kwargs,
|
**_kwargs,
|
||||||
):
|
):
|
||||||
if not _tool_progress_enabled(sid) or event_type != "tool.started" or not name:
|
if not _tool_progress_enabled(sid):
|
||||||
return
|
return
|
||||||
_emit("tool.progress", sid, {"name": name, "preview": preview or ""})
|
if event_type == "tool.started" and name:
|
||||||
|
_emit("tool.progress", sid, {"name": name, "preview": preview or ""})
|
||||||
|
return
|
||||||
|
if event_type == "reasoning.available" and preview and _reasoning_visible(sid):
|
||||||
|
_emit("reasoning.available", sid, {"text": str(preview)})
|
||||||
|
|
||||||
|
|
||||||
def _agent_cbs(sid: str) -> dict:
|
def _agent_cbs(sid: str) -> dict:
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ The client entrypoint is `src/entry.tsx`. It exits early if `stdin` is not a TTY
|
|||||||
python -m tui_gateway.entry
|
python -m tui_gateway.entry
|
||||||
```
|
```
|
||||||
|
|
||||||
By default it uses `venv/bin/python` from the repo root. Set `HERMES_PYTHON` to override.
|
Interpreter resolution order is: `HERMES_PYTHON` → `PYTHON` → `$VIRTUAL_ENV/bin/python` → `./.venv/bin/python` → `./venv/bin/python` → `python3` (or `python` on Windows).
|
||||||
|
|
||||||
The transport is newline-delimited JSON-RPC over stdio:
|
The transport is newline-delimited JSON-RPC over stdio:
|
||||||
|
|
||||||
@@ -224,6 +224,7 @@ Primary event types the client handles today:
|
|||||||
| `message.complete` | `{ text, rendered?, usage, status }` |
|
| `message.complete` | `{ text, rendered?, usage, status }` |
|
||||||
| `thinking.delta` | `{ text }` |
|
| `thinking.delta` | `{ text }` |
|
||||||
| `reasoning.delta` | `{ text }` |
|
| `reasoning.delta` | `{ text }` |
|
||||||
|
| `reasoning.available` | `{ text }` |
|
||||||
| `status.update` | `{ kind, text }` |
|
| `status.update` | `{ kind, text }` |
|
||||||
| `tool.start` | `{ tool_id, name, context? }` |
|
| `tool.start` | `{ tool_id, name, context? }` |
|
||||||
| `tool.progress` | `{ name, preview }` |
|
| `tool.progress` | `{ name, preview }` |
|
||||||
|
|||||||
354
ui-tui/src/__tests__/createGatewayEventHandler.test.ts
Normal file
354
ui-tui/src/__tests__/createGatewayEventHandler.test.ts
Normal file
@@ -0,0 +1,354 @@
|
|||||||
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||||
|
|
||||||
|
import { createGatewayEventHandler } from '../app/createGatewayEventHandler.js'
|
||||||
|
import { resetOverlayState } from '../app/overlayStore.js'
|
||||||
|
import { resetUiState } from '../app/uiStore.js'
|
||||||
|
import { estimateTokensRough } from '../lib/text.js'
|
||||||
|
import type { Msg } from '../types.js'
|
||||||
|
|
||||||
|
const ref = <T>(current: T) => ({ current })
|
||||||
|
|
||||||
|
describe('createGatewayEventHandler', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
resetOverlayState()
|
||||||
|
resetUiState()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('persists completed tool rows when message.complete lands immediately after tool.complete', () => {
|
||||||
|
const appended: Msg[] = []
|
||||||
|
|
||||||
|
const state = {
|
||||||
|
activity: [] as unknown[],
|
||||||
|
reasoningTokens: 0,
|
||||||
|
streaming: '',
|
||||||
|
toolTokens: 0,
|
||||||
|
tools: [] as unknown[],
|
||||||
|
turnTrail: [] as string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const setTools = vi.fn((next: unknown) => {
|
||||||
|
if (typeof next !== 'function') {
|
||||||
|
state.tools = next as unknown[]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const setTurnTrail = vi.fn((next: unknown) => {
|
||||||
|
if (typeof next !== 'function') {
|
||||||
|
state.turnTrail = next as string[]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const refs = {
|
||||||
|
activeToolsRef: ref([] as { context?: string; id: string; name: string; startedAt?: number }[]),
|
||||||
|
bufRef: ref(''),
|
||||||
|
interruptedRef: ref(false),
|
||||||
|
lastStatusNoteRef: ref(''),
|
||||||
|
persistedToolLabelsRef: ref(new Set<string>()),
|
||||||
|
protocolWarnedRef: ref(false),
|
||||||
|
reasoningRef: ref('mapped the page'),
|
||||||
|
statusTimerRef: ref<ReturnType<typeof setTimeout> | null>(null),
|
||||||
|
toolTokenAccRef: ref(0),
|
||||||
|
toolCompleteRibbonRef: ref(null),
|
||||||
|
turnToolsRef: ref([] as string[])
|
||||||
|
}
|
||||||
|
|
||||||
|
const onEvent = createGatewayEventHandler({
|
||||||
|
composer: {
|
||||||
|
dequeue: () => undefined,
|
||||||
|
queueEditRef: ref<number | null>(null),
|
||||||
|
sendQueued: vi.fn()
|
||||||
|
},
|
||||||
|
gateway: {
|
||||||
|
gw: { request: vi.fn() } as any,
|
||||||
|
rpc: vi.fn(async () => null)
|
||||||
|
},
|
||||||
|
session: {
|
||||||
|
STARTUP_RESUME_ID: '',
|
||||||
|
colsRef: ref(80),
|
||||||
|
newSession: vi.fn(),
|
||||||
|
resetSession: vi.fn(),
|
||||||
|
setCatalog: vi.fn()
|
||||||
|
},
|
||||||
|
system: {
|
||||||
|
bellOnComplete: false,
|
||||||
|
sys: vi.fn()
|
||||||
|
},
|
||||||
|
transcript: {
|
||||||
|
appendMessage: (msg: Msg) => appended.push(msg),
|
||||||
|
setHistoryItems: vi.fn(),
|
||||||
|
setMessages: vi.fn()
|
||||||
|
},
|
||||||
|
turn: {
|
||||||
|
actions: {
|
||||||
|
clearReasoning: vi.fn(() => {
|
||||||
|
refs.reasoningRef.current = ''
|
||||||
|
refs.toolTokenAccRef.current = 0
|
||||||
|
state.toolTokens = 0
|
||||||
|
}),
|
||||||
|
endReasoningPhase: vi.fn(),
|
||||||
|
idle: vi.fn(() => {
|
||||||
|
refs.activeToolsRef.current = []
|
||||||
|
state.tools = []
|
||||||
|
}),
|
||||||
|
pruneTransient: vi.fn(),
|
||||||
|
pulseReasoningStreaming: vi.fn(),
|
||||||
|
pushActivity: vi.fn(),
|
||||||
|
pushTrail: vi.fn(),
|
||||||
|
scheduleReasoning: vi.fn(),
|
||||||
|
scheduleStreaming: vi.fn(),
|
||||||
|
setActivity: vi.fn(),
|
||||||
|
setReasoningTokens: vi.fn((next: number) => {
|
||||||
|
state.reasoningTokens = next
|
||||||
|
}),
|
||||||
|
setStreaming: vi.fn((next: string) => {
|
||||||
|
state.streaming = next
|
||||||
|
}),
|
||||||
|
setToolTokens: vi.fn((next: number) => {
|
||||||
|
state.toolTokens = next
|
||||||
|
}),
|
||||||
|
setTools,
|
||||||
|
setTurnTrail
|
||||||
|
},
|
||||||
|
refs
|
||||||
|
}
|
||||||
|
} as any)
|
||||||
|
|
||||||
|
onEvent({
|
||||||
|
payload: { context: 'home page', name: 'search', tool_id: 'tool-1' },
|
||||||
|
type: 'tool.start'
|
||||||
|
} as any)
|
||||||
|
onEvent({
|
||||||
|
payload: { name: 'search', preview: 'hero cards' },
|
||||||
|
type: 'tool.progress'
|
||||||
|
} as any)
|
||||||
|
onEvent({
|
||||||
|
payload: { summary: 'done', tool_id: 'tool-1' },
|
||||||
|
type: 'tool.complete'
|
||||||
|
} as any)
|
||||||
|
onEvent({
|
||||||
|
payload: { text: 'final answer' },
|
||||||
|
type: 'message.complete'
|
||||||
|
} as any)
|
||||||
|
|
||||||
|
expect(appended).toHaveLength(1)
|
||||||
|
expect(appended[0]).toMatchObject({
|
||||||
|
role: 'assistant',
|
||||||
|
text: 'final answer',
|
||||||
|
thinking: 'mapped the page'
|
||||||
|
})
|
||||||
|
expect(appended[0]?.tools).toHaveLength(1)
|
||||||
|
expect(appended[0]?.tools?.[0]).toContain('hero cards')
|
||||||
|
expect(appended[0]?.toolTokens).toBeGreaterThan(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('keeps tool tokens across handler recreation mid-turn', () => {
|
||||||
|
const appended: Msg[] = []
|
||||||
|
|
||||||
|
const state = {
|
||||||
|
activity: [] as unknown[],
|
||||||
|
reasoningTokens: 0,
|
||||||
|
streaming: '',
|
||||||
|
toolTokens: 0,
|
||||||
|
tools: [] as unknown[],
|
||||||
|
turnTrail: [] as string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const refs = {
|
||||||
|
activeToolsRef: ref([] as { context?: string; id: string; name: string; startedAt?: number }[]),
|
||||||
|
bufRef: ref(''),
|
||||||
|
interruptedRef: ref(false),
|
||||||
|
lastStatusNoteRef: ref(''),
|
||||||
|
persistedToolLabelsRef: ref(new Set<string>()),
|
||||||
|
protocolWarnedRef: ref(false),
|
||||||
|
reasoningRef: ref('mapped the page'),
|
||||||
|
statusTimerRef: ref<ReturnType<typeof setTimeout> | null>(null),
|
||||||
|
toolTokenAccRef: ref(0),
|
||||||
|
toolCompleteRibbonRef: ref(null),
|
||||||
|
turnToolsRef: ref([] as string[])
|
||||||
|
}
|
||||||
|
|
||||||
|
const buildHandler = () =>
|
||||||
|
createGatewayEventHandler({
|
||||||
|
composer: {
|
||||||
|
dequeue: () => undefined,
|
||||||
|
queueEditRef: ref<number | null>(null),
|
||||||
|
sendQueued: vi.fn()
|
||||||
|
},
|
||||||
|
gateway: {
|
||||||
|
gw: { request: vi.fn() } as any,
|
||||||
|
rpc: vi.fn(async () => null)
|
||||||
|
},
|
||||||
|
session: {
|
||||||
|
STARTUP_RESUME_ID: '',
|
||||||
|
colsRef: ref(80),
|
||||||
|
newSession: vi.fn(),
|
||||||
|
resetSession: vi.fn(),
|
||||||
|
setCatalog: vi.fn()
|
||||||
|
},
|
||||||
|
system: {
|
||||||
|
bellOnComplete: false,
|
||||||
|
sys: vi.fn()
|
||||||
|
},
|
||||||
|
transcript: {
|
||||||
|
appendMessage: (msg: Msg) => appended.push(msg),
|
||||||
|
setHistoryItems: vi.fn(),
|
||||||
|
setMessages: vi.fn()
|
||||||
|
},
|
||||||
|
turn: {
|
||||||
|
actions: {
|
||||||
|
clearReasoning: vi.fn(() => {
|
||||||
|
refs.reasoningRef.current = ''
|
||||||
|
refs.toolTokenAccRef.current = 0
|
||||||
|
state.toolTokens = 0
|
||||||
|
}),
|
||||||
|
endReasoningPhase: vi.fn(),
|
||||||
|
idle: vi.fn(() => {
|
||||||
|
refs.activeToolsRef.current = []
|
||||||
|
state.tools = []
|
||||||
|
}),
|
||||||
|
pruneTransient: vi.fn(),
|
||||||
|
pulseReasoningStreaming: vi.fn(),
|
||||||
|
pushActivity: vi.fn(),
|
||||||
|
pushTrail: vi.fn(),
|
||||||
|
scheduleReasoning: vi.fn(),
|
||||||
|
scheduleStreaming: vi.fn(),
|
||||||
|
setActivity: vi.fn(),
|
||||||
|
setReasoningTokens: vi.fn((next: number) => {
|
||||||
|
state.reasoningTokens = next
|
||||||
|
}),
|
||||||
|
setStreaming: vi.fn((next: string) => {
|
||||||
|
state.streaming = next
|
||||||
|
}),
|
||||||
|
setToolTokens: vi.fn((next: number) => {
|
||||||
|
state.toolTokens = next
|
||||||
|
}),
|
||||||
|
setTools: vi.fn((next: unknown) => {
|
||||||
|
if (typeof next !== 'function') {
|
||||||
|
state.tools = next as unknown[]
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
setTurnTrail: vi.fn((next: unknown) => {
|
||||||
|
if (typeof next !== 'function') {
|
||||||
|
state.turnTrail = next as string[]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
refs
|
||||||
|
}
|
||||||
|
} as any)
|
||||||
|
|
||||||
|
buildHandler()({
|
||||||
|
payload: { context: 'home page', name: 'search', tool_id: 'tool-1' },
|
||||||
|
type: 'tool.start'
|
||||||
|
} as any)
|
||||||
|
|
||||||
|
const onEvent = buildHandler()
|
||||||
|
|
||||||
|
onEvent({
|
||||||
|
payload: { name: 'search', preview: 'hero cards' },
|
||||||
|
type: 'tool.progress'
|
||||||
|
} as any)
|
||||||
|
onEvent({
|
||||||
|
payload: { summary: 'done', tool_id: 'tool-1' },
|
||||||
|
type: 'tool.complete'
|
||||||
|
} as any)
|
||||||
|
onEvent({
|
||||||
|
payload: { text: 'final answer' },
|
||||||
|
type: 'message.complete'
|
||||||
|
} as any)
|
||||||
|
|
||||||
|
expect(appended).toHaveLength(1)
|
||||||
|
expect(appended[0]?.tools).toHaveLength(1)
|
||||||
|
expect(appended[0]?.toolTokens).toBeGreaterThan(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('ignores fallback reasoning.available when streamed reasoning already exists', () => {
|
||||||
|
const appended: Msg[] = []
|
||||||
|
const streamed = 'short streamed reasoning'
|
||||||
|
const fallback = 'x'.repeat(400)
|
||||||
|
|
||||||
|
const refs = {
|
||||||
|
activeToolsRef: ref([] as { context?: string; id: string; name: string; startedAt?: number }[]),
|
||||||
|
bufRef: ref(''),
|
||||||
|
interruptedRef: ref(false),
|
||||||
|
lastStatusNoteRef: ref(''),
|
||||||
|
persistedToolLabelsRef: ref(new Set<string>()),
|
||||||
|
protocolWarnedRef: ref(false),
|
||||||
|
reasoningRef: ref(''),
|
||||||
|
statusTimerRef: ref<ReturnType<typeof setTimeout> | null>(null),
|
||||||
|
toolTokenAccRef: ref(0),
|
||||||
|
toolCompleteRibbonRef: ref(null),
|
||||||
|
turnToolsRef: ref([] as string[])
|
||||||
|
}
|
||||||
|
|
||||||
|
const onEvent = createGatewayEventHandler({
|
||||||
|
composer: {
|
||||||
|
dequeue: () => undefined,
|
||||||
|
queueEditRef: ref<number | null>(null),
|
||||||
|
sendQueued: vi.fn()
|
||||||
|
},
|
||||||
|
gateway: {
|
||||||
|
gw: { request: vi.fn() } as any,
|
||||||
|
rpc: vi.fn(async () => null)
|
||||||
|
},
|
||||||
|
session: {
|
||||||
|
STARTUP_RESUME_ID: '',
|
||||||
|
colsRef: ref(80),
|
||||||
|
newSession: vi.fn(),
|
||||||
|
resetSession: vi.fn(),
|
||||||
|
setCatalog: vi.fn()
|
||||||
|
},
|
||||||
|
system: {
|
||||||
|
bellOnComplete: false,
|
||||||
|
sys: vi.fn()
|
||||||
|
},
|
||||||
|
transcript: {
|
||||||
|
appendMessage: (msg: Msg) => appended.push(msg),
|
||||||
|
setHistoryItems: vi.fn(),
|
||||||
|
setMessages: vi.fn()
|
||||||
|
},
|
||||||
|
turn: {
|
||||||
|
actions: {
|
||||||
|
clearReasoning: vi.fn(() => {
|
||||||
|
refs.reasoningRef.current = ''
|
||||||
|
refs.toolTokenAccRef.current = 0
|
||||||
|
}),
|
||||||
|
endReasoningPhase: vi.fn(),
|
||||||
|
idle: vi.fn(() => {
|
||||||
|
refs.activeToolsRef.current = []
|
||||||
|
}),
|
||||||
|
pruneTransient: vi.fn(),
|
||||||
|
pulseReasoningStreaming: vi.fn(),
|
||||||
|
pushActivity: vi.fn(),
|
||||||
|
pushTrail: vi.fn(),
|
||||||
|
scheduleReasoning: vi.fn(),
|
||||||
|
scheduleStreaming: vi.fn(),
|
||||||
|
setActivity: vi.fn(),
|
||||||
|
setReasoningTokens: vi.fn(),
|
||||||
|
setStreaming: vi.fn(),
|
||||||
|
setToolTokens: vi.fn(),
|
||||||
|
setTools: vi.fn(),
|
||||||
|
setTurnTrail: vi.fn()
|
||||||
|
},
|
||||||
|
refs
|
||||||
|
}
|
||||||
|
} as any)
|
||||||
|
|
||||||
|
onEvent({
|
||||||
|
payload: { text: streamed },
|
||||||
|
type: 'reasoning.delta'
|
||||||
|
} as any)
|
||||||
|
onEvent({
|
||||||
|
payload: { text: fallback },
|
||||||
|
type: 'reasoning.available'
|
||||||
|
} as any)
|
||||||
|
onEvent({
|
||||||
|
payload: { text: 'final answer' },
|
||||||
|
type: 'message.complete'
|
||||||
|
} as any)
|
||||||
|
|
||||||
|
expect(appended).toHaveLength(1)
|
||||||
|
expect(appended[0]?.thinking).toBe(streamed)
|
||||||
|
expect(appended[0]?.thinkingTokens).toBe(estimateTokensRough(streamed))
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -48,14 +48,14 @@ describe('fmtK', () => {
|
|||||||
expect(fmtK(999)).toBe('999')
|
expect(fmtK(999)).toBe('999')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('formats thousands as K', () => {
|
it('formats thousands as lowercase k', () => {
|
||||||
expect(fmtK(1000)).toBe('1K')
|
expect(fmtK(1000)).toBe('1k')
|
||||||
expect(fmtK(1500)).toBe('1.5K')
|
expect(fmtK(1500)).toBe('1.5k')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('formats millions and billions', () => {
|
it('formats millions and billions with lowercase suffixes', () => {
|
||||||
expect(fmtK(1_000_000)).toBe('1M')
|
expect(fmtK(1_000_000)).toBe('1m')
|
||||||
expect(fmtK(1_000_000_000)).toBe('1B')
|
expect(fmtK(1_000_000_000)).toBe('1b')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -849,11 +849,14 @@ export function App({ gw }: { gw: GatewayClient }) {
|
|||||||
scheduleReasoning: turnActions.scheduleReasoning,
|
scheduleReasoning: turnActions.scheduleReasoning,
|
||||||
scheduleStreaming: turnActions.scheduleStreaming,
|
scheduleStreaming: turnActions.scheduleStreaming,
|
||||||
setActivity: turnActions.setActivity,
|
setActivity: turnActions.setActivity,
|
||||||
|
setReasoningTokens: turnActions.setReasoningTokens,
|
||||||
setStreaming: turnActions.setStreaming,
|
setStreaming: turnActions.setStreaming,
|
||||||
|
setToolTokens: turnActions.setToolTokens,
|
||||||
setTools: turnActions.setTools,
|
setTools: turnActions.setTools,
|
||||||
setTurnTrail: turnActions.setTurnTrail
|
setTurnTrail: turnActions.setTurnTrail
|
||||||
},
|
},
|
||||||
refs: {
|
refs: {
|
||||||
|
activeToolsRef: turnRefs.activeToolsRef,
|
||||||
bufRef: turnRefs.bufRef,
|
bufRef: turnRefs.bufRef,
|
||||||
interruptedRef: turnRefs.interruptedRef,
|
interruptedRef: turnRefs.interruptedRef,
|
||||||
lastStatusNoteRef: turnRefs.lastStatusNoteRef,
|
lastStatusNoteRef: turnRefs.lastStatusNoteRef,
|
||||||
@@ -861,6 +864,7 @@ export function App({ gw }: { gw: GatewayClient }) {
|
|||||||
protocolWarnedRef: turnRefs.protocolWarnedRef,
|
protocolWarnedRef: turnRefs.protocolWarnedRef,
|
||||||
reasoningRef: turnRefs.reasoningRef,
|
reasoningRef: turnRefs.reasoningRef,
|
||||||
statusTimerRef: turnRefs.statusTimerRef,
|
statusTimerRef: turnRefs.statusTimerRef,
|
||||||
|
toolTokenAccRef: turnRefs.toolTokenAccRef,
|
||||||
toolCompleteRibbonRef: turnRefs.toolCompleteRibbonRef,
|
toolCompleteRibbonRef: turnRefs.toolCompleteRibbonRef,
|
||||||
turnToolsRef: turnRefs.turnToolsRef
|
turnToolsRef: turnRefs.turnToolsRef
|
||||||
}
|
}
|
||||||
@@ -1014,16 +1018,7 @@ export function App({ gw }: { gw: GatewayClient }) {
|
|||||||
|
|
||||||
dispatchSubmission([...composerState.inputBuf, value].join('\n'))
|
dispatchSubmission([...composerState.inputBuf, value].join('\n'))
|
||||||
},
|
},
|
||||||
[
|
[appendMessage, composerActions, composerRefs, composerState, dispatchSubmission, gw, sys, turnActions]
|
||||||
appendMessage,
|
|
||||||
composerActions,
|
|
||||||
composerRefs,
|
|
||||||
composerState,
|
|
||||||
dispatchSubmission,
|
|
||||||
gw,
|
|
||||||
sys,
|
|
||||||
turnActions
|
|
||||||
]
|
|
||||||
)
|
)
|
||||||
|
|
||||||
submitRef.current = submit
|
submitRef.current = submit
|
||||||
@@ -1142,11 +1137,13 @@ export function App({ gw }: { gw: GatewayClient }) {
|
|||||||
progress={{
|
progress={{
|
||||||
activity: turnState.activity,
|
activity: turnState.activity,
|
||||||
reasoning: turnState.reasoning,
|
reasoning: turnState.reasoning,
|
||||||
|
reasoningTokens: turnState.reasoningTokens,
|
||||||
reasoningActive: turnState.reasoningActive,
|
reasoningActive: turnState.reasoningActive,
|
||||||
reasoningStreaming: turnState.reasoningStreaming,
|
reasoningStreaming: turnState.reasoningStreaming,
|
||||||
showProgressArea,
|
showProgressArea,
|
||||||
showStreamingArea,
|
showStreamingArea,
|
||||||
streaming: turnState.streaming,
|
streaming: turnState.streaming,
|
||||||
|
toolTokens: turnState.toolTokens,
|
||||||
tools: turnState.tools,
|
tools: turnState.tools,
|
||||||
turnTrail: turnState.turnTrail
|
turnTrail: turnState.turnTrail
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -1,72 +1,18 @@
|
|||||||
import type { Dispatch, MutableRefObject, SetStateAction } from 'react'
|
|
||||||
|
|
||||||
import type { GatewayEvent } from '../gatewayClient.js'
|
import type { GatewayEvent } from '../gatewayClient.js'
|
||||||
import { asRpcResult, rpcErrorMessage } from '../lib/rpc.js'
|
import { asRpcResult, rpcErrorMessage } from '../lib/rpc.js'
|
||||||
import { buildToolTrailLine, isToolTrailResultLine, sameToolTrailGroup, toolTrailLabel } from '../lib/text.js'
|
import {
|
||||||
|
buildToolTrailLine,
|
||||||
|
estimateTokensRough,
|
||||||
|
isToolTrailResultLine,
|
||||||
|
sameToolTrailGroup,
|
||||||
|
toolTrailLabel
|
||||||
|
} from '../lib/text.js'
|
||||||
import { fromSkin } from '../theme.js'
|
import { fromSkin } from '../theme.js'
|
||||||
import type { Msg, SlashCatalog } from '../types.js'
|
|
||||||
|
|
||||||
import { introMsg, toTranscriptMessages } from './helpers.js'
|
import { introMsg, toTranscriptMessages } from './helpers.js'
|
||||||
import type { GatewayServices } from './interfaces.js'
|
import type { GatewayEventHandlerContext } from './interfaces.js'
|
||||||
import { patchOverlayState } from './overlayStore.js'
|
import { patchOverlayState } from './overlayStore.js'
|
||||||
import { getUiState, patchUiState } from './uiStore.js'
|
import { getUiState, patchUiState } from './uiStore.js'
|
||||||
import type { TurnActions, TurnRefs } from './useTurnState.js'
|
|
||||||
|
|
||||||
export interface GatewayEventHandlerContext {
|
|
||||||
composer: {
|
|
||||||
dequeue: () => string | undefined
|
|
||||||
queueEditRef: MutableRefObject<number | null>
|
|
||||||
sendQueued: (text: string) => void
|
|
||||||
}
|
|
||||||
gateway: GatewayServices
|
|
||||||
session: {
|
|
||||||
STARTUP_RESUME_ID: string
|
|
||||||
colsRef: MutableRefObject<number>
|
|
||||||
newSession: (msg?: string) => void
|
|
||||||
resetSession: () => void
|
|
||||||
setCatalog: Dispatch<SetStateAction<SlashCatalog | null>>
|
|
||||||
}
|
|
||||||
system: {
|
|
||||||
bellOnComplete: boolean
|
|
||||||
stdout?: NodeJS.WriteStream
|
|
||||||
sys: (text: string) => void
|
|
||||||
}
|
|
||||||
transcript: {
|
|
||||||
appendMessage: (msg: Msg) => void
|
|
||||||
setHistoryItems: Dispatch<SetStateAction<Msg[]>>
|
|
||||||
setMessages: Dispatch<SetStateAction<Msg[]>>
|
|
||||||
}
|
|
||||||
turn: {
|
|
||||||
actions: Pick<
|
|
||||||
TurnActions,
|
|
||||||
| 'clearReasoning'
|
|
||||||
| 'endReasoningPhase'
|
|
||||||
| 'idle'
|
|
||||||
| 'pruneTransient'
|
|
||||||
| 'pulseReasoningStreaming'
|
|
||||||
| 'pushActivity'
|
|
||||||
| 'pushTrail'
|
|
||||||
| 'scheduleReasoning'
|
|
||||||
| 'scheduleStreaming'
|
|
||||||
| 'setActivity'
|
|
||||||
| 'setStreaming'
|
|
||||||
| 'setTools'
|
|
||||||
| 'setTurnTrail'
|
|
||||||
>
|
|
||||||
refs: Pick<
|
|
||||||
TurnRefs,
|
|
||||||
| 'bufRef'
|
|
||||||
| 'interruptedRef'
|
|
||||||
| 'lastStatusNoteRef'
|
|
||||||
| 'persistedToolLabelsRef'
|
|
||||||
| 'protocolWarnedRef'
|
|
||||||
| 'reasoningRef'
|
|
||||||
| 'statusTimerRef'
|
|
||||||
| 'toolCompleteRibbonRef'
|
|
||||||
| 'turnToolsRef'
|
|
||||||
>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createGatewayEventHandler(ctx: GatewayEventHandlerContext): (ev: GatewayEvent) => void {
|
export function createGatewayEventHandler(ctx: GatewayEventHandlerContext): (ev: GatewayEvent) => void {
|
||||||
const { dequeue, queueEditRef, sendQueued } = ctx.composer
|
const { dequeue, queueEditRef, sendQueued } = ctx.composer
|
||||||
@@ -86,12 +32,15 @@ export function createGatewayEventHandler(ctx: GatewayEventHandlerContext): (ev:
|
|||||||
scheduleReasoning,
|
scheduleReasoning,
|
||||||
scheduleStreaming,
|
scheduleStreaming,
|
||||||
setActivity,
|
setActivity,
|
||||||
|
setReasoningTokens,
|
||||||
setStreaming,
|
setStreaming,
|
||||||
|
setToolTokens,
|
||||||
setTools,
|
setTools,
|
||||||
setTurnTrail
|
setTurnTrail
|
||||||
} = ctx.turn.actions
|
} = ctx.turn.actions
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
activeToolsRef,
|
||||||
bufRef,
|
bufRef,
|
||||||
interruptedRef,
|
interruptedRef,
|
||||||
lastStatusNoteRef,
|
lastStatusNoteRef,
|
||||||
@@ -99,6 +48,7 @@ export function createGatewayEventHandler(ctx: GatewayEventHandlerContext): (ev:
|
|||||||
protocolWarnedRef,
|
protocolWarnedRef,
|
||||||
reasoningRef,
|
reasoningRef,
|
||||||
statusTimerRef,
|
statusTimerRef,
|
||||||
|
toolTokenAccRef,
|
||||||
toolCompleteRibbonRef,
|
toolCompleteRibbonRef,
|
||||||
turnToolsRef
|
turnToolsRef
|
||||||
} = ctx.turn.refs
|
} = ctx.turn.refs
|
||||||
@@ -210,8 +160,12 @@ export function createGatewayEventHandler(ctx: GatewayEventHandlerContext): (ev:
|
|||||||
clearReasoning()
|
clearReasoning()
|
||||||
setActivity([])
|
setActivity([])
|
||||||
setTurnTrail([])
|
setTurnTrail([])
|
||||||
|
activeToolsRef.current = []
|
||||||
|
setTools([])
|
||||||
turnToolsRef.current = []
|
turnToolsRef.current = []
|
||||||
persistedToolLabelsRef.current.clear()
|
persistedToolLabelsRef.current.clear()
|
||||||
|
toolTokenAccRef.current = 0
|
||||||
|
setToolTokens(0)
|
||||||
|
|
||||||
break
|
break
|
||||||
|
|
||||||
@@ -286,21 +240,46 @@ export function createGatewayEventHandler(ctx: GatewayEventHandlerContext): (ev:
|
|||||||
case 'reasoning.delta':
|
case 'reasoning.delta':
|
||||||
if (p?.text) {
|
if (p?.text) {
|
||||||
reasoningRef.current += p.text
|
reasoningRef.current += p.text
|
||||||
|
setReasoningTokens(estimateTokensRough(reasoningRef.current))
|
||||||
scheduleReasoning()
|
scheduleReasoning()
|
||||||
pulseReasoningStreaming()
|
pulseReasoningStreaming()
|
||||||
}
|
}
|
||||||
|
|
||||||
break
|
break
|
||||||
|
case 'reasoning.available': {
|
||||||
|
const incoming = String(p?.text ?? '').trim()
|
||||||
|
|
||||||
|
if (!incoming) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
const current = reasoningRef.current.trim()
|
||||||
|
|
||||||
|
// `reasoning.available` is a backend fallback preview that can arrive after
|
||||||
|
// streamed reasoning. Preserve the live-visible reasoning/counts if we
|
||||||
|
// already saw deltas; only hydrate from this event when streaming gave us
|
||||||
|
// nothing.
|
||||||
|
if (!current) {
|
||||||
|
reasoningRef.current = incoming
|
||||||
|
setReasoningTokens(estimateTokensRough(reasoningRef.current))
|
||||||
|
scheduleReasoning()
|
||||||
|
pulseReasoningStreaming()
|
||||||
|
}
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
case 'tool.progress':
|
case 'tool.progress':
|
||||||
if (p?.preview) {
|
if (p?.preview) {
|
||||||
setTools(prev => {
|
const index = activeToolsRef.current.findIndex(tool => tool.name === p.name)
|
||||||
const index = prev.findIndex(tool => tool.name === p.name)
|
|
||||||
|
|
||||||
return index >= 0
|
if (index >= 0) {
|
||||||
? [...prev.slice(0, index), { ...prev[index]!, context: p.preview as string }, ...prev.slice(index + 1)]
|
const next = [...activeToolsRef.current]
|
||||||
: prev
|
|
||||||
})
|
next[index] = { ...next[index]!, context: p.preview as string }
|
||||||
|
activeToolsRef.current = next
|
||||||
|
setTools(next)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
break
|
break
|
||||||
@@ -311,44 +290,47 @@ export function createGatewayEventHandler(ctx: GatewayEventHandlerContext): (ev:
|
|||||||
}
|
}
|
||||||
|
|
||||||
break
|
break
|
||||||
|
case 'tool.start': {
|
||||||
case 'tool.start':
|
|
||||||
pruneTransient()
|
pruneTransient()
|
||||||
endReasoningPhase()
|
endReasoningPhase()
|
||||||
setTools(prev => [
|
const ctx = (p.context as string) || ''
|
||||||
...prev,
|
const sample = `${String(p.name ?? '')} ${ctx}`.trim()
|
||||||
{ id: p.tool_id, name: p.name, context: (p.context as string) || '', startedAt: Date.now() }
|
toolTokenAccRef.current += sample ? estimateTokensRough(sample) : 0
|
||||||
])
|
setToolTokens(toolTokenAccRef.current)
|
||||||
|
activeToolsRef.current = [
|
||||||
|
...activeToolsRef.current,
|
||||||
|
{ id: p.tool_id, name: p.name, context: ctx, startedAt: Date.now() }
|
||||||
|
]
|
||||||
|
setTools(activeToolsRef.current)
|
||||||
|
|
||||||
break
|
break
|
||||||
|
}
|
||||||
|
|
||||||
case 'tool.complete': {
|
case 'tool.complete': {
|
||||||
toolCompleteRibbonRef.current = null
|
toolCompleteRibbonRef.current = null
|
||||||
setTools(prev => {
|
const done = activeToolsRef.current.find(tool => tool.id === p.tool_id)
|
||||||
const done = prev.find(tool => tool.id === p.tool_id)
|
const name = done?.name ?? p.name
|
||||||
const name = done?.name ?? p.name
|
const label = toolTrailLabel(name)
|
||||||
const label = toolTrailLabel(name)
|
|
||||||
|
|
||||||
const line = buildToolTrailLine(
|
const line = buildToolTrailLine(
|
||||||
name,
|
name,
|
||||||
done?.context || '',
|
done?.context || '',
|
||||||
!!p.error,
|
!!p.error,
|
||||||
(p.error as string) || (p.summary as string) || ''
|
(p.error as string) || (p.summary as string) || ''
|
||||||
)
|
)
|
||||||
|
|
||||||
const next = [...turnToolsRef.current.filter(item => !sameToolTrailGroup(label, item)), line]
|
const next = [...turnToolsRef.current.filter(item => !sameToolTrailGroup(label, item)), line]
|
||||||
const remaining = prev.filter(tool => tool.id !== p.tool_id)
|
|
||||||
|
|
||||||
toolCompleteRibbonRef.current = { label, line }
|
activeToolsRef.current = activeToolsRef.current.filter(tool => tool.id !== p.tool_id)
|
||||||
|
setTools(activeToolsRef.current)
|
||||||
|
toolCompleteRibbonRef.current = { label, line }
|
||||||
|
|
||||||
if (!remaining.length) {
|
if (!activeToolsRef.current.length) {
|
||||||
next.push('analyzing tool output…')
|
next.push('analyzing tool output…')
|
||||||
}
|
}
|
||||||
|
|
||||||
turnToolsRef.current = next.slice(-8)
|
turnToolsRef.current = next.slice(-8)
|
||||||
setTurnTrail(turnToolsRef.current)
|
setTurnTrail(turnToolsRef.current)
|
||||||
|
|
||||||
return remaining
|
|
||||||
})
|
|
||||||
|
|
||||||
if (p?.inline_diff) {
|
if (p?.inline_diff) {
|
||||||
sys(p.inline_diff as string)
|
sys(p.inline_diff as string)
|
||||||
@@ -419,6 +401,8 @@ export function createGatewayEventHandler(ctx: GatewayEventHandlerContext): (ev:
|
|||||||
const finalText = (p?.rendered ?? p?.text ?? bufRef.current).trimStart()
|
const finalText = (p?.rendered ?? p?.text ?? bufRef.current).trimStart()
|
||||||
const persisted = persistedToolLabelsRef.current
|
const persisted = persistedToolLabelsRef.current
|
||||||
const savedReasoning = reasoningRef.current.trim()
|
const savedReasoning = reasoningRef.current.trim()
|
||||||
|
const savedReasoningTokens = savedReasoning ? estimateTokensRough(savedReasoning) : 0
|
||||||
|
const savedToolTokens = toolTokenAccRef.current
|
||||||
|
|
||||||
const savedTools = turnToolsRef.current.filter(
|
const savedTools = turnToolsRef.current.filter(
|
||||||
line => isToolTrailResultLine(line) && ![...persisted].some(item => sameToolTrailGroup(item, line))
|
line => isToolTrailResultLine(line) && ![...persisted].some(item => sameToolTrailGroup(item, line))
|
||||||
@@ -426,15 +410,13 @@ export function createGatewayEventHandler(ctx: GatewayEventHandlerContext): (ev:
|
|||||||
|
|
||||||
const wasInterrupted = interruptedRef.current
|
const wasInterrupted = interruptedRef.current
|
||||||
|
|
||||||
idle()
|
|
||||||
clearReasoning()
|
|
||||||
setStreaming('')
|
|
||||||
|
|
||||||
if (!wasInterrupted) {
|
if (!wasInterrupted) {
|
||||||
appendMessage({
|
appendMessage({
|
||||||
role: 'assistant',
|
role: 'assistant',
|
||||||
text: finalText,
|
text: finalText,
|
||||||
thinking: savedReasoning || undefined,
|
thinking: savedReasoning || undefined,
|
||||||
|
thinkingTokens: savedReasoning ? savedReasoningTokens : undefined,
|
||||||
|
toolTokens: savedTools.length ? savedToolTokens : undefined,
|
||||||
tools: savedTools.length ? savedTools : undefined
|
tools: savedTools.length ? savedTools : undefined
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -443,6 +425,9 @@ export function createGatewayEventHandler(ctx: GatewayEventHandlerContext): (ev:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
idle()
|
||||||
|
clearReasoning()
|
||||||
|
|
||||||
turnToolsRef.current = []
|
turnToolsRef.current = []
|
||||||
persistedToolLabelsRef.current.clear()
|
persistedToolLabelsRef.current.clear()
|
||||||
setActivity([])
|
setActivity([])
|
||||||
|
|||||||
@@ -1,57 +1,14 @@
|
|||||||
import type { Dispatch, MutableRefObject, SetStateAction } from 'react'
|
|
||||||
|
|
||||||
import { HOTKEYS } from '../constants.js'
|
import { HOTKEYS } from '../constants.js'
|
||||||
import { writeOsc52Clipboard } from '../lib/osc52.js'
|
import { writeOsc52Clipboard } from '../lib/osc52.js'
|
||||||
import { asRpcResult, rpcErrorMessage } from '../lib/rpc.js'
|
import { asRpcResult, rpcErrorMessage } from '../lib/rpc.js'
|
||||||
import { fmtK } from '../lib/text.js'
|
import { fmtK } from '../lib/text.js'
|
||||||
import type { DetailsMode, Msg, PanelSection, SessionInfo, SlashCatalog } from '../types.js'
|
import type { DetailsMode, PanelSection } from '../types.js'
|
||||||
|
|
||||||
import { imageTokenMeta, introMsg, nextDetailsMode, parseDetailsMode, toTranscriptMessages } from './helpers.js'
|
import { imageTokenMeta, introMsg, nextDetailsMode, parseDetailsMode, toTranscriptMessages } from './helpers.js'
|
||||||
import type { GatewayServices } from './interfaces.js'
|
import type { SlashHandlerContext } from './interfaces.js'
|
||||||
import { patchOverlayState } from './overlayStore.js'
|
import { patchOverlayState } from './overlayStore.js'
|
||||||
import { getUiState, patchUiState } from './uiStore.js'
|
import { getUiState, patchUiState } from './uiStore.js'
|
||||||
|
|
||||||
export interface SlashHandlerContext {
|
|
||||||
composer: {
|
|
||||||
enqueue: (text: string) => void
|
|
||||||
hasSelection: boolean
|
|
||||||
paste: (quiet?: boolean) => void
|
|
||||||
queueRef: MutableRefObject<string[]>
|
|
||||||
selection: {
|
|
||||||
copySelection: () => string
|
|
||||||
}
|
|
||||||
setInput: Dispatch<SetStateAction<string>>
|
|
||||||
}
|
|
||||||
gateway: GatewayServices
|
|
||||||
local: {
|
|
||||||
catalog: SlashCatalog | null
|
|
||||||
lastUserMsg: string
|
|
||||||
maybeWarn: (value: any) => void
|
|
||||||
messages: Msg[]
|
|
||||||
}
|
|
||||||
session: {
|
|
||||||
closeSession: (targetSid?: string | null) => Promise<unknown>
|
|
||||||
die: () => void
|
|
||||||
guardBusySessionSwitch: (what?: string) => boolean
|
|
||||||
newSession: (msg?: string) => void
|
|
||||||
resetVisibleHistory: (info?: SessionInfo | null) => void
|
|
||||||
resumeById: (id: string) => void
|
|
||||||
setSessionStartedAt: Dispatch<SetStateAction<number>>
|
|
||||||
}
|
|
||||||
transcript: {
|
|
||||||
page: (text: string, title?: string) => void
|
|
||||||
panel: (title: string, sections: PanelSection[]) => void
|
|
||||||
send: (text: string) => void
|
|
||||||
setHistoryItems: Dispatch<SetStateAction<Msg[]>>
|
|
||||||
setMessages: Dispatch<SetStateAction<Msg[]>>
|
|
||||||
sys: (text: string) => void
|
|
||||||
trimLastExchange: (items: Msg[]) => Msg[]
|
|
||||||
}
|
|
||||||
voice: {
|
|
||||||
setVoiceEnabled: Dispatch<SetStateAction<boolean>>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createSlashHandler(ctx: SlashHandlerContext): (cmd: string) => boolean {
|
export function createSlashHandler(ctx: SlashHandlerContext): (cmd: string) => boolean {
|
||||||
const { enqueue, hasSelection, paste, queueRef, selection, setInput } = ctx.composer
|
const { enqueue, hasSelection, paste, queueRef, selection, setInput } = ctx.composer
|
||||||
const { gw, rpc } = ctx.gateway
|
const { gw, rpc } = ctx.gateway
|
||||||
|
|||||||
@@ -1,14 +1,9 @@
|
|||||||
import { createContext, type ReactNode, useContext } from 'react'
|
import { createContext, useContext } from 'react'
|
||||||
|
|
||||||
import type { GatewayServices } from './interfaces.js'
|
import type { GatewayProviderProps, GatewayServices } from './interfaces.js'
|
||||||
|
|
||||||
const GatewayContext = createContext<GatewayServices | null>(null)
|
const GatewayContext = createContext<GatewayServices | null>(null)
|
||||||
|
|
||||||
export interface GatewayProviderProps {
|
|
||||||
children: ReactNode
|
|
||||||
value: GatewayServices
|
|
||||||
}
|
|
||||||
|
|
||||||
export function GatewayProvider({ children, value }: GatewayProviderProps) {
|
export function GatewayProvider({ children, value }: GatewayProviderProps) {
|
||||||
return <GatewayContext.Provider value={value}>{children}</GatewayContext.Provider>
|
return <GatewayContext.Provider value={value}>{children}</GatewayContext.Provider>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,11 +3,6 @@ import type { DetailsMode, Msg, SessionInfo } from '../types.js'
|
|||||||
|
|
||||||
const DETAILS_MODES: DetailsMode[] = ['hidden', 'collapsed', 'expanded']
|
const DETAILS_MODES: DetailsMode[] = ['hidden', 'collapsed', 'expanded']
|
||||||
|
|
||||||
export interface PasteSnippet {
|
|
||||||
label: string
|
|
||||||
text: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export const parseDetailsMode = (v: unknown): DetailsMode | null => {
|
export const parseDetailsMode = (v: unknown): DetailsMode | null => {
|
||||||
const s = typeof v === 'string' ? v.trim().toLowerCase() : ''
|
const s = typeof v === 'string' ? v.trim().toLowerCase() : ''
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,32 @@
|
|||||||
|
import type { ScrollBoxHandle } from '@hermes/ink'
|
||||||
|
import type { MutableRefObject, ReactNode, RefObject, SetStateAction } from 'react'
|
||||||
|
|
||||||
|
import type { PasteEvent } from '../components/textInput.js'
|
||||||
import type { GatewayClient } from '../gatewayClient.js'
|
import type { GatewayClient } from '../gatewayClient.js'
|
||||||
|
import type { RpcResult } from '../lib/rpc.js'
|
||||||
import type { Theme } from '../theme.js'
|
import type { Theme } from '../theme.js'
|
||||||
import type { ApprovalReq, ClarifyReq, DetailsMode, Msg, SecretReq, SessionInfo, SudoReq, Usage } from '../types.js'
|
import type {
|
||||||
|
ActiveTool,
|
||||||
|
ActivityItem,
|
||||||
|
ApprovalReq,
|
||||||
|
ClarifyReq,
|
||||||
|
DetailsMode,
|
||||||
|
Msg,
|
||||||
|
PanelSection,
|
||||||
|
SecretReq,
|
||||||
|
SessionInfo,
|
||||||
|
SlashCatalog,
|
||||||
|
SudoReq,
|
||||||
|
Usage
|
||||||
|
} from '../types.js'
|
||||||
|
|
||||||
|
export interface StateSetter<T> {
|
||||||
|
(value: SetStateAction<T>): void
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SelectionApi {
|
||||||
|
copySelection: () => string
|
||||||
|
}
|
||||||
|
|
||||||
export interface CompletionItem {
|
export interface CompletionItem {
|
||||||
display: string
|
display: string
|
||||||
@@ -9,7 +35,7 @@ export interface CompletionItem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface GatewayRpc {
|
export interface GatewayRpc {
|
||||||
(method: string, params?: Record<string, unknown>): Promise<any | null>
|
(method: string, params?: Record<string, unknown>): Promise<RpcResult | null>
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GatewayServices {
|
export interface GatewayServices {
|
||||||
@@ -17,6 +43,11 @@ export interface GatewayServices {
|
|||||||
rpc: GatewayRpc
|
rpc: GatewayRpc
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface GatewayProviderProps {
|
||||||
|
children: ReactNode
|
||||||
|
value: GatewayServices
|
||||||
|
}
|
||||||
|
|
||||||
export interface OverlayState {
|
export interface OverlayState {
|
||||||
approval: ApprovalReq | null
|
approval: ApprovalReq | null
|
||||||
clarify: ClarifyReq | null
|
clarify: ClarifyReq | null
|
||||||
@@ -65,3 +96,343 @@ export interface VirtualHistoryState {
|
|||||||
start: number
|
start: number
|
||||||
topSpacer: number
|
topSpacer: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ComposerPasteResult {
|
||||||
|
cursor: number
|
||||||
|
value: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ComposerActions {
|
||||||
|
clearIn: () => void
|
||||||
|
dequeue: () => string | undefined
|
||||||
|
enqueue: (text: string) => void
|
||||||
|
handleTextPaste: (event: PasteEvent) => ComposerPasteResult | null
|
||||||
|
openEditor: () => void
|
||||||
|
pushHistory: (text: string) => void
|
||||||
|
replaceQueue: (index: number, text: string) => void
|
||||||
|
setCompIdx: StateSetter<number>
|
||||||
|
setHistoryIdx: StateSetter<number | null>
|
||||||
|
setInput: StateSetter<string>
|
||||||
|
setInputBuf: StateSetter<string[]>
|
||||||
|
setPasteSnips: StateSetter<PasteSnippet[]>
|
||||||
|
setQueueEdit: (index: number | null) => void
|
||||||
|
syncQueue: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ComposerRefs {
|
||||||
|
historyDraftRef: MutableRefObject<string>
|
||||||
|
historyRef: MutableRefObject<string[]>
|
||||||
|
queueEditRef: MutableRefObject<number | null>
|
||||||
|
queueRef: MutableRefObject<string[]>
|
||||||
|
submitRef: MutableRefObject<(value: string) => void>
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ComposerState {
|
||||||
|
compIdx: number
|
||||||
|
compReplace: number
|
||||||
|
completions: CompletionItem[]
|
||||||
|
historyIdx: number | null
|
||||||
|
input: string
|
||||||
|
inputBuf: string[]
|
||||||
|
pasteSnips: PasteSnippet[]
|
||||||
|
queueEditIdx: number | null
|
||||||
|
queuedDisplay: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UseComposerStateOptions {
|
||||||
|
gw: GatewayClient
|
||||||
|
onClipboardPaste: (quiet?: boolean) => Promise<void> | void
|
||||||
|
submitRef: MutableRefObject<(value: string) => void>
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UseComposerStateResult {
|
||||||
|
actions: ComposerActions
|
||||||
|
refs: ComposerRefs
|
||||||
|
state: ComposerState
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface InterruptTurnOptions {
|
||||||
|
appendMessage: (msg: Msg) => void
|
||||||
|
gw: { request: (method: string, params?: Record<string, unknown>) => Promise<unknown> }
|
||||||
|
sid: string
|
||||||
|
sys: (text: string) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TurnActions {
|
||||||
|
clearReasoning: () => void
|
||||||
|
endReasoningPhase: () => void
|
||||||
|
idle: () => void
|
||||||
|
interruptTurn: (options: InterruptTurnOptions) => void
|
||||||
|
pruneTransient: () => void
|
||||||
|
pulseReasoningStreaming: () => void
|
||||||
|
pushActivity: (text: string, tone?: ActivityItem['tone'], replaceLabel?: string) => void
|
||||||
|
pushTrail: (line: string) => void
|
||||||
|
scheduleReasoning: () => void
|
||||||
|
scheduleStreaming: () => void
|
||||||
|
setActivity: StateSetter<ActivityItem[]>
|
||||||
|
setReasoning: StateSetter<string>
|
||||||
|
setReasoningTokens: StateSetter<number>
|
||||||
|
setReasoningActive: StateSetter<boolean>
|
||||||
|
setToolTokens: StateSetter<number>
|
||||||
|
setReasoningStreaming: StateSetter<boolean>
|
||||||
|
setStreaming: StateSetter<string>
|
||||||
|
setTools: StateSetter<ActiveTool[]>
|
||||||
|
setTurnTrail: StateSetter<string[]>
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TurnRefs {
|
||||||
|
activeToolsRef: MutableRefObject<ActiveTool[]>
|
||||||
|
bufRef: MutableRefObject<string>
|
||||||
|
interruptedRef: MutableRefObject<boolean>
|
||||||
|
lastStatusNoteRef: MutableRefObject<string>
|
||||||
|
persistedToolLabelsRef: MutableRefObject<Set<string>>
|
||||||
|
protocolWarnedRef: MutableRefObject<boolean>
|
||||||
|
reasoningRef: MutableRefObject<string>
|
||||||
|
reasoningStreamingTimerRef: MutableRefObject<ReturnType<typeof setTimeout> | null>
|
||||||
|
reasoningTimerRef: MutableRefObject<ReturnType<typeof setTimeout> | null>
|
||||||
|
statusTimerRef: MutableRefObject<ReturnType<typeof setTimeout> | null>
|
||||||
|
streamTimerRef: MutableRefObject<ReturnType<typeof setTimeout> | null>
|
||||||
|
toolTokenAccRef: MutableRefObject<number>
|
||||||
|
toolCompleteRibbonRef: MutableRefObject<ToolCompleteRibbon | null>
|
||||||
|
turnToolsRef: MutableRefObject<string[]>
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TurnState {
|
||||||
|
activity: ActivityItem[]
|
||||||
|
reasoning: string
|
||||||
|
reasoningTokens: number
|
||||||
|
reasoningActive: boolean
|
||||||
|
reasoningStreaming: boolean
|
||||||
|
streaming: string
|
||||||
|
toolTokens: number
|
||||||
|
tools: ActiveTool[]
|
||||||
|
turnTrail: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UseTurnStateResult {
|
||||||
|
actions: TurnActions
|
||||||
|
refs: TurnRefs
|
||||||
|
state: TurnState
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface InputHandlerActions {
|
||||||
|
answerClarify: (answer: string) => void
|
||||||
|
appendMessage: (msg: Msg) => void
|
||||||
|
die: () => void
|
||||||
|
dispatchSubmission: (full: string) => void
|
||||||
|
guardBusySessionSwitch: (what?: string) => boolean
|
||||||
|
newSession: (msg?: string) => void
|
||||||
|
sys: (text: string) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface InputHandlerContext {
|
||||||
|
actions: InputHandlerActions
|
||||||
|
composer: {
|
||||||
|
actions: ComposerActions
|
||||||
|
refs: ComposerRefs
|
||||||
|
state: ComposerState
|
||||||
|
}
|
||||||
|
gateway: GatewayServices
|
||||||
|
terminal: {
|
||||||
|
hasSelection: boolean
|
||||||
|
scrollRef: RefObject<ScrollBoxHandle | null>
|
||||||
|
scrollWithSelection: (delta: number) => void
|
||||||
|
selection: SelectionApi
|
||||||
|
stdout?: NodeJS.WriteStream
|
||||||
|
}
|
||||||
|
turn: {
|
||||||
|
actions: TurnActions
|
||||||
|
refs: TurnRefs
|
||||||
|
}
|
||||||
|
voice: {
|
||||||
|
recording: boolean
|
||||||
|
setProcessing: StateSetter<boolean>
|
||||||
|
setRecording: StateSetter<boolean>
|
||||||
|
}
|
||||||
|
wheelStep: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface InputHandlerResult {
|
||||||
|
pagerPageSize: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GatewayEventHandlerContext {
|
||||||
|
composer: {
|
||||||
|
dequeue: () => string | undefined
|
||||||
|
queueEditRef: MutableRefObject<number | null>
|
||||||
|
sendQueued: (text: string) => void
|
||||||
|
}
|
||||||
|
gateway: GatewayServices
|
||||||
|
session: {
|
||||||
|
STARTUP_RESUME_ID: string
|
||||||
|
colsRef: MutableRefObject<number>
|
||||||
|
newSession: (msg?: string) => void
|
||||||
|
resetSession: () => void
|
||||||
|
setCatalog: StateSetter<SlashCatalog | null>
|
||||||
|
}
|
||||||
|
system: {
|
||||||
|
bellOnComplete: boolean
|
||||||
|
stdout?: NodeJS.WriteStream
|
||||||
|
sys: (text: string) => void
|
||||||
|
}
|
||||||
|
transcript: {
|
||||||
|
appendMessage: (msg: Msg) => void
|
||||||
|
setHistoryItems: StateSetter<Msg[]>
|
||||||
|
setMessages: StateSetter<Msg[]>
|
||||||
|
}
|
||||||
|
turn: {
|
||||||
|
actions: Pick<
|
||||||
|
TurnActions,
|
||||||
|
| 'clearReasoning'
|
||||||
|
| 'endReasoningPhase'
|
||||||
|
| 'idle'
|
||||||
|
| 'pruneTransient'
|
||||||
|
| 'pulseReasoningStreaming'
|
||||||
|
| 'pushActivity'
|
||||||
|
| 'pushTrail'
|
||||||
|
| 'scheduleReasoning'
|
||||||
|
| 'scheduleStreaming'
|
||||||
|
| 'setActivity'
|
||||||
|
| 'setReasoningTokens'
|
||||||
|
| 'setStreaming'
|
||||||
|
| 'setToolTokens'
|
||||||
|
| 'setTools'
|
||||||
|
| 'setTurnTrail'
|
||||||
|
>
|
||||||
|
refs: Pick<
|
||||||
|
TurnRefs,
|
||||||
|
| 'activeToolsRef'
|
||||||
|
| 'bufRef'
|
||||||
|
| 'interruptedRef'
|
||||||
|
| 'lastStatusNoteRef'
|
||||||
|
| 'persistedToolLabelsRef'
|
||||||
|
| 'protocolWarnedRef'
|
||||||
|
| 'reasoningRef'
|
||||||
|
| 'statusTimerRef'
|
||||||
|
| 'toolTokenAccRef'
|
||||||
|
| 'toolCompleteRibbonRef'
|
||||||
|
| 'turnToolsRef'
|
||||||
|
>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SlashHandlerContext {
|
||||||
|
composer: {
|
||||||
|
enqueue: (text: string) => void
|
||||||
|
hasSelection: boolean
|
||||||
|
paste: (quiet?: boolean) => void
|
||||||
|
queueRef: MutableRefObject<string[]>
|
||||||
|
selection: SelectionApi
|
||||||
|
setInput: StateSetter<string>
|
||||||
|
}
|
||||||
|
gateway: GatewayServices
|
||||||
|
local: {
|
||||||
|
catalog: SlashCatalog | null
|
||||||
|
lastUserMsg: string
|
||||||
|
maybeWarn: (value: any) => void
|
||||||
|
messages: Msg[]
|
||||||
|
}
|
||||||
|
session: {
|
||||||
|
closeSession: (targetSid?: string | null) => Promise<unknown>
|
||||||
|
die: () => void
|
||||||
|
guardBusySessionSwitch: (what?: string) => boolean
|
||||||
|
newSession: (msg?: string) => void
|
||||||
|
resetVisibleHistory: (info?: SessionInfo | null) => void
|
||||||
|
resumeById: (id: string) => void
|
||||||
|
setSessionStartedAt: StateSetter<number>
|
||||||
|
}
|
||||||
|
transcript: {
|
||||||
|
page: (text: string, title?: string) => void
|
||||||
|
panel: (title: string, sections: PanelSection[]) => void
|
||||||
|
send: (text: string) => void
|
||||||
|
setHistoryItems: StateSetter<Msg[]>
|
||||||
|
setMessages: StateSetter<Msg[]>
|
||||||
|
sys: (text: string) => void
|
||||||
|
trimLastExchange: (items: Msg[]) => Msg[]
|
||||||
|
}
|
||||||
|
voice: {
|
||||||
|
setVoiceEnabled: StateSetter<boolean>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AppLayoutActions {
|
||||||
|
answerApproval: (choice: string) => void
|
||||||
|
answerClarify: (answer: string) => void
|
||||||
|
answerSecret: (value: string) => void
|
||||||
|
answerSudo: (pw: string) => void
|
||||||
|
onModelSelect: (value: string) => void
|
||||||
|
resumeById: (id: string) => void
|
||||||
|
setStickyPrompt: (value: string) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AppLayoutComposerProps {
|
||||||
|
cols: number
|
||||||
|
compIdx: number
|
||||||
|
completions: CompletionItem[]
|
||||||
|
empty: boolean
|
||||||
|
handleTextPaste: (event: PasteEvent) => ComposerPasteResult | null
|
||||||
|
input: string
|
||||||
|
inputBuf: string[]
|
||||||
|
pagerPageSize: number
|
||||||
|
queueEditIdx: number | null
|
||||||
|
queuedDisplay: string[]
|
||||||
|
submit: (value: string) => void
|
||||||
|
updateInput: StateSetter<string>
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AppLayoutProgressProps {
|
||||||
|
activity: ActivityItem[]
|
||||||
|
reasoning: string
|
||||||
|
reasoningTokens: number
|
||||||
|
reasoningActive: boolean
|
||||||
|
reasoningStreaming: boolean
|
||||||
|
showProgressArea: boolean
|
||||||
|
showStreamingArea: boolean
|
||||||
|
streaming: string
|
||||||
|
toolTokens: number
|
||||||
|
tools: ActiveTool[]
|
||||||
|
turnTrail: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AppLayoutStatusProps {
|
||||||
|
cwdLabel: string
|
||||||
|
durationLabel: string
|
||||||
|
showStickyPrompt: boolean
|
||||||
|
statusColor: string
|
||||||
|
stickyPrompt: string
|
||||||
|
voiceLabel: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AppLayoutTranscriptProps {
|
||||||
|
historyItems: Msg[]
|
||||||
|
scrollRef: RefObject<ScrollBoxHandle | null>
|
||||||
|
virtualHistory: VirtualHistoryState
|
||||||
|
virtualRows: TranscriptRow[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AppLayoutProps {
|
||||||
|
actions: AppLayoutActions
|
||||||
|
composer: AppLayoutComposerProps
|
||||||
|
mouseTracking: boolean
|
||||||
|
progress: AppLayoutProgressProps
|
||||||
|
status: AppLayoutStatusProps
|
||||||
|
transcript: AppLayoutTranscriptProps
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AppOverlaysProps {
|
||||||
|
cols: number
|
||||||
|
compIdx: number
|
||||||
|
completions: CompletionItem[]
|
||||||
|
onApprovalChoice: (choice: string) => void
|
||||||
|
onClarifyAnswer: (value: string) => void
|
||||||
|
onModelSelect: (value: string) => void
|
||||||
|
onPickerSelect: (sessionId: string) => void
|
||||||
|
onSecretSubmit: (value: string) => void
|
||||||
|
onSudoSubmit: (pw: string) => void
|
||||||
|
pagerPageSize: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PasteSnippet {
|
||||||
|
label: string
|
||||||
|
text: string
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,74 +4,18 @@ import { tmpdir } from 'node:os'
|
|||||||
import { join } from 'node:path'
|
import { join } from 'node:path'
|
||||||
|
|
||||||
import { useStore } from '@nanostores/react'
|
import { useStore } from '@nanostores/react'
|
||||||
import { type Dispatch, type MutableRefObject, type SetStateAction, useCallback, useState } from 'react'
|
import { useCallback, useState } from 'react'
|
||||||
|
|
||||||
import type { PasteEvent } from '../components/textInput.js'
|
import type { PasteEvent } from '../components/textInput.js'
|
||||||
import type { GatewayClient } from '../gatewayClient.js'
|
|
||||||
import { useCompletion } from '../hooks/useCompletion.js'
|
import { useCompletion } from '../hooks/useCompletion.js'
|
||||||
import { useInputHistory } from '../hooks/useInputHistory.js'
|
import { useInputHistory } from '../hooks/useInputHistory.js'
|
||||||
import { useQueue } from '../hooks/useQueue.js'
|
import { useQueue } from '../hooks/useQueue.js'
|
||||||
import { pasteTokenLabel, stripTrailingPasteNewlines } from '../lib/text.js'
|
import { pasteTokenLabel, stripTrailingPasteNewlines } from '../lib/text.js'
|
||||||
|
|
||||||
import { LARGE_PASTE } from './constants.js'
|
import { LARGE_PASTE } from './constants.js'
|
||||||
import type { PasteSnippet } from './helpers.js'
|
import type { PasteSnippet, UseComposerStateOptions, UseComposerStateResult } from './interfaces.js'
|
||||||
import type { CompletionItem } from './interfaces.js'
|
|
||||||
import { $isBlocked } from './overlayStore.js'
|
import { $isBlocked } from './overlayStore.js'
|
||||||
|
|
||||||
export interface ComposerPasteResult {
|
|
||||||
cursor: number
|
|
||||||
value: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ComposerActions {
|
|
||||||
clearIn: () => void
|
|
||||||
dequeue: () => string | undefined
|
|
||||||
enqueue: (text: string) => void
|
|
||||||
handleTextPaste: (event: PasteEvent) => ComposerPasteResult | null
|
|
||||||
openEditor: () => void
|
|
||||||
pushHistory: (text: string) => void
|
|
||||||
replaceQueue: (index: number, text: string) => void
|
|
||||||
setCompIdx: Dispatch<SetStateAction<number>>
|
|
||||||
setHistoryIdx: Dispatch<SetStateAction<number | null>>
|
|
||||||
setInput: Dispatch<SetStateAction<string>>
|
|
||||||
setInputBuf: Dispatch<SetStateAction<string[]>>
|
|
||||||
setPasteSnips: Dispatch<SetStateAction<PasteSnippet[]>>
|
|
||||||
setQueueEdit: (index: number | null) => void
|
|
||||||
syncQueue: () => void
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ComposerRefs {
|
|
||||||
historyDraftRef: MutableRefObject<string>
|
|
||||||
historyRef: MutableRefObject<string[]>
|
|
||||||
queueEditRef: MutableRefObject<number | null>
|
|
||||||
queueRef: MutableRefObject<string[]>
|
|
||||||
submitRef: MutableRefObject<(value: string) => void>
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ComposerState {
|
|
||||||
compIdx: number
|
|
||||||
compReplace: number
|
|
||||||
completions: CompletionItem[]
|
|
||||||
historyIdx: number | null
|
|
||||||
input: string
|
|
||||||
inputBuf: string[]
|
|
||||||
pasteSnips: PasteSnippet[]
|
|
||||||
queueEditIdx: number | null
|
|
||||||
queuedDisplay: string[]
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface UseComposerStateOptions {
|
|
||||||
gw: GatewayClient
|
|
||||||
onClipboardPaste: (quiet?: boolean) => Promise<void> | void
|
|
||||||
submitRef: MutableRefObject<(value: string) => void>
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface UseComposerStateResult {
|
|
||||||
actions: ComposerActions
|
|
||||||
refs: ComposerRefs
|
|
||||||
state: ComposerState
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useComposerState({ gw, onClipboardPaste, submitRef }: UseComposerStateOptions): UseComposerStateResult {
|
export function useComposerState({ gw, onClipboardPaste, submitRef }: UseComposerStateOptions): UseComposerStateResult {
|
||||||
const [input, setInput] = useState('')
|
const [input, setInput] = useState('')
|
||||||
const [inputBuf, setInputBuf] = useState<string[]>([])
|
const [inputBuf, setInputBuf] = useState<string[]>([])
|
||||||
|
|||||||
@@ -1,57 +1,9 @@
|
|||||||
import { type ScrollBoxHandle, useInput } from '@hermes/ink'
|
import { useInput } from '@hermes/ink'
|
||||||
import { useStore } from '@nanostores/react'
|
import { useStore } from '@nanostores/react'
|
||||||
import type { Dispatch, RefObject, SetStateAction } from 'react'
|
|
||||||
|
|
||||||
import type { Msg } from '../types.js'
|
import type { InputHandlerContext, InputHandlerResult } from './interfaces.js'
|
||||||
|
|
||||||
import type { GatewayServices } from './interfaces.js'
|
|
||||||
import { $isBlocked, $overlayState, patchOverlayState } from './overlayStore.js'
|
import { $isBlocked, $overlayState, patchOverlayState } from './overlayStore.js'
|
||||||
import { getUiState, patchUiState } from './uiStore.js'
|
import { getUiState, patchUiState } from './uiStore.js'
|
||||||
import type { ComposerActions, ComposerRefs, ComposerState } from './useComposerState.js'
|
|
||||||
import type { TurnActions, TurnRefs } from './useTurnState.js'
|
|
||||||
|
|
||||||
export interface InputHandlerActions {
|
|
||||||
answerClarify: (answer: string) => void
|
|
||||||
appendMessage: (msg: Msg) => void
|
|
||||||
die: () => void
|
|
||||||
dispatchSubmission: (full: string) => void
|
|
||||||
guardBusySessionSwitch: (what?: string) => boolean
|
|
||||||
newSession: (msg?: string) => void
|
|
||||||
sys: (text: string) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface InputHandlerContext {
|
|
||||||
actions: InputHandlerActions
|
|
||||||
composer: {
|
|
||||||
actions: ComposerActions
|
|
||||||
refs: ComposerRefs
|
|
||||||
state: ComposerState
|
|
||||||
}
|
|
||||||
gateway: GatewayServices
|
|
||||||
terminal: {
|
|
||||||
hasSelection: boolean
|
|
||||||
scrollRef: RefObject<ScrollBoxHandle | null>
|
|
||||||
scrollWithSelection: (delta: number) => void
|
|
||||||
selection: {
|
|
||||||
copySelection: () => string
|
|
||||||
}
|
|
||||||
stdout?: NodeJS.WriteStream
|
|
||||||
}
|
|
||||||
turn: {
|
|
||||||
actions: TurnActions
|
|
||||||
refs: TurnRefs
|
|
||||||
}
|
|
||||||
voice: {
|
|
||||||
recording: boolean
|
|
||||||
setProcessing: Dispatch<SetStateAction<boolean>>
|
|
||||||
setRecording: Dispatch<SetStateAction<boolean>>
|
|
||||||
}
|
|
||||||
wheelStep: number
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface InputHandlerResult {
|
|
||||||
pagerPageSize: number
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useInputHandlers(ctx: InputHandlerContext): InputHandlerResult {
|
export function useInputHandlers(ctx: InputHandlerContext): InputHandlerResult {
|
||||||
const { actions, composer, gateway, terminal, turn, voice, wheelStep } = ctx
|
const { actions, composer, gateway, terminal, turn, voice, wheelStep } = ctx
|
||||||
|
|||||||
@@ -1,89 +1,26 @@
|
|||||||
import {
|
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||||
type Dispatch,
|
|
||||||
type MutableRefObject,
|
|
||||||
type SetStateAction,
|
|
||||||
useCallback,
|
|
||||||
useEffect,
|
|
||||||
useRef,
|
|
||||||
useState
|
|
||||||
} from 'react'
|
|
||||||
|
|
||||||
import { isTransientTrailLine, sameToolTrailGroup } from '../lib/text.js'
|
import { isTransientTrailLine, sameToolTrailGroup } from '../lib/text.js'
|
||||||
import type { ActiveTool, ActivityItem, Msg } from '../types.js'
|
import type { ActiveTool, ActivityItem } from '../types.js'
|
||||||
|
|
||||||
import { REASONING_PULSE_MS, STREAM_BATCH_MS } from './constants.js'
|
import { REASONING_PULSE_MS, STREAM_BATCH_MS } from './constants.js'
|
||||||
import type { ToolCompleteRibbon } from './interfaces.js'
|
import type { InterruptTurnOptions, ToolCompleteRibbon, UseTurnStateResult } from './interfaces.js'
|
||||||
import { resetOverlayState } from './overlayStore.js'
|
import { resetOverlayState } from './overlayStore.js'
|
||||||
import { patchUiState } from './uiStore.js'
|
import { patchUiState } from './uiStore.js'
|
||||||
|
|
||||||
export interface InterruptTurnOptions {
|
|
||||||
appendMessage: (msg: Msg) => void
|
|
||||||
gw: { request: (method: string, params?: Record<string, unknown>) => Promise<unknown> }
|
|
||||||
sid: string
|
|
||||||
sys: (text: string) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface TurnActions {
|
|
||||||
clearReasoning: () => void
|
|
||||||
endReasoningPhase: () => void
|
|
||||||
idle: () => void
|
|
||||||
interruptTurn: (options: InterruptTurnOptions) => void
|
|
||||||
pruneTransient: () => void
|
|
||||||
pulseReasoningStreaming: () => void
|
|
||||||
pushActivity: (text: string, tone?: ActivityItem['tone'], replaceLabel?: string) => void
|
|
||||||
pushTrail: (line: string) => void
|
|
||||||
scheduleReasoning: () => void
|
|
||||||
scheduleStreaming: () => void
|
|
||||||
setActivity: Dispatch<SetStateAction<ActivityItem[]>>
|
|
||||||
setReasoning: Dispatch<SetStateAction<string>>
|
|
||||||
setReasoningActive: Dispatch<SetStateAction<boolean>>
|
|
||||||
setReasoningStreaming: Dispatch<SetStateAction<boolean>>
|
|
||||||
setStreaming: Dispatch<SetStateAction<string>>
|
|
||||||
setTools: Dispatch<SetStateAction<ActiveTool[]>>
|
|
||||||
setTurnTrail: Dispatch<SetStateAction<string[]>>
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface TurnRefs {
|
|
||||||
bufRef: MutableRefObject<string>
|
|
||||||
interruptedRef: MutableRefObject<boolean>
|
|
||||||
lastStatusNoteRef: MutableRefObject<string>
|
|
||||||
persistedToolLabelsRef: MutableRefObject<Set<string>>
|
|
||||||
protocolWarnedRef: MutableRefObject<boolean>
|
|
||||||
reasoningRef: MutableRefObject<string>
|
|
||||||
reasoningStreamingTimerRef: MutableRefObject<ReturnType<typeof setTimeout> | null>
|
|
||||||
reasoningTimerRef: MutableRefObject<ReturnType<typeof setTimeout> | null>
|
|
||||||
statusTimerRef: MutableRefObject<ReturnType<typeof setTimeout> | null>
|
|
||||||
streamTimerRef: MutableRefObject<ReturnType<typeof setTimeout> | null>
|
|
||||||
toolCompleteRibbonRef: MutableRefObject<ToolCompleteRibbon | null>
|
|
||||||
turnToolsRef: MutableRefObject<string[]>
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface TurnState {
|
|
||||||
activity: ActivityItem[]
|
|
||||||
reasoning: string
|
|
||||||
reasoningActive: boolean
|
|
||||||
reasoningStreaming: boolean
|
|
||||||
streaming: string
|
|
||||||
tools: ActiveTool[]
|
|
||||||
turnTrail: string[]
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface UseTurnStateResult {
|
|
||||||
actions: TurnActions
|
|
||||||
refs: TurnRefs
|
|
||||||
state: TurnState
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useTurnState(): UseTurnStateResult {
|
export function useTurnState(): UseTurnStateResult {
|
||||||
const [activity, setActivity] = useState<ActivityItem[]>([])
|
const [activity, setActivity] = useState<ActivityItem[]>([])
|
||||||
const [reasoning, setReasoning] = useState('')
|
const [reasoning, setReasoning] = useState('')
|
||||||
|
const [reasoningTokens, setReasoningTokens] = useState(0)
|
||||||
const [reasoningActive, setReasoningActive] = useState(false)
|
const [reasoningActive, setReasoningActive] = useState(false)
|
||||||
|
const [toolTokens, setToolTokens] = useState(0)
|
||||||
const [reasoningStreaming, setReasoningStreaming] = useState(false)
|
const [reasoningStreaming, setReasoningStreaming] = useState(false)
|
||||||
const [streaming, setStreaming] = useState('')
|
const [streaming, setStreaming] = useState('')
|
||||||
const [tools, setTools] = useState<ActiveTool[]>([])
|
const [tools, setTools] = useState<ActiveTool[]>([])
|
||||||
const [turnTrail, setTurnTrail] = useState<string[]>([])
|
const [turnTrail, setTurnTrail] = useState<string[]>([])
|
||||||
|
|
||||||
const activityIdRef = useRef(0)
|
const activityIdRef = useRef(0)
|
||||||
|
const activeToolsRef = useRef<ActiveTool[]>([])
|
||||||
const bufRef = useRef('')
|
const bufRef = useRef('')
|
||||||
const interruptedRef = useRef(false)
|
const interruptedRef = useRef(false)
|
||||||
const lastStatusNoteRef = useRef('')
|
const lastStatusNoteRef = useRef('')
|
||||||
@@ -94,6 +31,7 @@ export function useTurnState(): UseTurnStateResult {
|
|||||||
const reasoningTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
const reasoningTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
||||||
const statusTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
const statusTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
||||||
const streamTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
const streamTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
||||||
|
const toolTokenAccRef = useRef(0)
|
||||||
const toolCompleteRibbonRef = useRef<ToolCompleteRibbon | null>(null)
|
const toolCompleteRibbonRef = useRef<ToolCompleteRibbon | null>(null)
|
||||||
const turnToolsRef = useRef<string[]>([])
|
const turnToolsRef = useRef<string[]>([])
|
||||||
|
|
||||||
@@ -200,11 +138,15 @@ export function useTurnState(): UseTurnStateResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
reasoningRef.current = ''
|
reasoningRef.current = ''
|
||||||
|
toolTokenAccRef.current = 0
|
||||||
setReasoning('')
|
setReasoning('')
|
||||||
|
setReasoningTokens(0)
|
||||||
|
setToolTokens(0)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const idle = useCallback(() => {
|
const idle = useCallback(() => {
|
||||||
endReasoningPhase()
|
endReasoningPhase()
|
||||||
|
activeToolsRef.current = []
|
||||||
setTools([])
|
setTools([])
|
||||||
setTurnTrail([])
|
setTurnTrail([])
|
||||||
patchUiState({ busy: false })
|
patchUiState({ busy: false })
|
||||||
@@ -263,13 +205,16 @@ export function useTurnState(): UseTurnStateResult {
|
|||||||
scheduleStreaming,
|
scheduleStreaming,
|
||||||
setActivity,
|
setActivity,
|
||||||
setReasoning,
|
setReasoning,
|
||||||
|
setReasoningTokens,
|
||||||
setReasoningActive,
|
setReasoningActive,
|
||||||
|
setToolTokens,
|
||||||
setReasoningStreaming,
|
setReasoningStreaming,
|
||||||
setStreaming,
|
setStreaming,
|
||||||
setTools,
|
setTools,
|
||||||
setTurnTrail
|
setTurnTrail
|
||||||
},
|
},
|
||||||
refs: {
|
refs: {
|
||||||
|
activeToolsRef,
|
||||||
bufRef,
|
bufRef,
|
||||||
interruptedRef,
|
interruptedRef,
|
||||||
lastStatusNoteRef,
|
lastStatusNoteRef,
|
||||||
@@ -280,13 +225,16 @@ export function useTurnState(): UseTurnStateResult {
|
|||||||
reasoningTimerRef,
|
reasoningTimerRef,
|
||||||
statusTimerRef,
|
statusTimerRef,
|
||||||
streamTimerRef,
|
streamTimerRef,
|
||||||
|
toolTokenAccRef,
|
||||||
toolCompleteRibbonRef,
|
toolCompleteRibbonRef,
|
||||||
turnToolsRef
|
turnToolsRef
|
||||||
},
|
},
|
||||||
state: {
|
state: {
|
||||||
activity,
|
activity,
|
||||||
reasoning,
|
reasoning,
|
||||||
|
reasoningTokens,
|
||||||
reasoningActive,
|
reasoningActive,
|
||||||
|
toolTokens,
|
||||||
reasoningStreaming,
|
reasoningStreaming,
|
||||||
streaming,
|
streaming,
|
||||||
tools,
|
tools,
|
||||||
|
|||||||
@@ -1,84 +1,19 @@
|
|||||||
import { AlternateScreen, Box, NoSelect, ScrollBox, type ScrollBoxHandle, Text } from '@hermes/ink'
|
import { AlternateScreen, Box, NoSelect, ScrollBox, Text } from '@hermes/ink'
|
||||||
import { useStore } from '@nanostores/react'
|
import { useStore } from '@nanostores/react'
|
||||||
import type { RefObject } from 'react'
|
|
||||||
|
|
||||||
import { PLACEHOLDER } from '../app/constants.js'
|
import { PLACEHOLDER } from '../app/constants.js'
|
||||||
import type { CompletionItem, TranscriptRow, VirtualHistoryState } from '../app/interfaces.js'
|
import type { AppLayoutProps } from '../app/interfaces.js'
|
||||||
import { $isBlocked } from '../app/overlayStore.js'
|
import { $isBlocked } from '../app/overlayStore.js'
|
||||||
import { $uiState } from '../app/uiStore.js'
|
import { $uiState } from '../app/uiStore.js'
|
||||||
import type { ActiveTool, ActivityItem, Msg } from '../types.js'
|
|
||||||
|
|
||||||
import { StatusRule, StickyPromptTracker, TranscriptScrollbar } from './appChrome.js'
|
import { StatusRule, StickyPromptTracker, TranscriptScrollbar } from './appChrome.js'
|
||||||
import { AppOverlays } from './appOverlays.js'
|
import { AppOverlays } from './appOverlays.js'
|
||||||
import { Banner, Panel, SessionPanel } from './branding.js'
|
import { Banner, Panel, SessionPanel } from './branding.js'
|
||||||
import { MessageLine } from './messageLine.js'
|
import { MessageLine } from './messageLine.js'
|
||||||
import { QueuedMessages } from './queuedMessages.js'
|
import { QueuedMessages } from './queuedMessages.js'
|
||||||
import type { PasteEvent } from './textInput.js'
|
|
||||||
import { TextInput } from './textInput.js'
|
import { TextInput } from './textInput.js'
|
||||||
import { ToolTrail } from './thinking.js'
|
import { ToolTrail } from './thinking.js'
|
||||||
|
|
||||||
export interface AppLayoutActions {
|
|
||||||
answerApproval: (choice: string) => void
|
|
||||||
answerClarify: (answer: string) => void
|
|
||||||
answerSecret: (value: string) => void
|
|
||||||
answerSudo: (pw: string) => void
|
|
||||||
onModelSelect: (value: string) => void
|
|
||||||
resumeById: (id: string) => void
|
|
||||||
setStickyPrompt: (value: string) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AppLayoutComposerProps {
|
|
||||||
cols: number
|
|
||||||
compIdx: number
|
|
||||||
completions: CompletionItem[]
|
|
||||||
empty: boolean
|
|
||||||
handleTextPaste: (event: PasteEvent) => { cursor: number; value: string } | null
|
|
||||||
input: string
|
|
||||||
inputBuf: string[]
|
|
||||||
pagerPageSize: number
|
|
||||||
queueEditIdx: number | null
|
|
||||||
queuedDisplay: string[]
|
|
||||||
submit: (value: string) => void
|
|
||||||
updateInput: (next: string) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AppLayoutProgressProps {
|
|
||||||
activity: ActivityItem[]
|
|
||||||
reasoning: string
|
|
||||||
reasoningActive: boolean
|
|
||||||
reasoningStreaming: boolean
|
|
||||||
showProgressArea: boolean
|
|
||||||
showStreamingArea: boolean
|
|
||||||
streaming: string
|
|
||||||
tools: ActiveTool[]
|
|
||||||
turnTrail: string[]
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AppLayoutStatusProps {
|
|
||||||
cwdLabel: string
|
|
||||||
durationLabel: string
|
|
||||||
showStickyPrompt: boolean
|
|
||||||
statusColor: string
|
|
||||||
stickyPrompt: string
|
|
||||||
voiceLabel: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AppLayoutTranscriptProps {
|
|
||||||
historyItems: Msg[]
|
|
||||||
scrollRef: RefObject<ScrollBoxHandle | null>
|
|
||||||
virtualHistory: VirtualHistoryState
|
|
||||||
virtualRows: TranscriptRow[]
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AppLayoutProps {
|
|
||||||
actions: AppLayoutActions
|
|
||||||
composer: AppLayoutComposerProps
|
|
||||||
mouseTracking: boolean
|
|
||||||
progress: AppLayoutProgressProps
|
|
||||||
status: AppLayoutStatusProps
|
|
||||||
transcript: AppLayoutTranscriptProps
|
|
||||||
}
|
|
||||||
|
|
||||||
export function AppLayout({ actions, composer, mouseTracking, progress, status, transcript }: AppLayoutProps) {
|
export function AppLayout({ actions, composer, mouseTracking, progress, status, transcript }: AppLayoutProps) {
|
||||||
const ui = useStore($uiState)
|
const ui = useStore($uiState)
|
||||||
const isBlocked = useStore($isBlocked)
|
const isBlocked = useStore($isBlocked)
|
||||||
@@ -125,8 +60,10 @@ export function AppLayout({ actions, composer, mouseTracking, progress, status,
|
|||||||
reasoning={progress.reasoning}
|
reasoning={progress.reasoning}
|
||||||
reasoningActive={progress.reasoningActive}
|
reasoningActive={progress.reasoningActive}
|
||||||
reasoningStreaming={progress.reasoningStreaming}
|
reasoningStreaming={progress.reasoningStreaming}
|
||||||
|
reasoningTokens={progress.reasoningTokens}
|
||||||
t={ui.theme}
|
t={ui.theme}
|
||||||
tools={progress.tools}
|
tools={progress.tools}
|
||||||
|
toolTokens={progress.toolTokens}
|
||||||
trail={progress.turnTrail}
|
trail={progress.turnTrail}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { Box, Text } from '@hermes/ink'
|
|||||||
import { useStore } from '@nanostores/react'
|
import { useStore } from '@nanostores/react'
|
||||||
|
|
||||||
import { useGateway } from '../app/gatewayContext.js'
|
import { useGateway } from '../app/gatewayContext.js'
|
||||||
import type { CompletionItem } from '../app/interfaces.js'
|
import type { AppOverlaysProps } from '../app/interfaces.js'
|
||||||
import { $overlayState, patchOverlayState } from '../app/overlayStore.js'
|
import { $overlayState, patchOverlayState } from '../app/overlayStore.js'
|
||||||
import { $uiState } from '../app/uiStore.js'
|
import { $uiState } from '../app/uiStore.js'
|
||||||
|
|
||||||
@@ -12,19 +12,6 @@ import { ModelPicker } from './modelPicker.js'
|
|||||||
import { ApprovalPrompt, ClarifyPrompt } from './prompts.js'
|
import { ApprovalPrompt, ClarifyPrompt } from './prompts.js'
|
||||||
import { SessionPicker } from './sessionPicker.js'
|
import { SessionPicker } from './sessionPicker.js'
|
||||||
|
|
||||||
export interface AppOverlaysProps {
|
|
||||||
cols: number
|
|
||||||
compIdx: number
|
|
||||||
completions: CompletionItem[]
|
|
||||||
onApprovalChoice: (choice: string) => void
|
|
||||||
onClarifyAnswer: (value: string) => void
|
|
||||||
onModelSelect: (value: string) => void
|
|
||||||
onPickerSelect: (sessionId: string) => void
|
|
||||||
onSecretSubmit: (value: string) => void
|
|
||||||
onSudoSubmit: (pw: string) => void
|
|
||||||
pagerPageSize: number
|
|
||||||
}
|
|
||||||
|
|
||||||
export function AppOverlays({
|
export function AppOverlays({
|
||||||
cols,
|
cols,
|
||||||
compIdx,
|
compIdx,
|
||||||
|
|||||||
@@ -85,7 +85,14 @@ export const MessageLine = memo(function MessageLine({
|
|||||||
>
|
>
|
||||||
{showDetails && (
|
{showDetails && (
|
||||||
<Box flexDirection="column" marginBottom={1}>
|
<Box flexDirection="column" marginBottom={1}>
|
||||||
<ToolTrail detailsMode={detailsMode} reasoning={thinking} t={t} trail={msg.tools} />
|
<ToolTrail
|
||||||
|
detailsMode={detailsMode}
|
||||||
|
reasoning={thinking}
|
||||||
|
reasoningTokens={msg.thinkingTokens}
|
||||||
|
t={t}
|
||||||
|
toolTokens={msg.toolTokens}
|
||||||
|
trail={msg.tools}
|
||||||
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import { Box, Text } from '@hermes/ink'
|
import { Box, Text } from '@hermes/ink'
|
||||||
import { memo, type ReactNode, useEffect, useState } from 'react'
|
import { memo, type ReactNode, useEffect, useMemo, useState } from 'react'
|
||||||
import spinners, { type BrailleSpinnerName } from 'unicode-animations'
|
import spinners, { type BrailleSpinnerName } from 'unicode-animations'
|
||||||
|
|
||||||
import { FACES, VERBS } from '../constants.js'
|
|
||||||
import {
|
import {
|
||||||
|
estimateTokensRough,
|
||||||
|
fmtK,
|
||||||
formatToolCall,
|
formatToolCall,
|
||||||
parseToolTrailResultLine,
|
parseToolTrailResultLine,
|
||||||
pick,
|
pick,
|
||||||
@@ -89,6 +90,7 @@ function Chevron({
|
|||||||
count,
|
count,
|
||||||
onClick,
|
onClick,
|
||||||
open,
|
open,
|
||||||
|
suffix,
|
||||||
t,
|
t,
|
||||||
title,
|
title,
|
||||||
tone = 'dim'
|
tone = 'dim'
|
||||||
@@ -96,6 +98,7 @@ function Chevron({
|
|||||||
count?: number
|
count?: number
|
||||||
onClick: () => void
|
onClick: () => void
|
||||||
open: boolean
|
open: boolean
|
||||||
|
suffix?: string
|
||||||
t: Theme
|
t: Theme
|
||||||
title: string
|
title: string
|
||||||
tone?: 'dim' | 'error' | 'warn'
|
tone?: 'dim' | 'error' | 'warn'
|
||||||
@@ -108,6 +111,12 @@ function Chevron({
|
|||||||
<Text color={t.color.amber}>{open ? '▾ ' : '▸ '}</Text>
|
<Text color={t.color.amber}>{open ? '▾ ' : '▸ '}</Text>
|
||||||
{title}
|
{title}
|
||||||
{typeof count === 'number' ? ` (${count})` : ''}
|
{typeof count === 'number' ? ` (${count})` : ''}
|
||||||
|
{suffix ? (
|
||||||
|
<Text color={t.color.statusFg} dimColor>
|
||||||
|
{' '}
|
||||||
|
{suffix}
|
||||||
|
</Text>
|
||||||
|
) : null}
|
||||||
</Text>
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
)
|
)
|
||||||
@@ -128,29 +137,35 @@ export const Thinking = memo(function Thinking({
|
|||||||
streaming?: boolean
|
streaming?: boolean
|
||||||
t: Theme
|
t: Theme
|
||||||
}) {
|
}) {
|
||||||
const [tick, setTick] = useState(0)
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const id = setInterval(() => setTick(v => v + 1), 1100)
|
|
||||||
|
|
||||||
return () => clearInterval(id)
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const preview = thinkingPreview(reasoning, mode, THINKING_COT_MAX)
|
const preview = thinkingPreview(reasoning, mode, THINKING_COT_MAX)
|
||||||
|
const lines = useMemo(() => preview.split('\n').map(line => line.replace(/\t/g, ' ')), [preview])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box flexDirection="column">
|
<Box flexDirection="column">
|
||||||
<Text color={t.color.dim}>
|
|
||||||
<Spinner color={t.color.dim} /> {FACES[tick % FACES.length] ?? '(•_•)'}{' '}
|
|
||||||
{VERBS[tick % VERBS.length] ?? 'thinking'}…
|
|
||||||
</Text>
|
|
||||||
|
|
||||||
{preview ? (
|
{preview ? (
|
||||||
<Text color={t.color.dim} dimColor wrap={mode === 'full' ? 'wrap-trim' : 'truncate-end'}>
|
mode === 'full' ? (
|
||||||
<Text dimColor>└ </Text>
|
<Box flexDirection="row">
|
||||||
{preview}
|
<Text color={t.color.dim} dimColor>
|
||||||
<StreamCursor color={t.color.dim} dimColor streaming={streaming} visible={active} />
|
└{' '}
|
||||||
</Text>
|
</Text>
|
||||||
|
<Box flexDirection="column" flexGrow={1}>
|
||||||
|
{lines.map((line, index) => (
|
||||||
|
<Text color={t.color.dim} dimColor key={index} wrap="wrap-trim">
|
||||||
|
{line || ' '}
|
||||||
|
{index === lines.length - 1 ? (
|
||||||
|
<StreamCursor color={t.color.dim} dimColor streaming={streaming} visible={active} />
|
||||||
|
) : null}
|
||||||
|
</Text>
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
) : (
|
||||||
|
<Text color={t.color.dim} dimColor wrap="truncate-end">
|
||||||
|
<Text dimColor>└ </Text>
|
||||||
|
{preview}
|
||||||
|
<StreamCursor color={t.color.dim} dimColor streaming={streaming} visible={active} />
|
||||||
|
</Text>
|
||||||
|
)
|
||||||
) : active ? (
|
) : active ? (
|
||||||
<Text color={t.color.dim} dimColor>
|
<Text color={t.color.dim} dimColor>
|
||||||
<Text dimColor>└ </Text>
|
<Text dimColor>└ </Text>
|
||||||
@@ -175,9 +190,11 @@ export const ToolTrail = memo(function ToolTrail({
|
|||||||
detailsMode = 'collapsed',
|
detailsMode = 'collapsed',
|
||||||
reasoningActive = false,
|
reasoningActive = false,
|
||||||
reasoning = '',
|
reasoning = '',
|
||||||
|
reasoningTokens,
|
||||||
reasoningStreaming = false,
|
reasoningStreaming = false,
|
||||||
t,
|
t,
|
||||||
tools = [],
|
tools = [],
|
||||||
|
toolTokens,
|
||||||
trail = [],
|
trail = [],
|
||||||
activity = []
|
activity = []
|
||||||
}: {
|
}: {
|
||||||
@@ -185,9 +202,11 @@ export const ToolTrail = memo(function ToolTrail({
|
|||||||
detailsMode?: DetailsMode
|
detailsMode?: DetailsMode
|
||||||
reasoningActive?: boolean
|
reasoningActive?: boolean
|
||||||
reasoning?: string
|
reasoning?: string
|
||||||
|
reasoningTokens?: number
|
||||||
reasoningStreaming?: boolean
|
reasoningStreaming?: boolean
|
||||||
t: Theme
|
t: Theme
|
||||||
tools?: ActiveTool[]
|
tools?: ActiveTool[]
|
||||||
|
toolTokens?: number
|
||||||
trail?: string[]
|
trail?: string[]
|
||||||
activity?: ActivityItem[]
|
activity?: ActivityItem[]
|
||||||
}) {
|
}) {
|
||||||
@@ -311,6 +330,17 @@ export const ToolTrail = memo(function ToolTrail({
|
|||||||
const hasTools = groups.length > 0
|
const hasTools = groups.length > 0
|
||||||
const hasMeta = meta.length > 0
|
const hasMeta = meta.length > 0
|
||||||
const hasThinking = !!cot || reasoningActive || (busy && !hasTools)
|
const hasThinking = !!cot || reasoningActive || (busy && !hasTools)
|
||||||
|
const thinkingLive = reasoningActive || reasoningStreaming
|
||||||
|
|
||||||
|
const tokenCount = reasoningTokens !== undefined ? reasoningTokens : reasoning ? estimateTokensRough(reasoning) : 0
|
||||||
|
|
||||||
|
const toolTokenCount = toolTokens ?? 0
|
||||||
|
const totalTokenCount = tokenCount + toolTokenCount
|
||||||
|
const thinkingTokensLabel = tokenCount > 0 ? `~${fmtK(tokenCount)} tokens` : null
|
||||||
|
|
||||||
|
const toolTokensLabel = toolTokens !== undefined && toolTokens > 0 ? `~${fmtK(toolTokens)} tokens` : undefined
|
||||||
|
|
||||||
|
const totalTokensLabel = tokenCount > 0 && toolTokenCount > 0 ? `~${fmtK(totalTokenCount)} total` : null
|
||||||
|
|
||||||
// ── Hidden: errors/warnings only ──────────────────────────────
|
// ── Hidden: errors/warnings only ──────────────────────────────
|
||||||
|
|
||||||
@@ -368,6 +398,13 @@ export const ToolTrail = memo(function ToolTrail({
|
|||||||
))
|
))
|
||||||
: null
|
: null
|
||||||
|
|
||||||
|
const totalBlock = totalTokensLabel ? (
|
||||||
|
<Text color={t.color.statusFg} dimColor>
|
||||||
|
<Text color={t.color.amber}>Σ </Text>
|
||||||
|
{totalTokensLabel}
|
||||||
|
</Text>
|
||||||
|
) : null
|
||||||
|
|
||||||
// ── Expanded: flat, no accordions ──────────────────────────────
|
// ── Expanded: flat, no accordions ──────────────────────────────
|
||||||
|
|
||||||
if (detailsMode === 'expanded') {
|
if (detailsMode === 'expanded') {
|
||||||
@@ -376,6 +413,7 @@ export const ToolTrail = memo(function ToolTrail({
|
|||||||
{thinkingBlock}
|
{thinkingBlock}
|
||||||
{toolBlock}
|
{toolBlock}
|
||||||
{metaBlock}
|
{metaBlock}
|
||||||
|
{totalBlock}
|
||||||
</Box>
|
</Box>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -392,7 +430,20 @@ export const ToolTrail = memo(function ToolTrail({
|
|||||||
<Box flexDirection="column">
|
<Box flexDirection="column">
|
||||||
{hasThinking && (
|
{hasThinking && (
|
||||||
<>
|
<>
|
||||||
<Chevron onClick={() => setOpenThinking(v => !v)} open={openThinking} t={t} title="Thinking" />
|
<Box onClick={() => setOpenThinking(v => !v)}>
|
||||||
|
<Text color={t.color.dim} dimColor={!thinkingLive}>
|
||||||
|
<Text color={t.color.amber}>{openThinking ? '▾ ' : '▸ '}</Text>
|
||||||
|
<Text bold={thinkingLive} color={thinkingLive ? t.color.cornsilk : t.color.dim} dimColor={!thinkingLive}>
|
||||||
|
Thinking
|
||||||
|
</Text>
|
||||||
|
{thinkingTokensLabel ? (
|
||||||
|
<Text color={t.color.statusFg} dimColor>
|
||||||
|
{' '}
|
||||||
|
{thinkingTokensLabel}
|
||||||
|
</Text>
|
||||||
|
) : null}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
{openThinking && thinkingBlock}
|
{openThinking && thinkingBlock}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@@ -403,6 +454,7 @@ export const ToolTrail = memo(function ToolTrail({
|
|||||||
count={groups.length}
|
count={groups.length}
|
||||||
onClick={() => setOpenTools(v => !v)}
|
onClick={() => setOpenTools(v => !v)}
|
||||||
open={openTools}
|
open={openTools}
|
||||||
|
suffix={toolTokensLabel}
|
||||||
t={t}
|
t={t}
|
||||||
title="Tool calls"
|
title="Tool calls"
|
||||||
/>
|
/>
|
||||||
@@ -423,6 +475,8 @@ export const ToolTrail = memo(function ToolTrail({
|
|||||||
{openMeta && metaBlock}
|
{openMeta && metaBlock}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{totalBlock}
|
||||||
</Box>
|
</Box>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { type ChildProcess, spawn } from 'node:child_process'
|
import { type ChildProcess, spawn } from 'node:child_process'
|
||||||
import { EventEmitter } from 'node:events'
|
import { EventEmitter } from 'node:events'
|
||||||
|
import { existsSync } from 'node:fs'
|
||||||
import { delimiter, resolve } from 'node:path'
|
import { delimiter, resolve } from 'node:path'
|
||||||
import { createInterface } from 'node:readline'
|
import { createInterface } from 'node:readline'
|
||||||
|
|
||||||
@@ -8,6 +9,39 @@ const MAX_LOG_PREVIEW = 240
|
|||||||
const STARTUP_TIMEOUT_MS = Math.max(5000, parseInt(process.env.HERMES_TUI_STARTUP_TIMEOUT_MS ?? '15000', 10) || 15000)
|
const STARTUP_TIMEOUT_MS = Math.max(5000, parseInt(process.env.HERMES_TUI_STARTUP_TIMEOUT_MS ?? '15000', 10) || 15000)
|
||||||
const REQUEST_TIMEOUT_MS = Math.max(30000, parseInt(process.env.HERMES_TUI_RPC_TIMEOUT_MS ?? '120000', 10) || 120000)
|
const REQUEST_TIMEOUT_MS = Math.max(30000, parseInt(process.env.HERMES_TUI_RPC_TIMEOUT_MS ?? '120000', 10) || 120000)
|
||||||
|
|
||||||
|
const resolvePython = (root: string) => {
|
||||||
|
const configured = process.env.HERMES_PYTHON?.trim()
|
||||||
|
|
||||||
|
if (configured) {
|
||||||
|
return configured
|
||||||
|
}
|
||||||
|
|
||||||
|
const envPython = process.env.PYTHON?.trim()
|
||||||
|
|
||||||
|
if (envPython) {
|
||||||
|
return envPython
|
||||||
|
}
|
||||||
|
|
||||||
|
const venv = process.env.VIRTUAL_ENV?.trim()
|
||||||
|
|
||||||
|
const candidates = [
|
||||||
|
venv ? resolve(venv, 'bin/python') : '',
|
||||||
|
venv ? resolve(venv, 'Scripts/python.exe') : '',
|
||||||
|
resolve(root, '.venv/bin/python'),
|
||||||
|
resolve(root, '.venv/bin/python3'),
|
||||||
|
resolve(root, 'venv/bin/python'),
|
||||||
|
resolve(root, 'venv/bin/python3')
|
||||||
|
].filter(Boolean)
|
||||||
|
|
||||||
|
const hit = candidates.find(path => existsSync(path))
|
||||||
|
|
||||||
|
if (hit) {
|
||||||
|
return hit
|
||||||
|
}
|
||||||
|
|
||||||
|
return process.platform === 'win32' ? 'python' : 'python3'
|
||||||
|
}
|
||||||
|
|
||||||
export interface GatewayEvent {
|
export interface GatewayEvent {
|
||||||
type: string
|
type: string
|
||||||
session_id?: string
|
session_id?: string
|
||||||
@@ -53,7 +87,7 @@ export class GatewayClient extends EventEmitter {
|
|||||||
|
|
||||||
start() {
|
start() {
|
||||||
const root = process.env.HERMES_PYTHON_SRC_ROOT ?? resolve(import.meta.dirname, '../../')
|
const root = process.env.HERMES_PYTHON_SRC_ROOT ?? resolve(import.meta.dirname, '../../')
|
||||||
const python = process.env.HERMES_PYTHON ?? resolve(root, 'venv/bin/python')
|
const python = resolvePython(root)
|
||||||
const cwd = process.env.HERMES_CWD || root
|
const cwd = process.env.HERMES_CWD || root
|
||||||
const env = { ...process.env }
|
const env = { ...process.env }
|
||||||
const pyPath = (env.PYTHONPATH ?? '').trim()
|
const pyPath = (env.PYTHONPATH ?? '').trim()
|
||||||
|
|||||||
@@ -211,7 +211,7 @@ const COMPACT_NUMBER = new Intl.NumberFormat('en-US', {
|
|||||||
notation: 'compact'
|
notation: 'compact'
|
||||||
})
|
})
|
||||||
|
|
||||||
export const fmtK = (n: number) => COMPACT_NUMBER.format(n)
|
export const fmtK = (n: number) => COMPACT_NUMBER.format(n).replace(/[KMBT]$/, s => s.toLowerCase())
|
||||||
|
|
||||||
export const hasInterpolation = (s: string) => {
|
export const hasInterpolation = (s: string) => {
|
||||||
INTERPOLATION_RE.lastIndex = 0
|
INTERPOLATION_RE.lastIndex = 0
|
||||||
|
|||||||
@@ -29,6 +29,8 @@ export interface Msg {
|
|||||||
info?: SessionInfo
|
info?: SessionInfo
|
||||||
panelData?: PanelData
|
panelData?: PanelData
|
||||||
thinking?: string
|
thinking?: string
|
||||||
|
thinkingTokens?: number
|
||||||
|
toolTokens?: number
|
||||||
tools?: string[]
|
tools?: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user