claude-code/spec/07_hooks.md
2026-04-01 01:20:27 +05:30

2136 lines
82 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Claude Code — React Hooks
This document covers every hook in `src/hooks/`, `src/hooks/toolPermission/`, and `src/hooks/notifs/`. For each hook the entry covers: purpose, parameters/props, return value, key logic and side effects, and dependencies.
---
## Table of Contents
1. [Core / Utility Hooks](#core--utility-hooks)
2. [Input & Text Editing Hooks](#input--text-editing-hooks)
3. [Permission & Tool-Use Hooks](#permission--tool-use-hooks)
4. [Swarm / Teammate Hooks](#swarm--teammate-hooks)
5. [IDE Integration Hooks](#ide-integration-hooks)
6. [Remote & Session Hooks](#remote--session-hooks)
7. [Plugin & Suggestion Hooks](#plugin--suggestion-hooks)
8. [Notification Hooks (`notifs/`)](#notification-hooks-notifs)
9. [Tool Permission Subsystem (`toolPermission/`)](#tool-permission-subsystem-toolpermission)
10. [Non-Hook Utilities in `hooks/`](#non-hook-utilities-in-hooks)
---
## Core / Utility Hooks
### `useAfterFirstRender`
**File:** `hooks/useAfterFirstRender.ts`
**Purpose:** ANT-internal startup-time measurement hook. After the first render it writes startup time to stderr and calls `process.exit(0)` if the `CLAUDE_CODE_EXIT_AFTER_FIRST_RENDER` environment variable is set.
**Parameters:** none
**Return Value:** `void`
**Key Logic:**
- Reads env var `CLAUDE_CODE_EXIT_AFTER_FIRST_RENDER`.
- Uses a `useEffect` on `[]` to fire after first commit.
- Computes elapsed ms from `MACRO.STARTUP_TIMESTAMP`, writes to `process.stderr`, then exits.
**Dependencies:** `useEffect` (React)
---
### `useApiKeyVerification`
**File:** `hooks/useApiKeyVerification.ts`
**Purpose:** Manages the full lifecycle of API key verification — loading, valid, invalid, missing, or error — and exposes a `reverify` callback. Guards against running `apiKeyHelper` scripts before the trust dialog is dismissed to prevent RCE.
**Parameters:** none
**Return Value:** `ApiKeyVerificationResult``{ status: 'loading'|'valid'|'invalid'|'missing'|'error', reverify: () => void, errorMessage?: string }`
**Key Logic:**
- Subscribes to `AppState` for `trustDialogAccepted` and `apiKeyVerificationStatus`.
- Uses `useEffect` to run the verification on mount and whenever `reverify` is called.
- Skips the `apiKeyHelper` process before the trust dialog is shown.
- Returns a stable `reverify` callback via `useCallback`.
**Dependencies:** `useAppState`, `useSetAppState`, `useCallback`, `useEffect`
---
### `useBlink`
**File:** `hooks/useBlink.ts`
**Purpose:** Returns a blinking boolean flag synchronized with an animation-frame clock. Pauses when the terminal is blurred or the component is offscreen (OffscreenFreeze).
**Parameters:**
- `enabled: boolean` — when false, always returns `true` (cursor always visible).
- `intervalMs?: number` — blink period in milliseconds (default: 530).
**Return Value:** `[ref: RefObject<unknown>, isVisible: boolean]`
**Key Logic:**
- Uses `useAnimationFrame` (Ink hook) to read the shared clock counter.
- Divides counter by `intervalMs / frameMs` and toggles on even/odd.
- Returns same ref from Ink's `useOffscreenFreeze` to pause when not in viewport.
**Dependencies:** `useAnimationFrame` (ink), `useTerminalFocus` (ink), `useRef`, `useMemo`
---
### `useCommandQueue`
**File:** `hooks/useCommandQueue.ts`
**Purpose:** Exposes the current unified command queue as a reactive array. Any component can subscribe to observe queued commands without managing external store subscriptions manually.
**Parameters:** none
**Return Value:** `readonly QueuedCommand[]`
**Key Logic:**
- Wraps `useSyncExternalStore` over `messageQueueManager`'s subscribe/getSnapshot pair.
- Re-renders only when the queue reference changes (not on every push that doesn't change length).
**Dependencies:** `useSyncExternalStore` (React), `messageQueueManager`
---
### `useCopyOnSelect`
**File:** `hooks/useCopyOnSelect.ts`
**Purpose:** Automatically copies selected text to the clipboard when the user releases the mouse (mouseup) or double/triple-clicks. Also exports `useSelectionBgColor` for theming selected text.
**Parameters:**
- `selection: SelectionState` — current ink selection state.
- `isActive: boolean` — only active when true.
- `onCopied?: () => void` — callback fired after clipboard write.
**Return Value:** `void`
**Key Logic:**
- Subscribes to ink mouse events via `useEffect`.
- On mouseup: if selection is non-empty and `isActive`, calls `navigator.clipboard.writeText`.
- `useSelectionBgColor()` reads AppState theme to return the correct highlight color.
**Dependencies:** `useEffect`, `useAppState`, `useCallback`
---
### `useDoublePress`
**File:** `hooks/useDoublePress.ts`
**Purpose:** Returns a callback that implements double-press detection within an 800 ms window. Used for Ctrl+C/D to exit and double-Escape to clear input.
**Parameters:**
- `setPending: (show: boolean) => void` — called with `true` after first press, `false` after timeout.
- `onDoublePress: () => void` — called when the second press occurs within the window.
- `onFirstPress?: () => void` — optional side effect on first press.
**Return Value:** `() => void` — the wrapped press handler
**Key Logic:**
- Tracks `lastPressTime` in a ref.
- On call: if elapsed < 800 ms, calls `onDoublePress` and resets; otherwise calls `setPending(true)`, sets a 800 ms timer to call `setPending(false)`, and optionally calls `onFirstPress`.
**Dependencies:** `useRef`, `useCallback`
---
### `useElapsedTime`
**File:** `hooks/useElapsedTime.ts`
**Purpose:** Computes a human-readable elapsed time string (e.g. `"1m 23s"`) that updates while a task is running and freezes once it ends.
**Parameters:**
- `startTime: number` Unix timestamp (ms) when timing started.
- `isRunning: boolean` when false, elapsed is frozen.
- `ms?: number` update interval in ms (default: 1000).
- `pausedMs?: number` accumulated paused time to subtract.
- `endTime?: number` if provided, freezes at this timestamp.
**Return Value:** `string` formatted elapsed time like `"5s"`, `"1m 23s"`, `"2h 5m"`.
**Key Logic:**
- Uses `useSyncExternalStore` over a timer-based external clock.
- Clock updates every `ms` via `setInterval`; each subscriber gets a stable snapshot until it ticks.
- Formats the delta using `formatDuration`.
**Dependencies:** `useSyncExternalStore`, `useRef`
---
### `useExitOnCtrlCD`
**File:** `hooks/useExitOnCtrlCD.ts`
**Purpose:** Implements double-press Ctrl+C / Ctrl+D to exit. Returns pending state so callers can show a "Press again to exit" hint.
**Parameters:**
- `useKeybindingsHook: (bindings: ...) => void` injectable hook for binding.
- `onInterrupt?: () => void` called on first Ctrl+C press.
- `onExit?: () => void` called on second press.
- `isActive?: boolean` enables/disables the handler.
**Return Value:** `ExitState` `{ pending: boolean, keyName: string | null }`
**Key Logic:**
- Uses `useDoublePress` internally for both Ctrl+C and Ctrl+D.
- Sets `pending = true` after first press; false after timeout or second press.
- `keyName` tracks which key was pressed ('Ctrl-C' or 'Ctrl-D').
**Dependencies:** `useDoublePress`, `useState`, `useCallback`
---
### `useExitOnCtrlCDWithKeybindings`
**File:** `hooks/useExitOnCtrlCDWithKeybindings.ts`
**Purpose:** Convenience wrapper that wires `useExitOnCtrlCD` to the keybinding system.
**Parameters:**
- `onExit?: () => void`
- `onInterrupt?: () => void`
- `isActive?: boolean`
**Return Value:** `ExitState`
**Key Logic:** Passes `useKeybindings` as the hook parameter to `useExitOnCtrlCD`.
**Dependencies:** `useExitOnCtrlCD`, `useKeybindings`
---
### `useMemoryUsage`
**File:** `hooks/useMemoryUsage.ts`
**Purpose:** Polls Node.js `process.memoryUsage().heapUsed` every 10 seconds and returns a status when memory usage is high or critical.
**Parameters:** none
**Return Value:** `MemoryUsageInfo | null` `null` for normal; `{ heapUsed: number, status: 'high' | 'critical' }` when heap > 1.5 GB (high) or > 2.5 GB (critical).
**Key Logic:**
- Uses `useInterval` (usehooks-ts) with 10 000 ms period.
- Thresholds: `HIGH_HEAP_MB = 1536`, `CRITICAL_HEAP_MB = 2560`.
- Returns `null` when below thresholds.
**Dependencies:** `useInterval`, `useState`, `useEffect`
---
### `useMinDisplayTime`
**File:** `hooks/useMinDisplayTime.ts`
**Purpose:** Prevents UI flicker by guaranteeing each distinct value stays visible for at least `minMs` milliseconds before switching.
**Parameters:**
- `value: T` — the value to display.
- `minMs: number` — minimum display duration.
**Return Value:** `T` — the "stable" displayed value, may lag behind `value`.
**Key Logic:**
- Uses `useRef` to track the current stable value and the timestamp it was set.
- On `value` change: if `Date.now() - lastChanged >= minMs`, updates immediately; otherwise schedules a `setTimeout` to update after the remainder.
**Dependencies:** `useState`, `useRef`, `useEffect`
---
### `useNotifyAfterTimeout`
**File:** `hooks/useNotifyAfterTimeout.ts`
**Purpose:** Sends a desktop (OS-level) notification after 6 seconds of user inactivity — used to alert the user when Claude has been working unattended.
**Parameters:**
- `message: string` — the notification body text.
- `notificationType: string` — identifies the event type for analytics.
**Return Value:** `void`
**Key Logic:**
- Waits 6 000 ms after mount using `setTimeout`.
- Checks terminal focus state; only fires if the terminal is not focused.
- Calls `sendDesktopNotification` from a native module.
**Dependencies:** `useEffect`, `useRef`
---
### `useTimeout`
**File:** `hooks/useTimeout.ts`
**Purpose:** Returns a boolean that becomes `true` after `delay` ms. Resets when `resetTrigger` changes.
**Parameters:**
- `delay: number` — ms to wait.
- `resetTrigger?: number` — changing this value resets the timer.
**Return Value:** `boolean``false` until the delay elapses, then `true`.
**Key Logic:** Simple `useState` + `useEffect` with `setTimeout`. Cleanup clears the timeout on re-run or unmount.
**Dependencies:** `useState`, `useEffect`
---
### `useSettings`
**File:** `hooks/useSettings.ts`
**Purpose:** Reads the current settings from global AppState. Reactive — re-renders when settings change (e.g. file-watcher triggers).
**Parameters:** none
**Return Value:** `ReadonlySettings`
**Key Logic:** Returns `useAppState(s => s.settings)`.
**Dependencies:** `useAppState`
---
### `useSettingsChange`
**File:** `hooks/useSettingsChange.ts`
**Purpose:** Subscribes to the settings change detector and calls `onChange` with the new settings and the change source whenever the settings file is modified on disk.
**Parameters:**
- `onChange: (source: string, settings: Settings) => void`
**Return Value:** `void`
**Key Logic:**
- Uses `useEffect` to subscribe to `settingsChangeDetector.subscribe(onChange)`.
- Returns the unsubscribe function as the cleanup.
**Dependencies:** `useEffect`, `settingsChangeDetector`
---
### `useDeferredHookMessages`
**File:** `hooks/useDeferredHookMessages.ts`
**Purpose:** Injects `SessionStart` hook messages into the message list asynchronously on mount, avoiding blocking the first render.
**Parameters:**
- `pendingHookMessages: Message[]` — messages generated by session-start hooks.
- `setMessages: SetMessages` — the message list updater.
**Return Value:** `() => Promise<void>` — a stable async callback to trigger injection.
**Key Logic:**
- Defers via `setTimeout(0)` to let the first render complete before injecting hook messages.
- Uses `useRef` to avoid stale closure issues.
**Dependencies:** `useRef`, `useCallback`
---
### `useDiffData`
**File:** `hooks/useDiffData.ts`
**Purpose:** Fetches current git diff statistics and hunks on mount (used by the `/diff` command view).
**Parameters:** none
**Return Value:** `DiffData``{ stats: DiffStats, files: string[], hunks: DiffHunk[], loading: boolean }`
**Key Logic:**
- On mount, calls `getGitDiff()` which runs `git diff` in the cwd.
- Sets `loading: true` until the async fetch completes.
**Dependencies:** `useState`, `useEffect`
---
### `useFileHistorySnapshotInit`
**File:** `hooks/useFileHistorySnapshotInit.ts`
**Purpose:** One-time initialization of the file history state from snapshot data stored in the conversation log, restoring file timestamps across `/resume`.
**Parameters:**
- `initialFileHistorySnapshots: FileHistorySnapshot[]`
- `fileHistoryState: FileHistoryState`
- `onUpdateState: (state: FileHistoryState) => void`
**Return Value:** `void`
**Key Logic:**
- Uses `useEffect` with `[]` dep to run only once.
- Merges `initialFileHistorySnapshots` into `fileHistoryState` without overwriting newer entries.
**Dependencies:** `useEffect`
---
### `useInputBuffer`
**File:** `hooks/useInputBuffer.ts`
**Purpose:** Provides a debounced undo buffer for text input, enabling "undo last paste" or "undo last edit" functionality.
**Parameters:**
- `maxBufferSize: number` — maximum number of entries to keep.
- `debounceMs: number` — how long to wait before committing current value.
**Return Value:** `UseInputBufferResult``{ pushToBuffer, undo, canUndo, clearBuffer }`
**Key Logic:**
- Maintains a `string[]` undo stack in a ref.
- `pushToBuffer` is debounced: multiple rapid changes collapse into one buffer entry.
- `undo` pops the stack and calls `onChange` with the previous value.
**Dependencies:** `useRef`, `useCallback`, `useEffect`
---
### `useLogMessages`
**File:** `hooks/useLogMessages.ts`
**Purpose:** Incrementally records messages to the conversation transcript file (`.jsonl`) after each render. Avoids re-writing the full transcript on every update.
**Parameters:**
- `messages: readonly Message[]` — the current message list.
- `ignore?: boolean` — when true, skips recording.
**Return Value:** `void`
**Key Logic:**
- Tracks `lastProcessedIndex` in a ref to process only new messages.
- Handles edge cases: compaction (transcript size shrinks), first render, head-pointer rewind.
- Calls `recordTranscript(messages, from, to)` for new messages only.
- Deduplicates compact-summary boundaries.
**Dependencies:** `useEffect`, `useRef`
---
### `useMainLoopModel`
**File:** `hooks/useMainLoopModel.ts`
**Purpose:** Returns the resolved model name for the current session. Re-evaluates when GrowthBook flags are refreshed so model alias resolution stays current mid-session.
**Parameters:** none
**Return Value:** `ModelName`
**Key Logic:**
- Reads `settings.model` from AppState.
- Subscribes to `onGrowthBookRefresh` via `useEffect`; on each refresh, forces a re-render by incrementing a counter state.
- Calls `resolveModelAlias(model)` to translate user-facing aliases (e.g. `opus`) to concrete model IDs.
**Dependencies:** `useAppState`, `useEffect`, `useState`
---
### `useManagePlugins`
**File:** `hooks/useManagePlugins.ts`
**Purpose:** Loads the plugin list on mount and wires up plugin lifecycle management: delisting enforcement, MCP/LSP plugin counting, and refresh-needed notifications.
**Parameters:**
- `{ enabled?: boolean }`
**Return Value:** `void`
**Key Logic:**
- On mount (if enabled): calls `loadPlugins()` and writes results to AppState.
- Enforces delisted plugin removal by reading `delistedPlugins` from settings.
- Counts active MCP and LSP plugins and writes totals to AppState for /doctor diagnostics.
- Does NOT auto-refresh; refresh is triggered explicitly via `/reload-plugins`.
**Dependencies:** `useEffect`, `useSetAppState`, `useAppState`
---
### `useMergedClients`
**File:** `hooks/useMergedClients.ts`
**Purpose:** Deduplicates two MCP client lists (initial from settings + dynamically loaded) by server name.
**Parameters:**
- `initialClients: MCPServerConnection[]`
- `mcpClients: MCPServerConnection[]`
**Return Value:** `MCPServerConnection[]`
**Key Logic:** Uses `lodash.uniqBy([...initialClients, ...mcpClients], 'name')`. The `useMemo` dependency is the combined list length and name set.
**Dependencies:** `useMemo`, `lodash.uniqBy`
---
### `useMergedCommands`
**File:** `hooks/useMergedCommands.ts`
**Purpose:** Deduplicates command lists from initial load and MCP-sourced commands by command name.
**Parameters:**
- `initialCommands: Command[]`
- `mcpCommands: Command[]`
**Return Value:** `Command[]`
**Key Logic:** `useMemo` over `uniqBy([...initialCommands, ...mcpCommands], getCommandName)`.
**Dependencies:** `useMemo`
---
### `useMergedTools`
**File:** `hooks/useMergedTools.ts`
**Purpose:** Assembles the full tool pool for a session by combining built-in tools, MCP tools, and applying permission-context filtering.
**Parameters:**
- `initialTools: Tool[]`
- `mcpTools: Tool[]`
- `toolPermissionContext: ToolPermissionContext`
**Return Value:** `Tools` (the assembled tool set)
**Key Logic:**
- Calls `assembleToolPool(initialTools, mcpTools)` to build the combined list.
- Then calls `mergeAndFilterTools(pool, toolPermissionContext)` to remove disabled tools.
**Dependencies:** `useMemo`
---
### `useSkillsChange`
**File:** `hooks/useSkillsChange.ts`
**Purpose:** Keeps the command list fresh when skill files change on disk or when GrowthBook flags are refreshed.
**Parameters:**
- `cwd: string | undefined` — the current working directory for scanning skills.
- `onCommandsChange: (commands: Command[]) => void` — callback to update the command list.
**Return Value:** `void`
**Key Logic:**
- Subscribes to `skillChangeDetector.subscribe(handleChange)` — fires on skill file writes.
- On file change: calls `clearCommandsCache()` + `getCommands(cwd)` and calls `onCommandsChange`.
- Subscribes to `onGrowthBookRefresh(handleGrowthBookRefresh)` — on GB flag refresh, calls `clearCommandMemoizationCaches()` + `getCommands(cwd)` to re-evaluate feature-gated commands.
**Dependencies:** `useEffect`, `useCallback`
---
### `useUpdateNotification`
**File:** `hooks/useUpdateNotification.ts`
**Purpose:** Returns the new semantic version string when an auto-update has been downloaded, for display in the status bar. Returns `null` if no new version or the version hasn't changed since last notification.
**Parameters:**
- `updatedVersion: string | null | undefined` — the downloaded version (from auto-updater).
- `initialVersion?: string` — baseline version (default: `MACRO.VERSION`).
**Return Value:** `string | null`
**Key Logic:**
- Parses both versions with `semver` to extract `major.minor.patch`.
- Uses `useState` to track the last-notified semver.
- If the new semver differs from `lastNotifiedSemver`, sets state and returns the new value (triggers notification display). Otherwise returns `null`.
**Dependencies:** `useState`, `semver`
---
## Input & Text Editing Hooks
### `useTextInput`
**File:** `hooks/useTextInput.ts`
**Purpose:** Full readline-style text input handler. Manages cursor position, multiline editing, kill ring (Ctrl+K/U/W), yank (Ctrl+Y / Meta+Y), history navigation (Up/Down arrows), and ghost text rendering.
**Parameters:** `UseTextInputProps` including:
- `value: string` — current text value (controlled).
- `onChange: (value: string) => void`
- `onSubmit?: (value: string) => void`
- `onExit?: () => void`
- `onHistoryUp / onHistoryDown / onHistoryReset / onClearInput`
- `focus?: boolean`
- `mask?: string` — masks all chars with this string.
- `multiline?: boolean`
- `cursorChar: string`
- `columns: number` — terminal width for wrapping.
- `externalOffset: number` — cursor offset controlled externally.
- `onOffsetChange: (offset: number) => void`
- `inputFilter?: (input: string, key: Key) => string`
- `inlineGhostText?: InlineGhostText`
- `disableCursorMovementForUpDownKeys?: boolean`
- `disableEscapeDoublePress?: boolean`
- `maxVisibleLines?: number`
**Return Value:** `TextInputState``{ onInput, renderedValue, offset, setOffset, cursorLine, cursorColumn, viewportCharOffset, viewportCharEnd }`
**Key Logic:**
- `Cursor` class from `utils/Cursor.js` manages the text buffer and position arithmetic.
- Maps keypresses to cursor mutations: Ctrl+A (home), Ctrl+E (end), Ctrl+F/B (forward/back), Ctrl+N/P (next/prev line), Meta+F/B (word navigation).
- Kill ring: Ctrl+K (kill to end), Ctrl+U (kill to start), Ctrl+W (kill word). Successive kills append to ring.
- Yank: Ctrl+Y inserts last kill; Meta+Y cycles through the ring.
- Double-press Ctrl+C clears or exits (via `useDoublePress`).
- Double-press Escape clears input with "Esc again to clear" hint.
- SSH-coalesced Enter detection: `text\r` form triggers submit.
- Handles raw `\x7f` DEL characters for SSH/tmux compatibility.
- Inline ghost text rendered at cursor position when `inlineGhostText.insertPosition === offset`.
**Dependencies:** `useDoublePress`, `useNotifications`, `Cursor` class, `useCallback`
---
### `useVimInput`
**File:** `hooks/useVimInput.ts`
**Purpose:** Extends `useTextInput` with a full Vim normal/insert mode state machine, including operators (d, c, y), motions, dot-repeat, find (f/F/t/T), text objects (iw, aw, etc.), and yank register.
**Parameters:** `UseVimInputProps` — same as `UseTextInputProps` plus:
- `onModeChange?: (mode: VimMode) => void`
- `onUndo?: () => void`
**Return Value:** `VimInputState` — extends `TextInputState` with `{ mode: VimMode, setMode }`
**Key Logic:**
- Delegates INSERT mode keypresses to `useTextInput` after running `inputFilter`.
- In NORMAL mode, dispatches keypresses through `transition(state.command, input, ctx)` from `vim/transitions.ts`.
- Manages `vimStateRef` (current mode + pending command accumulator) and `persistentRef` (register, lastFind, lastChange for dot-repeat).
- Escape in INSERT: `switchToNormalMode()` moves cursor left by one.
- Arrow keys in NORMAL: mapped to h/j/k/l motions.
- `?` in NORMAL idle: enters `/` search by writing `?` to the input.
- `setModeExternal` allows callers to programmatically switch modes (used by `/vim` command).
**Dependencies:** `useTextInput`, `useState`, `useRef`, `useCallback`, vim operators/transitions
---
### `useSearchInput`
**File:** `hooks/useSearchInput.ts`
**Purpose:** Full readline-style text input for search boxes (history search, global search). Includes kill ring, yank, and word navigation.
**Parameters:** `UseSearchInputOptions``{ initialValue?, onKeyDown?, placeholder? }`
**Return Value:** `{ query: string, setQuery, cursorOffset: number, handleKeyDown: (key: Key, input: string) => void }`
**Key Logic:**
- Implements the same Ctrl key mapping as `useTextInput` but as a standalone reducer without React state for the cursor offset.
- Used in `HistorySearchInput` and `GlobalSearchDialog`.
**Dependencies:** `useState`, `useCallback`, `useRef`, kill ring utilities
---
### `useArrowKeyHistory`
**File:** `hooks/useArrowKeyHistory.tsx`
**Purpose:** Arrow-key navigation through input history with lazy chunked loading, mode-based filtering, and draft preservation.
**Parameters:**
- `onSetInput: (value: string) => void`
- `currentInput: string`
- `pastedContents: string[]` — paste-detected content to exclude from history matching.
- `setCursorOffset?: (offset: number) => void`
- `currentMode?: PromptInputMode`
**Return Value:** `{ handleHistoryUp, handleHistoryDown, handleHistoryReset }`
**Key Logic:**
- Loads history lazily in chunks of 50 from `getHistory()`.
- Navigates using an index pointer; on first Up saves the current draft.
- Filters entries by mode (e.g., bash mode only returns bash history).
- Shows a "Search history: Ctrl+R" hint notification on first use.
- Resets index when `currentInput` changes externally.
**Dependencies:** `useState`, `useRef`, `useCallback`, `useNotifications`
---
### `useHistorySearch`
**File:** `hooks/useHistorySearch.ts`
**Purpose:** Implements `Ctrl+R` backward incremental history search with query matching and keyboard navigation.
**Parameters:**
- `onSetInput: (value: string) => void`
- `currentInput: string`
- plus keybinding options.
**Return Value:** `{ historyQuery, setHistoryQuery, historyMatch, historyFailedMatch, handleKeyDown }`
**Key Logic:**
- Registers `history:search` keybinding (Ctrl+R) to activate search mode.
- In search mode, registers `historySearch:*` bindings (Enter to confirm, Escape to cancel, up/down to cycle matches).
- Filters history entries by substring match against `historyQuery`.
- `historyFailedMatch: boolean` — true when query has text but no match.
**Dependencies:** `useKeybinding`, `useKeybindings`, `useState`, `useCallback`, `useEffect`
---
### `useTypeahead`
**File:** `hooks/useTypeahead.tsx`
**Purpose:** The primary typeahead/autocomplete engine for the prompt input. Handles `@file`, `/command`, `#channel`, and directory suggestions using debounced fuzzy matching, shell completion, and MCP resources.
**Parameters:** Large props object including:
- `inputValue: string`, `cursorOffset: number`
- `commands: Command[]`, `agents: AgentDefinition[]`
- `mcpResources: MCPResource[]`
- `isLoading: boolean`
- `onSelect: (value: string) => void`
- `onToggleVisible: (show: boolean) => void`
**Return Value:** `{ suggestions, selectedIndex, handleKeyDown, isSuggesting, suggestionType, ... }`
**Key Logic:**
- Detects suggestion context from input: `@token` triggers file/resource/agent suggestions; `/` triggers command suggestions; `#channel` triggers Slack channel suggestions (if Slack MCP present).
- File suggestions use `generateUnifiedSuggestions` (nucleo + Fuse.js ranked).
- Command suggestions use `generateCommandSuggestions` with argument hint generation.
- Shell completions use `getShellCompletions` for bash/zsh completions.
- Path completions use `getPathCompletions` / `getDirectoryCompletions`.
- Registers as an overlay via `useRegisterOverlay` so escape/enter/arrow keys are captured.
- Uses `useDebounceCallback` (usehooks-ts) to rate-limit file lookups.
- Tracks keyboard navigation state (`selectedIndex`) internally.
- Session resume suggestions for `/resume` queries via `searchSessionsByCustomTitle`.
**Dependencies:** `useInput` (ink, backward-compat bridge), `useRegisterOverlay`, `useKeybindings`, `useDebounceCallback`, `useState`, `useRef`, `useMemo`, `useCallback`, `useEffect`, `generateUnifiedSuggestions`, `generateCommandSuggestions`, `getShellCompletions`
---
### `usePasteHandler`
**File:** `hooks/usePasteHandler.ts`
**Purpose:** Handles bracketed paste mode detection, large paste chunking, image file path detection, and macOS clipboard image fallback.
**Parameters:**
- `{ onPaste: (text: string) => void, onInput: (text: string, key: Key) => void, onImagePaste?: (base64: string, ...) => void }`
**Return Value:** `{ wrappedOnInput, pasteState, isPasting }`
**Key Logic:**
- Detects bracketed paste via `\x1b[?2004h` / `\x1b[200~` / `\x1b[201~` escape sequences.
- Splits large pastes (>1000 chars) into multiple `onPaste` calls to avoid blocking the event loop.
- Detects image file paths in paste content (extensions `.png`, `.jpg`, `.gif`, etc.) and triggers `onImagePaste`.
- On macOS: falls back to `pbpaste` for image clipboard content.
**Dependencies:** `useState`, `useRef`, `useCallback`, `useEffect`
---
### `useVoice`
**File:** `hooks/useVoice.ts`
**Purpose:** Hold-to-talk voice recording using the `voice_stream` STT endpoint. Auto-repeat key events extend the recording; releasing the key after `RELEASE_TIMEOUT_MS` stops it.
**Parameters:** `{ onTranscript: (text: string) => void, enabled: boolean }`
**Return Value:** `{ state: 'idle'|'recording'|'processing', handleKeyEvent: (fallbackMs?: number) => void }`
**Key Logic:**
- Calls `connectVoiceStream()` to open a WebSocket to `voice_stream` STT.
- Maps user locale to BCP-47 language codes for Deepgram (20+ languages mapped).
- Auto-repeat detection: key events arriving within 120 ms are considered "held".
- Modifier combos (Ctrl+Space etc.) use 2 000 ms `FIRST_PRESS_FALLBACK_MS`.
- Requires 5 rapid keydowns (HOLD_THRESHOLD) for bare-char bindings to activate, 2 for warmup feedback.
- Uses `useTerminalFocus` to pause recording when terminal loses focus.
- Fetches voice keyterms for improved domain recognition.
**Dependencies:** `useState`, `useRef`, `useCallback`, `useEffect`, `useTerminalFocus`, `connectVoiceStream`, `getVoiceKeyterms`
---
### `useVoiceEnabled`
**File:** `hooks/useVoiceEnabled.ts`
**Purpose:** Combines user intent (`settings.voiceEnabled`), OAuth auth check, and GrowthBook kill-switch into a single boolean indicating whether voice mode is available.
**Parameters:** none
**Return Value:** `boolean`
**Key Logic:**
- `userIntent` from `useAppState(s => s.settings.voiceEnabled === true)`.
- `authed` memoized on `authVersion` — avoids expensive `hasVoiceAuth()` call on every render.
- `isVoiceGrowthBookEnabled()` not memoized (cheap cached lookup, so mid-session kill-switch takes effect).
**Dependencies:** `useAppState`, `useMemo`
---
### `useVoiceIntegration`
**File:** `hooks/useVoiceIntegration.tsx`
**Purpose:** Orchestrates the full voice-mode integration: reading keybindings, detecting held keys, activating `useVoice`, suppressing full-width space input during recording, and showing status notifications.
**Parameters:**
- `{ onTranscript: (text: string) => void, isModalOverlayActive: boolean }`
**Return Value:** `{ voiceState: VoiceState, handleVoiceKeyEvent }`
**Key Logic:**
- Reads `voice:activate` keybinding from keybinding context (default: spacebar).
- Detects held key by counting rapid key events (HOLD_THRESHOLD=5 for bare chars, 1 for modifier combos).
- Shows warmup notification after WARMUP_THRESHOLD=2 events.
- Uses `useInput` (ink) as a backward-compat bridge until REPL wires `handleKeyDown` to `<Box onKeyDown>`.
- Guards on `useIsModalOverlayActive()` — does not activate voice while a modal is open.
- Dead-code elimination: conditionally requires `useVoice` only if `feature('VOICE_MODE')` is set; otherwise uses a no-op stub.
- Calls `normalizeFullWidthSpace` to handle full-width space (Japanese IME) by passing it to the transcript instead of activating voice.
**Dependencies:** `useVoice`, `useVoiceEnabled`, `useInput` (ink), `useOptionalKeybindingContext`, `useIsModalOverlayActive`, `useNotifications`, `useState`, `useRef`, `useMemo`, `useCallback`, `useEffect`
---
### `useVirtualScroll`
**File:** `hooks/useVirtualScroll.ts`
**Purpose:** React-level virtualization for `MessageRow` items inside a `ScrollBox`. Mounts only items in the viewport plus overscan, using spacer boxes to maintain scroll height.
**Parameters:**
- `scrollRef: RefObject<ScrollBoxHandle | null>` — reference to the ScrollBox.
- `itemKeys: readonly string[]` — stable keys for each item.
- `columns: number` — terminal width; triggers height cache rescaling on change.
**Return Value:** `VirtualScrollResult`:
- `range: [startIndex, endIndex)` — half-open slice to render.
- `topSpacer: number` — rows before the first rendered item.
- `bottomSpacer: number` — rows after last rendered item.
- `measureRef: (key) => ref` — attach to each item root `Box` for height measurement.
- `spacerRef: RefObject<DOMElement>` — attach to top spacer for drift-free origin tracking.
- `offsets: ArrayLike<number>` — cumulative y-offsets per item.
- `getItemTop: (index) => number` — reads live Yoga `computedTop`.
- `getItemElement: (index) => DOMElement | null`
- `getItemHeight: (index) => number | undefined`
- `scrollToIndex: (i) => void`
**Key Logic:**
- `DEFAULT_ESTIMATE = 3` rows for unmeasured items; `OVERSCAN_ROWS = 80`; `COLD_START_COUNT = 30`.
- `SCROLL_QUANTUM = 40` rows — scrollTop is quantized so React re-renders only when the mounted range needs to shift, not on every wheel tick.
- `SLIDE_STEP = 25` — caps new mounts per commit to bound reconcile time.
- `PESSIMISTIC_HEIGHT = 1` for coverage back-walk (guarantees viewport coverage).
- `MAX_MOUNTED_ITEMS = 300` cap.
- On column change: scales all cached heights by `oldCols/newCols` instead of clearing.
- Uses `useSyncExternalStore` over the ScrollBox's scroll-top external store.
- Uses `useLayoutEffect` to measure Yoga heights after each commit.
- Sticky-scroll: when pinned to the bottom, always renders the last N items.
**Dependencies:** `useRef`, `useMemo`, `useDeferredValue`, `useLayoutEffect`, `useSyncExternalStore`, `ScrollBox` handle
---
## Permission & Tool-Use Hooks
### `useCanUseTool`
**File:** `hooks/useCanUseTool.tsx`
**Purpose:** Core permission gate for tool execution. Called for every tool use attempt; routes through the appropriate handler (coordinator, interactive, swarm-worker) and resolves to a `PermissionDecision`.
**Parameters:**
- `setToolUseConfirmQueue: SetState<ToolUseConfirm[]>`
- `setToolPermissionContext: (ctx: ToolPermissionContext) => void`
**Return Value:** `CanUseToolFn``async (tool, input, toolUseContext, assistantMessage, toolUseID) => PermissionDecision`
**Key Logic:**
1. Calls `hasPermissionsToUseTool` to get the initial decision (`allow`, `deny`, or `ask`).
2. If `allow` or `deny`: logs and returns immediately.
3. If `ask`:
a. If swarm worker: delegates to `handleSwarmWorkerPermission` (forwards to leader via mailbox).
b. If coordinator worker: delegates to `handleCoordinatorPermission` (awaits hooks + classifier, then falls through).
c. Otherwise: delegates to `handleInteractivePermission` (shows dialog, races hooks/classifier/bridge/channel).
4. Creates a `PermissionContext` object with the full set of callbacks (logDecision, persistPermissions, tryClassifier, runHooks, etc.).
**Dependencies:** `useCallback`, `useAppState`, `useSetAppState`, `createPermissionContext`, `createPermissionQueueOps`, `handleCoordinatorPermission`, `handleInteractivePermission`, `handleSwarmWorkerPermission`
---
### `CancelRequestHandler` (exported as `useCancelRequest` module)
**File:** `hooks/useCancelRequest.ts`
**Purpose:** React component (renders `null`) that registers three keybinding handlers for cancellation:
1. `chat:cancel` (Escape) — cancels running task or pops queued command.
2. `app:interrupt` (Ctrl+C) — cancels running task; in teammate view, also kills all agents and exits.
3. `chat:killAgents` (Ctrl+X Ctrl+K) — two-press pattern to stop all background agents.
**Parameters:** `CancelRequestHandlerProps`:
- `setToolUseConfirmQueue`, `onCancel`, `onAgentsKilled`
- `isMessageSelectorVisible`, `screen`
- `abortSignal?: AbortSignal`
- `popCommandFromQueue?`, `vimMode`, `isLocalJSXCommand`, `isSearchingHistory`, `isHelpOpen`
- `inputMode?, inputValue?, streamMode?`
**Return Value:** `null`
**Key Logic:**
- `handleCancel`: Priority 1 — abort signal if task running. Priority 2 — pop command if queue non-empty. Fallback — call `onCancel`.
- `handleInterrupt`: if in teammate view, kills all agents + exits. Then calls `handleCancel`.
- `handleKillAgents`: first press shows "Press again" hint; second press within 3 000 ms (`KILL_AGENTS_CONFIRM_WINDOW_MS`) kills all `local_agent` tasks, emits SDK events, enqueues aggregate notification.
- `isEscapeActive` / `isCtrlCActive` guards: skip if overlay, vim INSERT, transcript, history-search, help, etc.
- `chat:killAgents` always registered to prevent Ctrl+K (chord prefix) passing to readline.
**Dependencies:** `useKeybinding`, `useAppState`, `useSetAppState`, `useCommandQueue`, `useNotifications`, `useIsOverlayActive`, `useCallback`, `useRef`, `killAllRunningAgentTasks`, `emitTaskTerminatedSdk`
---
### `useSwarmPermissionPoller`
**File:** `hooks/useSwarmPermissionPoller.ts`
**Purpose:** Polls every 500 ms for permission responses from the swarm leader when running as a worker agent. When a response arrives, it invokes the registered callback (`onAllow` or `onReject`).
**Parameters:** none
**Return Value:** `void`
**Key Logic:**
- Only active when `isSwarmWorker()` returns `true`.
- Uses `useInterval` (usehooks-ts) with `POLL_INTERVAL_MS = 500`.
- For each `requestId` in `pendingCallbacks`, calls `pollForResponse(requestId, agentName, teamName)`.
- On response: calls `processResponse(response)` which invokes the callback, then calls `removeWorkerResponse`.
- Module-level `pendingCallbacks: Map<string, PermissionResponseCallback>` and `pendingSandboxCallbacks: Map<...>`.
- Exported helper functions: `registerPermissionCallback`, `unregisterPermissionCallback`, `hasPermissionCallback`, `clearAllPendingCallbacks`, `processMailboxPermissionResponse`, `registerSandboxPermissionCallback`, `hasSandboxPermissionCallback`, `processSandboxPermissionResponse`.
**Dependencies:** `useCallback`, `useEffect`, `useRef`, `useInterval`, `permissionSync`
---
## Swarm / Teammate Hooks
### `useSwarmInitialization`
**File:** `hooks/useSwarmInitialization.ts`
**Purpose:** Initializes swarm features (teammate context and hooks) on mount. Handles both resumed sessions (teamName/agentName in transcript) and fresh spawns (environment variables).
**Parameters:**
- `setAppState: SetAppState`
- `initialMessages: Message[] | undefined`
- `{ enabled?: boolean }`
**Return Value:** `void`
**Key Logic:**
- Checks `isAgentSwarmsEnabled()` before doing anything.
- Resumed session path: reads `teamName`/`agentName` from `initialMessages[0]`, calls `initializeTeammateContextFromSession`, reads team file to get `agentId`, calls `initializeTeammateHooks`.
- Fresh spawn path: calls `getDynamicTeamContext()` to read env vars, then calls `initializeTeammateHooks`.
**Dependencies:** `useEffect`
---
### `useTeammateViewAutoExit`
**File:** `hooks/useTeammateViewAutoExit.ts`
**Purpose:** Auto-exits teammate viewing mode when the viewed teammate is killed, fails, encounters an error, or is evicted from the task map.
**Parameters:** none
**Return Value:** `void`
**Key Logic:**
- Selects only `viewingAgentTaskId` and the viewed task from AppState (avoids re-rendering on unrelated streaming updates).
- Narrows the task to `InProcessTeammateTask` type.
- Exits if task evicted, status is `killed`, `failed`, or error is present.
- Does NOT exit if status is `running`, `completed`, or `pending`.
**Dependencies:** `useEffect`, `useAppState`, `useSetAppState`, `exitTeammateView`
---
### `useBackgroundTaskNavigation`
**File:** `hooks/useBackgroundTaskNavigation.ts`
**Purpose:** Manages keyboard navigation of the background task list. Shift+Up/Down moves selection; Enter enters the view; `f` opens the full transcript; `k` kills the task; Escape exits.
**Parameters:** `options?: { isActive?: boolean }`
**Return Value:** `{ handleKeyDown: (key: Key, input: string) => void }`
**Key Logic:**
- Reads the list of background tasks from AppState.
- `selectedIndex` is clamped to `[0, tasks.length - 1]` whenever the list changes.
- Enter: sets `viewingAgentTaskId` in AppState to the selected task's ID.
- `f`: sets `showFullTranscript = true` in AppState.
- `k`: calls `killTask(task.id)`.
- Escape: calls `exitTeammateView(setAppState)`.
**Dependencies:** `useAppState`, `useSetAppState`, `useState`, `useEffect`, `useCallback`
---
### `useInboxPoller`
**File:** `hooks/useInboxPoller.ts`
**Purpose:** Polls the team lead's inbox every 1 second (or on demand when idle) and routes messages. Handles permission requests/responses, sandbox permissions, plan approvals, shutdown handling, team permission updates, mode-set requests, and regular messages.
**Parameters:**
- `{ enabled: boolean, isLoading: boolean, focusedInputDialog: string | null, onSubmitMessage: (msg) => void }`
**Return Value:** `void`
**Key Logic:**
- Uses `useInterval` with 1 000 ms period; no-ops if `!enabled`.
- Reads messages from the team lead's mailbox directory.
- Dispatches by message type:
- `permission_request` → adds to `toolUseConfirmQueue`.
- `permission_response` → calls `processMailboxPermissionResponse`.
- `sandbox_permission_request` → calls sandbox permission handler.
- `sandbox_permission_response` → calls `processSandboxPermissionResponse`.
- `plan_approval` → routes to plan approval handler.
- `shutdown` → gracefully exits.
- `team_permission_update` → updates AppState permission context.
- `mode_set` → changes permission mode.
- Regular messages → calls `onSubmitMessage` when idle.
- Delivers pending messages only when `!isLoading` and no focused input dialog.
**Dependencies:** `useInterval`, `useEffect`, `useRef`, `useAppState`, `useSetAppState`, `processMailboxPermissionResponse`, `processSandboxPermissionResponse`
---
### `useTaskListWatcher`
**File:** `hooks/useTaskListWatcher.ts`
**Purpose:** Watches a task list directory and automatically picks up open, unowned tasks to work on (tasks mode). Claims tasks atomically to prevent race conditions.
**Parameters:**
- `{ taskListId?: string, isLoading: boolean, onSubmitTask: (prompt: string) => boolean }`
**Return Value:** `void`
**Key Logic:**
- Calls `ensureTasksDir` on mount, then `watch(tasksDir, debouncedCheck)` with DEBOUNCE_MS=1000.
- Uses stable refs for `isLoading` and `onSubmitTask` to avoid Bun PathWatcherManager deadlock (oven-sh/bun#27469) by not recreating the watcher on every turn.
- `checkForTasks`: lists tasks, finds `status=pending`, `owner=undefined`, all `blockedBy` completed; calls `claimTask(taskListId, task.id, agentId)`.
- Formats task as `"Complete all open tasks. Start with task #N: ...\n\nDescription"`.
- Additional `useEffect` on `isLoading` to trigger check when going idle.
**Dependencies:** `fs.watch`, `useEffect`, `useRef`
---
### `useTasksV2`
**File:** `hooks/useTasksV2.ts`
**Purpose:** Exposes the current task list for the persistent TodoV2 UI. All consumers share a single `TasksV2Store` (singleton file-watcher) to avoid watcher churn.
**Parameters:** none
**Return Value:** `Task[] | undefined``undefined` when hidden (all completed for >5 s, or empty).
**Key Logic:**
- `TasksV2Store` class: manages `fs.watch`, `onTasksUpdated` subscription, debounced fetch (DEBOUNCE_MS=50), hide timer (HIDE_DELAY_MS=5000), fallback poll (FALLBACK_POLL_MS=5000).
- `getSnapshot` returns `undefined` when `#hidden = true`.
- `useSyncExternalStore` subscription; store starts on first subscriber, stops on last unsubscribe.
- Only active when `isTodoV2Enabled()` and (no team context, or is team lead).
- `useTasksV2WithCollapseEffect`: same as `useTasksV2` plus collapses the expanded task view in AppState when the list becomes hidden.
**Dependencies:** `useSyncExternalStore`, `useEffect`, `useAppState`, `useSetAppState`, `fs.watch`
---
### `useSessionBackgrounding`
**File:** `hooks/useSessionBackgrounding.ts`
**Purpose:** Manages Ctrl+B backgrounding and foregrounding of the current session. When a task is foregrounded, it syncs that task's messages to the main message list.
**Parameters:** (large props including setMessages, setIsLoading, tools, etc.)
**Return Value:** `{ handleBackgroundSession: () => void }`
**Key Logic:**
- On `handleBackgroundSession`: if a task is running, backgrounds it (writes to AppState background tasks); otherwise foregrounds the first background task.
- Foreground: injects the backgrounded task's messages into the main list via `setMessages`, resumes the task's streams.
**Dependencies:** `useCallback`, `useAppState`, `useSetAppState`
---
### `useScheduledTasks`
**File:** `hooks/useScheduledTasks.ts`
**Purpose:** Mounts the cron scheduler in the REPL. Fired tasks are enqueued via `enqueuePendingNotification` at `later` priority; teammate-scoped crons are injected directly into that teammate's message stream.
**Parameters:**
- `{ isLoading: boolean, assistantMode?: boolean, setMessages: Dispatch<SetStateAction<Message[]>> }`
**Return Value:** `void`
**Key Logic:**
- Gated on `isKairosCronEnabled()` at effect time.
- Uses `isLoadingRef` to avoid stale closures on `isLoading`.
- `onFireTask` callback: if task has `agentId`, finds the teammate and calls `injectUserMessageToTeammate`; otherwise creates a `ScheduledTaskFireMessage` and enqueues the prompt.
- `createCronScheduler` is the shared scheduler core (used also by `print.ts` for headless mode).
- `isKilled` callback polls `isKairosCronEnabled()` each tick as a mid-session killswitch.
**Dependencies:** `useEffect`, `useRef`, `useAppStateStore`, `useSetAppState`, `createCronScheduler`
---
## IDE Integration Hooks
### `useIDEIntegration`
**File:** `hooks/useIDEIntegration.tsx`
**Purpose:** Manages IDE auto-connection on startup. Detects running IDEs, sets up dynamic MCP config for the found IDE server, and shows the IDE onboarding dialog if needed.
**Parameters:**
- `{ autoConnectIdeFlag, ideToInstallExtension, setDynamicMcpConfig, setShowIdeOnboarding, setIDEInstallationState }`
**Return Value:** `void`
**Key Logic:**
- Calls `detectIDEs()` on mount to find running IDE extension servers.
- If found: calls `setDynamicMcpConfig` to add the IDE's MCP server config.
- Checks `settings.ideHintShownCount` to decide whether to show onboarding.
- Handles the `ideToInstallExtension` CLI flag for direct IDE extension install flows.
**Dependencies:** `useEffect`, `useRef`, `useAppState`
---
### `useIdeAtMentioned`
**File:** `hooks/useIdeAtMentioned.ts`
**Purpose:** Listens for `at_mentioned` MCP notifications from the IDE extension and calls a callback with file/line context so Claude can reference the file.
**Parameters:**
- `mcpClients: MCPServerConnection[]`
- `onAtMentioned: (filePath: string, lineNumber?: number) => void`
**Return Value:** `void`
**Key Logic:**
- Uses `getConnectedIdeClient(mcpClients)` to find the IDE client.
- Registers a notification handler for the `at_mentioned` method via `ideClient.client.setNotificationHandler`.
- Passes parsed `filePath` and `lineNumber` to `onAtMentioned`.
**Dependencies:** `useEffect`
---
### `useIdeConnectionStatus`
**File:** `hooks/useIdeConnectionStatus.ts`
**Purpose:** Returns the current IDE connection status (`connected`, `disconnected`, `pending`, or `null`) and the IDE name.
**Parameters:**
- `mcpClients?: MCPServerConnection[]`
**Return Value:** `{ status: IDEConnectionStatus | null, ideName: string | null }`
**Key Logic:**
- Uses `useMemo` over `mcpClients` to find the IDE client by checking `isIdeClient(client)`.
- Maps client connection state to the status enum.
**Dependencies:** `useMemo`
---
### `useIdeLogging`
**File:** `hooks/useIdeLogging.ts`
**Purpose:** Registers a `log_event` MCP notification handler on the IDE client to forward IDE telemetry events to the analytics system.
**Parameters:**
- `mcpClients: MCPServerConnection[]`
**Return Value:** `void`
**Key Logic:**
- Calls `getConnectedIdeClient` to find the IDE client.
- Registers a Zod-validated handler: `{ method: 'log_event', params: { eventName, eventData } }`.
- Calls `logEvent('tengu_ide_${eventName}', eventData)`.
**Dependencies:** `useEffect`, `zod`
---
### `useIdeSelection`
**File:** `hooks/useIdeSelection.ts`
**Purpose:** Listens for `selection_changed` MCP notifications from the IDE and delivers them as `IDESelection` objects to the REPL.
**Parameters:**
- `mcpClients: MCPServerConnection[]`
- `onSelect: (selection: IDESelection) => void`
**Return Value:** `void`
**Key Logic:**
- Finds IDE client, registers notification handler for `selection_changed`.
- Converts the raw notification payload to `IDESelection` format `{ filePath, text, lineStart, lineEnd, lineCount }`.
**Dependencies:** `useEffect`
---
### `useDiffInIDE`
**File:** `hooks/useDiffInIDE.ts`
**Purpose:** Opens a file diff in the connected IDE via MCP RPC. Handles user save/close/reject responses to finalize or revert the edit.
**Parameters:**
- `{ onChange, toolUseContext, filePath, edits, editMode }`
**Return Value:** `{ closeTabInIDE, showingDiffInIDE, ideName, hasError }`
**Key Logic:**
- Calls `ideClient.client.request('show_diff', { filePath, edits, ... })` to open the diff in the IDE.
- Waits for the IDE to respond with `saved`, `closed`, or `rejected`.
- On `saved`: calls `onChange` to apply the edit.
- On `rejected`: calls the abort controller.
- Returns `closeTabInIDE()` so the permission dialog can close the tab programmatically.
**Dependencies:** `useState`, `useRef`, `useCallback`, `useEffect`
---
## Remote & Session Hooks
### `useDirectConnect`
**File:** `hooks/useDirectConnect.ts`
**Purpose:** Manages a WebSocket connection to a DirectConnect server (local server mode). Routes inbound messages and permission requests between the server and the REPL.
**Parameters:**
- `{ config, setMessages, setIsLoading, setToolUseConfirmQueue, tools }`
**Return Value:** `UseDirectConnectResult``{ isConnected, send }`
**Key Logic:**
- Uses `directConnectManager` to manage the WebSocket lifecycle.
- Translates inbound SDK messages to `Message[]` format.
- Handles tool permission requests by pushing to `setToolUseConfirmQueue`.
- Reconnects on disconnect with exponential backoff.
**Dependencies:** `useEffect`, `useRef`, `useState`
---
### `useSSHSession`
**File:** `hooks/useSSHSession.ts`
**Purpose:** Wires an SSH session manager to the REPL. Handles reconnection, graceful shutdown on disconnect, and transcript message injection.
**Parameters:**
- `{ session, setMessages, setIsLoading, setToolUseConfirmQueue, tools }`
**Return Value:** `UseSSHSessionResult``{ isConnected, disconnect }`
**Key Logic:**
- Subscribes to SSH session events: `message`, `connect`, `disconnect`, `error`.
- On disconnect: injects a system message explaining the disconnect and showing reconnect options.
- On reconnect: injects a system message confirming reconnection.
- Handles graceful shutdown: drains pending messages before closing.
**Dependencies:** `useEffect`, `useRef`, `useState`
---
### `useRemoteSession`
**File:** `hooks/useRemoteSession.ts`
**Purpose:** Full CCR (Claude Code Remote) WebSocket session management. Handles bidirectional message conversion, streaming tool uses, permission request/response flow, response timeout detection, session title updates, and subagent task counting.
**Parameters:** Large props object including:
- `config: AppConfig`
- `setMessages: SetMessages`
- `setIsLoading`, `setToolUseConfirmQueue`
- `tools: Tool[]`
- `onSessionTitleUpdate?: (title: string) => void`
**Return Value:** `UseRemoteSessionResult``{ isConnected, sendMessage, sessionId, ... }`
**Key Logic:**
- Connects to CCR WebSocket on mount, reconnects on disconnect.
- Converts `SDKMessage` types to internal `Message` format on inbound.
- Converts outbound messages to SDK format for CCR consumption.
- Manages streaming tool uses: accumulates `input_json_delta` chunks, fires permission dialog on `tool_use` completion.
- Response timeout: sets a flag after 30 s of no response from the model.
- Session title: subscribes to `session_title_update` events and calls `onSessionTitleUpdate`.
- Subagent task counting: tracks `subagent_start` / `subagent_end` events to count running agents.
**Dependencies:** `useEffect`, `useState`, `useRef`, `useCallback`, `SessionsWebSocket`
---
### `useAssistantHistory`
**File:** `hooks/useAssistantHistory.ts`
**Purpose:** Lazy-loads older messages from a remote session's history as the user scrolls up in viewer-only mode.
**Parameters:**
- `{ config, setMessages, scrollRef, onPrepend }`
**Return Value:** `{ maybeLoadOlder: () => Promise<void> }`
**Key Logic:**
- On scroll-up event, calls `loadSessionHistory(sessionId, pageToken)`.
- Prepends loaded messages to the message list.
- Chains viewport fill: if the loaded messages don't fill the viewport, immediately loads another page.
- Scroll anchoring: saves the current scroll position before prepending and restores it after.
- Shows a sentinel message at the top ("Beginning of conversation") once all pages are exhausted.
**Dependencies:** `useCallback`, `useRef`, `useEffect`
---
### `useMailboxBridge`
**File:** `hooks/useMailboxBridge.ts`
**Purpose:** Bridges the mailbox message context to the REPL's submit function. Polls the mailbox on revision change when idle.
**Parameters:**
- `{ isLoading: boolean, onSubmitMessage: (msg) => void }`
**Return Value:** `void`
**Key Logic:**
- Subscribes to `mailboxRevision` changes in AppState.
- When revision bumps and `!isLoading`: calls `pollMailbox()` and submits any pending messages via `onSubmitMessage`.
**Dependencies:** `useEffect`, `useRef`, `useAppState`
---
### `useReplBridge`
**File:** `hooks/useReplBridge.tsx`
**Purpose:** Full REPL bridge session management — the main hook wiring the REPL to the Claude API. Manages the full query execution loop, permission flow, streaming message assembly, compact operations, and bridge connectivity.
**Parameters:** Large props including tools, messages, setMessages, config, and many callbacks.
**Return Value:** Large result including `{ onQuery, isLoading, abortController, toolUseConfirmQueue, ... }`
**Key Logic:** (file is >75k tokens; key points from reading the first 80 lines and the summary)
- Manages `abortController` lifecycle — creates a new one per query, aborts on cancel.
- Calls the streaming Claude API via `query.ts`.
- Assembles streaming `AssistantMessage` from delta events.
- Routes `tool_use` to `canUseTool` for permission gating.
- Handles compact boundary detection and auto-compact triggers.
- Integrates bridge callbacks for CCR permission relaying.
- Manages local session history recording.
**Dependencies:** `useState`, `useRef`, `useCallback`, `useEffect`, `useMemo`, `useCanUseTool`, `useLogMessages`, `query`, `compact`
---
### `useTeleportResume`
**File:** `hooks/useTeleportResume.tsx`
**Purpose:** Manages the async lifecycle of teleporting into a remote Code Session: loading state, error state, selected session tracking, and the `resumeSession` callback.
**Parameters:**
- `source: TeleportSource``'cliArg' | 'localCommand'` (for analytics).
**Return Value:** `{ resumeSession, isResuming, error, selectedSession, clearError }`
**Key Logic:**
- `resumeSession(session)`: sets `isResuming = true`, logs `tengu_teleport_resume_session`, calls `teleportResumeCodeSession(session.id)`, sets `teleportedSessionInfo` for reliability logging.
- On error: wraps in `TeleportResumeError` with `isOperationError` flag for UI differentiation.
- Uses React Compiler (`_c`) memoization.
**Dependencies:** `useState`, `useCallback`, `teleportResumeCodeSession`, `setTeleportedSessionInfo`
---
## Plugin & Suggestion Hooks
### `usePromptSuggestion`
**File:** `hooks/usePromptSuggestion.ts`
**Purpose:** Manages AI prompt completion suggestions: fetches a suggestion for the current input, tracks accept/ignore/submit outcomes, and logs telemetry.
**Parameters:**
- `{ inputValue: string, isAssistantResponding: boolean }`
**Return Value:** `{ suggestion: string | null, markAccepted, markShown, logOutcomeAtSubmission }`
**Key Logic:**
- Calls `generatePromptSuggestion(inputValue)` debounced after 300 ms of no typing.
- `markAccepted()`: records that the user pressed Tab to accept.
- `markShown()`: records when the ghost-text suggestion becomes visible.
- `logOutcomeAtSubmission()`: called on submit; logs `accept` (Tab was pressed), `ignore` (shown but not accepted), or `no_suggestion` outcome.
**Dependencies:** `useState`, `useRef`, `useCallback`, `useEffect`, `generatePromptSuggestion`
---
### `usePromptsFromClaudeInChrome`
**File:** `hooks/usePromptsFromClaudeInChrome.tsx`
**Purpose:** Listens for prompts sent from the Claude in Chrome extension via MCP notifications. Also syncs the current permission mode to the extension.
**Parameters:**
- `mcpClients: MCPServerConnection[]`
- `toolPermissionMode: PermissionMode`
**Return Value:** `void`
**Key Logic:**
- Finds the Chrome extension MCP client.
- Registers a notification handler for `prompt_from_chrome`.
- Submits received prompts to the command queue.
- On `toolPermissionMode` change: sends a `mode_changed` notification back to the extension.
**Dependencies:** `useEffect`, `useRef`
---
### `usePrStatus`
**File:** `hooks/usePrStatus.ts`
**Purpose:** Polls `gh pr status` every 60 seconds to detect review state changes on the current branch's PR.
**Parameters:**
- `isLoading: boolean`
- `enabled?: boolean`
**Return Value:** `PrStatusState``{ reviewStatus: 'approved'|'changes_requested'|'pending'|null, prUrl: string | null }`
**Key Logic:**
- Uses `useInterval` with 60 000 ms period; skips poll when `isLoading`.
- Stops polling after 60 minutes of idle time (no new turns).
- Permanently disables if a fetch takes >4 seconds (likely no `gh` binary or no PR).
- Runs `gh pr status --json reviewDecision,url` in a subprocess.
**Dependencies:** `useInterval`, `useState`, `useRef`
---
### `useClaudeCodeHintRecommendation`
**File:** `hooks/useClaudeCodeHintRecommendation.tsx`
**Purpose:** Surfaces plugin install prompts from `<claude-code-hint />` tags parsed from Claude's responses. Show-once semantics per plugin per session.
**Parameters:** none
**Return Value:** `{ recommendation: PluginRecommendation | null, handleResponse: (accepted: boolean) => void }`
**Key Logic:**
- Monitors `messages` for assistant messages containing `<claude-code-hint plugin="name" />` XML.
- Uses `usePluginRecommendationBase` state machine to gate display.
- `handleResponse(true)`: installs the plugin; `false`: dismisses.
**Dependencies:** `usePluginRecommendationBase`, `useAppState`, `useEffect`
---
### `useLspPluginRecommendation`
**File:** `hooks/useLspPluginRecommendation.tsx`
**Purpose:** Recommends an LSP plugin when the user edits a file whose extension matches a supported language and the LSP binary is present.
**Parameters:** none
**Return Value:** `{ recommendation: PluginRecommendation | null, handleResponse: (accepted: boolean) => void }`
**Key Logic:**
- Watches `messages` for `FileEdit` / `FileWrite` tool result messages.
- Extracts the file extension, checks if there's a matching LSP plugin.
- Uses `usePluginRecommendationBase` to avoid showing while another recommendation is active.
- Show-once per session (tracked in AppState).
**Dependencies:** `usePluginRecommendationBase`, `useAppState`, `useEffect`
---
### `usePluginRecommendationBase`
**File:** `hooks/usePluginRecommendationBase.tsx`
**Purpose:** Shared state machine for plugin recommendations. Guards against showing a recommendation while in remote mode, while another is already showing, or while a check is in-flight.
**Parameters:** generic `T`
**Return Value:** `{ recommendation: T | null, clearRecommendation, tryResolve: (candidate: T | null) => void }`
**Key Logic:**
- `tryResolve(candidate)`: if remote mode or already showing, no-op; otherwise sets `recommendation`.
- `clearRecommendation()`: clears the current recommendation.
- Guards `inFlight` ref to prevent concurrent checks.
**Dependencies:** `useState`, `useRef`, `useCallback`
---
### `useOfficialMarketplaceNotification`
**File:** `hooks/useOfficialMarketplaceNotification.tsx`
**Purpose:** Handles official marketplace auto-install on first launch and shows success/failure startup notifications.
**Parameters:** none
**Return Value:** `void`
**Key Logic:**
- On mount: checks `settings.autoInstallOfficialMarketplace` flag.
- If set and not yet installed: calls `installOfficialMarketplace()`.
- Shows a `startup` priority notification on success or failure.
**Dependencies:** `useEffect`, `useRef`, `useNotifications`
---
### `useChromeExtensionNotification`
**File:** `hooks/useChromeExtensionNotification.tsx`
**Purpose:** Shows startup notifications about the Chrome extension status: requires subscription, not installed, or default-enabled.
**Parameters:** none
**Return Value:** `void`
**Key Logic:**
- Uses `useStartupNotification` (notifs/) internally.
- Checks `chrome_extension_status` from settings/config.
- Returns appropriate notification text for each status.
**Dependencies:** `useStartupNotification`
---
### `useClipboardImageHint`
**File:** `hooks/useClipboardImageHint.ts`
**Purpose:** Shows a notification when the terminal gains focus and the clipboard contains an image. Debounced with 1 000 ms delay and a 30-second cooldown.
**Parameters:**
- `isFocused: boolean`
- `enabled: boolean`
**Return Value:** `void`
**Key Logic:**
- Watches `isFocused` changes.
- On focus gain: checks clipboard via `navigator.clipboard.read()` for `image/*` MIME.
- If image found: calls `addNotification({ key: 'clipboard-image', text: 'Image in clipboard · Ctrl+V to attach' })`.
- Cooldown: stores last-shown timestamp to avoid notification spam.
**Dependencies:** `useEffect`, `useRef`, `useNotifications`
---
### `useManagePlugins`
**File:** `hooks/useManagePlugins.ts` — (same as above; see Core section for full details)
---
### `useIssueFlagBanner`
**File:** `hooks/useIssueFlagBanner.ts`
**Purpose:** ANT-internal: shows an issue-flag banner after a session has friction signals (e.g. repeated tool retries) and has been active for at least 30 minutes with 3+ submits.
**Parameters:**
- `messages: readonly Message[]`
- `submitCount: number`
**Return Value:** `boolean` — whether to show the banner.
**Key Logic:**
- Counts tool errors, retries, and refusals in recent messages.
- Only enabled for ANT internal users (checks `isAntInternal()`).
- 30-minute cooldown stored in localStorage.
**Dependencies:** `useMemo`, `useRef`
---
### `useQueueProcessor`
**File:** `hooks/useQueueProcessor.ts`
**Purpose:** Processes queued commands from the unified command queue when no active query is running and no blocking UI is shown.
**Parameters:**
- `{ executeQueuedInput: (cmd: QueuedCommand) => void, hasActiveLocalJsxUI: boolean, queryGuard: () => boolean }`
**Return Value:** `void`
**Key Logic:**
- Uses two `useSyncExternalStore` subscriptions: one for the command queue, one for the "is loading" state.
- When the queue is non-empty, `!isLoading`, `!hasActiveLocalJsxUI`, and `queryGuard()` returns true: dequeues and executes the next command.
- Uses `useEffect` to trigger processing on state changes.
**Dependencies:** `useSyncExternalStore`, `useEffect`, `messageQueueManager`
---
### `useTurnDiffs`
**File:** `hooks/useTurnDiffs.ts`
**Purpose:** Extracts per-turn file diffs from the message list for display in the `/diff` view. Uses incremental processing — only new messages are scanned on each render.
**Parameters:**
- `messages: Message[]`
**Return Value:** `TurnDiff[]` — reverse-chronological list of turns that modified files.
**Key Logic:**
- `TurnDiff`: `{ turnIndex, userPromptPreview, timestamp, files: Map<string, TurnFileDiff>, stats }`.
- Detects turn boundaries from user messages that are not tool results and not `isMeta`.
- Collects `FileEdit`/`FileWrite` tool results within each turn.
- New-file hunks: generates synthetic `+line` hunks from `content`.
- Accumulated across edits to the same file in a turn.
- Cache ref holds `completedTurns` + `currentTurn` + `lastProcessedIndex` for O(n_new) processing.
**Dependencies:** `useMemo`, `useRef`
---
### `useSkillImprovementSurvey`
**File:** `hooks/useSkillImprovementSurvey.ts`
**Purpose:** Manages the skill improvement survey dialog. Triggered by `AppState.skillImprovementSurvey`, applies improvements to the skills file on accept.
**Parameters:**
- `setMessages: SetMessages`
**Return Value:** `{ isOpen: boolean, suggestion: SkillSuggestion | null, handleSelect: (selection) => void }`
**Key Logic:**
- Reads `AppState.skillImprovementSurvey` to get the pending survey.
- `handleSelect('apply')`: calls `applySkillImprovement(suggestion)` and injects a system message.
- `handleSelect('dismiss')`: clears the survey from AppState.
- On any selection: clears `AppState.skillImprovementSurvey`.
**Dependencies:** `useAppState`, `useSetAppState`, `useCallback`
---
### `useGlobalKeybindings`
**File:** `hooks/useGlobalKeybindings.tsx`
**Purpose:** React component (renders `null`) that registers global keybinding handlers for Ctrl+T (toggle todos), Ctrl+O (toggle transcript/messages), Ctrl+E (toggle show-all), and Escape/Ctrl+C (exit transcript).
**Parameters:** Props including `screen`, `isLoading`, `isSearchingHistory`, `isHelpOpen`, etc.
**Return Value:** `null`
**Key Logic:**
- Registers `view:toggleTasks`, `view:toggleTranscript`, `view:toggleShowAll` bindings.
- KAIROS feature flag gates: `view:toggleTasks` only active when `isTodoV2Enabled()`.
- `view:toggleTranscript` sets `expandedView = 'messages'` or `'none'` in AppState.
- `view:toggleShowAll` sets `showAllMessages` in AppState.
**Dependencies:** `useKeybinding`, `useAppState`, `useSetAppState`
---
### `CommandKeybindingHandlers`
**File:** `hooks/useCommandKeybindings.tsx`
**Purpose:** Registers all `command:*` keybinding actions as slash command submitters — e.g., `command:compact`, `command:memory`, `command:config`, etc.
**Parameters:** `{ onSubmit: (cmd: string) => void, isActive?: boolean }`
**Return Value:** `null`
**Key Logic:**
- Calls `useKeybindings` with a map of `command:X``() => onSubmit('/X')` entries.
- Derives the slash command name from the keybinding action name.
**Dependencies:** `useKeybindings`
---
### `useAwaySummary`
**File:** `hooks/useAwaySummary.ts`
**Purpose:** Appends a "while you were away" summary message after the terminal has been blurred for 5 minutes, when no turn is in progress.
**Parameters:**
- `messages: readonly Message[]`
- `setMessages: SetMessages`
- `isLoading: boolean`
**Return Value:** `void`
**Key Logic:**
- Gated on `feature('AWAY_SUMMARY')` bundle flag and `tengu_sedge_lantern` GrowthBook flag.
- Subscribes to `subscribeTerminalFocus` for blur/focus events.
- On blur: starts a `BLUR_DELAY_MS = 5 * 60_000` timer.
- Timer fire: if still loading, sets `pendingRef = true` (deferred); otherwise calls `generate()`.
- `generate()`: calls `generateAwaySummary(messages, signal)` → appends `createAwaySummaryMessage(text)`.
- On focus: clears timer, aborts in-flight generation, clears `pendingRef`.
- Second `useEffect` on `isLoading`: if `!isLoading` and `pendingRef` and still blurred, fires `generate()`.
- `hasSummarySinceLastUserTurn()`: walks backward to prevent duplicate summaries.
**Dependencies:** `useEffect`, `useRef`, `useCallback`, `subscribeTerminalFocus`, `generateAwaySummary`
---
### `useAfterFirstRender` / `renderPlaceholder`
See entry under "Core / Utility Hooks" for `useAfterFirstRender`.
---
## Notification Hooks (`notifs/`)
All hooks in `notifs/` use `useNotifications()` from `context/notifications.js` to push entries to the status bar notification queue. Most are gated on `!getIsRemoteMode()`.
---
### `useStartupNotification`
**File:** `hooks/notifs/useStartupNotification.ts`
**Purpose:** Base primitive for fire-once-on-mount notifications. Encapsulates the remote-mode gate and once-per-session ref guard used by most other `notifs/` hooks.
**Parameters:**
- `compute: () => Result | Promise<Result>` — returns `null` to skip, `Notification` for one, `Notification[]` for many.
**Return Value:** `void`
**Key Logic:**
- `hasRunRef` prevents re-firing on re-render.
- Runs `compute` inside `Promise.resolve().then(...)` to allow async.
- Catches errors via `logError`.
- Skips entirely in remote mode.
**Dependencies:** `useEffect`, `useRef`, `useNotifications`
---
### `useAutoModeUnavailableNotification`
**File:** `hooks/notifs/useAutoModeUnavailableNotification.ts`
**Purpose:** Shows a one-shot warning when the Shift+Tab mode carousel wraps past where "auto mode" would have been, explaining why auto mode is unavailable.
**Parameters:** none
**Key Logic:**
- Detects the wrap: `mode === 'default' && prevMode !== 'default' && prevMode !== 'auto' && !isAutoModeAvailable && hasAutoModeOptIn()`.
- Calls `getAutoModeUnavailableReason()` to get the specific reason (circuit-breaker, org-allowlist, settings).
- `shownRef` prevents showing more than once per session.
- Gated on `feature('TRANSCRIPT_CLASSIFIER')`.
---
### `useCanSwitchToExistingSubscription`
**File:** `hooks/notifs/useCanSwitchToExistingSubscription.tsx`
**Purpose:** Shows up to 3 times (MAX_SHOW_COUNT=3) across sessions a notification prompting users who have a Claude Pro/Max subscription but are logged in via API key to run `/login`.
**Key Logic:**
- Reads `globalConfig.subscriptionNoticeCount`; returns null if >= 3.
- Calls `getOauthProfileFromApiKey()` to check for Pro/Max subscription.
- Increments `subscriptionNoticeCount` in global config on each show.
- Renders a JSX notification with `color="suggestion"`.
---
### `useDeprecationWarningNotification`
**File:** `hooks/notifs/useDeprecationWarningNotification.tsx`
**Purpose:** Shows a `color="warning"` notification when the active model is deprecated.
**Parameters:** `model: string`
**Key Logic:**
- Calls `getModelDeprecationWarning(model)` on each `model` change.
- Uses `lastWarningRef` to avoid re-adding the same notification on re-render.
- Resets tracking if model changes to non-deprecated.
---
### `useFastModeNotification`
**File:** `hooks/notifs/useFastModeNotification.tsx`
**Purpose:** Shows real-time notifications for fast mode state changes: cooldown started/expired, org-level enable/disable, and overage rejection.
**Key Logic:**
- Subscribes to `onCooldownTriggered`, `onCooldownExpired`, `onFastModeOverageRejection`, `onOrgFastModeChanged` event emitters.
- On org-disabled while fast mode is active: disables fast mode in AppState.
- Shows immediate-priority notifications with `color="fastMode"` or `color="warning"`.
---
### `useIDEStatusIndicator`
**File:** `hooks/notifs/useIDEStatusIndicator.tsx`
**Purpose:** Shows IDE connection status in the notification area: hint to install extension, JetBrains info, install error, or current selection preview.
**Parameters:** `{ ideInstallationStatus, ideSelection, mcpClients }`
**Key Logic:**
- Uses `useIdeConnectionStatus(mcpClients)` to get current status.
- Shows "install extension" hint up to MAX_IDE_HINT_SHOW_COUNT=5 times (tracked in globalConfig).
- Shows JetBrains info notification (different flow than VS Code).
- Shows selection preview as a persistent notification when file/text is selected.
---
### `useInstallMessages`
**File:** `hooks/notifs/useInstallMessages.tsx`
**Purpose:** Shows startup notifications for native installer issues (PATH not configured, alias not set, install errors).
**Key Logic:**
- Calls `checkInstall()` to get installation messages.
- Maps message types to priorities: `error`/`userActionRequired``high`; `path`/`alias``medium`; others → `low`.
- Colors: `error``color="error"`; others → `color="warning"`.
---
### `useLspInitializationNotification`
**File:** `hooks/notifs/useLspInitializationNotification.tsx`
**Purpose:** Polls LSP server status every 5 000 ms and shows notifications when the LSP manager or individual servers fail to initialize.
**Key Logic:**
- Gated on `ENABLE_LSP_TOOL` env var.
- Polls `getInitializationStatus()` and `getLspServerManager()`.
- De-duplicates errors using `notifiedErrorsRef` set.
- Adds errors to `appState.plugins.errors` for `/doctor` display.
---
### `useMcpConnectivityStatus`
**File:** `hooks/notifs/useMcpConnectivityStatus.tsx`
**Purpose:** Shows notifications when MCP servers fail to connect or need authentication.
**Parameters:** `{ mcpClients?: MCPServerConnection[] }`
**Key Logic:**
- Filters `mcpClients` by connection state: `failed`, `needs_auth`.
- Separately tracks `claudeai` clients (connector) vs local clients (server).
- Shows JSX notifications with counts and `· /mcp` navigation hint.
---
### `useModelMigrationNotifications`
**File:** `hooks/notifs/useModelMigrationNotifications.tsx`
**Purpose:** Shows one-time notifications immediately after automatic model migrations (e.g. Sonnet 4.5 → 4.6, Opus Pro → Opus 4.6).
**Key Logic:**
- Uses `useStartupNotification` with a `MIGRATIONS` array of check functions.
- Each check reads a timestamp field from `globalConfig` and returns a notification if the timestamp is within the last 3 seconds (i.e., this is the launch that triggered the migration).
---
### `useNpmDeprecationNotification`
**File:** `hooks/notifs/useNpmDeprecationNotification.tsx`
**Purpose:** Shows a 15-second warning notification when Claude Code is running via an npm install (deprecated) rather than the native installer.
**Key Logic:**
- Skips if `isInBundledMode()` or `DISABLE_INSTALLATION_CHECKS` env var is set.
- Calls `getCurrentInstallationType()`; skips for `'development'` installs.
---
### `usePluginAutoupdateNotification`
**File:** `hooks/notifs/usePluginAutoupdateNotification.tsx`
**Purpose:** Subscribes to `onPluginsAutoUpdated` and shows a notification prompting the user to run `/reload-plugins` when plugins have been auto-updated in the background.
**Key Logic:**
- `useState([])` for `updatedPlugins` list.
- `onPluginsAutoUpdated` subscription fires with updated plugin IDs.
- Extracts plugin names (strips `@marketplace` suffix) and shows JSX notification with `color="success"`.
- 10 000 ms timeout.
---
### `usePluginInstallationStatus`
**File:** `hooks/notifs/usePluginInstallationStatus.tsx`
**Purpose:** Shows a notification when one or more plugins fail to install (from AppState `plugins.installationStatus`).
**Key Logic:**
- Reads `installationStatus.marketplaces` and `installationStatus.plugins` from AppState.
- Filters by `status === 'failed'`, memoizes counts.
- Shows `"N plugins failed to install · /plugin for details"` with `priority: 'medium'`.
---
### `useRateLimitWarningNotification`
**File:** `hooks/notifs/useRateLimitWarningNotification.tsx`
**Purpose:** Shows rate limit warnings: (1) immediate notification when entering overage mode; (2) warning notification when approaching usage limits.
**Parameters:** `model: string`
**Key Logic:**
- `useClaudeAiLimits()` for reactive limit data.
- `getRateLimitWarning(limits, model)` — string describing approaching limit.
- `getUsingOverageText(limits)` — string for overage mode.
- Overage notification shown once per overage entry (tracked via `hasShownOverageNotification` state).
- Team/enterprise: skips overage notification unless user has billing access.
- Warning notification shown only when text changes (deduped via `shownWarningRef`).
---
### `useSettingsErrors`
**File:** `hooks/notifs/useSettingsErrors.tsx`
**Purpose:** Watches for settings validation errors (from `getSettingsWithAllErrors`) and shows/removes a warning notification in the status bar.
**Return Value:** `ValidationError[]` — the current list of errors (also used for /doctor display).
**Key Logic:**
- Initial state populated synchronously from `getSettingsWithAllErrors()`.
- `useSettingsChange` subscription: re-reads errors on file change.
- Shows `"Found N settings issues · /doctor for details"` with 60 000 ms timeout.
- Removes notification when errors clear.
---
### `useTeammateShutdownNotification` / `useTeammateLifecycleNotification`
**File:** `hooks/notifs/useTeammateShutdownNotification.ts`
**Purpose:** Fires batched spawn/shutdown notifications when in-process teammates start or complete. Uses fold() to combine `"1 agent spawned"` + `"1 agent spawned"` into `"2 agents spawned"`.
**Key Logic:**
- Reads `tasks` from AppState.
- Tracks seen running/completed IDs in `seenRunningRef` / `seenCompletedRef`.
- `makeSpawnNotif(count)` / `makeShutdownNotif(count)` with 5 000 ms timeout and fold function.
- Exported as `useTeammateLifecycleNotification`.
---
## Tool Permission Subsystem (`toolPermission/`)
### `PermissionContext.ts`
**File:** `hooks/toolPermission/PermissionContext.ts`
**Purpose:** Factory for creating a `PermissionContext` object — the shared context passed to all three permission handlers. Contains all callbacks needed to approve, deny, log, queue, and persist permission decisions.
**Exported Functions:**
- `createPermissionContext(tool, input, toolUseContext, assistantMessage, toolUseID, setToolPermissionContext, queueOps?) → PermissionContext`
- `createPermissionQueueOps(setToolUseConfirmQueue) → PermissionQueueOps` — bridges React state setter to the generic queue interface.
- `createResolveOnce<T>(resolve) → ResolveOnce<T>` — atomic check-and-mark-as-resolved guard for races.
**PermissionContext methods:**
- `logDecision(args, opts?)` — delegates to `logPermissionDecision`.
- `logCancelled()` — logs `tengu_tool_use_cancelled` event.
- `persistPermissions(updates)` — calls `persistPermissionUpdates` and updates AppState.
- `resolveIfAborted(resolve)` — short-circuits if abort signal is fired.
- `cancelAndAbort(feedback?, isAbort?, contentBlocks?)` — builds a deny decision and aborts the controller if appropriate.
- `tryClassifier(pendingCheck, updatedInput)` — awaits classifier auto-approval (bash only; `BASH_CLASSIFIER` feature flag).
- `runHooks(permissionMode, suggestions, updatedInput?, startTimeMs?)` — executes PermissionRequest hooks sequentially.
- `buildAllow(updatedInput, opts?)``PermissionAllowDecision`
- `buildDeny(message, reason)``PermissionDenyDecision`
- `handleUserAllow(updatedInput, permissionUpdates, feedback?, startTimeMs?, contentBlocks?, decisionReason?)` — persists updates, logs, returns allow decision.
- `handleHookAllow(finalInput, permissionUpdates, startTimeMs?)` — same for hook-sourced allows.
- `pushToQueue(item)`, `removeFromQueue()`, `updateQueueItem(patch)` — queue management via `queueOps`.
---
### `permissionLogging.ts`
**File:** `hooks/toolPermission/permissionLogging.ts`
**Purpose:** Centralized analytics and telemetry logging for all tool permission decisions. Fans out to Statsig (logEvent), OTel telemetry, code-edit metrics, and the `toolUseContext.toolDecisions` map.
**Exported Functions:**
- `logPermissionDecision(ctx, args, startTimeMs?)` — main entry point.
- `isCodeEditingTool(toolName)` — checks if tool is Edit/Write/NotebookEdit.
- `buildCodeEditToolAttributes(tool, input, decision, source)` — builds OTel attributes including language from file path.
**Analytics Events:**
- `tengu_tool_use_granted_in_config` — auto-approved by settings allowlist.
- `tengu_tool_use_granted_in_prompt_permanent` / `_temporary` — user approved.
- `tengu_tool_use_granted_by_permission_hook` — hook approved.
- `tengu_tool_use_granted_by_classifier` — classifier approved.
- `tengu_tool_use_rejected_in_prompt` — any rejection.
- `tengu_tool_use_denied_in_config` — denied by settings denylist.
---
### `handlers/coordinatorHandler.ts`
**File:** `hooks/toolPermission/handlers/coordinatorHandler.ts`
**Purpose:** Handles the coordinator-worker permission flow: runs hooks then classifier (both awaited sequentially) before falling through to the interactive dialog.
**Exported:** `handleCoordinatorPermission(params) → Promise<PermissionDecision | null>`
**Parameters:** `CoordinatorPermissionParams``{ ctx, pendingClassifierCheck?, updatedInput, suggestions, permissionMode }`
**Logic:**
1. `await ctx.runHooks(...)` — if hooks return a decision, return it.
2. If `BASH_CLASSIFIER` flag: `await ctx.tryClassifier?.(...)` — if classifier returns, return it.
3. Return `null` → caller falls through to `handleInteractivePermission`.
4. On unexpected error: logs and returns null (graceful fallback to dialog).
---
### `handlers/interactiveHandler.ts`
**File:** `hooks/toolPermission/handlers/interactiveHandler.ts`
**Purpose:** Handles the interactive (main-agent) permission flow. Sets up the `ToolUseConfirm` queue entry with all callbacks and races user interaction against background automated checks (hooks, classifier, bridge, channel).
**Exported:** `handleInteractivePermission(params, resolve) → void` (synchronous setup)
**Key Logic:**
- Creates a `PermissionConfirm` queue entry with callbacks: `onAbort`, `onAllow`, `onReject`, `recheckPermission`, `onUserInteraction`, `onDismissCheckmark`.
- `createResolveOnce` guard ensures only the first resolution wins.
- `userInteracted` flag: prevents classifier from auto-approving after user interaction (200 ms grace period).
- **Race 1 — User**: `onAllow`/`onReject`/`onAbort` callbacks.
- **Race 2 — Hooks**: async `ctx.runHooks(...)` — if win, removes from queue, resolves.
- **Race 3 — Classifier**: `executeAsyncClassifierCheck(...)` — on allow, shows checkmark UI for 3 s (focused) or 1 s (blurred), then removes from queue.
- **Race 4 — Bridge (CCR)**: sends `permission_request` to CCR; subscribes to CCR response; on win, logs, resolves.
- **Race 5 — Channel**: sends structured `permission_request` to all active channel MCP servers (Telegram, iMessage); subscribes to response; on win, resolves.
- Checkmark dismissal: `onDismissCheckmark` allows user to press Escape during the checkmark window.
- Abort: if abort signal fires mid-dialog, `claim()` races to resolve with cancel.
---
### `handlers/swarmWorkerHandler.ts`
**File:** `hooks/toolPermission/handlers/swarmWorkerHandler.ts`
**Purpose:** Handles the swarm-worker permission flow: tries classifier auto-approval, then forwards the request to the team leader via mailbox. Awaits the leader's response.
**Exported:** `handleSwarmWorkerPermission(params) → Promise<PermissionDecision | null>`
**Logic:**
1. Returns `null` if not `isAgentSwarmsEnabled()` or not `isSwarmWorker()`.
2. If `BASH_CLASSIFIER`: tries `ctx.tryClassifier?.(...)`.
3. Creates a `Promise<PermissionDecision>` that resolves when the leader responds.
4. Registers `onAllow`/`onReject` callbacks via `registerPermissionCallback`.
5. Calls `sendPermissionRequestViaMailbox(request)` to notify the leader.
6. Sets `AppState.pendingWorkerRequest` for visual indicator.
7. On abort: resolves with `cancelAndAbort`.
8. On error: returns `null` (fallback to local UI handling).
---
## Non-Hook Utilities in `hooks/`
These files live in `hooks/` but are not React hooks.
### `fileSuggestions.ts`
**File:** `hooks/fileSuggestions.ts`
**Exports:**
- `generateFileSuggestions(query, cwd, ...) → Promise<SuggestionItem[]>` — main entry point. Manages a `FileIndex` singleton (native Rust/nucleo), fetches tracked files via `git ls-files`, falls back to `ripgrep`. Returns up to 15 scored matches.
- `startBackgroundCacheRefresh(cwd)` — queues a background refresh of untracked files.
- `clearFileSuggestionCaches()` — called on `/clear` to reset the index.
- `applyFileSuggestion(suggestion, inputValue, cursorOffset) → string` — replaces the `@token` in the input with the chosen file path.
- `findLongestCommonPrefix(suggestions) → string` — used for Tab-autocomplete of common prefix.
- `onIndexBuildComplete(callback)` — notifies when background index is built.
**Key Design:**
- Path signature (mtime + size) invalidates cache without full rebuild.
- `.ignore` / `.rgignore` file support.
- Directory name extraction for `@dir/` completions.
---
### `unifiedSuggestions.ts`
**File:** `hooks/unifiedSuggestions.ts`
**Exports:**
- `generateUnifiedSuggestions(query, mcpResources, agents, showOnEmpty) → Promise<SuggestionItem[]>` — merges file suggestions (nucleo), MCP resource suggestions (Fuse.js), and agent suggestions into a ranked list of up to 15 items.
**Key Design:**
- File suggestions use nucleo score (01 float).
- MCP resource suggestions use Fuse.js score (lower = better, inverted for sorting).
- Agent suggestions always appended at lower priority.
---
### `renderPlaceholder.ts`
**File:** `hooks/renderPlaceholder.ts`
**Exports:**
- `renderPlaceholder(placeholder, hidePlaceholderText?, cursorChar?) → string` — pure function that renders placeholder text with a cursor character appended. When `hidePlaceholderText = true` (voice recording mode), returns only the cursor.