mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-08 03:01:47 +00:00
refactor(tui): store-driven turn state + slash registry + module split
Hoist turn state from a 286-line hook into $turnState atom + turnController
singleton. createGatewayEventHandler becomes a typed dispatch over the
controller; its ctx shrinks from 30 fields to 5. Event-handler refs and 16
threaded actions are gone.
Fold three createSlash*Handler factories into a data-driven SlashCommand[]
registry under slash/commands/{core,session,ops}.ts. Aliases are data;
findSlashCommand does name+alias lookup. Shared guarded/guardedErr combinator
in slash/guarded.ts.
Split constants.ts + app/helpers.ts into config/ (timing/limits/env),
content/ (faces/placeholders/hotkeys/verbs/charms/fortunes), domain/ (roles/
details/messages/paths/slash/viewport/usage), protocol/ (interpolation/paste).
Type every RPC response in gatewayTypes.ts (26 new interfaces); drop all
`(r: any)` across slash + main app.
Shrink useMainApp from 1216 -> 646 lines by extracting useSessionLifecycle,
useSubmission, useConfigSync. Add <Fg> themed primitive and strip ~50
`as any` color casts.
Tests: 50 passing. Build + type-check clean.
This commit is contained in:
parent
9c71f3a6ea
commit
68ecdb6e26
56 changed files with 3666 additions and 4117 deletions
185
ui-tui/src/app/useSessionLifecycle.ts
Normal file
185
ui-tui/src/app/useSessionLifecycle.ts
Normal file
|
|
@ -0,0 +1,185 @@
|
|||
import { useCallback } from 'react'
|
||||
|
||||
import { introMsg, toTranscriptMessages } from '../domain/messages.js'
|
||||
import { ZERO } from '../domain/usage.js'
|
||||
import { type GatewayClient } from '../gatewayClient.js'
|
||||
import type { SessionCloseResponse, SessionCreateResponse, SessionResumeResponse } from '../gatewayTypes.js'
|
||||
import { asRpcResult } from '../lib/rpc.js'
|
||||
import type { Msg, SessionInfo, Usage } from '../types.js'
|
||||
|
||||
import type { ComposerActions, GatewayRpc, StateSetter } from './interfaces.js'
|
||||
import { patchOverlayState } from './overlayStore.js'
|
||||
import { turnController } from './turnController.js'
|
||||
import { patchTurnState } from './turnStore.js'
|
||||
import { getUiState, patchUiState } from './uiStore.js'
|
||||
|
||||
const usageFrom = (info: null | SessionInfo): Usage => (info?.usage ? { ...ZERO, ...info.usage } : ZERO)
|
||||
|
||||
const trimTail = (items: Msg[]) => {
|
||||
const q = [...items]
|
||||
|
||||
while (q.at(-1)?.role === 'assistant' || q.at(-1)?.role === 'tool') {
|
||||
q.pop()
|
||||
}
|
||||
|
||||
if (q.at(-1)?.role === 'user') {
|
||||
q.pop()
|
||||
}
|
||||
|
||||
return q
|
||||
}
|
||||
|
||||
export interface UseSessionLifecycleOptions {
|
||||
colsRef: { current: number }
|
||||
composerActions: ComposerActions
|
||||
gw: GatewayClient
|
||||
rpc: GatewayRpc
|
||||
setHistoryItems: StateSetter<Msg[]>
|
||||
setLastUserMsg: StateSetter<string>
|
||||
setSessionStartedAt: StateSetter<number>
|
||||
setStickyPrompt: StateSetter<string>
|
||||
setVoiceProcessing: StateSetter<boolean>
|
||||
setVoiceRecording: StateSetter<boolean>
|
||||
sys: (text: string) => void
|
||||
}
|
||||
|
||||
export function useSessionLifecycle(opts: UseSessionLifecycleOptions) {
|
||||
const {
|
||||
colsRef,
|
||||
composerActions,
|
||||
gw,
|
||||
rpc,
|
||||
setHistoryItems,
|
||||
setLastUserMsg,
|
||||
setSessionStartedAt,
|
||||
setStickyPrompt,
|
||||
setVoiceProcessing,
|
||||
setVoiceRecording,
|
||||
sys
|
||||
} = opts
|
||||
|
||||
const closeSession = useCallback(
|
||||
(targetSid?: null | string) =>
|
||||
targetSid ? rpc<SessionCloseResponse>('session.close', { session_id: targetSid }) : Promise.resolve(null),
|
||||
[rpc]
|
||||
)
|
||||
|
||||
const resetSession = useCallback(() => {
|
||||
turnController.fullReset()
|
||||
setVoiceRecording(false)
|
||||
setVoiceProcessing(false)
|
||||
patchUiState({ bgTasks: new Set(), info: null, sid: null, usage: ZERO })
|
||||
setHistoryItems([])
|
||||
setLastUserMsg('')
|
||||
setStickyPrompt('')
|
||||
composerActions.setPasteSnips([])
|
||||
}, [composerActions, setHistoryItems, setLastUserMsg, setStickyPrompt, setVoiceProcessing, setVoiceRecording])
|
||||
|
||||
const resetVisibleHistory = useCallback(
|
||||
(info: null | SessionInfo = null) => {
|
||||
turnController.idle()
|
||||
turnController.clearReasoning()
|
||||
turnController.turnTools = []
|
||||
turnController.persistedToolLabels.clear()
|
||||
|
||||
setHistoryItems(info ? [introMsg(info)] : [])
|
||||
setStickyPrompt('')
|
||||
setLastUserMsg('')
|
||||
composerActions.setPasteSnips([])
|
||||
patchTurnState({ activity: [] })
|
||||
patchUiState({ info, usage: usageFrom(info) })
|
||||
},
|
||||
[composerActions, setHistoryItems, setLastUserMsg, setStickyPrompt]
|
||||
)
|
||||
|
||||
const newSession = useCallback(
|
||||
async (msg?: string) => {
|
||||
await closeSession(getUiState().sid)
|
||||
|
||||
const r = await rpc<SessionCreateResponse>('session.create', { cols: colsRef.current })
|
||||
|
||||
if (!r) {
|
||||
return patchUiState({ status: 'ready' })
|
||||
}
|
||||
|
||||
resetSession()
|
||||
setSessionStartedAt(Date.now())
|
||||
patchUiState({ info: r.info ?? null, sid: r.session_id, status: 'ready', usage: usageFrom(r.info ?? null) })
|
||||
|
||||
if (r.info) {
|
||||
setHistoryItems([introMsg(r.info)])
|
||||
}
|
||||
|
||||
if (r.info?.credential_warning) {
|
||||
sys(`warning: ${r.info.credential_warning}`)
|
||||
}
|
||||
|
||||
if (msg) {
|
||||
sys(msg)
|
||||
}
|
||||
},
|
||||
[closeSession, colsRef, resetSession, rpc, setHistoryItems, setSessionStartedAt, sys]
|
||||
)
|
||||
|
||||
const resumeById = useCallback(
|
||||
(id: string) => {
|
||||
patchOverlayState({ picker: false })
|
||||
patchUiState({ status: 'resuming…' })
|
||||
|
||||
closeSession(getUiState().sid === id ? null : getUiState().sid).then(() =>
|
||||
gw
|
||||
.request<SessionResumeResponse>('session.resume', { cols: colsRef.current, session_id: id })
|
||||
.then(raw => {
|
||||
const r = asRpcResult<SessionResumeResponse>(raw)
|
||||
|
||||
if (!r) {
|
||||
sys('error: invalid response: session.resume')
|
||||
|
||||
return patchUiState({ status: 'ready' })
|
||||
}
|
||||
|
||||
resetSession()
|
||||
setSessionStartedAt(Date.now())
|
||||
|
||||
const resumed = toTranscriptMessages(r.messages)
|
||||
|
||||
setHistoryItems(r.info ? [introMsg(r.info), ...resumed] : resumed)
|
||||
patchUiState({
|
||||
info: r.info ?? null,
|
||||
sid: r.session_id,
|
||||
status: 'ready',
|
||||
usage: usageFrom(r.info ?? null)
|
||||
})
|
||||
})
|
||||
.catch((e: Error) => {
|
||||
sys(`error: ${e.message}`)
|
||||
patchUiState({ status: 'ready' })
|
||||
})
|
||||
)
|
||||
},
|
||||
[closeSession, colsRef, gw, resetSession, setHistoryItems, setSessionStartedAt, sys]
|
||||
)
|
||||
|
||||
const guardBusySessionSwitch = useCallback(
|
||||
(what = 'switch sessions') => {
|
||||
if (!getUiState().busy) {
|
||||
return false
|
||||
}
|
||||
|
||||
sys(`interrupt the current turn before trying to ${what}`)
|
||||
|
||||
return true
|
||||
},
|
||||
[sys]
|
||||
)
|
||||
|
||||
return {
|
||||
closeSession,
|
||||
guardBusySessionSwitch,
|
||||
newSession,
|
||||
resetSession,
|
||||
resetVisibleHistory,
|
||||
resumeById,
|
||||
trimLastExchange: trimTail
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue