mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-04-30 01:41:43 +00:00
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.
333 lines
8 KiB
TypeScript
333 lines
8 KiB
TypeScript
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 {
|
|
ActiveTool,
|
|
ActivityItem,
|
|
ApprovalReq,
|
|
ClarifyReq,
|
|
DetailsMode,
|
|
Msg,
|
|
PanelSection,
|
|
SecretReq,
|
|
SessionInfo,
|
|
SlashCatalog,
|
|
SubagentProgress,
|
|
SudoReq,
|
|
Usage
|
|
} from '../types.js'
|
|
|
|
export interface StateSetter<T> {
|
|
(value: SetStateAction<T>): void
|
|
}
|
|
|
|
export interface SelectionApi {
|
|
copySelection: () => string
|
|
}
|
|
|
|
export interface CompletionItem {
|
|
display: string
|
|
meta?: string
|
|
text: string
|
|
}
|
|
|
|
export interface GatewayRpc {
|
|
<T extends RpcResult = RpcResult>(method: string, params?: Record<string, unknown>): Promise<null | T>
|
|
}
|
|
|
|
export interface GatewayServices {
|
|
gw: GatewayClient
|
|
rpc: GatewayRpc
|
|
}
|
|
|
|
export interface GatewayProviderProps {
|
|
children: ReactNode
|
|
value: GatewayServices
|
|
}
|
|
|
|
export interface OverlayState {
|
|
approval: ApprovalReq | null
|
|
clarify: ClarifyReq | null
|
|
modelPicker: boolean
|
|
pager: null | PagerState
|
|
picker: boolean
|
|
secret: null | SecretReq
|
|
sudo: null | SudoReq
|
|
}
|
|
|
|
export interface PagerState {
|
|
lines: string[]
|
|
offset: number
|
|
title?: string
|
|
}
|
|
|
|
export interface TranscriptRow {
|
|
index: number
|
|
key: string
|
|
msg: Msg
|
|
}
|
|
|
|
export interface UiState {
|
|
bgTasks: Set<string>
|
|
busy: boolean
|
|
compact: boolean
|
|
detailsMode: DetailsMode
|
|
info: null | SessionInfo
|
|
sid: null | string
|
|
status: string
|
|
statusBar: boolean
|
|
theme: Theme
|
|
usage: Usage
|
|
}
|
|
|
|
export interface VirtualHistoryState {
|
|
bottomSpacer: number
|
|
end: number
|
|
measureRef: (key: string) => (el: unknown) => void
|
|
offsets: ArrayLike<number>
|
|
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<null | number>
|
|
setInput: StateSetter<string>
|
|
setInputBuf: StateSetter<string[]>
|
|
setPasteSnips: StateSetter<PasteSnippet[]>
|
|
setQueueEdit: (index: null | number) => void
|
|
syncQueue: () => void
|
|
}
|
|
|
|
export interface ComposerRefs {
|
|
historyDraftRef: MutableRefObject<string>
|
|
historyRef: MutableRefObject<string[]>
|
|
queueEditRef: MutableRefObject<null | number>
|
|
queueRef: MutableRefObject<string[]>
|
|
submitRef: MutableRefObject<(value: string) => void>
|
|
}
|
|
|
|
export interface ComposerState {
|
|
compIdx: number
|
|
compReplace: number
|
|
completions: CompletionItem[]
|
|
historyIdx: null | number
|
|
input: string
|
|
inputBuf: string[]
|
|
pasteSnips: PasteSnippet[]
|
|
queueEditIdx: null | number
|
|
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 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<null | ScrollBoxHandle>
|
|
scrollWithSelection: (delta: number) => void
|
|
selection: SelectionApi
|
|
stdout?: NodeJS.WriteStream
|
|
}
|
|
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<null | number>
|
|
sendQueued: (text: string) => void
|
|
}
|
|
gateway: GatewayServices
|
|
session: {
|
|
STARTUP_RESUME_ID: string
|
|
colsRef: MutableRefObject<number>
|
|
newSession: (msg?: string) => void
|
|
resetSession: () => void
|
|
setCatalog: StateSetter<null | SlashCatalog>
|
|
}
|
|
system: {
|
|
bellOnComplete: boolean
|
|
stdout?: NodeJS.WriteStream
|
|
sys: (text: string) => void
|
|
}
|
|
transcript: {
|
|
appendMessage: (msg: Msg) => void
|
|
setHistoryItems: StateSetter<Msg[]>
|
|
}
|
|
}
|
|
|
|
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: null | SlashCatalog
|
|
getHistoryItems: () => Msg[]
|
|
getLastUserMsg: () => string
|
|
maybeWarn: (value: unknown) => void
|
|
}
|
|
session: {
|
|
closeSession: (targetSid?: null | string) => Promise<unknown>
|
|
die: () => void
|
|
guardBusySessionSwitch: (what?: string) => boolean
|
|
newSession: (msg?: string) => void
|
|
resetVisibleHistory: (info?: null | SessionInfo) => void
|
|
resumeById: (id: string) => void
|
|
setSessionStartedAt: StateSetter<number>
|
|
}
|
|
slashFlightRef: MutableRefObject<number>
|
|
transcript: {
|
|
page: (text: string, title?: string) => void
|
|
panel: (title: string, sections: PanelSection[]) => void
|
|
send: (text: string) => void
|
|
setHistoryItems: 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: null | number
|
|
queuedDisplay: string[]
|
|
submit: (value: string) => void
|
|
updateInput: StateSetter<string>
|
|
}
|
|
|
|
export interface AppLayoutProgressProps {
|
|
activity: ActivityItem[]
|
|
reasoning: string
|
|
reasoningActive: boolean
|
|
reasoningStreaming: boolean
|
|
reasoningTokens: number
|
|
showProgressArea: boolean
|
|
showStreamingArea: boolean
|
|
streaming: string
|
|
subagents: SubagentProgress[]
|
|
toolTokens: number
|
|
tools: ActiveTool[]
|
|
turnTrail: string[]
|
|
}
|
|
|
|
export interface AppLayoutStatusProps {
|
|
cwdLabel: string
|
|
goodVibesTick: number
|
|
sessionStartedAt: null | number
|
|
showStickyPrompt: boolean
|
|
statusColor: string
|
|
stickyPrompt: string
|
|
voiceLabel: string
|
|
}
|
|
|
|
export interface AppLayoutTranscriptProps {
|
|
historyItems: Msg[]
|
|
scrollRef: RefObject<null | ScrollBoxHandle>
|
|
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
|
|
}
|