diff --git a/packages/vscode-ide-companion/scripts/prepackage.js b/packages/vscode-ide-companion/scripts/prepackage.js index 8db18a69..de76a335 100644 --- a/packages/vscode-ide-companion/scripts/prepackage.js +++ b/packages/vscode-ide-companion/scripts/prepackage.js @@ -35,7 +35,7 @@ function npmBin() { function run(cmd, args, opts = {}) { const res = spawnSync(cmd, args, { stdio: 'inherit', - shell: process.platform === 'win32' ? true : false, + shell: process.platform === 'win32', ...opts, }); if (res.error) { diff --git a/packages/vscode-ide-companion/src/services/acpSessionManager.ts b/packages/vscode-ide-companion/src/services/acpSessionManager.ts index cfa299bf..e2055a3a 100644 --- a/packages/vscode-ide-companion/src/services/acpSessionManager.ts +++ b/packages/vscode-ide-companion/src/services/acpSessionManager.ts @@ -54,27 +54,31 @@ export class AcpSessionManager { }; return new Promise((resolve, reject) => { - // different timeout durations based on methods - let timeoutDuration = 60000; // default 60 seconds - if ( - method === AGENT_METHODS.session_prompt || - method === AGENT_METHODS.initialize - ) { - timeoutDuration = 120000; // 2min for session_prompt and initialize - } + // No timeout for session_prompt as LLM tasks can take 5-10 minutes or longer + // The request should always terminate with a stop_reason + let timeoutId: NodeJS.Timeout | undefined; + let timeoutDuration: number | undefined; - const timeoutId = setTimeout(() => { - pendingRequests.delete(id); - reject(new Error(`Request ${method} timed out`)); - }, timeoutDuration); + if (method !== AGENT_METHODS.session_prompt) { + // Set timeout for other methods + timeoutDuration = method === AGENT_METHODS.initialize ? 120000 : 60000; + timeoutId = setTimeout(() => { + pendingRequests.delete(id); + reject(new Error(`Request ${method} timed out`)); + }, timeoutDuration); + } const pendingRequest: PendingRequest = { resolve: (value: T) => { - clearTimeout(timeoutId); + if (timeoutId) { + clearTimeout(timeoutId); + } resolve(value); }, reject: (error: Error) => { - clearTimeout(timeoutId); + if (timeoutId) { + clearTimeout(timeoutId); + } reject(error); }, timeoutId, 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 86ba42be..6bd3289a 100644 --- a/packages/vscode-ide-companion/src/webview/components/layout/InputForm.tsx +++ b/packages/vscode-ide-companion/src/webview/components/layout/InputForm.tsx @@ -144,10 +144,7 @@ export const InputForm: React.FC = ({ : ''; return ( -
+
{/* Inner background layer */} diff --git a/packages/vscode-ide-companion/src/webview/handlers/SessionMessageHandler.ts b/packages/vscode-ide-companion/src/webview/handlers/SessionMessageHandler.ts index 51dfbdd9..d8861b95 100644 --- a/packages/vscode-ide-companion/src/webview/handlers/SessionMessageHandler.ts +++ b/packages/vscode-ide-companion/src/webview/handlers/SessionMessageHandler.ts @@ -152,6 +152,24 @@ export class SessionMessageHandler extends BaseMessageHandler { this.currentStreamContent = ''; } + /** + * Notify the webview that streaming has finished. + */ + private sendStreamEnd(reason?: string): void { + const data: { timestamp: number; reason?: string } = { + timestamp: Date.now(), + }; + + if (reason) { + data.reason = reason; + } + + this.sendToWebView({ + type: 'streamEnd', + data, + }); + } + /** * Prompt user to login and invoke the registered login handler/command. * Returns true if a login was initiated. @@ -373,10 +391,7 @@ export class SessionMessageHandler extends BaseMessageHandler { ); } - this.sendToWebView({ - type: 'streamEnd', - data: { timestamp: Date.now() }, - }); + this.sendStreamEnd(); } catch (error) { console.error('[SessionMessageHandler] Error sending message:', error); @@ -398,10 +413,7 @@ export class SessionMessageHandler extends BaseMessageHandler { if (isAbortLike) { // Do not show VS Code error popup for intentional cancellations. // Ensure the webview knows the stream ended due to user action. - this.sendToWebView({ - type: 'streamEnd', - data: { timestamp: Date.now(), reason: 'user_cancelled' }, - }); + this.sendStreamEnd('user_cancelled'); return; } // Check for session not found error and handle it appropriately @@ -423,12 +435,39 @@ export class SessionMessageHandler extends BaseMessageHandler { type: 'sessionExpired', data: { message: 'Session expired. Please login again.' }, }); + this.sendStreamEnd('session_expired'); } else { - vscode.window.showErrorMessage(`Error sending message: ${error}`); - this.sendToWebView({ - type: 'error', - data: { message: errorMsg }, - }); + const isTimeoutError = + lower.includes('timeout') || lower.includes('timed out'); + if (isTimeoutError) { + // Note: session_prompt no longer has a timeout, so this should rarely occur + // This path may still be hit for other methods (initialize, etc.) or network-level timeouts + console.warn( + '[SessionMessageHandler] Request timed out; suppressing popup', + ); + + const timeoutMessage: ChatMessage = { + role: 'assistant', + content: + 'Request timed out. This may be due to a network issue. Please try again.', + timestamp: Date.now(), + }; + + // Send a timeout message to the WebView + this.sendToWebView({ + type: 'message', + data: timeoutMessage, + }); + this.sendStreamEnd('timeout'); + } else { + // Handling of Non-Timeout Errors + vscode.window.showErrorMessage(`Error sending message: ${error}`); + this.sendToWebView({ + type: 'error', + data: { message: errorMsg }, + }); + this.sendStreamEnd('error'); + } } } } diff --git a/packages/vscode-ide-companion/src/webview/hooks/useWebViewMessages.ts b/packages/vscode-ide-companion/src/webview/hooks/useWebViewMessages.ts index c8d507f2..8336825c 100644 --- a/packages/vscode-ide-companion/src/webview/hooks/useWebViewMessages.ts +++ b/packages/vscode-ide-companion/src/webview/hooks/useWebViewMessages.ts @@ -15,6 +15,14 @@ import type { ToolCallUpdate } from '../../types/chatTypes.js'; import type { ApprovalModeValue } from '../../types/approvalModeValueTypes.js'; import type { PlanEntry } from '../../types/chatTypes.js'; +const FORCE_CLEAR_STREAM_END_REASONS = new Set([ + 'user_cancelled', + 'cancelled', + 'timeout', + 'error', + 'session_expired', +]); + interface UseWebViewMessagesProps { // Session management sessionManagement: { @@ -364,12 +372,12 @@ export const useWebViewMessages = ({ ).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 + * Handle different types of stream end reasons that require a full reset: + * - 'user_cancelled' / 'cancelled': user explicitly cancelled + * - 'timeout' / 'error' / 'session_expired': request failed unexpectedly + * For these cases, immediately clear all active states. */ - if (reason === 'user_cancelled' || reason === 'cancelled') { + if (FORCE_CLEAR_STREAM_END_REASONS.has(reason)) { // Clear active execution tool call tracking, reset state activeExecToolCallsRef.current.clear(); // Clear waiting response state to ensure UI returns to normal @@ -393,6 +401,9 @@ export const useWebViewMessages = ({ } case 'error': + handlers.messageHandling.endStreaming(); + handlers.messageHandling.clearThinking(); + activeExecToolCallsRef.current.clear(); handlers.messageHandling.clearWaitingForResponse(); break; diff --git a/packages/vscode-ide-companion/src/webview/styles/tailwind.css b/packages/vscode-ide-companion/src/webview/styles/tailwind.css index 46d803d5..a48c172f 100644 --- a/packages/vscode-ide-companion/src/webview/styles/tailwind.css +++ b/packages/vscode-ide-companion/src/webview/styles/tailwind.css @@ -43,7 +43,7 @@ /* Composer: form wrapper */ .composer-form { - @apply relative flex flex-col max-w-[680px] mx-auto rounded-large border shadow-sm transition-colors duration-200; + @apply relative flex flex-col max-w-[680px] mx-auto rounded-large border shadow-sm transition-colors duration-200 z-[1]; background: var(--app-input-secondary-background); border-color: var(--app-input-border); color: var(--app-input-foreground);