Merge pull request #1261 from QwenLM/fix/vscode-ide-companion-opt-task-stop

fix(vscode-ide-companion): Optimize stream termination handling and fix style layering issues
This commit is contained in:
tanzhenxin
2025-12-19 15:15:04 +08:00
committed by GitHub
6 changed files with 89 additions and 38 deletions

View File

@@ -35,7 +35,7 @@ function npmBin() {
function run(cmd, args, opts = {}) { function run(cmd, args, opts = {}) {
const res = spawnSync(cmd, args, { const res = spawnSync(cmd, args, {
stdio: 'inherit', stdio: 'inherit',
shell: process.platform === 'win32' ? true : false, shell: process.platform === 'win32',
...opts, ...opts,
}); });
if (res.error) { if (res.error) {

View File

@@ -54,27 +54,31 @@ export class AcpSessionManager {
}; };
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
// different timeout durations based on methods // No timeout for session_prompt as LLM tasks can take 5-10 minutes or longer
let timeoutDuration = 60000; // default 60 seconds // The request should always terminate with a stop_reason
if ( let timeoutId: NodeJS.Timeout | undefined;
method === AGENT_METHODS.session_prompt || let timeoutDuration: number | undefined;
method === AGENT_METHODS.initialize
) {
timeoutDuration = 120000; // 2min for session_prompt and initialize
}
const timeoutId = setTimeout(() => { if (method !== AGENT_METHODS.session_prompt) {
// Set timeout for other methods
timeoutDuration = method === AGENT_METHODS.initialize ? 120000 : 60000;
timeoutId = setTimeout(() => {
pendingRequests.delete(id); pendingRequests.delete(id);
reject(new Error(`Request ${method} timed out`)); reject(new Error(`Request ${method} timed out`));
}, timeoutDuration); }, timeoutDuration);
}
const pendingRequest: PendingRequest<T> = { const pendingRequest: PendingRequest<T> = {
resolve: (value: T) => { resolve: (value: T) => {
if (timeoutId) {
clearTimeout(timeoutId); clearTimeout(timeoutId);
}
resolve(value); resolve(value);
}, },
reject: (error: Error) => { reject: (error: Error) => {
if (timeoutId) {
clearTimeout(timeoutId); clearTimeout(timeoutId);
}
reject(error); reject(error);
}, },
timeoutId, timeoutId,

View File

@@ -144,10 +144,7 @@ export const InputForm: React.FC<InputFormProps> = ({
: ''; : '';
return ( return (
<div <div className="p-1 px-4 pb-4 absolute bottom-0 left-0 right-0 bg-gradient-to-b from-transparent to-[var(--app-primary-background)]">
className="p-1 px-4 pb-4 absolute bottom-0 left-0 right-0"
style={{ backgroundColor: 'var(--app-primary-background)' }}
>
<div className="block"> <div className="block">
<form className="composer-form" onSubmit={onSubmit}> <form className="composer-form" onSubmit={onSubmit}>
{/* Inner background layer */} {/* Inner background layer */}

View File

@@ -152,6 +152,24 @@ export class SessionMessageHandler extends BaseMessageHandler {
this.currentStreamContent = ''; 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. * Prompt user to login and invoke the registered login handler/command.
* Returns true if a login was initiated. * Returns true if a login was initiated.
@@ -373,10 +391,7 @@ export class SessionMessageHandler extends BaseMessageHandler {
); );
} }
this.sendToWebView({ this.sendStreamEnd();
type: 'streamEnd',
data: { timestamp: Date.now() },
});
} catch (error) { } catch (error) {
console.error('[SessionMessageHandler] Error sending message:', error); console.error('[SessionMessageHandler] Error sending message:', error);
@@ -398,10 +413,7 @@ export class SessionMessageHandler extends BaseMessageHandler {
if (isAbortLike) { if (isAbortLike) {
// Do not show VS Code error popup for intentional cancellations. // Do not show VS Code error popup for intentional cancellations.
// Ensure the webview knows the stream ended due to user action. // Ensure the webview knows the stream ended due to user action.
this.sendToWebView({ this.sendStreamEnd('user_cancelled');
type: 'streamEnd',
data: { timestamp: Date.now(), reason: 'user_cancelled' },
});
return; return;
} }
// Check for session not found error and handle it appropriately // Check for session not found error and handle it appropriately
@@ -423,12 +435,39 @@ export class SessionMessageHandler extends BaseMessageHandler {
type: 'sessionExpired', type: 'sessionExpired',
data: { message: 'Session expired. Please login again.' }, data: { message: 'Session expired. Please login again.' },
}); });
this.sendStreamEnd('session_expired');
} else { } else {
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}`); vscode.window.showErrorMessage(`Error sending message: ${error}`);
this.sendToWebView({ this.sendToWebView({
type: 'error', type: 'error',
data: { message: errorMsg }, data: { message: errorMsg },
}); });
this.sendStreamEnd('error');
}
} }
} }
} }

View File

@@ -15,6 +15,14 @@ import type { ToolCallUpdate } from '../../types/chatTypes.js';
import type { ApprovalModeValue } from '../../types/approvalModeValueTypes.js'; import type { ApprovalModeValue } from '../../types/approvalModeValueTypes.js';
import type { PlanEntry } from '../../types/chatTypes.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 { interface UseWebViewMessagesProps {
// Session management // Session management
sessionManagement: { sessionManagement: {
@@ -364,12 +372,12 @@ export const useWebViewMessages = ({
).toLowerCase(); ).toLowerCase();
/** /**
* Handle different types of stream end reasons: * Handle different types of stream end reasons that require a full reset:
* - 'user_cancelled': User explicitly cancelled operation * - 'user_cancelled' / 'cancelled': user explicitly cancelled
* - 'cancelled': General cancellation * - 'timeout' / 'error' / 'session_expired': request failed unexpectedly
* For these cases, immediately clear all active states * 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 // Clear active execution tool call tracking, reset state
activeExecToolCallsRef.current.clear(); activeExecToolCallsRef.current.clear();
// Clear waiting response state to ensure UI returns to normal // Clear waiting response state to ensure UI returns to normal
@@ -393,6 +401,9 @@ export const useWebViewMessages = ({
} }
case 'error': case 'error':
handlers.messageHandling.endStreaming();
handlers.messageHandling.clearThinking();
activeExecToolCallsRef.current.clear();
handlers.messageHandling.clearWaitingForResponse(); handlers.messageHandling.clearWaitingForResponse();
break; break;

View File

@@ -43,7 +43,7 @@
/* Composer: form wrapper */ /* Composer: form wrapper */
.composer-form { .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); background: var(--app-input-secondary-background);
border-color: var(--app-input-border); border-color: var(--app-input-border);
color: var(--app-input-foreground); color: var(--app-input-foreground);