Tasks API
The Tasks API provides functionality for defining, executing, and managing tasks within the development environment.
Overview
The Tasks API enables you to:
- Define custom tasks for build, test, and deployment
- Execute tasks programmatically
- Monitor task execution and output
- Integrate with external tools and scripts
- Provide task templates and configurations
- Handle task dependencies and scheduling
- Support different task types (shell, process, custom)
Basic Usage
Task Manager
typescript
import { TraeAPI } from '@trae/api';
// Task manager
class TaskManager {
private tasks: Map<string, TaskDefinition> = new Map();
private runningTasks: Map<string, RunningTask> = new Map();
private taskHistory: TaskHistoryEntry[] = [];
private eventEmitter = new TraeAPI.EventEmitter<TaskEvent>();
private taskProviders: TaskProvider[] = [];
private taskTemplates: Map<string, TaskTemplate> = new Map();
constructor() {
this.initializeBuiltInTasks();
this.setupTaskProviders();
this.loadTaskTemplates();
}
// Initialize built-in tasks
private initializeBuiltInTasks(): void {
// Build tasks
this.registerTask({
id: 'build',
label: 'Build Project',
type: 'shell',
command: 'npm',
args: ['run', 'build'],
group: 'build',
presentation: {
echo: true,
reveal: 'always',
focus: false,
panel: 'shared'
},
problemMatcher: ['$tsc']
});
// Test tasks
this.registerTask({
id: 'test',
label: 'Run Tests',
type: 'shell',
command: 'npm',
args: ['test'],
group: 'test',
presentation: {
echo: true,
reveal: 'always',
focus: true,
panel: 'dedicated'
},
problemMatcher: ['$jest']
});
// Lint tasks
this.registerTask({
id: 'lint',
label: 'Lint Code',
type: 'shell',
command: 'npm',
args: ['run', 'lint'],
group: 'build',
presentation: {
echo: true,
reveal: 'silent',
focus: false,
panel: 'shared'
},
problemMatcher: ['$eslint-stylish']
});
// Development server
this.registerTask({
id: 'dev',
label: 'Start Development Server',
type: 'shell',
command: 'npm',
args: ['run', 'dev'],
group: 'build',
isBackground: true,
presentation: {
echo: true,
reveal: 'always',
focus: false,
panel: 'dedicated'
},
problemMatcher: {
pattern: {
regexp: '^(.*):(\\d+):(\\d+):\\s+(warning|error):\\s+(.*)$',
file: 1,
line: 2,
column: 3,
severity: 4,
message: 5
},
background: {
activeOnStart: true,
beginsPattern: '^.*compilation starting.*$',
endsPattern: '^.*compilation complete.*$'
}
}
});
}
// Register a task
registerTask(definition: TaskDefinition): void {
this.tasks.set(definition.id, definition);
this.eventEmitter.fire({ type: 'taskRegistered', task: definition });
}
// Unregister a task
unregisterTask(taskId: string): boolean {
const task = this.tasks.get(taskId);
if (task) {
this.tasks.delete(taskId);
this.eventEmitter.fire({ type: 'taskUnregistered', taskId });
return true;
}
return false;
}
// Get all tasks
getTasks(): TaskDefinition[] {
return Array.from(this.tasks.values());
}
// Get task by ID
getTask(taskId: string): TaskDefinition | undefined {
return this.tasks.get(taskId);
}
// Get tasks by group
getTasksByGroup(group: string): TaskDefinition[] {
return this.getTasks().filter(task => task.group === group);
}
// Execute a task
async executeTask(taskId: string, options: TaskExecutionOptions = {}): Promise<TaskExecution> {
const task = this.tasks.get(taskId);
if (!task) {
throw new Error(`Task '${taskId}' not found`);
}
// Check if task is already running and not allowed to run multiple instances
if (this.runningTasks.has(taskId) && !task.allowMultiple) {
throw new Error(`Task '${taskId}' is already running`);
}
const executionId = this.generateExecutionId();
const execution = new TaskExecution(executionId, task, options);
try {
// Resolve task variables
const resolvedTask = await this.resolveTaskVariables(task, options);
// Create running task
const runningTask: RunningTask = {
id: executionId,
task: resolvedTask,
execution,
startTime: Date.now(),
status: 'running'
};
this.runningTasks.set(executionId, runningTask);
this.eventEmitter.fire({ type: 'taskStarted', execution });
// Execute based on task type
let result: TaskResult;
switch (resolvedTask.type) {
case 'shell':
result = await this.executeShellTask(resolvedTask, execution);
break;
case 'process':
result = await this.executeProcessTask(resolvedTask, execution);
break;
case 'custom':
result = await this.executeCustomTask(resolvedTask, execution);
break;
default:
throw new Error(`Unsupported task type: ${resolvedTask.type}`);
}
// Update running task
runningTask.status = result.success ? 'completed' : 'failed';
runningTask.endTime = Date.now();
runningTask.result = result;
// Add to history
this.addToHistory({
taskId,
executionId,
startTime: runningTask.startTime,
endTime: runningTask.endTime!,
duration: runningTask.endTime! - runningTask.startTime,
success: result.success,
exitCode: result.exitCode
});
// Fire completion event
this.eventEmitter.fire({
type: result.success ? 'taskCompleted' : 'taskFailed',
execution,
result
});
// Remove from running tasks
this.runningTasks.delete(executionId);
return execution;
} catch (error) {
// Handle execution error
const runningTask = this.runningTasks.get(executionId);
if (runningTask) {
runningTask.status = 'failed';
runningTask.endTime = Date.now();
runningTask.error = error as Error;
}
this.eventEmitter.fire({
type: 'taskFailed',
execution,
error: error as Error
});
this.runningTasks.delete(executionId);
throw error;
}
}
// Execute shell task
private async executeShellTask(task: ResolvedTaskDefinition, execution: TaskExecution): Promise<TaskResult> {
const options: TraeAPI.ProcessExecutionOptions = {
cwd: task.options?.cwd || TraeAPI.workspace.rootPath,
env: { ...process.env, ...task.options?.env },
shell: task.options?.shell !== false
};
const args = task.args || [];
const fullCommand = `${task.command} ${args.join(' ')}`;
execution.appendOutput(`> Executing task: ${task.label}\n`);
execution.appendOutput(`> ${fullCommand}\n\n`);
return new Promise((resolve) => {
const process = TraeAPI.tasks.executeProcess(task.command, args, options);
let output = '';
let errorOutput = '';
process.onDidWrite(data => {
output += data;
execution.appendOutput(data);
});
process.onDidWriteErr(data => {
errorOutput += data;
execution.appendOutput(data);
});
process.onDidClose(exitCode => {
const success = exitCode === 0;
execution.appendOutput(`\n> Task ${success ? 'completed' : 'failed'} with exit code ${exitCode}\n`);
resolve({
success,
exitCode,
output,
errorOutput
});
});
// Store process reference for potential termination
execution.setProcess(process);
});
}
// Execute process task
private async executeProcessTask(task: ResolvedTaskDefinition, execution: TaskExecution): Promise<TaskResult> {
// Similar to shell task but without shell interpretation
const options: TraeAPI.ProcessExecutionOptions = {
cwd: task.options?.cwd || TraeAPI.workspace.rootPath,
env: { ...process.env, ...task.options?.env },
shell: false
};
const args = task.args || [];
execution.appendOutput(`> Executing process: ${task.label}\n`);
execution.appendOutput(`> ${task.command} ${args.join(' ')}\n\n`);
return new Promise((resolve) => {
const process = TraeAPI.tasks.executeProcess(task.command, args, options);
let output = '';
let errorOutput = '';
process.onDidWrite(data => {
output += data;
execution.appendOutput(data);
});
process.onDidWriteErr(data => {
errorOutput += data;
execution.appendOutput(data);
});
process.onDidClose(exitCode => {
const success = exitCode === 0;
execution.appendOutput(`\n> Process ${success ? 'completed' : 'failed'} with exit code ${exitCode}\n`);
resolve({
success,
exitCode,
output,
errorOutput
});
});
execution.setProcess(process);
});
}
// Execute custom task
private async executeCustomTask(task: ResolvedTaskDefinition, execution: TaskExecution): Promise<TaskResult> {
if (!task.customExecution) {
throw new Error('Custom task must have customExecution function');
}
execution.appendOutput(`> Executing custom task: ${task.label}\n\n`);
try {
const result = await task.customExecution(execution);
execution.appendOutput(`\n> Custom task ${result.success ? 'completed' : 'failed'}\n`);
return result;
} catch (error) {
execution.appendOutput(`\n> Custom task failed: ${error}\n`);
return {
success: false,
exitCode: 1,
output: '',
errorOutput: error instanceof Error ? error.message : String(error)
};
}
}
// Resolve task variables
private async resolveTaskVariables(task: TaskDefinition, options: TaskExecutionOptions): Promise<ResolvedTaskDefinition> {
const variables = {
workspaceFolder: TraeAPI.workspace.rootPath || '',
workspaceFolderBasename: TraeAPI.path.basename(TraeAPI.workspace.rootPath || ''),
file: TraeAPI.window.activeTextEditor?.document.uri.fsPath || '',
fileBasename: TraeAPI.path.basename(TraeAPI.window.activeTextEditor?.document.uri.fsPath || ''),
fileBasenameNoExtension: TraeAPI.path.parse(TraeAPI.window.activeTextEditor?.document.uri.fsPath || '').name,
fileDirname: TraeAPI.path.dirname(TraeAPI.window.activeTextEditor?.document.uri.fsPath || ''),
fileExtname: TraeAPI.path.extname(TraeAPI.window.activeTextEditor?.document.uri.fsPath || ''),
cwd: process.cwd(),
...options.variables
};
const resolveString = (str: string): string => {
return str.replace(/\$\{([^}]+)\}/g, (match, varName) => {
return variables[varName] || match;
});
};
const resolveArray = (arr: string[]): string[] => {
return arr.map(resolveString);
};
return {
...task,
command: resolveString(task.command),
args: task.args ? resolveArray(task.args) : undefined,
options: task.options ? {
...task.options,
cwd: task.options.cwd ? resolveString(task.options.cwd) : undefined,
env: task.options.env ? Object.fromEntries(
Object.entries(task.options.env).map(([key, value]) => [key, resolveString(value)])
) : undefined
} : undefined
};
}
// Terminate a running task
async terminateTask(executionId: string): Promise<boolean> {
const runningTask = this.runningTasks.get(executionId);
if (!runningTask) {
return false;
}
try {
await runningTask.execution.terminate();
runningTask.status = 'terminated';
runningTask.endTime = Date.now();
this.eventEmitter.fire({
type: 'taskTerminated',
execution: runningTask.execution
});
this.runningTasks.delete(executionId);
return true;
} catch (error) {
console.error('Failed to terminate task:', error);
return false;
}
}
// Get running tasks
getRunningTasks(): RunningTask[] {
return Array.from(this.runningTasks.values());
}
// Get task history
getTaskHistory(limit?: number): TaskHistoryEntry[] {
const history = [...this.taskHistory].reverse(); // Most recent first
return limit ? history.slice(0, limit) : history;
}
// Clear task history
clearTaskHistory(): void {
this.taskHistory = [];
}
// Add to history
private addToHistory(entry: TaskHistoryEntry): void {
this.taskHistory.push(entry);
// Limit history size
if (this.taskHistory.length > 1000) {
this.taskHistory = this.taskHistory.slice(-500); // Keep last 500 entries
}
}
// Generate execution ID
private generateExecutionId(): string {
return `task_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}
// Task providers
registerTaskProvider(provider: TaskProvider): TraeAPI.Disposable {
this.taskProviders.push(provider);
return {
dispose: () => {
const index = this.taskProviders.indexOf(provider);
if (index !== -1) {
this.taskProviders.splice(index, 1);
}
}
};
}
private setupTaskProviders(): void {
// Built-in npm task provider
this.registerTaskProvider(new NpmTaskProvider());
// Built-in script task provider
this.registerTaskProvider(new ScriptTaskProvider());
}
// Get tasks from providers
async getProvidedTasks(): Promise<TaskDefinition[]> {
const allTasks: TaskDefinition[] = [];
for (const provider of this.taskProviders) {
try {
const tasks = await provider.provideTasks();
allTasks.push(...tasks);
} catch (error) {
console.error('Task provider failed:', error);
}
}
return allTasks;
}
// Task templates
private loadTaskTemplates(): void {
// Build templates
this.taskTemplates.set('npm-build', {
id: 'npm-build',
name: 'NPM Build',
description: 'Build project using npm',
template: {
type: 'shell',
command: 'npm',
args: ['run', 'build'],
group: 'build',
presentation: {
echo: true,
reveal: 'always',
focus: false,
panel: 'shared'
}
}
});
this.taskTemplates.set('npm-test', {
id: 'npm-test',
name: 'NPM Test',
description: 'Run tests using npm',
template: {
type: 'shell',
command: 'npm',
args: ['test'],
group: 'test',
presentation: {
echo: true,
reveal: 'always',
focus: true,
panel: 'dedicated'
}
}
});
this.taskTemplates.set('docker-build', {
id: 'docker-build',
name: 'Docker Build',
description: 'Build Docker image',
template: {
type: 'shell',
command: 'docker',
args: ['build', '-t', '${workspaceFolderBasename}', '.'],
group: 'build',
presentation: {
echo: true,
reveal: 'always',
focus: false,
panel: 'shared'
}
}
});
this.taskTemplates.set('python-run', {
id: 'python-run',
name: 'Python Run',
description: 'Run Python script',
template: {
type: 'shell',
command: 'python',
args: ['${file}'],
group: 'build',
presentation: {
echo: true,
reveal: 'always',
focus: true,
panel: 'dedicated'
}
}
});
}
// Get task templates
getTaskTemplates(): TaskTemplate[] {
return Array.from(this.taskTemplates.values());
}
// Create task from template
createTaskFromTemplate(templateId: string, customization: Partial<TaskDefinition>): TaskDefinition {
const template = this.taskTemplates.get(templateId);
if (!template) {
throw new Error(`Task template '${templateId}' not found`);
}
const taskId = customization.id || `${templateId}_${Date.now()}`;
const label = customization.label || template.name;
return {
id: taskId,
label,
...template.template,
...customization
};
}
// Event handling
onDidStartTask(listener: (execution: TaskExecution) => void): TraeAPI.Disposable {
return this.eventEmitter.event(event => {
if (event.type === 'taskStarted') {
listener(event.execution);
}
});
}
onDidEndTask(listener: (execution: TaskExecution, result?: TaskResult) => void): TraeAPI.Disposable {
return this.eventEmitter.event(event => {
if (event.type === 'taskCompleted' || event.type === 'taskFailed' || event.type === 'taskTerminated') {
listener(event.execution, event.result);
}
});
}
onDidChangeTask(listener: (event: TaskEvent) => void): TraeAPI.Disposable {
return this.eventEmitter.event(listener);
}
// Dispose
dispose(): void {
// Terminate all running tasks
for (const [executionId] of this.runningTasks) {
this.terminateTask(executionId);
}
this.tasks.clear();
this.runningTasks.clear();
this.taskHistory = [];
this.taskProviders = [];
this.taskTemplates.clear();
this.eventEmitter.dispose();
}
}
// Task execution class
class TaskExecution {
private output = '';
private process: TraeAPI.ProcessExecution | null = null;
private outputChannel: TraeAPI.OutputChannel;
constructor(
public readonly id: string,
public readonly task: TaskDefinition,
public readonly options: TaskExecutionOptions
) {
this.outputChannel = TraeAPI.window.createOutputChannel(`Task: ${task.label}`);
if (task.presentation?.reveal === 'always' ||
(task.presentation?.reveal === 'silent' && task.presentation?.focus)) {
this.outputChannel.show(task.presentation?.focus);
}
}
appendOutput(data: string): void {
this.output += data;
this.outputChannel.append(data);
}
getOutput(): string {
return this.output;
}
setProcess(process: TraeAPI.ProcessExecution): void {
this.process = process;
}
async terminate(): Promise<void> {
if (this.process) {
await this.process.terminate();
}
}
dispose(): void {
this.outputChannel.dispose();
}
}
// NPM task provider
class NpmTaskProvider implements TaskProvider {
async provideTasks(): Promise<TaskDefinition[]> {
const tasks: TaskDefinition[] = [];
try {
const packageJsonPath = TraeAPI.path.join(TraeAPI.workspace.rootPath || '', 'package.json');
const packageJsonUri = TraeAPI.Uri.file(packageJsonPath);
const packageJsonContent = await TraeAPI.workspace.fs.readFile(packageJsonUri);
const packageJson = JSON.parse(packageJsonContent.toString());
if (packageJson.scripts) {
for (const [scriptName, scriptCommand] of Object.entries(packageJson.scripts)) {
tasks.push({
id: `npm:${scriptName}`,
label: `npm: ${scriptName}`,
type: 'shell',
command: 'npm',
args: ['run', scriptName],
group: this.getScriptGroup(scriptName),
detail: scriptCommand as string,
presentation: {
echo: true,
reveal: 'always',
focus: false,
panel: 'shared'
}
});
}
}
} catch (error) {
// No package.json or invalid JSON
}
return tasks;
}
private getScriptGroup(scriptName: string): string {
if (scriptName.includes('build')) return 'build';
if (scriptName.includes('test')) return 'test';
if (scriptName.includes('lint')) return 'build';
if (scriptName.includes('dev') || scriptName.includes('start')) return 'build';
return 'build';
}
}
// Script task provider
class ScriptTaskProvider implements TaskProvider {
async provideTasks(): Promise<TaskDefinition[]> {
const tasks: TaskDefinition[] = [];
try {
const workspaceFolder = TraeAPI.workspace.workspaceFolders?.[0];
if (!workspaceFolder) return tasks;
// Look for common script files
const scriptPatterns = ['**/*.sh', '**/*.bat', '**/*.cmd', '**/*.ps1'];
for (const pattern of scriptPatterns) {
const files = await TraeAPI.workspace.findFiles(pattern, '**/node_modules/**');
for (const file of files) {
const relativePath = TraeAPI.workspace.asRelativePath(file);
const fileName = TraeAPI.path.basename(file.fsPath);
const extension = TraeAPI.path.extname(fileName);
let command: string;
let args: string[] = [];
switch (extension) {
case '.sh':
command = 'bash';
args = [file.fsPath];
break;
case '.bat':
case '.cmd':
command = file.fsPath;
break;
case '.ps1':
command = 'powershell';
args = ['-File', file.fsPath];
break;
default:
continue;
}
tasks.push({
id: `script:${relativePath}`,
label: `Script: ${fileName}`,
type: 'shell',
command,
args,
group: 'build',
detail: relativePath,
presentation: {
echo: true,
reveal: 'always',
focus: false,
panel: 'shared'
}
});
}
}
} catch (error) {
console.error('Failed to provide script tasks:', error);
}
return tasks;
}
}
// Initialize task manager
const taskManager = new TaskManager();Task UI Integration
typescript
// Task UI provider
class TaskUIProvider {
private taskPanel: TraeAPI.WebviewPanel | null = null;
private statusBarItem: TraeAPI.StatusBarItem;
constructor(private taskManager: TaskManager) {
this.setupCommands();
this.setupStatusBar();
this.setupEventListeners();
}
private setupCommands(): void {
// Register task commands
TraeAPI.commands.registerCommand('tasks.showPanel', () => {
this.showTaskPanel();
});
TraeAPI.commands.registerCommand('tasks.runTask', async () => {
await this.showTaskPicker();
});
TraeAPI.commands.registerCommand('tasks.runBuildTask', async () => {
await this.runTaskByGroup('build');
});
TraeAPI.commands.registerCommand('tasks.runTestTask', async () => {
await this.runTaskByGroup('test');
});
TraeAPI.commands.registerCommand('tasks.terminateTask', async () => {
await this.showRunningTaskPicker();
});
TraeAPI.commands.registerCommand('tasks.restartTask', async () => {
await this.showRestartTaskPicker();
});
}
private setupStatusBar(): void {
this.statusBarItem = TraeAPI.window.createStatusBarItem(
TraeAPI.StatusBarAlignment.Left,
10
);
this.statusBarItem.text = '$(play) Tasks';
this.statusBarItem.tooltip = 'Run Task';
this.statusBarItem.command = 'tasks.runTask';
this.statusBarItem.show();
}
private async showTaskPicker(): Promise<void> {
const allTasks = this.taskManager.getTasks();
const providedTasks = await this.taskManager.getProvidedTasks();
const tasks = [...allTasks, ...providedTasks];
if (tasks.length === 0) {
TraeAPI.window.showInformationMessage('No tasks available');
return;
}
const items = tasks.map(task => ({
label: task.label,
description: task.detail || task.command,
detail: `Group: ${task.group || 'none'}`,
task
}));
const selected = await TraeAPI.window.showQuickPick(items, {
placeHolder: 'Select a task to run',
matchOnDescription: true,
matchOnDetail: true
});
if (selected) {
try {
await this.taskManager.executeTask(selected.task.id);
} catch (error) {
TraeAPI.window.showErrorMessage(`Failed to run task: ${error}`);
}
}
}
private async runTaskByGroup(group: string): Promise<void> {
const tasks = this.taskManager.getTasksByGroup(group);
const providedTasks = await this.taskManager.getProvidedTasks();
const groupTasks = [...tasks, ...providedTasks.filter(t => t.group === group)];
if (groupTasks.length === 0) {
TraeAPI.window.showInformationMessage(`No ${group} tasks available`);
return;
}
if (groupTasks.length === 1) {
try {
await this.taskManager.executeTask(groupTasks[0].id);
} catch (error) {
TraeAPI.window.showErrorMessage(`Failed to run task: ${error}`);
}
return;
}
const items = groupTasks.map(task => ({
label: task.label,
description: task.detail || task.command,
task
}));
const selected = await TraeAPI.window.showQuickPick(items, {
placeHolder: `Select a ${group} task to run`,
matchOnDescription: true
});
if (selected) {
try {
await this.taskManager.executeTask(selected.task.id);
} catch (error) {
TraeAPI.window.showErrorMessage(`Failed to run task: ${error}`);
}
}
}
private async showRunningTaskPicker(): Promise<void> {
const runningTasks = this.taskManager.getRunningTasks();
if (runningTasks.length === 0) {
TraeAPI.window.showInformationMessage('No tasks are currently running');
return;
}
const items = runningTasks.map(runningTask => ({
label: runningTask.task.label,
description: `Running for ${Math.floor((Date.now() - runningTask.startTime) / 1000)}s`,
detail: runningTask.task.command,
runningTask
}));
const selected = await TraeAPI.window.showQuickPick(items, {
placeHolder: 'Select a task to terminate',
matchOnDescription: true
});
if (selected) {
const confirmed = await TraeAPI.window.showWarningMessage(
`Are you sure you want to terminate '${selected.runningTask.task.label}'?`,
'Terminate',
'Cancel'
);
if (confirmed === 'Terminate') {
const success = await this.taskManager.terminateTask(selected.runningTask.id);
if (success) {
TraeAPI.window.showInformationMessage('Task terminated');
} else {
TraeAPI.window.showErrorMessage('Failed to terminate task');
}
}
}
}
private async showRestartTaskPicker(): Promise<void> {
const history = this.taskManager.getTaskHistory(10);
if (history.length === 0) {
TraeAPI.window.showInformationMessage('No recent tasks to restart');
return;
}
const items = history.map(entry => {
const task = this.taskManager.getTask(entry.taskId);
if (!task) return null;
return {
label: task.label,
description: `Last run: ${new Date(entry.endTime).toLocaleTimeString()}`,
detail: `Duration: ${entry.duration}ms, ${entry.success ? 'Success' : 'Failed'}`,
task
};
}).filter(Boolean) as any[];
if (items.length === 0) {
TraeAPI.window.showInformationMessage('No tasks available to restart');
return;
}
const selected = await TraeAPI.window.showQuickPick(items, {
placeHolder: 'Select a task to restart',
matchOnDescription: true
});
if (selected) {
try {
await this.taskManager.executeTask(selected.task.id);
} catch (error) {
TraeAPI.window.showErrorMessage(`Failed to restart task: ${error}`);
}
}
}
private async showTaskPanel(): Promise<void> {
if (this.taskPanel) {
this.taskPanel.reveal();
return;
}
this.taskPanel = TraeAPI.window.createWebviewPanel(
'tasks',
'Tasks',
TraeAPI.ViewColumn.One,
{
enableScripts: true,
retainContextWhenHidden: true
}
);
this.taskPanel.webview.html = this.getTaskHTML();
this.setupWebviewMessageHandling();
this.taskPanel.onDidDispose(() => {
this.taskPanel = null;
});
// Send initial data
this.updateTaskPanel();
}
private getTaskHTML(): string {
return `
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Tasks</title>
<style>
body {
margin: 0;
padding: 20px;
background: #1e1e1e;
color: #d4d4d4;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
font-size: 13px;
}
.tasks-container {
max-width: 1000px;
margin: 0 auto;
}
.section {
margin-bottom: 30px;
}
.section-title {
font-size: 16px;
font-weight: 600;
margin-bottom: 15px;
color: #ffffff;
border-bottom: 1px solid #3e3e42;
padding-bottom: 8px;
}
.task-list {
display: grid;
gap: 10px;
}
.task-item {
background: #2d2d30;
border: 1px solid #3e3e42;
border-radius: 6px;
padding: 15px;
cursor: pointer;
transition: all 0.2s ease;
}
.task-item:hover {
background: #37373d;
border-color: #007acc;
}
.task-item.running {
border-color: #f9c23c;
background: #3d3a2a;
}
.task-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.task-name {
font-weight: 500;
color: #ffffff;
}
.task-status {
padding: 2px 8px;
border-radius: 12px;
font-size: 11px;
font-weight: 500;
}
.task-status.running {
background: #f9c23c;
color: #000000;
}
.task-status.completed {
background: #4caf50;
color: #ffffff;
}
.task-status.failed {
background: #f44336;
color: #ffffff;
}
.task-command {
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
font-size: 12px;
color: #cccccc;
margin-bottom: 8px;
}
.task-details {
display: flex;
gap: 15px;
font-size: 11px;
color: #999999;
}
.task-actions {
display: flex;
gap: 8px;
margin-top: 10px;
}
.task-button {
padding: 4px 12px;
background: #0e639c;
border: none;
color: #ffffff;
border-radius: 3px;
cursor: pointer;
font-size: 11px;
}
.task-button:hover {
background: #1177bb;
}
.task-button.danger {
background: #d32f2f;
}
.task-button.danger:hover {
background: #f44336;
}
.no-tasks {
text-align: center;
color: #cccccc;
padding: 40px;
background: #2d2d30;
border-radius: 6px;
}
.history-item {
background: #2d2d30;
border: 1px solid #3e3e42;
border-radius: 6px;
padding: 12px;
margin-bottom: 8px;
}
.history-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 5px;
}
.history-name {
font-weight: 500;
}
.history-time {
font-size: 11px;
color: #999999;
}
.history-details {
display: flex;
gap: 15px;
font-size: 11px;
color: #cccccc;
}
.templates-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 15px;
}
.template-item {
background: #2d2d30;
border: 1px solid #3e3e42;
border-radius: 6px;
padding: 15px;
cursor: pointer;
transition: all 0.2s ease;
}
.template-item:hover {
background: #37373d;
border-color: #007acc;
}
.template-name {
font-weight: 500;
color: #ffffff;
margin-bottom: 8px;
}
.template-description {
font-size: 12px;
color: #cccccc;
margin-bottom: 10px;
}
.template-command {
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
font-size: 11px;
color: #999999;
}
</style>
</head>
<body>
<div class="tasks-container">
<div class="section">
<div class="section-title">Running Tasks</div>
<div id="runningTasks" class="task-list">
<div class="no-tasks">No tasks are currently running</div>
</div>
</div>
<div class="section">
<div class="section-title">Available Tasks</div>
<div id="availableTasks" class="task-list">
<div class="no-tasks">Loading tasks...</div>
</div>
</div>
<div class="section">
<div class="section-title">Task Templates</div>
<div id="taskTemplates" class="templates-grid">
<div class="no-tasks">Loading templates...</div>
</div>
</div>
<div class="section">
<div class="section-title">Recent Tasks</div>
<div id="taskHistory">
<div class="no-tasks">No recent tasks</div>
</div>
</div>
</div>
<script>
const vscode = acquireVsCodeApi();
function runTask(taskId) {
vscode.postMessage({
type: 'runTask',
taskId
});
}
function terminateTask(executionId) {
vscode.postMessage({
type: 'terminateTask',
executionId
});
}
function createFromTemplate(templateId) {
vscode.postMessage({
type: 'createFromTemplate',
templateId
});
}
function updateRunningTasks(tasks) {
const container = document.getElementById('runningTasks');
if (tasks.length === 0) {
container.innerHTML = '<div class="no-tasks">No tasks are currently running</div>';
return;
}
container.innerHTML = tasks.map(task => `
<div class="task-item running">
<div class="task-header">
<div class="task-name">${task.task.label}</div>
<div class="task-status running">Running</div>
</div>
<div class="task-command">${task.task.command} ${(task.task.args || []).join(' ')}</div>
<div class="task-details">
<span>Started: ${new Date(task.startTime).toLocaleTimeString()}</span>
<span>Duration: ${Math.floor((Date.now() - task.startTime) / 1000)}s</span>
</div>
<div class="task-actions">
<button class="task-button danger" onclick="terminateTask('${task.id}')">Terminate</button>
</div>
</div>
`).join('');
}
function updateAvailableTasks(tasks) {
const container = document.getElementById('availableTasks');
if (tasks.length === 0) {
container.innerHTML = '<div class="no-tasks">No tasks available</div>';
return;
}
container.innerHTML = tasks.map(task => `
<div class="task-item" onclick="runTask('${task.id}')">
<div class="task-header">
<div class="task-name">${task.label}</div>
</div>
<div class="task-command">${task.command} ${(task.args || []).join(' ')}</div>
<div class="task-details">
<span>Group: ${task.group || 'none'}</span>
<span>Type: ${task.type}</span>
</div>
</div>
`).join('');
}
function updateTaskTemplates(templates) {
const container = document.getElementById('taskTemplates');
if (templates.length === 0) {
container.innerHTML = '<div class="no-tasks">No templates available</div>';
return;
}
container.innerHTML = templates.map(template => `
<div class="template-item" onclick="createFromTemplate('${template.id}')">
<div class="template-name">${template.name}</div>
<div class="template-description">${template.description}</div>
<div class="template-command">${template.template.command} ${(template.template.args || []).join(' ')}</div>
</div>
`).join('');
}
function updateTaskHistory(history) {
const container = document.getElementById('taskHistory');
if (history.length === 0) {
container.innerHTML = '<div class="no-tasks">No recent tasks</div>';
return;
}
container.innerHTML = history.map(entry => `
<div class="history-item">
<div class="history-header">
<div class="history-name">${entry.taskId}</div>
<div class="history-time">${new Date(entry.endTime).toLocaleString()}</div>
</div>
<div class="history-details">
<span>Duration: ${entry.duration}ms</span>
<span>Status: ${entry.success ? 'Success' : 'Failed'}</span>
<span>Exit Code: ${entry.exitCode}</span>
</div>
</div>
`).join('');
}
// Handle messages from extension
window.addEventListener('message', event => {
const message = event.data;
switch (message.type) {
case 'updateRunningTasks':
updateRunningTasks(message.tasks);
break;
case 'updateAvailableTasks':
updateAvailableTasks(message.tasks);
break;
case 'updateTaskTemplates':
updateTaskTemplates(message.templates);
break;
case 'updateTaskHistory':
updateTaskHistory(message.history);
break;
}
});
</script>
</body>
</html>
`;
}
private setupWebviewMessageHandling(): void {
if (!this.taskPanel) return;
this.taskPanel.webview.onDidReceiveMessage(async message => {
switch (message.type) {
case 'runTask':
try {
await this.taskManager.executeTask(message.taskId);
} catch (error) {
TraeAPI.window.showErrorMessage(`Failed to run task: ${error}`);
}
break;
case 'terminateTask':
const success = await this.taskManager.terminateTask(message.executionId);
if (!success) {
TraeAPI.window.showErrorMessage('Failed to terminate task');
}
break;
case 'createFromTemplate':
await this.createTaskFromTemplate(message.templateId);
break;
}
});
}
private async createTaskFromTemplate(templateId: string): Promise<void> {
const label = await TraeAPI.window.showInputBox({
prompt: 'Enter task label',
placeHolder: 'My Custom Task'
});
if (!label) return;
try {
const task = this.taskManager.createTaskFromTemplate(templateId, { label });
this.taskManager.registerTask(task);
TraeAPI.window.showInformationMessage(`Task '${label}' created successfully`);
this.updateTaskPanel();
} catch (error) {
TraeAPI.window.showErrorMessage(`Failed to create task: ${error}`);
}
}
private async updateTaskPanel(): Promise<void> {
if (!this.taskPanel) return;
// Update running tasks
const runningTasks = this.taskManager.getRunningTasks();
this.taskPanel.webview.postMessage({
type: 'updateRunningTasks',
tasks: runningTasks
});
// Update available tasks
const allTasks = this.taskManager.getTasks();
const providedTasks = await this.taskManager.getProvidedTasks();
this.taskPanel.webview.postMessage({
type: 'updateAvailableTasks',
tasks: [...allTasks, ...providedTasks]
});
// Update templates
const templates = this.taskManager.getTaskTemplates();
this.taskPanel.webview.postMessage({
type: 'updateTaskTemplates',
templates
});
// Update history
const history = this.taskManager.getTaskHistory(10);
this.taskPanel.webview.postMessage({
type: 'updateTaskHistory',
history
});
}
private setupEventListeners(): void {
// Update status bar based on running tasks
this.taskManager.onDidChangeTask(event => {
const runningTasks = this.taskManager.getRunningTasks();
if (runningTasks.length > 0) {
this.statusBarItem.text = `$(sync~spin) ${runningTasks.length} task(s)`;
this.statusBarItem.tooltip = `${runningTasks.length} task(s) running`;
} else {
this.statusBarItem.text = '$(play) Tasks';
this.statusBarItem.tooltip = 'Run Task';
}
// Update task panel if open
this.updateTaskPanel();
});
}
}
// Initialize task UI
const taskUIProvider = new TaskUIProvider(taskManager);