diff --git "a/src/screens/REPL.tsx" "b/src/screens/REPL.tsx"
--- "a/src/screens/REPL.tsx"
+++ "b/src/screens/REPL.tsx"
@@ -1,369 +1,694 @@
-import { c as _c } from "react/compiler-runtime";
// biome-ignore-all assist/source/organizeImports: ANT-ONLY import markers must not be reordered
-import { feature } from 'bun:bundle';
-import { spawnSync } from 'child_process';
-import { snapshotOutputTokensForTurn, getCurrentTurnTokenBudget, getTurnOutputTokens, getBudgetContinuationCount, getTotalInputTokens } from '../bootstrap/state.js';
-import { parseTokenBudget } from '../utils/tokenBudget.js';
-import { count } from '../utils/array.js';
-import { dirname, join } from 'path';
-import { tmpdir } from 'os';
-import figures from 'figures';
+import { feature } from 'bun:bundle'
+import { spawnSync } from 'child_process'
+import {
+ snapshotOutputTokensForTurn,
+ getCurrentTurnTokenBudget,
+ getTurnOutputTokens,
+ getBudgetContinuationCount,
+ getTotalInputTokens,
+} from '../bootstrap/state.js'
+import { parseTokenBudget } from '../utils/tokenBudget.js'
+import { count } from '../utils/array.js'
+import { dirname, join } from 'path'
+import { tmpdir } from 'os'
+import figures from 'figures'
// eslint-disable-next-line custom-rules/prefer-use-keybindings -- / n N Esc [ v are bare letters in transcript modal context, same class as g/G/j/k in ScrollKeybindingHandler
-import { useInput } from '../ink.js';
-import { useSearchInput } from '../hooks/useSearchInput.js';
-import { useTerminalSize } from '../hooks/useTerminalSize.js';
-import { useSearchHighlight } from '../ink/hooks/use-search-highlight.js';
-import type { JumpHandle } from '../components/VirtualMessageList.js';
-import { renderMessagesToPlainText } from '../utils/exportRenderer.js';
-import { openFileInExternalEditor } from '../utils/editor.js';
-import { writeFile } from 'fs/promises';
-import { Box, Text, useStdin, useTheme, useTerminalFocus, useTerminalTitle, useTabStatus } from '../ink.js';
-import type { TabStatusKind } from '../ink/hooks/use-tab-status.js';
-import { CostThresholdDialog } from '../components/CostThresholdDialog.js';
-import { IdleReturnDialog } from '../components/IdleReturnDialog.js';
-import * as React from 'react';
-import { useEffect, useMemo, useRef, useState, useCallback, useDeferredValue, useLayoutEffect, type RefObject } from 'react';
-import { useNotifications } from '../context/notifications.js';
-import { sendNotification } from '../services/notifier.js';
-import { startPreventSleep, stopPreventSleep } from '../services/preventSleep.js';
-import { useTerminalNotification } from '../ink/useTerminalNotification.js';
-import { hasCursorUpViewportYankBug } from '../ink/terminal.js';
-import { createFileStateCacheWithSizeLimit, mergeFileStateCaches, READ_FILE_STATE_CACHE_SIZE } from '../utils/fileStateCache.js';
-import { updateLastInteractionTime, getLastInteractionTime, getOriginalCwd, getProjectRoot, getSessionId, switchSession, setCostStateForRestore, getTurnHookDurationMs, getTurnHookCount, resetTurnHookDuration, getTurnToolDurationMs, getTurnToolCount, resetTurnToolDuration, getTurnClassifierDurationMs, getTurnClassifierCount, resetTurnClassifierDuration } from '../bootstrap/state.js';
-import { asSessionId, asAgentId } from '../types/ids.js';
-import { logForDebugging } from '../utils/debug.js';
-import { QueryGuard } from '../utils/QueryGuard.js';
-import { isEnvTruthy } from '../utils/envUtils.js';
-import { formatTokens, truncateToWidth } from '../utils/format.js';
-import { consumeEarlyInput } from '../utils/earlyInput.js';
-import { setMemberActive } from '../utils/swarm/teamHelpers.js';
-import { isSwarmWorker, generateSandboxRequestId, sendSandboxPermissionRequestViaMailbox, sendSandboxPermissionResponseViaMailbox } from '../utils/swarm/permissionSync.js';
-import { registerSandboxPermissionCallback } from '../hooks/useSwarmPermissionPoller.js';
-import { getTeamName, getAgentName } from '../utils/teammate.js';
-import { WorkerPendingPermission } from '../components/permissions/WorkerPendingPermission.js';
-import { injectUserMessageToTeammate, getAllInProcessTeammateTasks } from '../tasks/InProcessTeammateTask/InProcessTeammateTask.js';
-import { isLocalAgentTask, queuePendingMessage, appendMessageToLocalAgent, type LocalAgentTaskState } from '../tasks/LocalAgentTask/LocalAgentTask.js';
-import { registerLeaderToolUseConfirmQueue, unregisterLeaderToolUseConfirmQueue, registerLeaderSetToolPermissionContext, unregisterLeaderSetToolPermissionContext } from '../utils/swarm/leaderPermissionBridge.js';
-import { endInteractionSpan } from '../utils/telemetry/sessionTracing.js';
-import { useLogMessages } from '../hooks/useLogMessages.js';
-import { useReplBridge } from '../hooks/useReplBridge.js';
-import { type Command, type CommandResultDisplay, type ResumeEntrypoint, getCommandName, isCommandEnabled } from '../commands.js';
-import type { PromptInputMode, QueuedCommand, VimMode } from '../types/textInputTypes.js';
-import { MessageSelector, selectableUserMessagesFilter, messagesAfterAreOnlySynthetic } from '../components/MessageSelector.js';
-import { useIdeLogging } from '../hooks/useIdeLogging.js';
-import { PermissionRequest, type ToolUseConfirm } from '../components/permissions/PermissionRequest.js';
-import { ElicitationDialog } from '../components/mcp/ElicitationDialog.js';
-import { PromptDialog } from '../components/hooks/PromptDialog.js';
-import type { PromptRequest, PromptResponse } from '../types/hooks.js';
-import PromptInput from '../components/PromptInput/PromptInput.js';
-import { PromptInputQueuedCommands } from '../components/PromptInput/PromptInputQueuedCommands.js';
-import { useRemoteSession } from '../hooks/useRemoteSession.js';
-import { useDirectConnect } from '../hooks/useDirectConnect.js';
-import type { DirectConnectConfig } from '../server/directConnectManager.js';
-import { useSSHSession } from '../hooks/useSSHSession.js';
-import { useAssistantHistory } from '../hooks/useAssistantHistory.js';
-import type { SSHSession } from '../ssh/createSSHSession.js';
-import { SkillImprovementSurvey } from '../components/SkillImprovementSurvey.js';
-import { useSkillImprovementSurvey } from '../hooks/useSkillImprovementSurvey.js';
-import { useMoreRight } from '../moreright/useMoreRight.js';
-import { SpinnerWithVerb, BriefIdleStatus, type SpinnerMode } from '../components/Spinner.js';
-import { getSystemPrompt } from '../constants/prompts.js';
-import { buildEffectiveSystemPrompt } from '../utils/systemPrompt.js';
-import { getSystemContext, getUserContext } from '../context.js';
-import { getMemoryFiles } from '../utils/claudemd.js';
-import { startBackgroundHousekeeping } from '../utils/backgroundHousekeeping.js';
-import { getTotalCost, saveCurrentSessionCosts, resetCostState, getStoredSessionCosts } from '../cost-tracker.js';
-import { useCostSummary } from '../costHook.js';
-import { useFpsMetrics } from '../context/fpsMetrics.js';
-import { useAfterFirstRender } from '../hooks/useAfterFirstRender.js';
-import { useDeferredHookMessages } from '../hooks/useDeferredHookMessages.js';
-import { addToHistory, removeLastFromHistory, expandPastedTextRefs, parseReferences } from '../history.js';
-import { prependModeCharacterToInput } from '../components/PromptInput/inputModes.js';
-import { prependToShellHistoryCache } from '../utils/suggestions/shellHistoryCompletion.js';
-import { useApiKeyVerification } from '../hooks/useApiKeyVerification.js';
-import { GlobalKeybindingHandlers } from '../hooks/useGlobalKeybindings.js';
-import { CommandKeybindingHandlers } from '../hooks/useCommandKeybindings.js';
-import { KeybindingSetup } from '../keybindings/KeybindingProviderSetup.js';
-import { useShortcutDisplay } from '../keybindings/useShortcutDisplay.js';
-import { getShortcutDisplay } from '../keybindings/shortcutFormat.js';
-import { CancelRequestHandler } from '../hooks/useCancelRequest.js';
-import { useBackgroundTaskNavigation } from '../hooks/useBackgroundTaskNavigation.js';
-import { useSwarmInitialization } from '../hooks/useSwarmInitialization.js';
-import { useTeammateViewAutoExit } from '../hooks/useTeammateViewAutoExit.js';
-import { errorMessage } from '../utils/errors.js';
-import { isHumanTurn } from '../utils/messagePredicates.js';
-import { logError } from '../utils/log.js';
+import { useInput } from '../ink.js'
+import { useSearchInput } from '../hooks/useSearchInput.js'
+import { useTerminalSize } from '../hooks/useTerminalSize.js'
+import { useSearchHighlight } from '../ink/hooks/use-search-highlight.js'
+import type { JumpHandle } from '../components/VirtualMessageList.js'
+import { renderMessagesToPlainText } from '../utils/exportRenderer.js'
+import { openFileInExternalEditor } from '../utils/editor.js'
+import { writeFile } from 'fs/promises'
+import {
+ Box,
+ Text,
+ useStdin,
+ useTheme,
+ useTerminalFocus,
+ useTerminalTitle,
+ useTabStatus,
+} from '../ink.js'
+import type { TabStatusKind } from '../ink/hooks/use-tab-status.js'
+import { CostThresholdDialog } from '../components/CostThresholdDialog.js'
+import { IdleReturnDialog } from '../components/IdleReturnDialog.js'
+import * as React from 'react'
+import {
+ useEffect,
+ useMemo,
+ useRef,
+ useState,
+ useCallback,
+ useDeferredValue,
+ useLayoutEffect,
+ type RefObject,
+} from 'react'
+import { useNotifications } from '../context/notifications.js'
+import { sendNotification } from '../services/notifier.js'
+import {
+ startPreventSleep,
+ stopPreventSleep,
+} from '../services/preventSleep.js'
+import { useTerminalNotification } from '../ink/useTerminalNotification.js'
+import { hasCursorUpViewportYankBug } from '../ink/terminal.js'
+import {
+ createFileStateCacheWithSizeLimit,
+ mergeFileStateCaches,
+ READ_FILE_STATE_CACHE_SIZE,
+} from '../utils/fileStateCache.js'
+import {
+ updateLastInteractionTime,
+ getLastInteractionTime,
+ getOriginalCwd,
+ getProjectRoot,
+ getSessionId,
+ switchSession,
+ setCostStateForRestore,
+ getTurnHookDurationMs,
+ getTurnHookCount,
+ resetTurnHookDuration,
+ getTurnToolDurationMs,
+ getTurnToolCount,
+ resetTurnToolDuration,
+ getTurnClassifierDurationMs,
+ getTurnClassifierCount,
+ resetTurnClassifierDuration,
+} from '../bootstrap/state.js'
+import { asSessionId, asAgentId } from '../types/ids.js'
+import { logForDebugging } from '../utils/debug.js'
+import { QueryGuard } from '../utils/QueryGuard.js'
+import { isEnvTruthy } from '../utils/envUtils.js'
+import { formatTokens, truncateToWidth } from '../utils/format.js'
+import { consumeEarlyInput } from '../utils/earlyInput.js'
+
+import { setMemberActive } from '../utils/swarm/teamHelpers.js'
+import {
+ isSwarmWorker,
+ generateSandboxRequestId,
+ sendSandboxPermissionRequestViaMailbox,
+ sendSandboxPermissionResponseViaMailbox,
+} from '../utils/swarm/permissionSync.js'
+import { registerSandboxPermissionCallback } from '../hooks/useSwarmPermissionPoller.js'
+import { getTeamName, getAgentName } from '../utils/teammate.js'
+import { WorkerPendingPermission } from '../components/permissions/WorkerPendingPermission.js'
+import {
+ injectUserMessageToTeammate,
+ getAllInProcessTeammateTasks,
+} from '../tasks/InProcessTeammateTask/InProcessTeammateTask.js'
+import {
+ isLocalAgentTask,
+ queuePendingMessage,
+ appendMessageToLocalAgent,
+ type LocalAgentTaskState,
+} from '../tasks/LocalAgentTask/LocalAgentTask.js'
+import {
+ registerLeaderToolUseConfirmQueue,
+ unregisterLeaderToolUseConfirmQueue,
+ registerLeaderSetToolPermissionContext,
+ unregisterLeaderSetToolPermissionContext,
+} from '../utils/swarm/leaderPermissionBridge.js'
+import { endInteractionSpan } from '../utils/telemetry/sessionTracing.js'
+import { useLogMessages } from '../hooks/useLogMessages.js'
+import { useReplBridge } from '../hooks/useReplBridge.js'
+import {
+ type Command,
+ type CommandResultDisplay,
+ type ResumeEntrypoint,
+ getCommandName,
+ isCommandEnabled,
+} from '../commands.js'
+import type {
+ PromptInputMode,
+ QueuedCommand,
+ VimMode,
+} from '../types/textInputTypes.js'
+import {
+ MessageSelector,
+ selectableUserMessagesFilter,
+ messagesAfterAreOnlySynthetic,
+} from '../components/MessageSelector.js'
+import { useIdeLogging } from '../hooks/useIdeLogging.js'
+import {
+ PermissionRequest,
+ type ToolUseConfirm,
+} from '../components/permissions/PermissionRequest.js'
+import { ElicitationDialog } from '../components/mcp/ElicitationDialog.js'
+import { PromptDialog } from '../components/hooks/PromptDialog.js'
+import type { PromptRequest, PromptResponse } from '../types/hooks.js'
+import PromptInput from '../components/PromptInput/PromptInput.js'
+import { PromptInputQueuedCommands } from '../components/PromptInput/PromptInputQueuedCommands.js'
+import { useRemoteSession } from '../hooks/useRemoteSession.js'
+import { useDirectConnect } from '../hooks/useDirectConnect.js'
+import type { DirectConnectConfig } from '../server/directConnectManager.js'
+import { useSSHSession } from '../hooks/useSSHSession.js'
+import { useAssistantHistory } from '../hooks/useAssistantHistory.js'
+import type { SSHSession } from '../ssh/createSSHSession.js'
+import { SkillImprovementSurvey } from '../components/SkillImprovementSurvey.js'
+import { useSkillImprovementSurvey } from '../hooks/useSkillImprovementSurvey.js'
+import { useMoreRight } from '../moreright/useMoreRight.js'
+import {
+ SpinnerWithVerb,
+ BriefIdleStatus,
+ type SpinnerMode,
+} from '../components/Spinner.js'
+import { getSystemPrompt } from '../constants/prompts.js'
+import { buildEffectiveSystemPrompt } from '../utils/systemPrompt.js'
+import { getSystemContext, getUserContext } from '../context.js'
+import { getMemoryFiles } from '../utils/claudemd.js'
+import { startBackgroundHousekeeping } from '../utils/backgroundHousekeeping.js'
+import {
+ getTotalCost,
+ saveCurrentSessionCosts,
+ resetCostState,
+ getStoredSessionCosts,
+} from '../cost-tracker.js'
+import { useCostSummary } from '../costHook.js'
+import { useFpsMetrics } from '../context/fpsMetrics.js'
+import { useAfterFirstRender } from '../hooks/useAfterFirstRender.js'
+import { useDeferredHookMessages } from '../hooks/useDeferredHookMessages.js'
+import {
+ addToHistory,
+ removeLastFromHistory,
+ expandPastedTextRefs,
+ parseReferences,
+} from '../history.js'
+import { prependModeCharacterToInput } from '../components/PromptInput/inputModes.js'
+import { prependToShellHistoryCache } from '../utils/suggestions/shellHistoryCompletion.js'
+import { useApiKeyVerification } from '../hooks/useApiKeyVerification.js'
+import { GlobalKeybindingHandlers } from '../hooks/useGlobalKeybindings.js'
+import { CommandKeybindingHandlers } from '../hooks/useCommandKeybindings.js'
+import { KeybindingSetup } from '../keybindings/KeybindingProviderSetup.js'
+import { useShortcutDisplay } from '../keybindings/useShortcutDisplay.js'
+import { getShortcutDisplay } from '../keybindings/shortcutFormat.js'
+import { CancelRequestHandler } from '../hooks/useCancelRequest.js'
+import { useBackgroundTaskNavigation } from '../hooks/useBackgroundTaskNavigation.js'
+import { useSwarmInitialization } from '../hooks/useSwarmInitialization.js'
+import { useTeammateViewAutoExit } from '../hooks/useTeammateViewAutoExit.js'
+import { errorMessage } from '../utils/errors.js'
+import { isHumanTurn } from '../utils/messagePredicates.js'
+import { logError } from '../utils/log.js'
// Dead code elimination: conditional imports
/* eslint-disable custom-rules/no-process-env-top-level, @typescript-eslint/no-require-imports */
-const useVoiceIntegration: typeof import('../hooks/useVoiceIntegration.js').useVoiceIntegration = feature('VOICE_MODE') ? require('../hooks/useVoiceIntegration.js').useVoiceIntegration : () => ({
- stripTrailing: () => 0,
- handleKeyEvent: () => {},
- resetAnchor: () => {}
-});
-const VoiceKeybindingHandler: typeof import('../hooks/useVoiceIntegration.js').VoiceKeybindingHandler = feature('VOICE_MODE') ? require('../hooks/useVoiceIntegration.js').VoiceKeybindingHandler : () => null;
+const useVoiceIntegration: typeof import('../hooks/useVoiceIntegration.js').useVoiceIntegration =
+ feature('VOICE_MODE')
+ ? require('../hooks/useVoiceIntegration.js').useVoiceIntegration
+ : () => ({
+ stripTrailing: () => 0,
+ handleKeyEvent: () => {},
+ resetAnchor: () => {},
+ })
+const VoiceKeybindingHandler: typeof import('../hooks/useVoiceIntegration.js').VoiceKeybindingHandler =
+ feature('VOICE_MODE')
+ ? require('../hooks/useVoiceIntegration.js').VoiceKeybindingHandler
+ : () => null
// Frustration detection is ant-only (dogfooding). Conditional require so external
// builds eliminate the module entirely (including its two O(n) useMemos that run
// on every messages change, plus the GrowthBook fetch).
-const useFrustrationDetection: typeof import('../components/FeedbackSurvey/useFrustrationDetection.js').useFrustrationDetection = "external" === 'ant' ? require('../components/FeedbackSurvey/useFrustrationDetection.js').useFrustrationDetection : () => ({
- state: 'closed',
- handleTranscriptSelect: () => {}
-});
+const useFrustrationDetection: typeof import('../components/FeedbackSurvey/useFrustrationDetection.js').useFrustrationDetection =
+ "external" === 'ant'
+ ? require('../components/FeedbackSurvey/useFrustrationDetection.js')
+ .useFrustrationDetection
+ : () => ({ state: 'closed', handleTranscriptSelect: () => {} })
// Ant-only org warning. Conditional require so the org UUID list is
// eliminated from external builds (one UUID is on excluded-strings).
-const useAntOrgWarningNotification: typeof import('../hooks/notifs/useAntOrgWarningNotification.js').useAntOrgWarningNotification = "external" === 'ant' ? require('../hooks/notifs/useAntOrgWarningNotification.js').useAntOrgWarningNotification : () => {};
+const useAntOrgWarningNotification: typeof import('../hooks/notifs/useAntOrgWarningNotification.js').useAntOrgWarningNotification =
+ "external" === 'ant'
+ ? require('../hooks/notifs/useAntOrgWarningNotification.js')
+ .useAntOrgWarningNotification
+ : () => {}
// Dead code elimination: conditional import for coordinator mode
-const getCoordinatorUserContext: (mcpClients: ReadonlyArray<{
- name: string;
-}>, scratchpadDir?: string) => {
- [k: string]: string;
-} = feature('COORDINATOR_MODE') ? require('../coordinator/coordinatorMode.js').getCoordinatorUserContext : () => ({});
+const getCoordinatorUserContext: (
+ mcpClients: ReadonlyArray<{ name: string }>,
+ scratchpadDir?: string,
+) => { [k: string]: string } = feature('COORDINATOR_MODE')
+ ? require('../coordinator/coordinatorMode.js').getCoordinatorUserContext
+ : () => ({})
/* eslint-enable custom-rules/no-process-env-top-level, @typescript-eslint/no-require-imports */
-import useCanUseTool from '../hooks/useCanUseTool.js';
-import type { ToolPermissionContext, Tool } from '../Tool.js';
-import { applyPermissionUpdate, applyPermissionUpdates, persistPermissionUpdate } from '../utils/permissions/PermissionUpdate.js';
-import { buildPermissionUpdates } from '../components/permissions/ExitPlanModePermissionRequest/ExitPlanModePermissionRequest.js';
-import { stripDangerousPermissionsForAutoMode } from '../utils/permissions/permissionSetup.js';
-import { getScratchpadDir, isScratchpadEnabled } from '../utils/permissions/filesystem.js';
-import { WEB_FETCH_TOOL_NAME } from '../tools/WebFetchTool/prompt.js';
-import { SLEEP_TOOL_NAME } from '../tools/SleepTool/prompt.js';
-import { clearSpeculativeChecks } from '../tools/BashTool/bashPermissions.js';
-import type { AutoUpdaterResult } from '../utils/autoUpdater.js';
-import { getGlobalConfig, saveGlobalConfig, getGlobalConfigWriteCount } from '../utils/config.js';
-import { hasConsoleBillingAccess } from '../utils/billing.js';
-import { logEvent, type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS } from 'src/services/analytics/index.js';
-import { getFeatureValue_CACHED_MAY_BE_STALE } from 'src/services/analytics/growthbook.js';
-import { textForResubmit, handleMessageFromStream, type StreamingToolUse, type StreamingThinking, isCompactBoundaryMessage, getMessagesAfterCompactBoundary, getContentText, createUserMessage, createAssistantMessage, createTurnDurationMessage, createAgentsKilledMessage, createApiMetricsMessage, createSystemMessage, createCommandInputMessage, formatCommandInputTags } from '../utils/messages.js';
-import { generateSessionTitle } from '../utils/sessionTitle.js';
-import { BASH_INPUT_TAG, COMMAND_MESSAGE_TAG, COMMAND_NAME_TAG, LOCAL_COMMAND_STDOUT_TAG } from '../constants/xml.js';
-import { escapeXml } from '../utils/xml.js';
-import type { ThinkingConfig } from '../utils/thinking.js';
-import { gracefulShutdownSync } from '../utils/gracefulShutdown.js';
-import { handlePromptSubmit, type PromptInputHelpers } from '../utils/handlePromptSubmit.js';
-import { useQueueProcessor } from '../hooks/useQueueProcessor.js';
-import { useMailboxBridge } from '../hooks/useMailboxBridge.js';
-import { useTelegramBridge } from '../hooks/useTelegramBridge.js';
-import { queryCheckpoint, logQueryProfileReport } from '../utils/queryProfiler.js';
-import type { Message as MessageType, UserMessage, ProgressMessage, HookResultMessage, PartialCompactDirection } from '../types/message.js';
-import { query } from '../query.js';
-import { mergeClients, useMergedClients } from '../hooks/useMergedClients.js';
-import { getQuerySourceForREPL } from '../utils/promptCategory.js';
-import { useMergedTools } from '../hooks/useMergedTools.js';
-import { mergeAndFilterTools } from '../utils/toolPool.js';
-import { useMergedCommands } from '../hooks/useMergedCommands.js';
-import { useSkillsChange } from '../hooks/useSkillsChange.js';
-import { useManagePlugins } from '../hooks/useManagePlugins.js';
-import { Messages } from '../components/Messages.js';
-import { TaskListV2 } from '../components/TaskListV2.js';
-import { TeammateViewHeader } from '../components/TeammateViewHeader.js';
-import { useTasksV2WithCollapseEffect } from '../hooks/useTasksV2.js';
-import { maybeMarkProjectOnboardingComplete } from '../projectOnboardingState.js';
-import type { MCPServerConnection } from '../services/mcp/types.js';
-import type { ScopedMcpServerConfig } from '../services/mcp/types.js';
-import { randomUUID, type UUID } from 'crypto';
-import { processSessionStartHooks } from '../utils/sessionStart.js';
-import { executeSessionEndHooks, getSessionEndHookTimeoutMs } from '../utils/hooks.js';
-import { type IDESelection, useIdeSelection } from '../hooks/useIdeSelection.js';
-import { getTools, assembleToolPool } from '../tools.js';
-import type { AgentDefinition } from '../tools/AgentTool/loadAgentsDir.js';
-import { resolveAgentTools } from '../tools/AgentTool/agentToolUtils.js';
-import { resumeAgentBackground } from '../tools/AgentTool/resumeAgent.js';
-import { useMainLoopModel } from '../hooks/useMainLoopModel.js';
-import { useAppState, useSetAppState, useAppStateStore } from '../state/AppState.js';
-import type { ContentBlockParam, ImageBlockParam } from '@anthropic-ai/sdk/resources/messages.mjs';
-import type { ProcessUserInputContext } from '../utils/processUserInput/processUserInput.js';
-import type { PastedContent } from '../utils/config.js';
-import { copyPlanForFork, copyPlanForResume, getPlanSlug, setPlanSlug } from '../utils/plans.js';
-import { clearSessionMetadata, resetSessionFilePointer, adoptResumedSessionFile, removeTranscriptMessage, restoreSessionMetadata, getCurrentSessionTitle, isEphemeralToolProgress, isLoggableMessage, saveWorktreeState, getAgentTranscript } from '../utils/sessionStorage.js';
-import { deserializeMessages } from '../utils/conversationRecovery.js';
-import { extractReadFilesFromMessages, extractBashToolsFromMessages } from '../utils/queryHelpers.js';
-import { resetMicrocompactState } from '../services/compact/microCompact.js';
-import { runPostCompactCleanup } from '../services/compact/postCompactCleanup.js';
-import { provisionContentReplacementState, reconstructContentReplacementState, type ContentReplacementRecord } from '../utils/toolResultStorage.js';
-import { partialCompactConversation } from '../services/compact/compact.js';
-import type { LogOption } from '../types/logs.js';
-import type { AgentColorName } from '../tools/AgentTool/agentColorManager.js';
-import { fileHistoryMakeSnapshot, type FileHistoryState, fileHistoryRewind, type FileHistorySnapshot, copyFileHistoryForResume, fileHistoryEnabled, fileHistoryHasAnyChanges } from '../utils/fileHistory.js';
-import { type AttributionState, incrementPromptCount } from '../utils/commitAttribution.js';
-import { recordAttributionSnapshot } from '../utils/sessionStorage.js';
-import { computeStandaloneAgentContext, restoreAgentFromSession, restoreSessionStateFromLog, restoreWorktreeForResume, exitRestoredWorktree } from '../utils/sessionRestore.js';
-import { isBgSession, updateSessionName, updateSessionActivity } from '../utils/concurrentSessions.js';
-import { isInProcessTeammateTask, type InProcessTeammateTaskState } from '../tasks/InProcessTeammateTask/types.js';
-import { restoreRemoteAgentTasks } from '../tasks/RemoteAgentTask/RemoteAgentTask.js';
-import { useInboxPoller } from '../hooks/useInboxPoller.js';
+import useCanUseTool from '../hooks/useCanUseTool.js'
+import type { ToolPermissionContext, Tool } from '../Tool.js'
+import {
+ applyPermissionUpdate,
+ applyPermissionUpdates,
+ persistPermissionUpdate,
+} from '../utils/permissions/PermissionUpdate.js'
+import { buildPermissionUpdates } from '../components/permissions/ExitPlanModePermissionRequest/ExitPlanModePermissionRequest.js'
+import { stripDangerousPermissionsForAutoMode } from '../utils/permissions/permissionSetup.js'
+import {
+ getScratchpadDir,
+ isScratchpadEnabled,
+} from '../utils/permissions/filesystem.js'
+import { WEB_FETCH_TOOL_NAME } from '../tools/WebFetchTool/prompt.js'
+import { SLEEP_TOOL_NAME } from '../tools/SleepTool/prompt.js'
+import { clearSpeculativeChecks } from '../tools/BashTool/bashPermissions.js'
+import type { AutoUpdaterResult } from '../utils/autoUpdater.js'
+import {
+ getGlobalConfig,
+ saveGlobalConfig,
+ getGlobalConfigWriteCount,
+} from '../utils/config.js'
+import { hasConsoleBillingAccess } from '../utils/billing.js'
+import {
+ logEvent,
+ type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
+} from 'src/services/analytics/index.js'
+import { getFeatureValue_CACHED_MAY_BE_STALE } from 'src/services/analytics/growthbook.js'
+import {
+ textForResubmit,
+ handleMessageFromStream,
+ type StreamingToolUse,
+ type StreamingThinking,
+ isCompactBoundaryMessage,
+ getMessagesAfterCompactBoundary,
+ getContentText,
+ createUserMessage,
+ createAssistantMessage,
+ createTurnDurationMessage,
+ createAgentsKilledMessage,
+ createApiMetricsMessage,
+ createSystemMessage,
+ createCommandInputMessage,
+ formatCommandInputTags,
+} from '../utils/messages.js'
+import { generateSessionTitle } from '../utils/sessionTitle.js'
+import {
+ BASH_INPUT_TAG,
+ COMMAND_MESSAGE_TAG,
+ COMMAND_NAME_TAG,
+ LOCAL_COMMAND_STDOUT_TAG,
+} from '../constants/xml.js'
+import { escapeXml } from '../utils/xml.js'
+import type { ThinkingConfig } from '../utils/thinking.js'
+import { gracefulShutdownSync } from '../utils/gracefulShutdown.js'
+import {
+ handlePromptSubmit,
+ type PromptInputHelpers,
+} from '../utils/handlePromptSubmit.js'
+import { useQueueProcessor } from '../hooks/useQueueProcessor.js'
+import { useMailboxBridge } from '../hooks/useMailboxBridge.js'
+import {
+ queryCheckpoint,
+ logQueryProfileReport,
+} from '../utils/queryProfiler.js'
+import type {
+ Message as MessageType,
+ UserMessage,
+ ProgressMessage,
+ HookResultMessage,
+ PartialCompactDirection,
+} from '../types/message.js'
+import { query } from '../query.js'
+import { mergeClients, useMergedClients } from '../hooks/useMergedClients.js'
+import { getQuerySourceForREPL } from '../utils/promptCategory.js'
+import { useMergedTools } from '../hooks/useMergedTools.js'
+import { mergeAndFilterTools } from '../utils/toolPool.js'
+import { useMergedCommands } from '../hooks/useMergedCommands.js'
+import { useSkillsChange } from '../hooks/useSkillsChange.js'
+import { useManagePlugins } from '../hooks/useManagePlugins.js'
+import { Messages } from '../components/Messages.js'
+import { TaskListV2 } from '../components/TaskListV2.js'
+import { TeammateViewHeader } from '../components/TeammateViewHeader.js'
+import { useTasksV2WithCollapseEffect } from '../hooks/useTasksV2.js'
+import { maybeMarkProjectOnboardingComplete } from '../projectOnboardingState.js'
+import type { MCPServerConnection } from '../services/mcp/types.js'
+import type { ScopedMcpServerConfig } from '../services/mcp/types.js'
+import { randomUUID, type UUID } from 'crypto'
+import { processSessionStartHooks } from '../utils/sessionStart.js'
+import {
+ executeSessionEndHooks,
+ getSessionEndHookTimeoutMs,
+} from '../utils/hooks.js'
+import { type IDESelection, useIdeSelection } from '../hooks/useIdeSelection.js'
+import { getTools, assembleToolPool } from '../tools.js'
+import type { AgentDefinition } from '../tools/AgentTool/loadAgentsDir.js'
+import { resolveAgentTools } from '../tools/AgentTool/agentToolUtils.js'
+import { resumeAgentBackground } from '../tools/AgentTool/resumeAgent.js'
+import { useMainLoopModel } from '../hooks/useMainLoopModel.js'
+import {
+ useAppState,
+ useSetAppState,
+ useAppStateStore,
+} from '../state/AppState.js'
+import type {
+ ContentBlockParam,
+ ImageBlockParam,
+} from '@anthropic-ai/sdk/resources/messages.mjs'
+import type { ProcessUserInputContext } from '../utils/processUserInput/processUserInput.js'
+import type { PastedContent } from '../utils/config.js'
+import {
+ copyPlanForFork,
+ copyPlanForResume,
+ getPlanSlug,
+ setPlanSlug,
+} from '../utils/plans.js'
+import {
+ clearSessionMetadata,
+ resetSessionFilePointer,
+ adoptResumedSessionFile,
+ removeTranscriptMessage,
+ restoreSessionMetadata,
+ getCurrentSessionTitle,
+ isEphemeralToolProgress,
+ isLoggableMessage,
+ saveWorktreeState,
+ getAgentTranscript,
+} from '../utils/sessionStorage.js'
+import { deserializeMessages } from '../utils/conversationRecovery.js'
+import {
+ extractReadFilesFromMessages,
+ extractBashToolsFromMessages,
+} from '../utils/queryHelpers.js'
+import { resetMicrocompactState } from '../services/compact/microCompact.js'
+import { runPostCompactCleanup } from '../services/compact/postCompactCleanup.js'
+import {
+ provisionContentReplacementState,
+ reconstructContentReplacementState,
+ type ContentReplacementRecord,
+} from '../utils/toolResultStorage.js'
+import { partialCompactConversation } from '../services/compact/compact.js'
+import type { LogOption } from '../types/logs.js'
+import type { AgentColorName } from '../tools/AgentTool/agentColorManager.js'
+import {
+ fileHistoryMakeSnapshot,
+ type FileHistoryState,
+ fileHistoryRewind,
+ type FileHistorySnapshot,
+ copyFileHistoryForResume,
+ fileHistoryEnabled,
+ fileHistoryHasAnyChanges,
+} from '../utils/fileHistory.js'
+import {
+ type AttributionState,
+ incrementPromptCount,
+} from '../utils/commitAttribution.js'
+import { recordAttributionSnapshot } from '../utils/sessionStorage.js'
+import {
+ computeStandaloneAgentContext,
+ restoreAgentFromSession,
+ restoreSessionStateFromLog,
+ restoreWorktreeForResume,
+ exitRestoredWorktree,
+} from '../utils/sessionRestore.js'
+import {
+ isBgSession,
+ updateSessionName,
+ updateSessionActivity,
+} from '../utils/concurrentSessions.js'
+import {
+ isInProcessTeammateTask,
+ type InProcessTeammateTaskState,
+} from '../tasks/InProcessTeammateTask/types.js'
+import { restoreRemoteAgentTasks } from '../tasks/RemoteAgentTask/RemoteAgentTask.js'
+import { useInboxPoller } from '../hooks/useInboxPoller.js'
// Dead code elimination: conditional import for loop mode
/* eslint-disable @typescript-eslint/no-require-imports */
-const proactiveModule = feature('PROACTIVE') || feature('KAIROS') ? require('../proactive/index.js') : null;
-const PROACTIVE_NO_OP_SUBSCRIBE = (_cb: () => void) => () => {};
-const PROACTIVE_FALSE = () => false;
-const SUGGEST_BG_PR_NOOP = (_p: string, _n: string): boolean => false;
-const useProactive = feature('PROACTIVE') || feature('KAIROS') ? require('../proactive/useProactive.js').useProactive : null;
-const useScheduledTasks = feature('AGENT_TRIGGERS') ? require('../hooks/useScheduledTasks.js').useScheduledTasks : null;
+const proactiveModule =
+ feature('PROACTIVE') || feature('KAIROS')
+ ? require('../proactive/index.js')
+ : null
+const PROACTIVE_NO_OP_SUBSCRIBE = (_cb: () => void) => () => {}
+const PROACTIVE_FALSE = () => false
+const SUGGEST_BG_PR_NOOP = (_p: string, _n: string): boolean => false
+const useProactive =
+ feature('PROACTIVE') || feature('KAIROS')
+ ? require('../proactive/useProactive.js').useProactive
+ : null
+const useScheduledTasks = feature('AGENT_TRIGGERS')
+ ? require('../hooks/useScheduledTasks.js').useScheduledTasks
+ : null
/* eslint-enable @typescript-eslint/no-require-imports */
-import { isAgentSwarmsEnabled } from '../utils/agentSwarmsEnabled.js';
-import { useTaskListWatcher } from '../hooks/useTaskListWatcher.js';
-import type { SandboxAskCallback, NetworkHostPattern } from '../utils/sandbox/sandbox-adapter.js';
-import { type IDEExtensionInstallationStatus, closeOpenDiffs, getConnectedIdeClient, type IdeType } from '../utils/ide.js';
-import { useIDEIntegration } from '../hooks/useIDEIntegration.js';
-import exit from '../commands/exit/index.js';
-import { ExitFlow } from '../components/ExitFlow.js';
-import { getCurrentWorktreeSession } from '../utils/worktree.js';
-import { popAllEditable, enqueue, type SetAppState, getCommandQueue, getCommandQueueLength, removeByFilter } from '../utils/messageQueueManager.js';
-import { useCommandQueue } from '../hooks/useCommandQueue.js';
-import { SessionBackgroundHint } from '../components/SessionBackgroundHint.js';
-import { startBackgroundSession } from '../tasks/LocalMainSessionTask.js';
-import { useSessionBackgrounding } from '../hooks/useSessionBackgrounding.js';
-import { diagnosticTracker } from '../services/diagnosticTracking.js';
-import { handleSpeculationAccept, type ActiveSpeculationState } from '../services/PromptSuggestion/speculation.js';
-import { IdeOnboardingDialog } from '../components/IdeOnboardingDialog.js';
-import { EffortCallout, shouldShowEffortCallout } from '../components/EffortCallout.js';
-import type { EffortValue } from '../utils/effort.js';
-import { RemoteCallout } from '../components/RemoteCallout.js';
+import { isAgentSwarmsEnabled } from '../utils/agentSwarmsEnabled.js'
+import { useTaskListWatcher } from '../hooks/useTaskListWatcher.js'
+import type {
+ SandboxAskCallback,
+ NetworkHostPattern,
+} from '../utils/sandbox/sandbox-adapter.js'
+
+import {
+ type IDEExtensionInstallationStatus,
+ closeOpenDiffs,
+ getConnectedIdeClient,
+ type IdeType,
+} from '../utils/ide.js'
+import { useIDEIntegration } from '../hooks/useIDEIntegration.js'
+import exit from '../commands/exit/index.js'
+import { ExitFlow } from '../components/ExitFlow.js'
+import { getCurrentWorktreeSession } from '../utils/worktree.js'
+import {
+ popAllEditable,
+ enqueue,
+ type SetAppState,
+ getCommandQueue,
+ getCommandQueueLength,
+ removeByFilter,
+} from '../utils/messageQueueManager.js'
+import { useCommandQueue } from '../hooks/useCommandQueue.js'
+import { SessionBackgroundHint } from '../components/SessionBackgroundHint.js'
+import { startBackgroundSession } from '../tasks/LocalMainSessionTask.js'
+import { useSessionBackgrounding } from '../hooks/useSessionBackgrounding.js'
+import { diagnosticTracker } from '../services/diagnosticTracking.js'
+import {
+ handleSpeculationAccept,
+ type ActiveSpeculationState,
+} from '../services/PromptSuggestion/speculation.js'
+import { IdeOnboardingDialog } from '../components/IdeOnboardingDialog.js'
+import {
+ EffortCallout,
+ shouldShowEffortCallout,
+} from '../components/EffortCallout.js'
+import type { EffortValue } from '../utils/effort.js'
+import { RemoteCallout } from '../components/RemoteCallout.js'
/* eslint-disable custom-rules/no-process-env-top-level, @typescript-eslint/no-require-imports */
-const AntModelSwitchCallout = "external" === 'ant' ? require('../components/AntModelSwitchCallout.js').AntModelSwitchCallout : null;
-const shouldShowAntModelSwitch = "external" === 'ant' ? require('../components/AntModelSwitchCallout.js').shouldShowModelSwitchCallout : (): boolean => false;
-const UndercoverAutoCallout = "external" === 'ant' ? require('../components/UndercoverAutoCallout.js').UndercoverAutoCallout : null;
+const AntModelSwitchCallout =
+ "external" === 'ant'
+ ? require('../components/AntModelSwitchCallout.js').AntModelSwitchCallout
+ : null
+const shouldShowAntModelSwitch =
+ "external" === 'ant'
+ ? require('../components/AntModelSwitchCallout.js')
+ .shouldShowModelSwitchCallout
+ : (): boolean => false
+const UndercoverAutoCallout =
+ "external" === 'ant'
+ ? require('../components/UndercoverAutoCallout.js').UndercoverAutoCallout
+ : null
/* eslint-enable custom-rules/no-process-env-top-level, @typescript-eslint/no-require-imports */
-import { activityManager } from '../utils/activityManager.js';
-import { createAbortController } from '../utils/abortController.js';
-import { MCPConnectionManager } from 'src/services/mcp/MCPConnectionManager.js';
-import { useFeedbackSurvey } from 'src/components/FeedbackSurvey/useFeedbackSurvey.js';
-import { useMemorySurvey } from 'src/components/FeedbackSurvey/useMemorySurvey.js';
-import { usePostCompactSurvey } from 'src/components/FeedbackSurvey/usePostCompactSurvey.js';
-import { FeedbackSurvey } from 'src/components/FeedbackSurvey/FeedbackSurvey.js';
-import { useInstallMessages } from 'src/hooks/notifs/useInstallMessages.js';
-import { useAwaySummary } from 'src/hooks/useAwaySummary.js';
-import { useChromeExtensionNotification } from 'src/hooks/useChromeExtensionNotification.js';
-import { useOfficialMarketplaceNotification } from 'src/hooks/useOfficialMarketplaceNotification.js';
-import { usePromptsFromClaudeInChrome } from 'src/hooks/usePromptsFromClaudeInChrome.js';
-import { getTipToShowOnSpinner, recordShownTip } from 'src/services/tips/tipScheduler.js';
-import type { Theme } from 'src/utils/theme.js';
-import { checkAndDisableBypassPermissionsIfNeeded, checkAndDisableAutoModeIfNeeded, useKickOffCheckAndDisableBypassPermissionsIfNeeded, useKickOffCheckAndDisableAutoModeIfNeeded } from 'src/utils/permissions/bypassPermissionsKillswitch.js';
-import { SandboxManager } from 'src/utils/sandbox/sandbox-adapter.js';
-import { SANDBOX_NETWORK_ACCESS_TOOL_NAME } from 'src/cli/structuredIO.js';
-import { useFileHistorySnapshotInit } from 'src/hooks/useFileHistorySnapshotInit.js';
-import { SandboxPermissionRequest } from 'src/components/permissions/SandboxPermissionRequest.js';
-import { SandboxViolationExpandedView } from 'src/components/SandboxViolationExpandedView.js';
-import { useSettingsErrors } from 'src/hooks/notifs/useSettingsErrors.js';
-import { useMcpConnectivityStatus } from 'src/hooks/notifs/useMcpConnectivityStatus.js';
-import { useAutoModeUnavailableNotification } from 'src/hooks/notifs/useAutoModeUnavailableNotification.js';
-import { AUTO_MODE_DESCRIPTION } from 'src/components/AutoModeOptInDialog.js';
-import { useLspInitializationNotification } from 'src/hooks/notifs/useLspInitializationNotification.js';
-import { useLspPluginRecommendation } from 'src/hooks/useLspPluginRecommendation.js';
-import { LspRecommendationMenu } from 'src/components/LspRecommendation/LspRecommendationMenu.js';
-import { useClaudeCodeHintRecommendation } from 'src/hooks/useClaudeCodeHintRecommendation.js';
-import { PluginHintMenu } from 'src/components/ClaudeCodeHint/PluginHintMenu.js';
-import { DesktopUpsellStartup, shouldShowDesktopUpsellStartup } from 'src/components/DesktopUpsell/DesktopUpsellStartup.js';
-import { usePluginInstallationStatus } from 'src/hooks/notifs/usePluginInstallationStatus.js';
-import { usePluginAutoupdateNotification } from 'src/hooks/notifs/usePluginAutoupdateNotification.js';
-import { performStartupChecks } from 'src/utils/plugins/performStartupChecks.js';
-import { UserTextMessage } from 'src/components/messages/UserTextMessage.js';
-import { AwsAuthStatusBox } from '../components/AwsAuthStatusBox.js';
-import { useRateLimitWarningNotification } from 'src/hooks/notifs/useRateLimitWarningNotification.js';
-import { useDeprecationWarningNotification } from 'src/hooks/notifs/useDeprecationWarningNotification.js';
-import { useNpmDeprecationNotification } from 'src/hooks/notifs/useNpmDeprecationNotification.js';
-import { useIDEStatusIndicator } from 'src/hooks/notifs/useIDEStatusIndicator.js';
-import { useModelMigrationNotifications } from 'src/hooks/notifs/useModelMigrationNotifications.js';
-import { useCanSwitchToExistingSubscription } from 'src/hooks/notifs/useCanSwitchToExistingSubscription.js';
-import { useTeammateLifecycleNotification } from 'src/hooks/notifs/useTeammateShutdownNotification.js';
-import { useFastModeNotification } from 'src/hooks/notifs/useFastModeNotification.js';
-import { AutoRunIssueNotification, shouldAutoRunIssue, getAutoRunIssueReasonText, getAutoRunCommand, type AutoRunIssueReason } from '../utils/autoRunIssue.js';
-import type { HookProgress } from '../types/hooks.js';
-import { TungstenLiveMonitor } from '../tools/TungstenTool/TungstenLiveMonitor.js';
+import { activityManager } from '../utils/activityManager.js'
+import { createAbortController } from '../utils/abortController.js'
+import { MCPConnectionManager } from 'src/services/mcp/MCPConnectionManager.js'
+import { useFeedbackSurvey } from 'src/components/FeedbackSurvey/useFeedbackSurvey.js'
+import { useMemorySurvey } from 'src/components/FeedbackSurvey/useMemorySurvey.js'
+import { usePostCompactSurvey } from 'src/components/FeedbackSurvey/usePostCompactSurvey.js'
+import { FeedbackSurvey } from 'src/components/FeedbackSurvey/FeedbackSurvey.js'
+import { useInstallMessages } from 'src/hooks/notifs/useInstallMessages.js'
+import { useAwaySummary } from 'src/hooks/useAwaySummary.js'
+import { useChromeExtensionNotification } from 'src/hooks/useChromeExtensionNotification.js'
+import { useOfficialMarketplaceNotification } from 'src/hooks/useOfficialMarketplaceNotification.js'
+import { usePromptsFromClaudeInChrome } from 'src/hooks/usePromptsFromClaudeInChrome.js'
+import {
+ getTipToShowOnSpinner,
+ recordShownTip,
+} from 'src/services/tips/tipScheduler.js'
+import type { Theme } from 'src/utils/theme.js'
+import {
+ checkAndDisableBypassPermissionsIfNeeded,
+ checkAndDisableAutoModeIfNeeded,
+ useKickOffCheckAndDisableBypassPermissionsIfNeeded,
+ useKickOffCheckAndDisableAutoModeIfNeeded,
+} from 'src/utils/permissions/bypassPermissionsKillswitch.js'
+import { SandboxManager } from 'src/utils/sandbox/sandbox-adapter.js'
+import { SANDBOX_NETWORK_ACCESS_TOOL_NAME } from 'src/cli/structuredIO.js'
+import { useFileHistorySnapshotInit } from 'src/hooks/useFileHistorySnapshotInit.js'
+import { SandboxPermissionRequest } from 'src/components/permissions/SandboxPermissionRequest.js'
+import { SandboxViolationExpandedView } from 'src/components/SandboxViolationExpandedView.js'
+import { useSettingsErrors } from 'src/hooks/notifs/useSettingsErrors.js'
+import { useMcpConnectivityStatus } from 'src/hooks/notifs/useMcpConnectivityStatus.js'
+import { useAutoModeUnavailableNotification } from 'src/hooks/notifs/useAutoModeUnavailableNotification.js'
+import { AUTO_MODE_DESCRIPTION } from 'src/components/AutoModeOptInDialog.js'
+import { useLspInitializationNotification } from 'src/hooks/notifs/useLspInitializationNotification.js'
+import { useLspPluginRecommendation } from 'src/hooks/useLspPluginRecommendation.js'
+import { LspRecommendationMenu } from 'src/components/LspRecommendation/LspRecommendationMenu.js'
+import { useClaudeCodeHintRecommendation } from 'src/hooks/useClaudeCodeHintRecommendation.js'
+import { PluginHintMenu } from 'src/components/ClaudeCodeHint/PluginHintMenu.js'
+import {
+ DesktopUpsellStartup,
+ shouldShowDesktopUpsellStartup,
+} from 'src/components/DesktopUpsell/DesktopUpsellStartup.js'
+import { usePluginInstallationStatus } from 'src/hooks/notifs/usePluginInstallationStatus.js'
+import { usePluginAutoupdateNotification } from 'src/hooks/notifs/usePluginAutoupdateNotification.js'
+import { performStartupChecks } from 'src/utils/plugins/performStartupChecks.js'
+import { UserTextMessage } from 'src/components/messages/UserTextMessage.js'
+import { AwsAuthStatusBox } from '../components/AwsAuthStatusBox.js'
+import { useRateLimitWarningNotification } from 'src/hooks/notifs/useRateLimitWarningNotification.js'
+import { useDeprecationWarningNotification } from 'src/hooks/notifs/useDeprecationWarningNotification.js'
+import { useNpmDeprecationNotification } from 'src/hooks/notifs/useNpmDeprecationNotification.js'
+import { useIDEStatusIndicator } from 'src/hooks/notifs/useIDEStatusIndicator.js'
+import { useModelMigrationNotifications } from 'src/hooks/notifs/useModelMigrationNotifications.js'
+import { useCanSwitchToExistingSubscription } from 'src/hooks/notifs/useCanSwitchToExistingSubscription.js'
+import { useTeammateLifecycleNotification } from 'src/hooks/notifs/useTeammateShutdownNotification.js'
+import { useFastModeNotification } from 'src/hooks/notifs/useFastModeNotification.js'
+import {
+ AutoRunIssueNotification,
+ shouldAutoRunIssue,
+ getAutoRunIssueReasonText,
+ getAutoRunCommand,
+ type AutoRunIssueReason,
+} from '../utils/autoRunIssue.js'
+import type { HookProgress } from '../types/hooks.js'
+import { TungstenLiveMonitor } from '../tools/TungstenTool/TungstenLiveMonitor.js'
/* eslint-disable @typescript-eslint/no-require-imports */
-const WebBrowserPanelModule = feature('WEB_BROWSER_TOOL') ? require('../tools/WebBrowserTool/WebBrowserPanel.js') as typeof import('../tools/WebBrowserTool/WebBrowserPanel.js') : null;
-const UltraplanLaunchDialog: typeof import('../components/UltraplanLaunchDialog.js').UltraplanLaunchDialog = feature('ULTRAPLAN') ? require('../components/UltraplanLaunchDialog.js').UltraplanLaunchDialog : () => null;
-const UltraplanChoiceDialog: typeof import('../components/UltraplanChoiceDialog.js').UltraplanChoiceDialog = feature('ULTRAPLAN') ? require('../components/UltraplanChoiceDialog.js').UltraplanChoiceDialog : () => null;
-const launchUltraplan: typeof import('../commands/ultraplan.js').launchUltraplan = feature('ULTRAPLAN') ? require('../commands/ultraplan.js').launchUltraplan : () => Promise.resolve('');
+const WebBrowserPanelModule = feature('WEB_BROWSER_TOOL')
+ ? (require('../tools/WebBrowserTool/WebBrowserPanel.js') as typeof import('../tools/WebBrowserTool/WebBrowserPanel.js'))
+ : null
/* eslint-enable @typescript-eslint/no-require-imports */
-import { IssueFlagBanner } from '../components/PromptInput/IssueFlagBanner.js';
-import { useIssueFlagBanner } from '../hooks/useIssueFlagBanner.js';
-import { triggerCompanionReaction } from '../buddy/companionReact.js';
-import { CompanionSprite, CompanionFloatingBubble, MIN_COLS_FOR_FULL_SPRITE } from '../buddy/CompanionSprite.js';
-import { DevBar } from '../components/DevBar.js';
+import { IssueFlagBanner } from '../components/PromptInput/IssueFlagBanner.js'
+import { useIssueFlagBanner } from '../hooks/useIssueFlagBanner.js'
+import {
+ CompanionSprite,
+ CompanionFloatingBubble,
+ MIN_COLS_FOR_FULL_SPRITE,
+} from '../buddy/CompanionSprite.js'
+import { DevBar } from '../components/DevBar.js'
// Session manager removed - using AppState now
-import type { RemoteSessionConfig } from '../remote/RemoteSessionManager.js';
-import { REMOTE_SAFE_COMMANDS } from '../commands.js';
-import type { RemoteMessageContent } from '../utils/teleport/api.js';
-import { FullscreenLayout, useUnseenDivider, computeUnseenDivider } from '../components/FullscreenLayout.js';
-import { isFullscreenEnvEnabled, maybeGetTmuxMouseHint, isMouseTrackingEnabled } from '../utils/fullscreen.js';
-import { AlternateScreen } from '../ink/components/AlternateScreen.js';
-import { ScrollKeybindingHandler } from '../components/ScrollKeybindingHandler.js';
-import { useMessageActions, MessageActionsKeybindings, MessageActionsBar, type MessageActionsState, type MessageActionsNav, type MessageActionCaps } from '../components/messageActions.js';
-import { setClipboard } from '../ink/termio/osc.js';
-import type { ScrollBoxHandle } from '../ink/components/ScrollBox.js';
-import { createAttachmentMessage, getQueuedCommandAttachments } from '../utils/attachments.js';
+import type { RemoteSessionConfig } from '../remote/RemoteSessionManager.js'
+import { REMOTE_SAFE_COMMANDS } from '../commands.js'
+import type { RemoteMessageContent } from '../utils/teleport/api.js'
+import {
+ FullscreenLayout,
+ useUnseenDivider,
+ computeUnseenDivider,
+} from '../components/FullscreenLayout.js'
+import {
+ isFullscreenEnvEnabled,
+ maybeGetTmuxMouseHint,
+ isMouseTrackingEnabled,
+} from '../utils/fullscreen.js'
+import { AlternateScreen } from '../ink/components/AlternateScreen.js'
+import { ScrollKeybindingHandler } from '../components/ScrollKeybindingHandler.js'
+import {
+ useMessageActions,
+ MessageActionsKeybindings,
+ MessageActionsBar,
+ type MessageActionsState,
+ type MessageActionsNav,
+ type MessageActionCaps,
+} from '../components/messageActions.js'
+import { setClipboard } from '../ink/termio/osc.js'
+import type { ScrollBoxHandle } from '../ink/components/ScrollBox.js'
+import {
+ createAttachmentMessage,
+ getQueuedCommandAttachments,
+} from '../utils/attachments.js'
// Stable empty array for hooks that accept MCPServerConnection[] — avoids
// creating a new [] literal on every render in remote mode, which would
// cause useEffect dependency changes and infinite re-render loops.
-const EMPTY_MCP_CLIENTS: MCPServerConnection[] = [];
+const EMPTY_MCP_CLIENTS: MCPServerConnection[] = []
// Stable stub for useAssistantHistory's non-KAIROS branch — avoids a new
// function identity each render, which would break composedOnScroll's memo.
-const HISTORY_STUB = {
- maybeLoadOlder: (_: ScrollBoxHandle) => {}
-};
+const HISTORY_STUB = { maybeLoadOlder: (_: ScrollBoxHandle) => {} }
// Window after a user-initiated scroll during which type-into-empty does NOT
// repin to bottom. Josh Rosen's workflow: Claude emits long output → scroll
// up to read the start → start typing → before this fix, snapped to bottom.
// https://anthropic.slack.com/archives/C07VBSHV7EV/p1773545449871739
-const RECENT_SCROLL_REPIN_WINDOW_MS = 3000;
+const RECENT_SCROLL_REPIN_WINDOW_MS = 3000
// Use LRU cache to prevent unbounded memory growth
// 100 files should be sufficient for most coding sessions while preventing
// memory issues when working across many files in large projects
function median(values: number[]): number {
- const sorted = [...values].sort((a, b) => a - b);
- const mid = Math.floor(sorted.length / 2);
- return sorted.length % 2 === 0 ? Math.round((sorted[mid - 1]! + sorted[mid]!) / 2) : sorted[mid]!;
+ const sorted = [...values].sort((a, b) => a - b)
+ const mid = Math.floor(sorted.length / 2)
+ return sorted.length % 2 === 0
+ ? Math.round((sorted[mid - 1]! + sorted[mid]!) / 2)
+ : sorted[mid]!
}
/**
* Small component to display transcript mode footer with dynamic keybinding.
* Must be rendered inside KeybindingSetup to access keybinding context.
*/
-function TranscriptModeFooter(t0) {
- const $ = _c(9);
- const {
- showAllInTranscript,
- virtualScroll,
- searchBadge,
- suppressShowAll: t1,
- status
- } = t0;
- const suppressShowAll = t1 === undefined ? false : t1;
- const toggleShortcut = useShortcutDisplay("app:toggleTranscript", "Global", "ctrl+o");
- const showAllShortcut = useShortcutDisplay("transcript:toggleShowAll", "Transcript", "ctrl+e");
- const t2 = searchBadge ? " \xB7 n/N to navigate" : virtualScroll ? ` · ${figures.arrowUp}${figures.arrowDown} scroll · home/end top/bottom` : suppressShowAll ? "" : ` · ${showAllShortcut} to ${showAllInTranscript ? "collapse" : "show all"}`;
- let t3;
- if ($[0] !== t2 || $[1] !== toggleShortcut) {
- t3 = Showing detailed transcript · {toggleShortcut} to toggle{t2};
- $[0] = t2;
- $[1] = toggleShortcut;
- $[2] = t3;
- } else {
- t3 = $[2];
- }
- let t4;
- if ($[3] !== searchBadge || $[4] !== status) {
- t4 = status ? <>{status} > : searchBadge ? <>{searchBadge.current}/{searchBadge.count}{" "}> : null;
- $[3] = searchBadge;
- $[4] = status;
- $[5] = t4;
- } else {
- t4 = $[5];
- }
- let t5;
- if ($[6] !== t3 || $[7] !== t4) {
- t5 = {t3}{t4};
- $[6] = t3;
- $[7] = t4;
- $[8] = t5;
- } else {
- t5 = $[8];
- }
- return t5;
+function TranscriptModeFooter({
+ showAllInTranscript,
+ virtualScroll,
+ searchBadge,
+ suppressShowAll = false,
+ status,
+}: {
+ showAllInTranscript: boolean
+ virtualScroll: boolean
+ /** Minimap while navigating a closed-bar search. Shows n/N hints +
+ * right-aligned count instead of scroll hints. */
+ searchBadge?: { current: number; count: number }
+ /** Hide the ctrl+e hint. The [ dump path shares this footer with
+ * env-opted dump (CLAUDE_CODE_NO_FLICKER=0 / DISABLE_VIRTUAL_SCROLL=1),
+ * but ctrl+e only works in the env case — useGlobalKeybindings.tsx
+ * gates on !virtualScrollActive which is env-derived, doesn't know
+ * [ happened. */
+ suppressShowAll?: boolean
+ /** Transient status (v-for-editor progress). Notifications render inside
+ * PromptInput which isn't mounted in transcript — addNotification queues
+ * but nothing draws it. */
+ status?: string
+}): React.ReactNode {
+ const toggleShortcut = useShortcutDisplay(
+ 'app:toggleTranscript',
+ 'Global',
+ 'ctrl+o',
+ )
+ const showAllShortcut = useShortcutDisplay(
+ 'transcript:toggleShowAll',
+ 'Transcript',
+ 'ctrl+e',
+ )
+ return (
+
+
+ Showing detailed transcript · {toggleShortcut} to toggle
+ {searchBadge
+ ? ' · n/N to navigate'
+ : virtualScroll
+ ? ` · ${figures.arrowUp}${figures.arrowDown} scroll · home/end top/bottom`
+ : suppressShowAll
+ ? ''
+ : ` · ${showAllShortcut} to ${showAllInTranscript ? 'collapse' : 'show all'}`}
+
+ {status ? (
+ // v-for-editor render progress — transient, preempts the search
+ // badge since the user just pressed v and wants to see what's
+ // happening. Clears after 4s.
+ <>
+
+ {status}
+ >
+ ) : searchBadge ? (
+ // Engine-counted — close enough for a rough location hint. May
+ // drift from render-count for ghost/phantom messages.
+ <>
+
+
+ {searchBadge.current}/{searchBadge.count}
+ {' '}
+
+ >
+ ) : null}
+
+ )
}
/** less-style / bar. 1-row, same border-top styling as TranscriptModeFooter
@@ -377,30 +702,27 @@ function TranscriptSearchBar({
onClose,
onCancel,
setHighlight,
- initialQuery
+ initialQuery,
}: {
- jumpRef: RefObject;
- count: number;
- current: number;
+ jumpRef: RefObject
+ count: number
+ current: number
/** Enter — commit. Query persists for n/N. */
- onClose: (lastQuery: string) => void;
+ onClose: (lastQuery: string) => void
/** Esc/ctrl+c/ctrl+g — undo to pre-/ state. */
- onCancel: () => void;
- setHighlight: (query: string) => void;
+ onCancel: () => void
+ setHighlight: (query: string) => void
// Seed with the previous query (less: / shows last pattern). Mount-fire
// of the effect re-scans with the same query — idempotent (same matches,
// nearest-ptr, same highlights). User can edit or clear.
- initialQuery: string;
+ initialQuery: string
}): React.ReactNode {
- const {
- query,
- cursorOffset
- } = useSearchInput({
+ const { query, cursorOffset } = useSearchInput({
isActive: true,
initialQuery,
onExit: () => onClose(query),
- onCancel
- });
+ onCancel,
+ })
// Index warm-up runs before the query effect so it measures the real
// cost — otherwise setSearchQuery fills the cache first and warm
// reports ~0ms while the user felt the actual lag.
@@ -412,72 +734,89 @@ function TranscriptSearchBar({
// null initial, warmDone would be true on mount → [query] fires →
// setSearchQuery fills cache → warm reports ~0ms while the user felt
// the real lag.
- const [indexStatus, setIndexStatus] = React.useState<'building' | {
- ms: number;
- } | null>('building');
+ const [indexStatus, setIndexStatus] = React.useState<
+ 'building' | { ms: number } | null
+ >('building')
React.useEffect(() => {
- let alive = true;
- const warm = jumpRef.current?.warmSearchIndex;
+ let alive = true
+ const warm = jumpRef.current?.warmSearchIndex
if (!warm) {
- setIndexStatus(null); // VML not mounted yet — rare, skip indicator
- return;
+ setIndexStatus(null) // VML not mounted yet — rare, skip indicator
+ return
}
- setIndexStatus('building');
+ setIndexStatus('building')
warm().then(ms => {
- if (!alive) return;
+ if (!alive) return
// <20ms = imperceptible. No point showing "indexed in 3ms".
if (ms < 20) {
- setIndexStatus(null);
+ setIndexStatus(null)
} else {
- setIndexStatus({
- ms
- });
- setTimeout(() => alive && setIndexStatus(null), 2000);
+ setIndexStatus({ ms })
+ setTimeout(() => alive && setIndexStatus(null), 2000)
}
- });
+ })
return () => {
- alive = false;
- };
+ alive = false
+ }
// eslint-disable-next-line react-hooks/exhaustive-deps
- }, []); // mount-only: bar opens once per /
+ }, []) // mount-only: bar opens once per /
// Gate the query effect on warm completion. setHighlight stays instant
// (screen-space overlay, no indexing). setSearchQuery (the scan) waits.
- const warmDone = indexStatus !== 'building';
+ const warmDone = indexStatus !== 'building'
useEffect(() => {
- if (!warmDone) return;
- jumpRef.current?.setSearchQuery(query);
- setHighlight(query);
+ if (!warmDone) return
+ jumpRef.current?.setSearchQuery(query)
+ setHighlight(query)
// eslint-disable-next-line react-hooks/exhaustive-deps
- }, [query, warmDone]);
- const off = cursorOffset;
- const cursorChar = off < query.length ? query[off] : ' ';
- return
+ }, [query, warmDone])
+ const off = cursorOffset
+ const cursorChar = off < query.length ? query[off] : ' '
+ return (
+
/
{query.slice(0, off)}
{cursorChar}
{off < query.length && {query.slice(off + 1)}}
- {indexStatus === 'building' ? indexing… : indexStatus ? indexed in {indexStatus.ms}ms : count === 0 && query ? no matches : count > 0 ?
- // Engine-counted (indexOf on extractSearchText). May drift from
- // render-count for ghost/phantom messages — badge is a rough
- // location hint. scanElement gives exact per-message positions
- // but counting ALL would cost ~1-3ms × matched-messages.
-
+ {indexStatus === 'building' ? (
+ indexing…
+ ) : indexStatus ? (
+ indexed in {indexStatus.ms}ms
+ ) : count === 0 && query ? (
+ no matches
+ ) : count > 0 ? (
+ // Engine-counted (indexOf on extractSearchText). May drift from
+ // render-count for ghost/phantom messages — badge is a rough
+ // location hint. scanElement gives exact per-message positions
+ // but counting ALL would cost ~1-3ms × matched-messages.
+
{current}/{count}
{' '}
- : null}
- ;
+
+ ) : null}
+
+ )
}
-const TITLE_ANIMATION_FRAMES = ['⠂', '⠐'];
-const TITLE_STATIC_PREFIX = '✳';
-const TITLE_ANIMATION_INTERVAL_MS = 960;
+
+const TITLE_ANIMATION_FRAMES = ['⠂', '⠐']
+const TITLE_STATIC_PREFIX = '✳'
+const TITLE_ANIMATION_INTERVAL_MS = 960
/**
* Sets the terminal tab title, with an animated prefix glyph while a query
@@ -486,94 +825,86 @@ const TITLE_ANIMATION_INTERVAL_MS = 960;
* entire REPL tree. Before extraction, the tick was ~1 REPL render/sec for
* the duration of every turn, dragging PromptInput and friends along.
*/
-function AnimatedTerminalTitle(t0) {
- const $ = _c(6);
- const {
- isAnimating,
- title,
- disabled,
- noPrefix
- } = t0;
- const terminalFocused = useTerminalFocus();
- const [frame, setFrame] = useState(0);
- let t1;
- let t2;
- if ($[0] !== disabled || $[1] !== isAnimating || $[2] !== noPrefix || $[3] !== terminalFocused) {
- t1 = () => {
- if (disabled || noPrefix || !isAnimating || !terminalFocused) {
- return;
- }
- const interval = setInterval(_temp2, TITLE_ANIMATION_INTERVAL_MS, setFrame);
- return () => clearInterval(interval);
- };
- t2 = [disabled, noPrefix, isAnimating, terminalFocused];
- $[0] = disabled;
- $[1] = isAnimating;
- $[2] = noPrefix;
- $[3] = terminalFocused;
- $[4] = t1;
- $[5] = t2;
- } else {
- t1 = $[4];
- t2 = $[5];
- }
- useEffect(t1, t2);
- const prefix = isAnimating ? TITLE_ANIMATION_FRAMES[frame] ?? TITLE_STATIC_PREFIX : TITLE_STATIC_PREFIX;
- useTerminalTitle(disabled ? null : noPrefix ? title : `${prefix} ${title}`);
- return null;
-}
-function _temp2(setFrame_0) {
- return setFrame_0(_temp);
-}
-function _temp(f) {
- return (f + 1) % TITLE_ANIMATION_FRAMES.length;
+function AnimatedTerminalTitle({
+ isAnimating,
+ title,
+ disabled,
+ noPrefix,
+}: {
+ isAnimating: boolean
+ title: string
+ disabled: boolean
+ noPrefix: boolean
+}): null {
+ const terminalFocused = useTerminalFocus()
+ const [frame, setFrame] = useState(0)
+ useEffect(() => {
+ if (disabled || noPrefix || !isAnimating || !terminalFocused) return
+ const interval = setInterval(
+ setFrame => setFrame(f => (f + 1) % TITLE_ANIMATION_FRAMES.length),
+ TITLE_ANIMATION_INTERVAL_MS,
+ setFrame,
+ )
+ return () => clearInterval(interval)
+ }, [disabled, noPrefix, isAnimating, terminalFocused])
+ const prefix = isAnimating
+ ? (TITLE_ANIMATION_FRAMES[frame] ?? TITLE_STATIC_PREFIX)
+ : TITLE_STATIC_PREFIX
+ useTerminalTitle(disabled ? null : noPrefix ? title : `${prefix} ${title}`)
+ return null
}
+
export type Props = {
- commands: Command[];
- debug: boolean;
- initialTools: Tool[];
+ commands: Command[]
+ debug: boolean
+ initialTools: Tool[]
// Initial messages to populate the REPL with
- initialMessages?: MessageType[];
+ initialMessages?: MessageType[]
// Deferred hook messages promise — REPL renders immediately and injects
// hook messages when they resolve. Awaited before the first API call.
- pendingHookMessages?: Promise;
- initialFileHistorySnapshots?: FileHistorySnapshot[];
+ pendingHookMessages?: Promise
+ initialFileHistorySnapshots?: FileHistorySnapshot[]
// Content-replacement records from a resumed session's transcript — used to
// reconstruct contentReplacementState so the same results are re-replaced
- initialContentReplacements?: ContentReplacementRecord[];
+ initialContentReplacements?: ContentReplacementRecord[]
// Initial agent context for session resume (name/color set via /rename or /color)
- initialAgentName?: string;
- initialAgentColor?: AgentColorName;
- mcpClients?: MCPServerConnection[];
- dynamicMcpConfig?: Record;
- autoConnectIdeFlag?: boolean;
- strictMcpConfig?: boolean;
- systemPrompt?: string;
- appendSystemPrompt?: string;
+ initialAgentName?: string
+ initialAgentColor?: AgentColorName
+ mcpClients?: MCPServerConnection[]
+ dynamicMcpConfig?: Record
+ autoConnectIdeFlag?: boolean
+ strictMcpConfig?: boolean
+ systemPrompt?: string
+ appendSystemPrompt?: string
// Optional callback invoked before query execution
// Called after user message is added to conversation but before API call
// Return false to prevent query execution
- onBeforeQuery?: (input: string, newMessages: MessageType[]) => Promise;
+ onBeforeQuery?: (
+ input: string,
+ newMessages: MessageType[],
+ ) => Promise
// Optional callback when a turn completes (model finishes responding)
- onTurnComplete?: (messages: MessageType[]) => void | Promise;
+ onTurnComplete?: (messages: MessageType[]) => void | Promise
// When true, disables REPL input (hides prompt and prevents message selector)
- disabled?: boolean;
+ disabled?: boolean
// Optional agent definition to use for the main thread
- mainThreadAgentDefinition?: AgentDefinition;
+ mainThreadAgentDefinition?: AgentDefinition
// When true, disables all slash commands
- disableSlashCommands?: boolean;
+ disableSlashCommands?: boolean
// Task list id: when set, enables tasks mode that watches a task list and auto-processes tasks.
- taskListId?: string;
+ taskListId?: string
// Remote session config for --remote mode (uses CCR as execution engine)
- remoteSessionConfig?: RemoteSessionConfig;
+ remoteSessionConfig?: RemoteSessionConfig
// Direct connect config for `claude connect` mode (connects to a claude server)
- directConnectConfig?: DirectConnectConfig;
+ directConnectConfig?: DirectConnectConfig
// SSH session for `claude ssh` mode (local REPL, remote tools over ssh)
- sshSession?: SSHSession;
+ sshSession?: SSHSession
// Thinking configuration to use when thinking is enabled
- thinkingConfig: ThinkingConfig;
-};
-export type Screen = 'prompt' | 'transcript';
+ thinkingConfig: ThinkingConfig
+}
+
+export type Screen = 'prompt' | 'transcript'
+
export function REPL({
commands: initialCommands,
debug,
@@ -599,67 +930,92 @@ export function REPL({
remoteSessionConfig,
directConnectConfig,
sshSession,
- thinkingConfig
+ thinkingConfig,
}: Props): React.ReactNode {
- const isRemoteSession = !!remoteSessionConfig;
+ const isRemoteSession = !!remoteSessionConfig
// Env-var gates hoisted to mount-time — isEnvTruthy does toLowerCase+trim+
// includes, and these were on the render path (hot during PageUp spam).
- const titleDisabled = useMemo(() => isEnvTruthy(process.env.CLAUDE_CODE_DISABLE_TERMINAL_TITLE), []);
- const moreRightEnabled = useMemo(() => "external" === 'ant' && isEnvTruthy(process.env.CLAUDE_MORERIGHT), []);
- const disableVirtualScroll = useMemo(() => isEnvTruthy(process.env.CLAUDE_CODE_DISABLE_VIRTUAL_SCROLL), []);
- const disableMessageActions = feature('MESSAGE_ACTIONS') ?
- // biome-ignore lint/correctness/useHookAtTopLevel: feature() is a compile-time constant
- useMemo(() => isEnvTruthy(process.env.CLAUDE_CODE_DISABLE_MESSAGE_ACTIONS), []) : false;
+ const titleDisabled = useMemo(
+ () => isEnvTruthy(process.env.CLAUDE_CODE_DISABLE_TERMINAL_TITLE),
+ [],
+ )
+ const moreRightEnabled = useMemo(
+ () =>
+ "external" === 'ant' &&
+ isEnvTruthy(process.env.CLAUDE_MORERIGHT),
+ [],
+ )
+ const disableVirtualScroll = useMemo(
+ () => isEnvTruthy(process.env.CLAUDE_CODE_DISABLE_VIRTUAL_SCROLL),
+ [],
+ )
+ const disableMessageActions = feature('MESSAGE_ACTIONS')
+ ? // biome-ignore lint/correctness/useHookAtTopLevel: feature() is a compile-time constant
+ useMemo(
+ () => isEnvTruthy(process.env.CLAUDE_CODE_DISABLE_MESSAGE_ACTIONS),
+ [],
+ )
+ : false
// Log REPL mount/unmount lifecycle
useEffect(() => {
- logForDebugging(`[REPL:mount] REPL mounted, disabled=${disabled}`);
- return () => logForDebugging(`[REPL:unmount] REPL unmounting`);
- }, [disabled]);
+ logForDebugging(`[REPL:mount] REPL mounted, disabled=${disabled}`)
+ return () => logForDebugging(`[REPL:unmount] REPL unmounting`)
+ }, [disabled])
// Agent definition is state so /resume can update it mid-session
- const [mainThreadAgentDefinition, setMainThreadAgentDefinition] = useState(initialMainThreadAgentDefinition);
- const toolPermissionContext = useAppState(s => s.toolPermissionContext);
- const verbose = useAppState(s => s.verbose);
- const mcp = useAppState(s => s.mcp);
- const plugins = useAppState(s => s.plugins);
- const agentDefinitions = useAppState(s => s.agentDefinitions);
- const fileHistory = useAppState(s => s.fileHistory);
- const initialMessage = useAppState(s => s.initialMessage);
- const queuedCommands = useCommandQueue();
+ const [mainThreadAgentDefinition, setMainThreadAgentDefinition] = useState(
+ initialMainThreadAgentDefinition,
+ )
+
+ const toolPermissionContext = useAppState(s => s.toolPermissionContext)
+ const verbose = useAppState(s => s.verbose)
+ const mcp = useAppState(s => s.mcp)
+ const plugins = useAppState(s => s.plugins)
+ const agentDefinitions = useAppState(s => s.agentDefinitions)
+ const fileHistory = useAppState(s => s.fileHistory)
+ const initialMessage = useAppState(s => s.initialMessage)
+ const queuedCommands = useCommandQueue()
// feature() is a build-time constant — dead code elimination removes the hook
// call entirely in external builds, so this is safe despite looking conditional.
// These fields contain excluded strings that must not appear in external builds.
- const spinnerTip = useAppState(s => s.spinnerTip);
- const showExpandedTodos = useAppState(s => s.expandedView) === 'tasks';
- const pendingWorkerRequest = useAppState(s => s.pendingWorkerRequest);
- const pendingSandboxRequest = useAppState(s => s.pendingSandboxRequest);
- const teamContext = useAppState(s => s.teamContext);
- const tasks = useAppState(s => s.tasks);
- const workerSandboxPermissions = useAppState(s => s.workerSandboxPermissions);
- const elicitation = useAppState(s => s.elicitation);
- const ultraplanPendingChoice = useAppState(s => s.ultraplanPendingChoice);
- const ultraplanLaunchPending = useAppState(s => s.ultraplanLaunchPending);
- const viewingAgentTaskId = useAppState(s => s.viewingAgentTaskId);
- const setAppState = useSetAppState();
+ const spinnerTip = useAppState(s => s.spinnerTip)
+ const showExpandedTodos = useAppState(s => s.expandedView) === 'tasks'
+ const pendingWorkerRequest = useAppState(s => s.pendingWorkerRequest)
+ const pendingSandboxRequest = useAppState(s => s.pendingSandboxRequest)
+ const teamContext = useAppState(s => s.teamContext)
+ const tasks = useAppState(s => s.tasks)
+ const workerSandboxPermissions = useAppState(s => s.workerSandboxPermissions)
+ const elicitation = useAppState(s => s.elicitation)
+ const ultraplanPendingChoice = useAppState(s => s.ultraplanPendingChoice)
+ const ultraplanLaunchPending = useAppState(s => s.ultraplanLaunchPending)
+ const viewingAgentTaskId = useAppState(s => s.viewingAgentTaskId)
+ const setAppState = useSetAppState()
// Bootstrap: retained local_agent that hasn't loaded disk yet → read
// sidechain JSONL and UUID-merge with whatever stream has appended so far.
// Stream appends immediately on retain (no defer); bootstrap fills the
// prefix. Disk-write-before-yield means live is always a suffix of disk.
- const viewedLocalAgent = viewingAgentTaskId ? tasks[viewingAgentTaskId] : undefined;
- const needsBootstrap = isLocalAgentTask(viewedLocalAgent) && viewedLocalAgent.retain && !viewedLocalAgent.diskLoaded;
+ const viewedLocalAgent = viewingAgentTaskId
+ ? tasks[viewingAgentTaskId]
+ : undefined
+ const needsBootstrap =
+ isLocalAgentTask(viewedLocalAgent) &&
+ viewedLocalAgent.retain &&
+ !viewedLocalAgent.diskLoaded
useEffect(() => {
- if (!viewingAgentTaskId || !needsBootstrap) return;
- const taskId = viewingAgentTaskId;
+ if (!viewingAgentTaskId || !needsBootstrap) return
+ const taskId = viewingAgentTaskId
void getAgentTranscript(asAgentId(taskId)).then(result => {
setAppState(prev => {
- const t = prev.tasks[taskId];
- if (!isLocalAgentTask(t) || t.diskLoaded || !t.retain) return prev;
- const live = t.messages ?? [];
- const liveUuids = new Set(live.map(m => m.uuid));
- const diskOnly = result ? result.messages.filter(m => !liveUuids.has(m.uuid)) : [];
+ const t = prev.tasks[taskId]
+ if (!isLocalAgentTask(t) || t.diskLoaded || !t.retain) return prev
+ const live = t.messages ?? []
+ const liveUuids = new Set(live.map(m => m.uuid))
+ const diskOnly = result
+ ? result.messages.filter(m => !liveUuids.has(m.uuid))
+ : []
return {
...prev,
tasks: {
@@ -667,29 +1023,36 @@ export function REPL({
[taskId]: {
...t,
messages: [...diskOnly, ...live],
- diskLoaded: true
- }
- }
- };
- });
- });
- }, [viewingAgentTaskId, needsBootstrap, setAppState]);
- const store = useAppStateStore();
- const terminal = useTerminalNotification();
- const mainLoopModel = useMainLoopModel();
+ diskLoaded: true,
+ },
+ },
+ }
+ })
+ })
+ }, [viewingAgentTaskId, needsBootstrap, setAppState])
+
+ const store = useAppStateStore()
+ const terminal = useTerminalNotification()
+ const mainLoopModel = useMainLoopModel()
// Note: standaloneAgentContext is initialized in main.tsx (via initialState) or
// ResumeConversation.tsx (via setAppState before rendering REPL) to avoid
// useEffect-based state initialization on mount (per CLAUDE.md guidelines)
// Local state for commands (hot-reloadable when skill files change)
- const [localCommands, setLocalCommands] = useState(initialCommands);
+ const [localCommands, setLocalCommands] = useState(initialCommands)
// Watch for skill file changes and reload all commands
- useSkillsChange(isRemoteSession ? undefined : getProjectRoot(), setLocalCommands);
+ useSkillsChange(
+ isRemoteSession ? undefined : getProjectRoot(),
+ setLocalCommands,
+ )
// Track proactive mode for tools dependency - SleepTool filters by proactive state
- const proactiveActive = React.useSyncExternalStore(proactiveModule?.subscribeToProactiveChanges ?? PROACTIVE_NO_OP_SUBSCRIBE, proactiveModule?.isProactiveActive ?? PROACTIVE_FALSE);
+ const proactiveActive = React.useSyncExternalStore(
+ proactiveModule?.subscribeToProactiveChanges ?? PROACTIVE_NO_OP_SUBSCRIBE,
+ proactiveModule?.isProactiveActive ?? PROACTIVE_FALSE,
+ )
// BriefTool.isEnabled() reads getUserMsgOptIn() from bootstrap state, which
// /brief flips mid-session alongside isBriefOnly. The memo below needs a
@@ -697,99 +1060,113 @@ export function REPL({
// the AppState mirror that triggers the re-render. Without this, toggling
// /brief mid-session leaves the stale tool list (no SendUserMessage) and
// the model emits plain text the brief filter hides.
- const isBriefOnly = useAppState(s => s.isBriefOnly);
- const localTools = useMemo(() => getTools(toolPermissionContext), [toolPermissionContext, proactiveActive, isBriefOnly]);
- useKickOffCheckAndDisableBypassPermissionsIfNeeded();
- useKickOffCheckAndDisableAutoModeIfNeeded();
- const [dynamicMcpConfig, setDynamicMcpConfig] = useState | undefined>(initialDynamicMcpConfig);
- const onChangeDynamicMcpConfig = useCallback((config: Record) => {
- setDynamicMcpConfig(config);
- }, [setDynamicMcpConfig]);
- const [screen, setScreen] = useState('prompt');
- const [showAllInTranscript, setShowAllInTranscript] = useState(false);
+ const isBriefOnly = useAppState(s => s.isBriefOnly)
+
+ const localTools = useMemo(
+ () => getTools(toolPermissionContext),
+ [toolPermissionContext, proactiveActive, isBriefOnly],
+ )
+
+ useKickOffCheckAndDisableBypassPermissionsIfNeeded()
+ useKickOffCheckAndDisableAutoModeIfNeeded()
+
+ const [dynamicMcpConfig, setDynamicMcpConfig] = useState<
+ Record | undefined
+ >(initialDynamicMcpConfig)
+
+ const onChangeDynamicMcpConfig = useCallback(
+ (config: Record) => {
+ setDynamicMcpConfig(config)
+ },
+ [setDynamicMcpConfig],
+ )
+
+ const [screen, setScreen] = useState('prompt')
+ const [showAllInTranscript, setShowAllInTranscript] = useState(false)
// [ forces the dump-to-scrollback path inside transcript mode. Separate
// from CLAUDE_CODE_NO_FLICKER=0 (which is process-lifetime) — this is
// ephemeral, reset on transcript exit. Diagnostic escape hatch so
// terminal/tmux native cmd-F can search the full flat render.
- const [dumpMode, setDumpMode] = useState(false);
+ const [dumpMode, setDumpMode] = useState(false)
// v-for-editor render progress. Inline in the footer — notifications
// render inside PromptInput which isn't mounted in transcript.
- const [editorStatus, setEditorStatus] = useState('');
+ const [editorStatus, setEditorStatus] = useState('')
// Incremented on transcript exit. Async v-render captures this at start;
// each status write no-ops if stale (user left transcript mid-render —
// the stable setState would otherwise stamp a ghost toast into the next
// session). Also clears any pending 4s auto-clear.
- const editorGenRef = useRef(0);
- const editorTimerRef = useRef | undefined>(undefined);
- const editorRenderingRef = useRef(false);
- const {
- addNotification,
- removeNotification
- } = useNotifications();
+ const editorGenRef = useRef(0)
+ const editorTimerRef = useRef | undefined>(
+ undefined,
+ )
+ const editorRenderingRef = useRef(false)
+ const { addNotification, removeNotification } = useNotifications()
// eslint-disable-next-line prefer-const
- let trySuggestBgPRIntercept = SUGGEST_BG_PR_NOOP;
- const mcpClients = useMergedClients(initialMcpClients, mcp.clients);
+ let trySuggestBgPRIntercept = SUGGEST_BG_PR_NOOP
+
+ const mcpClients = useMergedClients(initialMcpClients, mcp.clients)
// IDE integration
- const [ideSelection, setIDESelection] = useState(undefined);
- const [ideToInstallExtension, setIDEToInstallExtension] = useState(null);
- const [ideInstallationStatus, setIDEInstallationStatus] = useState(null);
- const [showIdeOnboarding, setShowIdeOnboarding] = useState(false);
+ const [ideSelection, setIDESelection] = useState(
+ undefined,
+ )
+ const [ideToInstallExtension, setIDEToInstallExtension] =
+ useState(null)
+ const [ideInstallationStatus, setIDEInstallationStatus] =
+ useState(null)
+ const [showIdeOnboarding, setShowIdeOnboarding] = useState(false)
// Dead code elimination: model switch callout state (ant-only)
const [showModelSwitchCallout, setShowModelSwitchCallout] = useState(() => {
if ("external" === 'ant') {
- return shouldShowAntModelSwitch();
+ return shouldShowAntModelSwitch()
}
- return false;
- });
- const [showEffortCallout, setShowEffortCallout] = useState(() => shouldShowEffortCallout(mainLoopModel));
- const showRemoteCallout = useAppState(s => s.showRemoteCallout);
- const [showDesktopUpsellStartup, setShowDesktopUpsellStartup] = useState(() => shouldShowDesktopUpsellStartup());
+ return false
+ })
+ const [showEffortCallout, setShowEffortCallout] = useState(() =>
+ shouldShowEffortCallout(mainLoopModel),
+ )
+ const showRemoteCallout = useAppState(s => s.showRemoteCallout)
+ const [showDesktopUpsellStartup, setShowDesktopUpsellStartup] = useState(() =>
+ shouldShowDesktopUpsellStartup(),
+ )
// notifications
- useModelMigrationNotifications();
- useCanSwitchToExistingSubscription();
- useIDEStatusIndicator({
- ideSelection,
- mcpClients,
- ideInstallationStatus
- });
- useMcpConnectivityStatus({
- mcpClients
- });
- useAutoModeUnavailableNotification();
- usePluginInstallationStatus();
- usePluginAutoupdateNotification();
- useSettingsErrors();
- useRateLimitWarningNotification(mainLoopModel);
- useFastModeNotification();
- useDeprecationWarningNotification(mainLoopModel);
- useNpmDeprecationNotification();
- useAntOrgWarningNotification();
- useInstallMessages();
- useChromeExtensionNotification();
- useOfficialMarketplaceNotification();
- useLspInitializationNotification();
- useTeammateLifecycleNotification();
+ useModelMigrationNotifications()
+ useCanSwitchToExistingSubscription()
+ useIDEStatusIndicator({ ideSelection, mcpClients, ideInstallationStatus })
+ useMcpConnectivityStatus({ mcpClients })
+ useAutoModeUnavailableNotification()
+ usePluginInstallationStatus()
+ usePluginAutoupdateNotification()
+ useSettingsErrors()
+ useRateLimitWarningNotification(mainLoopModel)
+ useFastModeNotification()
+ useDeprecationWarningNotification(mainLoopModel)
+ useNpmDeprecationNotification()
+ useAntOrgWarningNotification()
+ useInstallMessages()
+ useChromeExtensionNotification()
+ useOfficialMarketplaceNotification()
+ useLspInitializationNotification()
+ useTeammateLifecycleNotification()
const {
recommendation: lspRecommendation,
- handleResponse: handleLspResponse
- } = useLspPluginRecommendation();
+ handleResponse: handleLspResponse,
+ } = useLspPluginRecommendation()
const {
recommendation: hintRecommendation,
- handleResponse: handleHintResponse
- } = useClaudeCodeHintRecommendation();
+ handleResponse: handleHintResponse,
+ } = useClaudeCodeHintRecommendation()
// Memoize the combined initial tools array to prevent reference changes
const combinedInitialTools = useMemo(() => {
- return [...localTools, ...initialTools];
- }, [localTools, initialTools]);
+ return [...localTools, ...initialTools]
+ }, [localTools, initialTools])
// Initialize plugin management
- useManagePlugins({
- enabled: !isRemoteSession
- });
- const tasksV2 = useTasksV2WithCollapseEffect();
+ useManagePlugins({ enabled: !isRemoteSession })
+
+ const tasksV2 = useTasksV2WithCollapseEffect()
// Start background plugin installations
@@ -800,47 +1177,71 @@ export function REPL({
// This ensures that plugin installations from repository and user settings only
// happen after explicit user consent to trust the current working directory.
useEffect(() => {
- if (isRemoteSession) return;
- void performStartupChecks(setAppState);
- }, [setAppState, isRemoteSession]);
+ if (isRemoteSession) return
+ void performStartupChecks(setAppState)
+ }, [setAppState, isRemoteSession])
// Allow Claude in Chrome MCP to send prompts through MCP notifications
// and sync permission mode changes to the Chrome extension
- usePromptsFromClaudeInChrome(isRemoteSession ? EMPTY_MCP_CLIENTS : mcpClients, toolPermissionContext.mode);
+ usePromptsFromClaudeInChrome(
+ isRemoteSession ? EMPTY_MCP_CLIENTS : mcpClients,
+ toolPermissionContext.mode,
+ )
// Initialize swarm features: teammate hooks and context
// Handles both fresh spawns and resumed teammate sessions
useSwarmInitialization(setAppState, initialMessages, {
- enabled: !isRemoteSession
- });
- const mergedTools = useMergedTools(combinedInitialTools, mcp.tools, toolPermissionContext);
+ enabled: !isRemoteSession,
+ })
+
+ const mergedTools = useMergedTools(
+ combinedInitialTools,
+ mcp.tools,
+ toolPermissionContext,
+ )
// Apply agent tool restrictions if mainThreadAgentDefinition is set
- const {
- tools,
- allowedAgentTypes
- } = useMemo(() => {
+ const { tools, allowedAgentTypes } = useMemo(() => {
if (!mainThreadAgentDefinition) {
return {
tools: mergedTools,
- allowedAgentTypes: undefined as string[] | undefined
- };
+ allowedAgentTypes: undefined as string[] | undefined,
+ }
}
- const resolved = resolveAgentTools(mainThreadAgentDefinition, mergedTools, false, true);
+ const resolved = resolveAgentTools(
+ mainThreadAgentDefinition,
+ mergedTools,
+ false,
+ true,
+ )
return {
tools: resolved.resolvedTools,
- allowedAgentTypes: resolved.allowedAgentTypes
- };
- }, [mainThreadAgentDefinition, mergedTools]);
+ allowedAgentTypes: resolved.allowedAgentTypes,
+ }
+ }, [mainThreadAgentDefinition, mergedTools])
// Merge commands from local state, plugins, and MCP
- const commandsWithPlugins = useMergedCommands(localCommands, plugins.commands as Command[]);
- const mergedCommands = useMergedCommands(commandsWithPlugins, mcp.commands as Command[]);
+ const commandsWithPlugins = useMergedCommands(
+ localCommands,
+ plugins.commands as Command[],
+ )
+ const mergedCommands = useMergedCommands(
+ commandsWithPlugins,
+ mcp.commands as Command[],
+ )
// Filter out all commands if disableSlashCommands is true
- const commands = useMemo(() => disableSlashCommands ? [] : mergedCommands, [disableSlashCommands, mergedCommands]);
- useIdeLogging(isRemoteSession ? EMPTY_MCP_CLIENTS : mcp.clients);
- useIdeSelection(isRemoteSession ? EMPTY_MCP_CLIENTS : mcp.clients, setIDESelection);
- const [streamMode, setStreamMode] = useState('responding');
+ const commands = useMemo(
+ () => (disableSlashCommands ? [] : mergedCommands),
+ [disableSlashCommands, mergedCommands],
+ )
+
+ useIdeLogging(isRemoteSession ? EMPTY_MCP_CLIENTS : mcp.clients)
+ useIdeSelection(
+ isRemoteSession ? EMPTY_MCP_CLIENTS : mcp.clients,
+ setIDESelection,
+ )
+
+ const [streamMode, setStreamMode] = useState('responding')
// Ref mirror so onSubmit can read the latest value without adding
// streamMode to its deps. streamMode flips between
// requesting/responding/tool-use ~10x per turn during streaming; having it
@@ -849,99 +1250,115 @@ export function REPL({
// invalidation. The only consumers inside callbacks are debug logging and
// telemetry (handlePromptSubmit.ts), so a stale-by-one-render value is
// harmless — but ref mirrors sync on every render anyway so it's fresh.
- const streamModeRef = useRef(streamMode);
- streamModeRef.current = streamMode;
- const [streamingToolUses, setStreamingToolUses] = useState([]);
- const [streamingThinking, setStreamingThinking] = useState(null);
+ const streamModeRef = useRef(streamMode)
+ streamModeRef.current = streamMode
+ const [streamingToolUses, setStreamingToolUses] = useState<
+ StreamingToolUse[]
+ >([])
+ const [streamingThinking, setStreamingThinking] =
+ useState(null)
// Auto-hide streaming thinking after 30 seconds of being completed
useEffect(() => {
- if (streamingThinking && !streamingThinking.isStreaming && streamingThinking.streamingEndedAt) {
- const elapsed = Date.now() - streamingThinking.streamingEndedAt;
- const remaining = 30000 - elapsed;
+ if (
+ streamingThinking &&
+ !streamingThinking.isStreaming &&
+ streamingThinking.streamingEndedAt
+ ) {
+ const elapsed = Date.now() - streamingThinking.streamingEndedAt
+ const remaining = 30000 - elapsed
if (remaining > 0) {
- const timer = setTimeout(setStreamingThinking, remaining, null);
- return () => clearTimeout(timer);
+ const timer = setTimeout(setStreamingThinking, remaining, null)
+ return () => clearTimeout(timer)
} else {
- setStreamingThinking(null);
+ setStreamingThinking(null)
}
}
- }, [streamingThinking]);
- const [abortController, setAbortController] = useState(null);
+ }, [streamingThinking])
+
+ const [abortController, setAbortController] =
+ useState(null)
// Ref that always points to the current abort controller, used by the
// REPL bridge to abort the active query when a remote interrupt arrives.
- const abortControllerRef = useRef(null);
- abortControllerRef.current = abortController;
+ const abortControllerRef = useRef(null)
+ abortControllerRef.current = abortController
// Ref for the bridge result callback — set after useReplBridge initializes,
// read in the onQuery finally block to notify mobile clients that a turn ended.
- const sendBridgeResultRef = useRef<() => void>(() => {});
+ const sendBridgeResultRef = useRef<() => void>(() => {})
// Ref for the synchronous restore callback — set after restoreMessageSync is
// defined, read in the onQuery finally block for auto-restore on interrupt.
- const restoreMessageSyncRef = useRef<(m: UserMessage) => void>(() => {});
+ const restoreMessageSyncRef = useRef<(m: UserMessage) => void>(() => {})
// Ref to the fullscreen layout's scroll box for keyboard scrolling.
// Null when fullscreen mode is disabled (ref never attached).
- const scrollRef = useRef(null);
+ const scrollRef = useRef(null)
// Separate ref for the modal slot's inner ScrollBox — passed through
// FullscreenLayout → ModalContext so Tabs can attach it to its own
// ScrollBox for tall content (e.g. /status's MCP-server list). NOT
// keyboard-driven — ScrollKeybindingHandler stays on the outer ref so
// PgUp/PgDn/wheel always scroll the transcript behind the modal.
// Plumbing kept for future modal-scroll wiring.
- const modalScrollRef = useRef(null);
+ const modalScrollRef = useRef(null)
// Timestamp of the last user-initiated scroll (wheel, PgUp/PgDn, ctrl+u,
// End/Home, G, drag-to-scroll). Stamped in composedOnScroll — the single
// chokepoint ScrollKeybindingHandler calls for every user scroll action.
// Programmatic scrolls (repinScroll's scrollToBottom, sticky auto-follow)
// do NOT go through composedOnScroll, so they don't stamp this. Ref not
// state: no re-render on every wheel tick.
- const lastUserScrollTsRef = useRef(0);
+ const lastUserScrollTsRef = useRef(0)
// Synchronous state machine for the query lifecycle. Replaces the
// error-prone dual-state pattern where isLoading (React state, async
// batched) and isQueryRunning (ref, sync) could desync. See QueryGuard.ts.
- const queryGuard = React.useRef(new QueryGuard()).current;
+ const queryGuard = React.useRef(new QueryGuard()).current
// Subscribe to the guard — true during dispatching or running.
// This is the single source of truth for "is a local query in flight".
- const isQueryActive = React.useSyncExternalStore(queryGuard.subscribe, queryGuard.getSnapshot);
+ const isQueryActive = React.useSyncExternalStore(
+ queryGuard.subscribe,
+ queryGuard.getSnapshot,
+ )
// Separate loading flag for operations outside the local query guard:
// remote sessions (useRemoteSession / useDirectConnect) and foregrounded
// background tasks (useSessionBackgrounding). These don't route through
// onQuery / queryGuard, so they need their own spinner-visibility state.
// Initialize true if remote mode with initial prompt (CCR processing it).
- const [isExternalLoading, setIsExternalLoadingRaw] = React.useState(remoteSessionConfig?.hasInitialPrompt ?? false);
+ const [isExternalLoading, setIsExternalLoadingRaw] = React.useState(
+ remoteSessionConfig?.hasInitialPrompt ?? false,
+ )
// Derived: any loading source active. Read-only — no setter. Local query
// loading is driven by queryGuard (reserve/tryStart/end/cancelReservation),
// external loading by setIsExternalLoading.
- const isLoading = isQueryActive || isExternalLoading;
+ const isLoading = isQueryActive || isExternalLoading
// Elapsed time is computed by SpinnerWithVerb from these refs on each
// animation frame, avoiding a useInterval that re-renders the entire REPL.
- const [userInputOnProcessing, setUserInputOnProcessingRaw] = React.useState(undefined);
+ const [userInputOnProcessing, setUserInputOnProcessingRaw] = React.useState<
+ string | undefined
+ >(undefined)
// messagesRef.current.length at the moment userInputOnProcessing was set.
// The placeholder hides once displayedMessages grows past this — i.e. the
// real user message has landed in the visible transcript.
- const userInputBaselineRef = React.useRef(0);
+ const userInputBaselineRef = React.useRef(0)
// True while the submitted prompt is being processed but its user message
// hasn't reached setMessages yet. setMessages uses this to keep the
// baseline in sync when unrelated async messages (bridge status, hook
// results, scheduled tasks) land during that window.
- const userMessagePendingRef = React.useRef(false);
+ const userMessagePendingRef = React.useRef(false)
// Wall-clock time tracking refs for accurate elapsed time calculation
- const loadingStartTimeRef = React.useRef(0);
- const totalPausedMsRef = React.useRef(0);
- const pauseStartTimeRef = React.useRef(null);
+ const loadingStartTimeRef = React.useRef(0)
+ const totalPausedMsRef = React.useRef(0)
+ const pauseStartTimeRef = React.useRef(null)
const resetTimingRefs = React.useCallback(() => {
- loadingStartTimeRef.current = Date.now();
- totalPausedMsRef.current = 0;
- pauseStartTimeRef.current = null;
- }, []);
+ loadingStartTimeRef.current = Date.now()
+ totalPausedMsRef.current = 0
+ pauseStartTimeRef.current = null
+ }, [])
// Reset timing refs inline when isQueryActive transitions false→true.
// queryGuard.reserve() (in executeUserInput) fires BEFORE processUserInput's
@@ -951,52 +1368,57 @@ export function REPL({
// first render where isQueryActive is observed true — the same render that
// first shows the spinner — so the ref is correct by the time the spinner
// reads it. See INC-4549.
- const wasQueryActiveRef = React.useRef(false);
+ const wasQueryActiveRef = React.useRef(false)
if (isQueryActive && !wasQueryActiveRef.current) {
- resetTimingRefs();
+ resetTimingRefs()
}
- wasQueryActiveRef.current = isQueryActive;
+ wasQueryActiveRef.current = isQueryActive
// Wrapper for setIsExternalLoading that resets timing refs on transition
// to true — SpinnerWithVerb reads these for elapsed time, so they must be
// reset for remote sessions / foregrounded tasks too (not just local
// queries, which reset them in onQuery). Without this, a remote-only
// session would show ~56 years elapsed (Date.now() - 0).
- const setIsExternalLoading = React.useCallback((value: boolean) => {
- setIsExternalLoadingRaw(value);
- if (value) resetTimingRefs();
- }, [resetTimingRefs]);
+ const setIsExternalLoading = React.useCallback(
+ (value: boolean) => {
+ setIsExternalLoadingRaw(value)
+ if (value) resetTimingRefs()
+ },
+ [resetTimingRefs],
+ )
// Start time of the first turn that had swarm teammates running
// Used to compute total elapsed time (including teammate execution) for the deferred message
- const swarmStartTimeRef = React.useRef(null);
- const swarmBudgetInfoRef = React.useRef<{
- tokens: number;
- limit: number;
- nudges: number;
- } | undefined>(undefined);
+ const swarmStartTimeRef = React.useRef(null)
+ const swarmBudgetInfoRef = React.useRef<
+ { tokens: number; limit: number; nudges: number } | undefined
+ >(undefined)
// Ref to track current focusedInputDialog for use in callbacks
// This avoids stale closures when checking dialog state in timer callbacks
- const focusedInputDialogRef = React.useRef>(undefined);
+ const focusedInputDialogRef =
+ React.useRef>(undefined)
// How long after the last keystroke before deferred dialogs are shown
- const PROMPT_SUPPRESSION_MS = 1500;
+ const PROMPT_SUPPRESSION_MS = 1500
// True when user is actively typing — defers interrupt dialogs so keystrokes
// don't accidentally dismiss or answer a permission prompt the user hasn't read yet.
- const [isPromptInputActive, setIsPromptInputActive] = React.useState(false);
- const [autoUpdaterResult, setAutoUpdaterResult] = useState(null);
+ const [isPromptInputActive, setIsPromptInputActive] = React.useState(false)
+
+ const [autoUpdaterResult, setAutoUpdaterResult] =
+ useState(null)
+
useEffect(() => {
if (autoUpdaterResult?.notifications) {
autoUpdaterResult.notifications.forEach(notification => {
addNotification({
key: 'auto-updater-notification',
text: notification,
- priority: 'low'
- });
- });
+ priority: 'low',
+ })
+ })
}
- }, [autoUpdaterResult, addNotification]);
+ }, [autoUpdaterResult, addNotification])
// tmux + fullscreen + `mouse off`: one-time hint that wheel won't scroll.
// We no longer mutate tmux's session-scoped mouse option (it poisoned
@@ -1008,50 +1430,52 @@ export function REPL({
addNotification({
key: 'tmux-mouse-hint',
text: hint,
- priority: 'low'
- });
+ priority: 'low',
+ })
}
- });
+ })
}
// eslint-disable-next-line react-hooks/exhaustive-deps
- }, []);
- const [showUndercoverCallout, setShowUndercoverCallout] = useState(false);
+ }, [])
+
+ const [showUndercoverCallout, setShowUndercoverCallout] = useState(false)
useEffect(() => {
if ("external" === 'ant') {
void (async () => {
// Wait for repo classification to settle (memoized, no-op if primed).
- const {
- isInternalModelRepo
- } = await import('../utils/commitAttribution.js');
- await isInternalModelRepo();
- const {
- shouldShowUndercoverAutoNotice
- } = await import('../utils/undercover.js');
+ const { isInternalModelRepo } = await import(
+ '../utils/commitAttribution.js'
+ )
+ await isInternalModelRepo()
+ const { shouldShowUndercoverAutoNotice } = await import(
+ '../utils/undercover.js'
+ )
if (shouldShowUndercoverAutoNotice()) {
- setShowUndercoverCallout(true);
+ setShowUndercoverCallout(true)
}
- })();
+ })()
}
// eslint-disable-next-line react-hooks/exhaustive-deps
- }, []);
+ }, [])
+
const [toolJSX, setToolJSXInternal] = useState<{
- jsx: React.ReactNode | null;
- shouldHidePromptInput: boolean;
- shouldContinueAnimation?: true;
- showSpinner?: boolean;
- isLocalJSXCommand?: boolean;
- isImmediate?: boolean;
- } | null>(null);
+ jsx: React.ReactNode | null
+ shouldHidePromptInput: boolean
+ shouldContinueAnimation?: true
+ showSpinner?: boolean
+ isLocalJSXCommand?: boolean
+ isImmediate?: boolean
+ } | null>(null)
// Track local JSX commands separately so tools can't overwrite them.
// This enables "immediate" commands (like /btw) to persist while Claude is processing.
const localJSXCommandRef = useRef<{
- jsx: React.ReactNode | null;
- shouldHidePromptInput: boolean;
- shouldContinueAnimation?: true;
- showSpinner?: boolean;
- isLocalJSXCommand: true;
- } | null>(null);
+ jsx: React.ReactNode | null
+ shouldHidePromptInput: boolean
+ shouldContinueAnimation?: true
+ showSpinner?: boolean
+ isLocalJSXCommand: true
+ } | null>(null)
// Wrapper for setToolJSX that preserves local JSX commands (like /btw).
// When a local JSX command is active, we ignore updates from tools
@@ -1062,89 +1486,108 @@ export function REPL({
// 2. Set `isLocalJSXCommand: true` when calling setToolJSX in the command's JSX
// 3. In the onDone callback, use `setToolJSX({ jsx: null, shouldHidePromptInput: false, clearLocalJSX: true })`
// to explicitly clear the overlay when the user dismisses it
- const setToolJSX = useCallback((args: {
- jsx: React.ReactNode | null;
- shouldHidePromptInput: boolean;
- shouldContinueAnimation?: true;
- showSpinner?: boolean;
- isLocalJSXCommand?: boolean;
- clearLocalJSX?: boolean;
- } | null) => {
- // If setting a local JSX command, store it in the ref
- if (args?.isLocalJSXCommand) {
- const {
- clearLocalJSX: _,
- ...rest
- } = args;
- localJSXCommandRef.current = {
- ...rest,
- isLocalJSXCommand: true
- };
- setToolJSXInternal(rest);
- return;
- }
+ const setToolJSX = useCallback(
+ (
+ args: {
+ jsx: React.ReactNode | null
+ shouldHidePromptInput: boolean
+ shouldContinueAnimation?: true
+ showSpinner?: boolean
+ isLocalJSXCommand?: boolean
+ clearLocalJSX?: boolean
+ } | null,
+ ) => {
+ // If setting a local JSX command, store it in the ref
+ if (args?.isLocalJSXCommand) {
+ const { clearLocalJSX: _, ...rest } = args
+ localJSXCommandRef.current = { ...rest, isLocalJSXCommand: true }
+ setToolJSXInternal(rest)
+ return
+ }
- // If there's an active local JSX command in the ref
- if (localJSXCommandRef.current) {
- // Allow clearing only if explicitly requested (from onDone callbacks)
- if (args?.clearLocalJSX) {
- localJSXCommandRef.current = null;
- setToolJSXInternal(null);
- return;
+ // If there's an active local JSX command in the ref
+ if (localJSXCommandRef.current) {
+ // Allow clearing only if explicitly requested (from onDone callbacks)
+ if (args?.clearLocalJSX) {
+ localJSXCommandRef.current = null
+ setToolJSXInternal(null)
+ return
+ }
+ // Otherwise, keep the local JSX command visible - ignore tool updates
+ return
}
- // Otherwise, keep the local JSX command visible - ignore tool updates
- return;
- }
- // No active local JSX command, allow any update
- if (args?.clearLocalJSX) {
- setToolJSXInternal(null);
- return;
- }
- setToolJSXInternal(args);
- }, []);
- const [toolUseConfirmQueue, setToolUseConfirmQueue] = useState([]);
+ // No active local JSX command, allow any update
+ if (args?.clearLocalJSX) {
+ setToolJSXInternal(null)
+ return
+ }
+ setToolJSXInternal(args)
+ },
+ [],
+ )
+ const [toolUseConfirmQueue, setToolUseConfirmQueue] = useState<
+ ToolUseConfirm[]
+ >([])
// Sticky footer JSX registered by permission request components (currently
// only ExitPlanModePermissionRequest). Renders in FullscreenLayout's `bottom`
// slot so response options stay visible while the user scrolls a long plan.
- const [permissionStickyFooter, setPermissionStickyFooter] = useState(null);
- const [sandboxPermissionRequestQueue, setSandboxPermissionRequestQueue] = useState void;
- }>>([]);
- const [promptQueue, setPromptQueue] = useState void;
- reject: (error: Error) => void;
- }>>([]);
+ const [permissionStickyFooter, setPermissionStickyFooter] =
+ useState(null)
+ const [sandboxPermissionRequestQueue, setSandboxPermissionRequestQueue] =
+ useState<
+ Array<{
+ hostPattern: NetworkHostPattern
+ resolvePromise: (allowConnection: boolean) => void
+ }>
+ >([])
+ const [promptQueue, setPromptQueue] = useState<
+ Array<{
+ request: PromptRequest
+ title: string
+ toolInputSummary?: string | null
+ resolve: (response: PromptResponse) => void
+ reject: (error: Error) => void
+ }>
+ >([])
// Track bridge cleanup functions for sandbox permission requests so the
// local dialog handler can cancel the remote prompt when the local user
// responds first. Keyed by host to support concurrent same-host requests.
- const sandboxBridgeCleanupRef = useRef