mirror of
https://github.com/microsoft/vscode.git
synced 2026-07-16 17:03:29 +01:00
Recover from terminal hangs when shell integration breaks or the pty exits (#313312)
This commit is contained in:
+17
@@ -243,6 +243,23 @@ export async function trackIdleOnPrompt(
|
||||
scheduler.schedule();
|
||||
}
|
||||
}, 30_000));
|
||||
// Hard wall-clock safety net for the case where shell integration never
|
||||
// engages at all — no OSC `C`/`D` is ever parsed so state never advances
|
||||
// to Executing, and yet the existing data-idle fallbacks somehow fail to
|
||||
// fire (e.g. because data arrives in a single event before listeners are
|
||||
// active, or because `onData` throttling masks idle periods). Without
|
||||
// this, trackIdleOnPrompt can block the rich strategy for the lifetime
|
||||
// of the chat request. Genuinely long-running commands with working
|
||||
// shell integration reach Executing/PromptAfterExecuting before this
|
||||
// fires, so this is purely a fallback for the broken-handshake path.
|
||||
const hardCapScheduler = store.add(new RunOnceScheduler(() => {
|
||||
if (state === TerminalState.Initial || state === TerminalState.Prompt) {
|
||||
log?.(`Hard cap fired after 60s in state ${stateNames[state]} (dataEvents=${dataEventCount})`);
|
||||
setState(TerminalState.PromptAfterExecuting, 'hardCap');
|
||||
scheduler.schedule();
|
||||
}
|
||||
}, 60_000));
|
||||
hardCapScheduler.schedule();
|
||||
// Only schedule when a prompt sequence (A) is seen after an execute sequence (C). This prevents
|
||||
// cases where the command is executed before the prompt is written. While not perfect, sitting
|
||||
// on an A without a C following shortly after is a very good indicator that the command is done
|
||||
|
||||
+26
-1
@@ -2282,9 +2282,34 @@ export class RunInTerminalTool extends Disposable implements IToolImpl {
|
||||
}));
|
||||
|
||||
// Clean up all background resources when the terminal is disposed
|
||||
// (e.g. user closes the terminal) to avoid leaking listeners and monitors.
|
||||
// (e.g. user closes the terminal). Send a completion notification so
|
||||
// the agent isn't left waiting for an `onCommandFinished` event that
|
||||
// will never fire — the pty exited before shell integration could
|
||||
// emit the end marker. Output captured here is whatever was buffered
|
||||
// up until disposal.
|
||||
// Capture the execution reference now — by the time onDisposed fires,
|
||||
// onDidDisposeInstance listeners may have already removed it from
|
||||
// _activeExecutions.
|
||||
const executionForDisposal = RunInTerminalTool._activeExecutions.get(termId);
|
||||
store.add(terminalInstance.onDisposed(() => {
|
||||
if (handleSessionCancelled()) {
|
||||
return;
|
||||
}
|
||||
const currentOutput = executionForDisposal?.getOutput() ?? '';
|
||||
const exitCode = terminalInstance.exitCode;
|
||||
const exitCodeText = exitCode !== undefined ? ` with exit code ${exitCode}` : '';
|
||||
disposeNotification();
|
||||
const message = `[Terminal ${termId} notification: terminal exited${exitCodeText}. The terminal process ended before the command could complete normally; further commands cannot be sent to this terminal ID.]\nTerminal output:\n${currentOutput}`;
|
||||
this._logService.debug(`RunInTerminalTool: Background terminal ${termId} disposed${exitCodeText}, notifying chat session`);
|
||||
this._chatService.sendRequest(chatSessionResource, message, {
|
||||
...sendOptions,
|
||||
queue: ChatRequestQueueKind.Steering,
|
||||
isSystemInitiated: true,
|
||||
systemInitiatedLabel: localize('terminalProcessExited', "`{0}` terminal exited", commandName),
|
||||
terminalExecutionId: termId,
|
||||
}).catch(e => {
|
||||
this._logService.warn(`RunInTerminalTool: Failed to send terminal-exited notification for terminal ${termId}`, e);
|
||||
});
|
||||
}));
|
||||
|
||||
// When a checkpoint is restored, requests are removed from the model.
|
||||
|
||||
Reference in New Issue
Block a user