Skip to content

Commit a472377

Browse files
authored
migrate run task, get task output to core, fix bugs (microsoft#256774)
1 parent b4f9fff commit a472377

File tree

12 files changed

+416
-10
lines changed

12 files changed

+416
-10
lines changed

src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2438,6 +2438,10 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer
24382438
return folder;
24392439
}
24402440

2441+
getTerminalForTask(task: Task): URI | undefined {
2442+
return this._taskSystem?.getTerminalForTask(task);
2443+
}
2444+
24412445
protected async _computeWorkspaceTasks(runSource: TaskRunSource = TaskRunSource.User): Promise<Map<string, IWorkspaceFolderTaskResult>> {
24422446
const promises: Promise<IWorkspaceFolderTaskResult | undefined>[] = [];
24432447
for (const folder of this.workspaceFolders) {

src/vs/workbench/contrib/tasks/browser/terminalTaskSystem.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,16 @@ export class TerminalTaskSystem extends Disposable implements ITaskSystem {
276276
}
277277
}
278278

279+
getTerminalForTask(task: Task): URI | undefined {
280+
for (const key in this._terminals) {
281+
const value = this._terminals[key];
282+
if (value.lastTask === task.getMapKey()) {
283+
return value.terminal.resource;
284+
}
285+
}
286+
return undefined;
287+
}
288+
279289
public rerun(): ITaskExecuteResult | undefined {
280290
if (this._lastTask && this._lastTask.verify()) {
281291
if ((this._lastTask.task.runOptions.reevaluateOnRerun !== undefined) && !this._lastTask.task.runOptions.reevaluateOnRerun) {

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { Task, ContributedTask, CustomTask, ITaskSet, TaskSorter, ITaskEvent, IT
1414
import { ITaskSummary, ITaskTerminateResponse, ITaskSystemInfo } from './taskSystem.js';
1515
import { IStringDictionary } from '../../../../base/common/collections.js';
1616
import { RawContextKey, ContextKeyExpr } from '../../../../platform/contextkey/common/contextkey.js';
17+
import { URI } from '../../../../base/common/uri.js';
1718

1819
export type { ITaskSummary, Task, ITaskTerminateResponse as TaskTerminateResponse };
1920

@@ -87,6 +88,7 @@ export interface ITaskService {
8788
getWorkspaceTasks(runSource?: TaskRunSource): Promise<Map<string, IWorkspaceFolderTaskResult>>;
8889
getSavedTasks(type: 'persistent' | 'historical'): Promise<(Task | ConfiguringTask)[]>;
8990
removeRecentlyUsedTask(taskRecentlyUsedKey: string): void;
91+
getTerminalForTask(task: Task): URI | undefined;
9092
/**
9193
* @param alias The task's name, label or defined identifier.
9294
*/

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,7 @@ export interface ITaskSystem {
151151
customExecutionComplete(task: Task, result: number): Promise<void>;
152152
isTaskVisible(task: Task): boolean;
153153
getTaskForTerminal(instanceId: number): Task | undefined;
154+
getTerminalForTask(task: Task): URI | undefined;
154155
getFirstInstance(task: Task): Task | undefined;
155156
get lastTask(): VerifiedTask | undefined;
156157
set lastTask(task: VerifiedTask);

src/vs/workbench/contrib/terminal/terminal.all.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import './browser/terminalView.js';
1616
import '../terminalContrib/accessibility/browser/terminal.accessibility.contribution.js';
1717
import '../terminalContrib/autoReplies/browser/terminal.autoReplies.contribution.js';
1818
import '../terminalContrib/chatAgentTools/browser/terminal.chatAgentTools.contribution.js';
19+
import '../terminalContrib/chatAgentTools/browser/task.chatAgentTools.contribution.js';
1920
import '../terminalContrib/developer/browser/terminal.developer.contribution.js';
2021
import '../terminalContrib/environmentChanges/browser/terminal.environmentChanges.contribution.js';
2122
import '../terminalContrib/find/browser/terminal.find.contribution.js';

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

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ export function getOutput(instance: ITerminalInstance, startMarker?: IXtermMarke
4040
}
4141

4242
export async function pollForOutputAndIdle(
43-
execution: { getOutput: () => string },
43+
execution: { getOutput: () => string; isActive?: () => Promise<boolean> },
4444
extendedPolling: boolean,
4545
token: CancellationToken,
4646
languageModelsService: ILanguageModelsService,
@@ -87,16 +87,18 @@ export async function pollForOutputAndIdle(
8787
noNewDataCount = 0;
8888
lastBufferLength = currentBufferLength;
8989
}
90-
const isLikelyFinished = await assessOutputForFinishedState(buffer, token, languageModelsService);
91-
terminalExecutionIdleBeforeTimeout = isLikelyFinished && noNewDataCount >= PollingConsts.MinNoDataEvents;
92-
if (terminalExecutionIdleBeforeTimeout) {
93-
return { terminalExecutionIdleBeforeTimeout, output: buffer, pollDurationMs: Date.now() - pollStartTime + (extendedPolling ? PollingConsts.FirstPollingMaxDuration : 0) };
90+
91+
if (noNewDataCount >= PollingConsts.MinNoDataEvents) {
92+
terminalExecutionIdleBeforeTimeout = await assessOutputForFinishedState(buffer, execution, token, languageModelsService);
93+
if (terminalExecutionIdleBeforeTimeout) {
94+
return { terminalExecutionIdleBeforeTimeout, output: buffer, pollDurationMs: Date.now() - pollStartTime + (extendedPolling ? PollingConsts.FirstPollingMaxDuration : 0) };
95+
}
9496
}
9597
}
9698
return { terminalExecutionIdleBeforeTimeout, output: buffer, pollDurationMs: Date.now() - pollStartTime + (extendedPolling ? PollingConsts.FirstPollingMaxDuration : 0) };
9799
}
98100

99-
export async function promptForMorePolling(command: string, execution: { getOutput: () => string }, token: CancellationToken, context: IToolInvocationContext, chatService: IChatService): Promise<boolean> {
101+
export async function promptForMorePolling(command: string, context: IToolInvocationContext, chatService: IChatService): Promise<boolean> {
100102
const chatModel = chatService.getSession(context.sessionId);
101103
if (chatModel instanceof ChatModel) {
102104
const request = chatModel.getRequests().at(-1);
@@ -123,7 +125,10 @@ export async function promptForMorePolling(command: string, execution: { getOutp
123125
return false; // Fallback to not waiting if we can't prompt the user
124126
}
125127

126-
export async function assessOutputForFinishedState(buffer: string, token: CancellationToken, languageModelsService: ILanguageModelsService): Promise<boolean> {
128+
export async function assessOutputForFinishedState(buffer: string, execution: { getOutput: () => string; isActive?: () => Promise<boolean> }, token: CancellationToken, languageModelsService: ILanguageModelsService): Promise<boolean> {
129+
if (execution.isActive && ((await execution.isActive()) === false)) {
130+
return true;
131+
}
127132
const models = await languageModelsService.selectLanguageModels({ vendor: 'copilot', family: 'gpt-4o-mini' });
128133
if (!models.length) {
129134
return false;
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
import { ITerminalInstance } from '../../../terminal/browser/terminal.js';
7+
import type { IMarker as IXtermMarker } from '@xterm/xterm';
8+
9+
export function getOutput(instance: ITerminalInstance, startMarker?: IXtermMarker): string {
10+
if (!instance.xterm || !instance.xterm.raw) {
11+
return '';
12+
}
13+
const lines: string[] = [];
14+
for (let y = Math.min(startMarker?.line ?? 0, 0); y < instance.xterm!.raw.buffer.active.length; y++) {
15+
const line = instance.xterm!.raw.buffer.active.getLine(y);
16+
if (!line) {
17+
continue;
18+
}
19+
lines.push(line.translateToString(true));
20+
}
21+
return lines.join('\n');
22+
}

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

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -323,10 +323,10 @@ export class RunInTerminalTool extends Disposable implements IToolImpl {
323323
}
324324
const execution = new BackgroundTerminalExecution(toolTerminal.instance, xterm, command);
325325
RunInTerminalTool._backgroundExecutions.set(termId, execution);
326-
// Poll for output until the terminal is idle or some time has passed
326+
327327
outputAndIdle = await pollForOutputAndIdle(execution, false, token, this._languageModelsService);
328328
if (!outputAndIdle.terminalExecutionIdleBeforeTimeout) {
329-
const extendPolling = await promptForMorePolling(command, execution, token, invocation.context, this._chatService);
329+
const extendPolling = await promptForMorePolling(command, invocation.context, this._chatService);
330330
if (extendPolling) {
331331
outputAndIdle = await pollForOutputAndIdle(execution, true, token, this._languageModelsService);
332332
}
@@ -687,7 +687,6 @@ class BackgroundTerminalExecution extends Disposable {
687687
this._startMarker = this._register(this._xterm.raw.registerMarker());
688688
this._instance.runCommand(this._commandLine, true);
689689
}
690-
691690
getOutput(): string {
692691
return getOutput(this._instance, this._startMarker);
693692
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
import { Disposable } from '../../../../../base/common/lifecycle.js';
7+
import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js';
8+
import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js';
9+
import { registerWorkbenchContribution2, WorkbenchPhase, type IWorkbenchContribution } from '../../../../common/contributions.js';
10+
import { ILanguageModelToolsService } from '../../../chat/common/languageModelToolsService.js';
11+
import { GetTaskOutputTool, GetTaskOutputToolData } from './task/getTaskOutputTool.js';
12+
import { RunTaskTool, RunTaskToolData } from './task/runTaskTool.js';
13+
14+
// #region Workbench contributions
15+
16+
class ChatAgentToolsContribution extends Disposable implements IWorkbenchContribution {
17+
18+
static readonly ID = 'task.chatAgentTools';
19+
20+
constructor(
21+
@IConfigurationService configurationService: IConfigurationService,
22+
@IInstantiationService instantiationService: IInstantiationService,
23+
@ILanguageModelToolsService toolsService: ILanguageModelToolsService,
24+
) {
25+
super();
26+
27+
const runTaskTool = instantiationService.createInstance(RunTaskTool);
28+
this._register(toolsService.registerToolData(RunTaskToolData));
29+
this._register(toolsService.registerToolImplementation(RunTaskToolData.id, runTaskTool));
30+
31+
const getTaskOutputTool = instantiationService.createInstance(GetTaskOutputTool);
32+
this._register(toolsService.registerToolData(GetTaskOutputToolData));
33+
this._register(toolsService.registerToolImplementation(GetTaskOutputToolData.id, getTaskOutputTool));
34+
}
35+
}
36+
registerWorkbenchContribution2(ChatAgentToolsContribution.ID, ChatAgentToolsContribution, WorkbenchPhase.AfterRestored);
37+
38+
// #endregion Contributions
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
import type { CancellationToken } from '../../../../../../base/common/cancellation.js';
7+
import { MarkdownString } from '../../../../../../base/common/htmlContent.js';
8+
import { Disposable } from '../../../../../../base/common/lifecycle.js';
9+
import { localize } from '../../../../../../nls.js';
10+
import { ToolDataSource, type CountTokensCallback, type IPreparedToolInvocation, type IToolData, type IToolImpl, type IToolInvocation, type IToolInvocationPreparationContext, type IToolResult, type ToolProgress } from '../../../../chat/common/languageModelToolsService.js';
11+
import { ITaskService } from '../../../../tasks/common/taskService.js';
12+
import { ITerminalService } from '../../../../terminal/browser/terminal.js';
13+
import { getOutput } from '../bufferOutputPolling.js';
14+
import { getTaskDefinition, getTaskForTool } from './taskHelpers.js';
15+
16+
export const GetTaskOutputToolData: IToolData = {
17+
id: 'get_task_output2',
18+
toolReferenceName: 'getTaskOutput',
19+
displayName: localize('getTaskOutputTool.displayName', 'Get Task Output'),
20+
modelDescription: 'Get the output of a task',
21+
source: ToolDataSource.Internal,
22+
canBeReferencedInPrompt: true,
23+
inputSchema: {
24+
type: 'object',
25+
properties: {
26+
id: {
27+
type: 'string',
28+
description: 'The task ID for which to get the output.'
29+
},
30+
workspaceFolder: {
31+
type: 'string',
32+
description: 'The workspace folder path containing the task'
33+
},
34+
},
35+
required: [
36+
'id',
37+
'workspaceFolder'
38+
]
39+
}
40+
};
41+
42+
export interface IGetTaskOutputInputParams {
43+
id: string;
44+
workspaceFolder: string;
45+
}
46+
47+
export class GetTaskOutputTool extends Disposable implements IToolImpl {
48+
constructor(
49+
@ITaskService private readonly _tasksService: ITaskService,
50+
@ITerminalService private readonly _terminalService: ITerminalService,
51+
) {
52+
super();
53+
}
54+
async prepareToolInvocation(context: IToolInvocationPreparationContext, token: CancellationToken): Promise<IPreparedToolInvocation | undefined> {
55+
const args = context.parameters as IGetTaskOutputInputParams;
56+
57+
const taskDefinition = getTaskDefinition(args.id);
58+
const task = await getTaskForTool(args.id, taskDefinition, args.workspaceFolder, this._tasksService);
59+
if (!task) {
60+
return { invocationMessage: new MarkdownString(localize('copilotChat.taskNotFound', 'Task not found: `{0}`', args.id)) };
61+
}
62+
const activeTasks = await this._tasksService.getActiveTasks();
63+
if (activeTasks.includes(task)) {
64+
return { invocationMessage: new MarkdownString(localize('copilotChat.taskAlreadyRunning', 'The task `{0}` is already running.', taskDefinition.taskLabel)) };
65+
}
66+
67+
return {
68+
invocationMessage: new MarkdownString(localize('copilotChat.checkingTerminalOutput', 'Checking terminal output for `{0}`', taskDefinition.taskLabel)),
69+
pastTenseMessage: new MarkdownString(localize('copilotChat.checkedTerminalOutput', 'Checked terminal output for `{0}`', taskDefinition.taskLabel)),
70+
};
71+
}
72+
73+
async invoke(invocation: IToolInvocation, _countTokens: CountTokensCallback, _progress: ToolProgress, token: CancellationToken): Promise<IToolResult> {
74+
const args = invocation.parameters as IGetTaskOutputInputParams;
75+
const taskDefinition = getTaskDefinition(args.id);
76+
const task = await getTaskForTool(args.id, taskDefinition, args.workspaceFolder, this._tasksService);
77+
if (!task) {
78+
return { content: [], toolResultMessage: new MarkdownString(localize('copilotChat.taskNotFound', 'Task not found: `{0}`', args.id)) };
79+
}
80+
81+
const resource = this._tasksService.getTerminalForTask(task);
82+
const terminal = this._terminalService.instances.find(t => t.resource.path === resource?.path && t.resource.scheme === resource.scheme);
83+
if (!terminal) {
84+
return { content: [], toolResultMessage: new MarkdownString(localize('copilotChat.terminalNotFound', 'Terminal not found for task `{0}`', taskDefinition?.taskLabel)) };
85+
}
86+
return {
87+
content: [{
88+
kind: 'text',
89+
value: localize('copilotChat.taskOutput', 'Output of task `{0}`:\n{1}', taskDefinition.taskLabel, getOutput(terminal))
90+
}]
91+
};
92+
}
93+
}

0 commit comments

Comments
 (0)