From bca288e742723e2e3938994f544a16bf6baf60a7 Mon Sep 17 00:00:00 2001 From: yiliang114 <1204183885@qq.com> Date: Sat, 13 Dec 2025 16:28:58 +0800 Subject: [PATCH] wip(vscode-ide-companion): OnboardingPage --- .../src/cli/cliDetector.ts | 57 +++++++++----- .../src/cli/cliVersionChecker.ts | 78 +++++++++++++++++++ .../src/services/acpConnection.ts | 4 +- .../src/services/acpMessageHandler.ts | 10 --- .../src/services/qwenAgentManager.ts | 40 ++-------- .../src/services/qwenConnectionHandler.ts | 11 +-- .../src/utils/authErrors.ts | 35 +++++++-- .../vscode-ide-companion/src/webview/App.tsx | 4 +- .../src/webview/WebViewProvider.ts | 30 ++++++- .../webview/components/layout/InputForm.tsx | 5 +- .../{OnboardingPage.tsx => Onboarding.tsx} | 40 +++------- .../src/webview/hooks/useWebViewMessages.ts | 59 +++++++++----- 12 files changed, 235 insertions(+), 138 deletions(-) create mode 100644 packages/vscode-ide-companion/src/cli/cliVersionChecker.ts rename packages/vscode-ide-companion/src/webview/components/layout/{OnboardingPage.tsx => Onboarding.tsx} (51%) diff --git a/packages/vscode-ide-companion/src/cli/cliDetector.ts b/packages/vscode-ide-companion/src/cli/cliDetector.ts index c1de868a..c59389d8 100644 --- a/packages/vscode-ide-companion/src/cli/cliDetector.ts +++ b/packages/vscode-ide-companion/src/cli/cliDetector.ts @@ -25,17 +25,36 @@ export class CliDetector { private static readonly CACHE_DURATION_MS = 30000; // 30 seconds /** - * Lightweight check if the Qwen Code CLI is installed - * This version only checks for CLI existence without getting version info for faster performance - * @param forceRefresh - Force a new check, ignoring cache - * @returns Detection result with installation status and path + * Lightweight CLI Detection Method + * + * This method is designed for performance optimization, checking only if the CLI exists + * without retrieving version information. + * Suitable for quick detection scenarios, such as pre-checks before initializing connections. + * + * Compared to the full detectQwenCli method, this method: + * - Omits version information retrieval step + * - Uses shorter timeout (3 seconds) + * - Faster response time + * + * @param forceRefresh - Whether to force refresh cached results, default is false + * @returns Promise - Detection result containing installation status and path + * + * @example + * ```typescript + * const result = await CliDetector.detectQwenCliLightweight(); + * if (result.isInstalled) { + * console.log('CLI installed at:', result.cliPath); + * } else { + * console.log('CLI not found:', result.error); + * } + * ``` */ static async detectQwenCliLightweight( forceRefresh = false, ): Promise { const now = Date.now(); - // Return cached result if available and not expired + // Check if cached result is available and not expired (30-second validity) if ( !forceRefresh && this.cachedResult && @@ -56,7 +75,7 @@ export class CliDetector { // Check if qwen command exists try { - // Use simpler detection without NVM for speed + // Use simplified detection without NVM for speed const detectionCommand = isWindows ? `${whichCommand} qwen` : `${whichCommand} qwen`; @@ -66,32 +85,34 @@ export class CliDetector { detectionCommand, ); + // Execute command to detect CLI path, set shorter timeout (3 seconds) const { stdout } = await execAsync(detectionCommand, { timeout: 3000, // Reduced timeout for faster detection shell: isWindows ? undefined : '/bin/bash', }); - // The output may contain multiple lines - // We want the first line which should be the actual path + // Output may contain multiple lines, get first line as actual path const lines = stdout .trim() .split('\n') .filter((line) => line.trim()); - const cliPath = lines[0]; // Just take the first path + const cliPath = lines[0]; // Take only the first path console.log('[CliDetector] Found CLI at:', cliPath); + // Build successful detection result, note no version information this.cachedResult = { isInstalled: true, cliPath, - // Version is not retrieved in lightweight detection + // Version information not retrieved in lightweight detection }; this.lastCheckTime = now; return this.cachedResult; } catch (detectionError) { console.log('[CliDetector] CLI not found, error:', detectionError); - // CLI not found - let error = `Qwen Code CLI not found in PATH. Please install it using: npm install -g @qwen-code/qwen-code@latest`; + + // CLI not found, build error message + let error = `Qwen Code CLI not found in PATH. Please install using: npm install -g @qwen-code/qwen-code@latest`; // Provide specific guidance for permission errors if (detectionError instanceof Error) { @@ -100,11 +121,11 @@ export class CliDetector { errorMessage.includes('EACCES') || errorMessage.includes('Permission denied') ) { - error += `\n\nThis may be due to permission issues. Possible solutions: - \n1. Reinstall the CLI without sudo: npm install -g @qwen-code/qwen-code@latest + error += `\n\nThis may be due to permission issues. Solutions: + \n1. Reinstall CLI without sudo: npm install -g @qwen-code/qwen-code@latest \n2. If previously installed with sudo, fix ownership: sudo chown -R $(whoami) $(npm config get prefix)/lib/node_modules/@qwen-code/qwen-code \n3. Use nvm for Node.js version management to avoid permission issues - \n4. Check your PATH environment variable includes npm's global bin directory`; + \n4. Check PATH environment variable includes npm's global bin directory`; } } @@ -127,11 +148,11 @@ export class CliDetector { errorMessage.includes('EACCES') || errorMessage.includes('Permission denied') ) { - userFriendlyError += `\n\nThis may be due to permission issues. Possible solutions: - \n1. Reinstall the CLI without sudo: npm install -g @qwen-code/qwen-code@latest + userFriendlyError += `\n\nThis may be due to permission issues. Solutions: + \n1. Reinstall CLI without sudo: npm install -g @qwen-code/qwen-code@latest \n2. If previously installed with sudo, fix ownership: sudo chown -R $(whoami) $(npm config get prefix)/lib/node_modules/@qwen-code/qwen-code \n3. Use nvm for Node.js version management to avoid permission issues - \n4. Check your PATH environment variable includes npm's global bin directory`; + \n4. Check PATH environment variable includes npm's global bin directory`; } this.cachedResult = { diff --git a/packages/vscode-ide-companion/src/cli/cliVersionChecker.ts b/packages/vscode-ide-companion/src/cli/cliVersionChecker.ts new file mode 100644 index 00000000..3a9db333 --- /dev/null +++ b/packages/vscode-ide-companion/src/cli/cliVersionChecker.ts @@ -0,0 +1,78 @@ +/** + * @license + * Copyright 2025 Qwen Team + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as vscode from 'vscode'; +import { CliContextManager } from './cliContextManager.js'; +import { CliVersionManager } from './cliVersionManager.js'; +import { MIN_CLI_VERSION_FOR_SESSION_METHODS } from './cliVersionManager.js'; +import type { CliVersionInfo } from './cliVersionManager.js'; + +/** + * Check CLI version and show warning if below minimum requirement + * + * @returns Version information + */ +export async function checkCliVersionAndWarn(): Promise { + try { + const cliContextManager = CliContextManager.getInstance(); + const versionInfo = + await CliVersionManager.getInstance().detectCliVersion(true); + cliContextManager.setCurrentVersionInfo(versionInfo); + + if (!versionInfo.isSupported) { + vscode.window.showWarningMessage( + `Qwen Code CLI version ${versionInfo.version} is below the minimum required version. Some features may not work properly. Please upgrade to version ${MIN_CLI_VERSION_FOR_SESSION_METHODS} or later.`, + ); + } + } catch (error) { + console.error('[CliVersionChecker] Failed to check CLI version:', error); + } +} + +/** + * Process server version information from initialize response + * + * @param init - Initialize response object + */ +export function processServerVersion(init: unknown): void { + try { + const obj = (init || {}) as Record; + + // Extract version information from initialize response + const serverVersion = + obj['version'] || obj['serverVersion'] || obj['cliVersion']; + if (serverVersion) { + console.log( + '[CliVersionChecker] Server version from initialize response:', + serverVersion, + ); + + // Update CLI context with version info from server + const cliContextManager = CliContextManager.getInstance(); + + // Create version info directly without async call + const versionInfo: CliVersionInfo = { + version: String(serverVersion), + isSupported: true, // Assume supported for now + features: { + supportsSessionList: true, + supportsSessionLoad: true, + }, + detectionResult: { + isInstalled: true, + version: String(serverVersion), + }, + }; + + cliContextManager.setCurrentVersionInfo(versionInfo); + } + } catch (error) { + console.error( + '[CliVersionChecker] Failed to process server version:', + error, + ); + } +} diff --git a/packages/vscode-ide-companion/src/services/acpConnection.ts b/packages/vscode-ide-companion/src/services/acpConnection.ts index f999c602..f4c95948 100644 --- a/packages/vscode-ide-companion/src/services/acpConnection.ts +++ b/packages/vscode-ide-companion/src/services/acpConnection.ts @@ -42,9 +42,7 @@ export class AcpConnection { onPermissionRequest: (data: AcpPermissionRequest) => Promise<{ optionId: string; }> = () => Promise.resolve({ optionId: 'allow' }); - onEndTurn: (reason?: string) => void = (reason?: string | undefined) => { - console.log('[ACP] onEndTurn__________ reason:', reason || 'unknown'); - }; + onEndTurn: () => void = () => {}; // Called after successful initialize() with the initialize result onInitialized: (init: unknown) => void = () => {}; diff --git a/packages/vscode-ide-companion/src/services/acpMessageHandler.ts b/packages/vscode-ide-companion/src/services/acpMessageHandler.ts index e07cedfb..7e4a0ef0 100644 --- a/packages/vscode-ide-companion/src/services/acpMessageHandler.ts +++ b/packages/vscode-ide-companion/src/services/acpMessageHandler.ts @@ -111,16 +111,6 @@ export class AcpMessageHandler { message.result, ); - console.log( - '[ACP] Response for message.result:', - message.result, - message.result && - typeof message.result === 'object' && - 'stopReason' in message.result, - - !!callbacks.onEndTurn, - ); - if (message.result && typeof message.result === 'object') { const stopReasonValue = (message.result as { stopReason?: unknown }).stopReason ?? diff --git a/packages/vscode-ide-companion/src/services/qwenAgentManager.ts b/packages/vscode-ide-companion/src/services/qwenAgentManager.ts index 3c701331..a8bb61fe 100644 --- a/packages/vscode-ide-companion/src/services/qwenAgentManager.ts +++ b/packages/vscode-ide-companion/src/services/qwenAgentManager.ts @@ -24,10 +24,8 @@ import { import { QwenSessionUpdateHandler } from './qwenSessionUpdateHandler.js'; import { CliContextManager } from '../cli/cliContextManager.js'; import { authMethod } from '../types/acpTypes.js'; -import { - MIN_CLI_VERSION_FOR_SESSION_METHODS, - type CliVersionInfo, -} from '../cli/cliVersionManager.js'; +import { MIN_CLI_VERSION_FOR_SESSION_METHODS } from '../cli/cliVersionManager.js'; +import { processServerVersion } from '../cli/cliVersionChecker.js'; import { isAuthenticationRequiredError } from '../utils/authErrors.js'; export type { ChatMessage, PlanEntry, ToolCallUpdateData }; @@ -149,37 +147,10 @@ export class QwenAgentManager { // Initialize callback to surface available modes and current mode to UI this.connection.onInitialized = (init: unknown) => { try { + // Process server version information + processServerVersion(init); + const obj = (init || {}) as Record; - - // Extract version information from initialize response - const serverVersion = - obj['version'] || obj['serverVersion'] || obj['cliVersion']; - if (serverVersion) { - console.log( - '[QwenAgentManager] Server version from initialize response:', - serverVersion, - ); - - // Update CLI context with version info from server - const cliContextManager = CliContextManager.getInstance(); - - // Create version info directly without async call - const versionInfo: CliVersionInfo = { - version: String(serverVersion), - isSupported: true, // Assume supported for now - features: { - supportsSessionList: true, - supportsSessionLoad: true, - }, - detectionResult: { - isInstalled: true, - version: String(serverVersion), - }, - }; - - cliContextManager.setCurrentVersionInfo(versionInfo); - } - const modes = obj['modes'] as | { currentModeId?: 'plan' | 'default' | 'auto-edit' | 'yolo'; @@ -1369,7 +1340,6 @@ export class QwenAgentManager { * @param callback - Called when ACP stopReason is reported */ onEndTurn(callback: (reason?: string) => void): void { - console.log('[QwenAgentManager] onEndTurn__________ callback:', callback); this.callbacks.onEndTurn = callback; this.sessionUpdateHandler.updateCallbacks(this.callbacks); } diff --git a/packages/vscode-ide-companion/src/services/qwenConnectionHandler.ts b/packages/vscode-ide-companion/src/services/qwenConnectionHandler.ts index e35ce64c..2fbdd406 100644 --- a/packages/vscode-ide-companion/src/services/qwenConnectionHandler.ts +++ b/packages/vscode-ide-companion/src/services/qwenConnectionHandler.ts @@ -10,12 +10,12 @@ * Handles Qwen Agent connection establishment, authentication, and session creation */ -// import * as vscode from 'vscode'; import type { AcpConnection } from './acpConnection.js'; import type { QwenSessionReader } from '../services/qwenSessionReader.js'; import { CliDetector } from '../cli/cliDetector.js'; import { authMethod } from '../types/acpTypes.js'; import { isAuthenticationRequiredError } from '../utils/authErrors.js'; +import { checkCliVersionAndWarn } from '../cli/cliVersionChecker.js'; export interface QwenConnectionResult { sessionCreated: boolean; @@ -59,19 +59,12 @@ export class QwenConnectionHandler { } console.log('[QwenAgentManager] CLI detected at:', detectionResult.cliPath); - // TODO: @yiliang114. closed temporarily // Show warning if CLI version is below minimum requirement - // if (!versionInfo.isSupported) { - // // Wait to determine release version number - // vscode.window.showWarningMessage( - // `Qwen Code CLI version ${versionInfo.version} is below the minimum required version. Some features may not work properly. Please upgrade to version ${MIN_CLI_VERSION_FOR_SESSION_METHODS} or later.`, - // ); - // } + await checkCliVersionAndWarn(); // Build extra CLI arguments (only essential parameters) const extraArgs: string[] = []; - // TODO: await connection.connect(cliPath!, workingDir, extraArgs); // Try to restore existing session or create new session diff --git a/packages/vscode-ide-companion/src/utils/authErrors.ts b/packages/vscode-ide-companion/src/utils/authErrors.ts index 4b7de266..d598d8cc 100644 --- a/packages/vscode-ide-companion/src/utils/authErrors.ts +++ b/packages/vscode-ide-companion/src/utils/authErrors.ts @@ -4,23 +4,48 @@ * SPDX-License-Identifier: Apache-2.0 */ +/** + * Authentication Error Utility + * + * Used to uniformly identify and handle various authentication-related error messages. + * Determines if re-authentication is needed by matching predefined error patterns. + * + * @param error - The error object or string to check + * @returns true if it's an authentication-related error, false otherwise + */ const AUTH_ERROR_PATTERNS = [ - 'Authentication required', - '(code: -32000)', - 'Unauthorized', - 'Invalid token', - 'Session expired', + 'Authentication required', // Standard authentication request message + '(code: -32000)', // RPC error code -32000 indicates authentication failure + 'Unauthorized', // HTTP unauthorized error + 'Invalid token', // Invalid token + 'Session expired', // Session expired ]; +/** + * Determines if the given error is authentication-related + * + * This function detects various forms of authentication errors, including: + * - Direct error objects + * - String-form error messages + * - Other types of errors converted to strings for pattern matching + * + * @param error - The error object to check, can be an Error instance, string, or other type + * @returns boolean - true if the error is authentication-related, false otherwise + */ export const isAuthenticationRequiredError = (error: unknown): boolean => { + // Null check to avoid unnecessary processing if (!error) { return false; } + + // Extract error message text const message = error instanceof Error ? error.message : typeof error === 'string' ? error : String(error); + + // Match authentication-related errors using predefined patterns return AUTH_ERROR_PATTERNS.some((pattern) => message.includes(pattern)); }; diff --git a/packages/vscode-ide-companion/src/webview/App.tsx b/packages/vscode-ide-companion/src/webview/App.tsx index d083740c..ca9b86b4 100644 --- a/packages/vscode-ide-companion/src/webview/App.tsx +++ b/packages/vscode-ide-companion/src/webview/App.tsx @@ -29,7 +29,7 @@ import { PermissionDrawer } from './components/PermissionDrawer/PermissionDrawer import { ToolCall } from './components/messages/toolcalls/ToolCall.js'; import { hasToolCallOutput } from './components/messages/toolcalls/shared/utils.js'; import { EmptyState } from './components/layout/EmptyState.js'; -import { OnboardingPage } from './components/layout/OnboardingPage.js'; +import { Onboarding } from './components/layout/Onboarding.js'; import { type CompletionItem } from '../types/completionItemTypes.js'; import { useCompletionTrigger } from './hooks/useCompletionTrigger.js'; import { ChatHeader } from './components/layout/ChatHeader.js'; @@ -670,7 +670,7 @@ export const App: React.FC = () => { > {!hasContent ? ( isAuthenticated === false ? ( - { vscode.postMessage({ type: 'login', data: {} }); messageHandling.setWaitingForResponse( diff --git a/packages/vscode-ide-companion/src/webview/WebViewProvider.ts b/packages/vscode-ide-companion/src/webview/WebViewProvider.ts index 34e6d24f..fa9506f1 100644 --- a/packages/vscode-ide-companion/src/webview/WebViewProvider.ts +++ b/packages/vscode-ide-companion/src/webview/WebViewProvider.ts @@ -17,6 +17,19 @@ import { getFileName } from './utils/webviewUtils.js'; import { type ApprovalModeValue } from '../types/acpTypes.js'; import { isAuthenticationRequiredError } from '../utils/authErrors.js'; +/** + * WebView Provider Class + * + * Manages the WebView panel lifecycle, agent connection, and message handling. + * Acts as the central coordinator between VS Code extension and WebView UI. + * + * Key responsibilities: + * - WebView panel creation and management + * - Qwen agent connection and session management + * - Message routing between extension and WebView + * - Authentication state handling + * - Permission request processing + */ export class WebViewProvider { private panelManager: PanelManager; private messageHandler: MessageHandler; @@ -122,7 +135,6 @@ export class WebViewProvider { // Setup end-turn handler from ACP stopReason notifications this.agentManager.onEndTurn((reason) => { - console.log(' ============== ', reason); // Ensure WebView exits streaming state even if no explicit streamEnd was emitted elsewhere this.sendMessageToWebView({ type: 'streamEnd', @@ -521,6 +533,12 @@ export class WebViewProvider { /** * Attempt to restore authentication state and initialize connection * This is called when the webview is first shown + * + * This method tries to establish a connection without forcing authentication, + * allowing detection of existing authentication state. If connection fails, + * initializes an empty conversation to allow browsing history. + * + * @returns Promise - Resolves when auth state restoration attempt is complete */ private async attemptAuthStateRestoration(): Promise { try { @@ -550,6 +568,16 @@ export class WebViewProvider { /** * Internal: perform actual connection/initialization (no auth locking). + * + * This method handles the complete agent connection and initialization workflow: + * 1. Detects if Qwen CLI is installed + * 2. If CLI is not installed, prompts user for installation + * 3. If CLI is installed, attempts to connect to the agent + * 4. Handles authentication requirements and session creation + * 5. Notifies WebView of connection status + * + * @param options - Connection options including auto-authentication setting + * @returns Promise - Resolves when initialization is complete */ private async doInitializeAgentConnection(options?: { autoAuthenticate?: boolean; diff --git a/packages/vscode-ide-companion/src/webview/components/layout/InputForm.tsx b/packages/vscode-ide-companion/src/webview/components/layout/InputForm.tsx index 997c3f9a..1982b18f 100644 --- a/packages/vscode-ide-companion/src/webview/components/layout/InputForm.tsx +++ b/packages/vscode-ide-companion/src/webview/components/layout/InputForm.tsx @@ -172,7 +172,7 @@ export const InputForm: React.FC = ({
= ({ : 'false' } onInput={(e) => { - if (composerDisabled) { - return; - } const target = e.target as HTMLDivElement; // Filter out zero-width space that we use to maintain height const text = target.textContent?.replace(/\u200B/g, '') || ''; diff --git a/packages/vscode-ide-companion/src/webview/components/layout/OnboardingPage.tsx b/packages/vscode-ide-companion/src/webview/components/layout/Onboarding.tsx similarity index 51% rename from packages/vscode-ide-companion/src/webview/components/layout/OnboardingPage.tsx rename to packages/vscode-ide-companion/src/webview/components/layout/Onboarding.tsx index 945f86a4..5bafcfc3 100644 --- a/packages/vscode-ide-companion/src/webview/components/layout/OnboardingPage.tsx +++ b/packages/vscode-ide-companion/src/webview/components/layout/Onboarding.tsx @@ -1,23 +1,30 @@ -import type React from 'react'; +/** + * @license + * Copyright 2025 Qwen Team + * SPDX-License-Identifier: Apache-2.0 + */ + import { generateIconUrl } from '../../utils/resourceUrl.js'; interface OnboardingPageProps { onLogin: () => void; } -export const OnboardingPage: React.FC = ({ onLogin }) => { +export const Onboarding: React.FC = ({ onLogin }) => { const iconUri = generateIconUrl('icon.png'); return (
+ {/* Application icon container with brand logo and decorative close icon */}
Qwen Code Logo + {/* Decorative close icon for enhanced visual effect */}
= ({ onLogin }) => {
+ {/* Text content area */}

Welcome to Qwen Code @@ -40,40 +48,12 @@ export const OnboardingPage: React.FC = ({ onLogin }) => {

- {/*
-
-

Get Started

-
    -
  • -
    - Understand complex codebases faster -
  • -
  • -
    - Navigate with AI-powered suggestions -
  • -
  • -
    - Transform code with confidence -
  • -
-
- -
*/} - - - {/*
-

- By logging in, you agree to the Terms of Service and Privacy - Policy. -

-
*/}
diff --git a/packages/vscode-ide-companion/src/webview/hooks/useWebViewMessages.ts b/packages/vscode-ide-companion/src/webview/hooks/useWebViewMessages.ts index a227e470..dc9a33b7 100644 --- a/packages/vscode-ide-companion/src/webview/hooks/useWebViewMessages.ts +++ b/packages/vscode-ide-companion/src/webview/hooks/useWebViewMessages.ts @@ -329,7 +329,6 @@ export const useWebViewMessages = ({ } } } - console.log('[useWebViewMessages1111]__________ other message:', msg); break; } @@ -351,30 +350,42 @@ export const useWebViewMessages = ({ } case 'streamEnd': { - // Always end local streaming state and collapse any thoughts + // Always end local streaming state and clear thinking state handlers.messageHandling.endStreaming(); handlers.messageHandling.clearThinking(); - // If the stream ended due to explicit user cancel, proactively - // clear the waiting indicator and reset any tracked exec calls. - // This avoids the UI being stuck with the Stop button visible - // after rejecting a permission request. + // If stream ended due to explicit user cancellation, proactively clear + // waiting indicator and reset tracked execution calls. + // This avoids UI getting stuck with Stop button visible after + // rejecting a permission request. try { const reason = ( (message.data as { reason?: string } | undefined)?.reason || '' ).toLowerCase(); + + /** + * Handle different types of stream end reasons: + * - 'user_cancelled': User explicitly cancelled operation + * - 'cancelled': General cancellation + * For these cases, immediately clear all active states + */ if (reason === 'user_cancelled' || reason === 'cancelled') { + // Clear active execution tool call tracking, reset state activeExecToolCallsRef.current.clear(); + // Clear waiting response state to ensure UI returns to normal handlers.messageHandling.clearWaitingForResponse(); break; } } catch (_error) { - // best-effort + // Best-effort handling, errors don't affect main flow } - // Otherwise, clear the generic waiting indicator only if there are - // no active long-running tool calls. If there are still active - // execute/bash/command calls, keep the hint visible. + /** + * For other types of stream end (non-user cancellation): + * Only clear generic waiting indicator when there are no active + * long-running tool calls. If there are still active execute/bash/command + * calls, keep the hint visible. + */ if (activeExecToolCallsRef.current.size === 0) { handlers.messageHandling.clearWaitingForResponse(); } @@ -575,15 +586,21 @@ export const useWebViewMessages = ({ // While long-running tools (e.g., execute/bash/command) are in progress, // surface a lightweight loading indicator and expose the Stop button. try { + const id = (toolCallData.toolCallId || '').toString(); const kind = (toolCallData.kind || '').toString().toLowerCase(); - const isExec = + const isExecKind = kind === 'execute' || kind === 'bash' || kind === 'command'; + // CLI sometimes omits kind in tool_call_update payloads; fall back to + // whether we've already tracked this ID as an exec tool. + const wasTrackedExec = activeExecToolCallsRef.current.has(id); + const isExec = isExecKind || wasTrackedExec; - if (isExec) { - const id = (toolCallData.toolCallId || '').toString(); + if (!isExec || !id) { + break; + } - // Maintain the active set by status - if (status === 'pending' || status === 'in_progress') { + if (status === 'pending' || status === 'in_progress') { + if (isExecKind) { activeExecToolCallsRef.current.add(id); // Build a helpful hint from rawInput @@ -597,14 +614,14 @@ export const useWebViewMessages = ({ } const hint = cmd ? `Running: ${cmd}` : 'Running command...'; handlers.messageHandling.setWaitingForResponse(hint); - } else if (status === 'completed' || status === 'failed') { - activeExecToolCallsRef.current.delete(id); } + } else if (status === 'completed' || status === 'failed') { + activeExecToolCallsRef.current.delete(id); + } - // If no active exec tool remains, clear the waiting message. - if (activeExecToolCallsRef.current.size === 0) { - handlers.messageHandling.clearWaitingForResponse(); - } + // If no active exec tool remains, clear the waiting message. + if (activeExecToolCallsRef.current.size === 0) { + handlers.messageHandling.clearWaitingForResponse(); } } catch (_error) { // Best-effort UI hint; ignore errors