Skip to content

Commit f6cf585

Browse files
authored
Provide problem locations as details in chat response (microsoft#261690)
1 parent 6752afe commit f6cf585

File tree

6 files changed

+48
-45
lines changed

6 files changed

+48
-45
lines changed

src/vs/workbench/contrib/tasks/common/taskService.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,7 @@ import { ITaskSummary, ITaskTerminateResponse, ITaskSystemInfo } from './taskSys
1414
import { IStringDictionary } from '../../../../base/common/collections.js';
1515
import { RawContextKey, ContextKeyExpr } from '../../../../platform/contextkey/common/contextkey.js';
1616
import { URI } from '../../../../base/common/uri.js';
17-
1817
export type { ITaskSummary, Task, ITaskTerminateResponse as TaskTerminateResponse };
19-
2018
export const CustomExecutionSupportedContext = new RawContextKey<boolean>('customExecutionSupported', false, nls.localize('tasks.customExecutionSupported', "Whether CustomExecution tasks are supported. Consider using in the when clause of a \'taskDefinition\' contribution."));
2119
export const ShellExecutionSupportedContext = new RawContextKey<boolean>('shellExecutionSupported', false, nls.localize('tasks.shellExecutionSupported', "Whether ShellExecution tasks are supported. Consider using in the when clause of a \'taskDefinition\' contribution."));
2220
export const TaskCommandsRegistered = new RawContextKey<boolean>('taskCommandsRegistered', false, nls.localize('tasks.taskCommandsRegistered', "Whether the task commands have been registered yet"));

src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/bufferOutputPolling.ts

Lines changed: 11 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ import type { Terminal as RawXtermTerminal, IMarker as IXtermMarker } from '@xte
1717
import { Task } from '../../../tasks/common/taskService.js';
1818
import { IMarker, IMarkerService } from '../../../../../platform/markers/common/markers.js';
1919
import { ProblemMatcher, ProblemMatcherRegistry } from '../../../tasks/common/problemMatcher.js';
20+
import { Range } from '../../../../../editor/common/core/range.js';
21+
import { ILinkLocation } from './taskHelpers.js';
2022

2123
export const enum PollingConsts {
2224
MinNoDataEvents = 2, // Minimum number of no data checks before considering the terminal idle
@@ -34,7 +36,7 @@ export const enum PollingConsts {
3436
export async function racePollingOrPrompt(
3537
pollFn: () => Promise<{ terminalExecutionIdleBeforeTimeout: boolean; output: string; pollDurationMs?: number; modelOutputEvalResponse?: string }>,
3638
promptFn: () => { promise: Promise<boolean>; part?: Pick<ChatElicitationRequestPart, 'hide' | 'onDidRequestHide'> },
37-
originalResult: { terminalExecutionIdleBeforeTimeout: boolean; output: string; pollDurationMs?: number; modelOutputEvalResponse?: string },
39+
originalResult: { terminalExecutionIdleBeforeTimeout: boolean; output: string; resources?: ILinkLocation[]; pollDurationMs?: number; modelOutputEvalResponse?: string },
3840
token: CancellationToken,
3941
languageModelsService: ILanguageModelsService,
4042
markerService: IMarkerService,
@@ -56,7 +58,6 @@ export async function racePollingOrPrompt(
5658
promptResolved = true;
5759
return { type: 'prompt', result };
5860
});
59-
6061
const raceResult = await Promise.race([
6162
pollPromiseWrapped,
6263
promptPromiseWrapped
@@ -105,7 +106,7 @@ export async function pollForOutputAndIdle(
105106
languageModelsService: Pick<ILanguageModelsService, 'selectLanguageModels' | 'sendChatRequest'>,
106107
markerService: Pick<IMarkerService, 'read'>,
107108
knownMatchers?: ProblemMatcher[]
108-
): Promise<{ terminalExecutionIdleBeforeTimeout: boolean; output: string; pollDurationMs?: number; modelOutputEvalResponse?: string }> {
109+
): Promise<{ terminalExecutionIdleBeforeTimeout: boolean; output: string; resources?: ILinkLocation[]; pollDurationMs?: number; modelOutputEvalResponse?: string }> {
109110
const maxWaitMs = extendedPolling ? PollingConsts.ExtendedPollingMaxDuration : PollingConsts.FirstPollingMaxDuration;
110111
const maxInterval = PollingConsts.MaxPollingIntervalDuration;
111112
let currentInterval = PollingConsts.MinPollingDuration;
@@ -157,35 +158,19 @@ export async function pollForOutputAndIdle(
157158
}
158159
}
159160
terminalExecutionIdleBeforeTimeout = true;
161+
let resources: ILinkLocation[] | undefined;
160162
if (execution.task) {
161-
const problems = await getProblemsForTasks(execution.task, markerService, execution.dependencyTasks, knownMatchers);
163+
const problems = getProblemsForTasks(execution.task, markerService, execution.dependencyTasks, knownMatchers);
162164
if (problems) {
163165
// Problem matchers exist for this task
164166
const problemList: string[] = [];
165167
for (const [, problemArray] of problems.entries()) {
168+
resources = [];
166169
if (problemArray.length) {
167170
for (const p of problemArray) {
168-
let location = '';
169-
let label = p.resource ? p.resource.path.split('/').pop() ?? p.resource.toString() : '';
170-
let uri = p.resource ? p.resource.toString() : '';
171-
if (typeof p.startLineNumber === 'number' && typeof p.startColumn === 'number') {
172-
uri += `:${p.startLineNumber}:${p.startColumn}`;
173-
label += `:${p.startLineNumber}:${p.startColumn}`;
174-
if (typeof p.endLineNumber === 'number' && typeof p.endColumn === 'number') {
175-
uri += `-${p.endLineNumber}:${p.endColumn}`;
176-
label += `-${p.endLineNumber}:${p.endColumn}`;
177-
}
178-
} else if (typeof p.startLineNumber === 'number') {
179-
uri += `:${p.startLineNumber}`;
180-
label += `:${p.startLineNumber}`;
181-
} else if (typeof p.startColumn === 'number') {
182-
uri += `:${p.startColumn}`;
183-
label += `:${p.startColumn}`;
184-
}
185-
if (uri) {
186-
location = `[${label}](${uri})`;
187-
}
188-
problemList.push(`Problem: ${p.message} at ${location ? ` ${location}` : 'unknown'}`);
171+
resources.push({ uri: p.resource, range: new Range(p.startLineNumber ?? 1, p.startColumn ?? 1, p.endLineNumber ?? (p.startLineNumber ?? 1), p.endColumn ?? (p.startColumn ?? 1)) });
172+
const label = p.resource ? p.resource.path.split('/').pop() ?? p.resource.toString() : '';
173+
problemList.push(`Problem: ${p.message} in ${label}`);
189174
}
190175
}
191176
}
@@ -195,6 +180,7 @@ export async function pollForOutputAndIdle(
195180
return {
196181
terminalExecutionIdleBeforeTimeout,
197182
output: problemList.join('\n'),
183+
resources,
198184
pollDurationMs: Date.now() - pollStartTime + (extendedPolling ? PollingConsts.FirstPollingMaxDuration : 0)
199185
};
200186
}

src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/taskHelpers.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { CancellationToken } from '../../../../../base/common/cancellation.js';
77
import { IStringDictionary } from '../../../../../base/common/collections.js';
88
import { MarkdownString } from '../../../../../base/common/htmlContent.js';
99
import { URI } from '../../../../../base/common/uri.js';
10+
import { Range } from '../../../../../editor/common/core/range.js';
1011
import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js';
1112
import { IMarkerService } from '../../../../../platform/markers/common/markers.js';
1213
import { IChatService } from '../../../chat/common/chatService.js';
@@ -141,8 +142,8 @@ export async function resolveDependencyTasks(parentTask: Task, workspaceFolder:
141142
* Collects output, polling duration, and idle status for all terminals.
142143
*/
143144
export async function collectTerminalResults(
144-
terminals: ITerminalInstance[], task: Task, languageModelsService: ILanguageModelsService, markerService: IMarkerService, chatService: IChatService, invocationContext: any, progress: ToolProgress, token: CancellationToken, isActive?: () => Promise<boolean>, dependencyTasks?: Task[]): Promise<Array<{ name: string; output: string; pollDurationMs: number; idle: boolean }>> {
145-
const results: Array<{ name: string; output: string; pollDurationMs: number; idle: boolean }> = [];
145+
terminals: ITerminalInstance[], task: Task, languageModelsService: ILanguageModelsService, markerService: IMarkerService, chatService: IChatService, invocationContext: any, progress: ToolProgress, token: CancellationToken, isActive?: () => Promise<boolean>, dependencyTasks?: Task[]): Promise<Array<{ name: string; output: string; resources?: ILinkLocation[]; pollDurationMs: number; idle: boolean }>> {
146+
const results: Array<{ name: string; output: string; resources?: ILinkLocation[]; pollDurationMs: number; idle: boolean }> = [];
146147
for (const terminal of terminals) {
147148
progress.report({ message: new MarkdownString(`Checking output for \`${terminal.shellLaunchConfig.name ?? 'unknown'}\``) });
148149
let outputAndIdle = await pollForOutputAndIdle({ getOutput: () => getOutput(terminal.xterm?.raw), isActive, task, dependencyTasks }, false, token, languageModelsService, markerService);
@@ -161,9 +162,11 @@ export async function collectTerminalResults(
161162
name: terminal.shellLaunchConfig.name ?? 'unknown',
162163
output: outputAndIdle?.output ?? '',
163164
pollDurationMs: outputAndIdle?.pollDurationMs ?? 0,
164-
idle: !!outputAndIdle?.terminalExecutionIdleBeforeTimeout
165+
idle: !!outputAndIdle?.terminalExecutionIdleBeforeTimeout,
166+
resources: outputAndIdle?.resources
165167
});
166168
}
167169
return results;
168170
}
169171

172+
export interface ILinkLocation { uri: URI; range: Range }

src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/task/createAndRunTaskTool.ts

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -140,20 +140,26 @@ export class CreateAndRunTaskTool implements IToolImpl {
140140
});
141141
}
142142

143-
let output = '';
143+
let resultSummary = '';
144144
if (result?.exitCode) {
145-
output = localize('copilotChat.taskFailedWithExitCode', 'Task `{0}` failed with exit code {1}.', args.task.label, result.exitCode);
145+
resultSummary = localize('copilotChat.taskFailedWithExitCode', 'Task `{0}` failed with exit code {1}.', args.task.label, result.exitCode);
146146
} else {
147-
output += `Task \`${args.task.label}\` `;
148-
output += terminalResults.every(r => r.idle)
147+
resultSummary += `Task \`${args.task.label}\` `;
148+
resultSummary += terminalResults.every(r => r.idle)
149149
? 'finished.'
150150
: 'started and will continue to run in the background.';
151151
}
152152

153-
const details = terminalResults.map(r => `Terminal: ${r.name}\nOutput:\n${r.output}`).join('\n\n');
153+
const details = terminalResults.map(r => `Terminal: ${r.name}\nOutput:\n${r.output}`);
154+
const uniqueDetails = Array.from(new Set(details)).join('\n\n');
154155
return {
155-
content: [{ kind: 'text', value: `Task output summary:\n${details}` }],
156-
toolResultMessage: output
156+
content: [{ kind: 'text', value: uniqueDetails }],
157+
toolResultMessage: new MarkdownString(resultSummary),
158+
toolResultDetails: Array.from(new Map(
159+
terminalResults
160+
.flatMap(r => r.resources?.filter(res => res.uri && res.range).map(res => ({ uri: res.uri, range: res.range })) ?? [])
161+
.map(item => [`${item.uri.toString()}-${item.range.toString()}`, item])
162+
).values())
157163
};
158164
}
159165

src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/task/getTaskOutputTool.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -104,12 +104,16 @@ export class GetTaskOutputTool extends Disposable implements IToolImpl {
104104
undefined,
105105
dependencyTasks
106106
);
107-
const details = terminalResults.map(r => `Terminal: ${r.name}\nOutput:\n${r.output}`).join('\n\n');
107+
const details = terminalResults.map(r => `Terminal: ${r.name}\nOutput:\n${r.output}`);
108+
const uniqueDetails = Array.from(new Set(details)).join('\n\n');
108109
return {
109-
content: [{
110-
kind: 'text',
111-
value: `Output of task \`${taskLabel}\`: ${details}`
112-
}]
110+
content: [{ kind: 'text', value: uniqueDetails }],
111+
toolResultMessage: new MarkdownString(localize('copilotChat.checkedTerminalOutput', 'Checked output for task `{0}`', taskLabel)),
112+
toolResultDetails: Array.from(new Map(
113+
terminalResults
114+
.flatMap(r => r.resources?.filter(res => res.uri && res.range).map(res => ({ uri: res.uri, range: res.range })) ?? [])
115+
.map(item => [`${item.uri.toString()}-${item.range.toString()}`, item])
116+
).values())
113117
};
114118
}
115119
}

src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/task/runTaskTool.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -109,10 +109,16 @@ export class RunTaskTool implements IToolImpl {
109109
: 'started and will continue to run in the background.';
110110
}
111111

112-
const details = terminalResults.map(r => `Terminal: ${r.name}\nOutput:\n${r.output}`).join('\n\n');
112+
const details = terminalResults.map(r => `Terminal: ${r.name}\nOutput:\n${r.output}`);
113+
const uniqueDetails = Array.from(new Set(details)).join('\n\n');
113114
return {
114-
content: [{ kind: 'text', value: details }],
115-
toolResultMessage: new MarkdownString(resultSummary)
115+
content: [{ kind: 'text', value: uniqueDetails }],
116+
toolResultMessage: new MarkdownString(resultSummary),
117+
toolResultDetails: Array.from(new Map(
118+
terminalResults
119+
.flatMap(r => r.resources?.filter(res => res.uri && res.range).map(res => ({ uri: res.uri, range: res.range })) ?? [])
120+
.map(item => [`${item.uri.toString()}-${item.range.toString()}`, item])
121+
).values())
116122
};
117123
}
118124

0 commit comments

Comments
 (0)