diff --git "a/src/screens/REPL.tsx.bak" "b/src/screens/REPL.tsx.bak"
new file mode 100644--- /dev/null
+++ "b/src/screens/REPL.tsx.bak"
@@ -0,0 +1,5017 @@
+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';
+// 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';
+// 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;
+// 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: () => {}
+});
+// 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 : () => {};
+// 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 : () => ({});
+/* 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';
+// 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;
+/* 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';
+/* 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;
+/* 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';
+/* 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('');
+/* 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';
+// 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';
+
+// 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[] = [];
+
+// 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) => {}
+};
+// 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;
+
+// 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]!;
+}
+
+/**
+ * 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;
+}
+
+/** less-style / bar. 1-row, same border-top styling as TranscriptModeFooter
+ * so swapping them in the bottom slot doesn't shift ScrollBox height.
+ * useSearchInput handles readline editing; we report query changes and
+ * render the counter. Incremental — re-search + highlight per keystroke. */
+function TranscriptSearchBar({
+ jumpRef,
+ count,
+ current,
+ onClose,
+ onCancel,
+ setHighlight,
+ initialQuery
+}: {
+ jumpRef: RefObject;
+ count: number;
+ current: number;
+ /** Enter — commit. Query persists for n/N. */
+ onClose: (lastQuery: string) => void;
+ /** Esc/ctrl+c/ctrl+g — undo to pre-/ state. */
+ 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;
+}): React.ReactNode {
+ const {
+ query,
+ cursorOffset
+ } = useSearchInput({
+ isActive: true,
+ initialQuery,
+ onExit: () => onClose(query),
+ 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.
+ // First / in a transcript session pays the extractSearchText cost.
+ // Subsequent / return 0 immediately (indexWarmed ref in VML).
+ // Transcript is frozen at ctrl+o so the cache stays valid.
+ // Initial 'building' so warmDone is false on mount — the [query] effect
+ // waits for the warm effect's first resolve instead of racing it. With
+ // 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');
+ React.useEffect(() => {
+ let alive = true;
+ const warm = jumpRef.current?.warmSearchIndex;
+ if (!warm) {
+ setIndexStatus(null); // VML not mounted yet — rare, skip indicator
+ return;
+ }
+ setIndexStatus('building');
+ warm().then(ms => {
+ if (!alive) return;
+ // <20ms = imperceptible. No point showing "indexed in 3ms".
+ if (ms < 20) {
+ setIndexStatus(null);
+ } else {
+ setIndexStatus({
+ ms
+ });
+ setTimeout(() => alive && setIndexStatus(null), 2000);
+ }
+ });
+ return () => {
+ alive = false;
+ };
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []); // 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';
+ useEffect(() => {
+ 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.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.
+
+ {current}/{count}
+ {' '}
+ : null}
+ ;
+}
+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
+ * is running. Isolated from REPL so the 960ms animation tick re-renders only
+ * this leaf component (which returns null — pure side-effect) instead of the
+ * 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;
+}
+export type Props = {
+ commands: Command[];
+ debug: boolean;
+ initialTools: Tool[];
+ // Initial messages to populate the REPL with
+ 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[];
+ // Content-replacement records from a resumed session's transcript — used to
+ // reconstruct contentReplacementState so the same results are re-replaced
+ 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;
+ // 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;
+ // Optional callback when a turn completes (model finishes responding)
+ onTurnComplete?: (messages: MessageType[]) => void | Promise;
+ // When true, disables REPL input (hides prompt and prevents message selector)
+ disabled?: boolean;
+ // Optional agent definition to use for the main thread
+ mainThreadAgentDefinition?: AgentDefinition;
+ // When true, disables all slash commands
+ disableSlashCommands?: boolean;
+ // Task list id: when set, enables tasks mode that watches a task list and auto-processes tasks.
+ taskListId?: string;
+ // Remote session config for --remote mode (uses CCR as execution engine)
+ remoteSessionConfig?: RemoteSessionConfig;
+ // Direct connect config for `claude connect` mode (connects to a claude server)
+ directConnectConfig?: DirectConnectConfig;
+ // SSH session for `claude ssh` mode (local REPL, remote tools over ssh)
+ sshSession?: SSHSession;
+ // Thinking configuration to use when thinking is enabled
+ thinkingConfig: ThinkingConfig;
+};
+export type Screen = 'prompt' | 'transcript';
+export function REPL({
+ commands: initialCommands,
+ debug,
+ initialTools,
+ initialMessages,
+ pendingHookMessages,
+ initialFileHistorySnapshots,
+ initialContentReplacements,
+ initialAgentName,
+ initialAgentColor,
+ mcpClients: initialMcpClients,
+ dynamicMcpConfig: initialDynamicMcpConfig,
+ autoConnectIdeFlag,
+ strictMcpConfig = false,
+ systemPrompt: customSystemPrompt,
+ appendSystemPrompt,
+ onBeforeQuery,
+ onTurnComplete,
+ disabled = false,
+ mainThreadAgentDefinition: initialMainThreadAgentDefinition,
+ disableSlashCommands = false,
+ taskListId,
+ remoteSessionConfig,
+ directConnectConfig,
+ sshSession,
+ thinkingConfig
+}: Props): React.ReactNode {
+ 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;
+
+ // Log REPL mount/unmount lifecycle
+ useEffect(() => {
+ 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();
+ // 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();
+
+ // 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;
+ useEffect(() => {
+ 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)) : [];
+ return {
+ ...prev,
+ tasks: {
+ ...prev.tasks,
+ [taskId]: {
+ ...t,
+ messages: [...diskOnly, ...live],
+ 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);
+
+ // Watch for skill file changes and reload all commands
+ 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);
+
+ // BriefTool.isEnabled() reads getUserMsgOptIn() from bootstrap state, which
+ // /brief flips mid-session alongside isBriefOnly. The memo below needs a
+ // React-visible dep to re-run getTools() when that happens; isBriefOnly is
+ // 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);
+ // [ 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);
+ // v-for-editor render progress. Inline in the footer — notifications
+ // render inside PromptInput which isn't mounted in transcript.
+ 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();
+
+ // eslint-disable-next-line prefer-const
+ 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);
+ // Dead code elimination: model switch callout state (ant-only)
+ const [showModelSwitchCallout, setShowModelSwitchCallout] = useState(() => {
+ if ("external" === 'ant') {
+ return shouldShowAntModelSwitch();
+ }
+ 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();
+ const {
+ recommendation: lspRecommendation,
+ handleResponse: handleLspResponse
+ } = useLspPluginRecommendation();
+ const {
+ recommendation: hintRecommendation,
+ handleResponse: handleHintResponse
+ } = useClaudeCodeHintRecommendation();
+
+ // Memoize the combined initial tools array to prevent reference changes
+ const combinedInitialTools = useMemo(() => {
+ return [...localTools, ...initialTools];
+ }, [localTools, initialTools]);
+
+ // Initialize plugin management
+ useManagePlugins({
+ enabled: !isRemoteSession
+ });
+ const tasksV2 = useTasksV2WithCollapseEffect();
+
+ // Start background plugin installations
+
+ // SECURITY: This code is guaranteed to run ONLY after the "trust this folder" dialog
+ // has been confirmed by the user. The trust dialog is shown in cli.tsx (line ~387)
+ // before the REPL component is rendered. The dialog blocks execution until the user
+ // accepts, and only then is the REPL component mounted and this effect runs.
+ // 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]);
+
+ // 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);
+
+ // 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);
+
+ // Apply agent tool restrictions if mainThreadAgentDefinition is set
+ const {
+ tools,
+ allowedAgentTypes
+ } = useMemo(() => {
+ if (!mainThreadAgentDefinition) {
+ return {
+ tools: mergedTools,
+ allowedAgentTypes: undefined as string[] | undefined
+ };
+ }
+ const resolved = resolveAgentTools(mainThreadAgentDefinition, mergedTools, false, true);
+ return {
+ tools: resolved.resolvedTools,
+ 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[]);
+ // 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');
+ // 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
+ // in onSubmit's deps was recreating onSubmit on every flip, which
+ // cascaded into PromptInput prop churn and downstream useCallback/useMemo
+ // 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);
+
+ // 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 (remaining > 0) {
+ const timer = setTimeout(setStreamingThinking, remaining, null);
+ return () => clearTimeout(timer);
+ } else {
+ setStreamingThinking(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;
+
+ // 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>(() => {});
+
+ // 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>(() => {});
+
+ // Ref to the fullscreen layout's scroll box for keyboard scrolling.
+ // Null when fullscreen mode is disabled (ref never attached).
+ 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);
+ // 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);
+
+ // 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;
+
+ // 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);
+
+ // 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);
+
+ // 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;
+
+ // 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);
+ // 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);
+ // 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);
+
+ // 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 resetTimingRefs = React.useCallback(() => {
+ 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
+ // first await, but the ref reset in onQuery's try block runs AFTER. During
+ // that gap, React renders the spinner with loadingStartTimeRef=0, computing
+ // elapsedTimeMs = Date.now() - 0 ≈ 56 years. This inline reset runs on the
+ // 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);
+ if (isQueryActive && !wasQueryActiveRef.current) {
+ resetTimingRefs();
+ }
+ 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]);
+
+ // 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);
+
+ // 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);
+
+ // How long after the last keystroke before deferred dialogs are shown
+ 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);
+ useEffect(() => {
+ if (autoUpdaterResult?.notifications) {
+ autoUpdaterResult.notifications.forEach(notification => {
+ addNotification({
+ key: 'auto-updater-notification',
+ text: notification,
+ priority: 'low'
+ });
+ });
+ }
+ }, [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
+ // sibling panes); tmux users already know this tradeoff from vim/less.
+ useEffect(() => {
+ if (isFullscreenEnvEnabled()) {
+ void maybeGetTmuxMouseHint().then(hint => {
+ if (hint) {
+ addNotification({
+ key: 'tmux-mouse-hint',
+ text: hint,
+ priority: 'low'
+ });
+ }
+ });
+ }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
+ 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');
+ if (shouldShowUndercoverAutoNotice()) {
+ 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);
+
+ // 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);
+
+ // Wrapper for setToolJSX that preserves local JSX commands (like /btw).
+ // When a local JSX command is active, we ignore updates from tools
+ // unless they explicitly set clearLocalJSX: true (from onDone callbacks).
+ //
+ // TO ADD A NEW IMMEDIATE COMMAND:
+ // 1. Set `immediate: true` in the command definition
+ // 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;
+ }
+
+ // 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;
+ }
+
+ // No active local JSX command, allow any update
+ if (args?.clearLocalJSX) {
+ setToolJSXInternal(null);
+ return;
+ }
+ setToolJSXInternal(args);
+ }, []);
+ const [toolUseConfirmQueue, setToolUseConfirmQueue] = useState([]);
+ // 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;
+ }>>([]);
+
+ // 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