diff --git a/scripts/system-check.ts b/scripts/system-check.ts index e129685a..2e12da5a 100644 --- a/scripts/system-check.ts +++ b/scripts/system-check.ts @@ -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() diff --git a/src/screens/REPL.tsx b/src/screens/REPL.tsx index 9fdd3b11..65df5ca4 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). @@ -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(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; } diff --git a/src/services/mcp/client.ts b/src/services/mcp/client.ts index 0b3afc6a..b053dbb6 100644 --- a/src/services/mcp/client.ts +++ b/src/services/mcp/client.ts @@ -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, - (value, key) => - key.toLowerCase() === 'authorization' ? '[REDACTED]' : value, - ) + transportOptions.requestInit.headers as Record, + (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, diff --git a/src/services/notifier.ts b/src/services/notifier.ts index 330e16a0..b2136a4e 100644 --- a/src/services/notifier.ts +++ b/src/services/notifier.ts @@ -35,7 +35,7 @@ export async function sendNotification( }) } -const DEFAULT_TITLE = 'Claude Code' +const DEFAULT_TITLE = 'Open Claude' async function sendToChannel( channel: string, diff --git a/src/utils/sessionTitle.ts b/src/utils/sessionTitle.ts index 5a722c88..141833b4 100644 --- a/src/utils/sessionTitle.ts +++ b/src/utils/sessionTitle.ts @@ -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' } } +