git:20260826.fb8c6e0 to git:20260829.bb874cc

1 added, 1 removed. Audit A to A.

# DOX — packages/client/src/hooks
Files in this directory. One row per source file.
| File | Purpose |
|------|---------|
| `useActiveChatSelection.ts` | `useActiveChatSelection(containerRef, mapRange) → { isSelecting, isSelectingRef, selectionSpanRef, selectionAnchorRef }`. `selectionchange` listener; `isSelecting` true when a non-collapsed Selection has anchor OR focus inside `containerRef` (cross-boundary safe, NOT anchor-only). Boolean flip microtask-coalesced (ref + single setState); `selectionSpanRef` updated synchronously each event via `mapRange` (PROACTIVE capture while rows mounted → `rangeExtractor` reads it so selected rows never unmount). See change: preserve-chat-selection-during-churn. `isSelectingRef` publishes the boolean SYNCHRONOUSLY inside the listener (D6) for guards running outside render (the virtualizer `onChange` bottom-pin) — the old render-time mirror in `ChatView` lagged a microtask AND a render, so a chunk on the first frame of a drag still hit the pin; the debounced `isSelecting` STATE is unchanged because render-driven effects need the re-render and the →false edge. `selectionAnchorRef` is the drag-ORIGIN `[data-index]` row `Element` (never its `data-index` — an insertion above renumbers indices, the same retarget bug being fixed), captured ONCE on the collapsed→non-collapsed transition via `nextAnchor` and cleared on collapse; `null` for a cross-boundary drag anchored outside the container. See change: anchor-chat-selection-against-row-growth (D4/D6). |
| `useAnthropicPeerProbe.ts` | `useAnthropicPeerProbe() -> { peerMissing, peerReason }` for the Anthropic OAuth row hint. Reads `/api/health.plugins[]`, finds the `flows-anthropic-bridge` row, derives `peerMissing` STRICTLY from `lastProbe.peers[PEER_AM_LEGACY].ok === false` (legacy key `@pi/anthropic-messages`, imported from `flows-anthropic-bridge-plugin/peer-probe`); every other shape (no response, non-OK, no plugin row, no `lastProbe`, malformed payload, loading) is fail-open `false`. Re-reads on mount, a 60s poll while mounted (cadence from `usePiCompatibility.ts`), window `focus`, and a successful `pi-package-event` `package_operation_complete`. Also exports `ANTHROPIC_PEER_SOURCE` (from `RECOMMENDED_EXTENSIONS`) + `IMPORT_FAILURE_PREFIX`. See change: warn-missing-anthropic-messages-peer. |
| `useAppHidden.ts` | NEW. Exports `useAppHidden()` hook + `applyAppHiddenClass(root, hidden)`. Toggles `app-hidden` class on document root from `document.visibilityState`; listens visibilitychange + window blur/focus. CSS `:root.app-hidden *` sets `animation-play-state: paused`, freezes compositor when window hidden to tray. See change: throttle-idle-ui-animations. |
| `useArchiveListing.ts` | Fetches `GET /api/openspec-archive?cwd=` into `entries: ArchiveEntry[]` with `isLoading`/`error`. Exports `groupByDate(entries)` (sort newest-first) and `filterEntries(entries, query)` (case-insensitive slug match). Re-fetches on `cwd` change; cancels stale fetch. |
| `useAsyncAction.ts` | `useAsyncAction(fn, opts) → { pending, error, run, bind }`. Wraps async action. Tracks idle→pending→success\|error. `bind` spreads `onClick`+`disabled`. Guards concurrent runs via pendingRef. Routes failure to `opts.showToast` (error variant), success to `opts.onSuccess`+`opts.successToast`. `confirm:"http"` (default) ends pending when fn settles. `confirm:"ws"` registers WS handler on run() via `opts.onMessage` before fn fires, holds pending until `opts.confirmEvent(msg, result)` matches echoed requestId, clears on match; `opts.confirmTimeoutMs` (default 15000) fallback emits info "Still working in the background…" toast, never stuck-spins. Generalizes WorktreeInitButton FSM. See change: add-async-action-feedback. |
| `useAuthStatus.ts` | Fetches `GET /auth/status` into `authStatus: AuthStatus | null` (`authenticated`, `authEnabled`, `user`) with `loading`. Falls back to `{ authenticated: true, authEnabled: false }` on 404. Exports `redirectToLogin()` — redirects to `/auth/login?return=`. |
| `useContentViews.ts` | URL-routing navigation helpers. `handleOpenDirectorySettings(cwd)` (renamed from `handleOpenPiResources`, change: add-folder-actions-menu — route + label always said Directory Settings, only the name lagged) navigates to `buildFolderSettingsUrl(cwd)`. `handleViewPiResourceFile(filePath, title)` navigates to `buildPiResourceFileUrl(filePath, title)`. Takes `navigate: (to: string) => void`. |
| `useDebugToolsVisible.ts` | Deprecated shim over `useDisplayPrefs().debugTools`. Exports `DEBUG_TOOL_NAMES` set + `isDebugTool(toolName)`. `useDebugToolsVisible()` returns `[boolean, setter]`; setter PATCHes `/api/preferences/display` `{ debugTools }` and strips legacy `show-debug-tools` localStorage key. |
| `useDisplayPrefs.ts` | `useDisplayPrefs(sessionId?): DisplayPrefs` — reads context, returns `mergeDisplayPrefs(global, getSessionOverride(sessionId))`. Returns `DISPLAY_PRESETS.standard` when global undefined (pre-first-launch). See change: configurable-chat-display. |
| `useDocumentTitle.ts` | Sets `document.title` via `buildDocumentTitle(session, folderCwd)`; resets to `"PI Dashboard"` on cleanup. Re-runs on `session`/`folderCwd` change. |
| `useFolderUrgencySort.ts` | Per-folder opt-in urgency-sort pref. Default off. localStorage key dashboard:folder-urgency-sort (FOLDER_URGENCY_SORT_KEY). Returns {isOn,toggle}. Pure-client persistence. See change: improve-dashboard-attention-routing. |
| `useHostPlatform.ts` | One-shot probe of `/api/health` `platform` field. Returns host OS (darwin\|win32\|linux) for Settings → Tools install-hint filtering. `browserPlatformFallback()` reads `navigator.userAgentData.platform` when probe misses. Module-scope cache like `useLaunchSource`. See change: register-bash-and-tool-install-help. |
| `useImagePaste.ts` | Clipboard-image-paste state. Supports uncontrolled (owns `pendingImages`) and controlled (`images`/`onImagesChange`) modes. Exports `MAX_IMAGE_SIZE` (10MB base64), `SUPPORTED_IMAGE_TYPES` set. Returns `{ pendingImages, imageError, handlePaste, removeImage, clearImages, addFiles }`. `addFiles(FileList|File[])` shares paste's MIME/size validation (`ingestBlob`) for the composer `+` attach-image file-picker path. Auto-clears errors after 3s. See change: redesign-prompt-input. |
| `useInflightBashTools.ts` | Pure selector `selectInflightBashTools(state)` + memoized hook `useInflightBashTools(state)`. Filters event-reducer `toolCalls` Map to unresolved `bash` tools (toolName matched case-insensitive). Returns `Array<{ toolCallId, command, startedAt }>` sorted newest-first by `startedAt`. Consumed by `App.tsx` (builds `inflightBashMap` via `useMemo`) and threaded through `SessionList` to `SessionCard`'s PROCESS subcard. Also extends `event-reducer.ts` `ToolCallState` with `startedAt` (stamped at `tool_execution_start`). See change: redesign-process-list-activity-bar. |
| `useInitStatus.ts` | `useInitStatus(cwd) → { status: WorktreeInitStatus\|null, refetch }`. Single shared `GET /api/git/worktree/init-status` probe for a folder-action-bar row; feeds BOTH `ProjectInitButton` (scaffold) and `WorktreeInitButton` (hook run) from one fetch (avoids double-probe). `refetch` re-issues after a hook run flips the gate. Fail-open via `fetchWorktreeInitStatus`. See change: distinguish-initialize-actions. |
| `useInstalledPackages.ts` | Fetches `GET /api/packages/installed?scope=&cwd=` into `packages: InstalledPackage[]` with `isLoading`/`error`/`refresh`. Auto-refreshes on `pi-package-event` `package_operation_complete` success. Cancels stale fetches via mounted ref. |
| `useInstallPrompt.ts` | PWA install-prompt state. Returns `{ canInstall, isInstalled, isIOS, prompt }`. Defers `beforeinstallprompt` event; `isInstalled`/`isIOS` detected via `display-mode: standalone` + UA. `prompt()` triggers deferred install. |
| `useLaunchSource.ts` | One-shot probe of `/api/health` `launchSource` field (`"electron" | "standalone" | "bridge"`). Module-level cached + deduped inflight. Returns `null` while in flight; consumers fail-open. Exports test-only `__resetLaunchSourceCacheForTests()`. |
| `useMainSpecsReader.ts` | Reads `openspec/specs/` directory, fetches each `spec.md` in parallel, concatenates into single markdown `content`. Returns `{ specNames, content, isLoading, error }`. Aborts stale loads via `AbortController`. Re-runs on `cwd` change. |
| `useMediaQuery.ts` | Re-export shim. Forwards `useMediaQuery` from `@blackbelt-technology/pi-dashboard-client-utils/useMediaQuery`. Migration Layer 0. |
| `useMessageHandler.ts` | New `case "view_messages_update"`: replaces `viewMessagesMap.get(sessionId)` with `msg.viewMessages.slice()`. → see `useMessageHandler.ts.AGENTS.md` `history_backfill_result` gains a head-free exhaustion branch: an exhausted HEAD-FREE gap resolves to `atFloor` (terminus) instead of splicing the divider out, and is NOT marked `unservable` — nothing failed, the walk finished. A response whose divider was never placed (`!gap.dividerPlaced`) is skipped entirely, so a no-op splice cannot advance the bookkeeping and desync gap state from `messages[]`. `createHistoryGapState` carries the announced `windowShape` through. See change: add-tail-only-replay-window (D5, D6). |
| `useMobile.tsx` | Re-export shim. Forwards `useMobile` from `@blackbelt-technology/pi-dashboard-client-utils/useMobile`. Migration Layer 0. |
| `useOpenSpecActions.ts` | OpenSpec action callbacks. `handleOpenSpecRefresh`/`handleBulkArchive` send WS `openspec_refresh`/`openspec_bulk_archive`. `handleReadArtifact` navigates to `buildOpenSpecPreviewUrl`. `handleAttachProposal`/`handleDetachProposal`/`handleReplaceProposal` send WS attach/detach/accept_replace_proposal/dismiss_replace_proposal. |
| `useOpenSpecReader.ts` | Fetches OpenSpec change artifact content. `activeTab` derives from URL `initialArtifact` (single source of truth). Builds `tabs` from `artifacts` with `statusColor`. `specs` artifact fetches directory + all `spec.md` in parallel. AbortController cancels stale loads. Returns `{ content, isLoading, error, tabs, activeTab, title }`. |
| `usePackageOperations.ts` | Subscriber over singleton `packageQueue`. Returns `operation`, `install`/`remove`/`update` (enqueue), `coreUpdate(name)` (enqueues `kind:"pi-core"` under source `pi-core:<scoped-npm-name>`; `name` is `PiCorePackage.name`, `scope:"global"` is a placeholder the endpoint ignores — change: unify-pi-core-into-package-queue), `move`/`moveStateFor`/`clearMove` (via `moveTracker`), `resetToNpm(source,{scope,cwd})` (POST `/reset-to-npm`, registers a `moveTracker` state kind:`"reset"` — change: reset-override-to-npm), `statusFor`/`messageFor`/`queueDepth`/`runningSource`/`isAnyRunning`. `clearOperation`/`handleMessage` are back-compat no-ops. Uses `useSyncExternalStore` with stable snapshot. |
| `usePackageSearch.ts` | Debounced npm package search via `GET /api/packages/search?q=&type=`. Returns `{ query, setQuery, typeFilter, setTypeFilter, packages, total, isLoading, error, refresh }`. 400ms debounce on query; no debounce on initial/type-only. Aborts stale requests. |
| `usePendingPromptTimeout.ts` | Calls `onTimeout` after 30s if `hasPendingPrompt` stays true and `paused` is false. Timer restarts when `paused` flips false→true→false (resume after queue drain). `onTimeout` kept in ref. Callers arm it on `pendingPrompt.status === "sending"` (never `!!pendingPrompt`), so a settled `failed` bubble never re-arms the timer and is never wiped. See change: fix-optimistic-prompt-stuck-sending. |
| `usePiChangelog.ts` | Lazy hook (enabled gate). Refetches on `pi_core_update_complete` WS event for matching `pkg`. Never throws. See change: pi-update-whats-new-panel. |
- | `usePiCompatibility.ts` | NEW. Fetches `/api/health` on mount + every 60s. Returns `compatibility` field or null. See change: restore-pi-version-skew-surface. |
+ | `usePiCompatibility.ts` | NEW. Fetches `/api/health` on mount + every 60s (instance-scoped: invoke ONCE per panel, pass fields down). Returns `{ compatibility, piRuntime }`, each null when absent/unresolvable. Exports `PiCompatibility`, `PiRuntimeHealth` (mirrors server `PiDivergenceHealth` — versions + divergence only, D2 gate). Breaking: was nullable `PiCompatibility` (now a field of the returned object, null when absent). See change: restore-pi-version-skew-surface, surface-pi-runtime-on-general. |
| `usePiCoreVersions.ts` | Fetches `GET /api/pi-core/versions` into `status: PiCoreStatus` with `isLoading`/`error`/`refresh(force?)`. Polls every 30 min. Force-refreshes on `pi-core-event` `pi_core_update_complete`. |
| `usePiResourceFileFetch.ts` | Fetches `GET /api/pi-resource-file?path=` into `{ content, isLoading, error }`. Detects source language from extension via `SOURCE_LANG_MAP`, wraps content in fenced code block. Re-fetches on `filePath` change. |
| `usePiResources.ts` | Fetches `GET /api/pi-resources?cwd=&refresh=` into `data: PiResourcesResult` with `isLoading`/`error`/`refresh`. Polls every 30s. Guards against stale `cwd` via ref. Clears state when `cwd` is null. `usePiResources(null,{globalOnly:true})` omits cwd (server scans its own cwd) for the global Settings resource pages; caller reads `data.global`. See change: resources-card-tabs. |
| `useResourceActivation.ts` | Owns the Resources-surface activation UX. `useResourceActivation(cwd?)` → `{isEnabled, toggle(r,scope,packageSource?), isFolderControlled, pending, reload, clearPending, error, clearError, trustPrompt, resolveTrust, dismissTrust}`. `toggle` no-ops for `agent` (pi has no agent activation dim). Optimistic enabled-override map (reverts on toggle failure); after a toggle sets `pending={scope,cwd?,count}` from `affectedSessions` for the one-click "Reload N sessions" affordance. Failures surface as `error={filePath,message,kind}` — `kind:"server"` carries the server's own message, `kind:"network"` marks a request that never reached the server; both revert the optimistic flip. A `403 trustRequired` response opens `trustPrompt` instead of converging the control; `resolveTrust(optionId)` persists via `submitResourceTrust` then replays the deferred toggle, `decline`/`dismissTrust` reverts and writes nothing. `isFolderControlled(r)` marks a global resource whose activation this folder took over. `cwd` is sent at BOTH scopes so the resource resolves against the viewed folder. See changes: folder-resource-activation-toggle, resources-card-tabs, project-scope-disable-global-resources. |
| `usePluginEnabledSet.ts` | Drives `registry.setEnabledSet(ids)` from `/api/health.plugins[]` snapshot on mount + on every `plugin-config-update` DOM event (re-emitted by `useMessageHandler` from `plugin_config_update` WS). Also primes per-plugin requirement caches. See change: add-plugin-activation-ui. |
| `usePluginToggle.tsx` | Exports `usePluginList`, `usePluginToggle`, `applyDesiredEnabled`. `usePluginList` owns the `GET /api/plugins` fetch + `plugin-config-update` subscription and layers a DESIRED-state `enabled` overlay on the server's runtime snapshot (which lags until restart); seeded from `GET /api/config`.plugins. `usePluginToggle` owns cascade preview + `cascadeDialog` element, per-row toggling/error state, and restart-required/`restartError` state. Shared by `PluginsSection`, `PluginSettingsPage`, and the settings nav rail. See change: plugin-settings-pages. |
| `usePopoverFlip.ts` | Shared viewport-anchored popover positioning hook. `usePopoverFlip(triggerRef, { open, estimatedHeight?, gap?, threshold?, estimatedWidth?, minPopoverHeight? }) → { flipUp, maxHeight, minHeight, anchorRight, maxWidth }`. Measures trigger `getBoundingClientRect` on open + on passive resize/scroll while open. Vertical: default down; flips up when `spaceBelow < min(estimatedHeight, threshold=200)` AND `spaceAbove > spaceBelow`; `maxHeight = max(0, chosen-side space)` — a TRUE BOUND, never floor-inflated. Horizontal: default `anchorRight=true` (right-0); flips to left-0 only when `Number.isFinite(estimatedWidth)` AND `spaceRightAnchor(=rect.right-gap) < estimatedWidth` AND `spaceLeftAnchor(=innerWidth-rect.left-gap) > spaceRightAnchor`; `maxWidth = max(MIN_POPOVER_WIDTH=160 floor, chosen-anchor space)`. `estimatedWidth` default Infinity → never flips horizontally (backward-compatible with the 7 right-anchored consumers). `typeof window` guard. Single source of truth replacing hand-rolled bottom-full/max-h-NN flip in ModelSelector, ThinkingLevelSelector, CommandInput. Restores specced auto-flip on ChatViewMenu. Adopted in ThemePicker, PackageRow, WorktreeActionsMenu mobile sheet. **Boundary-aware (fix-popover-container-clip):** optional `boundaryRef?: RefObject<HTMLElement\|null>` → BOTH axes measure `leftEdge/rightEdge/topEdge/bottomEdge` from `boundaryRef.current.getBoundingClientRect()` (else `0..innerWidth`/`0..innerHeight`, byte-identical viewport fallback); `preferredAnchor?: "left"\|"right"` (default `"right"`) keeps a `left-0` consumer left unless it must flip; `minContentWidth?` (default 0) flips instead of clamping `maxWidth` below readability (clamps only when neither side fits). Boundary staleness: while open+boundary, adds a `scroll` listener on the boundary + a `ResizeObserver` on it (both re-`measure()`), torn down with the window listeners. Dev-only `console.warn` when the boundary does not contain the trigger (self-boundary mis-wire guard). Boundary ref supplied via `PopoverBoundaryContext`/`usePopoverBoundary`. Measures in `useLayoutEffect` (pre-paint) so the popover never paints a frame at the initial CLOSED_STATE (right-0/160 floor) before the corrected anchor lands (no flicker; deterministic rect reads). See change: fix-popover-viewport-flip. See change: fix-popover-horizontal-flip — adds horizontal axis (anchorRight/maxWidth). See change: fix-popover-container-clip. **Height split into bound + floor (fix-popover-pane-bounded-height):** the old `maxHeight = max(MIN_POPOVER_HEIGHT, space)` applied the floor AFTER clamping, so it could exceed the space it was clamped to and push the popover past the boundary edge (growing the host pane's scroll extent → second scrollbar). Now `maxHeight = max(0, space)` (bound; the `0` clamp only guards a trigger scrolled outside the boundary in both directions, where a negative CSS `max-height` would be dropped and leave the popover unbounded) and `minHeight = min(minPopoverHeight, maxHeight)` (floor, capped by the bound → can never overflow; collapses onto `maxHeight` when space < floor). Consumers MUST apply BOTH — applying only `maxHeight` silently loses the floor. Rendered height = natural content height clamped between them BY THE BROWSER during normal layout; the hook measures no content. `minPopoverHeight?` (default `MIN_POPOVER_HEIGHT=120`) is per-consumer, not global: `LIST_POPOVER_MIN_HEIGHT=260` is exported for filterable list popovers (ModelSelector, CommandInput composer dropdown); short fixed menus keep 120 or they render dead space. The floor opt-in lives INSIDE each consumer component, not at its mount site, so a new host re-mounting a consumer inherits both bounds and cannot forget the floor — **9 consumer surfaces over these 8 call sites**; the 9th is `components/openspec/useOpenSpecRunConfigRow.tsx` (OpenSpec launch dialogs), which re-mounts ModelSelector + ThinkingLevelSelector. Enumeration table: `openspec/changes/fix-popover-pane-bounded-height/specs/popover-viewport-positioning/spec.md`. `CLOSED_STATE.minHeight = 0` (a closed popover asserts no floor). Measure path is REFLOW-FREE by contract — it reads only `getBoundingClientRect()` on trigger + boundary, never `scrollHeight`/content metrics, because it runs on every window `resize`/`scroll` plus boundary `scroll`/`ResizeObserver`; test H7 spies the `scrollHeight` getters and asserts no access. See change: fix-popover-pane-bounded-height. **Additive `triggerRect` (add-overlay-layering-system):** also returns the trigger's viewport rect `{top,bottom,left,right,width}` (null while closed) so a PORTALED consumer can position a `fixed` panel; existing inline consumers ignore it (no behaviour change). The existing capture-phase window `scroll` listener is what makes a portaled panel track an ancestor scroller (sidebar). See change: add-overlay-layering-system. |
| `useProvidersReady.ts` | Polls `/api/providers` + `/api/provider-auth/status`, returns `ProvidersReadyState` (`loading`, `ready`, `count`). Refetches on window focus + `provider-auth-event`. Exports `useProvidersReady`. |
| `useRecommendedExtensions.ts` | Fetches `GET /api/packages/recommended`, returns `EnrichedRecommendedExtension[]` + `isLoading`/`error`/`refresh`. Refetches on `pi-package-event` (`package_operation_complete`+success). Exports `useRecommendedExtensions`. |
| `useSessionActions.ts` | Session action callbacks extracted from App.tsx. Sends… → see `useSessionActions.ts.AGENTS.md` |
| `useSessionDiff.ts` | Fetches `GET /api/session-diff?sessionId=`, returns `SessionDiffResponse` + `isLoading`/`error`/`refresh`. Refetches on `sessionId` change. Exports `useSessionDiff`, `UseSessionDiffResult`. |
| `useSessionState.ts` | Embed-side session-state accumulator. Exports `useSessionState(sessionId?)` (`{state, apply, reset}`) and the pure `SessionStateAccumulator` fold over `ServerToBrowserMessage` — a switch SEPARATE from `useMessageHandler`'s, with its own `addInteractiveRequest` call sites. `case "prompt_request"` → `addPromptBusRequest`; `case "notify"` → `addNotify` (render-only chat row, never an `interactiveRequests` entry). See change: split-notify-from-prompt-request. Reset/replay carry sites go through `carryPendingPrompt`. See change: fix-optimistic-prompt-stuck-sending. |
| `useSidebarState.ts` | Persists sidebar `width` + `collapsed` to `localStorage` (`dashboard:sidebar-width`, `dashboard:sidebar-collapsed`). Clamps width 180–500. Exports `useSidebarState`, `SidebarState`, `MIN_WIDTH`/`MAX_WIDTH`/`DEFAULT_WIDTH`/`WIDTH_KEY`/`COLLAPSED_KEY`. |
| `useStaleToolReconcile.ts` | Session-scoped stale running-tool heal (survives transcript virtualization). `selectStaleRunningTools` scans `toolCalls` for `running` rows older than `STALE_TOOL_MS` (25s); `reconcile` fetches `GET /api/sessions/:id/tool-result/:toolCallId` (HTTP, off the WS send buffer). HTTP 200 → `synthesizeToolEndEvent` applies the authoritative result; 404 leaves the row running + re-arms (`RECONCILE_REARM_MS` 15s) + increments a per-row 404 count. See change: fix-stuck-tool-card-on-dropped-event. **Supersede heal (last resort):** `selectSupersededHealTargets(states, min404, get404)` picks `running` rows with ≥`SUPERSEDE_MIN_404` (2) 404s AND `hasLaterAssistantInference` true; the same tick applies `synthesizeSupersededEnd` (complete + `healedBy:"superseded"` + loud sentinel body), `console.warn`s a running heal total. Runs AFTER the base probe so a real 200 always wins; a real end later overwrites the placeholder (reducer D4). See change: fix-stuck-tool-card-superseded-heal. |
| `useSubagentResyncCadence.ts` | Open-inspector liveness (D4 v1): a mounted detail view re-fires `subagent_resync_request` on a backoff cadence — `CADENCE_BASE_MS` 2000, ×2 per idle tick, `CADENCE_MAX_MS` 30000 ceiling, reset on entry growth. ONE shared timer per subagent key, so inline inspector + popout never double-fire. No `emptyTimeline` precondition (that precondition is why a mounted view never re-fires today). See change: reduce-subagent-details-payload. |
| `useSwipeBack.ts` | iOS-style left-edge swipe-back gesture. Touch listeners decide horizontal vs vertical after 10px, triggers `onBack` past `threshold` of screen width. Returns `containerRef` + `swipeState` (`offset`, `swiping`). Exports `useSwipeBack`, `SwipeBackOptions`. |
| `useTreeColumnWidth.ts` | Persisted width + drag lifecycle for the Instructions folder-tree column (peer of `useSidebarState`). Clamp 200–560, key `dashboard:dirset-width`; live width during drag, commits to `localStorage` on mouseup. Returns `{width, containerRef, startResize}`; `localStorage` throw degrades to in-memory. Exports `useTreeColumnWidth`, `TreeColumnWidth`, `MIN_WIDTH`/`MAX_WIDTH`/`DEFAULT_WIDTH`/`WIDTH_KEY`. See change: directory-settings-tree-and-resize. |
| `useTheme.ts` | Theme mode + named-theme state. Reads `dashboard:theme`/`dashboard:theme-name` from `localStorage`, resolves system pref via `prefers-color-scheme`, applies `data-theme` attr + CSS var overrides. Listens for OS changes. Exports `useTheme`, `ThemeState`, `applyThemeVars`, `ThemePreference`, `ResolvedTheme`, `STORAGE_KEY`, `THEME_NAME_KEY`. |
| `useToolFullResult.ts` | NEW. Fetch hook `useToolFullResult(sessionId, toolCallId) → { result?, error?, loading, fetchFull }`. Hits tool-result endpoint; 404 → error "result evicted". Consumed by `ToolCallStep` Show-full-output button. See change: adopt-pi-071-072-073-features. |
| `useViewDispatcher.ts` | Sends `session_view`/`session_unview` on `viewedSessionId` transitions, re-sends `session_view` on every WebSocket (re)connect into `connected`. Exports `useViewDispatcher`, `UseViewDispatcherDeps`. |
| `useWebSocket.ts` | WebSocket lifecycle: connects `url`, parses `ServerToBrowserMessage`, exposes `send`/`onMessage`/`status` (`connected`/`connecting`/`offline`/`auth_required`). Exponential backoff reconnect (via `connectRef` so reconnect re-runs the current path), auth probe after `OFFLINE_THRESHOLD` closes, registers `send` as plugin-action sender. Paired-device browsers (`getDeviceBearer()` set) mint a FRESH single-use `/api/ws-ticket` per (re)connect and present only `?ticket=` (durable bearer never rides WS, F6); unpaired browsers skip ticketing (cookie/loopback path unchanged). Exports `useWebSocket`, `ConnectionStatus`. See change: make-pairing-qr-camera-scannable. |
| `useZoomPan.ts` | Re-export shim. Forwards `@blackbelt-technology/pi-dashboard-client-utils/useZoomPan` (moved in `complete-flows-plugin-migration` Layer 0). |