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