feat: add inline token count etc and fix venv
This commit is contained in:
@@ -1,72 +1,18 @@
|
||||
import type { Dispatch, MutableRefObject, SetStateAction } from 'react'
|
||||
|
||||
import type { GatewayEvent } from '../gatewayClient.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 type { Msg, SlashCatalog } from '../types.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 { 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 {
|
||||
const { dequeue, queueEditRef, sendQueued } = ctx.composer
|
||||
@@ -86,12 +32,15 @@ export function createGatewayEventHandler(ctx: GatewayEventHandlerContext): (ev:
|
||||
scheduleReasoning,
|
||||
scheduleStreaming,
|
||||
setActivity,
|
||||
setReasoningTokens,
|
||||
setStreaming,
|
||||
setToolTokens,
|
||||
setTools,
|
||||
setTurnTrail
|
||||
} = ctx.turn.actions
|
||||
|
||||
const {
|
||||
activeToolsRef,
|
||||
bufRef,
|
||||
interruptedRef,
|
||||
lastStatusNoteRef,
|
||||
@@ -99,6 +48,7 @@ export function createGatewayEventHandler(ctx: GatewayEventHandlerContext): (ev:
|
||||
protocolWarnedRef,
|
||||
reasoningRef,
|
||||
statusTimerRef,
|
||||
toolTokenAccRef,
|
||||
toolCompleteRibbonRef,
|
||||
turnToolsRef
|
||||
} = ctx.turn.refs
|
||||
@@ -210,8 +160,12 @@ export function createGatewayEventHandler(ctx: GatewayEventHandlerContext): (ev:
|
||||
clearReasoning()
|
||||
setActivity([])
|
||||
setTurnTrail([])
|
||||
activeToolsRef.current = []
|
||||
setTools([])
|
||||
turnToolsRef.current = []
|
||||
persistedToolLabelsRef.current.clear()
|
||||
toolTokenAccRef.current = 0
|
||||
setToolTokens(0)
|
||||
|
||||
break
|
||||
|
||||
@@ -286,21 +240,46 @@ export function createGatewayEventHandler(ctx: GatewayEventHandlerContext): (ev:
|
||||
case 'reasoning.delta':
|
||||
if (p?.text) {
|
||||
reasoningRef.current += p.text
|
||||
setReasoningTokens(estimateTokensRough(reasoningRef.current))
|
||||
scheduleReasoning()
|
||||
pulseReasoningStreaming()
|
||||
}
|
||||
|
||||
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':
|
||||
if (p?.preview) {
|
||||
setTools(prev => {
|
||||
const index = prev.findIndex(tool => tool.name === p.name)
|
||||
const index = activeToolsRef.current.findIndex(tool => tool.name === p.name)
|
||||
|
||||
return index >= 0
|
||||
? [...prev.slice(0, index), { ...prev[index]!, context: p.preview as string }, ...prev.slice(index + 1)]
|
||||
: prev
|
||||
})
|
||||
if (index >= 0) {
|
||||
const next = [...activeToolsRef.current]
|
||||
|
||||
next[index] = { ...next[index]!, context: p.preview as string }
|
||||
activeToolsRef.current = next
|
||||
setTools(next)
|
||||
}
|
||||
}
|
||||
|
||||
break
|
||||
@@ -311,44 +290,47 @@ export function createGatewayEventHandler(ctx: GatewayEventHandlerContext): (ev:
|
||||
}
|
||||
|
||||
break
|
||||
|
||||
case 'tool.start':
|
||||
case 'tool.start': {
|
||||
pruneTransient()
|
||||
endReasoningPhase()
|
||||
setTools(prev => [
|
||||
...prev,
|
||||
{ id: p.tool_id, name: p.name, context: (p.context as string) || '', startedAt: Date.now() }
|
||||
])
|
||||
const ctx = (p.context as string) || ''
|
||||
const sample = `${String(p.name ?? '')} ${ctx}`.trim()
|
||||
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
|
||||
}
|
||||
|
||||
case 'tool.complete': {
|
||||
toolCompleteRibbonRef.current = null
|
||||
setTools(prev => {
|
||||
const done = prev.find(tool => tool.id === p.tool_id)
|
||||
const name = done?.name ?? p.name
|
||||
const label = toolTrailLabel(name)
|
||||
const done = activeToolsRef.current.find(tool => tool.id === p.tool_id)
|
||||
const name = done?.name ?? p.name
|
||||
const label = toolTrailLabel(name)
|
||||
|
||||
const line = buildToolTrailLine(
|
||||
name,
|
||||
done?.context || '',
|
||||
!!p.error,
|
||||
(p.error as string) || (p.summary as string) || ''
|
||||
)
|
||||
const line = buildToolTrailLine(
|
||||
name,
|
||||
done?.context || '',
|
||||
!!p.error,
|
||||
(p.error as string) || (p.summary as string) || ''
|
||||
)
|
||||
|
||||
const next = [...turnToolsRef.current.filter(item => !sameToolTrailGroup(label, item)), line]
|
||||
const remaining = prev.filter(tool => tool.id !== p.tool_id)
|
||||
const next = [...turnToolsRef.current.filter(item => !sameToolTrailGroup(label, item)), line]
|
||||
|
||||
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) {
|
||||
next.push('analyzing tool output…')
|
||||
}
|
||||
if (!activeToolsRef.current.length) {
|
||||
next.push('analyzing tool output…')
|
||||
}
|
||||
|
||||
turnToolsRef.current = next.slice(-8)
|
||||
setTurnTrail(turnToolsRef.current)
|
||||
|
||||
return remaining
|
||||
})
|
||||
turnToolsRef.current = next.slice(-8)
|
||||
setTurnTrail(turnToolsRef.current)
|
||||
|
||||
if (p?.inline_diff) {
|
||||
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 persisted = persistedToolLabelsRef.current
|
||||
const savedReasoning = reasoningRef.current.trim()
|
||||
const savedReasoningTokens = savedReasoning ? estimateTokensRough(savedReasoning) : 0
|
||||
const savedToolTokens = toolTokenAccRef.current
|
||||
|
||||
const savedTools = turnToolsRef.current.filter(
|
||||
line => isToolTrailResultLine(line) && ![...persisted].some(item => sameToolTrailGroup(item, line))
|
||||
@@ -426,15 +410,13 @@ export function createGatewayEventHandler(ctx: GatewayEventHandlerContext): (ev:
|
||||
|
||||
const wasInterrupted = interruptedRef.current
|
||||
|
||||
idle()
|
||||
clearReasoning()
|
||||
setStreaming('')
|
||||
|
||||
if (!wasInterrupted) {
|
||||
appendMessage({
|
||||
role: 'assistant',
|
||||
text: finalText,
|
||||
thinking: savedReasoning || undefined,
|
||||
thinkingTokens: savedReasoning ? savedReasoningTokens : undefined,
|
||||
toolTokens: savedTools.length ? savedToolTokens : undefined,
|
||||
tools: savedTools.length ? savedTools : undefined
|
||||
})
|
||||
|
||||
@@ -443,6 +425,9 @@ export function createGatewayEventHandler(ctx: GatewayEventHandlerContext): (ev:
|
||||
}
|
||||
}
|
||||
|
||||
idle()
|
||||
clearReasoning()
|
||||
|
||||
turnToolsRef.current = []
|
||||
persistedToolLabelsRef.current.clear()
|
||||
setActivity([])
|
||||
|
||||
@@ -1,57 +1,14 @@
|
||||
import type { Dispatch, MutableRefObject, SetStateAction } from 'react'
|
||||
|
||||
import { HOTKEYS } from '../constants.js'
|
||||
import { writeOsc52Clipboard } from '../lib/osc52.js'
|
||||
import { asRpcResult, rpcErrorMessage } from '../lib/rpc.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 type { GatewayServices } from './interfaces.js'
|
||||
import type { SlashHandlerContext } from './interfaces.js'
|
||||
import { patchOverlayState } from './overlayStore.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 {
|
||||
const { enqueue, hasSelection, paste, queueRef, selection, setInput } = ctx.composer
|
||||
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)
|
||||
|
||||
export interface GatewayProviderProps {
|
||||
children: ReactNode
|
||||
value: GatewayServices
|
||||
}
|
||||
|
||||
export function GatewayProvider({ children, value }: GatewayProviderProps) {
|
||||
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']
|
||||
|
||||
export interface PasteSnippet {
|
||||
label: string
|
||||
text: string
|
||||
}
|
||||
|
||||
export const parseDetailsMode = (v: unknown): DetailsMode | null => {
|
||||
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 { RpcResult } from '../lib/rpc.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 {
|
||||
display: string
|
||||
@@ -9,7 +35,7 @@ export interface CompletionItem {
|
||||
}
|
||||
|
||||
export interface GatewayRpc {
|
||||
(method: string, params?: Record<string, unknown>): Promise<any | null>
|
||||
(method: string, params?: Record<string, unknown>): Promise<RpcResult | null>
|
||||
}
|
||||
|
||||
export interface GatewayServices {
|
||||
@@ -17,6 +43,11 @@ export interface GatewayServices {
|
||||
rpc: GatewayRpc
|
||||
}
|
||||
|
||||
export interface GatewayProviderProps {
|
||||
children: ReactNode
|
||||
value: GatewayServices
|
||||
}
|
||||
|
||||
export interface OverlayState {
|
||||
approval: ApprovalReq | null
|
||||
clarify: ClarifyReq | null
|
||||
@@ -65,3 +96,343 @@ export interface VirtualHistoryState {
|
||||
start: 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 { 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 { GatewayClient } from '../gatewayClient.js'
|
||||
import { useCompletion } from '../hooks/useCompletion.js'
|
||||
import { useInputHistory } from '../hooks/useInputHistory.js'
|
||||
import { useQueue } from '../hooks/useQueue.js'
|
||||
import { pasteTokenLabel, stripTrailingPasteNewlines } from '../lib/text.js'
|
||||
|
||||
import { LARGE_PASTE } from './constants.js'
|
||||
import type { PasteSnippet } from './helpers.js'
|
||||
import type { CompletionItem } from './interfaces.js'
|
||||
import type { PasteSnippet, UseComposerStateOptions, UseComposerStateResult } from './interfaces.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 {
|
||||
const [input, setInput] = useState('')
|
||||
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 type { Dispatch, RefObject, SetStateAction } from 'react'
|
||||
|
||||
import type { Msg } from '../types.js'
|
||||
|
||||
import type { GatewayServices } from './interfaces.js'
|
||||
import type { InputHandlerContext, InputHandlerResult } from './interfaces.js'
|
||||
import { $isBlocked, $overlayState, patchOverlayState } from './overlayStore.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 {
|
||||
const { actions, composer, gateway, terminal, turn, voice, wheelStep } = ctx
|
||||
|
||||
@@ -1,89 +1,26 @@
|
||||
import {
|
||||
type Dispatch,
|
||||
type MutableRefObject,
|
||||
type SetStateAction,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState
|
||||
} from 'react'
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
|
||||
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 type { ToolCompleteRibbon } from './interfaces.js'
|
||||
import type { InterruptTurnOptions, ToolCompleteRibbon, UseTurnStateResult } from './interfaces.js'
|
||||
import { resetOverlayState } from './overlayStore.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 {
|
||||
const [activity, setActivity] = useState<ActivityItem[]>([])
|
||||
const [reasoning, setReasoning] = useState('')
|
||||
const [reasoningTokens, setReasoningTokens] = useState(0)
|
||||
const [reasoningActive, setReasoningActive] = useState(false)
|
||||
const [toolTokens, setToolTokens] = useState(0)
|
||||
const [reasoningStreaming, setReasoningStreaming] = useState(false)
|
||||
const [streaming, setStreaming] = useState('')
|
||||
const [tools, setTools] = useState<ActiveTool[]>([])
|
||||
const [turnTrail, setTurnTrail] = useState<string[]>([])
|
||||
|
||||
const activityIdRef = useRef(0)
|
||||
const activeToolsRef = useRef<ActiveTool[]>([])
|
||||
const bufRef = useRef('')
|
||||
const interruptedRef = useRef(false)
|
||||
const lastStatusNoteRef = useRef('')
|
||||
@@ -94,6 +31,7 @@ export function useTurnState(): UseTurnStateResult {
|
||||
const reasoningTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
||||
const statusTimerRef = 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 turnToolsRef = useRef<string[]>([])
|
||||
|
||||
@@ -200,11 +138,15 @@ export function useTurnState(): UseTurnStateResult {
|
||||
}
|
||||
|
||||
reasoningRef.current = ''
|
||||
toolTokenAccRef.current = 0
|
||||
setReasoning('')
|
||||
setReasoningTokens(0)
|
||||
setToolTokens(0)
|
||||
}, [])
|
||||
|
||||
const idle = useCallback(() => {
|
||||
endReasoningPhase()
|
||||
activeToolsRef.current = []
|
||||
setTools([])
|
||||
setTurnTrail([])
|
||||
patchUiState({ busy: false })
|
||||
@@ -263,13 +205,16 @@ export function useTurnState(): UseTurnStateResult {
|
||||
scheduleStreaming,
|
||||
setActivity,
|
||||
setReasoning,
|
||||
setReasoningTokens,
|
||||
setReasoningActive,
|
||||
setToolTokens,
|
||||
setReasoningStreaming,
|
||||
setStreaming,
|
||||
setTools,
|
||||
setTurnTrail
|
||||
},
|
||||
refs: {
|
||||
activeToolsRef,
|
||||
bufRef,
|
||||
interruptedRef,
|
||||
lastStatusNoteRef,
|
||||
@@ -280,13 +225,16 @@ export function useTurnState(): UseTurnStateResult {
|
||||
reasoningTimerRef,
|
||||
statusTimerRef,
|
||||
streamTimerRef,
|
||||
toolTokenAccRef,
|
||||
toolCompleteRibbonRef,
|
||||
turnToolsRef
|
||||
},
|
||||
state: {
|
||||
activity,
|
||||
reasoning,
|
||||
reasoningTokens,
|
||||
reasoningActive,
|
||||
toolTokens,
|
||||
reasoningStreaming,
|
||||
streaming,
|
||||
tools,
|
||||
|
||||
Reference in New Issue
Block a user