Skip to content

Commit 6076a48

Browse files
committed
- startDebuggingAndWaitForStop streams integrated-terminal output by subscribing to window.onDidStartTerminalShellExecution / window.onDidEndTerminalShellExecution and piping each TerminalShellExecution.read() stream into the runtime diagnostics buffer. This keeps crash context available even when adapters bypass the Debug Console (e.g., configs with console: "integratedTerminal").
- The capture path now relies solely on stable shell-integration APIs—no `--enable-proposed-api` or manifest `enabledApiProposals` entry is required.
1 parent 2ca8626 commit 6076a48

File tree

5 files changed

+265
-71
lines changed

5 files changed

+265
-71
lines changed

agents.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -142,8 +142,8 @@ The debug tracker extension provides API services for monitoring debug sessions
142142

143143
### Runtime Diagnostics Capture
144144

145-
- `startDebuggingAndWaitForStop` now streams integrated-terminal output using `vscode.window.onDidWriteTerminalData` so crash diagnostics are available even when adapters bypass the Debug Console (e.g., configs with `console: "integratedTerminal"`).
146-
- The extension declares the `terminalDataWriteEvent` proposal via `enabledApiProposals` in `package.json` and tests pass `--enable-proposed-api dkattan.copilot-breakpoint-debugger` through `.vscode-test.mjs` launch args.
145+
- `startDebuggingAndWaitForStop` streams integrated-terminal output by subscribing to `window.onDidStartTerminalShellExecution` / `window.onDidEndTerminalShellExecution` and piping each `TerminalShellExecution.read()` stream into the runtime diagnostics buffer. This keeps crash context available even when adapters bypass the Debug Console (e.g., configs with `console: "integratedTerminal"`).
146+
- The capture path now relies solely on stable shell-integration APIs—no `--enable-proposed-api` or manifest `enabledApiProposals` entry is required.
147147
- Runtime error messages automatically append exit codes, DAP stderr, and/or terminal lines (capped by `copilot-debugger.maxOutputLines`), keeping messaging concise while surfacing crash context for Copilot tools.
148148

149149
## External Dependencies

package.json

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,6 @@
3636
"onLanguageModelTools",
3737
"onStartupFinished"
3838
],
39-
"enabledApiProposals": [
40-
"terminalDataWriteEvent"
41-
],
4239
"contributes": {
4340
"commands": [
4441
{

src/session.ts

Lines changed: 82 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -216,29 +216,36 @@ interface TerminalOutputCapture {
216216
dispose: () => void;
217217
}
218218

219-
interface TerminalDataWriteEvent {
220-
terminal: vscode.Terminal;
221-
data: string;
222-
}
223-
224-
type TerminalDataWindow = typeof vscode.window & {
225-
onDidWriteTerminalData?: (
226-
listener: (event: TerminalDataWriteEvent) => void,
227-
thisArgs?: unknown,
228-
disposables?: vscode.Disposable[]
229-
) => vscode.Disposable;
230-
};
231-
232-
const createTerminalOutputCapture = (maxLines: number): TerminalOutputCapture => {
233-
const terminalDataEmitter = (vscode.window as TerminalDataWindow)
234-
.onDidWriteTerminalData;
235-
if (maxLines <= 0 || typeof terminalDataEmitter !== "function") {
219+
export const createTerminalOutputCapture = (
220+
maxLines: number
221+
): TerminalOutputCapture => {
222+
type TerminalShellWindow = typeof vscode.window & {
223+
onDidStartTerminalShellExecution?: vscode.Event<
224+
vscode.TerminalShellExecutionStartEvent
225+
>;
226+
onDidEndTerminalShellExecution?: vscode.Event<
227+
vscode.TerminalShellExecutionEndEvent
228+
>;
229+
};
230+
const terminalShellWindow = vscode.window as TerminalShellWindow;
231+
const startEvent = terminalShellWindow.onDidStartTerminalShellExecution;
232+
const endEvent = terminalShellWindow.onDidEndTerminalShellExecution;
233+
if (
234+
maxLines <= 0 ||
235+
typeof startEvent !== "function" ||
236+
typeof endEvent !== "function"
237+
) {
236238
return { snapshot: () => [], dispose: () => undefined };
237239
}
238240
const lines: string[] = [];
239241
const pendingByTerminal = new Map<vscode.Terminal, string>();
240242
const trackedTerminals = new Set<vscode.Terminal>();
241243
const initialTerminals = new Set(vscode.window.terminals);
244+
const activeExecutions = new Map<
245+
vscode.TerminalShellExecution,
246+
vscode.Terminal
247+
>();
248+
const pumpTasks = new Set<Promise<void>>();
242249
const pushLine = (terminal: vscode.Terminal, raw: string) => {
243250
const sanitized = stripAnsiEscapeCodes(raw).trim();
244251
if (!sanitized) {
@@ -249,6 +256,14 @@ const createTerminalOutputCapture = (maxLines: number): TerminalOutputCapture =>
249256
lines.shift();
250257
}
251258
};
259+
const appendChunk = (terminal: vscode.Terminal, chunk: string) => {
260+
const combined = (pendingByTerminal.get(terminal) ?? "") + chunk;
261+
const segments = combined.split(/\r?\n/);
262+
pendingByTerminal.set(terminal, segments.pop() ?? "");
263+
for (const segment of segments) {
264+
pushLine(terminal, segment);
265+
}
266+
};
252267
const considerTerminal = (terminal: vscode.Terminal): boolean => {
253268
if (trackedTerminals.has(terminal)) {
254269
return true;
@@ -263,13 +278,18 @@ const createTerminalOutputCapture = (maxLines: number): TerminalOutputCapture =>
263278
}
264279
return false;
265280
};
266-
const flushPending = () => {
267-
for (const [terminal, remainder] of pendingByTerminal.entries()) {
281+
const flushPending = (terminal?: vscode.Terminal) => {
282+
if (terminal) {
283+
const remainder = pendingByTerminal.get(terminal);
268284
if (remainder && remainder.trim()) {
269285
pushLine(terminal, remainder);
270286
}
287+
pendingByTerminal.delete(terminal);
288+
return;
289+
}
290+
for (const tracked of Array.from(pendingByTerminal.keys())) {
291+
flushPending(tracked);
271292
}
272-
pendingByTerminal.clear();
273293
};
274294
const disposables: vscode.Disposable[] = [];
275295
disposables.push(
@@ -284,16 +304,51 @@ const createTerminalOutputCapture = (maxLines: number): TerminalOutputCapture =>
284304
})
285305
);
286306
disposables.push(
287-
terminalDataEmitter((event: TerminalDataWriteEvent) => {
307+
startEvent((event) => {
288308
if (!considerTerminal(event.terminal)) {
289309
return;
290310
}
291-
const combined = (pendingByTerminal.get(event.terminal) ?? "") + event.data;
292-
const segments = combined.split(/\r?\n/);
293-
pendingByTerminal.set(event.terminal, segments.pop() ?? "");
294-
for (const segment of segments) {
295-
pushLine(event.terminal, segment);
311+
let stream: AsyncIterable<string>;
312+
try {
313+
stream = event.execution.read();
314+
} catch (err) {
315+
logger.warn(
316+
`Failed to read terminal shell execution data: ${
317+
err instanceof Error ? err.message : String(err)
318+
}`
319+
);
320+
return;
321+
}
322+
activeExecutions.set(event.execution, event.terminal);
323+
const pump = (async () => {
324+
try {
325+
for await (const chunk of stream) {
326+
if (!chunk) {
327+
continue;
328+
}
329+
appendChunk(event.terminal, chunk);
330+
}
331+
} catch (streamErr) {
332+
logger.warn(
333+
`Terminal shell execution stream failed: ${
334+
streamErr instanceof Error ? streamErr.message : String(streamErr)
335+
}`
336+
);
337+
} finally {
338+
activeExecutions.delete(event.execution);
339+
}
340+
})();
341+
pumpTasks.add(pump);
342+
void pump.finally(() => pumpTasks.delete(pump));
343+
})
344+
);
345+
disposables.push(
346+
endEvent((event) => {
347+
const terminal = activeExecutions.get(event.execution) ?? event.terminal;
348+
if (!terminal || !trackedTerminals.has(terminal)) {
349+
return;
296350
}
351+
flushPending(terminal);
297352
})
298353
);
299354
return {
@@ -306,6 +361,7 @@ const createTerminalOutputCapture = (maxLines: number): TerminalOutputCapture =>
306361
while (disposables.length) {
307362
disposables.pop()?.dispose();
308363
}
364+
pumpTasks.clear();
309365
trackedTerminals.clear();
310366
},
311367
};

src/test/multiRootWorkspace.test.ts

Lines changed: 40 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
1-
import * as assert from 'node:assert';
2-
import * as path from 'node:path';
3-
import * as vscode from 'vscode';
4-
import { DAPHelpers } from '../debugUtils';
5-
import { startDebuggingAndWaitForStop } from '../session';
1+
import * as assert from "node:assert";
2+
import * as path from "node:path";
3+
import * as vscode from "vscode";
4+
import { DAPHelpers } from "../debugUtils";
5+
import { startDebuggingAndWaitForStop } from "../session";
66
import {
77
activateCopilotDebugger,
88
getExtensionRoot,
99
openScriptDocument,
1010
stopAllDebugSessions,
11-
} from './utils/startDebuggerToolTestUtils';
11+
} from "./utils/startDebuggerToolTestUtils";
1212

1313
/**
1414
* Collect all variables from all scopes in the current debug session
@@ -56,80 +56,80 @@ function assertVariablesPresent(
5656
// Check that expected variables are present
5757
for (const varName of expectedVariables) {
5858
const found = allVariables.some(
59-
v => v.name === varName || v.name.includes(varName)
59+
(v) => v.name === varName || v.name.includes(varName)
6060
);
6161
assert.ok(
6262
found,
6363
`Expected variable '${varName}' to be present in: ${JSON.stringify(
64-
allVariables.map(v => v.name)
64+
allVariables.map((v) => v.name)
6565
)}`
6666
);
6767
}
6868
}
6969

70-
describe('multi-Root Workspace Integration', () => {
70+
describe("multi-Root Workspace Integration", () => {
7171
afterEach(async () => {
7272
await stopAllDebugSessions();
7373
});
7474

7575
before(() => {
7676
// Log test host information to understand which process we're in
77-
console.log('=== Multi-root Workspace Test Host Info ===');
78-
console.log('Process ID:', process.pid);
79-
console.log('Process title:', process.title);
80-
console.log('VS Code version:', vscode.version);
81-
console.log('VS Code app name:', vscode.env.appName);
82-
console.log('VS Code remote name:', vscode.env.remoteName || 'local');
77+
console.log("=== Multi-root Workspace Test Host Info ===");
78+
console.log("Process ID:", process.pid);
79+
console.log("Process title:", process.title);
80+
console.log("VS Code version:", vscode.version);
81+
console.log("VS Code app name:", vscode.env.appName);
82+
console.log("VS Code remote name:", vscode.env.remoteName || "local");
8383
console.log(
84-
'Initial workspace folders:',
84+
"Initial workspace folders:",
8585
vscode.workspace.workspaceFolders?.length || 0
8686
);
8787
if (vscode.workspace.workspaceFolders) {
8888
console.log(
89-
'Initial folders:',
90-
vscode.workspace.workspaceFolders.map(f => ({
89+
"Initial folders:",
90+
vscode.workspace.workspaceFolders.map((f) => ({
9191
name: f.name,
9292
path: f.uri.fsPath,
9393
}))
9494
);
9595
}
96-
console.log('==========================================');
96+
console.log("==========================================");
9797

9898
assert.ok(
9999
vscode.workspace.workspaceFolders?.length,
100-
'No workspace folders found. Ensure test-workspace.code-workspace is being loaded by the test runner.'
100+
"No workspace folders found. Ensure test-workspace.code-workspace is being loaded by the test runner."
101101
);
102102
assert.equal(
103103
vscode.workspace.name,
104-
'test-workspace (Workspace)',
105-
'Unexpected workspace name, should at least have (Workspace) to indicate a .code-workspace is open'
104+
"test-workspace (Workspace)",
105+
"Unexpected workspace name, should at least have (Workspace) to indicate a .code-workspace is open"
106106
);
107107
});
108108

109-
it('workspace B (Node.js) - individual debug session', async () => {
109+
it("workspace B (Node.js) - individual debug session", async () => {
110110
const extensionRoot = getExtensionRoot();
111-
const workspaceFolder = path.join(extensionRoot, 'test-workspace', 'b');
112-
const scriptUri = vscode.Uri.file(path.join(workspaceFolder, 'test.js'));
111+
const workspaceFolder = path.join(extensionRoot, "test-workspace", "b");
112+
const scriptUri = vscode.Uri.file(path.join(workspaceFolder, "test.js"));
113113
const lineInsideLoop = 9;
114114

115115
await openScriptDocument(scriptUri);
116116
await activateCopilotDebugger();
117117

118-
const configurationName = 'Run test.js';
118+
const configurationName = "Run test.js";
119119
const baseParams = {
120120
workspaceFolder,
121121
nameOrConfiguration: configurationName,
122122
};
123123

124124
const context = await startDebuggingAndWaitForStop(
125125
Object.assign({}, baseParams, {
126-
sessionName: 'workspace-b-node',
126+
sessionName: "workspace-b-node",
127127
breakpointConfig: {
128128
breakpoints: [
129129
{
130130
path: scriptUri.fsPath,
131131
line: lineInsideLoop,
132-
variableFilter: ['i'],
132+
variableFilter: ["i"],
133133
},
134134
],
135135
},
@@ -145,51 +145,51 @@ describe('multi-Root Workspace Integration', () => {
145145

146146
// Assert the file path contains the expected file
147147
assert.ok(
148-
context.frame.source?.path?.includes('test.js'),
148+
context.frame.source?.path?.includes("test.js"),
149149
`Expected file path to contain 'test.js', got: ${context.frame.source?.path}`
150150
);
151151

152152
// Collect variables from scopes using active session
153153
const activeSession = vscode.debug.activeDebugSession;
154-
assert.ok(activeSession, 'No active debug session after breakpoint hit');
154+
assert.ok(activeSession, "No active debug session after breakpoint hit");
155155

156156
const preCollected = flattenScopeVariables(context.scopeVariables);
157157
const allVariables = preCollected.length
158158
? preCollected
159159
: await collectAllVariables(activeSession, context.scopes);
160160

161161
// Verify that we got the expected variables
162-
assertVariablesPresent(allVariables, ['i']);
162+
assertVariablesPresent(allVariables, ["i"]);
163163
});
164164

165-
it('workspace B with conditional breakpoint (Node.js)', async function () {
165+
it("workspace B with conditional breakpoint (Node.js)", async function () {
166166
this.timeout(5000);
167167

168168
const extensionRoot = getExtensionRoot();
169-
const workspaceFolder = path.join(extensionRoot, 'test-workspace', 'b');
170-
const scriptUri = vscode.Uri.file(path.join(workspaceFolder, 'test.js'));
169+
const workspaceFolder = path.join(extensionRoot, "test-workspace", "b");
170+
const scriptUri = vscode.Uri.file(path.join(workspaceFolder, "test.js"));
171171

172172
await openScriptDocument(scriptUri);
173173
await activateCopilotDebugger();
174174

175-
const configurationName = 'Run test.js';
175+
const configurationName = "Run test.js";
176176
const baseParams = {
177177
workspaceFolder,
178178
nameOrConfiguration: configurationName,
179179
};
180-
const condition = 'i >= 3';
180+
const condition = "i >= 3";
181181
const lineInsideLoop = 9;
182182

183183
const context = await startDebuggingAndWaitForStop(
184184
Object.assign({}, baseParams, {
185-
sessionName: 'workspace-b-conditional-node',
185+
sessionName: "workspace-b-conditional-node",
186186
breakpointConfig: {
187187
breakpoints: [
188188
{
189189
path: scriptUri.fsPath,
190190
line: lineInsideLoop,
191191
condition,
192-
variableFilter: ['i'],
192+
variableFilter: ["i"],
193193
},
194194
],
195195
},
@@ -205,15 +205,15 @@ describe('multi-Root Workspace Integration', () => {
205205

206206
// Collect variables from scopes using active session
207207
const activeSession = vscode.debug.activeDebugSession;
208-
assert.ok(activeSession, 'No active debug session after breakpoint hit');
208+
assert.ok(activeSession, "No active debug session after breakpoint hit");
209209

210210
const preCollected = flattenScopeVariables(context.scopeVariables);
211211
const allVariables = preCollected.length
212212
? preCollected
213213
: await collectAllVariables(activeSession, context.scopes);
214214

215215
// Verify we got the variable 'i' and that its value is >= 3
216-
const iVariable = allVariables.find(v => v.name === 'i');
216+
const iVariable = allVariables.find((v) => v.name === "i");
217217
assert.ok(iVariable, "Variable 'i' should be present");
218218
const iValue = Number.parseInt(iVariable.value, 10);
219219
assert.ok(

0 commit comments

Comments
 (0)