Tasks API
Tasks APIは、Traeエディタ内でタスクの作成、管理、実行を行うための包括的なシステムを提供します。ビルド、テスト、リント、カスタムスクリプトなど、様々なタスクを効率的に処理できます。
概要
Tasks APIを使用すると、以下のことが可能になります:
- タスク管理: タスクの登録、実行、終了
- タスクプロバイダー: npm、スクリプト、カスタムタスクプロバイダー
- タスクテンプレート: 一般的なタスクの事前定義済みテンプレート
- タスク実行: シェル、プロセス、カスタムタスクの実行
- 変数解決: ワークスペースとユーザー変数の解決
- タスク履歴: 実行履歴の追跡と管理
- UIインテグレーション: タスクパネル、ステータスバー、コマンドパレット
基本的な使用方法
タスクマネージャー
typescript
import { TraeAPI } from '@trae/api';
class TaskManager {
private tasks: Map<string, Task> = new Map();
private taskProviders: TaskProvider[] = [];
private runningTasks: Map<string, TaskExecution> = new Map();
private taskHistory: TaskHistoryEntry[] = [];
private taskTemplates: Map<string, TaskTemplate> = new Map();
constructor() {
this.initializeBuiltinTasks();
this.initializeTaskProviders();
this.initializeTaskTemplates();
}
private initializeBuiltinTasks(): void {
// ビルトインタスクの初期化
const builtinTasks: Task[] = [
{
id: 'build',
label: 'Build',
type: 'shell',
command: 'npm',
args: ['run', 'build'],
group: 'build',
detail: 'プロジェクトをビルドします'
},
{
id: 'test',
label: 'Test',
type: 'shell',
command: 'npm',
args: ['test'],
group: 'test',
detail: 'テストを実行します'
},
{
id: 'lint',
label: 'Lint',
type: 'shell',
command: 'npm',
args: ['run', 'lint'],
group: 'build',
detail: 'コードをリントします'
},
{
id: 'dev',
label: 'Dev Server',
type: 'shell',
command: 'npm',
args: ['run', 'dev'],
group: 'build',
detail: '開発サーバーを起動します',
isBackground: true
}
];
builtinTasks.forEach(task => this.tasks.set(task.id, task));
}
// タスクの登録
registerTask(task: Task): void {
this.tasks.set(task.id, task);
this.notifyTaskChange('added', task);
}
// タスクの登録解除
unregisterTask(taskId: string): boolean {
const task = this.tasks.get(taskId);
if (task) {
this.tasks.delete(taskId);
this.notifyTaskChange('removed', task);
return true;
}
return false;
}
// タスクの取得
getTask(taskId: string): Task | undefined {
return this.tasks.get(taskId);
}
// 全タスクの取得
getTasks(): Task[] {
return Array.from(this.tasks.values());
}
// グループ別タスクの取得
getTasksByGroup(group: string): Task[] {
return this.getTasks().filter(task => task.group === group);
}
// タスクの実行
async executeTask(taskId: string, options?: TaskExecutionOptions): Promise<TaskExecution> {
const task = this.getTask(taskId);
if (!task) {
throw new Error(`Task not found: ${taskId}`);
}
// 変数の解決
const resolvedTask = await this.resolveTaskVariables(task);
// タスクの実行
const execution = await this.createTaskExecution(resolvedTask, options);
this.runningTasks.set(execution.id, execution);
// イベントの発火
this.notifyTaskStart(execution);
try {
await execution.start();
// 履歴に追加
this.addToHistory({
taskId: task.id,
startTime: execution.startTime,
endTime: Date.now(),
duration: Date.now() - execution.startTime,
success: execution.exitCode === 0,
exitCode: execution.exitCode
});
this.notifyTaskEnd(execution);
} catch (error) {
this.addToHistory({
taskId: task.id,
startTime: execution.startTime,
endTime: Date.now(),
duration: Date.now() - execution.startTime,
success: false,
exitCode: -1,
error: error.message
});
this.notifyTaskEnd(execution);
throw error;
} finally {
this.runningTasks.delete(execution.id);
}
return execution;
}
// タスクの実行(タイプ別)
private async createTaskExecution(task: Task, options?: TaskExecutionOptions): Promise<TaskExecution> {
switch (task.type) {
case 'shell':
return new ShellTaskExecution(task, options);
case 'process':
return new ProcessTaskExecution(task, options);
case 'custom':
return new CustomTaskExecution(task, options);
default:
throw new Error(`Unsupported task type: ${task.type}`);
}
}
// 変数の解決
private async resolveTaskVariables(task: Task): Promise<Task> {
const workspaceRoot = TraeAPI.workspace.workspaceFolders?.[0]?.uri.fsPath || '';
const userHome = process.env.HOME || process.env.USERPROFILE || '';
const variables = {
'${workspaceFolder}': workspaceRoot,
'${workspaceRoot}': workspaceRoot,
'${userHome}': userHome,
'${env:NODE_ENV}': process.env.NODE_ENV || 'development'
};
const resolveString = (str: string): string => {
let resolved = str;
for (const [variable, value] of Object.entries(variables)) {
resolved = resolved.replace(new RegExp(variable.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'), value);
}
return resolved;
};
return {
...task,
command: resolveString(task.command),
args: task.args?.map(resolveString),
cwd: task.cwd ? resolveString(task.cwd) : undefined,
env: task.env ? Object.fromEntries(
Object.entries(task.env).map(([key, value]) => [key, resolveString(value)])
) : undefined
};
}
// タスクの終了
async terminateTask(executionId: string): Promise<boolean> {
const execution = this.runningTasks.get(executionId);
if (execution) {
try {
await execution.terminate();
this.runningTasks.delete(executionId);
return true;
} catch (error) {
console.error('Failed to terminate task:', error);
return false;
}
}
return false;
}
// 実行中タスクの取得
getRunningTasks(): TaskExecution[] {
return Array.from(this.runningTasks.values());
}
// タスク履歴の取得
getTaskHistory(limit?: number): TaskHistoryEntry[] {
const history = [...this.taskHistory].reverse();
return limit ? history.slice(0, limit) : history;
}
// 履歴への追加
private addToHistory(entry: TaskHistoryEntry): void {
this.taskHistory.push(entry);
// 履歴の制限(最大100件)
if (this.taskHistory.length > 100) {
this.taskHistory = this.taskHistory.slice(-100);
}
}
// タスクプロバイダーの登録
registerTaskProvider(provider: TaskProvider): void {
this.taskProviders.push(provider);
}
// プロバイダータスクの取得
async getProvidedTasks(): Promise<Task[]> {
const allTasks: Task[] = [];
for (const provider of this.taskProviders) {
try {
const tasks = await provider.provideTasks();
allTasks.push(...tasks);
} catch (error) {
console.error(`Task provider error:`, error);
}
}
return allTasks;
}
// イベント通知
private notifyTaskChange(type: 'added' | 'removed', task: Task): void {
TraeAPI.events.emit('onDidChangeTask', { type, task });
}
private notifyTaskStart(execution: TaskExecution): void {
TraeAPI.events.emit('onDidStartTask', execution);
}
private notifyTaskEnd(execution: TaskExecution): void {
TraeAPI.events.emit('onDidEndTask', execution);
}
// リソースの解放
dispose(): void {
// 実行中のタスクを終了
for (const execution of this.runningTasks.values()) {
execution.terminate().catch(console.error);
}
this.runningTasks.clear();
// プロバイダーの解放
for (const provider of this.taskProviders) {
if (provider.dispose) {
provider.dispose();
}
}
this.taskProviders.length = 0;
}
}タスクプロバイダー
NPMタスクプロバイダー
typescript
class NpmTaskProvider implements TaskProvider {
async provideTasks(): Promise<Task[]> {
const packageJsonPath = path.join(TraeAPI.workspace.workspaceFolders?.[0]?.uri.fsPath || '', 'package.json');
if (!fs.existsSync(packageJsonPath)) {
return [];
}
try {
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
const scripts = packageJson.scripts || {};
return Object.entries(scripts).map(([name, script]) => ({
id: `npm:${name}`,
label: `npm: ${name}`,
type: 'shell' as const,
command: 'npm',
args: ['run', name],
group: this.getTaskGroup(name),
detail: `npm run ${name}`,
source: 'npm'
}));
} catch (error) {
console.error('Failed to parse package.json:', error);
return [];
}
}
private getTaskGroup(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 'misc';
}
}スクリプトタスクプロバイダー
typescript
class ScriptTaskProvider implements TaskProvider {
async provideTasks(): Promise<Task[]> {
const workspaceRoot = TraeAPI.workspace.workspaceFolders?.[0]?.uri.fsPath;
if (!workspaceRoot) return [];
const tasks: Task[] = [];
// .scriptsディレクトリのスクリプトを検索
const scriptsDir = path.join(workspaceRoot, '.scripts');
if (fs.existsSync(scriptsDir)) {
const scriptFiles = fs.readdirSync(scriptsDir)
.filter(file => file.endsWith('.sh') || file.endsWith('.bat') || file.endsWith('.ps1'));
for (const file of scriptFiles) {
const name = path.basename(file, path.extname(file));
const scriptPath = path.join(scriptsDir, file);
tasks.push({
id: `script:${name}`,
label: `Script: ${name}`,
type: 'shell',
command: this.getShellCommand(file),
args: [scriptPath],
group: 'misc',
detail: `Run script: ${file}`,
source: 'scripts'
});
}
}
return tasks;
}
private getShellCommand(filename: string): string {
if (filename.endsWith('.sh')) return 'bash';
if (filename.endsWith('.bat')) return 'cmd';
if (filename.endsWith('.ps1')) return 'powershell';
return 'node';
}
}タスクテンプレート
typescript
class TaskTemplateManager {
private templates: Map<string, TaskTemplate> = new Map();
constructor() {
this.initializeBuiltinTemplates();
}
private initializeBuiltinTemplates(): void {
const builtinTemplates: TaskTemplate[] = [
{
id: 'npm-build',
name: 'NPM Build',
description: 'NPMビルドタスクを作成します',
template: {
type: 'shell',
command: 'npm',
args: ['run', 'build'],
group: 'build'
}
},
{
id: 'npm-test',
name: 'NPM Test',
description: 'NPMテストタスクを作成します',
template: {
type: 'shell',
command: 'npm',
args: ['test'],
group: 'test'
}
},
{
id: 'docker-build',
name: 'Docker Build',
description: 'Dockerビルドタスクを作成します',
template: {
type: 'shell',
command: 'docker',
args: ['build', '-t', '${input:imageName}', '.'],
group: 'build'
}
},
{
id: 'python-run',
name: 'Python Run',
description: 'Pythonスクリプト実行タスクを作成します',
template: {
type: 'shell',
command: 'python',
args: ['${input:scriptPath}'],
group: 'misc'
}
}
];
builtinTemplates.forEach(template => this.templates.set(template.id, template));
}
getTemplate(id: string): TaskTemplate | undefined {
return this.templates.get(id);
}
getTemplates(): TaskTemplate[] {
return Array.from(this.templates.values());
}
registerTemplate(template: TaskTemplate): void {
this.templates.set(template.id, template);
}
createTaskFromTemplate(templateId: string, options: TaskCreationOptions): Task {
const template = this.getTemplate(templateId);
if (!template) {
throw new Error(`Template not found: ${templateId}`);
}
const taskId = options.id || `${templateId}-${Date.now()}`;
return {
id: taskId,
label: options.label || template.name,
...template.template,
detail: options.detail || template.description
};
}
}イベント処理
typescript
// タスク開始イベント
TraeAPI.events.on('onDidStartTask', (execution: TaskExecution) => {
console.log(`Task started: ${execution.task.label}`);
// ステータスバーの更新
TraeAPI.window.setStatusBarMessage(
`$(sync~spin) Running: ${execution.task.label}`,
execution.promise
);
});
// タスク終了イベント
TraeAPI.events.on('onDidEndTask', (execution: TaskExecution) => {
console.log(`Task ended: ${execution.task.label}, Exit code: ${execution.exitCode}`);
if (execution.exitCode === 0) {
TraeAPI.window.showInformationMessage(`Task '${execution.task.label}' completed successfully`);
} else {
TraeAPI.window.showErrorMessage(`Task '${execution.task.label}' failed with exit code ${execution.exitCode}`);
}
});
// タスク変更イベント
TraeAPI.events.on('onDidChangeTask', (event: TaskChangeEvent) => {
console.log(`Task ${event.type}: ${event.task.label}`);
});タスク実行
TaskExecutionクラス
typescript
abstract class TaskExecution {
public readonly id: string;
public readonly task: Task;
public readonly startTime: number;
public exitCode: number = 0;
public promise: Promise<void>;
private output: string[] = [];
private outputChannel: TraeAPI.OutputChannel;
constructor(task: Task, options?: TaskExecutionOptions) {
this.id = `${task.id}-${Date.now()}`;
this.task = task;
this.startTime = Date.now();
this.outputChannel = TraeAPI.window.createOutputChannel(`Task: ${task.label}`);
if (options?.showOutput) {
this.outputChannel.show();
}
}
abstract start(): Promise<void>;
abstract terminate(): Promise<void>;
protected addOutput(data: string): void {
this.output.push(data);
this.outputChannel.append(data);
}
getOutput(): string[] {
return [...this.output];
}
dispose(): void {
this.outputChannel.dispose();
}
}
class ShellTaskExecution extends TaskExecution {
private process?: ChildProcess;
async start(): Promise<void> {
return new Promise((resolve, reject) => {
const command = this.task.command;
const args = this.task.args || [];
const options = {
cwd: this.task.cwd || TraeAPI.workspace.workspaceFolders?.[0]?.uri.fsPath,
env: { ...process.env, ...this.task.env },
shell: true
};
this.addOutput(`> ${command} ${args.join(' ')}\n`);
this.process = spawn(command, args, options);
this.process.stdout?.on('data', (data) => {
this.addOutput(data.toString());
});
this.process.stderr?.on('data', (data) => {
this.addOutput(data.toString());
});
this.process.on('close', (code) => {
this.exitCode = code || 0;
this.addOutput(`\nProcess exited with code ${this.exitCode}\n`);
if (this.exitCode === 0) {
resolve();
} else {
reject(new Error(`Process exited with code ${this.exitCode}`));
}
});
this.process.on('error', (error) => {
this.addOutput(`\nError: ${error.message}\n`);
reject(error);
});
});
}
async terminate(): Promise<void> {
if (this.process && !this.process.killed) {
this.process.kill('SIGTERM');
// 強制終了のタイムアウト
setTimeout(() => {
if (this.process && !this.process.killed) {
this.process.kill('SIGKILL');
}
}, 5000);
}
}
}
class ProcessTaskExecution extends TaskExecution {
private process?: ChildProcess;
async start(): Promise<void> {
return new Promise((resolve, reject) => {
const options = {
cwd: this.task.cwd || TraeAPI.workspace.workspaceFolders?.[0]?.uri.fsPath,
env: { ...process.env, ...this.task.env }
};
this.addOutput(`> ${this.task.command} ${(this.task.args || []).join(' ')}\n`);
this.process = spawn(this.task.command, this.task.args || [], options);
this.process.stdout?.on('data', (data) => {
this.addOutput(data.toString());
});
this.process.stderr?.on('data', (data) => {
this.addOutput(data.toString());
});
this.process.on('close', (code) => {
this.exitCode = code || 0;
this.addOutput(`\nProcess exited with code ${this.exitCode}\n`);
if (this.exitCode === 0) {
resolve();
} else {
reject(new Error(`Process exited with code ${this.exitCode}`));
}
});
this.process.on('error', (error) => {
this.addOutput(`\nError: ${error.message}\n`);
reject(error);
});
});
}
async terminate(): Promise<void> {
if (this.process && !this.process.killed) {
this.process.kill();
}
}
}
class CustomTaskExecution extends TaskExecution {
async start(): Promise<void> {
// カスタムタスクの実装
if (this.task.customHandler) {
try {
await this.task.customHandler(this);
} catch (error) {
this.exitCode = 1;
throw error;
}
} else {
throw new Error('Custom task handler not provided');
}
}
async terminate(): Promise<void> {
// カスタムタスクの終了処理
if (this.task.customTerminator) {
await this.task.customTerminator(this);
}
}
}UIプロバイダー
typescript
class TaskUIProvider {
private taskManager: TaskManager;
private statusBarItem: TraeAPI.StatusBarItem;
private taskPanel?: TraeAPI.WebviewPanel;
constructor(private taskManager: TaskManager) {
this.setupCommands();
this.setupStatusBar();
this.setupEventListeners();
}
private setupCommands(): void {
// タスクコマンドの登録
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('利用可能なタスクがありません');
return;
}
const items = tasks.map(task => ({
label: task.label,
description: task.detail || task.command,
detail: `グループ: ${task.group || 'なし'}`,
task
}));
const selected = await TraeAPI.window.showQuickPick(items, {
placeHolder: '実行するタスクを選択してください',
matchOnDescription: true,
matchOnDetail: true
});
if (selected) {
try {
await this.taskManager.executeTask(selected.task.id);
} catch (error) {
TraeAPI.window.showErrorMessage(`タスクの実行に失敗しました: ${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(`${group}タスクが利用できません`);
return;
}
if (groupTasks.length === 1) {
try {
await this.taskManager.executeTask(groupTasks[0].id);
} catch (error) {
TraeAPI.window.showErrorMessage(`タスクの実行に失敗しました: ${error}`);
}
return;
}
const items = groupTasks.map(task => ({
label: task.label,
description: task.detail || task.command,
task
}));
const selected = await TraeAPI.window.showQuickPick(items, {
placeHolder: `実行する${group}タスクを選択してください`,
matchOnDescription: true
});
if (selected) {
try {
await this.taskManager.executeTask(selected.task.id);
} catch (error) {
TraeAPI.window.showErrorMessage(`タスクの実行に失敗しました: ${error}`);
}
}
}
private async showRunningTaskPicker(): Promise<void> {
const runningTasks = this.taskManager.getRunningTasks();
if (runningTasks.length === 0) {
TraeAPI.window.showInformationMessage('現在実行中のタスクはありません');
return;
}
const items = runningTasks.map(runningTask => ({
label: runningTask.task.label,
description: `実行時間: ${Math.floor((Date.now() - runningTask.startTime) / 1000)}秒`,
detail: runningTask.task.command,
runningTask
}));
const selected = await TraeAPI.window.showQuickPick(items, {
placeHolder: '終了するタスクを選択してください',
matchOnDescription: true
});
if (selected) {
const confirmed = await TraeAPI.window.showWarningMessage(
`'${selected.runningTask.task.label}'を終了してもよろしいですか?`,
'終了',
'キャンセル'
);
if (confirmed === '終了') {
const success = await this.taskManager.terminateTask(selected.runningTask.id);
if (success) {
TraeAPI.window.showInformationMessage('タスクを終了しました');
} else {
TraeAPI.window.showErrorMessage('タスクの終了に失敗しました');
}
}
}
}
private async showRestartTaskPicker(): Promise<void> {
const history = this.taskManager.getTaskHistory(10);
if (history.length === 0) {
TraeAPI.window.showInformationMessage('再実行可能な最近のタスクがありません');
return;
}
const items = history.map(entry => {
const task = this.taskManager.getTask(entry.taskId);
if (!task) return null;
return {
label: task.label,
description: `最終実行: ${new Date(entry.endTime).toLocaleTimeString()}`,
detail: `実行時間: ${entry.duration}ms, ${entry.success ? '成功' : '失敗'}`,
task
};
}).filter(Boolean) as any[];
if (items.length === 0) {
TraeAPI.window.showInformationMessage('再実行可能なタスクがありません');
return;
}
const selected = await TraeAPI.window.showQuickPick(items, {
placeHolder: '再実行するタスクを選択してください',
matchOnDescription: true
});
if (selected) {
try {
await this.taskManager.executeTask(selected.task.id);
} catch (error) {
TraeAPI.window.showErrorMessage(`タスクの再実行に失敗しました: ${error}`);
}
}
}
private async showTaskPanel(): Promise<void> {
if (this.taskPanel) {
this.taskPanel.reveal();
return;
}
this.taskPanel = TraeAPI.window.createWebviewPanel(
'tasks',
'タスク',
TraeAPI.ViewColumn.One,
{
enableScripts: true,
retainContextWhenHidden: true
}
);
this.taskPanel.webview.html = this.getTaskHTML();
this.setupWebviewMessageHandling();
this.taskPanel.onDidDispose(() => {
this.taskPanel = null;
});
// 初期データの送信
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>タスク</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">実行中のタスク</div>
<div id="runningTasks" class="task-list">
<div class="no-tasks">現在実行中のタスクはありません</div>
</div>
</div>
<div class="section">
<div class="section-title">利用可能なタスク</div>
<div id="availableTasks" class="task-list">
<div class="no-tasks">タスクを読み込み中...</div>
</div>
</div>
<div class="section">
<div class="section-title">タスクテンプレート</div>
<div id="taskTemplates" class="templates-grid">
<div class="no-tasks">テンプレートを読み込み中...</div>
</div>
</div>
<div class="section">
<div class="section-title">最近のタスク</div>
<div id="taskHistory">
<div class="no-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">現在実行中のタスクはありません</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">実行中</div>
</div>
<div class="task-command">\${task.task.command} \${(task.task.args || []).join(' ')}</div>
<div class="task-details">
<span>開始時刻: \${new Date(task.startTime).toLocaleTimeString()}</span>
<span>実行時間: \${Math.floor((Date.now() - task.startTime) / 1000)}秒</span>
</div>
<div class="task-actions">
<button class="task-button danger" onclick="terminateTask('\${task.id}')">終了</button>
</div>
</div>
\`).join('');
}
function updateAvailableTasks(tasks) {
const container = document.getElementById('availableTasks');
if (tasks.length === 0) {
container.innerHTML = '<div class="no-tasks">利用可能なタスクがありません</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>グループ: \${task.group || 'なし'}</span>
<span>タイプ: \${task.type}</span>
</div>
</div>
\`).join('');
}
function updateTaskTemplates(templates) {
const container = document.getElementById('taskTemplates');
if (templates.length === 0) {
container.innerHTML = '<div class="no-tasks">利用可能なテンプレートがありません</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">最近のタスクはありません</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>実行時間: \${entry.duration}ms</span>
<span>ステータス: \${entry.success ? '成功' : '失敗'}</span>
<span>終了コード: \${entry.exitCode}</span>
</div>
</div>
\`).join('');
}
// 拡張機能からのメッセージを処理
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(`タスクの実行に失敗しました: ${error}`);
}
break;
case 'terminateTask':
const success = await this.taskManager.terminateTask(message.executionId);
if (!success) {
TraeAPI.window.showErrorMessage('タスクの終了に失敗しました');
}
break;
case 'createFromTemplate':
await this.createTaskFromTemplate(message.templateId);
break;
}
});
}
private async createTaskFromTemplate(templateId: string): Promise<void> {
const label = await TraeAPI.window.showInputBox({
prompt: 'タスクラベルを入力してください',
placeHolder: 'マイカスタムタスク'
});
if (!label) return;
try {
const task = this.taskManager.createTaskFromTemplate(templateId, { label });
this.taskManager.registerTask(task);
TraeAPI.window.showInformationMessage(`タスク '${label}' が正常に作成されました`);
this.updateTaskPanel();
} catch (error) {
TraeAPI.window.showErrorMessage(`タスクの作成に失敗しました: ${error}`);
}
}
private async updateTaskPanel(): Promise<void> {
if (!this.taskPanel) return;
// 実行中タスクの更新
const runningTasks = this.taskManager.getRunningTasks();
this.taskPanel.webview.postMessage({
type: 'updateRunningTasks',
tasks: runningTasks
});
// 利用可能タスクの更新
const allTasks = this.taskManager.getTasks();
const providedTasks = await this.taskManager.getProvidedTasks();
this.taskPanel.webview.postMessage({
type: 'updateAvailableTasks',
tasks: [...allTasks, ...providedTasks]
});
// テンプレートの更新
const templates = this.taskManager.getTemplates();
this.taskPanel.webview.postMessage({
type: 'updateTaskTemplates',
templates
});
// 履歴の更新
const history = this.taskManager.getTaskHistory(10);
this.taskPanel.webview.postMessage({
type: 'updateTaskHistory',
history
});
}
dispose(): void {
this.statusBarItem.dispose();
this.taskPanel?.dispose();
}
}インターフェース定義
typescript
interface Task {
id: string;
label: string;
type: 'shell' | 'process' | 'custom';
command: string;
args?: string[];
cwd?: string;
env?: Record<string, string>;
group?: string;
detail?: string;
source?: string;
isBackground?: boolean;
customHandler?: (execution: TaskExecution) => Promise<void>;
customTerminator?: (execution: TaskExecution) => Promise<void>;
}
interface TaskProvider {
provideTasks(): Promise<Task[]>;
dispose?(): void;
}
interface TaskTemplate {
id: string;
name: string;
description: string;
template: Partial<Task>;
}
interface TaskExecutionOptions {
showOutput?: boolean;
cwd?: string;
env?: Record<string, string>;
}
interface TaskCreationOptions {
id?: string;
label: string;
detail?: string;
}
interface TaskHistoryEntry {
taskId: string;
startTime: number;
endTime: number;
duration: number;
success: boolean;
exitCode: number;
error?: string;
}
interface TaskChangeEvent {
type: 'added' | 'removed';
task: Task;
}API リファレンス
コアインターフェース
TaskController
typescript
interface TaskController {
// タスク管理
registerTask(task: Task): void;
unregisterTask(taskId: string): boolean;
getTask(taskId: string): Task | undefined;
getTasks(): Task[];
getTasksByGroup(group: string): Task[];
// タスク実行
executeTask(taskId: string, options?: TaskExecutionOptions): Promise<TaskExecution>;
terminateTask(executionId: string): Promise<boolean>;
getRunningTasks(): TaskExecution[];
// プロバイダー
registerTaskProvider(provider: TaskProvider): void;
getProvidedTasks(): Promise<Task[]>;
// テンプレート
registerTemplate(template: TaskTemplate): void;
getTemplate(id: string): TaskTemplate | undefined;
getTemplates(): TaskTemplate[];
createTaskFromTemplate(templateId: string, options: TaskCreationOptions): Task;
// 履歴
getTaskHistory(limit?: number): TaskHistoryEntry[];
// イベント
onDidStartTask: TraeAPI.Event<TaskExecution>;
onDidEndTask: TraeAPI.Event<TaskExecution>;
onDidChangeTask: TraeAPI.Event<TaskChangeEvent>;
}TaskItem
typescript
interface TaskItem extends TraeAPI.TreeItem {
task: Task;
execution?: TaskExecution;
}TaskRun
typescript
interface TaskRun {
task: Task;
execution: TaskExecution;
token: TraeAPI.CancellationToken;
}TaskRunRequest
typescript
interface TaskRunRequest {
include?: Task[];
exclude?: Task[];
continuous?: boolean;
}ベストプラクティス
1. 適切なタスクタイプの選択
typescript
// シェルコマンドの場合
const shellTask: Task = {
id: 'build',
label: 'Build Project',
type: 'shell',
command: 'npm run build'
};
// 直接プロセス実行の場合
const processTask: Task = {
id: 'test',
label: 'Run Tests',
type: 'process',
command: 'node',
args: ['test.js']
};
// カスタム処理の場合
const customTask: Task = {
id: 'custom',
label: 'Custom Task',
type: 'custom',
command: 'custom',
customHandler: async (execution) => {
// カスタム処理の実装
}
};2. 変数の活用
typescript
const task: Task = {
id: 'build-workspace',
label: 'Build Workspace',
type: 'shell',
command: 'npm',
args: ['run', 'build'],
cwd: '${workspaceFolder}',
env: {
NODE_ENV: '${env:NODE_ENV}'
}
};3. エラーハンドリング
typescript
try {
const execution = await taskManager.executeTask('build');
console.log('Task completed successfully');
} catch (error) {
console.error('Task failed:', error);
TraeAPI.window.showErrorMessage(`Build failed: ${error.message}`);
}4. リソース管理
typescript
class MyTaskProvider implements TaskProvider {
private disposables: TraeAPI.Disposable[] = [];
constructor() {
// イベントリスナーの登録
this.disposables.push(
TraeAPI.workspace.onDidChangeConfiguration(() => {
this.refresh();
})
);
}
async provideTasks(): Promise<Task[]> {
// タスクの提供
return [];
}
dispose(): void {
this.disposables.forEach(d => d.dispose());
}
}関連API
- Commands API - コマンドの登録と実行
- Workspace API - ワークスペース情報の取得