Co-authored-by: Max Tuzzolino <maxtuzz@Maxs-MacBook-Pro.local>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Max Tuzzolino <max.tuzsmith@gmail.com>
Co-authored-by: Ralph Khreish <35776126+Crunchyman-ralph@users.noreply.github.com>
This commit is contained in:
Eyal Toledano
2025-09-19 18:08:20 -04:00
committed by GitHub
parent 4e126430a0
commit fce841490a
55 changed files with 3559 additions and 693 deletions

View File

@@ -11,7 +11,6 @@ interface TaskMetadataSidebarProps {
tasks: TaskMasterTask[];
complexity: any;
isSubtask: boolean;
sendMessage: (message: any) => Promise<any>;
onStatusChange: (status: TaskMasterTask['status']) => void;
onDependencyClick: (depId: string) => void;
isRegenerating?: boolean;
@@ -23,13 +22,12 @@ export const TaskMetadataSidebar: React.FC<TaskMetadataSidebarProps> = ({
tasks,
complexity,
isSubtask,
sendMessage,
onStatusChange,
onDependencyClick,
isRegenerating = false,
isAppending = false
}) => {
const { vscode } = useVSCodeContext();
const { sendMessage } = useVSCodeContext();
const [isLoadingComplexity, setIsLoadingComplexity] = useState(false);
const [mcpComplexityScore, setMcpComplexityScore] = useState<
number | undefined
@@ -101,26 +99,37 @@ export const TaskMetadataSidebar: React.FC<TaskMetadataSidebarProps> = ({
};
// Handle starting a task
const handleStartTask = () => {
const handleStartTask = async () => {
if (!currentTask || isStartingTask) {
return;
}
setIsStartingTask(true);
// Send message to extension to open terminal
if (vscode) {
vscode.postMessage({
try {
// Send message to extension to open terminal
const result = await sendMessage({
type: 'openTerminal',
taskId: currentTask.id,
taskTitle: currentTask.title
data: {
taskId: currentTask.id,
taskTitle: currentTask.title
}
});
}
// Reset loading state after a short delay
setTimeout(() => {
// Handle the response
if (result && !result.success) {
console.error('Terminal execution failed:', result.error);
// The extension will show VS Code error notification and webview toast
} else if (result && result.success) {
console.log('Terminal started successfully:', result.terminalName);
}
} catch (error) {
console.error('Failed to start task:', error);
// This handles network/communication errors
} finally {
// Reset loading state
setIsStartingTask(false);
}, 500);
}
};
// Effect to handle complexity on task change

View File

@@ -208,7 +208,6 @@ export const TaskDetailsView: React.FC<TaskDetailsViewProps> = ({
tasks={allTasks}
complexity={complexity}
isSubtask={isSubtask}
sendMessage={sendMessage}
onStatusChange={handleStatusChange}
onDependencyClick={handleDependencyClick}
/>

View File

@@ -8,6 +8,7 @@ import { ConfigService } from './services/config-service';
import { PollingService } from './services/polling-service';
import { createPollingStrategy } from './services/polling-strategies';
import { TaskRepository } from './services/task-repository';
import { TerminalManager } from './services/terminal-manager';
import { WebviewManager } from './services/webview-manager';
import { EventEmitter } from './utils/event-emitter';
import { ExtensionLogger } from './utils/logger';
@@ -22,6 +23,7 @@ let logger: ExtensionLogger;
let mcpClient: MCPClientManager;
let api: TaskMasterApi;
let repository: TaskRepository;
let terminalManager: TerminalManager;
let pollingService: PollingService;
let webviewManager: WebviewManager;
let events: EventEmitter;
@@ -46,6 +48,9 @@ export async function activate(context: vscode.ExtensionContext) {
// Repository with caching (actually useful for performance)
repository = new TaskRepository(api, logger);
// Terminal manager for task execution
terminalManager = new TerminalManager(context, logger);
// Config service for TaskMaster config.json
configService = new ConfigService(logger);
@@ -56,7 +61,13 @@ export async function activate(context: vscode.ExtensionContext) {
pollingService = new PollingService(repository, strategy, logger);
// Webview manager (cleaner than global panel array) - create before connection
webviewManager = new WebviewManager(context, repository, events, logger);
webviewManager = new WebviewManager(
context,
repository,
events,
logger,
terminalManager
);
webviewManager.setConfigService(configService);
// Sidebar webview manager
@@ -210,10 +221,11 @@ function registerCommands(context: vscode.ExtensionContext) {
);
}
export function deactivate() {
export async function deactivate() {
logger?.log('👋 TaskMaster Extension deactivating...');
pollingService?.stop();
webviewManager?.dispose();
await terminalManager?.dispose();
api?.destroy();
mcpClient?.disconnect();
}

View File

@@ -0,0 +1,156 @@
/**
* Terminal Manager - Handles task execution in VS Code terminals
* Uses @tm/core for consistent task management with the CLI
*/
import * as vscode from 'vscode';
import { createTaskMasterCore, type TaskMasterCore } from '@tm/core';
import type { ExtensionLogger } from '../utils/logger';
export interface TerminalExecutionOptions {
taskId: string;
taskTitle: string;
tag?: string;
}
export interface TerminalExecutionResult {
success: boolean;
error?: string;
terminalName?: string;
}
export class TerminalManager {
private terminals = new Map<string, vscode.Terminal>();
private tmCore?: TaskMasterCore;
constructor(
private context: vscode.ExtensionContext,
private logger: ExtensionLogger
) {}
/**
* Execute a task in a new VS Code terminal with Claude
* Uses @tm/core for consistent task management with the CLI
*/
async executeTask(
options: TerminalExecutionOptions
): Promise<TerminalExecutionResult> {
const { taskTitle, tag } = options;
// Ensure taskId is always a string
const taskId = String(options.taskId);
this.logger.log(
`Starting task execution for ${taskId}: ${taskTitle}${tag ? ` (tag: ${tag})` : ''}`
);
this.logger.log(`TaskId type: ${typeof taskId}, value: ${taskId}`);
try {
// Initialize tm-core if needed
await this.initializeCore();
// Use tm-core to start the task (same as CLI)
const startResult = await this.tmCore!.startTask(taskId, {
dryRun: false,
force: false,
updateStatus: true
});
if (!startResult.started || !startResult.executionOutput) {
throw new Error(
startResult.error || 'Failed to start task with tm-core'
);
}
// Create terminal with custom TaskMaster icon
const terminalName = `Task ${taskId}: ${taskTitle}`;
const terminal = this.createTerminal(terminalName);
// Store terminal reference for potential cleanup
this.terminals.set(taskId, terminal);
// Show terminal and run Claude command
terminal.show();
const command = `claude "${startResult.executionOutput}"`;
terminal.sendText(command);
this.logger.log(`Launched Claude for task ${taskId} using tm-core`);
return {
success: true,
terminalName
};
} catch (error) {
this.logger.error('Failed to execute task:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error'
};
}
}
/**
* Create a new terminal with TaskMaster branding
*/
private createTerminal(name: string): vscode.Terminal {
const workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath;
return vscode.window.createTerminal({
name,
cwd: workspaceRoot,
iconPath: new vscode.ThemeIcon('play') // Use a VS Code built-in icon for now
});
}
/**
* Initialize TaskMaster Core (same as CLI)
*/
private async initializeCore(): Promise<void> {
if (!this.tmCore) {
const workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath;
if (!workspaceRoot) {
throw new Error('No workspace folder found');
}
this.tmCore = await createTaskMasterCore({ projectPath: workspaceRoot });
}
}
/**
* Get terminal by task ID (if still active)
*/
getTerminalByTaskId(taskId: string): vscode.Terminal | undefined {
return this.terminals.get(taskId);
}
/**
* Clean up terminated terminals
*/
cleanupTerminal(taskId: string): void {
const terminal = this.terminals.get(taskId);
if (terminal) {
this.terminals.delete(taskId);
}
}
/**
* Dispose all managed terminals and clean up tm-core
*/
async dispose(): Promise<void> {
this.terminals.forEach((terminal) => {
try {
terminal.dispose();
} catch (error) {
this.logger.error('Failed to dispose terminal:', error);
}
});
this.terminals.clear();
if (this.tmCore) {
try {
await this.tmCore.close();
this.tmCore = undefined;
} catch (error) {
this.logger.error('Failed to close tm-core:', error);
}
}
}
}

View File

@@ -8,6 +8,7 @@ import type { EventEmitter } from '../utils/event-emitter';
import type { ExtensionLogger } from '../utils/logger';
import type { ConfigService } from './config-service';
import type { TaskRepository } from './task-repository';
import type { TerminalManager } from './terminal-manager';
export class WebviewManager {
private panels = new Set<vscode.WebviewPanel>();
@@ -19,7 +20,8 @@ export class WebviewManager {
private context: vscode.ExtensionContext,
private repository: TaskRepository,
private events: EventEmitter,
private logger: ExtensionLogger
private logger: ExtensionLogger,
private terminalManager: TerminalManager
) {}
setConfigService(configService: ConfigService): void {
@@ -362,27 +364,67 @@ export class WebviewManager {
return;
case 'openTerminal':
// Open VS Code terminal for task execution
// Delegate terminal execution to TerminalManager
const { taskId, taskTitle } = data.data || data; // Handle both nested and direct data
this.logger.log(
`Opening terminal for task ${data.taskId}: ${data.taskTitle}`
`Webview openTerminal - taskId: ${taskId} (type: ${typeof taskId}), taskTitle: ${taskTitle}`
);
try {
const terminal = vscode.window.createTerminal({
name: `Task ${data.taskId}: ${data.taskTitle}`,
cwd: vscode.workspace.workspaceFolders?.[0]?.uri.fsPath
});
terminal.show();
// Get current tag to ensure we're working in the right context
let currentTag = 'master'; // default fallback
if (this.mcpClient) {
try {
const tagsResult = await this.mcpClient.callTool('list_tags', {
projectRoot: vscode.workspace.workspaceFolders?.[0]?.uri.fsPath,
showMetadata: false
});
this.logger.log('Terminal created and shown successfully');
response = { success: true };
} catch (error) {
this.logger.error('Failed to create terminal:', error);
response = {
success: false,
error: error instanceof Error ? error.message : 'Unknown error'
};
let parsedData;
if (
tagsResult?.content &&
Array.isArray(tagsResult.content) &&
tagsResult.content[0]?.text
) {
try {
parsedData = JSON.parse(tagsResult.content[0].text);
if (parsedData?.data?.currentTag) {
currentTag = parsedData.data.currentTag;
}
} catch (e) {
this.logger.warn(
'Failed to parse tags response for terminal execution'
);
}
}
} catch (error) {
this.logger.warn(
'Failed to get current tag for terminal execution:',
error
);
}
}
const result = await this.terminalManager.executeTask({
taskId,
taskTitle,
tag: currentTag
});
response = result;
// Show user feedback AFTER sending the response (like the working "TaskMaster connected!" example)
setImmediate(() => {
if (result.success) {
// Success: Show info message
vscode.window.showInformationMessage(
`✅ Started Claude session for Task ${taskId}: ${taskTitle}`
);
} else {
// Error: Show VS Code native error notification only
const errorMsg = `Failed to start task: ${result.error}`;
vscode.window.showErrorMessage(errorMsg);
}
});
break;
default: