Skip to content

Commit dff6109

Browse files
committed
Add a new tools which allow to list and run tasks
fixes #15503
1 parent 0815844 commit dff6109

File tree

5 files changed

+142
-0
lines changed

5 files changed

+142
-0
lines changed

package-lock.json

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/ai-ide/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
"@theia/workspace": "1.60.0",
2828
"@theia/ai-mcp": "1.60.0",
2929
"@theia/search-in-workspace": "1.60.0",
30+
"@theia/task": "1.60.0",
3031
"ignore": "^6.0.0",
3132
"minimatch": "^9.0.0",
3233
"date-fns": "^4.1.0"

packages/ai-ide/src/browser/frontend-module.ts

+3
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import { CoderAgent } from './coder-agent';
2222
import { FileContentFunction, FileDiagonsticProvider, GetWorkspaceDirectoryStructure, GetWorkspaceFileList, WorkspaceFunctionScope } from './workspace-functions';
2323
import { WorkspaceSearchProvider } from './workspace-search-provider';
2424
import { FrontendApplicationContribution, PreferenceContribution, WidgetFactory, bindViewContribution } from '@theia/core/lib/browser';
25+
import { TaskListProvider, TaskRunnerProvider } from './workspace-task-provider';
2526
import { WorkspacePreferencesSchema } from './workspace-preferences';
2627
import {
2728
ReplaceContentInFileFunctionHelper,
@@ -83,6 +84,8 @@ export default new ContainerModule(bind => {
8384
bindToolProvider(WorkspaceSearchProvider, bind);
8485

8586
bindToolProvider(WriteChangeToFileProvider, bind);
87+
bindToolProvider(TaskListProvider, bind);
88+
bindToolProvider(TaskRunnerProvider, bind);
8689
bind(ReplaceContentInFileFunctionHelper).toSelf().inSingletonScope();
8790
bindToolProvider(ReplaceContentInFileProvider, bind);
8891
bindToolProvider(ListChatContext, bind);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
// *****************************************************************************
2+
// Copyright (C) 2024 EclipseSource GmbH.
3+
//
4+
// This program and the accompanying materials are made available under the
5+
// terms of the Eclipse Public License v. 2.0 which is available at
6+
// http://www.eclipse.org/legal/epl-2.0.
7+
//
8+
// This Source Code may also be made available under the following Secondary
9+
// Licenses when the conditions for such availability set forth in the Eclipse
10+
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
11+
// with the GNU Classpath Exception which is available at
12+
// https://www.gnu.org/software/classpath/license.html.
13+
//
14+
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
15+
// *****************************************************************************
16+
17+
import { ToolProvider, ToolRequest } from '@theia/ai-core';
18+
import { inject, injectable } from '@theia/core/shared/inversify';
19+
import { TaskService } from '@theia/task/lib/browser/task-service';
20+
import { TerminalService } from '@theia/terminal/lib/browser/base/terminal-service';
21+
import { MutableChatRequestModel } from '@theia/ai-chat';
22+
import { CancellationToken } from '@theia/core';
23+
24+
export const LIST_TASKS_ID = 'listTasks';
25+
export const RUN_TASK_ID = 'runTask';
26+
27+
@injectable()
28+
export class TaskListProvider implements ToolProvider {
29+
30+
@inject(TaskService)
31+
protected readonly taskService: TaskService;
32+
33+
getTool(): ToolRequest {
34+
return {
35+
id: LIST_TASKS_ID,
36+
name: LIST_TASKS_ID,
37+
description: 'Lists available tool tasks in the workspace, such as build, run, test. Tasks can be filtered by name.',
38+
parameters: {
39+
type: 'object',
40+
properties: {
41+
filter: {
42+
type: 'string',
43+
description: 'Filter to apply on task names (empty string to retrieve all tasks).'
44+
}
45+
},
46+
required: ['filter']
47+
},
48+
handler: async (argString: string) => {
49+
const filterArgs: { filter: string } = JSON.parse(argString);
50+
const tasks = await this.getAvailableTasks(filterArgs.filter);
51+
const taskString = JSON.stringify(tasks);
52+
return taskString;
53+
}
54+
};
55+
}
56+
private async getAvailableTasks(filter: string = ''): Promise<string[]> {
57+
const userActionToken = this.taskService.startUserAction();
58+
const tasks = await this.taskService.getTasks(userActionToken);
59+
const filteredTasks = tasks.filter(task => task.label.toLowerCase().includes(filter.toLowerCase()));
60+
return filteredTasks.map(task => task.label);
61+
}
62+
}
63+
64+
@injectable()
65+
export class TaskRunnerProvider implements ToolProvider {
66+
67+
@inject(TaskService)
68+
protected readonly taskService: TaskService;
69+
70+
@inject(TerminalService)
71+
protected readonly terminalService: TerminalService;
72+
73+
getTool(): ToolRequest {
74+
return {
75+
id: RUN_TASK_ID,
76+
name: RUN_TASK_ID,
77+
description: 'Executes a specified task.',
78+
parameters: {
79+
type: 'object',
80+
properties: {
81+
taskName: {
82+
type: 'string',
83+
description: 'The name of the task to execute.'
84+
}
85+
},
86+
required: ['taskName']
87+
},
88+
handler: async (argString: string, ctx: MutableChatRequestModel) => this.handleRunTask(argString, ctx?.response?.cancellationToken)
89+
90+
};
91+
}
92+
93+
private async handleRunTask(argString: string, cancellationToken?: CancellationToken): Promise<string> {
94+
try {
95+
const args: { taskName: string } = JSON.parse(argString);
96+
97+
const token = this.taskService.startUserAction();
98+
99+
const taskInfo = await this.taskService.runTaskByLabel(token, args.taskName);
100+
if (!taskInfo) {
101+
return `Did not find a task for the label: '${args.taskName}'`;
102+
}
103+
cancellationToken?.onCancellationRequested(() => {
104+
this.taskService.terminateTask(taskInfo);
105+
});
106+
107+
const signal = await this.taskService.getTerminateSignal(taskInfo.taskId);
108+
if (taskInfo.terminalId) {
109+
const terminal = this.terminalService.getByTerminalId(taskInfo.terminalId!);
110+
111+
const length = terminal?.buffer.length ?? 0;
112+
const numberOfLines = Math.min(length, 50);
113+
const result: string[] = [];
114+
const allLines = terminal?.buffer.getLines(0, length).reverse() ?? [];
115+
116+
// collect the first 50 lines:
117+
const firstLines = allLines.slice(0, numberOfLines);
118+
result.push(...firstLines);
119+
// collect the last 50 lines:
120+
if (length > numberOfLines) {
121+
const lastLines = allLines.slice(length - numberOfLines);
122+
result.push(...lastLines);
123+
}
124+
terminal?.clearOutput();
125+
return result.join('\n');
126+
}
127+
return `No terminal output available. The terminate signal was :${signal}.`;
128+
129+
} catch (error) {
130+
return JSON.stringify({ success: false, message: error.message || 'Failed to run task' });
131+
}
132+
}
133+
}
134+

packages/ai-ide/tsconfig.json

+3
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,9 @@
4242
{
4343
"path": "../search-in-workspace"
4444
},
45+
{
46+
"path": "../task"
47+
},
4548
{
4649
"path": "../terminal"
4750
},

0 commit comments

Comments
 (0)