diff --git a/src/screens/REPL.tsx b/src/screens/REPL.tsx
index 9fdd3b11..ef1513aa 100644
--- a/src/screens/REPL.tsx
+++ b/src/screens/REPL.tsx
@@ -97,8 +97,8 @@ import { logError } from '../utils/log.js';
/* 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: () => {}
+ 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
@@ -106,11 +106,11 @@ const VoiceKeybindingHandler: typeof import('../hooks/useVoiceIntegration.js').V
// 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: () => {}
+ 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;
@@ -192,7 +192,7 @@ 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_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;
@@ -297,7 +297,7 @@ 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) => {}
+ 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
@@ -448,28 +448,28 @@ function TranscriptSearchBar({
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}
- ;
+ // applySearchHighlight scans the whole screen buffer. The query
+ // text rendered here IS on screen — /foo matches its own 'foo' in
+ // the bar. With no content matches that's the ONLY visible match →
+ // gets CURRENT → underlined. noSelect makes searchHighlight.ts:76
+ // skip these cells (same exclusion as gutters). You can't text-
+ // select the bar either; it's transient chrome, fine.
+ noSelect>
+ /
+ {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 = '✳';
@@ -605,8 +605,8 @@ export function REPL({
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;
+ // biome-ignore lint/correctness/useHookAtTopLevel: feature() is a compile-time constant
+ useMemo(() => isEnvTruthy(process.env.CLAUDE_CODE_DISABLE_MESSAGE_ACTIONS), []) : false;
// Agent definition is state so /resume can update it mid-session
const [mainThreadAgentDefinition, setMainThreadAgentDefinition] = useState(initialMainThreadAgentDefinition);
@@ -865,11 +865,11 @@ export function REPL({
// 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).
@@ -1246,8 +1246,8 @@ export function REPL({
const cursorNavRef = useRef(null);
// Memoized so Messages' React.memo holds.
const unseenDivider = useMemo(() => computeUnseenDivider(messages, dividerIndex),
- // eslint-disable-next-line react-hooks/exhaustive-deps -- length change covers appends; useUnseenDivider's count-drop guard clears dividerIndex on replace/rewind
- [dividerIndex, messages.length]);
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- length change covers appends; useUnseenDivider's count-drop guard clears dividerIndex on replace/rewind
+ [dividerIndex, messages.length]);
// Re-pin scroll to bottom and clear the unseen-messages baseline. Called
// on any user-driven return-to-live action (submit, type-into-empty,
// overlay appear/dismiss).
@@ -1276,13 +1276,13 @@ export function REPL({
const {
maybeLoadOlder
} = feature('KAIROS') ?
- // biome-ignore lint/correctness/useHookAtTopLevel: feature() is a compile-time constant
- useAssistantHistory({
- config: remoteSessionConfig,
- setMessages,
- scrollRef,
- onPrepend: shiftDivider
- }) : HISTORY_STUB;
+ // biome-ignore lint/correctness/useHookAtTopLevel: feature() is a compile-time constant
+ useAssistantHistory({
+ config: remoteSessionConfig,
+ setMessages,
+ scrollRef,
+ onPrepend: shiftDivider
+ }) : HISTORY_STUB;
// Compose useUnseenDivider's callbacks with the lazy-load trigger.
const composedOnScroll = useCallback((sticky: boolean, handle: ScrollBoxHandle) => {
lastUserScrollTsRef.current = Date.now();
@@ -1593,12 +1593,12 @@ export function REPL({
swarmStartTimeRef.current = null;
swarmBudgetInfoRef.current = undefined;
setMessages(prev => [...prev, createTurnDurationMessage(totalMs, deferredBudget,
- // Count only what recordTranscript will persist — ephemeral
- // progress ticks and non-ant attachments are filtered by
- // isLoggableMessage and never reach disk. Using raw prev.length
- // would make checkResumeConsistency report false delta<0 for
- // every turn that ran a progress-emitting tool.
- count(prev, isLoggableMessage))]);
+ // Count only what recordTranscript will persist — ephemeral
+ // progress ticks and non-ant attachments are filtered by
+ // isLoggableMessage and never reach disk. Using raw prev.length
+ // would make checkResumeConsistency report false delta<0 for
+ // every turn that ran a progress-emitting tool.
+ count(prev, isLoggableMessage))]);
}
}, [hasRunningTeammates, setMessages]);
@@ -1665,19 +1665,19 @@ export function REPL({
setToolJSX
});
const showSpinner = (!toolJSX || toolJSX.showSpinner === true) && toolUseConfirmQueue.length === 0 && promptQueue.length === 0 && (
- // Show spinner during input processing, API call, while teammates are running,
- // or while pending task notifications are queued (prevents spinner bounce between consecutive notifications)
- isLoading || userInputOnProcessing || hasRunningTeammates ||
- // Keep spinner visible while task notifications are queued for processing.
- // Without this, the spinner briefly disappears between consecutive notifications
- // (e.g., multiple background agents completing in rapid succession) because
- // isLoading goes false momentarily between processing each one.
- getCommandQueueLength() > 0) &&
- // Hide spinner when waiting for leader to approve permission request
- !pendingWorkerRequest && !onlySleepToolActive && (
- // Hide spinner when streaming text is visible (the text IS the feedback),
- // but keep it when isBriefOnly suppresses the streaming text display
- !visibleStreamingText || isBriefOnly);
+ // Show spinner during input processing, API call, while teammates are running,
+ // or while pending task notifications are queued (prevents spinner bounce between consecutive notifications)
+ isLoading || userInputOnProcessing || hasRunningTeammates ||
+ // Keep spinner visible while task notifications are queued for processing.
+ // Without this, the spinner briefly disappears between consecutive notifications
+ // (e.g., multiple background agents completing in rapid succession) because
+ // isLoading goes false momentarily between processing each one.
+ getCommandQueueLength() > 0) &&
+ // Hide spinner when waiting for leader to approve permission request
+ !pendingWorkerRequest && !onlySleepToolActive && (
+ // Hide spinner when streaming text is visible (the text IS the feedback),
+ // but keep it when isBriefOnly suppresses the streaming text display
+ !visibleStreamingText || isBriefOnly);
// Check if any permission or ask question prompt is currently visible
// This is used to prevent the survey from opening while prompts are active
@@ -2323,9 +2323,9 @@ export function REPL({
addNotification({
key: 'sandbox-unavailable',
jsx: <>
- sandbox disabled
- · /sandbox
- >,
+ sandbox disabled
+ · /sandbox
+ >,
priority: 'medium'
});
}, [addNotification]);
@@ -2676,7 +2676,7 @@ export function REPL({
// useDeferredHookMessages) and attachment messages (appended by
// processTextPrompt) — both pushed length past 1 on turn one, so the
// title silently fell through to the "Claude Code" default.
- if (getAPIProvider() === 'firstParty' && !titleDisabled && !sessionTitle && !agentTitle && !haikuTitleAttemptedRef.current) {
+ if (!titleDisabled && !sessionTitle && !agentTitle && !haikuTitleAttemptedRef.current) {
const firstUserMessage = newMessages.find(m => m.type === 'user' && !m.isMeta);
const text = firstUserMessage?.type === 'user' ? getContentText(firstUserMessage.message.content) : null;
// Skip synthetic breadcrumbs — slash-command output, prompt-skill
@@ -2686,7 +2686,7 @@ export function REPL({
if (text && !text.startsWith(`<${LOCAL_COMMAND_STDOUT_TAG}>`) && !text.startsWith(`<${COMMAND_MESSAGE_TAG}>`) && !text.startsWith(`<${COMMAND_NAME_TAG}>`) && !text.startsWith(`<${BASH_INPUT_TAG}>`)) {
haikuTitleAttemptedRef.current = true;
void generateSessionTitle(text, new AbortController().signal).then(title => {
- if (title) setHaikuTitle(title);else haikuTitleAttemptedRef.current = false;
+ if (title) setHaikuTitle(title); else haikuTitleAttemptedRef.current = false;
}, () => {
haikuTitleAttemptedRef.current = false;
});
@@ -2760,11 +2760,11 @@ export function REPL({
});
}
queryCheckpoint('query_context_loading_start');
- const [,, defaultSystemPrompt, baseUserContext, systemContext] = await Promise.all([
- // IMPORTANT: do this after setMessages() above, to avoid UI jank
- checkAndDisableBypassPermissionsIfNeeded(toolPermissionContext, setAppState),
- // Gated on TRANSCRIPT_CLASSIFIER so GrowthBook kill switch runs wherever auto mode is built in
- feature('TRANSCRIPT_CLASSIFIER') ? checkAndDisableAutoModeIfNeeded(toolPermissionContext, setAppState, store.getState().fastMode) : undefined, getSystemPrompt(freshTools, mainLoopModelParam, Array.from(toolPermissionContext.additionalWorkingDirectories.keys()), freshMcpClients), getUserContext(), getSystemContext()]);
+ const [, , defaultSystemPrompt, baseUserContext, systemContext] = await Promise.all([
+ // IMPORTANT: do this after setMessages() above, to avoid UI jank
+ checkAndDisableBypassPermissionsIfNeeded(toolPermissionContext, setAppState),
+ // Gated on TRANSCRIPT_CLASSIFIER so GrowthBook kill switch runs wherever auto mode is built in
+ feature('TRANSCRIPT_CLASSIFIER') ? checkAndDisableAutoModeIfNeeded(toolPermissionContext, setAppState, store.getState().fastMode) : undefined, getSystemPrompt(freshTools, mainLoopModelParam, Array.from(toolPermissionContext.additionalWorkingDirectories.keys()), freshMcpClients), getUserContext(), getSystemContext()]);
const userContext = {
...baseUserContext,
...getCoordinatorUserContext(freshMcpClients, isScratchpadEnabled() ? getScratchpadDir() : undefined),
@@ -3110,9 +3110,9 @@ export function REPL({
if (typeof content === 'string' && !initialMsg.message.planContent) {
// Route through onSubmit for proper processing including UserPromptSubmit hooks
void onSubmit(content, {
- setCursorOffset: () => {},
- clearBuffer: () => {},
- resetHistory: () => {}
+ setCursorOffset: () => { },
+ clearBuffer: () => { },
+ resetHistory: () => { }
});
} else {
// Plan messages or complex content (images, etc.) - send directly to model
@@ -3121,10 +3121,10 @@ export function REPL({
const newAbortController = createAbortController();
setAbortController(newAbortController);
void onQuery([initialMsg.message], newAbortController, true,
- // shouldQuery
- [],
- // additionalAllowedTools
- mainLoopModel);
+ // shouldQuery
+ [],
+ // additionalAllowedTools
+ mainLoopModel);
}
// Reset ref after a delay to allow new initial messages
@@ -3526,18 +3526,18 @@ export function REPL({
setStashedPrompt(undefined);
}
}, [queryGuard,
- // isLoading is read at the !isLoading checks above for input-clearing
- // and submitCount gating. It's derived from isQueryActive || isExternalLoading,
- // so including it here ensures the closure captures the fresh value.
- isLoading, isExternalLoading, inputMode, commands, setInputValue, setInputMode, setPastedContents, setSubmitCount, setIDESelection, setToolJSX, getToolUseContext,
- // messages is read via messagesRef.current inside the callback to
- // keep onSubmit stable across message updates (see L2384/L2400/L2662).
- // Without this, each setMessages call (~30× per turn) recreates
- // onSubmit, pinning the REPL render scope (1776B) + that render's
- // messages array in downstream closures (PromptInput, handleAutoRunIssue).
- // Heap analysis showed ~9 REPL scopes and ~15 messages array versions
- // accumulating after #20174/#20175, all traced to this dep.
- mainLoopModel, pastedContents, ideSelection, setUserInputOnProcessing, setAbortController, addNotification, onQuery, stashedPrompt, setStashedPrompt, setAppState, onBeforeQuery, canUseTool, remoteSession, setMessages, awaitPendingHooks, repinScroll]);
+ // isLoading is read at the !isLoading checks above for input-clearing
+ // and submitCount gating. It's derived from isQueryActive || isExternalLoading,
+ // so including it here ensures the closure captures the fresh value.
+ isLoading, isExternalLoading, inputMode, commands, setInputValue, setInputMode, setPastedContents, setSubmitCount, setIDESelection, setToolJSX, getToolUseContext,
+ // messages is read via messagesRef.current inside the callback to
+ // keep onSubmit stable across message updates (see L2384/L2400/L2662).
+ // Without this, each setMessages call (~30× per turn) recreates
+ // onSubmit, pinning the REPL render scope (1776B) + that render's
+ // messages array in downstream closures (PromptInput, handleAutoRunIssue).
+ // Heap analysis showed ~9 REPL scopes and ~15 messages array versions
+ // accumulating after #20174/#20175, all traced to this dep.
+ mainLoopModel, pastedContents, ideSelection, setUserInputOnProcessing, setAbortController, addNotification, onQuery, stashedPrompt, setStashedPrompt, setAppState, onBeforeQuery, canUseTool, remoteSession, setMessages, awaitPendingHooks, repinScroll]);
// Callback for when user submits input while viewing a teammate's transcript
const onAgentSubmit = useCallback(async (input: string, task: InProcessTeammateTaskState | LocalAgentTaskState, helpers: PromptInputHelpers) => {
@@ -3558,8 +3558,8 @@ export function REPL({
addNotification({
key: `resume-agent-failed-${task.id}`,
jsx:
- Failed to resume agent: {errorMessage(err)}
- ,
+ Failed to resume agent: {errorMessage(err)}
+ ,
priority: 'low'
});
});
@@ -3577,9 +3577,9 @@ export function REPL({
const command = autoRunIssueReason ? getAutoRunCommand(autoRunIssueReason) : '/issue';
setAutoRunIssueReason(null); // Clear the state
onSubmit(command, {
- setCursorOffset: () => {},
- clearBuffer: () => {},
- resetHistory: () => {}
+ setCursorOffset: () => { },
+ clearBuffer: () => { },
+ resetHistory: () => { }
}).catch(err => {
logForDebugging(`Auto-run ${command} failed: ${errorMessage(err)}`);
});
@@ -3592,9 +3592,9 @@ export function REPL({
const handleSurveyRequestFeedback = useCallback(() => {
const command = "external" === 'ant' ? '/issue' : '/feedback';
onSubmit(command, {
- setCursorOffset: () => {},
- clearBuffer: () => {},
- resetHistory: () => {}
+ setCursorOffset: () => { },
+ clearBuffer: () => { },
+ resetHistory: () => { }
}).catch(err => {
logForDebugging(`Survey feedback request failed: ${err instanceof Error ? err.message : String(err)}`);
});
@@ -3609,9 +3609,9 @@ export function REPL({
onSubmitRef.current = onSubmit;
const handleOpenRateLimitOptions = useCallback(() => {
void onSubmitRef.current('/rate-limit-options', {
- setCursorOffset: () => {},
- clearBuffer: () => {},
- resetHistory: () => {}
+ setCursorOffset: () => { },
+ clearBuffer: () => { },
+ resetHistory: () => { }
});
}, []);
const handleExit = useCallback(async () => {
@@ -3628,14 +3628,14 @@ export function REPL({
}
const showWorktree = getCurrentWorktreeSession() !== null;
if (showWorktree) {
- setExitFlow( {}} onCancel={() => {
+ setExitFlow( { }} onCancel={() => {
setExitFlow(null);
setIsExiting(false);
}} />);
return;
}
const exitMod = await exit.load();
- const exitFlowResult = await exitMod.call(() => {});
+ const exitFlowResult = await exitMod.call(() => { });
setExitFlow(exitFlowResult);
// If call() returned without killing the process (bg session detach),
// clear isExiting so the UI is usable on reattach. No-op on the normal
@@ -3749,18 +3749,18 @@ export function REPL({
};
const messageActionCaps: MessageActionCaps = {
copy: text =>
- // setClipboard RETURNS OSC 52 — caller must stdout.write (tmux side-effects load-buffer, but that's tmux-only).
- void setClipboard(text).then(raw => {
- if (raw) process.stdout.write(raw);
- addNotification({
- // Same key as text-selection copy — repeated copies replace toast, don't queue.
- key: 'selection-copied',
- text: 'copied',
- color: 'success',
- priority: 'immediate',
- timeoutMs: 2000
- });
- }),
+ // setClipboard RETURNS OSC 52 — caller must stdout.write (tmux side-effects load-buffer, but that's tmux-only).
+ void setClipboard(text).then(raw => {
+ if (raw) process.stdout.write(raw);
+ addNotification({
+ // Same key as text-selection copy — repeated copies replace toast, don't queue.
+ key: 'selection-copied',
+ text: 'copied',
+ color: 'success',
+ priority: 'immediate',
+ timeoutMs: 2000
+ });
+ }),
edit: async msg => {
// Same skip-confirm check as /rewind: lossless → direct, else confirm dialog.
const rawIdx = findRawIndex(msg.uuid);
@@ -3856,14 +3856,14 @@ export function REPL({
const executeQueuedInput = useCallback(async (queuedCommands: QueuedCommand[]) => {
await handlePromptSubmit({
helpers: {
- setCursorOffset: () => {},
- clearBuffer: () => {},
- resetHistory: () => {}
+ setCursorOffset: () => { },
+ clearBuffer: () => { },
+ resetHistory: () => { }
},
queryGuard,
commands,
- onInputChange: () => {},
- setPastedContents: () => {},
+ onInputChange: () => { },
+ setPastedContents: () => { },
setToolJSX,
getToolUseContext,
messages,
@@ -3924,8 +3924,8 @@ export function REPL({
// User hasn't interacted since response ended, check other conditions
const idleTimeSinceResponse = Date.now() - lastQueryCompletionTime;
if (!isLoading && !toolJSX &&
- // Use ref to get current dialog state, avoiding stale closure
- focusedInputDialogRef.current === undefined && idleTimeSinceResponse >= getGlobalConfig().messageIdleNotifThresholdMs) {
+ // Use ref to get current dialog state, avoiding stale closure
+ focusedInputDialogRef.current === undefined && idleTimeSinceResponse >= getGlobalConfig().messageIdleNotifThresholdMs) {
void sendNotification({
message: 'Claude is waiting for your input',
notificationType: 'idle_prompt'
@@ -3957,13 +3957,13 @@ export function REPL({
addNotif({
key: 'idle-return-hint',
jsx: mode === 'hint_v2' ? <>
- new task?
- /clear
- to save
- {formattedTokens} tokens
- > :
- new task? /clear to save {formattedTokens} tokens
- ,
+ new task?
+ /clear
+ to save
+ {formattedTokens} tokens
+ > :
+ new task? /clear to save {formattedTokens} tokens
+ ,
priority: 'medium',
// Persist until submit — the hint fires at T+75min idle, user may
// not return for hours. removeNotification in useEffect cleanup
@@ -4015,17 +4015,17 @@ export function REPL({
// Voice input integration (VOICE_MODE builds only)
const voice = feature('VOICE_MODE') ?
- // biome-ignore lint/correctness/useHookAtTopLevel: feature() is a compile-time constant
- useVoiceIntegration({
- setInputValueRaw,
- inputValueRef,
- insertTextRef
- }) : {
- stripTrailing: () => 0,
- handleKeyEvent: () => {},
- resetAnchor: () => {},
- interimRange: null
- };
+ // biome-ignore lint/correctness/useHookAtTopLevel: feature() is a compile-time constant
+ useVoiceIntegration({
+ setInputValueRaw,
+ inputValueRef,
+ insertTextRef
+ }) : {
+ stripTrailing: () => 0,
+ handleKeyEvent: () => { },
+ resetAnchor: () => { },
+ interimRange: null
+ };
useInboxPoller({
enabled: isAgentSwarmsEnabled(),
isLoading,
@@ -4228,11 +4228,11 @@ export function REPL({
event.stopImmediatePropagation();
}
},
- // Search needs virtual scroll (jumpRef drives VirtualMessageList). [
- // kills it, so !dumpMode — after [ there's nothing to jump in.
- {
- isActive: screen === 'transcript' && virtualScrollActive && !searchOpen && !dumpMode
- });
+ // Search needs virtual scroll (jumpRef drives VirtualMessageList). [
+ // kills it, so !dumpMode — after [ there's nothing to jump in.
+ {
+ isActive: screen === 'transcript' && virtualScrollActive && !searchOpen && !dumpMode
+ });
const {
setQuery: setHighlight,
scanElement,
@@ -4323,12 +4323,12 @@ export function REPL({
})();
}
},
- // !searchOpen: typing 'v' or '[' in the search bar is search input, not
- // a command. No !dumpMode here — v should work after [ (the [ handler
- // guards itself inline).
- {
- isActive: screen === 'transcript' && virtualScrollActive && !searchOpen
- });
+ // !searchOpen: typing 'v' or '[' in the search bar is search input, not
+ // a command. No !dumpMode here — v should work after [ (the [ handler
+ // guards itself inline).
+ {
+ isActive: screen === 'transcript' && virtualScrollActive && !searchOpen
+ });
// Fresh `less` per transcript entry. Prevents stale highlights matching
// unrelated normal-mode text (overlay is alt-screen-global) and avoids
@@ -4396,78 +4396,78 @@ export function REPL({
const transcriptScrollRef = isFullscreenEnvEnabled() && !disableVirtualScroll && !dumpMode ? scrollRef : undefined;
const transcriptMessagesElement = ;
const transcriptToolJSX = toolJSX &&
- {toolJSX.jsx}
- ;
+ {toolJSX.jsx}
+ ;
const transcriptReturn =
-
-
- {feature('VOICE_MODE') ? : null}
-
- {transcriptScrollRef ?
- // ScrollKeybindingHandler must mount before CancelRequestHandler so
- // ctrl+c-with-selection copies instead of cancelling the active task.
- // Its raw useInput handler only stops propagation when a selection
- // exists — without one, ctrl+c falls through to CancelRequestHandler.
- jumpRef.current?.disarmSearch()} /> : null}
-
- {transcriptScrollRef ?
- {transcriptMessagesElement}
- {transcriptToolJSX}
-
- >} bottom={searchOpen ? {
- // Enter — commit. 0-match guard: junk query shouldn't
- // persist (badge hidden, n/N dead anyway).
- setSearchQuery(searchCount > 0 ? q : '');
- setSearchOpen(false);
- // onCancel path: bar unmounts before its useEffect([query])
- // can fire with ''. Without this, searchCount stays stale
- // (n guard at :4956 passes) and VML's matches[] too
- // (nextMatch walks the old array). Phantom nav, no
- // highlight. onExit (Enter, q non-empty) still commits.
- if (!q) {
- setSearchCount(0);
- setSearchCurrent(0);
+
+
+ {feature('VOICE_MODE') ? : null}
+
+ {transcriptScrollRef ?
+ // ScrollKeybindingHandler must mount before CancelRequestHandler so
+ // ctrl+c-with-selection copies instead of cancelling the active task.
+ // Its raw useInput handler only stops propagation when a selection
+ // exists — without one, ctrl+c falls through to CancelRequestHandler.
+ jumpRef.current?.disarmSearch()} /> : null}
+
+ {transcriptScrollRef ?
+ {transcriptMessagesElement}
+ {transcriptToolJSX}
+
+ >} bottom={searchOpen ? {
+ // Enter — commit. 0-match guard: junk query shouldn't
+ // persist (badge hidden, n/N dead anyway).
+ setSearchQuery(searchCount > 0 ? q : '');
+ setSearchOpen(false);
+ // onCancel path: bar unmounts before its useEffect([query])
+ // can fire with ''. Without this, searchCount stays stale
+ // (n guard at :4956 passes) and VML's matches[] too
+ // (nextMatch walks the old array). Phantom nav, no
+ // highlight. onExit (Enter, q non-empty) still commits.
+ if (!q) {
+ setSearchCount(0);
+ setSearchCurrent(0);
+ jumpRef.current?.setSearchQuery('');
+ }
+ }} onCancel={() => {
+ // Esc/ctrl+c/ctrl+g — undo. Bar's effect last fired
+ // with whatever was typed. searchQuery (REPL state)
+ // is unchanged since / (onClose = commit, didn't run).
+ // Two VML calls: '' restores anchor (0-match else-
+ // branch), then searchQuery re-scans from anchor's
+ // nearest. Both synchronous — one React batch.
+ // setHighlight explicit: REPL's sync-effect dep is
+ // searchQuery (unchanged), wouldn't re-fire.
+ setSearchOpen(false);
jumpRef.current?.setSearchQuery('');
- }
- }} onCancel={() => {
- // Esc/ctrl+c/ctrl+g — undo. Bar's effect last fired
- // with whatever was typed. searchQuery (REPL state)
- // is unchanged since / (onClose = commit, didn't run).
- // Two VML calls: '' restores anchor (0-match else-
- // branch), then searchQuery re-scans from anchor's
- // nearest. Both synchronous — one React batch.
- // setHighlight explicit: REPL's sync-effect dep is
- // searchQuery (unchanged), wouldn't re-fire.
- setSearchOpen(false);
- jumpRef.current?.setSearchQuery('');
- jumpRef.current?.setSearchQuery(searchQuery);
- setHighlight(searchQuery);
- }} setHighlight={setHighlight} /> : 0 ? {
- current: searchCurrent,
- count: searchCount
- } : undefined} />} /> : <>
- {transcriptMessagesElement}
- {transcriptToolJSX}
-
-
- >}
- ;
+ jumpRef.current?.setSearchQuery(searchQuery);
+ setHighlight(searchQuery);
+ }} setHighlight={setHighlight} /> : 0 ? {
+ current: searchCurrent,
+ count: searchCount
+ } : undefined} />} /> : <>
+ {transcriptMessagesElement}
+ {transcriptToolJSX}
+
+
+ >}
+ ;
// The virtual-scroll branch (FullscreenLayout above) needs
// 's constraint — without it,
// ScrollBox's flexGrow has no ceiling, viewport = content height,
@@ -4478,8 +4478,8 @@ export function REPL({
// unwrapped — it wants native terminal scrollback.
if (transcriptScrollRef) {
return
- {transcriptReturn}
- ;
+ {transcriptReturn}
+ ;
}
return transcriptReturn;
}
@@ -4541,11 +4541,11 @@ export function REPL({
// early return above wraps its virtual-scroll branch the same way; only
// the 30-cap dump branch stays unwrapped for native terminal scrollback.
const mainReturn =
-
-
- {feature('VOICE_MODE') ? : null}
-
- {/* ScrollKeybindingHandler must mount before CancelRequestHandler so
+
+
+ {feature('VOICE_MODE') ? : null}
+
+ {/* ScrollKeybindingHandler must mount before CancelRequestHandler so
ctrl+c-with-selection copies instead of cancelling the active task.
Its raw useInput handler only stops propagation when a selection
exists — without one, ctrl+c falls through to CancelRequestHandler.
@@ -4553,40 +4553,40 @@ export function REPL({
the modal's inner ScrollBox is not keyboard-driven. onScroll
stays suppressed while a modal is showing so scroll doesn't
stamp divider/pill state. */}
-
- {feature('MESSAGE_ACTIONS') && isFullscreenEnvEnabled() && !disableMessageActions ? : null}
-
-
- : undefined} modal={centeredModal} modalScrollRef={modalScrollRef} dividerYRef={dividerYRef} hidePill={!!viewedAgentTask} hideSticky={!!viewedTeammateTask} newMessageCount={unseenDivider?.count ?? 0} onPillClick={() => {
+
+ {feature('MESSAGE_ACTIONS') && isFullscreenEnvEnabled() && !disableMessageActions ? : null}
+
+
+ : undefined} modal={centeredModal} modalScrollRef={modalScrollRef} dividerYRef={dividerYRef} hidePill={!!viewedAgentTask} hideSticky={!!viewedTeammateTask} newMessageCount={unseenDivider?.count ?? 0} onPillClick={() => {
setCursor(null);
jumpToNew(scrollRef.current);
}} scrollable={<>
-
-
-
- {/* Hide the processing placeholder while a modal is showing —
+
+
+
+ {/* Hide the processing placeholder while a modal is showing —
it would sit at the last visible transcript row right above
the ▔ divider, showing "❯ /config" as redundant clutter
(the modal IS the /config UI). Outside modals it stays so
the user sees their input echoed while Claude processes. */}
- {!disabled && placeholderText && !centeredModal && }
- {toolJSX && !(toolJSX.isLocalJSXCommand && toolJSX.isImmediate) && !toolJsxCentered &&
- {toolJSX.jsx}
- }
- {"external" === 'ant' && }
- {feature('WEB_BROWSER_TOOL') ? WebBrowserPanelModule && : null}
-
- {showSpinner && 0} leaderIsIdle={!isLoading} />}
- {!showSpinner && !isLoading && !userInputOnProcessing && !hasRunningTeammates && isBriefOnly && !viewedAgentTask && }
- {isFullscreenEnvEnabled() && }
- >} bottom={
- {feature('BUDDY') && companionNarrow && isFullscreenEnvEnabled() && companionVisible ? : null}
-
- {permissionStickyFooter}
- {/* Immediate local-jsx commands (/btw, /sandbox, /assistant,
+ {toolJSX && !(toolJSX.isLocalJSXCommand && toolJSX.isImmediate) && !toolJsxCentered &&
+ {toolJSX.jsx}
+ }
+ {"external" === 'ant' && }
+ {feature('WEB_BROWSER_TOOL') ? WebBrowserPanelModule && : null}
+
+ {showSpinner && 0} leaderIsIdle={!isLoading} />}
+ {!showSpinner && !isLoading && !userInputOnProcessing && !hasRunningTeammates && isBriefOnly && !viewedAgentTask && }
+ {isFullscreenEnvEnabled() && }
+ >} bottom={
+ {feature('BUDDY') && companionNarrow && isFullscreenEnvEnabled() && companionVisible ? : null}
+
+ {permissionStickyFooter}
+ {/* Immediate local-jsx commands (/btw, /sandbox, /assistant,
/issue) render here, NOT inside scrollable. They stay mounted
while the main conversation streams behind them, so ScrollBox
relayouts on each new message would drag them around. bottom
@@ -4595,13 +4595,13 @@ export function REPL({
stays in scrollable: the main loop is paused so no jiggle,
and their tall content (DiffDetailView renders up to 400
lines with no internal scroll) needs the outer ScrollBox. */}
- {toolJSX?.isLocalJSXCommand && toolJSX.isImmediate && !toolJsxCentered &&
- {toolJSX.jsx}
- }
- {!showSpinner && !toolJSX?.isLocalJSXCommand && showExpandedTodos && tasksV2 && tasksV2.length > 0 &&
-
- }
- {focusedInputDialog === 'sandbox-permission' &&
+ {toolJSX.jsx}
+ }
+ {!showSpinner && !toolJSX?.isLocalJSXCommand && showExpandedTodos && tasksV2 && tasksV2.length > 0 &&
+
+ }
+ {focusedInputDialog === 'sandbox-permission' && {
@@ -4650,7 +4650,7 @@ export function REPL({
sandboxBridgeCleanupRef.current.delete(approvedHost);
}
}} />}
- {focusedInputDialog === 'prompt' && {
+ {focusedInputDialog === 'prompt' && {
const item = promptQueue[0];
if (!item) return;
item.resolve({
@@ -4664,12 +4664,12 @@ export function REPL({
item.reject(new Error('Prompt cancelled by user'));
setPromptQueue(([, ...tail]) => tail);
}} />}
- {/* Show pending indicator on worker while waiting for leader approval */}
- {pendingWorkerRequest && }
- {/* Show pending indicator for sandbox permission on worker side */}
- {pendingSandboxRequest && }
- {/* Worker sandbox permission requests from swarm workers */}
- {focusedInputDialog === 'worker-sandbox-permission' && }
+ {/* Show pending indicator for sandbox permission on worker side */}
+ {pendingSandboxRequest && }
+ {/* Worker sandbox permission requests from swarm workers */}
+ {focusedInputDialog === 'worker-sandbox-permission' && }
- {focusedInputDialog === 'elicitation' && {
+ {focusedInputDialog === 'elicitation' && {
const currentRequest = elicitation.queue[0];
if (!currentRequest) return;
// Call respond callback to resolve Promise
@@ -4742,7 +4742,7 @@ export function REPL({
}));
currentRequest?.onWaitingDismiss?.(action);
}} />}
- {focusedInputDialog === 'cost' && {
+ {focusedInputDialog === 'cost' && {
setShowCostDialog(false);
setHaveShownCostDialog(true);
saveGlobalConfig(current => ({
@@ -4751,7 +4751,7 @@ export function REPL({
}));
logEvent('tengu_cost_threshold_acknowledged', {});
}} />}
- {focusedInputDialog === 'idle-return' && idleReturnPending && {
+ {focusedInputDialog === 'idle-return' && idleReturnPending && {
const pending = idleReturnPending;
setIdleReturnPending(null);
logEvent('tengu_idle_return_action', {
@@ -4793,13 +4793,13 @@ export function REPL({
}
skipIdleCheckRef.current = true;
void onSubmitRef.current(pending.input, {
- setCursorOffset: () => {},
- clearBuffer: () => {},
- resetHistory: () => {}
+ setCursorOffset: () => { },
+ clearBuffer: () => { },
+ resetHistory: () => { }
});
}} />}
- {focusedInputDialog === 'ide-onboarding' && setShowIdeOnboarding(false)} installationStatus={ideInstallationStatus} />}
- {"external" === 'ant' && focusedInputDialog === 'model-switch' && AntModelSwitchCallout && {
+ {focusedInputDialog === 'ide-onboarding' && setShowIdeOnboarding(false)} installationStatus={ideInstallationStatus} />}
+ {"external" === 'ant' && focusedInputDialog === 'model-switch' && AntModelSwitchCallout && {
setShowModelSwitchCallout(false);
if (selection === 'switch' && modelAlias) {
setAppState(prev => ({
@@ -4809,8 +4809,8 @@ export function REPL({
}));
}
}} />}
- {"external" === 'ant' && focusedInputDialog === 'undercover-callout' && UndercoverAutoCallout && setShowUndercoverCallout(false)} />}
- {focusedInputDialog === 'effort-callout' && {
+ {"external" === 'ant' && focusedInputDialog === 'undercover-callout' && UndercoverAutoCallout && setShowUndercoverCallout(false)} />}
+ {focusedInputDialog === 'effort-callout' && {
setShowEffortCallout(false);
if (selection !== 'dismiss') {
setAppState(prev => ({
@@ -4819,7 +4819,7 @@ export function REPL({
}));
}
}} />}
- {focusedInputDialog === 'remote-callout' && {
+ {focusedInputDialog === 'remote-callout' && {
setAppState(prev => {
if (!prev.showRemoteCallout) return prev;
return {
@@ -4834,17 +4834,17 @@ export function REPL({
});
}} />}
- {exitFlow}
+ {exitFlow}
- {focusedInputDialog === 'plugin-hint' && hintRecommendation && }
+ {focusedInputDialog === 'plugin-hint' && hintRecommendation && }
- {focusedInputDialog === 'lsp-recommendation' && lspRecommendation && }
+ {focusedInputDialog === 'lsp-recommendation' && lspRecommendation && }
- {focusedInputDialog === 'desktop-upsell' && setShowDesktopUpsellStartup(false)} />}
+ {focusedInputDialog === 'desktop-upsell' && setShowDesktopUpsellStartup(false)} />}
- {feature('ULTRAPLAN') ? focusedInputDialog === 'ultraplan-choice' && ultraplanPendingChoice && store.getState()} setConversationId={setConversationId} /> : null}
+ {feature('ULTRAPLAN') ? focusedInputDialog === 'ultraplan-choice' && ultraplanPendingChoice && store.getState()} setConversationId={setConversationId} /> : null}
- {feature('ULTRAPLAN') ? focusedInputDialog === 'ultraplan-launch' && ultraplanLaunchPending && {
+ {feature('ULTRAPLAN') ? focusedInputDialog === 'ultraplan-launch' && ultraplanLaunchPending && {
const blurb = ultraplanLaunchPending.blurb;
setAppState(prev => prev.ultraplanLaunchPending ? {
...prev,
@@ -4884,26 +4884,26 @@ export function REPL({
}).then(appendStdout).catch(logError);
}} /> : null}
- {mrRender()}
+ {mrRender()}
- {!toolJSX?.shouldHidePromptInput && !focusedInputDialog && !isExiting && !disabled && !cursor && <>
- {autoRunIssueReason && }
- {postCompactSurvey.state !== 'closed' ? : memorySurvey.state !== 'closed' ? : }
- {/* Frustration-triggered transcript sharing prompt */}
- {frustrationDetection.state !== 'closed' && {}} handleTranscriptSelect={frustrationDetection.handleTranscriptSelect} inputValue={inputValue} setInputValue={setInputValue} />}
- {/* Skill improvement survey - appears when improvements detected (ant-only) */}
- {"external" === 'ant' && skillImprovementSurvey.suggestion && }
- {showIssueFlagBanner && }
- {}
-
-
- >}
- {cursor &&
- // inputValue is REPL state; typed text survives the round-trip.
- }
- {focusedInputDialog === 'message-selector' && {
+ {!toolJSX?.shouldHidePromptInput && !focusedInputDialog && !isExiting && !disabled && !cursor && <>
+ {autoRunIssueReason && }
+ {postCompactSurvey.state !== 'closed' ? : memorySurvey.state !== 'closed' ? : }
+ {/* Frustration-triggered transcript sharing prompt */}
+ {frustrationDetection.state !== 'closed' && { }} handleTranscriptSelect={frustrationDetection.handleTranscriptSelect} inputValue={inputValue} setInputValue={setInputValue} />}
+ {/* Skill improvement survey - appears when improvements detected (ant-only) */}
+ {"external" === 'ant' && skillImprovementSurvey.suggestion && }
+ {showIssueFlagBanner && }
+ { }
+
+
+ >}
+ {cursor &&
+ // inputValue is REPL state; typed text survives the round-trip.
+ }
+ {focusedInputDialog === 'message-selector' && {
await fileHistoryRewind((updater: (prev: FileHistoryState) => FileHistoryState) => {
setAppState(prev => ({
...prev,
@@ -4985,16 +4985,16 @@ export function REPL({
setIsMessageSelectorVisible(false);
setMessageSelectorPreselect(undefined);
}} />}
- {"external" === 'ant' && }
-
- {feature('BUDDY') && !(companionNarrow && isFullscreenEnvEnabled()) && companionVisible ? : null}
- } />
-
- ;
+ {"external" === 'ant' && }
+
+ {feature('BUDDY') && !(companionNarrow && isFullscreenEnvEnabled()) && companionVisible ? : null}
+ } />
+
+ ;
if (isFullscreenEnvEnabled()) {
return
- {mainReturn}
- ;
+ {mainReturn}
+ ;
}
return mainReturn;
}