Something went wrong. Try again.
source dump of claude code forked from oppi.li/claude-code
Something went wrong. Try again.
12 kB · 447 lines
TypeScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448import { HOOK_EVENTS, type HookEvent } from 'src/entrypoints/agentSdkTypes.js'import type { AppState } from 'src/state/AppState.js'import type { Message } from 'src/types/message.js'import { logForDebugging } from '../debug.js'import type { AggregatedHookResult } from '../hooks.js'import type { HookCommand } from '../settings/types.js'import { isHookEqual } from './hooksSettings.js'
type OnHookSuccess = ( hook: HookCommand | FunctionHook, result: AggregatedHookResult,) => void
/** Function hook callback - returns true if check passes, false to block */export type FunctionHookCallback = ( messages: Message[], signal?: AbortSignal,) => boolean | Promise<boolean>
/** * Function hook type with callback embedded. * Session-scoped only, cannot be persisted to settings.json. */export type FunctionHook = { type: 'function' id?: string // Optional unique ID for removal timeout?: number callback: FunctionHookCallback errorMessage: string statusMessage?: string}
type SessionHookMatcher = { matcher: string skillRoot?: string hooks: Array<{ hook: HookCommand | FunctionHook onHookSuccess?: OnHookSuccess }>}
export type SessionStore = { hooks: { [event in HookEvent]?: SessionHookMatcher[] }}
/** * Map (not Record) so .set/.delete don't change the container's identity. * Mutator functions mutate the Map and return prev unchanged, letting * store.ts's Object.is(next, prev) check short-circuit and skip listener * notification. Session hooks are ephemeral per-agent runtime callbacks, * never reactively read (only getAppState() snapshots in the query loop). * Same pattern as agentControllers on LocalWorkflowTaskState. * * This matters under high-concurrency workflows: parallel() with N * schema-mode agents fires N addFunctionHook calls in one synchronous * tick. With a Record + spread, each call cost O(N) to copy the growing * map (O(N²) total) plus fired all ~30 store listeners. With Map: .set() * is O(1), return prev means zero listener fires. */export type SessionHooksState = Map<string, SessionStore>
/** * Add a command or prompt hook to the session. * Session hooks are temporary, in-memory only, and cleared when session ends. */export function addSessionHook( setAppState: (updater: (prev: AppState) => AppState) => void, sessionId: string, event: HookEvent, matcher: string, hook: HookCommand, onHookSuccess?: OnHookSuccess, skillRoot?: string,): void { addHookToSession( setAppState, sessionId, event, matcher, hook, onHookSuccess, skillRoot, )}
/** * Add a function hook to the session. * Function hooks execute TypeScript callbacks in-memory for validation. * @returns The hook ID (for removal) */export function addFunctionHook( setAppState: (updater: (prev: AppState) => AppState) => void, sessionId: string, event: HookEvent, matcher: string, callback: FunctionHookCallback, errorMessage: string, options?: { timeout?: number id?: string },): string { const id = options?.id || `function-hook-${Date.now()}-${Math.random()}` const hook: FunctionHook = { type: 'function', id, timeout: options?.timeout || 5000, callback, errorMessage, } addHookToSession(setAppState, sessionId, event, matcher, hook) return id}
/** * Remove a function hook by ID from the session. */export function removeFunctionHook( setAppState: (updater: (prev: AppState) => AppState) => void, sessionId: string, event: HookEvent, hookId: string,): void { setAppState(prev => { const store = prev.sessionHooks.get(sessionId) if (!store) { return prev }
const eventMatchers = store.hooks[event] || []
// Remove the hook with matching ID from all matchers const updatedMatchers = eventMatchers .map(matcher => { const updatedHooks = matcher.hooks.filter(h => { if (h.hook.type !== 'function') return true return h.hook.id !== hookId })
return updatedHooks.length > 0 ? { ...matcher, hooks: updatedHooks } : null }) .filter((m): m is SessionHookMatcher => m !== null)
const newHooks = updatedMatchers.length > 0 ? { ...store.hooks, [event]: updatedMatchers } : Object.fromEntries( Object.entries(store.hooks).filter(([e]) => e !== event), )
prev.sessionHooks.set(sessionId, { hooks: newHooks }) return prev })
logForDebugging( `Removed function hook ${hookId} for event ${event} in session ${sessionId}`, )}
/** * Internal helper to add a hook to session state */function addHookToSession( setAppState: (updater: (prev: AppState) => AppState) => void, sessionId: string, event: HookEvent, matcher: string, hook: HookCommand | FunctionHook, onHookSuccess?: OnHookSuccess, skillRoot?: string,): void { setAppState(prev => { const store = prev.sessionHooks.get(sessionId) ?? { hooks: {} } const eventMatchers = store.hooks[event] || []
// Find existing matcher or create new one const existingMatcherIndex = eventMatchers.findIndex( m => m.matcher === matcher && m.skillRoot === skillRoot, )
let updatedMatchers: SessionHookMatcher[] if (existingMatcherIndex >= 0) { // Add to existing matcher updatedMatchers = [...eventMatchers] const existingMatcher = updatedMatchers[existingMatcherIndex]! updatedMatchers[existingMatcherIndex] = { matcher: existingMatcher.matcher, skillRoot: existingMatcher.skillRoot, hooks: [...existingMatcher.hooks, { hook, onHookSuccess }], } } else { // Create new matcher updatedMatchers = [ ...eventMatchers, { matcher, skillRoot, hooks: [{ hook, onHookSuccess }], }, ] }
const newHooks = { ...store.hooks, [event]: updatedMatchers }
prev.sessionHooks.set(sessionId, { hooks: newHooks }) return prev })
logForDebugging( `Added session hook for event ${event} in session ${sessionId}`, )}
/** * Remove a specific hook from the session * @param setAppState The function to update the app state * @param sessionId The session ID * @param event The hook event * @param hook The hook command to remove */export function removeSessionHook( setAppState: (updater: (prev: AppState) => AppState) => void, sessionId: string, event: HookEvent, hook: HookCommand,): void { setAppState(prev => { const store = prev.sessionHooks.get(sessionId) if (!store) { return prev }
const eventMatchers = store.hooks[event] || []
// Remove the hook from all matchers const updatedMatchers = eventMatchers .map(matcher => { const updatedHooks = matcher.hooks.filter( h => !isHookEqual(h.hook, hook), )
return updatedHooks.length > 0 ? { ...matcher, hooks: updatedHooks } : null }) .filter((m): m is SessionHookMatcher => m !== null)
const newHooks = updatedMatchers.length > 0 ? { ...store.hooks, [event]: updatedMatchers } : { ...store.hooks }
if (updatedMatchers.length === 0) { delete newHooks[event] }
prev.sessionHooks.set(sessionId, { ...store, hooks: newHooks }) return prev })
logForDebugging( `Removed session hook for event ${event} in session ${sessionId}`, )}
// Extended hook matcher that includes optional skillRoot for skill-scoped hooksexport type SessionDerivedHookMatcher = { matcher: string hooks: HookCommand[] skillRoot?: string}
/** * Convert session hook matchers to regular hook matchers * @param sessionMatchers The session hook matchers to convert * @returns Regular hook matchers (with optional skillRoot preserved) */function convertToHookMatchers( sessionMatchers: SessionHookMatcher[],): SessionDerivedHookMatcher[] { return sessionMatchers.map(sm => ({ matcher: sm.matcher, skillRoot: sm.skillRoot, // Filter out function hooks - they can't be persisted to HookMatcher format hooks: sm.hooks .map(h => h.hook) .filter((h): h is HookCommand => h.type !== 'function'), }))}
/** * Get all session hooks for a specific event (excluding function hooks) * @param appState The app state * @param sessionId The session ID * @param event Optional event to filter by * @returns Hook matchers for the event, or all hooks if no event specified */export function getSessionHooks( appState: AppState, sessionId: string, event?: HookEvent,): Map<HookEvent, SessionDerivedHookMatcher[]> { const store = appState.sessionHooks.get(sessionId) if (!store) { return new Map() }
const result = new Map<HookEvent, SessionDerivedHookMatcher[]>()
if (event) { const sessionMatchers = store.hooks[event] if (sessionMatchers) { result.set(event, convertToHookMatchers(sessionMatchers)) } return result }
for (const evt of HOOK_EVENTS) { const sessionMatchers = store.hooks[evt] if (sessionMatchers) { result.set(evt, convertToHookMatchers(sessionMatchers)) } }
return result}
type FunctionHookMatcher = { matcher: string hooks: FunctionHook[]}
/** * Get all session function hooks for a specific event * Function hooks are kept separate because they can't be persisted to HookMatcher format. * @param appState The app state * @param sessionId The session ID * @param event Optional event to filter by * @returns Function hook matchers for the event */export function getSessionFunctionHooks( appState: AppState, sessionId: string, event?: HookEvent,): Map<HookEvent, FunctionHookMatcher[]> { const store = appState.sessionHooks.get(sessionId) if (!store) { return new Map() }
const result = new Map<HookEvent, FunctionHookMatcher[]>()
const extractFunctionHooks = ( sessionMatchers: SessionHookMatcher[], ): FunctionHookMatcher[] => { return sessionMatchers .map(sm => ({ matcher: sm.matcher, hooks: sm.hooks .map(h => h.hook) .filter((h): h is FunctionHook => h.type === 'function'), })) .filter(m => m.hooks.length > 0) }
if (event) { const sessionMatchers = store.hooks[event] if (sessionMatchers) { const functionMatchers = extractFunctionHooks(sessionMatchers) if (functionMatchers.length > 0) { result.set(event, functionMatchers) } } return result }
for (const evt of HOOK_EVENTS) { const sessionMatchers = store.hooks[evt] if (sessionMatchers) { const functionMatchers = extractFunctionHooks(sessionMatchers) if (functionMatchers.length > 0) { result.set(evt, functionMatchers) } } }
return result}
/** * Get the full hook entry (including callbacks) for a specific session hook */export function getSessionHookCallback( appState: AppState, sessionId: string, event: HookEvent, matcher: string, hook: HookCommand | FunctionHook,): | { hook: HookCommand | FunctionHook onHookSuccess?: OnHookSuccess } | undefined { const store = appState.sessionHooks.get(sessionId) if (!store) { return undefined }
const eventMatchers = store.hooks[event] if (!eventMatchers) { return undefined }
// Find the hook in the matchers for (const matcherEntry of eventMatchers) { if (matcherEntry.matcher === matcher || matcher === '') { const hookEntry = matcherEntry.hooks.find(h => isHookEqual(h.hook, hook)) if (hookEntry) { return hookEntry } } }
return undefined}
/** * Clear all session hooks for a specific session * @param setAppState The function to update the app state * @param sessionId The session ID */export function clearSessionHooks( setAppState: (updater: (prev: AppState) => AppState) => void, sessionId: string,): void { setAppState(prev => { prev.sessionHooks.delete(sessionId) return prev })
logForDebugging(`Cleared all session hooks for session ${sessionId}`)}