Merge pull request #105 from rajrasane/fix/third-party-provider-compatibility

fix: Improve session title handling and Docker compatibility
This commit is contained in:
Kevin Codex
2026-04-02 13:50:18 +08:00
committed by GitHub
5 changed files with 376 additions and 372 deletions

View File

@@ -289,7 +289,7 @@ function checkOllamaProcessorMode(): CheckResult {
if (result.status !== 0) {
const detail = (result.stderr || result.stdout || 'Unable to run ollama ps').trim()
return fail('Ollama processor mode', detail)
return pass('Ollama processor mode', `Native CLI check failed (${detail}). Assuming valid Docker/remote backend since HTTP ping passed.`)
}
const output = (result.stdout || '').trim()

View File

@@ -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 <Box borderTopDimColor borderBottom={false} borderLeft={false} borderRight={false} borderStyle="single" marginTop={1} paddingLeft={2} width="100%"
// 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>
<Text>/</Text>
<Text>{query.slice(0, off)}</Text>
<Text inverse>{cursorChar}</Text>
{off < query.length && <Text>{query.slice(off + 1)}</Text>}
<Box flexGrow={1} />
{indexStatus === 'building' ? <Text dimColor>indexing </Text> : indexStatus ? <Text dimColor>indexed in {indexStatus.ms}ms </Text> : count === 0 && query ? <Text color="error">no matches </Text> : 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.
<Text dimColor>
{current}/{count}
{' '}
</Text> : null}
</Box>;
// 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>
<Text>/</Text>
<Text>{query.slice(0, off)}</Text>
<Text inverse>{cursorChar}</Text>
{off < query.length && <Text>{query.slice(off + 1)}</Text>}
<Box flexGrow={1} />
{indexStatus === 'building' ? <Text dimColor>indexing </Text> : indexStatus ? <Text dimColor>indexed in {indexStatus.ms}ms </Text> : count === 0 && query ? <Text color="error">no matches </Text> : 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.
<Text dimColor>
{current}/{count}
{' '}
</Text> : null}
</Box>;
}
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).
@@ -1127,7 +1127,7 @@ export function REPL({
// session from mid-conversation context.
const haikuTitleAttemptedRef = useRef((initialMessages?.length ?? 0) > 0);
const agentTitle = mainThreadAgentDefinition?.agentType;
const terminalTitle = sessionTitle ?? agentTitle ?? haikuTitle ?? 'Claude Code';
const terminalTitle = sessionTitle ?? agentTitle ?? haikuTitle ?? 'Open Claude';
const isWaitingForApproval = toolUseConfirmQueue.length > 0 || promptQueue.length > 0 || pendingWorkerRequest || pendingSandboxRequest;
// Local-jsx commands (like /plugin, /config) show user-facing dialogs that
// wait for input. Require jsx != null — if the flag is stuck true but jsx
@@ -1246,8 +1246,8 @@ export function REPL({
const cursorNavRef = useRef<MessageActionsNav | null>(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: <>
<Text color="warning">sandbox disabled</Text>
<Text dimColor> · /sandbox</Text>
</>,
<Text color="warning">sandbox disabled</Text>
<Text dimColor> · /sandbox</Text>
</>,
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: <Text color="error">
Failed to resume agent: {errorMessage(err)}
</Text>,
Failed to resume agent: {errorMessage(err)}
</Text>,
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(<ExitFlow showWorktree onDone={() => {}} onCancel={() => {
setExitFlow(<ExitFlow showWorktree onDone={() => { }} 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' ? <>
<Text dimColor>new task? </Text>
<Text color="suggestion">/clear</Text>
<Text dimColor> to save </Text>
<Text color="suggestion">{formattedTokens} tokens</Text>
</> : <Text color="warning">
new task? /clear to save {formattedTokens} tokens
</Text>,
<Text dimColor>new task? </Text>
<Text color="suggestion">/clear</Text>
<Text dimColor> to save </Text>
<Text color="suggestion">{formattedTokens} tokens</Text>
</> : <Text color="warning">
new task? /clear to save {formattedTokens} tokens
</Text>,
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 = <Messages messages={transcriptMessages} tools={tools} commands={commands} verbose={true} toolJSX={null} toolUseConfirmQueue={[]} inProgressToolUseIDs={inProgressToolUseIDs} isMessageSelectorVisible={false} conversationId={conversationId} screen={screen} agentDefinitions={agentDefinitions} streamingToolUses={transcriptStreamingToolUses} showAllInTranscript={showAllInTranscript} onOpenRateLimitOptions={handleOpenRateLimitOptions} isLoading={isLoading} hidePastThinking={true} streamingThinking={streamingThinking} scrollRef={transcriptScrollRef} jumpRef={jumpRef} onSearchMatchesChange={onSearchMatchesChange} scanElement={scanElement} setPositions={setPositions} disableRenderCap={dumpMode} />;
const transcriptToolJSX = toolJSX && <Box flexDirection="column" width="100%">
{toolJSX.jsx}
</Box>;
{toolJSX.jsx}
</Box>;
const transcriptReturn = <KeybindingSetup>
<AnimatedTerminalTitle isAnimating={titleIsAnimating} title={terminalTitle} disabled={titleDisabled} noPrefix={showStatusInTerminalTab} />
<GlobalKeybindingHandlers {...globalKeybindingProps} />
{feature('VOICE_MODE') ? <VoiceKeybindingHandler voiceHandleKeyEvent={voice.handleKeyEvent} stripTrailing={voice.stripTrailing} resetAnchor={voice.resetAnchor} isActive={!toolJSX?.isLocalJSXCommand} /> : null}
<CommandKeybindingHandlers onSubmit={onSubmit} isActive={!toolJSX?.isLocalJSXCommand} />
{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.
<ScrollKeybindingHandler scrollRef={scrollRef}
// Yield wheel/ctrl+u/d to UltraplanChoiceDialog's own scroll
// handler while the modal is showing.
isActive={focusedInputDialog !== 'ultraplan-choice'}
// g/G/j/k/ctrl+u/ctrl+d would eat keystrokes the search bar
// wants. Off while searching.
isModal={!searchOpen}
// Manual scroll exits the search context — clear the yellow
// current-match marker. Positions are (msg, rowOffset)-keyed;
// j/k changes scrollTop so rowOffset is stale → wrong row
// gets yellow. Next n/N re-establishes via step()→jump().
onScroll={() => jumpRef.current?.disarmSearch()} /> : null}
<CancelRequestHandler {...cancelRequestProps} />
{transcriptScrollRef ? <FullscreenLayout scrollRef={scrollRef} scrollable={<>
{transcriptMessagesElement}
{transcriptToolJSX}
<SandboxViolationExpandedView />
</>} bottom={searchOpen ? <TranscriptSearchBar jumpRef={jumpRef}
// Seed was tried (c01578c8) — broke /hello muscle
// memory (cursor lands after 'foo', /hello → foohello).
// Cancel-restore handles the 'don't lose prior search'
// concern differently (onCancel re-applies searchQuery).
initialQuery="" count={searchCount} current={searchCurrent} onClose={q => {
// 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);
<AnimatedTerminalTitle isAnimating={titleIsAnimating} title={terminalTitle} disabled={titleDisabled} noPrefix={showStatusInTerminalTab} />
<GlobalKeybindingHandlers {...globalKeybindingProps} />
{feature('VOICE_MODE') ? <VoiceKeybindingHandler voiceHandleKeyEvent={voice.handleKeyEvent} stripTrailing={voice.stripTrailing} resetAnchor={voice.resetAnchor} isActive={!toolJSX?.isLocalJSXCommand} /> : null}
<CommandKeybindingHandlers onSubmit={onSubmit} isActive={!toolJSX?.isLocalJSXCommand} />
{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.
<ScrollKeybindingHandler scrollRef={scrollRef}
// Yield wheel/ctrl+u/d to UltraplanChoiceDialog's own scroll
// handler while the modal is showing.
isActive={focusedInputDialog !== 'ultraplan-choice'}
// g/G/j/k/ctrl+u/ctrl+d would eat keystrokes the search bar
// wants. Off while searching.
isModal={!searchOpen}
// Manual scroll exits the search context — clear the yellow
// current-match marker. Positions are (msg, rowOffset)-keyed;
// j/k changes scrollTop so rowOffset is stale → wrong row
// gets yellow. Next n/N re-establishes via step()→jump().
onScroll={() => jumpRef.current?.disarmSearch()} /> : null}
<CancelRequestHandler {...cancelRequestProps} />
{transcriptScrollRef ? <FullscreenLayout scrollRef={scrollRef} scrollable={<>
{transcriptMessagesElement}
{transcriptToolJSX}
<SandboxViolationExpandedView />
</>} bottom={searchOpen ? <TranscriptSearchBar jumpRef={jumpRef}
// Seed was tried (c01578c8) — broke /hello muscle
// memory (cursor lands after 'foo', /hello → foohello).
// Cancel-restore handles the 'don't lose prior search'
// concern differently (onCancel re-applies searchQuery).
initialQuery="" count={searchCount} current={searchCurrent} onClose={q => {
// 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} /> : <TranscriptModeFooter showAllInTranscript={showAllInTranscript} virtualScroll={true} status={editorStatus || undefined} searchBadge={searchQuery && searchCount > 0 ? {
current: searchCurrent,
count: searchCount
} : undefined} />} /> : <>
{transcriptMessagesElement}
{transcriptToolJSX}
<SandboxViolationExpandedView />
<TranscriptModeFooter showAllInTranscript={showAllInTranscript} virtualScroll={false} suppressShowAll={dumpMode} status={editorStatus || undefined} />
</>}
</KeybindingSetup>;
jumpRef.current?.setSearchQuery(searchQuery);
setHighlight(searchQuery);
}} setHighlight={setHighlight} /> : <TranscriptModeFooter showAllInTranscript={showAllInTranscript} virtualScroll={true} status={editorStatus || undefined} searchBadge={searchQuery && searchCount > 0 ? {
current: searchCurrent,
count: searchCount
} : undefined} />} /> : <>
{transcriptMessagesElement}
{transcriptToolJSX}
<SandboxViolationExpandedView />
<TranscriptModeFooter showAllInTranscript={showAllInTranscript} virtualScroll={false} suppressShowAll={dumpMode} status={editorStatus || undefined} />
</>}
</KeybindingSetup>;
// The virtual-scroll branch (FullscreenLayout above) needs
// <AlternateScreen>'s <Box height={rows}> 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 <AlternateScreen mouseTracking={isMouseTrackingEnabled()}>
{transcriptReturn}
</AlternateScreen>;
{transcriptReturn}
</AlternateScreen>;
}
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 = <KeybindingSetup>
<AnimatedTerminalTitle isAnimating={titleIsAnimating} title={terminalTitle} disabled={titleDisabled} noPrefix={showStatusInTerminalTab} />
<GlobalKeybindingHandlers {...globalKeybindingProps} />
{feature('VOICE_MODE') ? <VoiceKeybindingHandler voiceHandleKeyEvent={voice.handleKeyEvent} stripTrailing={voice.stripTrailing} resetAnchor={voice.resetAnchor} isActive={!toolJSX?.isLocalJSXCommand} /> : null}
<CommandKeybindingHandlers onSubmit={onSubmit} isActive={!toolJSX?.isLocalJSXCommand} />
{/* ScrollKeybindingHandler must mount before CancelRequestHandler so
<AnimatedTerminalTitle isAnimating={titleIsAnimating} title={terminalTitle} disabled={titleDisabled} noPrefix={showStatusInTerminalTab} />
<GlobalKeybindingHandlers {...globalKeybindingProps} />
{feature('VOICE_MODE') ? <VoiceKeybindingHandler voiceHandleKeyEvent={voice.handleKeyEvent} stripTrailing={voice.stripTrailing} resetAnchor={voice.resetAnchor} isActive={!toolJSX?.isLocalJSXCommand} /> : null}
<CommandKeybindingHandlers onSubmit={onSubmit} isActive={!toolJSX?.isLocalJSXCommand} />
{/* 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. */}
<ScrollKeybindingHandler scrollRef={scrollRef} isActive={isFullscreenEnvEnabled() && (centeredModal != null || !focusedInputDialog || focusedInputDialog === 'tool-permission')} onScroll={centeredModal || toolPermissionOverlay || viewedAgentTask ? undefined : composedOnScroll} />
{feature('MESSAGE_ACTIONS') && isFullscreenEnvEnabled() && !disableMessageActions ? <MessageActionsKeybindings handlers={messageActionHandlers} isActive={cursor !== null} /> : null}
<CancelRequestHandler {...cancelRequestProps} />
<MCPConnectionManager key={remountKey} dynamicMcpConfig={dynamicMcpConfig} isStrictMcpConfig={strictMcpConfig}>
<FullscreenLayout scrollRef={scrollRef} overlay={toolPermissionOverlay} bottomFloat={feature('BUDDY') && companionVisible && !companionNarrow ? <CompanionFloatingBubble /> : undefined} modal={centeredModal} modalScrollRef={modalScrollRef} dividerYRef={dividerYRef} hidePill={!!viewedAgentTask} hideSticky={!!viewedTeammateTask} newMessageCount={unseenDivider?.count ?? 0} onPillClick={() => {
<ScrollKeybindingHandler scrollRef={scrollRef} isActive={isFullscreenEnvEnabled() && (centeredModal != null || !focusedInputDialog || focusedInputDialog === 'tool-permission')} onScroll={centeredModal || toolPermissionOverlay || viewedAgentTask ? undefined : composedOnScroll} />
{feature('MESSAGE_ACTIONS') && isFullscreenEnvEnabled() && !disableMessageActions ? <MessageActionsKeybindings handlers={messageActionHandlers} isActive={cursor !== null} /> : null}
<CancelRequestHandler {...cancelRequestProps} />
<MCPConnectionManager key={remountKey} dynamicMcpConfig={dynamicMcpConfig} isStrictMcpConfig={strictMcpConfig}>
<FullscreenLayout scrollRef={scrollRef} overlay={toolPermissionOverlay} bottomFloat={feature('BUDDY') && companionVisible && !companionNarrow ? <CompanionFloatingBubble /> : undefined} modal={centeredModal} modalScrollRef={modalScrollRef} dividerYRef={dividerYRef} hidePill={!!viewedAgentTask} hideSticky={!!viewedTeammateTask} newMessageCount={unseenDivider?.count ?? 0} onPillClick={() => {
setCursor(null);
jumpToNew(scrollRef.current);
}} scrollable={<>
<TeammateViewHeader />
<Messages messages={displayedMessages} tools={tools} commands={commands} verbose={verbose} toolJSX={toolJSX} toolUseConfirmQueue={toolUseConfirmQueue} inProgressToolUseIDs={viewedTeammateTask ? viewedTeammateTask.inProgressToolUseIDs ?? new Set() : inProgressToolUseIDs} isMessageSelectorVisible={isMessageSelectorVisible} conversationId={conversationId} screen={screen} streamingToolUses={streamingToolUses} showAllInTranscript={showAllInTranscript} agentDefinitions={agentDefinitions} onOpenRateLimitOptions={handleOpenRateLimitOptions} isLoading={isLoading} streamingText={isLoading && !viewedAgentTask ? visibleStreamingText : null} isBriefOnly={viewedAgentTask ? false : isBriefOnly} unseenDivider={viewedAgentTask ? undefined : unseenDivider} scrollRef={isFullscreenEnvEnabled() ? scrollRef : undefined} trackStickyPrompt={isFullscreenEnvEnabled() ? true : undefined} cursor={cursor} setCursor={setCursor} cursorNavRef={cursorNavRef} />
<AwsAuthStatusBox />
{/* Hide the processing placeholder while a modal is showing —
<TeammateViewHeader />
<Messages messages={displayedMessages} tools={tools} commands={commands} verbose={verbose} toolJSX={toolJSX} toolUseConfirmQueue={toolUseConfirmQueue} inProgressToolUseIDs={viewedTeammateTask ? viewedTeammateTask.inProgressToolUseIDs ?? new Set() : inProgressToolUseIDs} isMessageSelectorVisible={isMessageSelectorVisible} conversationId={conversationId} screen={screen} streamingToolUses={streamingToolUses} showAllInTranscript={showAllInTranscript} agentDefinitions={agentDefinitions} onOpenRateLimitOptions={handleOpenRateLimitOptions} isLoading={isLoading} streamingText={isLoading && !viewedAgentTask ? visibleStreamingText : null} isBriefOnly={viewedAgentTask ? false : isBriefOnly} unseenDivider={viewedAgentTask ? undefined : unseenDivider} scrollRef={isFullscreenEnvEnabled() ? scrollRef : undefined} trackStickyPrompt={isFullscreenEnvEnabled() ? true : undefined} cursor={cursor} setCursor={setCursor} cursorNavRef={cursorNavRef} />
<AwsAuthStatusBox />
{/* 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 && <UserTextMessage param={{
{!disabled && placeholderText && !centeredModal && <UserTextMessage param={{
text: placeholderText,
type: 'text'
}} addMargin={true} verbose={verbose} />}
{toolJSX && !(toolJSX.isLocalJSXCommand && toolJSX.isImmediate) && !toolJsxCentered && <Box flexDirection="column" width="100%">
{toolJSX.jsx}
</Box>}
{"external" === 'ant' && <TungstenLiveMonitor />}
{feature('WEB_BROWSER_TOOL') ? WebBrowserPanelModule && <WebBrowserPanelModule.WebBrowserPanel /> : null}
<Box flexGrow={1} />
{showSpinner && <SpinnerWithVerb mode={streamMode} spinnerTip={spinnerTip} responseLengthRef={responseLengthRef} apiMetricsRef={apiMetricsRef} overrideMessage={spinnerMessage} spinnerSuffix={stopHookSpinnerSuffix} verbose={verbose} loadingStartTimeRef={loadingStartTimeRef} totalPausedMsRef={totalPausedMsRef} pauseStartTimeRef={pauseStartTimeRef} overrideColor={spinnerColor} overrideShimmerColor={spinnerShimmerColor} hasActiveTools={inProgressToolUseIDs.size > 0} leaderIsIdle={!isLoading} />}
{!showSpinner && !isLoading && !userInputOnProcessing && !hasRunningTeammates && isBriefOnly && !viewedAgentTask && <BriefIdleStatus />}
{isFullscreenEnvEnabled() && <PromptInputQueuedCommands />}
</>} bottom={<Box flexDirection={feature('BUDDY') && companionNarrow ? 'column' : 'row'} width="100%" alignItems={feature('BUDDY') && companionNarrow ? undefined : 'flex-end'}>
{feature('BUDDY') && companionNarrow && isFullscreenEnvEnabled() && companionVisible ? <CompanionSprite /> : null}
<Box flexDirection="column" flexGrow={1}>
{permissionStickyFooter}
{/* Immediate local-jsx commands (/btw, /sandbox, /assistant,
{toolJSX && !(toolJSX.isLocalJSXCommand && toolJSX.isImmediate) && !toolJsxCentered && <Box flexDirection="column" width="100%">
{toolJSX.jsx}
</Box>}
{"external" === 'ant' && <TungstenLiveMonitor />}
{feature('WEB_BROWSER_TOOL') ? WebBrowserPanelModule && <WebBrowserPanelModule.WebBrowserPanel /> : null}
<Box flexGrow={1} />
{showSpinner && <SpinnerWithVerb mode={streamMode} spinnerTip={spinnerTip} responseLengthRef={responseLengthRef} apiMetricsRef={apiMetricsRef} overrideMessage={spinnerMessage} spinnerSuffix={stopHookSpinnerSuffix} verbose={verbose} loadingStartTimeRef={loadingStartTimeRef} totalPausedMsRef={totalPausedMsRef} pauseStartTimeRef={pauseStartTimeRef} overrideColor={spinnerColor} overrideShimmerColor={spinnerShimmerColor} hasActiveTools={inProgressToolUseIDs.size > 0} leaderIsIdle={!isLoading} />}
{!showSpinner && !isLoading && !userInputOnProcessing && !hasRunningTeammates && isBriefOnly && !viewedAgentTask && <BriefIdleStatus />}
{isFullscreenEnvEnabled() && <PromptInputQueuedCommands />}
</>} bottom={<Box flexDirection={feature('BUDDY') && companionNarrow ? 'column' : 'row'} width="100%" alignItems={feature('BUDDY') && companionNarrow ? undefined : 'flex-end'}>
{feature('BUDDY') && companionNarrow && isFullscreenEnvEnabled() && companionVisible ? <CompanionSprite /> : null}
<Box flexDirection="column" flexGrow={1}>
{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 && <Box flexDirection="column" width="100%">
{toolJSX.jsx}
</Box>}
{!showSpinner && !toolJSX?.isLocalJSXCommand && showExpandedTodos && tasksV2 && tasksV2.length > 0 && <Box width="100%" flexDirection="column">
<TaskListV2 tasks={tasksV2} isStandalone={true} />
</Box>}
{focusedInputDialog === 'sandbox-permission' && <SandboxPermissionRequest key={sandboxPermissionRequestQueue[0]!.hostPattern.host} hostPattern={sandboxPermissionRequestQueue[0]!.hostPattern} onUserResponse={(response: {
{toolJSX?.isLocalJSXCommand && toolJSX.isImmediate && !toolJsxCentered && <Box flexDirection="column" width="100%">
{toolJSX.jsx}
</Box>}
{!showSpinner && !toolJSX?.isLocalJSXCommand && showExpandedTodos && tasksV2 && tasksV2.length > 0 && <Box width="100%" flexDirection="column">
<TaskListV2 tasks={tasksV2} isStandalone={true} />
</Box>}
{focusedInputDialog === 'sandbox-permission' && <SandboxPermissionRequest key={sandboxPermissionRequestQueue[0]!.hostPattern.host} hostPattern={sandboxPermissionRequestQueue[0]!.hostPattern} onUserResponse={(response: {
allow: boolean;
persistToSettings: boolean;
}) => {
@@ -4650,7 +4650,7 @@ export function REPL({
sandboxBridgeCleanupRef.current.delete(approvedHost);
}
}} />}
{focusedInputDialog === 'prompt' && <PromptDialog key={promptQueue[0]!.request.prompt} title={promptQueue[0]!.title} toolInputSummary={promptQueue[0]!.toolInputSummary} request={promptQueue[0]!.request} onRespond={selectedKey => {
{focusedInputDialog === 'prompt' && <PromptDialog key={promptQueue[0]!.request.prompt} title={promptQueue[0]!.title} toolInputSummary={promptQueue[0]!.toolInputSummary} request={promptQueue[0]!.request} onRespond={selectedKey => {
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 && <WorkerPendingPermission toolName={pendingWorkerRequest.toolName} description={pendingWorkerRequest.description} />}
{/* Show pending indicator for sandbox permission on worker side */}
{pendingSandboxRequest && <WorkerPendingPermission toolName="Network Access" description={`Waiting for leader to approve network access to ${pendingSandboxRequest.host}`} />}
{/* Worker sandbox permission requests from swarm workers */}
{focusedInputDialog === 'worker-sandbox-permission' && <SandboxPermissionRequest key={workerSandboxPermissions.queue[0]!.requestId} hostPattern={{
{/* Show pending indicator on worker while waiting for leader approval */}
{pendingWorkerRequest && <WorkerPendingPermission toolName={pendingWorkerRequest.toolName} description={pendingWorkerRequest.description} />}
{/* Show pending indicator for sandbox permission on worker side */}
{pendingSandboxRequest && <WorkerPendingPermission toolName="Network Access" description={`Waiting for leader to approve network access to ${pendingSandboxRequest.host}`} />}
{/* Worker sandbox permission requests from swarm workers */}
{focusedInputDialog === 'worker-sandbox-permission' && <SandboxPermissionRequest key={workerSandboxPermissions.queue[0]!.requestId} hostPattern={{
host: workerSandboxPermissions.queue[0]!.host,
port: undefined
} as NetworkHostPattern} onUserResponse={(response: {
@@ -4713,7 +4713,7 @@ export function REPL({
}
}));
}} />}
{focusedInputDialog === 'elicitation' && <ElicitationDialog key={elicitation.queue[0]!.serverName + ':' + String(elicitation.queue[0]!.requestId)} event={elicitation.queue[0]!} onResponse={(action, content) => {
{focusedInputDialog === 'elicitation' && <ElicitationDialog key={elicitation.queue[0]!.serverName + ':' + String(elicitation.queue[0]!.requestId)} event={elicitation.queue[0]!} onResponse={(action, content) => {
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' && <CostThresholdDialog onDone={() => {
{focusedInputDialog === 'cost' && <CostThresholdDialog onDone={() => {
setShowCostDialog(false);
setHaveShownCostDialog(true);
saveGlobalConfig(current => ({
@@ -4751,7 +4751,7 @@ export function REPL({
}));
logEvent('tengu_cost_threshold_acknowledged', {});
}} />}
{focusedInputDialog === 'idle-return' && idleReturnPending && <IdleReturnDialog idleMinutes={idleReturnPending.idleMinutes} totalInputTokens={getTotalInputTokens()} onDone={async action => {
{focusedInputDialog === 'idle-return' && idleReturnPending && <IdleReturnDialog idleMinutes={idleReturnPending.idleMinutes} totalInputTokens={getTotalInputTokens()} onDone={async action => {
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' && <IdeOnboardingDialog onDone={() => setShowIdeOnboarding(false)} installationStatus={ideInstallationStatus} />}
{"external" === 'ant' && focusedInputDialog === 'model-switch' && AntModelSwitchCallout && <AntModelSwitchCallout onDone={(selection: string, modelAlias?: string) => {
{focusedInputDialog === 'ide-onboarding' && <IdeOnboardingDialog onDone={() => setShowIdeOnboarding(false)} installationStatus={ideInstallationStatus} />}
{"external" === 'ant' && focusedInputDialog === 'model-switch' && AntModelSwitchCallout && <AntModelSwitchCallout onDone={(selection: string, modelAlias?: string) => {
setShowModelSwitchCallout(false);
if (selection === 'switch' && modelAlias) {
setAppState(prev => ({
@@ -4809,8 +4809,8 @@ export function REPL({
}));
}
}} />}
{"external" === 'ant' && focusedInputDialog === 'undercover-callout' && UndercoverAutoCallout && <UndercoverAutoCallout onDone={() => setShowUndercoverCallout(false)} />}
{focusedInputDialog === 'effort-callout' && <EffortCallout model={mainLoopModel} onDone={selection => {
{"external" === 'ant' && focusedInputDialog === 'undercover-callout' && UndercoverAutoCallout && <UndercoverAutoCallout onDone={() => setShowUndercoverCallout(false)} />}
{focusedInputDialog === 'effort-callout' && <EffortCallout model={mainLoopModel} onDone={selection => {
setShowEffortCallout(false);
if (selection !== 'dismiss') {
setAppState(prev => ({
@@ -4819,7 +4819,7 @@ export function REPL({
}));
}
}} />}
{focusedInputDialog === 'remote-callout' && <RemoteCallout onDone={selection => {
{focusedInputDialog === 'remote-callout' && <RemoteCallout onDone={selection => {
setAppState(prev => {
if (!prev.showRemoteCallout) return prev;
return {
@@ -4834,17 +4834,17 @@ export function REPL({
});
}} />}
{exitFlow}
{exitFlow}
{focusedInputDialog === 'plugin-hint' && hintRecommendation && <PluginHintMenu pluginName={hintRecommendation.pluginName} pluginDescription={hintRecommendation.pluginDescription} marketplaceName={hintRecommendation.marketplaceName} sourceCommand={hintRecommendation.sourceCommand} onResponse={handleHintResponse} />}
{focusedInputDialog === 'plugin-hint' && hintRecommendation && <PluginHintMenu pluginName={hintRecommendation.pluginName} pluginDescription={hintRecommendation.pluginDescription} marketplaceName={hintRecommendation.marketplaceName} sourceCommand={hintRecommendation.sourceCommand} onResponse={handleHintResponse} />}
{focusedInputDialog === 'lsp-recommendation' && lspRecommendation && <LspRecommendationMenu pluginName={lspRecommendation.pluginName} pluginDescription={lspRecommendation.pluginDescription} fileExtension={lspRecommendation.fileExtension} onResponse={handleLspResponse} />}
{focusedInputDialog === 'lsp-recommendation' && lspRecommendation && <LspRecommendationMenu pluginName={lspRecommendation.pluginName} pluginDescription={lspRecommendation.pluginDescription} fileExtension={lspRecommendation.fileExtension} onResponse={handleLspResponse} />}
{focusedInputDialog === 'desktop-upsell' && <DesktopUpsellStartup onDone={() => setShowDesktopUpsellStartup(false)} />}
{focusedInputDialog === 'desktop-upsell' && <DesktopUpsellStartup onDone={() => setShowDesktopUpsellStartup(false)} />}
{feature('ULTRAPLAN') ? focusedInputDialog === 'ultraplan-choice' && ultraplanPendingChoice && <UltraplanChoiceDialog plan={ultraplanPendingChoice.plan} sessionId={ultraplanPendingChoice.sessionId} taskId={ultraplanPendingChoice.taskId} setMessages={setMessages} readFileState={readFileState.current} getAppState={() => store.getState()} setConversationId={setConversationId} /> : null}
{feature('ULTRAPLAN') ? focusedInputDialog === 'ultraplan-choice' && ultraplanPendingChoice && <UltraplanChoiceDialog plan={ultraplanPendingChoice.plan} sessionId={ultraplanPendingChoice.sessionId} taskId={ultraplanPendingChoice.taskId} setMessages={setMessages} readFileState={readFileState.current} getAppState={() => store.getState()} setConversationId={setConversationId} /> : null}
{feature('ULTRAPLAN') ? focusedInputDialog === 'ultraplan-launch' && ultraplanLaunchPending && <UltraplanLaunchDialog onChoice={(choice, opts) => {
{feature('ULTRAPLAN') ? focusedInputDialog === 'ultraplan-launch' && ultraplanLaunchPending && <UltraplanLaunchDialog onChoice={(choice, opts) => {
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 && <AutoRunIssueNotification onRun={handleAutoRunIssue} onCancel={handleCancelAutoRunIssue} reason={getAutoRunIssueReasonText(autoRunIssueReason)} />}
{postCompactSurvey.state !== 'closed' ? <FeedbackSurvey state={postCompactSurvey.state} lastResponse={postCompactSurvey.lastResponse} handleSelect={postCompactSurvey.handleSelect} inputValue={inputValue} setInputValue={setInputValue} onRequestFeedback={handleSurveyRequestFeedback} /> : memorySurvey.state !== 'closed' ? <FeedbackSurvey state={memorySurvey.state} lastResponse={memorySurvey.lastResponse} handleSelect={memorySurvey.handleSelect} handleTranscriptSelect={memorySurvey.handleTranscriptSelect} inputValue={inputValue} setInputValue={setInputValue} onRequestFeedback={handleSurveyRequestFeedback} message="How well did Claude use its memory? (optional)" /> : <FeedbackSurvey state={feedbackSurvey.state} lastResponse={feedbackSurvey.lastResponse} handleSelect={feedbackSurvey.handleSelect} handleTranscriptSelect={feedbackSurvey.handleTranscriptSelect} inputValue={inputValue} setInputValue={setInputValue} onRequestFeedback={didAutoRunIssueRef.current ? undefined : handleSurveyRequestFeedback} />}
{/* Frustration-triggered transcript sharing prompt */}
{frustrationDetection.state !== 'closed' && <FeedbackSurvey state={frustrationDetection.state} lastResponse={null} handleSelect={() => {}} handleTranscriptSelect={frustrationDetection.handleTranscriptSelect} inputValue={inputValue} setInputValue={setInputValue} />}
{/* Skill improvement survey - appears when improvements detected (ant-only) */}
{"external" === 'ant' && skillImprovementSurvey.suggestion && <SkillImprovementSurvey isOpen={skillImprovementSurvey.isOpen} skillName={skillImprovementSurvey.suggestion.skillName} updates={skillImprovementSurvey.suggestion.updates} handleSelect={skillImprovementSurvey.handleSelect} inputValue={inputValue} setInputValue={setInputValue} />}
{showIssueFlagBanner && <IssueFlagBanner />}
{}
<PromptInput debug={debug} ideSelection={ideSelection} hasSuppressedDialogs={!!hasSuppressedDialogs} isLocalJSXCommandActive={isShowingLocalJSXCommand} getToolUseContext={getToolUseContext} toolPermissionContext={toolPermissionContext} setToolPermissionContext={setToolPermissionContext} apiKeyStatus={apiKeyStatus} commands={commands} agents={agentDefinitions.activeAgents} isLoading={isLoading} onExit={handleExit} verbose={verbose} messages={messages} onAutoUpdaterResult={setAutoUpdaterResult} autoUpdaterResult={autoUpdaterResult} input={inputValue} onInputChange={setInputValue} mode={inputMode} onModeChange={setInputMode} stashedPrompt={stashedPrompt} setStashedPrompt={setStashedPrompt} submitCount={submitCount} onShowMessageSelector={handleShowMessageSelector} onMessageActionsEnter={
// Works during isLoading — edit cancels first; uuid selection survives appends.
feature('MESSAGE_ACTIONS') && isFullscreenEnvEnabled() && !disableMessageActions ? enterMessageActions : undefined} mcpClients={mcpClients} pastedContents={pastedContents} setPastedContents={setPastedContents} vimMode={vimMode} setVimMode={setVimMode} showBashesDialog={showBashesDialog} setShowBashesDialog={setShowBashesDialog} onSubmit={onSubmit} onAgentSubmit={onAgentSubmit} isSearchingHistory={isSearchingHistory} setIsSearchingHistory={setIsSearchingHistory} helpOpen={isHelpOpen} setHelpOpen={setIsHelpOpen} insertTextRef={feature('VOICE_MODE') ? insertTextRef : undefined} voiceInterimRange={voice.interimRange} />
<SessionBackgroundHint onBackgroundSession={handleBackgroundSession} isLoading={isLoading} />
</>}
{cursor &&
// inputValue is REPL state; typed text survives the round-trip.
<MessageActionsBar cursor={cursor} />}
{focusedInputDialog === 'message-selector' && <MessageSelector messages={messages} preselectedMessage={messageSelectorPreselect} onPreRestore={onCancel} onRestoreCode={async (message: UserMessage) => {
{!toolJSX?.shouldHidePromptInput && !focusedInputDialog && !isExiting && !disabled && !cursor && <>
{autoRunIssueReason && <AutoRunIssueNotification onRun={handleAutoRunIssue} onCancel={handleCancelAutoRunIssue} reason={getAutoRunIssueReasonText(autoRunIssueReason)} />}
{postCompactSurvey.state !== 'closed' ? <FeedbackSurvey state={postCompactSurvey.state} lastResponse={postCompactSurvey.lastResponse} handleSelect={postCompactSurvey.handleSelect} inputValue={inputValue} setInputValue={setInputValue} onRequestFeedback={handleSurveyRequestFeedback} /> : memorySurvey.state !== 'closed' ? <FeedbackSurvey state={memorySurvey.state} lastResponse={memorySurvey.lastResponse} handleSelect={memorySurvey.handleSelect} handleTranscriptSelect={memorySurvey.handleTranscriptSelect} inputValue={inputValue} setInputValue={setInputValue} onRequestFeedback={handleSurveyRequestFeedback} message="How well did Claude use its memory? (optional)" /> : <FeedbackSurvey state={feedbackSurvey.state} lastResponse={feedbackSurvey.lastResponse} handleSelect={feedbackSurvey.handleSelect} handleTranscriptSelect={feedbackSurvey.handleTranscriptSelect} inputValue={inputValue} setInputValue={setInputValue} onRequestFeedback={didAutoRunIssueRef.current ? undefined : handleSurveyRequestFeedback} />}
{/* Frustration-triggered transcript sharing prompt */}
{frustrationDetection.state !== 'closed' && <FeedbackSurvey state={frustrationDetection.state} lastResponse={null} handleSelect={() => { }} handleTranscriptSelect={frustrationDetection.handleTranscriptSelect} inputValue={inputValue} setInputValue={setInputValue} />}
{/* Skill improvement survey - appears when improvements detected (ant-only) */}
{"external" === 'ant' && skillImprovementSurvey.suggestion && <SkillImprovementSurvey isOpen={skillImprovementSurvey.isOpen} skillName={skillImprovementSurvey.suggestion.skillName} updates={skillImprovementSurvey.suggestion.updates} handleSelect={skillImprovementSurvey.handleSelect} inputValue={inputValue} setInputValue={setInputValue} />}
{showIssueFlagBanner && <IssueFlagBanner />}
{ }
<PromptInput debug={debug} ideSelection={ideSelection} hasSuppressedDialogs={!!hasSuppressedDialogs} isLocalJSXCommandActive={isShowingLocalJSXCommand} getToolUseContext={getToolUseContext} toolPermissionContext={toolPermissionContext} setToolPermissionContext={setToolPermissionContext} apiKeyStatus={apiKeyStatus} commands={commands} agents={agentDefinitions.activeAgents} isLoading={isLoading} onExit={handleExit} verbose={verbose} messages={messages} onAutoUpdaterResult={setAutoUpdaterResult} autoUpdaterResult={autoUpdaterResult} input={inputValue} onInputChange={setInputValue} mode={inputMode} onModeChange={setInputMode} stashedPrompt={stashedPrompt} setStashedPrompt={setStashedPrompt} submitCount={submitCount} onShowMessageSelector={handleShowMessageSelector} onMessageActionsEnter={
// Works during isLoading — edit cancels first; uuid selection survives appends.
feature('MESSAGE_ACTIONS') && isFullscreenEnvEnabled() && !disableMessageActions ? enterMessageActions : undefined} mcpClients={mcpClients} pastedContents={pastedContents} setPastedContents={setPastedContents} vimMode={vimMode} setVimMode={setVimMode} showBashesDialog={showBashesDialog} setShowBashesDialog={setShowBashesDialog} onSubmit={onSubmit} onAgentSubmit={onAgentSubmit} isSearchingHistory={isSearchingHistory} setIsSearchingHistory={setIsSearchingHistory} helpOpen={isHelpOpen} setHelpOpen={setIsHelpOpen} insertTextRef={feature('VOICE_MODE') ? insertTextRef : undefined} voiceInterimRange={voice.interimRange} />
<SessionBackgroundHint onBackgroundSession={handleBackgroundSession} isLoading={isLoading} />
</>}
{cursor &&
// inputValue is REPL state; typed text survives the round-trip.
<MessageActionsBar cursor={cursor} />}
{focusedInputDialog === 'message-selector' && <MessageSelector messages={messages} preselectedMessage={messageSelectorPreselect} onPreRestore={onCancel} onRestoreCode={async (message: UserMessage) => {
await fileHistoryRewind((updater: (prev: FileHistoryState) => FileHistoryState) => {
setAppState(prev => ({
...prev,
@@ -4985,16 +4985,16 @@ export function REPL({
setIsMessageSelectorVisible(false);
setMessageSelectorPreselect(undefined);
}} />}
{"external" === 'ant' && <DevBar />}
</Box>
{feature('BUDDY') && !(companionNarrow && isFullscreenEnvEnabled()) && companionVisible ? <CompanionSprite /> : null}
</Box>} />
</MCPConnectionManager>
</KeybindingSetup>;
{"external" === 'ant' && <DevBar />}
</Box>
{feature('BUDDY') && !(companionNarrow && isFullscreenEnvEnabled()) && companionVisible ? <CompanionSprite /> : null}
</Box>} />
</MCPConnectionManager>
</KeybindingSetup>;
if (isFullscreenEnvEnabled()) {
return <AlternateScreen mouseTracking={isMouseTrackingEnabled()}>
{mainReturn}
</AlternateScreen>;
{mainReturn}
</AlternateScreen>;
}
return mainReturn;
}

View File

@@ -116,8 +116,8 @@ import { getLoggingSafeMcpBaseUrl } from './utils.js'
/* eslint-disable @typescript-eslint/no-require-imports */
const fetchMcpSkillsForClient = feature('MCP_SKILLS')
? (
require('../../skills/mcpSkills.js') as typeof import('../../skills/mcpSkills.js')
).fetchMcpSkillsForClient
require('../../skills/mcpSkills.js') as typeof import('../../skills/mcpSkills.js')
).fetchMcpSkillsForClient
: null
import { UnauthorizedError } from '@modelcontextprotocol/sdk/client/auth.js'
@@ -240,12 +240,12 @@ const claudeInChromeToolRendering =
// GrowthBook tengu_malort_pedway (see gates.ts).
const computerUseWrapper = feature('CHICAGO_MCP')
? (): typeof import('../../utils/computerUse/wrapper.js') =>
require('../../utils/computerUse/wrapper.js')
require('../../utils/computerUse/wrapper.js')
: undefined
const isComputerUseMCPServer = feature('CHICAGO_MCP')
? (
require('../../utils/computerUse/common.js') as typeof import('../../utils/computerUse/common.js')
).isComputerUseMCPServer
require('../../utils/computerUse/common.js') as typeof import('../../utils/computerUse/common.js')
).isComputerUseMCPServer
: undefined
import { mkdir, readFile, unlink, writeFile } from 'fs/promises'
@@ -326,9 +326,9 @@ function mcpBaseUrlAnalytics(serverRef: ScopedMcpServerConfig): {
const url = getLoggingSafeMcpBaseUrl(serverRef)
return url
? {
mcpServerBaseUrl:
url as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
}
mcpServerBaseUrl:
url as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
}
: {}
}
@@ -683,20 +683,20 @@ export const connectToServer = memoize(
const transportOptions: SSEClientTransportOptions =
proxyOptions.dispatcher
? {
eventSourceInit: {
fetch: async (url: string | URL, init?: RequestInit) => {
// eslint-disable-next-line eslint-plugin-n/no-unsupported-features/node-builtins
return fetch(url, {
...init,
...proxyOptions,
headers: {
'User-Agent': getMCPUserAgent(),
...init?.headers,
},
})
},
eventSourceInit: {
fetch: async (url: string | URL, init?: RequestInit) => {
// eslint-disable-next-line eslint-plugin-n/no-unsupported-features/node-builtins
return fetch(url, {
...init,
...proxyOptions,
headers: {
'User-Agent': getMCPUserAgent(),
...init?.headers,
},
})
},
}
},
}
: {}
transport = new SSEClientTransport(
@@ -832,8 +832,8 @@ export const connectToServer = memoize(
'User-Agent': getMCPUserAgent(),
...(sessionIngressToken &&
!hasOAuthTokens && {
Authorization: `Bearer ${sessionIngressToken}`,
}),
Authorization: `Bearer ${sessionIngressToken}`,
}),
...combinedHeaders,
},
},
@@ -842,10 +842,10 @@ export const connectToServer = memoize(
// Redact sensitive headers before logging
const headersForLogging = transportOptions.requestInit?.headers
? mapValues(
transportOptions.requestInit.headers as Record<string, string>,
(value, key) =>
key.toLowerCase() === 'authorization' ? '[REDACTED]' : value,
)
transportOptions.requestInit.headers as Record<string, string>,
(value, key) =>
key.toLowerCase() === 'authorization' ? '[REDACTED]' : value,
)
: undefined
logMCPDebug(
@@ -985,7 +985,7 @@ export const connectToServer = memoize(
const client = new Client(
{
name: 'claude-code',
title: 'Claude Code',
title: 'Open Claude',
version: MACRO.VERSION ?? 'unknown',
description: "Anthropic's agentic coding tool",
websiteUrl: PRODUCT_URL,
@@ -1054,9 +1054,9 @@ export const connectToServer = memoize(
`Connection timeout triggered after ${elapsed}ms (limit: ${getConnectionTimeoutMs()}ms)`,
)
if (inProcessServer) {
inProcessServer.close().catch(() => {})
inProcessServer.close().catch(() => { })
}
transport.close().catch(() => {})
transport.close().catch(() => { })
reject(
new TelemetrySafeError_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS(
`MCP server "${name}" connection timed out after ${getConnectionTimeoutMs()}ms`,
@@ -1145,9 +1145,9 @@ export const connectToServer = memoize(
})
}
if (inProcessServer) {
inProcessServer.close().catch(() => {})
inProcessServer.close().catch(() => { })
}
transport.close().catch(() => {})
transport.close().catch(() => { })
if (stderrOutput) {
logMCPError(name, `Server stderr: ${stderrOutput}`)
}
@@ -1627,7 +1627,7 @@ export const connectToServer = memoize(
logMCPError(name, `Connection failed: ${errorMessage(error)}`)
if (inProcessServer) {
inProcessServer.close().catch(() => {})
inProcessServer.close().catch(() => { })
}
return {
name,
@@ -1779,8 +1779,8 @@ export const fetchToolsForClient = memoizeWithLRU(
searchHint:
typeof tool._meta?.['anthropic/searchHint'] === 'string'
? tool._meta['anthropic/searchHint']
.replace(/\s+/g, ' ')
.trim() || undefined
.replace(/\s+/g, ' ')
.trim() || undefined
: undefined,
alwaysLoad: tool._meta?.['anthropic/alwaysLoad'] === true,
async description() {
@@ -1871,11 +1871,11 @@ export const fetchToolsForClient = memoizeWithLRU(
onProgress:
onProgress && toolUseId
? progressData => {
onProgress({
toolUseID: toolUseId,
data: progressData,
})
}
onProgress({
toolUseID: toolUseId,
data: progressData,
})
}
: undefined,
handleElicitation: context.handleElicitation,
})
@@ -1975,14 +1975,14 @@ export const fetchToolsForClient = memoizeWithLRU(
return `${client.name} - ${displayName} (MCP)`
},
...(isClaudeInChromeMCPServer(client.name) &&
(client.config.type === 'stdio' || !client.config.type)
(client.config.type === 'stdio' || !client.config.type)
? claudeInChromeToolRendering().getClaudeInChromeMCPToolOverrides(
tool.name,
)
tool.name,
)
: {}),
...(feature('CHICAGO_MCP') &&
(client.config.type === 'stdio' || !client.config.type) &&
isComputerUseMCPServer!(client.name)
(client.config.type === 'stdio' || !client.config.type) &&
isComputerUseMCPServer!(client.name)
? computerUseWrapper!().getComputerUseMCPToolOverrides(tool.name)
: {}),
}
@@ -2876,9 +2876,9 @@ export async function callMCPToolWithUrlElicitationRetry({
const errorData = error.data
const rawElicitations =
errorData != null &&
typeof errorData === 'object' &&
'elicitations' in errorData &&
Array.isArray(errorData.elicitations)
typeof errorData === 'object' &&
'elicitations' in errorData &&
Array.isArray(errorData.elicitations)
? (errorData.elicitations as unknown[])
: []
@@ -3101,16 +3101,16 @@ async function callMCPTool({
timeout: timeoutMs,
onprogress: onProgress
? sdkProgress => {
onProgress({
type: 'mcp_progress',
status: 'progress',
serverName: name,
toolName: tool,
progress: sdkProgress.progress,
total: sdkProgress.total,
progressMessage: sdkProgress.message,
})
}
onProgress({
type: 'mcp_progress',
status: 'progress',
serverName: name,
toolName: tool,
progress: sdkProgress.progress,
total: sdkProgress.total,
progressMessage: sdkProgress.message,
})
}
: undefined,
},
),
@@ -3280,7 +3280,7 @@ export async function setupSdkMcpClients(
const client = new Client(
{
name: 'claude-code',
title: 'Claude Code',
title: 'Open Claude',
version: MACRO.VERSION ?? 'unknown',
description: "Anthropic's agentic coding tool",
websiteUrl: PRODUCT_URL,

View File

@@ -35,7 +35,7 @@ export async function sendNotification(
})
}
const DEFAULT_TITLE = 'Claude Code'
const DEFAULT_TITLE = 'Open Claude'
async function sendToChannel(
channel: string,

View File

@@ -124,6 +124,10 @@ export async function generateSessionTitle(
level: 'error',
})
logEvent('tengu_session_title_generated', { success: false })
return null
// Fallback: When using 3P providers without a compatible schema,
// default to the application name.
return 'Open Claude'
}
}