diff --git a/drizzle/0002_lyrical_impossible_man.sql b/drizzle/0002_lyrical_impossible_man.sql new file mode 100644 index 00000000..d3795025 --- /dev/null +++ b/drizzle/0002_lyrical_impossible_man.sql @@ -0,0 +1,34 @@ +ALTER TABLE `workspaces` RENAME TO `tasks`;--> statement-breakpoint +PRAGMA foreign_keys=OFF;--> statement-breakpoint +CREATE TABLE `__new_tasks` ( + `id` text PRIMARY KEY NOT NULL, + `project_id` text NOT NULL, + `name` text NOT NULL, + `branch` text NOT NULL, + `path` text NOT NULL, + `status` text DEFAULT 'idle' NOT NULL, + `agent_id` text, + `metadata` text, + `created_at` text DEFAULT CURRENT_TIMESTAMP NOT NULL, + `updated_at` text DEFAULT CURRENT_TIMESTAMP NOT NULL, + FOREIGN KEY (`project_id`) REFERENCES `projects`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +INSERT INTO `__new_tasks`("id", "project_id", "name", "branch", "path", "status", "agent_id", "metadata", "created_at", "updated_at") SELECT "id", "project_id", "name", "branch", "path", "status", "agent_id", "metadata", "created_at", "updated_at" FROM `tasks`;--> statement-breakpoint +DROP TABLE `tasks`;--> statement-breakpoint +ALTER TABLE `__new_tasks` RENAME TO `tasks`;--> statement-breakpoint +PRAGMA foreign_keys=ON;--> statement-breakpoint +CREATE INDEX `idx_tasks_project_id` ON `tasks` (`project_id`);--> statement-breakpoint +CREATE TABLE `__new_conversations` ( + `id` text PRIMARY KEY NOT NULL, + `task_id` text NOT NULL, + `title` text NOT NULL, + `created_at` text DEFAULT CURRENT_TIMESTAMP NOT NULL, + `updated_at` text DEFAULT CURRENT_TIMESTAMP NOT NULL, + FOREIGN KEY (`task_id`) REFERENCES `tasks`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +INSERT INTO `__new_conversations`("id", "task_id", "title", "created_at", "updated_at") SELECT "id", "workspace_id", "title", "created_at", "updated_at" FROM `conversations`;--> statement-breakpoint +DROP TABLE `conversations`;--> statement-breakpoint +ALTER TABLE `__new_conversations` RENAME TO `conversations`;--> statement-breakpoint +CREATE INDEX `idx_conversations_task_id` ON `conversations` (`task_id`); \ No newline at end of file diff --git a/drizzle/meta/0001_snapshot.json b/drizzle/meta/0001_snapshot.json index c3aeb73b..90204e26 100644 --- a/drizzle/meta/0001_snapshot.json +++ b/drizzle/meta/0001_snapshot.json @@ -1,5 +1,5 @@ { - "version": "7", + "version": "6", "dialect": "sqlite", "id": "ec5ad35e-22ec-4c0b-9d48-cfb1033d9d93", "prevId": "b932945e-f26a-4c07-9c63-08179d95d5bc", diff --git a/drizzle/meta/0002_snapshot.json b/drizzle/meta/0002_snapshot.json new file mode 100644 index 00000000..fc0d0457 --- /dev/null +++ b/drizzle/meta/0002_snapshot.json @@ -0,0 +1,347 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "f61fcd81-f000-4e2d-84f2-2f79133ae5d6", + "prevId": "ec5ad35e-22ec-4c0b-9d48-cfb1033d9d93", + "tables": { + "conversations": { + "name": "conversations", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "task_id": { + "name": "task_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_conversations_task_id": { + "name": "idx_conversations_task_id", + "columns": ["task_id"], + "isUnique": false + } + }, + "foreignKeys": { + "conversations_task_id_tasks_id_fk": { + "name": "conversations_task_id_tasks_id_fk", + "tableFrom": "conversations", + "tableTo": "tasks", + "columnsFrom": ["task_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "messages": { + "name": "messages", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "conversation_id": { + "name": "conversation_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sender": { + "name": "sender", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "timestamp": { + "name": "timestamp", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_messages_conversation_id": { + "name": "idx_messages_conversation_id", + "columns": ["conversation_id"], + "isUnique": false + }, + "idx_messages_timestamp": { + "name": "idx_messages_timestamp", + "columns": ["timestamp"], + "isUnique": false + } + }, + "foreignKeys": { + "messages_conversation_id_conversations_id_fk": { + "name": "messages_conversation_id_conversations_id_fk", + "tableFrom": "messages", + "tableTo": "conversations", + "columnsFrom": ["conversation_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "projects": { + "name": "projects", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "git_remote": { + "name": "git_remote", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "git_branch": { + "name": "git_branch", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "base_ref": { + "name": "base_ref", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "github_repository": { + "name": "github_repository", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "github_connected": { + "name": "github_connected", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_projects_path": { + "name": "idx_projects_path", + "columns": ["path"], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "tasks": { + "name": "tasks", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "branch": { + "name": "branch", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'idle'" + }, + "agent_id": { + "name": "agent_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_tasks_project_id": { + "name": "idx_tasks_project_id", + "columns": ["project_id"], + "isUnique": false + } + }, + "foreignKeys": { + "tasks_project_id_projects_id_fk": { + "name": "tasks_project_id_projects_id_fk", + "tableFrom": "tasks", + "tableTo": "projects", + "columnsFrom": ["project_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": { + "\"workspaces\"": "\"tasks\"" + }, + "columns": { + "\"conversations\".\"workspace_id\"": "\"conversations\".\"task_id\"" + } + }, + "internal": { + "indexes": {} + } +} diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index c018fded..4f2cbf82 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -11,10 +11,17 @@ }, { "idx": 1, - "version": "7", + "version": "6", "when": 1761240000000, "tag": "0001_add_base_ref_to_projects", "breakpoints": true + }, + { + "idx": 2, + "version": "6", + "when": 1765592430354, + "tag": "0002_lyrical_impossible_man", + "breakpoints": true } ] } diff --git a/src/main/db/schema.ts b/src/main/db/schema.ts index d65977b3..62b413f8 100644 --- a/src/main/db/schema.ts +++ b/src/main/db/schema.ts @@ -24,8 +24,8 @@ export const projects = sqliteTable( }) ); -export const workspaces = sqliteTable( - 'workspaces', +export const tasks = sqliteTable( + 'tasks', { id: text('id').primaryKey(), projectId: text('project_id') @@ -45,7 +45,7 @@ export const workspaces = sqliteTable( .default(sql`CURRENT_TIMESTAMP`), }, (table) => ({ - projectIdIdx: index('idx_workspaces_project_id').on(table.projectId), + projectIdIdx: index('idx_tasks_project_id').on(table.projectId), }) ); @@ -53,9 +53,9 @@ export const conversations = sqliteTable( 'conversations', { id: text('id').primaryKey(), - workspaceId: text('workspace_id') + taskId: text('task_id') .notNull() - .references(() => workspaces.id, { onDelete: 'cascade' }), + .references(() => tasks.id, { onDelete: 'cascade' }), title: text('title').notNull(), createdAt: text('created_at') .notNull() @@ -65,7 +65,7 @@ export const conversations = sqliteTable( .default(sql`CURRENT_TIMESTAMP`), }, (table) => ({ - workspaceIdIdx: index('idx_conversations_workspace_id').on(table.workspaceId), + taskIdIdx: index('idx_conversations_task_id').on(table.taskId), }) ); @@ -90,21 +90,21 @@ export const messages = sqliteTable( ); export const projectsRelations = relations(projects, ({ many }) => ({ - workspaces: many(workspaces), + tasks: many(tasks), })); -export const workspacesRelations = relations(workspaces, ({ one, many }) => ({ +export const tasksRelations = relations(tasks, ({ one, many }) => ({ project: one(projects, { - fields: [workspaces.projectId], + fields: [tasks.projectId], references: [projects.id], }), conversations: many(conversations), })); export const conversationsRelations = relations(conversations, ({ one, many }) => ({ - workspace: one(workspaces, { - fields: [conversations.workspaceId], - references: [workspaces.id], + task: one(tasks, { + fields: [conversations.taskId], + references: [tasks.id], }), messages: many(messages), })); @@ -117,6 +117,6 @@ export const messagesRelations = relations(messages, ({ one }) => ({ })); export type ProjectRow = typeof projects.$inferSelect; -export type WorkspaceRow = typeof workspaces.$inferSelect; +export type TaskRow = typeof tasks.$inferSelect; export type ConversationRow = typeof conversations.$inferSelect; export type MessageRow = typeof messages.$inferSelect; diff --git a/src/main/ipc/containerIpc.ts b/src/main/ipc/containerIpc.ts index 01156af9..298eaa77 100644 --- a/src/main/ipc/containerIpc.ts +++ b/src/main/ipc/containerIpc.ts @@ -4,7 +4,7 @@ import { log } from '../lib/logger'; import { ContainerConfigLoadError, ContainerConfigLoadErrorCode, - loadWorkspaceContainerConfig, + loadTaskContainerConfig, } from '../services/containerConfigService'; import type { ResolvedContainerConfig } from '@shared/container'; import { @@ -63,13 +63,13 @@ export function registerContainerIpc(): void { ipcMain.handle( 'container:load-config', async (_event, args): Promise => { - const workspacePath = resolveWorkspacePath(args); - if (!workspacePath) { + const taskPath = resolveTaskPath(args); + if (!taskPath) { return { ok: false, error: { code: 'INVALID_ARGUMENT', - message: '`workspacePath` must be a non-empty string', + message: '`taskPath` must be a non-empty string', configPath: null, configKey: null, }, @@ -77,7 +77,7 @@ export function registerContainerIpc(): void { } try { - const result = await loadWorkspaceContainerConfig(workspacePath); + const result = await loadTaskContainerConfig(taskPath); if (result.ok) { return { ok: true, @@ -114,7 +114,7 @@ export function registerContainerIpc(): void { ok: false, error: { code: 'INVALID_ARGUMENT', - message: '`workspaceId` and `workspacePath` must be provided to start a container run', + message: '`taskId` and `taskPath` must be provided to start a container run', configPath: null, configKey: null, }, @@ -131,11 +131,11 @@ export function registerContainerIpc(): void { 'container:stop-run', async (_event, args): Promise<{ ok: boolean; error?: string }> => { try { - const workspaceId = typeof args?.workspaceId === 'string' ? args.workspaceId.trim() : ''; - if (!workspaceId) { - return { ok: false, error: '`workspaceId` must be provided' }; + const taskId = typeof args?.taskId === 'string' ? args.taskId.trim() : ''; + if (!taskId) { + return { ok: false, error: '`taskId` must be provided' }; } - const res = await containerRunnerService.stopRun(workspaceId); + const res = await containerRunnerService.stopRun(taskId); return res as any; } catch (error: any) { return { ok: false, error: error?.message || String(error) }; @@ -158,11 +158,11 @@ export function registerContainerIpc(): void { | { ok: false; error: string } > => { try { - const workspaceId = typeof args?.workspaceId === 'string' ? args.workspaceId.trim() : ''; - if (!workspaceId) { - return { ok: false, error: '`workspaceId` must be provided' } as const; + const taskId = typeof args?.taskId === 'string' ? args.taskId.trim() : ''; + if (!taskId) { + return { ok: false, error: '`taskId` must be provided' } as const; } - return await containerRunnerService.inspectRun(workspaceId); + return await containerRunnerService.inspectRun(taskId); } catch (error: any) { const message = error?.message || String(error); log.warn('container:inspect-run failed', message); @@ -177,9 +177,8 @@ export function registerContainerIpc(): void { try { const service = typeof args?.service === 'string' ? args.service : ''; const allowNetwork = args?.allowNetwork === true; - const workspacePath = - typeof args?.workspacePath === 'string' ? args.workspacePath : undefined; - const res = await resolveServiceIcon({ service, allowNetwork, workspacePath }); + const taskPath = typeof args?.taskPath === 'string' ? args.taskPath : undefined; + const res = await resolveServiceIcon({ service, allowNetwork, taskPath }); if (res.ok) return { ok: true, dataUrl: res.dataUrl }; return { ok: false }; } catch (error: any) { @@ -189,14 +188,14 @@ export function registerContainerIpc(): void { ); } -function resolveWorkspacePath(args: unknown): string | null { +function resolveTaskPath(args: unknown): string | null { if (typeof args === 'string') { const trimmed = args.trim(); return trimmed.length > 0 ? trimmed : null; } if (args && typeof args === 'object') { - const candidate = (args as { workspacePath?: unknown }).workspacePath; + const candidate = (args as { taskPath?: unknown }).taskPath; if (typeof candidate === 'string') { const trimmed = candidate.trim(); if (trimmed.length > 0) { @@ -218,8 +217,8 @@ function serializeError(error: ContainerConfigLoadError): SerializedContainerCon } function parseStartRunArgs(args: unknown): { - workspaceId: string; - workspacePath: string; + taskId: string; + taskPath: string; runId?: string; mode?: RunnerMode; } | null { @@ -228,10 +227,9 @@ function parseStartRunArgs(args: unknown): { } const payload = args as Record; - const workspaceId = typeof payload.workspaceId === 'string' ? payload.workspaceId.trim() : ''; - const workspacePath = - typeof payload.workspacePath === 'string' ? payload.workspacePath.trim() : ''; - if (!workspaceId || !workspacePath) { + const taskId = typeof payload.taskId === 'string' ? payload.taskId.trim() : ''; + const taskPath = typeof payload.taskPath === 'string' ? payload.taskPath.trim() : ''; + if (!taskId || !taskPath) { return null; } @@ -247,7 +245,7 @@ function parseStartRunArgs(args: unknown): { } } - return { workspaceId, workspacePath, runId, mode }; + return { taskId, taskPath, runId, mode }; } function serializeStartRunResult(result: ContainerStartResult): ContainerStartIpcResponse { diff --git a/src/main/ipc/dbIpc.ts b/src/main/ipc/dbIpc.ts index 6d8cdb55..f23b9a7c 100644 --- a/src/main/ipc/dbIpc.ts +++ b/src/main/ipc/dbIpc.ts @@ -23,21 +23,21 @@ export function registerDatabaseIpc() { } }); - ipcMain.handle('db:getWorkspaces', async (_, projectId?: string) => { + ipcMain.handle('db:getTasks', async (_, projectId?: string) => { try { - return await databaseService.getWorkspaces(projectId); + return await databaseService.getTasks(projectId); } catch (error) { - log.error('Failed to get workspaces:', error); + log.error('Failed to get tasks:', error); return []; } }); - ipcMain.handle('db:saveWorkspace', async (_, workspace: any) => { + ipcMain.handle('db:saveTask', async (_, task: any) => { try { - await databaseService.saveWorkspace(workspace); + await databaseService.saveTask(task); return { success: true }; } catch (error) { - log.error('Failed to save workspace:', error); + log.error('Failed to save task:', error); return { success: false, error: (error as Error).message }; } }); @@ -62,9 +62,9 @@ export function registerDatabaseIpc() { } }); - ipcMain.handle('db:getConversations', async (_, workspaceId: string) => { + ipcMain.handle('db:getConversations', async (_, taskId: string) => { try { - const conversations = await databaseService.getConversations(workspaceId); + const conversations = await databaseService.getConversations(taskId); return { success: true, conversations }; } catch (error) { log.error('Failed to get conversations:', error); @@ -72,9 +72,9 @@ export function registerDatabaseIpc() { } }); - ipcMain.handle('db:getOrCreateDefaultConversation', async (_, workspaceId: string) => { + ipcMain.handle('db:getOrCreateDefaultConversation', async (_, taskId: string) => { try { - const conversation = await databaseService.getOrCreateDefaultConversation(workspaceId); + const conversation = await databaseService.getOrCreateDefaultConversation(taskId); return { success: true, conversation }; } catch (error) { log.error('Failed to get or create default conversation:', error); @@ -112,19 +112,19 @@ export function registerDatabaseIpc() { } }); - ipcMain.handle('db:deleteWorkspace', async (_, workspaceId: string) => { + ipcMain.handle('db:deleteTask', async (_, taskId: string) => { try { - // Stop any running Docker container for this workspace before deletion - const stopResult = await containerRunnerService.stopRun(workspaceId); + // Stop any running Docker container for this task before deletion + const stopResult = await containerRunnerService.stopRun(taskId); if (!stopResult.ok) { - // Log but don't fail workspace deletion if container stop fails - log.warn('Failed to stop container during workspace deletion:', stopResult.error); + // Log but don't fail task deletion if container stop fails + log.warn('Failed to stop container during task deletion:', stopResult.error); } - await databaseService.deleteWorkspace(workspaceId); + await databaseService.deleteTask(taskId); return { success: true }; } catch (error) { - log.error('Failed to delete workspace:', error); + log.error('Failed to delete task:', error); return { success: false, error: (error as Error).message }; } }); diff --git a/src/main/ipc/gitIpc.ts b/src/main/ipc/gitIpc.ts index 7dc6542a..25d28c1f 100644 --- a/src/main/ipc/gitIpc.ts +++ b/src/main/ipc/gitIpc.ts @@ -37,9 +37,9 @@ export function registerGitIpc() { } const GIT = resolveGitBin(); // Git: Status (moved from Codex IPC) - ipcMain.handle('git:get-status', async (_, workspacePath: string) => { + ipcMain.handle('git:get-status', async (_, taskPath: string) => { try { - const changes = await gitGetStatus(workspacePath); + const changes = await gitGetStatus(taskPath); return { success: true, changes }; } catch (error) { return { success: false, error: error as string }; @@ -47,23 +47,20 @@ export function registerGitIpc() { }); // Git: Per-file diff (moved from Codex IPC) - ipcMain.handle( - 'git:get-file-diff', - async (_, args: { workspacePath: string; filePath: string }) => { - try { - const diff = await gitGetFileDiff(args.workspacePath, args.filePath); - return { success: true, diff }; - } catch (error) { - return { success: false, error: error as string }; - } + ipcMain.handle('git:get-file-diff', async (_, args: { taskPath: string; filePath: string }) => { + try { + const diff = await gitGetFileDiff(args.taskPath, args.filePath); + return { success: true, diff }; + } catch (error) { + return { success: false, error: error as string }; } - ); + }); // Git: Stage file - ipcMain.handle('git:stage-file', async (_, args: { workspacePath: string; filePath: string }) => { + ipcMain.handle('git:stage-file', async (_, args: { taskPath: string; filePath: string }) => { try { - log.info('Staging file:', { workspacePath: args.workspacePath, filePath: args.filePath }); - await gitStageFile(args.workspacePath, args.filePath); + log.info('Staging file:', { taskPath: args.taskPath, filePath: args.filePath }); + await gitStageFile(args.taskPath, args.filePath); log.info('File staged successfully:', args.filePath); return { success: true }; } catch (error) { @@ -73,47 +70,43 @@ export function registerGitIpc() { }); // Git: Revert file - ipcMain.handle( - 'git:revert-file', - async (_, args: { workspacePath: string; filePath: string }) => { - try { - log.info('Reverting file:', { workspacePath: args.workspacePath, filePath: args.filePath }); - const result = await gitRevertFile(args.workspacePath, args.filePath); - log.info('File operation completed:', { filePath: args.filePath, action: result.action }); - return { success: true, action: result.action }; - } catch (error) { - log.error('Failed to revert file:', { filePath: args.filePath, error }); - return { success: false, error: error instanceof Error ? error.message : String(error) }; - } + ipcMain.handle('git:revert-file', async (_, args: { taskPath: string; filePath: string }) => { + try { + log.info('Reverting file:', { taskPath: args.taskPath, filePath: args.filePath }); + const result = await gitRevertFile(args.taskPath, args.filePath); + log.info('File operation completed:', { filePath: args.filePath, action: result.action }); + return { success: true, action: result.action }; + } catch (error) { + log.error('Failed to revert file:', { filePath: args.filePath, error }); + return { success: false, error: error instanceof Error ? error.message : String(error) }; } - ); + }); // Git: Generate PR title and description ipcMain.handle( 'git:generate-pr-content', async ( _, args: { - workspacePath: string; + taskPath: string; base?: string; } ) => { - const { workspacePath, base = 'main' } = - args || ({} as { workspacePath: string; base?: string }); + const { taskPath, base = 'main' } = args || ({} as { taskPath: string; base?: string }); try { - // Try to get the workspace to find which provider was used + // Try to get the task to find which provider was used let providerId: string | null = null; try { - const workspace = await databaseService.getWorkspaceByPath(workspacePath); - if (workspace?.agentId) { - providerId = workspace.agentId; - log.debug('Found workspace provider for PR generation', { workspacePath, providerId }); + const task = await databaseService.getTaskByPath(taskPath); + if (task?.agentId) { + providerId = task.agentId; + log.debug('Found task provider for PR generation', { taskPath, providerId }); } } catch (error) { - log.debug('Could not lookup workspace provider', { error }); + log.debug('Could not lookup task provider', { error }); // Non-fatal - continue without provider } - const result = await prGenerationService.generatePrContent(workspacePath, base, providerId); + const result = await prGenerationService.generatePrContent(taskPath, base, providerId); return { success: true, ...result }; } catch (error) { log.error('Failed to generate PR content:', error); @@ -131,7 +124,7 @@ export function registerGitIpc() { async ( _, args: { - workspacePath: string; + taskPath: string; title?: string; body?: string; base?: string; @@ -141,10 +134,10 @@ export function registerGitIpc() { fill?: boolean; } ) => { - const { workspacePath, title, body, base, head, draft, web, fill } = + const { taskPath, title, body, base, head, draft, web, fill } = args || ({} as { - workspacePath: string; + taskPath: string; title?: string; body?: string; base?: string; @@ -161,12 +154,12 @@ export function registerGitIpc() { const { stdout: statusOut } = await execAsync( 'git status --porcelain --untracked-files=all', { - cwd: workspacePath, + cwd: taskPath, } ); if (statusOut && statusOut.trim().length > 0) { const { stdout: addOut, stderr: addErr } = await execAsync('git add -A', { - cwd: workspacePath, + cwd: taskPath, }); if (addOut?.trim()) outputs.push(addOut.trim()); if (addErr?.trim()) outputs.push(addErr.trim()); @@ -175,7 +168,7 @@ export function registerGitIpc() { try { const { stdout: commitOut, stderr: commitErr } = await execAsync( `git commit -m ${JSON.stringify(commitMsg)}`, - { cwd: workspacePath } + { cwd: taskPath } ); if (commitOut?.trim()) outputs.push(commitOut.trim()); if (commitErr?.trim()) outputs.push(commitErr.trim()); @@ -195,16 +188,16 @@ export function registerGitIpc() { // Ensure branch is pushed to origin so PR includes latest commit try { - await execAsync('git push', { cwd: workspacePath }); + await execAsync('git push', { cwd: taskPath }); outputs.push('git push: success'); } catch (pushErr) { try { const { stdout: branchOut } = await execAsync('git rev-parse --abbrev-ref HEAD', { - cwd: workspacePath, + cwd: taskPath, }); const branch = branchOut.trim(); await execAsync(`git push --set-upstream origin ${JSON.stringify(branch)}`, { - cwd: workspacePath, + cwd: taskPath, }); outputs.push(`git push --set-upstream origin ${branch}: success`); } catch (pushErr2) { @@ -222,13 +215,13 @@ export function registerGitIpc() { try { const { stdout: repoOut } = await execAsync( 'gh repo view --json nameWithOwner -q .nameWithOwner', - { cwd: workspacePath } + { cwd: taskPath } ); repoNameWithOwner = (repoOut || '').trim(); } catch { try { const { stdout: urlOut } = await execAsync('git remote get-url origin', { - cwd: workspacePath, + cwd: taskPath, }); const url = (urlOut || '').trim(); // Handle both SSH and HTTPS forms @@ -246,14 +239,14 @@ export function registerGitIpc() { // Determine current branch and default base branch (fallback to main) let currentBranch = ''; try { - const { stdout } = await execAsync('git branch --show-current', { cwd: workspacePath }); + const { stdout } = await execAsync('git branch --show-current', { cwd: taskPath }); currentBranch = (stdout || '').trim(); } catch {} let defaultBranch = 'main'; try { const { stdout } = await execAsync( 'gh repo view --json defaultBranchRef -q .defaultBranchRef.name', - { cwd: workspacePath } + { cwd: taskPath } ); const db = (stdout || '').trim(); if (db) defaultBranch = db; @@ -261,7 +254,7 @@ export function registerGitIpc() { try { const { stdout } = await execAsync( 'git remote show origin | sed -n "/HEAD branch/s/.*: //p"', - { cwd: workspacePath } + { cwd: taskPath } ); const db2 = (stdout || '').trim(); if (db2) defaultBranch = db2; @@ -273,7 +266,7 @@ export function registerGitIpc() { const baseRef = base || defaultBranch; const { stdout: aheadOut } = await execAsync( `git rev-list --count ${JSON.stringify(`origin/${baseRef}`)}..HEAD`, - { cwd: workspacePath } + { cwd: taskPath } ); const aheadCount = parseInt((aheadOut || '0').trim(), 10) || 0; if (aheadCount <= 0) { @@ -331,7 +324,7 @@ current branch '${currentBranch}' ahead of base '${baseRef}'.`, let stdout: string; let stderr: string; try { - const result = await execAsync(cmd, { cwd: workspacePath }); + const result = await execAsync(cmd, { cwd: taskPath }); stdout = result.stdout || ''; stderr = result.stderr || ''; } finally { @@ -378,11 +371,11 @@ current branch '${currentBranch}' ahead of base '${baseRef}'.`, ); // Git: Get PR status for current branch via GitHub CLI - ipcMain.handle('git:get-pr-status', async (_, args: { workspacePath: string }) => { - const { workspacePath } = args || ({} as { workspacePath: string }); + ipcMain.handle('git:get-pr-status', async (_, args: { taskPath: string }) => { + const { taskPath } = args || ({} as { taskPath: string }); try { // Ensure we're in a git repo - await execAsync('git rev-parse --is-inside-work-tree', { cwd: workspacePath }); + await execAsync('git rev-parse --is-inside-work-tree', { cwd: taskPath }); const queryFields = [ 'number', @@ -400,7 +393,7 @@ current branch '${currentBranch}' ahead of base '${baseRef}'.`, ]; const cmd = `gh pr view --json ${queryFields.join(',')} -q .`; try { - const { stdout } = await execAsync(cmd, { cwd: workspacePath }); + const { stdout } = await execAsync(cmd, { cwd: taskPath }); const json = (stdout || '').trim(); const data = json ? JSON.parse(json) : null; if (!data) return { success: false, error: 'No PR data returned' }; @@ -424,7 +417,7 @@ current branch '${currentBranch}' ahead of base '${baseRef}'.`, ? `git diff --shortstat ${JSON.stringify(targetRef)}...HEAD` : 'git diff --shortstat HEAD~1..HEAD'; try { - const { stdout: diffOut } = await execAsync(shortstatCmd, { cwd: workspacePath }); + const { stdout: diffOut } = await execAsync(shortstatCmd, { cwd: taskPath }); const statLine = (diffOut || '').trim(); const m = statLine && @@ -461,25 +454,25 @@ current branch '${currentBranch}' ahead of base '${baseRef}'.`, async ( _, args: { - workspacePath: string; + taskPath: string; commitMessage?: string; createBranchIfOnDefault?: boolean; branchPrefix?: string; } ) => { const { - workspacePath, - commitMessage = 'chore: apply workspace changes', + taskPath, + commitMessage = 'chore: apply task changes', createBranchIfOnDefault = true, branchPrefix = 'orch', } = (args || ({} as { - workspacePath: string; + taskPath: string; commitMessage?: string; createBranchIfOnDefault?: boolean; branchPrefix?: string; })) as { - workspacePath: string; + taskPath: string; commitMessage?: string; createBranchIfOnDefault?: boolean; branchPrefix?: string; @@ -487,11 +480,11 @@ current branch '${currentBranch}' ahead of base '${baseRef}'.`, try { // Ensure we're in a git repo - await execAsync('git rev-parse --is-inside-work-tree', { cwd: workspacePath }); + await execAsync('git rev-parse --is-inside-work-tree', { cwd: taskPath }); // Determine current branch const { stdout: currentBranchOut } = await execAsync('git branch --show-current', { - cwd: workspacePath, + cwd: taskPath, }); const currentBranch = (currentBranchOut || '').trim(); @@ -500,7 +493,7 @@ current branch '${currentBranch}' ahead of base '${baseRef}'.`, try { const { stdout } = await execAsync( 'gh repo view --json defaultBranchRef -q .defaultBranchRef.name', - { cwd: workspacePath } + { cwd: taskPath } ); const db = (stdout || '').trim(); if (db) defaultBranch = db; @@ -508,7 +501,7 @@ current branch '${currentBranch}' ahead of base '${baseRef}'.`, try { const { stdout } = await execAsync( 'git remote show origin | sed -n "/HEAD branch/s/.*: //p"', - { cwd: workspacePath } + { cwd: taskPath } ); const db2 = (stdout || '').trim(); if (db2) defaultBranch = db2; @@ -520,21 +513,21 @@ current branch '${currentBranch}' ahead of base '${baseRef}'.`, if (createBranchIfOnDefault && (!currentBranch || currentBranch === defaultBranch)) { const short = Date.now().toString(36); const name = `${branchPrefix}/${short}`; - await execAsync(`git checkout -b ${JSON.stringify(name)}`, { cwd: workspacePath }); + await execAsync(`git checkout -b ${JSON.stringify(name)}`, { cwd: taskPath }); activeBranch = name; } // Stage (only if needed) and commit try { const { stdout: st } = await execAsync('git status --porcelain --untracked-files=all', { - cwd: workspacePath, + cwd: taskPath, }); const hasWorkingChanges = Boolean(st && st.trim().length > 0); const readStagedFiles = async () => { try { const { stdout } = await execAsync('git diff --cached --name-only', { - cwd: workspacePath, + cwd: taskPath, }); return (stdout || '') .split('\n') @@ -549,18 +542,18 @@ current branch '${currentBranch}' ahead of base '${baseRef}'.`, // Only auto-stage everything when nothing is staged yet (preserves manual staging choices) if (hasWorkingChanges && stagedFiles.length === 0) { - await execAsync('git add -A', { cwd: workspacePath }); + await execAsync('git add -A', { cwd: taskPath }); } // Never commit plan mode artifacts try { - await execAsync('git reset -q .emdash || true', { cwd: workspacePath }); + await execAsync('git reset -q .emdash || true', { cwd: taskPath }); } catch {} try { - await execAsync('git reset -q PLANNING.md || true', { cwd: workspacePath }); + await execAsync('git reset -q PLANNING.md || true', { cwd: taskPath }); } catch {} try { - await execAsync('git reset -q planning.md || true', { cwd: workspacePath }); + await execAsync('git reset -q planning.md || true', { cwd: taskPath }); } catch {} stagedFiles = await readStagedFiles(); @@ -568,7 +561,7 @@ current branch '${currentBranch}' ahead of base '${baseRef}'.`, if (stagedFiles.length > 0) { try { await execAsync(`git commit -m ${JSON.stringify(commitMessage)}`, { - cwd: workspacePath, + cwd: taskPath, }); } catch (commitErr) { const msg = commitErr as string; @@ -581,14 +574,14 @@ current branch '${currentBranch}' ahead of base '${baseRef}'.`, // Push current branch (set upstream if needed) try { - await execAsync('git push', { cwd: workspacePath }); + await execAsync('git push', { cwd: taskPath }); } catch (pushErr) { await execAsync(`git push --set-upstream origin ${JSON.stringify(activeBranch)}`, { - cwd: workspacePath, + cwd: taskPath, }); } - const { stdout: out } = await execAsync('git status -sb', { cwd: workspacePath }); + const { stdout: out } = await execAsync('git status -sb', { cwd: taskPath }); return { success: true, branch: activeBranch, output: (out || '').trim() }; } catch (error) { log.error('Failed to commit and push:', error); @@ -598,15 +591,15 @@ current branch '${currentBranch}' ahead of base '${baseRef}'.`, ); // Git: Get branch status (current branch, default branch, ahead/behind counts) - ipcMain.handle('git:get-branch-status', async (_, args: { workspacePath: string }) => { - const { workspacePath } = args || ({} as { workspacePath: string }); + ipcMain.handle('git:get-branch-status', async (_, args: { taskPath: string }) => { + const { taskPath } = args || ({} as { taskPath: string }); try { // Ensure repo (avoid /bin/sh by using execFile) - await execFileAsync(GIT, ['rev-parse', '--is-inside-work-tree'], { cwd: workspacePath }); + await execFileAsync(GIT, ['rev-parse', '--is-inside-work-tree'], { cwd: taskPath }); // Current branch const { stdout: currentBranchOut } = await execFileAsync(GIT, ['branch', '--show-current'], { - cwd: workspacePath, + cwd: taskPath, }); const branch = (currentBranchOut || '').trim(); @@ -616,7 +609,7 @@ current branch '${currentBranch}' ahead of base '${baseRef}'.`, const { stdout } = await execFileAsync( 'gh', ['repo', 'view', '--json', 'defaultBranchRef', '-q', '.defaultBranchRef.name'], - { cwd: workspacePath } + { cwd: taskPath } ); const db = (stdout || '').trim(); if (db) defaultBranch = db; @@ -626,7 +619,7 @@ current branch '${currentBranch}' ahead of base '${baseRef}'.`, const { stdout } = await execFileAsync( GIT, ['symbolic-ref', '--short', 'refs/remotes/origin/HEAD'], - { cwd: workspacePath } + { cwd: taskPath } ); const line = (stdout || '').trim(); const last = line.split('/').pop(); @@ -642,7 +635,7 @@ current branch '${currentBranch}' ahead of base '${baseRef}'.`, const { stdout } = await execFileAsync( GIT, ['rev-list', '--left-right', '--count', `origin/${defaultBranch}...HEAD`], - { cwd: workspacePath } + { cwd: taskPath } ); const parts = (stdout || '').trim().split(/\s+/); if (parts.length >= 2) { @@ -651,7 +644,7 @@ current branch '${currentBranch}' ahead of base '${baseRef}'.`, } } catch { try { - const { stdout } = await execFileAsync(GIT, ['status', '-sb'], { cwd: workspacePath }); + const { stdout } = await execFileAsync(GIT, ['status', '-sb'], { cwd: taskPath }); const line = (stdout || '').split(/\n/)[0] || ''; const m = line.match(/ahead\s+(\d+)/i); const n = line.match(/behind\s+(\d+)/i); diff --git a/src/main/ipc/githubIpc.ts b/src/main/ipc/githubIpc.ts index 6db26227..663fe321 100644 --- a/src/main/ipc/githubIpc.ts +++ b/src/main/ipc/githubIpc.ts @@ -260,7 +260,7 @@ export function registerGithubIpc() { projectId: string; prNumber: number; prTitle?: string; - workspaceName?: string; + taskName?: string; branchName?: string; } ) => { @@ -271,9 +271,9 @@ export function registerGithubIpc() { } const defaultSlug = slugify(args.prTitle || `pr-${prNumber}`) || `pr-${prNumber}`; - const workspaceName = - args.workspaceName && args.workspaceName.trim().length > 0 - ? args.workspaceName.trim() + const taskName = + args.taskName && args.taskName.trim().length > 0 + ? args.taskName.trim() : `pr-${prNumber}-${defaultSlug}`; const branchName = args.branchName || `pr/${prNumber}`; @@ -282,13 +282,13 @@ export function registerGithubIpc() { const existing = currentWorktrees.find((wt) => wt.branch === branchName); if (existing) { - return { success: true, worktree: existing, branchName, workspaceName: existing.name }; + return { success: true, worktree: existing, branchName, taskName: existing.name }; } await githubService.ensurePullRequestBranch(projectPath, prNumber, branchName); const worktreesDir = path.resolve(projectPath, '..', 'worktrees'); - const slug = slugify(workspaceName) || `pr-${prNumber}`; + const slug = slugify(taskName) || `pr-${prNumber}`; let worktreePath = path.join(worktreesDir, slug); if (fs.existsSync(worktreePath)) { @@ -297,13 +297,13 @@ export function registerGithubIpc() { const worktree = await worktreeService.createWorktreeFromBranch( projectPath, - workspaceName, + taskName, branchName, projectId, { worktreePath } ); - return { success: true, worktree, branchName, workspaceName }; + return { success: true, worktree, branchName, taskName }; } catch (error) { log.error('Failed to create PR worktree:', error); const message = diff --git a/src/main/ipc/hostPreviewIpc.ts b/src/main/ipc/hostPreviewIpc.ts index 71552398..7116d803 100644 --- a/src/main/ipc/hostPreviewIpc.ts +++ b/src/main/ipc/hostPreviewIpc.ts @@ -7,15 +7,15 @@ export function registerHostPreviewIpc() { async ( _e, args: { - workspaceId: string; - workspacePath: string; + taskId: string; + taskPath: string; script?: string; parentProjectPath?: string; } ) => { - const id = String(args?.workspaceId || '').trim(); - const wp = String(args?.workspacePath || '').trim(); - if (!id || !wp) return { ok: false, error: 'workspaceId and workspacePath are required' }; + const id = String(args?.taskId || '').trim(); + const wp = String(args?.taskPath || '').trim(); + if (!id || !wp) return { ok: false, error: 'taskId and taskPath are required' }; return hostPreviewService.start(id, wp, { script: args?.script, parentProjectPath: args?.parentProjectPath, @@ -23,15 +23,12 @@ export function registerHostPreviewIpc() { } ); - ipcMain.handle( - 'preview:host:setup', - async (_e, args: { workspaceId: string; workspacePath: string }) => { - const id = String(args?.workspaceId || '').trim(); - const wp = String(args?.workspacePath || '').trim(); - if (!id || !wp) return { ok: false, error: 'workspaceId and workspacePath are required' }; - return hostPreviewService.setup(id, wp); - } - ); + ipcMain.handle('preview:host:setup', async (_e, args: { taskId: string; taskPath: string }) => { + const id = String(args?.taskId || '').trim(); + const wp = String(args?.taskPath || '').trim(); + if (!id || !wp) return { ok: false, error: 'taskId and taskPath are required' }; + return hostPreviewService.setup(id, wp); + }); ipcMain.handle('preview:host:stop', async (_e, id: string) => { const wid = String(id || '').trim(); diff --git a/src/main/ipc/telemetryIpc.ts b/src/main/ipc/telemetryIpc.ts index e4f0b476..e7ae7527 100644 --- a/src/main/ipc/telemetryIpc.ts +++ b/src/main/ipc/telemetryIpc.ts @@ -9,7 +9,7 @@ import { // Events allowed from renderer process // Main process-only events (app_started, app_closed, app_window_focused, github_connection_triggered, -// github_connected, workspace_snapshot, app_session, agent_run_start, agent_run_finish) should NOT be here +// github_connected, task_snapshot, app_session, agent_run_start, agent_run_finish) should NOT be here const RENDERER_ALLOWED_EVENTS = new Set([ // Legacy 'feature_used', @@ -20,12 +20,12 @@ const RENDERER_ALLOWED_EVENTS = new Set([ 'project_added_success', 'project_deleted', 'project_view_opened', - // Workspace management - 'workspace_created', - 'workspace_deleted', - 'workspace_provider_switched', - 'workspace_custom_named', - 'workspace_advanced_options_opened', + // Task management + 'task_created', + 'task_deleted', + 'task_provider_switched', + 'task_custom_named', + 'task_advanced_options_opened', // Terminal (Right Sidebar) 'terminal_entered', 'terminal_command_executed', diff --git a/src/main/main.ts b/src/main/main.ts index 389fc1fe..0b1644ac 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -146,21 +146,21 @@ app.whenReady().then(async () => { // Initialize telemetry (privacy-first, anonymous) telemetry.init({ installSource: app.isPackaged ? 'dmg' : 'dev' }); - // Best-effort: capture a coarse snapshot of project/workspace counts (no names/paths) + // Best-effort: capture a coarse snapshot of project/task counts (no names/paths) try { - const [projects, workspaces] = await Promise.all([ + const [projects, tasks] = await Promise.all([ databaseService.getProjects(), - databaseService.getWorkspaces(), + databaseService.getTasks(), ]); const projectCount = projects.length; - const workspaceCount = workspaces.length; + const taskCount = tasks.length; const toBucket = (n: number) => n === 0 ? '0' : n <= 2 ? '1-2' : n <= 5 ? '3-5' : n <= 10 ? '6-10' : '>10'; - telemetry.capture('workspace_snapshot', { + telemetry.capture('task_snapshot', { project_count: projectCount, project_count_bucket: toBucket(projectCount), - workspace_count: workspaceCount, - workspace_count_bucket: toBucket(workspaceCount), + task_count: taskCount, + task_count_bucket: toBucket(taskCount), } as any); } catch { // ignore errors — telemetry is best-effort only diff --git a/src/main/preload.ts b/src/main/preload.ts index 4bfa33bb..513b241a 100644 --- a/src/main/preload.ts +++ b/src/main/preload.ts @@ -85,7 +85,7 @@ contextBridge.exposeInMainWorld('electronAPI', { // Worktree management worktreeCreate: (args: { projectPath: string; - workspaceName: string; + taskName: string; projectId: string; autoApprove?: boolean; }) => ipcRenderer.invoke('worktree:create', args), @@ -111,7 +111,7 @@ contextBridge.exposeInMainWorld('electronAPI', { ipcRenderer.invoke('fs:write', { root, relPath, content, mkdirs }), fsRemove: (root: string, relPath: string) => ipcRenderer.invoke('fs:remove', { root, relPath }), // Attachments - saveAttachment: (args: { workspacePath: string; srcPath: string; subdir?: string }) => + saveAttachment: (args: { taskPath: string; srcPath: string; subdir?: string }) => ipcRenderer.invoke('fs:save-attachment', args), // Project management @@ -123,23 +123,23 @@ contextBridge.exposeInMainWorld('electronAPI', { fetchProjectBaseRef: (args: { projectId: string; projectPath: string }) => ipcRenderer.invoke('projectSettings:fetchBaseRef', args), getGitInfo: (projectPath: string) => ipcRenderer.invoke('git:getInfo', projectPath), - getGitStatus: (workspacePath: string) => ipcRenderer.invoke('git:get-status', workspacePath), - getFileDiff: (args: { workspacePath: string; filePath: string }) => + getGitStatus: (taskPath: string) => ipcRenderer.invoke('git:get-status', taskPath), + getFileDiff: (args: { taskPath: string; filePath: string }) => ipcRenderer.invoke('git:get-file-diff', args), - stageFile: (args: { workspacePath: string; filePath: string }) => + stageFile: (args: { taskPath: string; filePath: string }) => ipcRenderer.invoke('git:stage-file', args), - revertFile: (args: { workspacePath: string; filePath: string }) => + revertFile: (args: { taskPath: string; filePath: string }) => ipcRenderer.invoke('git:revert-file', args), gitCommitAndPush: (args: { - workspacePath: string; + taskPath: string; commitMessage?: string; createBranchIfOnDefault?: boolean; branchPrefix?: string; }) => ipcRenderer.invoke('git:commit-and-push', args), - generatePrContent: (args: { workspacePath: string; base?: string }) => + generatePrContent: (args: { taskPath: string; base?: string }) => ipcRenderer.invoke('git:generate-pr-content', args), createPullRequest: (args: { - workspacePath: string; + taskPath: string; title?: string; body?: string; base?: string; @@ -148,24 +148,22 @@ contextBridge.exposeInMainWorld('electronAPI', { web?: boolean; fill?: boolean; }) => ipcRenderer.invoke('git:create-pr', args), - getPrStatus: (args: { workspacePath: string }) => ipcRenderer.invoke('git:get-pr-status', args), - getBranchStatus: (args: { workspacePath: string }) => + getPrStatus: (args: { taskPath: string }) => ipcRenderer.invoke('git:get-pr-status', args), + getBranchStatus: (args: { taskPath: string }) => ipcRenderer.invoke('git:get-branch-status', args), listRemoteBranches: (args: { projectPath: string; remote?: string }) => ipcRenderer.invoke('git:list-remote-branches', args), - loadContainerConfig: (workspacePath: string) => - ipcRenderer.invoke('container:load-config', { workspacePath }), + loadContainerConfig: (taskPath: string) => + ipcRenderer.invoke('container:load-config', { taskPath }), startContainerRun: (args: { - workspaceId: string; - workspacePath: string; + taskId: string; + taskPath: string; runId?: string; mode?: 'container' | 'host'; }) => ipcRenderer.invoke('container:start-run', args), - stopContainerRun: (workspaceId: string) => - ipcRenderer.invoke('container:stop-run', { workspaceId }), - inspectContainerRun: (workspaceId: string) => - ipcRenderer.invoke('container:inspect-run', { workspaceId }), - resolveServiceIcon: (args: { service: string; allowNetwork?: boolean; workspacePath?: string }) => + stopContainerRun: (taskId: string) => ipcRenderer.invoke('container:stop-run', { taskId }), + inspectContainerRun: (taskId: string) => ipcRenderer.invoke('container:inspect-run', { taskId }), + resolveServiceIcon: (args: { service: string; allowNetwork?: boolean; taskPath?: string }) => ipcRenderer.invoke('icons:resolve-service', args), openExternal: (url: string) => ipcRenderer.invoke('app:openExternal', url), // Telemetry (minimal, anonymous) @@ -243,7 +241,7 @@ contextBridge.exposeInMainWorld('electronAPI', { projectId: string; prNumber: number; prTitle?: string; - workspaceName?: string; + taskName?: string; branchName?: string; }) => ipcRenderer.invoke('github:createPullRequestWorktree', args), githubLogout: () => ipcRenderer.invoke('github:logout'), @@ -276,16 +274,16 @@ contextBridge.exposeInMainWorld('electronAPI', { // Database methods getProjects: () => ipcRenderer.invoke('db:getProjects'), saveProject: (project: any) => ipcRenderer.invoke('db:saveProject', project), - getWorkspaces: (projectId?: string) => ipcRenderer.invoke('db:getWorkspaces', projectId), - saveWorkspace: (workspace: any) => ipcRenderer.invoke('db:saveWorkspace', workspace), + getTasks: (projectId?: string) => ipcRenderer.invoke('db:getTasks', projectId), + saveTask: (task: any) => ipcRenderer.invoke('db:saveTask', task), deleteProject: (projectId: string) => ipcRenderer.invoke('db:deleteProject', projectId), - deleteWorkspace: (workspaceId: string) => ipcRenderer.invoke('db:deleteWorkspace', workspaceId), + deleteTask: (taskId: string) => ipcRenderer.invoke('db:deleteTask', taskId), // Conversation management saveConversation: (conversation: any) => ipcRenderer.invoke('db:saveConversation', conversation), - getConversations: (workspaceId: string) => ipcRenderer.invoke('db:getConversations', workspaceId), - getOrCreateDefaultConversation: (workspaceId: string) => - ipcRenderer.invoke('db:getOrCreateDefaultConversation', workspaceId), + getConversations: (taskId: string) => ipcRenderer.invoke('db:getConversations', taskId), + getOrCreateDefaultConversation: (taskId: string) => + ipcRenderer.invoke('db:getOrCreateDefaultConversation', taskId), saveMessage: (message: any) => ipcRenderer.invoke('db:saveMessage', message), getMessages: (conversationId: string) => ipcRenderer.invoke('db:getMessages', conversationId), deleteConversation: (conversationId: string) => @@ -296,8 +294,8 @@ contextBridge.exposeInMainWorld('electronAPI', { ipcRenderer.invoke('debug:append-log', filePath, content, options ?? {}), // PlanMode strict lock - planApplyLock: (workspacePath: string) => ipcRenderer.invoke('plan:lock', workspacePath), - planReleaseLock: (workspacePath: string) => ipcRenderer.invoke('plan:unlock', workspacePath), + planApplyLock: (taskPath: string) => ipcRenderer.invoke('plan:lock', taskPath), + planReleaseLock: (taskPath: string) => ipcRenderer.invoke('plan:unlock', taskPath), onPlanEvent: ( listener: (data: { type: 'write_blocked' | 'remove_blocked'; @@ -322,14 +320,14 @@ contextBridge.exposeInMainWorld('electronAPI', { // Host preview (non-container) hostPreviewStart: (args: { - workspaceId: string; - workspacePath: string; + taskId: string; + taskPath: string; script?: string; parentProjectPath?: string; }) => ipcRenderer.invoke('preview:host:start', args), - hostPreviewSetup: (args: { workspaceId: string; workspacePath: string }) => + hostPreviewSetup: (args: { taskId: string; taskPath: string }) => ipcRenderer.invoke('preview:host:setup', args), - hostPreviewStop: (workspaceId: string) => ipcRenderer.invoke('preview:host:stop', workspaceId), + hostPreviewStop: (taskId: string) => ipcRenderer.invoke('preview:host:stop', taskId), hostPreviewStopAll: (exceptId?: string) => ipcRenderer.invoke('preview:host:stopAll', exceptId), onHostPreviewEvent: (listener: (data: any) => void) => { const channel = 'preview:host:event'; @@ -426,7 +424,7 @@ export interface ElectronAPI { // Worktree management worktreeCreate: (args: { projectPath: string; - workspaceName: string; + taskName: string; projectId: string; autoApprove?: boolean; }) => Promise<{ success: boolean; worktree?: any; error?: string }>; @@ -463,7 +461,7 @@ export interface ElectronAPI { rootPath?: string; error?: string; }>; - getGitStatus: (workspacePath: string) => Promise<{ + getGitStatus: (taskPath: string) => Promise<{ success: boolean; changes?: Array<{ path: string; @@ -474,19 +472,19 @@ export interface ElectronAPI { }>; error?: string; }>; - getFileDiff: (args: { workspacePath: string; filePath: string }) => Promise<{ + getFileDiff: (args: { taskPath: string; filePath: string }) => Promise<{ success: boolean; diff?: { lines: Array<{ left?: string; right?: string; type: 'context' | 'add' | 'del' }> }; error?: string; }>; gitCommitAndPush: (args: { - workspacePath: string; + taskPath: string; commitMessage?: string; createBranchIfOnDefault?: boolean; branchPrefix?: string; }) => Promise<{ success: boolean; branch?: string; output?: string; error?: string }>; createPullRequest: (args: { - workspacePath: string; + taskPath: string; title?: string; body?: string; base?: string; @@ -523,7 +521,7 @@ export interface ElectronAPI { onRunEvent: (callback: (event: any) => void) => void; removeRunEventListeners: () => void; - loadContainerConfig: (workspacePath: string) => Promise< + loadContainerConfig: (taskPath: string) => Promise< | { ok: true; config: any; sourcePath: string | null } | { ok: false; @@ -542,8 +540,8 @@ export interface ElectronAPI { } >; startContainerRun: (args: { - workspaceId: string; - workspacePath: string; + taskId: string; + taskPath: string; runId?: string; mode?: 'container' | 'host'; }) => Promise< @@ -564,7 +562,7 @@ export interface ElectronAPI { }; } >; - stopContainerRun: (workspaceId: string) => Promise<{ ok: boolean; error?: string }>; + stopContainerRun: (taskId: string) => Promise<{ ok: boolean; error?: string }>; // GitHub integration githubAuth: () => Promise<{ @@ -610,13 +608,13 @@ export interface ElectronAPI { projectId: string; prNumber: number; prTitle?: string; - workspaceName?: string; + taskName?: string; branchName?: string; }) => Promise<{ success: boolean; worktree?: any; branchName?: string; - workspaceName?: string; + taskName?: string; error?: string; }>; githubLogout: () => Promise; @@ -626,18 +624,18 @@ export interface ElectronAPI { // Database methods getProjects: () => Promise; saveProject: (project: any) => Promise<{ success: boolean; error?: string }>; - getWorkspaces: (projectId?: string) => Promise; - saveWorkspace: (workspace: any) => Promise<{ success: boolean; error?: string }>; + getTasks: (projectId?: string) => Promise; + saveTask: (task: any) => Promise<{ success: boolean; error?: string }>; deleteProject: (projectId: string) => Promise<{ success: boolean; error?: string }>; - deleteWorkspace: (workspaceId: string) => Promise<{ success: boolean; error?: string }>; + deleteTask: (taskId: string) => Promise<{ success: boolean; error?: string }>; // Conversation management saveConversation: (conversation: any) => Promise<{ success: boolean; error?: string }>; getConversations: ( - workspaceId: string + taskId: string ) => Promise<{ success: boolean; conversations?: any[]; error?: string }>; getOrCreateDefaultConversation: ( - workspaceId: string + taskId: string ) => Promise<{ success: boolean; conversation?: any; error?: string }>; saveMessage: (message: any) => Promise<{ success: boolean; error?: string }>; getMessages: ( @@ -647,18 +645,18 @@ export interface ElectronAPI { // Host preview (non-container) hostPreviewStart: (args: { - workspaceId: string; - workspacePath: string; + taskId: string; + taskPath: string; script?: string; parentProjectPath?: string; }) => Promise<{ ok: boolean; error?: string }>; hostPreviewSetup: (args: { - workspaceId: string; - workspacePath: string; + taskId: string; + taskPath: string; }) => Promise<{ ok: boolean; error?: string }>; - hostPreviewStop: (workspaceId: string) => Promise<{ ok: boolean }>; + hostPreviewStop: (taskId: string) => Promise<{ ok: boolean }>; onHostPreviewEvent: ( - listener: (data: { type: 'url'; workspaceId: string; url: string }) => void + listener: (data: { type: 'url'; taskId: string; url: string }) => void ) => () => void; // Main-managed browser (WebContentsView) diff --git a/src/main/services/DatabaseService.ts b/src/main/services/DatabaseService.ts index bcb1eb70..bc548489 100644 --- a/src/main/services/DatabaseService.ts +++ b/src/main/services/DatabaseService.ts @@ -5,11 +5,11 @@ import { resolveDatabasePath, resolveMigrationsPath } from '../db/path'; import { getDrizzleClient } from '../db/drizzleClient'; import { projects as projectsTable, - workspaces as workspacesTable, + tasks as tasksTable, conversations as conversationsTable, messages as messagesTable, type ProjectRow, - type WorkspaceRow, + type TaskRow, type ConversationRow, type MessageRow, } from '../db/schema'; @@ -32,7 +32,7 @@ export interface Project { updatedAt: string; } -export interface Workspace { +export interface Task { id: string; projectId: string; name: string; @@ -47,7 +47,7 @@ export interface Workspace { export interface Conversation { id: string; - workspaceId: string; + taskId: string; title: string; createdAt: string; updatedAt: string; @@ -207,69 +207,65 @@ export class DatabaseService { return this.getProjectById(projectId); } - async saveWorkspace(workspace: Omit): Promise { + async saveTask(task: Omit): Promise { if (this.disabled) return; const metadataValue = - typeof workspace.metadata === 'string' - ? workspace.metadata - : workspace.metadata - ? JSON.stringify(workspace.metadata) + typeof task.metadata === 'string' + ? task.metadata + : task.metadata + ? JSON.stringify(task.metadata) : null; const { db } = await getDrizzleClient(); await db - .insert(workspacesTable) + .insert(tasksTable) .values({ - id: workspace.id, - projectId: workspace.projectId, - name: workspace.name, - branch: workspace.branch, - path: workspace.path, - status: workspace.status, - agentId: workspace.agentId ?? null, + id: task.id, + projectId: task.projectId, + name: task.name, + branch: task.branch, + path: task.path, + status: task.status, + agentId: task.agentId ?? null, metadata: metadataValue, updatedAt: sql`CURRENT_TIMESTAMP`, }) .onConflictDoUpdate({ - target: workspacesTable.id, + target: tasksTable.id, set: { - projectId: workspace.projectId, - name: workspace.name, - branch: workspace.branch, - path: workspace.path, - status: workspace.status, - agentId: workspace.agentId ?? null, + projectId: task.projectId, + name: task.name, + branch: task.branch, + path: task.path, + status: task.status, + agentId: task.agentId ?? null, metadata: metadataValue, updatedAt: sql`CURRENT_TIMESTAMP`, }, }); } - async getWorkspaces(projectId?: string): Promise { + async getTasks(projectId?: string): Promise { if (this.disabled) return []; const { db } = await getDrizzleClient(); - const rows: WorkspaceRow[] = projectId + const rows: TaskRow[] = projectId ? await db .select() - .from(workspacesTable) - .where(eq(workspacesTable.projectId, projectId)) - .orderBy(desc(workspacesTable.updatedAt)) - : await db.select().from(workspacesTable).orderBy(desc(workspacesTable.updatedAt)); - return rows.map((row) => this.mapDrizzleWorkspaceRow(row)); + .from(tasksTable) + .where(eq(tasksTable.projectId, projectId)) + .orderBy(desc(tasksTable.updatedAt)) + : await db.select().from(tasksTable).orderBy(desc(tasksTable.updatedAt)); + return rows.map((row) => this.mapDrizzleTaskRow(row)); } - async getWorkspaceByPath(workspacePath: string): Promise { + async getTaskByPath(taskPath: string): Promise { if (this.disabled) return null; const { db } = await getDrizzleClient(); - const rows = await db - .select() - .from(workspacesTable) - .where(eq(workspacesTable.path, workspacePath)) - .limit(1); + const rows = await db.select().from(tasksTable).where(eq(tasksTable.path, taskPath)).limit(1); if (rows.length === 0) return null; - return this.mapDrizzleWorkspaceRow(rows[0]); + return this.mapDrizzleTaskRow(rows[0]); } async deleteProject(projectId: string): Promise { @@ -278,10 +274,10 @@ export class DatabaseService { await db.delete(projectsTable).where(eq(projectsTable.id, projectId)); } - async deleteWorkspace(workspaceId: string): Promise { + async deleteTask(taskId: string): Promise { if (this.disabled) return; const { db } = await getDrizzleClient(); - await db.delete(workspacesTable).where(eq(workspacesTable.id, workspaceId)); + await db.delete(tasksTable).where(eq(tasksTable.id, taskId)); } // Conversation management methods @@ -293,7 +289,7 @@ export class DatabaseService { .insert(conversationsTable) .values({ id: conversation.id, - workspaceId: conversation.workspaceId, + taskId: conversation.taskId, title: conversation.title, updatedAt: sql`CURRENT_TIMESTAMP`, }) @@ -306,22 +302,22 @@ export class DatabaseService { }); } - async getConversations(workspaceId: string): Promise { + async getConversations(taskId: string): Promise { if (this.disabled) return []; const { db } = await getDrizzleClient(); const rows = await db .select() .from(conversationsTable) - .where(eq(conversationsTable.workspaceId, workspaceId)) + .where(eq(conversationsTable.taskId, taskId)) .orderBy(desc(conversationsTable.updatedAt)); return rows.map((row) => this.mapDrizzleConversationRow(row)); } - async getOrCreateDefaultConversation(workspaceId: string): Promise { + async getOrCreateDefaultConversation(taskId: string): Promise { if (this.disabled) { return { - id: `conv-${workspaceId}-default`, - workspaceId, + id: `conv-${taskId}-default`, + taskId, title: 'Default Conversation', createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), @@ -332,7 +328,7 @@ export class DatabaseService { const existingRows = await db .select() .from(conversationsTable) - .where(eq(conversationsTable.workspaceId, workspaceId)) + .where(eq(conversationsTable.taskId, taskId)) .orderBy(asc(conversationsTable.createdAt)) .limit(1); @@ -340,10 +336,10 @@ export class DatabaseService { return this.mapDrizzleConversationRow(existingRows[0]); } - const conversationId = `conv-${workspaceId}-${Date.now()}`; + const conversationId = `conv-${taskId}-${Date.now()}`; await this.saveConversation({ id: conversationId, - workspaceId, + taskId, title: 'Default Conversation', }); @@ -359,7 +355,7 @@ export class DatabaseService { return { id: conversationId, - workspaceId, + taskId, title: 'Default Conversation', createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), @@ -483,18 +479,18 @@ export class DatabaseService { }; } - private mapDrizzleWorkspaceRow(row: WorkspaceRow): Workspace { + private mapDrizzleTaskRow(row: TaskRow): Task { return { id: row.id, projectId: row.projectId, name: row.name, branch: row.branch, path: row.path, - status: (row.status as Workspace['status']) ?? 'idle', + status: (row.status as Task['status']) ?? 'idle', agentId: row.agentId ?? null, metadata: typeof row.metadata === 'string' && row.metadata.length > 0 - ? this.parseWorkspaceMetadata(row.metadata, row.id) + ? this.parseTaskMetadata(row.metadata, row.id) : null, createdAt: row.createdAt, updatedAt: row.updatedAt, @@ -504,7 +500,7 @@ export class DatabaseService { private mapDrizzleConversationRow(row: ConversationRow): Conversation { return { id: row.id, - workspaceId: row.workspaceId, + taskId: row.taskId, title: row.title, createdAt: row.createdAt, updatedAt: row.updatedAt, @@ -522,11 +518,11 @@ export class DatabaseService { }; } - private parseWorkspaceMetadata(serialized: string, workspaceId: string): any { + private parseTaskMetadata(serialized: string, taskId: string): any { try { return JSON.parse(serialized); } catch (error) { - console.warn(`Failed to parse workspace metadata for ${workspaceId}`, error); + console.warn(`Failed to parse task metadata for ${taskId}`, error); return null; } } diff --git a/src/main/services/GitHubService.ts b/src/main/services/GitHubService.ts index d098054a..f7b337df 100644 --- a/src/main/services/GitHubService.ts +++ b/src/main/services/GitHubService.ts @@ -748,7 +748,7 @@ export class GitHubService { } /** - * Clone a repository to local workspace + * Clone a repository to local task directory */ async cloneRepository( repoUrl: string, diff --git a/src/main/services/GitService.ts b/src/main/services/GitService.ts index fe32c4b0..dc13bb73 100644 --- a/src/main/services/GitService.ts +++ b/src/main/services/GitService.ts @@ -13,10 +13,10 @@ export type GitChange = { isStaged: boolean; }; -export async function getStatus(workspacePath: string): Promise { +export async function getStatus(taskPath: string): Promise { try { await execFileAsync('git', ['rev-parse', '--is-inside-work-tree'], { - cwd: workspacePath, + cwd: taskPath, }); } catch { return []; @@ -26,7 +26,7 @@ export async function getStatus(workspacePath: string): Promise { 'git', ['status', '--porcelain', '--untracked-files=all'], { - cwd: workspacePath, + cwd: taskPath, } ); @@ -80,20 +80,20 @@ export async function getStatus(workspacePath: string): Promise { try { const staged = await execFileAsync('git', ['diff', '--numstat', '--cached', '--', filePath], { - cwd: workspacePath, + cwd: taskPath, }); if (staged.stdout && staged.stdout.trim()) sumNumstat(staged.stdout); } catch {} try { const unstaged = await execFileAsync('git', ['diff', '--numstat', '--', filePath], { - cwd: workspacePath, + cwd: taskPath, }); if (unstaged.stdout && unstaged.stdout.trim()) sumNumstat(unstaged.stdout); } catch {} if (additions === 0 && deletions === 0 && statusCode.includes('?')) { - const absPath = path.join(workspacePath, filePath); + const absPath = path.join(taskPath, filePath); try { const stat = fs.existsSync(absPath) ? fs.statSync(absPath) : undefined; if (stat && stat.isFile()) { @@ -111,12 +111,12 @@ export async function getStatus(workspacePath: string): Promise { return changes; } -export async function stageFile(workspacePath: string, filePath: string): Promise { - await execFileAsync('git', ['add', '--', filePath], { cwd: workspacePath }); +export async function stageFile(taskPath: string, filePath: string): Promise { + await execFileAsync('git', ['add', '--', filePath], { cwd: taskPath }); } export async function revertFile( - workspacePath: string, + taskPath: string, filePath: string ): Promise<{ action: 'unstaged' | 'reverted' }> { // Check if file is staged @@ -125,13 +125,13 @@ export async function revertFile( 'git', ['diff', '--cached', '--name-only', '--', filePath], { - cwd: workspacePath, + cwd: taskPath, } ); if (stagedStatus.trim()) { // File is staged, unstage it (but keep working directory changes) - await execFileAsync('git', ['reset', 'HEAD', '--', filePath], { cwd: workspacePath }); + await execFileAsync('git', ['reset', 'HEAD', '--', filePath], { cwd: taskPath }); return { action: 'unstaged' }; } } catch {} @@ -139,11 +139,11 @@ export async function revertFile( // Check if file is tracked in git (exists in HEAD) let fileExistsInHead = false; try { - await execFileAsync('git', ['cat-file', '-e', `HEAD:${filePath}`], { cwd: workspacePath }); + await execFileAsync('git', ['cat-file', '-e', `HEAD:${filePath}`], { cwd: taskPath }); fileExistsInHead = true; } catch { // File doesn't exist in HEAD (it's a new/untracked file), delete it - const absPath = path.join(workspacePath, filePath); + const absPath = path.join(taskPath, filePath); if (fs.existsSync(absPath)) { fs.unlinkSync(absPath); } @@ -153,7 +153,7 @@ export async function revertFile( // File exists in HEAD, revert it if (fileExistsInHead) { try { - await execFileAsync('git', ['checkout', 'HEAD', '--', filePath], { cwd: workspacePath }); + await execFileAsync('git', ['checkout', 'HEAD', '--', filePath], { cwd: taskPath }); } catch (error) { // If checkout fails, don't delete the file - throw the error instead throw new Error( @@ -165,14 +165,14 @@ export async function revertFile( } export async function getFileDiff( - workspacePath: string, + taskPath: string, filePath: string ): Promise<{ lines: Array<{ left?: string; right?: string; type: 'context' | 'add' | 'del' }> }> { try { const { stdout } = await execFileAsync( 'git', ['diff', '--no-color', '--unified=2000', 'HEAD', '--', filePath], - { cwd: workspacePath } + { cwd: taskPath } ); const linesRaw = stdout.split('\n'); @@ -197,13 +197,13 @@ export async function getFileDiff( if (result.length === 0) { try { - const abs = path.join(workspacePath, filePath); + const abs = path.join(taskPath, filePath); if (fs.existsSync(abs)) { const content = fs.readFileSync(abs, 'utf8'); return { lines: content.split('\n').map((l) => ({ right: l, type: 'add' as const })) }; } else { const { stdout: prev } = await execFileAsync('git', ['show', `HEAD:${filePath}`], { - cwd: workspacePath, + cwd: taskPath, }); return { lines: prev.split('\n').map((l) => ({ left: l, type: 'del' as const })) }; } @@ -215,7 +215,7 @@ export async function getFileDiff( return { lines: result }; } catch { try { - const abs = path.join(workspacePath, filePath); + const abs = path.join(taskPath, filePath); const content = fs.readFileSync(abs, 'utf8'); const lines = content.split('\n'); return { lines: lines.map((l) => ({ right: l, type: 'add' as const })) }; @@ -224,7 +224,7 @@ export async function getFileDiff( const { stdout } = await execFileAsync( 'git', ['diff', '--no-color', '--unified=2000', 'HEAD', '--', filePath], - { cwd: workspacePath } + { cwd: taskPath } ); const linesRaw = stdout.split('\n'); const result: Array<{ left?: string; right?: string; type: 'context' | 'add' | 'del' }> = @@ -249,7 +249,7 @@ export async function getFileDiff( if (result.length === 0) { try { const { stdout: prev } = await execFileAsync('git', ['show', `HEAD:${filePath}`], { - cwd: workspacePath, + cwd: taskPath, }); return { lines: prev.split('\n').map((l) => ({ left: l, type: 'del' as const })) }; } catch { diff --git a/src/main/services/PrGenerationService.ts b/src/main/services/PrGenerationService.ts index aff76fc3..24f0a28b 100644 --- a/src/main/services/PrGenerationService.ts +++ b/src/main/services/PrGenerationService.ts @@ -17,57 +17,49 @@ export interface GeneratedPrContent { export class PrGenerationService { /** * Generate PR title and description based on git changes - * @param workspacePath - Path to the workspace + * @param taskPath - Path to the task * @param baseBranch - Base branch to compare against (default: 'main') - * @param preferredProviderId - Optional provider ID to use first (e.g., from workspace.agentId) + * @param preferredProviderId - Optional provider ID to use first (e.g., from task.agentId) */ async generatePrContent( - workspacePath: string, + taskPath: string, baseBranch: string = 'main', preferredProviderId?: string | null ): Promise { try { // Get git diff and commit messages - const { diff, commits, changedFiles } = await this.getGitContext(workspacePath, baseBranch); + const { diff, commits, changedFiles } = await this.getGitContext(taskPath, baseBranch); if (!diff && commits.length === 0) { return this.generateFallbackContent(changedFiles); } - // Try the workspace's provider first if specified + // Try the task's provider first if specified if (preferredProviderId && this.isValidProviderId(preferredProviderId)) { try { const preferredResult = await this.generateWithProvider( preferredProviderId as ProviderId, - workspacePath, + taskPath, diff, commits ); if (preferredResult) { - log.info(`Generated PR content with workspace provider: ${preferredProviderId}`); + log.info(`Generated PR content with task provider: ${preferredProviderId}`); return { title: preferredResult.title, description: this.normalizeMarkdown(preferredResult.description), }; } } catch (error) { - log.debug( - `Workspace provider ${preferredProviderId} generation failed, trying fallbacks`, - { - error, - } - ); + log.debug(`Task provider ${preferredProviderId} generation failed, trying fallbacks`, { + error, + }); } } // Try Claude Code as fallback (preferred default) try { - const claudeResult = await this.generateWithProvider( - 'claude', - workspacePath, - diff, - commits - ); + const claudeResult = await this.generateWithProvider('claude', taskPath, diff, commits); if (claudeResult) { log.info('Generated PR content with Claude Code'); return { @@ -81,7 +73,7 @@ export class PrGenerationService { // Try Codex as fallback try { - const codexResult = await this.generateWithProvider('codex', workspacePath, diff, commits); + const codexResult = await this.generateWithProvider('codex', taskPath, diff, commits); if (codexResult) { log.info('Generated PR content with Codex'); return { @@ -105,7 +97,7 @@ export class PrGenerationService { * Get git context (diff, commits, changed files) for PR generation */ private async getGitContext( - workspacePath: string, + taskPath: string, baseBranch: string ): Promise<{ diff: string; commits: string[]; changedFiles: string[] }> { let diff = ''; @@ -116,11 +108,11 @@ export class PrGenerationService { // Check if base branch exists (local or remote) let baseBranchExists = false; try { - await execAsync(`git rev-parse --verify ${baseBranch}`, { cwd: workspacePath }); + await execAsync(`git rev-parse --verify ${baseBranch}`, { cwd: taskPath }); baseBranchExists = true; } catch { try { - await execAsync(`git rev-parse --verify origin/${baseBranch}`, { cwd: workspacePath }); + await execAsync(`git rev-parse --verify origin/${baseBranch}`, { cwd: taskPath }); baseBranchExists = true; baseBranch = `origin/${baseBranch}`; } catch { @@ -132,7 +124,7 @@ export class PrGenerationService { // Get diff between base branch and current HEAD (committed changes) try { const { stdout: diffOut } = await execAsync(`git diff ${baseBranch}...HEAD --stat`, { - cwd: workspacePath, + cwd: taskPath, maxBuffer: 10 * 1024 * 1024, }); diff = diffOut || ''; @@ -140,7 +132,7 @@ export class PrGenerationService { // Get list of changed files from commits const { stdout: filesOut } = await execAsync( `git diff --name-only ${baseBranch}...HEAD`, - { cwd: workspacePath } + { cwd: taskPath } ); const committedFiles = (filesOut || '') .split('\n') @@ -151,7 +143,7 @@ export class PrGenerationService { // Get commit messages const { stdout: commitsOut } = await execAsync( `git log ${baseBranch}..HEAD --pretty=format:"%s"`, - { cwd: workspacePath } + { cwd: taskPath } ); commits = (commitsOut || '') .split('\n') @@ -166,7 +158,7 @@ export class PrGenerationService { // This ensures PR description includes changes that will be committed try { const { stdout: workingDiff } = await execAsync('git diff --stat', { - cwd: workspacePath, + cwd: taskPath, maxBuffer: 10 * 1024 * 1024, }); const workingDiffText = workingDiff || ''; @@ -182,7 +174,7 @@ export class PrGenerationService { // Get uncommitted changed files and merge with committed files const { stdout: filesOut } = await execAsync('git diff --name-only', { - cwd: workspacePath, + cwd: taskPath, }); const uncommittedFiles = (filesOut || '') .split('\n') @@ -200,13 +192,13 @@ export class PrGenerationService { if (commits.length === 0 && diff.length === 0) { try { const { stdout: stagedDiff } = await execAsync('git diff --cached --stat', { - cwd: workspacePath, + cwd: taskPath, maxBuffer: 10 * 1024 * 1024, }); if (stagedDiff) { diff = stagedDiff; const { stdout: filesOut } = await execAsync('git diff --cached --name-only', { - cwd: workspacePath, + cwd: taskPath, }); changedFiles = (filesOut || '') .split('\n') @@ -227,7 +219,7 @@ export class PrGenerationService { */ private async generateWithProvider( providerId: ProviderId, - workspacePath: string, + taskPath: string, diff: string, commits: string[] ): Promise { @@ -244,7 +236,7 @@ export class PrGenerationService { // Check if provider CLI is available try { await execFileAsync(cliCommand, provider.versionArgs || ['--version'], { - cwd: workspacePath, + cwd: taskPath, }); } catch { log.debug(`Provider ${providerId} CLI not available`); @@ -282,7 +274,7 @@ export class PrGenerationService { // Spawn the provider CLI const child = spawn(cliCommand, args, { - cwd: workspacePath, + cwd: taskPath, stdio: ['pipe', 'pipe', 'pipe'], env: { ...process.env, diff --git a/src/main/services/TerminalSnapshotService.ts b/src/main/services/TerminalSnapshotService.ts index 099443c3..7f67196c 100644 --- a/src/main/services/TerminalSnapshotService.ts +++ b/src/main/services/TerminalSnapshotService.ts @@ -117,7 +117,7 @@ class TerminalSnapshotService { const json = JSON.stringify(payload); const bytes = Buffer.byteLength(json, 'utf8'); if (bytes > MAX_SNAPSHOT_BYTES) { - return { ok: false, error: 'Snapshot size exceeds per-workspace limit' }; + return { ok: false, error: 'Snapshot size exceeds per-task limit' }; } await ensureDir(); diff --git a/src/main/services/WorktreeService.ts b/src/main/services/WorktreeService.ts index 6537a202..5e406b4a 100644 --- a/src/main/services/WorktreeService.ts +++ b/src/main/services/WorktreeService.ts @@ -25,7 +25,7 @@ export class WorktreeService { private worktrees = new Map(); /** - * Slugify workspace name to make it shell-safe + * Slugify task name to make it shell-safe */ private slugify(name: string): string { return name @@ -45,16 +45,16 @@ export class WorktreeService { } /** - * Create a new Git worktree for an agent workspace + * Create a new Git worktree for an agent task */ async createWorktree( projectPath: string, - workspaceName: string, + taskName: string, projectId: string, autoApprove?: boolean ): Promise { try { - const sluggedName = this.slugify(workspaceName); + const sluggedName = this.slugify(taskName); const timestamp = Date.now(); const { getAppSettings } = await import('../settings'); const settings = getAppSettings(); @@ -113,7 +113,7 @@ export class WorktreeService { const worktreeInfo: WorktreeInfo = { id: worktreeId, - name: workspaceName, + name: taskName, branch: branchName, path: worktreePath, projectId, @@ -123,7 +123,7 @@ export class WorktreeService { this.worktrees.set(worktreeInfo.id, worktreeInfo); - log.info(`Created worktree: ${workspaceName} -> ${branchName}`); + log.info(`Created worktree: ${taskName} -> ${branchName}`); // Push the new branch to origin and set upstream so PRs work out of the box if (settings?.repository?.pushOnCreate !== false) { @@ -249,7 +249,7 @@ export class WorktreeService { n = n.replace(/^[./-]+/, '').replace(/[./-]+$/, ''); // Avoid reserved ref names if (!n || n === 'HEAD') { - n = `agent/${this.slugify('workspace')}-${Date.now()}`; + n = `agent/${this.slugify('task')}-${Date.now()}`; } return n; } @@ -772,13 +772,13 @@ export class WorktreeService { async createWorktreeFromBranch( projectPath: string, - workspaceName: string, + taskName: string, branchName: string, projectId: string, options?: { worktreePath?: string } ): Promise { - const normalizedName = workspaceName || branchName.replace(/\//g, '-'); - const sluggedName = this.slugify(normalizedName) || 'workspace'; + const normalizedName = taskName || branchName.replace(/\//g, '-'); + const sluggedName = this.slugify(normalizedName) || 'task'; const targetPath = options?.worktreePath || path.join(projectPath, '..', `worktrees/${sluggedName}-${Date.now()}`); diff --git a/src/main/services/containerConfigService.ts b/src/main/services/containerConfigService.ts index 4a0fdac4..20d0d124 100644 --- a/src/main/services/containerConfigService.ts +++ b/src/main/services/containerConfigService.ts @@ -55,11 +55,11 @@ export interface ContainerConfigLoadFailure { export type ContainerConfigLoadResult = ContainerConfigLoadSuccess | ContainerConfigLoadFailure; -export async function loadWorkspaceContainerConfig( - workspacePath: string +export async function loadTaskContainerConfig( + taskPath: string ): Promise { - const configPath = path.join(workspacePath, CONFIG_RELATIVE_PATH); - const inferredPackageManager = inferPackageManager(workspacePath); + const configPath = path.join(taskPath, CONFIG_RELATIVE_PATH); + const inferredPackageManager = inferPackageManager(taskPath); const readResult = await readConfigFile(configPath); if (readResult.error) { @@ -97,9 +97,9 @@ export async function loadWorkspaceContainerConfig( } } -function inferPackageManager(workspacePath: string): PackageManager | undefined { +function inferPackageManager(taskPath: string): PackageManager | undefined { for (const { file, manager } of PACKAGE_MANAGER_LOCKFILES) { - const candidate = path.join(workspacePath, file); + const candidate = path.join(taskPath, file); if (fs.existsSync(candidate)) { return manager; } @@ -150,6 +150,6 @@ function parseConfigJson( } } -export function inferPackageManagerForWorkspace(workspacePath: string): PackageManager | undefined { - return inferPackageManager(workspacePath); +export function inferPackageManagerForTask(taskPath: string): PackageManager | undefined { + return inferPackageManager(taskPath); } diff --git a/src/main/services/containerRunnerService.ts b/src/main/services/containerRunnerService.ts index 30ccdd4a..629a41d6 100644 --- a/src/main/services/containerRunnerService.ts +++ b/src/main/services/containerRunnerService.ts @@ -22,7 +22,7 @@ import { log } from '../lib/logger'; import { ContainerConfigLoadError, ContainerConfigLoadResult, - loadWorkspaceContainerConfig, + loadTaskContainerConfig, } from './containerConfigService'; const RUN_EVENT_CHANNEL = 'runner-event'; @@ -54,8 +54,8 @@ export interface ContainerStartError { } export interface ContainerStartOptions { - workspaceId: string; - workspacePath: string; + taskId: string; + taskPath: string; runId?: string; mode?: RunnerMode; now?: () => number; @@ -93,32 +93,32 @@ export class ContainerRunnerService extends EventEmitter { return this; } - private findComposeFile(workspacePath: string): string | null { + private findComposeFile(taskPath: string): string | null { const candidates = ['docker-compose.yml', 'docker-compose.yaml', 'compose.yml', 'compose.yaml']; for (const rel of candidates) { - const abs = path.join(workspacePath, rel); + const abs = path.join(taskPath, rel); if (fs.existsSync(abs)) return abs; } return null; } private async startComposeRun(args: { - workspaceId: string; - workspacePath: string; + taskId: string; + taskPath: string; runId: string; mode: RunnerMode; config: ResolvedContainerConfig; now: () => number; composeFile: string; }): Promise { - const { workspaceId, workspacePath, runId, mode, config, now, composeFile } = args; + const { taskId, taskPath, runId, mode, config, now, composeFile } = args; const execAsync = promisify(exec); - const project = `emdash_ws_${workspaceId}`; + const project = `emdash_ws_${taskId}`; const emitLifecycle = ( status: 'building' | 'starting' | 'ready' | 'stopping' | 'stopped' | 'failed' ) => { - this.emitRunnerEvent({ ts: now(), workspaceId, runId, mode, type: 'lifecycle', status }); + this.emitRunnerEvent({ ts: now(), taskId, runId, mode, type: 'lifecycle', status }); }; const emitPorts = ( ports: Array<{ service: string; container: number; host: number }>, @@ -133,7 +133,7 @@ export class ContainerRunnerService extends EventEmitter { })); this.emitRunnerEvent({ ts: now(), - workspaceId, + taskId, runId, mode, type: 'ports', @@ -150,7 +150,7 @@ export class ContainerRunnerService extends EventEmitter { const message = 'Docker Compose is not available. Please install/update Docker Desktop.'; this.emitRunnerEvent({ ts: now(), - workspaceId, + taskId, runId, mode, type: 'error', @@ -164,7 +164,7 @@ export class ContainerRunnerService extends EventEmitter { } // Always attempt autodiscovery first to avoid introducing unknown services (e.g. default 'app') - const discovered = await this.discoverComposePorts(composeFile, workspacePath); + const discovered = await this.discoverComposePorts(composeFile, taskPath); let portRequests: ResolvedContainerPortConfig[] = []; if (discovered.length > 0) { portRequests = discovered.map((d) => ({ @@ -194,12 +194,12 @@ export class ContainerRunnerService extends EventEmitter { } // Build a sanitized compose file that removes host bindings (ports) and replaces with expose - const sanitizedAbs = path.join(workspacePath, '.emdash', 'compose.sanitized.json'); + const sanitizedAbs = path.join(taskPath, '.emdash', 'compose.sanitized.json'); try { fs.mkdirSync(path.dirname(sanitizedAbs), { recursive: true }); } catch {} try { - const cfgJson = await this.loadComposeConfigJson(composeFile, workspacePath); + const cfgJson = await this.loadComposeConfigJson(composeFile, taskPath); const portMap = new Map(); for (const req of portRequests) { const arr = portMap.get(req.service) ?? []; @@ -213,7 +213,7 @@ export class ContainerRunnerService extends EventEmitter { } // Write override file mapping container ports -> random host ports - const overrideAbs = path.join(workspacePath, '.emdash', 'compose.override.yml'); + const overrideAbs = path.join(taskPath, '.emdash', 'compose.override.yml'); try { fs.mkdirSync(path.dirname(overrideAbs), { recursive: true }); } catch {} @@ -221,7 +221,7 @@ export class ContainerRunnerService extends EventEmitter { // Run compose up -d const argsArr: string[] = ['compose']; - const envFileAbs = config.envFile ? path.resolve(workspacePath, config.envFile) : null; + const envFileAbs = config.envFile ? path.resolve(taskPath, config.envFile) : null; if (envFileAbs && fs.existsSync(envFileAbs)) argsArr.push('--env-file', envFileAbs); // Prefer sanitized file when available const composePathForUp = fs.existsSync(sanitizedAbs) ? sanitizedAbs : composeFile; @@ -250,7 +250,7 @@ export class ContainerRunnerService extends EventEmitter { return { ok: true, runId, config, sourcePath: null }; } catch (error) { log.error('[containers] compose run failed', error); - const serialized = this.serializeStartError(error, { workspaceId, runId, mode, now }); + const serialized = this.serializeStartError(error, { taskId, runId, mode, now }); if (serialized.event) this.emitRunnerEvent(serialized.event); return { ok: false, error: serialized.error }; } @@ -319,11 +319,11 @@ export class ContainerRunnerService extends EventEmitter { return result.length ? result : allocated; } - private async loadComposeConfigJson(composeFile: string, workspacePath: string): Promise { + private async loadComposeConfigJson(composeFile: string, taskPath: string): Promise { const execAsync = promisify(exec); const { stdout } = await execAsync( `docker compose -f ${JSON.stringify(composeFile)} config --format json`, - { cwd: workspacePath } + { cwd: taskPath } ); try { return JSON.parse(stdout || '{}'); @@ -359,13 +359,13 @@ export class ContainerRunnerService extends EventEmitter { private async discoverComposePorts( composeFile: string, - workspacePath: string + taskPath: string ): Promise> { const execAsync = promisify(exec); try { const { stdout } = await execAsync( `docker compose -f ${JSON.stringify(composeFile)} config --format json`, - { cwd: workspacePath } + { cwd: taskPath } ); const cfg = JSON.parse(stdout || '{}'); const services = cfg?.services || cfg?.Services || {}; @@ -429,7 +429,7 @@ export class ContainerRunnerService extends EventEmitter { return this.emit(RUN_EVENT_CHANNEL, event); } - async inspectRun(workspaceId: string): Promise< + async inspectRun(taskId: string): Promise< | { ok: true; running: boolean; @@ -439,7 +439,7 @@ export class ContainerRunnerService extends EventEmitter { | { ok: false; error: string } > { const execAsync = promisify(exec); - const project = `emdash_ws_${workspaceId}`; + const project = `emdash_ws_${taskId}`; try { const { stdout } = await execAsync( `docker compose -p ${JSON.stringify(project)} ps --format json` @@ -492,24 +492,24 @@ export class ContainerRunnerService extends EventEmitter { * Emits runner events compatible with the existing renderer. */ async startRun(options: ContainerStartOptions): Promise { - const existing = this.startInFlight.get(options.workspaceId); + const existing = this.startInFlight.get(options.taskId); if (existing) return existing; const promise = this._startRunImpl(options).finally(() => { - this.startInFlight.delete(options.workspaceId); + this.startInFlight.delete(options.taskId); }); - this.startInFlight.set(options.workspaceId, promise); + this.startInFlight.set(options.taskId, promise); return promise; } private async _startRunImpl(options: ContainerStartOptions): Promise { - const { workspaceId, workspacePath } = options; - if (!workspaceId || !workspacePath) { + const { taskId, taskPath } = options; + if (!taskId || !taskPath) { return { ok: false, error: { code: 'INVALID_ARGUMENT', - message: '`workspaceId` and `workspacePath` are required', + message: '`taskId` and `taskPath` are required', configKey: null, configPath: null, }, @@ -517,7 +517,7 @@ export class ContainerRunnerService extends EventEmitter { } // Load container config - const loadResult = await this.loadConfig(workspacePath); + const loadResult = await this.loadConfig(taskPath); if (loadResult.ok === false) { return { ok: false, @@ -544,7 +544,7 @@ export class ContainerRunnerService extends EventEmitter { const emitLifecycle = ( status: 'building' | 'starting' | 'ready' | 'stopping' | 'stopped' | 'failed' ) => { - this.emitRunnerEvent({ ts: now(), workspaceId, runId, mode, type: 'lifecycle', status }); + this.emitRunnerEvent({ ts: now(), taskId, runId, mode, type: 'lifecycle', status }); }; const emitPorts = ( @@ -560,7 +560,7 @@ export class ContainerRunnerService extends EventEmitter { })); this.emitRunnerEvent({ ts: now(), - workspaceId, + taskId, runId, mode, type: 'ports', @@ -570,15 +570,15 @@ export class ContainerRunnerService extends EventEmitter { }; try { - // Host-side preflight checks to prevent unintended workspace mutations - const absWorkspace = path.resolve(workspacePath); - const workdirAbs = path.resolve(absWorkspace, config.workdir); + // Host-side preflight checks to prevent unintended task mutations + const absTaskPath = path.resolve(taskPath); + const workdirAbs = path.resolve(absTaskPath, config.workdir); if (!fs.existsSync(workdirAbs)) { const message = `Configured workdir does not exist: ${workdirAbs}`; const event = { ts: now(), - workspaceId, + taskId, runId, mode, type: 'error' as const, @@ -602,7 +602,7 @@ export class ContainerRunnerService extends EventEmitter { const message = `No package.json found in workdir: ${workdirAbs}. Set the correct 'workdir' in .emdash/config.json`; this.emitRunnerEvent({ ts: now(), - workspaceId, + taskId, runId, mode, type: 'error', @@ -631,7 +631,7 @@ export class ContainerRunnerService extends EventEmitter { const message = 'Docker is not available or not responding. Please start Docker Desktop.'; const event = { ts: now(), - workspaceId, + taskId, runId, mode, type: 'error' as const, @@ -645,13 +645,13 @@ export class ContainerRunnerService extends EventEmitter { }; } - // Prefer compose runner when a compose file exists at the workspace root - const composeBase = this.findComposeFile(absWorkspace); + // Prefer compose runner when a compose file exists at the task root + const composeBase = this.findComposeFile(absTaskPath); if (composeBase) { log.info('[containers] compose detected; delegating to compose runner'); return await this.startComposeRun({ - workspaceId, - workspacePath: absWorkspace, + taskId, + taskPath: absTaskPath, runId, mode, config, @@ -671,7 +671,7 @@ export class ContainerRunnerService extends EventEmitter { emitLifecycle('building'); // Ensure no leftover container with the same name - const containerName = `emdash_ws_${workspaceId}`; + const containerName = `emdash_ws_${taskId}`; try { await execAsync(`docker rm -f ${JSON.stringify(containerName)}`); } catch {} @@ -685,8 +685,8 @@ export class ContainerRunnerService extends EventEmitter { dockerArgs.push('-p', `${m.host}:${m.container}`); } - // Workspace mount and workdir - dockerArgs.push('-v', `${absWorkspace}:/workspace`); + // Task mount and workdir + dockerArgs.push('-v', `${absTaskPath}:/workspace`); const workdir = path.posix.join('/workspace', config.workdir.replace(/\\/g, '/')); dockerArgs.push('-w', workdir); @@ -698,12 +698,12 @@ export class ContainerRunnerService extends EventEmitter { // Env file (optional) if (config.envFile) { - const envAbs = path.resolve(workspacePath, config.envFile); + const envAbs = path.resolve(taskPath, config.envFile); if (!fs.existsSync(envAbs)) { const message = `Env file not found: ${envAbs}`; this.emitRunnerEvent({ ts: now(), - workspaceId, + taskId, runId, mode, type: 'error', @@ -757,7 +757,7 @@ export class ContainerRunnerService extends EventEmitter { ); this.emitRunnerEvent({ ts: now(), - workspaceId, + taskId, runId, mode, type: 'lifecycle', @@ -775,7 +775,7 @@ export class ContainerRunnerService extends EventEmitter { } catch (error) { log.error('[containers] docker run failed', error); const serialized = this.serializeStartError(error, { - workspaceId, + taskId, runId, mode, now, @@ -785,16 +785,16 @@ export class ContainerRunnerService extends EventEmitter { } } - /** Stop and remove a running container for a task (workspace) */ - async stopRun(workspaceId: string, opts: { now?: () => number; mode?: RunnerMode } = {}) { + /** Stop and remove a running container for a task */ + async stopRun(taskId: string, opts: { now?: () => number; mode?: RunnerMode } = {}) { const now = opts.now ?? Date.now; const mode = opts.mode ?? 'container'; const runId = this.generateRunId(now); - const containerName = `emdash_ws_${workspaceId}`; + const containerName = `emdash_ws_${taskId}`; try { this.emitRunnerEvent({ ts: now(), - workspaceId, + taskId, runId, mode, type: 'lifecycle', @@ -810,7 +810,7 @@ export class ContainerRunnerService extends EventEmitter { } catch {} this.emitRunnerEvent({ ts: now(), - workspaceId, + taskId, runId, mode, type: 'lifecycle', @@ -821,7 +821,7 @@ export class ContainerRunnerService extends EventEmitter { const message = e instanceof Error ? e.message : String(e); this.emitRunnerEvent({ ts: now(), - workspaceId, + taskId, runId, mode, type: 'error', @@ -833,20 +833,20 @@ export class ContainerRunnerService extends EventEmitter { } async startMockRun(options: ContainerStartOptions): Promise { - const { workspaceId, workspacePath } = options; - if (!workspaceId || !workspacePath) { + const { taskId, taskPath } = options; + if (!taskId || !taskPath) { return { ok: false, error: { code: 'INVALID_ARGUMENT', - message: '`workspaceId` and `workspacePath` are required', + message: '`taskId` and `taskPath` are required', configKey: null, configPath: null, }, }; } - const loadResult = await this.loadConfig(workspacePath); + const loadResult = await this.loadConfig(taskPath); if (loadResult.ok === false) { return { ok: false, @@ -860,7 +860,7 @@ export class ContainerRunnerService extends EventEmitter { try { const events = await generateMockStartEvents({ - workspaceId, + taskId, config: loadResult.config, portAllocator: this.portAllocator, runId, @@ -881,7 +881,7 @@ export class ContainerRunnerService extends EventEmitter { } catch (error) { log.error('container runner start failed', error); const serialized = this.serializeStartError(error, { - workspaceId, + taskId, runId, mode, now, @@ -896,8 +896,8 @@ export class ContainerRunnerService extends EventEmitter { } } - private async loadConfig(workspacePath: string): Promise { - return loadWorkspaceContainerConfig(workspacePath); + private async loadConfig(taskPath: string): Promise { + return loadTaskContainerConfig(taskPath); } private serializeConfigError(error: ContainerConfigLoadError): ContainerStartError { @@ -912,7 +912,7 @@ export class ContainerRunnerService extends EventEmitter { private serializeStartError( cause: unknown, context: { - workspaceId: string; + taskId: string; runId: string; mode: RunnerMode; now: () => number; @@ -921,7 +921,7 @@ export class ContainerRunnerService extends EventEmitter { if (cause instanceof PortAllocationError) { const event: RunnerErrorEvent = { ts: context.now(), - workspaceId: context.workspaceId, + taskId: context.taskId, runId: context.runId, mode: context.mode, type: 'error', @@ -960,7 +960,7 @@ export class ContainerRunnerService extends EventEmitter { }, event: { ts: context.now(), - workspaceId: context.workspaceId, + taskId: context.taskId, runId: context.runId, mode: context.mode, type: 'error', diff --git a/src/main/services/fsIpc.ts b/src/main/services/fsIpc.ts index a3b8c8fd..44936bb9 100644 --- a/src/main/services/fsIpc.ts +++ b/src/main/services/fsIpc.ts @@ -155,14 +155,14 @@ export function registerFsIpc(): void { } ); - // Save an attachment (e.g., image) into a workspace-managed folder + // Save an attachment (e.g., image) into a task-managed folder ipcMain.handle( 'fs:save-attachment', - async (_event, args: { workspacePath: string; srcPath: string; subdir?: string }) => { + async (_event, args: { taskPath: string; srcPath: string; subdir?: string }) => { try { - const { workspacePath, srcPath } = args; - if (!workspacePath || !fs.existsSync(workspacePath)) - return { success: false, error: 'Invalid workspacePath' }; + const { taskPath, srcPath } = args; + if (!taskPath || !fs.existsSync(taskPath)) + return { success: false, error: 'Invalid taskPath' }; if (!srcPath || !fs.existsSync(srcPath)) return { success: false, error: 'Invalid srcPath' }; @@ -171,11 +171,7 @@ export function registerFsIpc(): void { return { success: false, error: 'Unsupported attachment type' }; } - const baseDir = path.join( - workspacePath, - '.emdash', - args.subdir || DEFAULT_ATTACHMENTS_SUBDIR - ); + const baseDir = path.join(taskPath, '.emdash', args.subdir || DEFAULT_ATTACHMENTS_SUBDIR); fs.mkdirSync(baseDir, { recursive: true }); const baseName = path.basename(srcPath); @@ -191,11 +187,11 @@ export function registerFsIpc(): void { fs.copyFileSync(srcPath, destAbs); - const relFromWorkspace = path.relative(workspacePath, destAbs); + const relFromTask = path.relative(taskPath, destAbs); return { success: true, absPath: destAbs, - relPath: relFromWorkspace, + relPath: relFromTask, fileName: destName, }; } catch (error) { diff --git a/src/main/services/hostPreviewService.ts b/src/main/services/hostPreviewService.ts index 837a2ca2..f59227ee 100644 --- a/src/main/services/hostPreviewService.ts +++ b/src/main/services/hostPreviewService.ts @@ -7,7 +7,7 @@ import { log } from '../lib/logger'; export type HostPreviewEvent = { type: 'url' | 'setup' | 'exit'; - workspaceId: string; + taskId: string; url?: string; status?: 'starting' | 'line' | 'done' | 'error'; line?: string; @@ -38,13 +38,10 @@ function normalizeUrl(u: string): string { class HostPreviewService extends EventEmitter { private procs = new Map(); - private procCwds = new Map(); // Track cwd for each workspaceId + private procCwds = new Map(); // Track cwd for each taskId - async setup( - workspaceId: string, - workspacePath: string - ): Promise<{ ok: boolean; error?: string }> { - const cwd = path.resolve(workspacePath); + async setup(taskId: string, taskPath: string): Promise<{ ok: boolean; error?: string }> { + const cwd = path.resolve(taskPath); const pm = detectPackageManager(cwd); const cmd = pm; // Prefer clean install for npm when lockfile exists @@ -56,12 +53,12 @@ class HostPreviewService extends EventEmitter { shell: true, env: { ...process.env, BROWSER: 'none' }, }); - this.emit('event', { type: 'setup', workspaceId, status: 'starting' } as HostPreviewEvent); + this.emit('event', { type: 'setup', taskId, status: 'starting' } as HostPreviewEvent); const onData = (buf: Buffer) => { const line = buf.toString(); this.emit('event', { type: 'setup', - workspaceId, + taskId, status: 'line', line, } as HostPreviewEvent); @@ -75,12 +72,12 @@ class HostPreviewService extends EventEmitter { }); child.on('error', reject); }); - this.emit('event', { type: 'setup', workspaceId, status: 'done' } as HostPreviewEvent); + this.emit('event', { type: 'setup', taskId, status: 'done' } as HostPreviewEvent); return { ok: true }; } catch (e: any) { this.emit('event', { type: 'setup', - workspaceId, + taskId, status: 'error', line: e?.message || String(e), } as HostPreviewEvent); @@ -121,24 +118,24 @@ class HostPreviewService extends EventEmitter { } async start( - workspaceId: string, - workspacePath: string, + taskId: string, + taskPath: string, opts?: { script?: string; parentProjectPath?: string } ): Promise<{ ok: boolean; error?: string }> { - const cwd = path.resolve(workspacePath); + const cwd = path.resolve(taskPath); // Log the resolved path to help debug worktree issues log.info?.('[hostPreview] start', { - workspaceId, - workspacePath, + taskId, + taskPath, resolvedCwd: cwd, cwdExists: fs.existsSync(cwd), hasPackageJson: fs.existsSync(path.join(cwd, 'package.json')), }); - // Check if process already exists for this workspaceId - const existingProc = this.procs.get(workspaceId); - const existingCwd = this.procCwds.get(workspaceId); + // Check if process already exists for this taskId + const existingProc = this.procs.get(taskId); + const existingCwd = this.procCwds.get(taskId); // If process exists, verify it's running from the correct directory if (existingProc) { @@ -149,32 +146,32 @@ class HostPreviewService extends EventEmitter { // Process is still running - check if cwd matches if (existingCwd && path.resolve(existingCwd) === cwd) { log.info?.('[hostPreview] reusing existing process', { - workspaceId, + taskId, cwd: existingCwd, }); return { ok: true }; } else { // Process exists but is running from wrong directory - stop it log.info?.('[hostPreview] stopping process with wrong cwd', { - workspaceId, + taskId, oldCwd: existingCwd, newCwd: cwd, }); try { existingProc.kill(); } catch {} - this.procs.delete(workspaceId); - this.procCwds.delete(workspaceId); + this.procs.delete(taskId); + this.procCwds.delete(taskId); } } catch { // Process has exited - clean up - this.procs.delete(workspaceId); - this.procCwds.delete(workspaceId); + this.procs.delete(taskId); + this.procCwds.delete(taskId); } } const pm = detectPackageManager(cwd); - // Preflight: if the workspace lacks node_modules but the parent has it, try linking + // Preflight: if the task lacks node_modules but the parent has it, try linking try { const parent = (opts?.parentProjectPath || '').trim(); if (parent) { @@ -187,7 +184,7 @@ class HostPreviewService extends EventEmitter { const linkType = process.platform === 'win32' ? 'junction' : 'dir'; fs.symlinkSync(parentNm, wsNm, linkType as any); log.info?.('[hostPreview] linked node_modules', { - workspaceId, + taskId, wsNm, parentNm, linkType, @@ -235,12 +232,12 @@ class HostPreviewService extends EventEmitter { shell: true, env: { ...process.env, BROWSER: 'none' }, }); - this.emit('event', { type: 'setup', workspaceId, status: 'starting' } as HostPreviewEvent); + this.emit('event', { type: 'setup', taskId, status: 'starting' } as HostPreviewEvent); const onData = (buf: Buffer) => { try { this.emit('event', { type: 'setup', - workspaceId, + taskId, status: 'line', line: buf.toString(), } as HostPreviewEvent); @@ -254,7 +251,7 @@ class HostPreviewService extends EventEmitter { }); inst.on('error', reject); }); - this.emit('event', { type: 'setup', workspaceId, status: 'done' } as HostPreviewEvent); + this.emit('event', { type: 'setup', taskId, status: 'done' } as HostPreviewEvent); } } catch {} @@ -290,7 +287,7 @@ class HostPreviewService extends EventEmitter { else args.push(...extra); } log.info?.('[hostPreview] start', { - workspaceId, + taskId, cwd, pm, cmd, @@ -300,7 +297,7 @@ class HostPreviewService extends EventEmitter { }); } catch { log.info?.('[hostPreview] start', { - workspaceId, + taskId, cwd, pm, cmd, @@ -313,8 +310,8 @@ class HostPreviewService extends EventEmitter { const tryStart = async (maxRetries = 3): Promise<{ ok: boolean; error?: string }> => { try { const child = spawn(cmd, args, { cwd, env, shell: true }); - this.procs.set(workspaceId, child); - this.procCwds.set(workspaceId, cwd); // Store the cwd for this process + this.procs.set(taskId, child); + this.procCwds.set(taskId, cwd); // Store the cwd for this process let urlEmitted = false; let sawAddrInUse = false; @@ -325,7 +322,7 @@ class HostPreviewService extends EventEmitter { try { this.emit('event', { type: 'setup', - workspaceId, + taskId, status: 'line', line, } as HostPreviewEvent); @@ -351,7 +348,7 @@ class HostPreviewService extends EventEmitter { try { this.emit('event', { type: 'url', - workspaceId, + taskId, url: urlToProbe, } as HostPreviewEvent); } catch {} @@ -401,7 +398,7 @@ class HostPreviewService extends EventEmitter { try { this.emit('event', { type: 'url', - workspaceId, + taskId, url: `http://localhost:${Number(env.PORT) || forcedPort}`, } as HostPreviewEvent); } catch {} @@ -417,8 +414,8 @@ class HostPreviewService extends EventEmitter { child.on('exit', async () => { clearInterval(probeInterval); - this.procs.delete(workspaceId); - this.procCwds.delete(workspaceId); // Clean up cwd tracking + this.procs.delete(taskId); + this.procCwds.delete(taskId); // Clean up cwd tracking const runtimeMs = Date.now() - startedAt; const quickFail = runtimeMs < 4000; // exited very quickly if (!urlEmitted && (sawAddrInUse || quickFail) && maxRetries > 0) { @@ -437,7 +434,7 @@ class HostPreviewService extends EventEmitter { else if (pm === 'npm') args.push('--', '-p', String(forcedPort)); else args.push('-p', String(forcedPort)); log.info?.('[hostPreview] retry on new port', { - workspaceId, + taskId, port: forcedPort, retriesLeft: maxRetries - 1, }); @@ -445,7 +442,7 @@ class HostPreviewService extends EventEmitter { return; } try { - this.emit('event', { type: 'exit', workspaceId } as HostPreviewEvent); + this.emit('event', { type: 'exit', taskId } as HostPreviewEvent); } catch {} }); return { ok: true }; @@ -458,14 +455,14 @@ class HostPreviewService extends EventEmitter { return await tryStart(3); } - stop(workspaceId: string): { ok: boolean } { - const p = this.procs.get(workspaceId); + stop(taskId: string): { ok: boolean } { + const p = this.procs.get(taskId); if (!p) return { ok: true }; try { p.kill(); } catch {} - this.procs.delete(workspaceId); - this.procCwds.delete(workspaceId); // Clean up cwd tracking + this.procs.delete(taskId); + this.procCwds.delete(taskId); // Clean up cwd tracking return { ok: true }; } diff --git a/src/main/services/iconService.ts b/src/main/services/iconService.ts index df22e390..1c0ebba0 100644 --- a/src/main/services/iconService.ts +++ b/src/main/services/iconService.ts @@ -125,16 +125,16 @@ async function fetchHttps( export async function resolveServiceIcon(opts: { service: string; - workspacePath?: string; + taskPath?: string; allowNetwork?: boolean; }): Promise<{ ok: true; dataUrl: string } | { ok: false }> { const service = opts.service?.trim(); if (!service) return { ok: false }; const slug = toSlug(service); - // 1) Workspace overrides - if (opts.workspacePath) { - const p = path.join(opts.workspacePath, '.emdash', 'service-icons'); + // 1) Task overrides + if (opts.taskPath) { + const p = path.join(opts.taskPath, '.emdash', 'service-icons'); const candidates = ['.svg', '.png', '.jpg', '.jpeg', '.ico'].map((ext) => path.join(p, `${slug}${ext}`) ); diff --git a/src/main/services/planLockIpc.ts b/src/main/services/planLockIpc.ts index 442b0327..55c2d6b1 100644 --- a/src/main/services/planLockIpc.ts +++ b/src/main/services/planLockIpc.ts @@ -130,18 +130,18 @@ function releaseLock(root: string): { success: boolean; restored: number; error? } export function registerPlanLockIpc(): void { - ipcMain.handle('plan:lock', async (_e, workspacePath: string) => { + ipcMain.handle('plan:lock', async (_e, taskPath: string) => { if (isWindows()) { // Best-effort: still attempt chmod; ACL hardening could be added with icacls in a future pass - return applyLock(workspacePath); + return applyLock(taskPath); } - return applyLock(workspacePath); + return applyLock(taskPath); }); - ipcMain.handle('plan:unlock', async (_e, workspacePath: string) => { + ipcMain.handle('plan:unlock', async (_e, taskPath: string) => { if (isWindows()) { - return releaseLock(workspacePath); + return releaseLock(taskPath); } - return releaseLock(workspacePath); + return releaseLock(taskPath); }); } diff --git a/src/main/services/ptyIpc.ts b/src/main/services/ptyIpc.ts index 7df5a9fa..5cec87af 100644 --- a/src/main/services/ptyIpc.ts +++ b/src/main/services/ptyIpc.ts @@ -198,25 +198,25 @@ export function registerPtyIpc(): void { function parseProviderPty(id: string): { providerId: ProviderId; - workspaceId: string; + taskId: string; } | null { - // Chat terminals are named `${provider}-main-${workspaceId}` + // Chat terminals are named `${provider}-main-${taskId}` const match = /^([a-z0-9_-]+)-main-(.+)$/.exec(id); if (!match) return null; const providerId = match[1] as ProviderId; if (!PROVIDER_IDS.includes(providerId)) return null; - const workspaceId = match[2]; - return { providerId, workspaceId }; + const taskId = match[2]; + return { providerId, taskId }; } -function providerRunKey(providerId: ProviderId, workspaceId: string) { - return `${providerId}:${workspaceId}`; +function providerRunKey(providerId: ProviderId, taskId: string) { + return `${providerId}:${taskId}`; } function maybeMarkProviderStart(id: string) { const parsed = parseProviderPty(id); if (!parsed) return; - const key = providerRunKey(parsed.providerId, parsed.workspaceId); + const key = providerRunKey(parsed.providerId, parsed.taskId); if (providerPtyTimers.has(key)) return; providerPtyTimers.set(key, Date.now()); telemetry.capture('agent_run_start', { provider: parsed.providerId }); @@ -229,7 +229,7 @@ function maybeMarkProviderFinish( ) { const parsed = parseProviderPty(id); if (!parsed) return; - const key = providerRunKey(parsed.providerId, parsed.workspaceId); + const key = providerRunKey(parsed.providerId, parsed.taskId); const started = providerPtyTimers.get(key); providerPtyTimers.delete(key); diff --git a/src/main/services/worktreeIpc.ts b/src/main/services/worktreeIpc.ts index 83f0002e..237b96e0 100644 --- a/src/main/services/worktreeIpc.ts +++ b/src/main/services/worktreeIpc.ts @@ -9,7 +9,7 @@ export function registerWorktreeIpc(): void { event, args: { projectPath: string; - workspaceName: string; + taskName: string; projectId: string; autoApprove?: boolean; } @@ -17,7 +17,7 @@ export function registerWorktreeIpc(): void { try { const worktree = await worktreeService.createWorktree( args.projectPath, - args.workspaceName, + args.taskName, args.projectId, args.autoApprove ); diff --git a/src/main/telemetry.ts b/src/main/telemetry.ts index 423ae272..dc077621 100644 --- a/src/main/telemetry.ts +++ b/src/main/telemetry.ts @@ -38,12 +38,12 @@ type TelemetryEvent = | 'project_added_success' // when a project is added successfully (both entrypoint buttons) | 'project_deleted' | 'project_view_opened' // when a user opens a project and see the Task overview in main screen (not the sidebar) - // Workspace management - | 'workspace_created' // when a new workspace (task) is created (track) (with all attributes, if initial prompt is used (but dont store the initial prompt itself)) - | 'workspace_deleted' // when a workspace (task) is deleted - | 'workspace_provider_switched' // when a workspace (task) is switched to a different provider - | 'workspace_custom_named' // when a Task (workspace) is given a custom name instead of the default generated one - | 'workspace_advanced_options_opened' // when a workspace (task) advanced options are opened + // Task management + | 'task_created' // when a new task is created (track) (with all attributes, if initial prompt is used (but dont store the initial prompt itself)) + | 'task_deleted' // when a task is deleted + | 'task_provider_switched' // when a task is switched to a different provider + | 'task_custom_named' // when a task is given a custom name instead of the default generated one + | 'task_advanced_options_opened' // when task advanced options are opened // Terminal (Right Sidebar) | 'terminal_entered' //when a user enters the terminal (right sidebar) with his mouse | 'terminal_command_executed' //when a user executes a command in the terminal @@ -61,8 +61,8 @@ type TelemetryEvent = // Linear integration | 'linear_connected' | 'linear_disconnected' - | 'linear_issues_searched' // when creating a new task (workspace) and the Linear issue search is opened - | 'linear_issue_selected' // when a user selects a Linear issue to create a new task (workspace) (no need to send task, just selecting issue) + | 'linear_issues_searched' // when creating a new task and the Linear issue search is opened + | 'linear_issue_selected' // when a user selects a Linear issue to create a new task (no need to send task, just selecting issue) // Jira integration | 'jira_connected' | 'jira_disconnected' @@ -93,7 +93,7 @@ type TelemetryEvent = | 'feature_used' | 'error' // Aggregates (privacy-safe) - | 'workspace_snapshot' + | 'task_snapshot' // Session summary (duration only) | 'app_session' // Agent usage (provider-level only) @@ -219,8 +219,8 @@ function sanitizeEventAndProps(event: TelemetryEvent, props: Record 'duration_ms', 'session_duration_ms', 'outcome', - 'workspace_count', - 'workspace_count_bucket', + 'task_count', + 'task_count_bucket', 'project_count', 'project_count_bucket', ]); diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index eb6a1031..98a10b29 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -3,9 +3,9 @@ import { Button } from './components/ui/button'; import { FolderOpen } from 'lucide-react'; import LeftSidebar from './components/LeftSidebar'; import ProjectMainView from './components/ProjectMainView'; -import WorkspaceModal from './components/WorkspaceModal'; +import TaskModal from './components/TaskModal'; import ChatInterface from './components/ChatInterface'; -import MultiAgentWorkspace from './components/MultiAgentWorkspace'; +import MultiAgentTask from './components/MultiAgentTask'; import { Toaster } from './components/ui/toaster'; import useUpdateNotifier from './hooks/useUpdateNotifier'; import RequirementsNotice from './components/RequirementsNotice'; @@ -30,8 +30,8 @@ import type { ImperativePanelHandle } from 'react-resizable-panels'; import SettingsModal from './components/SettingsModal'; import CommandPaletteWrapper from './components/CommandPaletteWrapper'; import FirstLaunchModal from './components/FirstLaunchModal'; -import type { Project, Workspace } from './types/app'; -import type { WorkspaceMetadata as ChatWorkspaceMetadata } from './types/chat'; +import type { Project, Task } from './types/app'; +import type { TaskMetadata } from './types/chat'; import AppKeyboardShortcuts from './components/AppKeyboardShortcuts'; import { usePlanToasts } from './hooks/usePlanToasts'; import { terminalSessionRegistry } from './terminal/SessionRegistry'; @@ -78,9 +78,6 @@ const RightSidebarBridge: React.FC<{ return null; }; -// Shared types -type WorkspaceMetadata = ChatWorkspaceMetadata; - const TITLEBAR_HEIGHT = '36px'; const PANEL_LAYOUT_STORAGE_KEY = 'emdash.layout.left-main-right.v2'; const DEFAULT_PANEL_LAYOUT: [number, number, number] = [20, 60, 20]; @@ -121,11 +118,11 @@ const AppContent: React.FC = () => { const [showDeviceFlowModal, setShowDeviceFlowModal] = useState(false); const [projects, setProjects] = useState([]); const [selectedProject, setSelectedProject] = useState(null); - const [showWorkspaceModal, setShowWorkspaceModal] = useState(false); + const [showTaskModal, setShowTaskModal] = useState(false); const [showHomeView, setShowHomeView] = useState(true); - const [isCreatingWorkspace, setIsCreatingWorkspace] = useState(false); - const [activeWorkspace, setActiveWorkspace] = useState(null); - const [activeWorkspaceProvider, setActiveWorkspaceProvider] = useState(null); + const [isCreatingTask, setIsCreatingTask] = useState(false); + const [activeTask, setActiveTask] = useState(null); + const [activeTaskProvider, setActiveTaskProvider] = useState(null); const [installedProviders, setInstalledProviders] = useState>({}); const [showSettings, setShowSettings] = useState(false); const [showCommandPalette, setShowCommandPalette] = useState(false); @@ -135,7 +132,7 @@ const AppContent: React.FC = () => { const showAgentRequirement = Object.keys(installedProviders).length > 0 && Object.values(installedProviders).every((v) => v === false); - const deletingWorkspaceIdsRef = useRef>(new Set()); + const deletingTaskIdsRef = useRef>(new Set()); const normalizePathForComparison = useCallback( (input: string | null | undefined) => { @@ -325,7 +322,7 @@ const AppContent: React.FC = () => { })(); setSelectedProject(project); setShowHomeView(false); - setActiveWorkspace(null); + setActiveTask(null); }, []); const handleRightSidebarCollapsedChange = useCallback((collapsed: boolean) => { @@ -459,13 +456,13 @@ const AppContent: React.FC = () => { // Refresh GH status via hook checkStatus(); - const projectsWithWorkspaces = await Promise.all( + const projectsWithTasks = await Promise.all( initialProjects.map(async (project) => { - const workspaces = await window.electronAPI.getWorkspaces(project.id); - return withRepoKey({ ...project, workspaces }); + const tasks = await window.electronAPI.getTasks(project.id); + return withRepoKey({ ...project, tasks }); }) ); - const ordered = applyProjectOrder(projectsWithWorkspaces); + const ordered = applyProjectOrder(projectsWithTasks); setProjects(ordered); // Prefer cached provider status (populated in the background) @@ -549,7 +546,7 @@ const AppContent: React.FC = () => { branch: gitInfo.branch || undefined, baseRef: computeBaseRef(gitInfo.baseRef, gitInfo.remote, gitInfo.branch), }, - workspaces: [], + tasks: [], }; if (isAuthenticated && isGithubRemote) { @@ -704,8 +701,8 @@ const AppContent: React.FC = () => { } }; - const handleCreateWorkspace = async ( - workspaceName: string, + const handleCreateTask = async ( + taskName: string, initialPrompt?: string, providerRuns: import('./types/chat').ProviderRun[] = [{ provider: 'claude', runs: 1 }], linkedLinearIssue: LinearIssueSummary | null = null, @@ -715,7 +712,7 @@ const AppContent: React.FC = () => { ) => { if (!selectedProject) return; - setIsCreatingWorkspace(true); + setIsCreatingTask(true); try { let preparedPrompt: string | undefined = undefined; if (initialPrompt && initialPrompt.trim()) { @@ -811,7 +808,7 @@ const AppContent: React.FC = () => { preparedPrompt = parts.join('\n'); } - const workspaceMetadata: WorkspaceMetadata | null = + const taskMetadata: TaskMetadata | null = linkedLinearIssue || linkedJiraIssue || linkedGithubIssue || preparedPrompt || autoApprove ? { linearIssue: linkedLinearIssue ?? null, @@ -827,9 +824,9 @@ const AppContent: React.FC = () => { const isMultiAgent = totalRuns > 1; const primaryProvider = providerRuns[0]?.provider || 'claude'; - let newWorkspace: Workspace; + let newTask: Task; if (isMultiAgent) { - // Multi-agent workspace: create worktrees for each provider×runs combo + // Multi-agent task: create worktrees for each provider×runs combo const variants: Array<{ id: string; provider: Provider; @@ -842,10 +839,10 @@ const AppContent: React.FC = () => { for (const { provider, runs } of providerRuns) { for (let instanceIdx = 1; instanceIdx <= runs; instanceIdx++) { const instanceSuffix = runs > 1 ? `-${instanceIdx}` : ''; - const variantName = `${workspaceName}-${provider.toLowerCase()}${instanceSuffix}`; + const variantName = `${taskName}-${provider.toLowerCase()}${instanceSuffix}`; const worktreeResult = await window.electronAPI.worktreeCreate({ projectPath: selectedProject.path, - workspaceName: variantName, + taskName: variantName, projectId: selectedProject.id, autoApprove, }); @@ -857,7 +854,7 @@ const AppContent: React.FC = () => { } const worktree = worktreeResult.worktree; variants.push({ - id: `${workspaceName}-${provider.toLowerCase()}${instanceSuffix}`, + id: `${taskName}-${provider.toLowerCase()}${instanceSuffix}`, provider: provider, name: variantName, branch: worktree.branch, @@ -867,8 +864,8 @@ const AppContent: React.FC = () => { } } - const multiMeta: WorkspaceMetadata = { - ...(workspaceMetadata || {}), + const multiMeta: TaskMetadata = { + ...(taskMetadata || {}), multiAgent: { enabled: true, maxProviders: 4, @@ -878,11 +875,11 @@ const AppContent: React.FC = () => { }, }; - const groupId = `ws-${workspaceName}-${Date.now()}`; - newWorkspace = { + const groupId = `ws-${taskName}-${Date.now()}`; + newTask = { id: groupId, projectId: selectedProject.id, - name: workspaceName, + name: taskName, branch: variants[0]?.branch || selectedProject.gitInfo.branch || 'main', path: variants[0]?.path || selectedProject.path, status: 'idle', @@ -890,23 +887,23 @@ const AppContent: React.FC = () => { metadata: multiMeta, }; - const saveResult = await window.electronAPI.saveWorkspace({ - ...newWorkspace, + const saveResult = await window.electronAPI.saveTask({ + ...newTask, agentId: primaryProvider, metadata: multiMeta, }); if (!saveResult?.success) { const { log } = await import('./lib/logger'); - log.error('Failed to save multi-agent workspace:', saveResult?.error); - toast({ title: 'Error', description: 'Failed to create multi-agent workspace.' }); - setIsCreatingWorkspace(false); + log.error('Failed to save multi-agent task:', saveResult?.error); + toast({ title: 'Error', description: 'Failed to create multi-agent task.' }); + setIsCreatingTask(false); return; } } else { // Create worktree const worktreeResult = await window.electronAPI.worktreeCreate({ projectPath: selectedProject.path, - workspaceName, + taskName, projectId: selectedProject.id, autoApprove, }); @@ -917,40 +914,38 @@ const AppContent: React.FC = () => { const worktree = worktreeResult.worktree; - newWorkspace = { + newTask = { id: worktree.id, projectId: selectedProject.id, - name: workspaceName, + name: taskName, branch: worktree.branch, path: worktree.path, status: 'idle', agentId: primaryProvider, - metadata: workspaceMetadata, + metadata: taskMetadata, }; - const saveResult = await window.electronAPI.saveWorkspace({ - ...newWorkspace, + const saveResult = await window.electronAPI.saveTask({ + ...newTask, agentId: primaryProvider, - metadata: workspaceMetadata, + metadata: taskMetadata, }); if (!saveResult?.success) { const { log } = await import('./lib/logger'); - log.error('Failed to save workspace:', saveResult?.error); - toast({ title: 'Error', description: 'Failed to create workspace.' }); - setIsCreatingWorkspace(false); + log.error('Failed to save task:', saveResult?.error); + toast({ title: 'Error', description: 'Failed to create task.' }); + setIsCreatingTask(false); return; } } { - if (workspaceMetadata?.linearIssue) { + if (taskMetadata?.linearIssue) { try { - const convoResult = await window.electronAPI.getOrCreateDefaultConversation( - newWorkspace.id - ); + const convoResult = await window.electronAPI.getOrCreateDefaultConversation(newTask.id); if (convoResult?.success && convoResult.conversation?.id) { - const issue = workspaceMetadata.linearIssue; + const issue = taskMetadata.linearIssue; const detailParts: string[] = []; const stateName = issue.state?.name?.trim(); const assigneeName = @@ -980,7 +975,7 @@ const AppContent: React.FC = () => { } await window.electronAPI.saveMessage({ - id: `linear-context-${newWorkspace.id}`, + id: `linear-context-${newTask.id}`, conversationId: convoResult.conversation.id, content: lines.join('\n'), sender: 'agent', @@ -992,17 +987,15 @@ const AppContent: React.FC = () => { } } catch (seedError) { const { log } = await import('./lib/logger'); - log.error('Failed to seed workspace with Linear issue context:', seedError as any); + log.error('Failed to seed task with Linear issue context:', seedError as any); } } - if (workspaceMetadata?.githubIssue) { + if (taskMetadata?.githubIssue) { try { - const convoResult = await window.electronAPI.getOrCreateDefaultConversation( - newWorkspace.id - ); + const convoResult = await window.electronAPI.getOrCreateDefaultConversation(newTask.id); if (convoResult?.success && convoResult.conversation?.id) { - const issue = workspaceMetadata.githubIssue; + const issue = taskMetadata.githubIssue; const detailParts: string[] = []; const stateName = issue.state?.toString()?.trim(); const assignees = Array.isArray(issue.assignees) @@ -1038,7 +1031,7 @@ const AppContent: React.FC = () => { } await window.electronAPI.saveMessage({ - id: `github-context-${newWorkspace.id}`, + id: `github-context-${newTask.id}`, conversationId: convoResult.conversation.id, content: lines.join('\n'), sender: 'agent', @@ -1050,17 +1043,15 @@ const AppContent: React.FC = () => { } } catch (seedError) { const { log } = await import('./lib/logger'); - log.error('Failed to seed workspace with GitHub issue context:', seedError as any); + log.error('Failed to seed task with GitHub issue context:', seedError as any); } } - if (workspaceMetadata?.jiraIssue) { + if (taskMetadata?.jiraIssue) { try { - const convoResult = await window.electronAPI.getOrCreateDefaultConversation( - newWorkspace.id - ); + const convoResult = await window.electronAPI.getOrCreateDefaultConversation(newTask.id); if (convoResult?.success && convoResult.conversation?.id) { - const issue: any = workspaceMetadata.jiraIssue; + const issue: any = taskMetadata.jiraIssue; const lines: string[] = []; const line1 = `Linked Jira issue: ${issue.key || ''}${issue.summary ? ` — ${issue.summary}` : ''}`.trim(); @@ -1075,7 +1066,7 @@ const AppContent: React.FC = () => { if (issue.url) lines.push(`URL: ${issue.url}`); await window.electronAPI.saveMessage({ - id: `jira-context-${newWorkspace.id}`, + id: `jira-context-${newTask.id}`, conversationId: convoResult.conversation.id, content: lines.join('\n'), sender: 'agent', @@ -1087,7 +1078,7 @@ const AppContent: React.FC = () => { } } catch (seedError) { const { log } = await import('./lib/logger'); - log.error('Failed to seed workspace with Jira issue context:', seedError as any); + log.error('Failed to seed task with Jira issue context:', seedError as any); } } @@ -1096,7 +1087,7 @@ const AppContent: React.FC = () => { project.id === selectedProject.id ? { ...project, - workspaces: [newWorkspace, ...(project.workspaces || [])], + tasks: [newTask, ...(project.tasks || [])], } : project ) @@ -1106,116 +1097,109 @@ const AppContent: React.FC = () => { prev ? { ...prev, - workspaces: [newWorkspace, ...(prev.workspaces || [])], + tasks: [newTask, ...(prev.tasks || [])], } : null ); - // Track workspace creation + // Track task creation const { captureTelemetry } = await import('./lib/telemetryClient'); - const isMultiAgent = (newWorkspace.metadata as any)?.multiAgent?.enabled; - captureTelemetry('workspace_created', { - provider: isMultiAgent ? 'multi' : (newWorkspace.agentId as string) || 'codex', - has_initial_prompt: !!workspaceMetadata?.initialPrompt, + const isMultiAgent = (newTask.metadata as any)?.multiAgent?.enabled; + captureTelemetry('task_created', { + provider: isMultiAgent ? 'multi' : (newTask.agentId as string) || 'codex', + has_initial_prompt: !!taskMetadata?.initialPrompt, }); - // Set the active workspace and its provider (none if multi-agent) - setActiveWorkspace(newWorkspace); - if ((newWorkspace.metadata as any)?.multiAgent?.enabled) { - setActiveWorkspaceProvider(null); + // Set the active task and its provider (none if multi-agent) + setActiveTask(newTask); + if ((newTask.metadata as any)?.multiAgent?.enabled) { + setActiveTaskProvider(null); } else { - // Use the saved agentId from the workspace, which should match primaryProvider - setActiveWorkspaceProvider( - (newWorkspace.agentId as Provider) || primaryProvider || 'codex' - ); + // Use the saved agentId from the task, which should match primaryProvider + setActiveTaskProvider((newTask.agentId as Provider) || primaryProvider || 'codex'); } } } catch (error) { const { log } = await import('./lib/logger'); - log.error('Failed to create workspace:', error as any); + log.error('Failed to create task:', error as any); toast({ title: 'Error', description: (error as Error)?.message || - 'Failed to create workspace. Please check the console for details.', + 'Failed to create task. Please check the console for details.', }); } finally { - setIsCreatingWorkspace(false); + setIsCreatingTask(false); } }; const handleGoHome = () => { setSelectedProject(null); setShowHomeView(true); - setActiveWorkspace(null); + setActiveTask(null); }; const handleSelectProject = (project: Project) => { activateProjectView(project); }; - const handleSelectWorkspace = (workspace: Workspace) => { - setActiveWorkspace(workspace); - // Load provider from workspace.agentId if it exists, otherwise default to null + const handleSelectTask = (task: Task) => { + setActiveTask(task); + // Load provider from task.agentId if it exists, otherwise default to null // This ensures the selected provider persists across app restarts - if ((workspace.metadata as any)?.multiAgent?.enabled) { - setActiveWorkspaceProvider(null); + if ((task.metadata as any)?.multiAgent?.enabled) { + setActiveTaskProvider(null); } else { - // Use agentId from workspace if available, otherwise fall back to 'codex' for backwards compatibility - setActiveWorkspaceProvider((workspace.agentId as Provider) || 'codex'); + // Use agentId from task if available, otherwise fall back to 'codex' for backwards compatibility + setActiveTaskProvider((task.agentId as Provider) || 'codex'); } }; - const handleStartCreateWorkspaceFromSidebar = useCallback( + const handleStartCreateTaskFromSidebar = useCallback( (project: Project) => { const targetProject = projects.find((p) => p.id === project.id) || project; activateProjectView(targetProject); - setShowWorkspaceModal(true); + setShowTaskModal(true); }, [activateProjectView, projects] ); - const removeWorkspaceFromState = (projectId: string, workspaceId: string, wasActive: boolean) => { - const filterWorkspaces = (list?: Workspace[]) => - (list || []).filter((w) => w.id !== workspaceId); + const removeTaskFromState = (projectId: string, taskId: string, wasActive: boolean) => { + const filterTasks = (list?: Task[]) => (list || []).filter((w) => w.id !== taskId); setProjects((prev) => prev.map((project) => - project.id === projectId - ? { ...project, workspaces: filterWorkspaces(project.workspaces) } - : project + project.id === projectId ? { ...project, tasks: filterTasks(project.tasks) } : project ) ); setSelectedProject((prev) => - prev && prev.id === projectId - ? { ...prev, workspaces: filterWorkspaces(prev.workspaces) } - : prev + prev && prev.id === projectId ? { ...prev, tasks: filterTasks(prev.tasks) } : prev ); if (wasActive) { - setActiveWorkspace(null); - setActiveWorkspaceProvider(null); + setActiveTask(null); + setActiveTaskProvider(null); } }; - const handleDeleteWorkspace = async ( + const handleDeleteTask = async ( targetProject: Project, - workspace: Workspace, + task: Task, options?: { silent?: boolean } ): Promise => { - if (deletingWorkspaceIdsRef.current.has(workspace.id)) { + if (deletingTaskIdsRef.current.has(task.id)) { toast({ title: 'Deletion in progress', - description: `"${workspace.name}" is already being removed.`, + description: `"${task.name}" is already being removed.`, }); return false; } - const wasActive = activeWorkspace?.id === workspace.id; - const workspaceSnapshot = { ...workspace }; - deletingWorkspaceIdsRef.current.add(workspace.id); - removeWorkspaceFromState(targetProject.id, workspace.id, wasActive); + const wasActive = activeTask?.id === task.id; + const taskSnapshot = { ...task }; + deletingTaskIdsRef.current.add(task.id); + removeTaskFromState(targetProject.id, task.id, wasActive); const runDeletion = async (): Promise => { try { @@ -1224,30 +1208,30 @@ const AppContent: React.FC = () => { const { initialPromptSentKey } = await import('./lib/keys'); try { // Legacy key (no provider) - const legacy = initialPromptSentKey(workspace.id); + const legacy = initialPromptSentKey(task.id); localStorage.removeItem(legacy); } catch {} try { // Provider-scoped keys for (const p of TERMINAL_PROVIDER_IDS) { - const k = initialPromptSentKey(workspace.id, p); + const k = initialPromptSentKey(task.id, p); localStorage.removeItem(k); } } catch {} } catch {} try { - window.electronAPI.ptyKill?.(`workspace-${workspace.id}`); + window.electronAPI.ptyKill?.(`task-${task.id}`); } catch {} try { for (const provider of TERMINAL_PROVIDER_IDS) { try { - window.electronAPI.ptyKill?.(`${provider}-main-${workspace.id}`); + window.electronAPI.ptyKill?.(`${provider}-main-${task.id}`); } catch {} } } catch {} const sessionIds = [ - `workspace-${workspace.id}`, - ...TERMINAL_PROVIDER_IDS.map((provider) => `${provider}-main-${workspace.id}`), + `task-${task.id}`, + ...TERMINAL_PROVIDER_IDS.map((provider) => `${provider}-main-${task.id}`), ]; await Promise.allSettled( @@ -1264,11 +1248,11 @@ const AppContent: React.FC = () => { const [removeResult, deleteResult] = await Promise.allSettled([ window.electronAPI.worktreeRemove({ projectPath: targetProject.path, - worktreeId: workspace.id, - worktreePath: workspace.path, - branch: workspace.branch, + worktreeId: task.id, + worktreePath: task.path, + branch: task.branch, }), - window.electronAPI.deleteWorkspace(workspace.id), + window.electronAPI.deleteTask(task.id), ]); if (removeResult.status !== 'fulfilled' || !removeResult.value?.success) { @@ -1282,84 +1266,76 @@ const AppContent: React.FC = () => { if (deleteResult.status !== 'fulfilled' || !deleteResult.value?.success) { const errorMsg = deleteResult.status === 'fulfilled' - ? deleteResult.value?.error || 'Failed to delete workspace' + ? deleteResult.value?.error || 'Failed to delete task' : deleteResult.reason?.message || String(deleteResult.reason); throw new Error(errorMsg); } - // Track workspace deletion + // Track task deletion const { captureTelemetry } = await import('./lib/telemetryClient'); - captureTelemetry('workspace_deleted'); + captureTelemetry('task_deleted'); if (!options?.silent) { toast({ title: 'Task deleted', - description: workspace.name, + description: task.name, }); } return true; } catch (error) { const { log } = await import('./lib/logger'); - log.error('Failed to delete workspace:', error as any); + log.error('Failed to delete task:', error as any); toast({ title: 'Error', description: error instanceof Error ? error.message - : 'Could not delete workspace. Check the console for details.', + : 'Could not delete task. Check the console for details.', variant: 'destructive', }); try { - const refreshedWorkspaces = await window.electronAPI.getWorkspaces(targetProject.id); + const refreshedTasks = await window.electronAPI.getTasks(targetProject.id); setProjects((prev) => prev.map((project) => - project.id === targetProject.id - ? { ...project, workspaces: refreshedWorkspaces } - : project + project.id === targetProject.id ? { ...project, tasks: refreshedTasks } : project ) ); setSelectedProject((prev) => - prev && prev.id === targetProject.id - ? { ...prev, workspaces: refreshedWorkspaces } - : prev + prev && prev.id === targetProject.id ? { ...prev, tasks: refreshedTasks } : prev ); if (wasActive) { - const restored = refreshedWorkspaces.find((w) => w.id === workspace.id); + const restored = refreshedTasks.find((w) => w.id === task.id); if (restored) { - handleSelectWorkspace(restored); + handleSelectTask(restored); } } } catch (refreshError) { - log.error('Failed to refresh workspaces after delete failure:', refreshError as any); + log.error('Failed to refresh tasks after delete failure:', refreshError as any); setProjects((prev) => prev.map((project) => { if (project.id !== targetProject.id) return project; - const existing = project.workspaces || []; - const alreadyPresent = existing.some((w) => w.id === workspaceSnapshot.id); - return alreadyPresent - ? project - : { ...project, workspaces: [workspaceSnapshot, ...existing] }; + const existing = project.tasks || []; + const alreadyPresent = existing.some((w) => w.id === taskSnapshot.id); + return alreadyPresent ? project : { ...project, tasks: [taskSnapshot, ...existing] }; }) ); setSelectedProject((prev) => { if (!prev || prev.id !== targetProject.id) return prev; - const existing = prev.workspaces || []; - const alreadyPresent = existing.some((w) => w.id === workspaceSnapshot.id); - return alreadyPresent - ? prev - : { ...prev, workspaces: [workspaceSnapshot, ...existing] }; + const existing = prev.tasks || []; + const alreadyPresent = existing.some((w) => w.id === taskSnapshot.id); + return alreadyPresent ? prev : { ...prev, tasks: [taskSnapshot, ...existing] }; }); if (wasActive) { - handleSelectWorkspace(workspaceSnapshot); + handleSelectTask(taskSnapshot); } } return false; } finally { - deletingWorkspaceIdsRef.current.delete(workspace.id); + deletingTaskIdsRef.current.delete(task.id); } }; @@ -1400,7 +1376,7 @@ const AppContent: React.FC = () => { setProjects((prev) => prev.filter((p) => p.id !== project.id)); if (selectedProject?.id === project.id) { setSelectedProject(null); - setActiveWorkspace(null); + setActiveTask(null); setShowHomeView(true); } toast({ title: 'Project deleted', description: `"${project.name}" was removed.` }); @@ -1488,11 +1464,11 @@ const AppContent: React.FC = () => {
{ - handleSelectWorkspace(ws); + onOpenTask={(ws: any) => { + handleSelectTask(ws); setShowKanban(false); }} - onCreateWorkspace={() => setShowWorkspaceModal(true)} + onCreateTask={() => setShowTaskModal(true)} />
); @@ -1561,29 +1537,29 @@ const AppContent: React.FC = () => { if (selectedProject) { return (
- {activeWorkspace ? ( - (activeWorkspace.metadata as any)?.multiAgent?.enabled ? ( - ) : ( ) ) : ( setShowWorkspaceModal(true)} - activeWorkspace={activeWorkspace} - onSelectWorkspace={handleSelectWorkspace} - onDeleteWorkspace={handleDeleteWorkspace} - isCreatingWorkspace={isCreatingWorkspace} + onCreateTask={() => setShowTaskModal(true)} + activeTask={activeTask} + onSelectTask={handleSelectTask} + onDeleteTask={handleDeleteTask} + isCreatingTask={isCreatingTask} onDeleteProject={handleDeleteProject} /> )} @@ -1654,19 +1630,17 @@ const AppContent: React.FC = () => { onToggleSettings={handleToggleSettings} isSettingsOpen={showSettings} currentPath={ - activeWorkspace?.metadata?.multiAgent?.enabled + activeTask?.metadata?.multiAgent?.enabled ? null - : activeWorkspace?.path || selectedProject?.path || null + : activeTask?.path || selectedProject?.path || null } defaultPreviewUrl={ - activeWorkspace?.id - ? getContainerRunState(activeWorkspace.id)?.previewUrl || null - : null + activeTask?.id ? getContainerRunState(activeTask.id)?.previewUrl || null : null } - workspaceId={activeWorkspace?.id || null} - workspacePath={activeWorkspace?.path || null} + taskId={activeTask?.id || null} + taskPath={activeTask?.path || null} projectPath={selectedProject?.path || null} - isWorkspaceMultiAgent={Boolean(activeWorkspace?.metadata?.multiAgent?.enabled)} + isTaskMultiAgent={Boolean(activeTask?.metadata?.multiAgent?.enabled)} githubUser={user} onToggleKanban={handleToggleKanban} isKanbanOpen={Boolean(showKanban)} @@ -1694,8 +1668,8 @@ const AppContent: React.FC = () => { onSelectProject={handleSelectProject} onGoHome={handleGoHome} onOpenProject={handleOpenProject} - onSelectWorkspace={handleSelectWorkspace} - activeWorkspace={activeWorkspace || undefined} + onSelectTask={handleSelectTask} + activeTask={activeTask || undefined} onReorderProjects={handleReorderProjects} onReorderProjectsFull={handleReorderProjectsFull} githubInstalled={ghInstalled} @@ -1705,9 +1679,9 @@ const AppContent: React.FC = () => { githubLoading={githubLoading} githubStatusMessage={githubStatusMessage} onSidebarContextChange={handleSidebarContextChange} - onCreateWorkspaceForProject={handleStartCreateWorkspaceFromSidebar} - isCreatingWorkspace={isCreatingWorkspace} - onDeleteWorkspace={handleDeleteWorkspace} + onCreateTaskForProject={handleStartCreateTaskFromSidebar} + isCreatingTask={isCreatingTask} + onDeleteTask={handleDeleteTask} onDeleteProject={handleDeleteProject} isHomeView={showHomeView} /> @@ -1741,7 +1715,7 @@ const AppContent: React.FC = () => { order={3} > @@ -1754,18 +1728,18 @@ const AppContent: React.FC = () => { onClose={handleCloseCommandPalette} projects={projects} handleSelectProject={handleSelectProject} - handleSelectWorkspace={handleSelectWorkspace} + handleSelectTask={handleSelectTask} handleGoHome={handleGoHome} handleOpenProject={handleOpenProject} handleOpenSettings={handleOpenSettings} /> - setShowWorkspaceModal(false)} - onCreateWorkspace={handleCreateWorkspace} + setShowTaskModal(false)} + onCreateTask={handleCreateTask} projectName={selectedProject?.name || ''} defaultBranch={selectedProject?.gitInfo.branch || 'main'} - existingNames={(selectedProject?.workspaces || []).map((w) => w.name)} + existingNames={(selectedProject?.tasks || []).map((w) => w.name)} projectPath={selectedProject?.path} /> @@ -1777,10 +1751,10 @@ const AppContent: React.FC = () => { /> diff --git a/src/renderer/components/ActiveRuns.tsx b/src/renderer/components/ActiveRuns.tsx index 9ce723bc..6830d44b 100644 --- a/src/renderer/components/ActiveRuns.tsx +++ b/src/renderer/components/ActiveRuns.tsx @@ -17,10 +17,10 @@ import { interface Props { projects: any[]; onSelectProject?: (project: any) => void; - onSelectWorkspace?: (workspace: any) => void; + onSelectTask?: (task: any) => void; } -const ActiveRuns: React.FC = ({ projects, onSelectProject, onSelectWorkspace }) => { +const ActiveRuns: React.FC = ({ projects, onSelectProject, onSelectTask }) => { const [activeRuns, setActiveRuns] = React.useState(() => (getAllRunStates() || []).filter((s) => ['building', 'starting', 'ready'].includes(s.status)) ); @@ -35,18 +35,18 @@ const ActiveRuns: React.FC = ({ projects, onSelectProject, onSelectWorksp if (!activeRuns.length) return null; - // Resolve workspace/project mapping for display and navigation - const byId = new Map(); + // Resolve task/project mapping for display and navigation + const byId = new Map(); for (const s of activeRuns) { - let match: { project: any | null; workspace: any | null } | null = null; + let match: { project: any | null; task: any | null } | null = null; for (const proj of projects) { - const ws = (proj.workspaces || []).find((w: any) => w.id === s.workspaceId) || null; + const ws = (proj.tasks || []).find((w: any) => w.id === s.taskId) || null; if (ws) { - match = { project: proj, workspace: ws }; + match = { project: proj, task: ws }; break; } } - byId.set(s.workspaceId, match ?? { project: null, workspace: null }); + byId.set(s.taskId, match ?? { project: null, task: null }); } return ( @@ -63,19 +63,19 @@ const ActiveRuns: React.FC = ({ projects, onSelectProject, onSelectWorksp {activeRuns.map((s) => { - const info = byId.get(s.workspaceId); + const info = byId.get(s.taskId); const project = info?.project || null; - const ws = info?.workspace || null; - const name = ws?.name || s.workspaceId; + const ws = info?.task || null; + const name = ws?.name || s.taskId; const previewUrl = s.previewUrl; const onOpen = () => { if (project && ws) { onSelectProject?.(project); - onSelectWorkspace?.(ws); + onSelectTask?.(ws); } }; return ( - +
- {typedProject.workspaces?.map((workspace) => { - const isActive = activeWorkspace?.id === workspace.id; + {typedProject.tasks?.map((task) => { + const isActive = activeTask?.id === task.id; return (
{ e.stopPropagation(); if ( @@ -291,19 +291,19 @@ const LeftSidebar: React.FC = ({ ) { onSelectProject(typedProject); } - onSelectWorkspace && onSelectWorkspace(workspace); + onSelectTask && onSelectTask(task); }} - className={`group/workspace min-w-0 rounded-md px-2 py-1.5 hover:bg-black/5 dark:hover:bg-white/5 ${ + className={`group/task min-w-0 rounded-md px-2 py-1.5 hover:bg-black/5 dark:hover:bg-white/5 ${ isActive ? 'bg-black/5 dark:bg-white/5' : '' }`} - title={workspace.name} + title={task.name} > - onDeleteWorkspace(typedProject, workspace) + onDeleteTask + ? () => onDeleteTask(typedProject, task) : undefined } /> diff --git a/src/renderer/components/MultiAgentWorkspace.tsx b/src/renderer/components/MultiAgentTask.tsx similarity index 93% rename from src/renderer/components/MultiAgentWorkspace.tsx rename to src/renderer/components/MultiAgentTask.tsx index 2c4be636..64b61359 100644 --- a/src/renderer/components/MultiAgentWorkspace.tsx +++ b/src/renderer/components/MultiAgentTask.tsx @@ -1,5 +1,5 @@ import React, { useEffect, useMemo, useRef, useState } from 'react'; -import { type Workspace } from '../types/chat'; +import { type Task } from '../types/chat'; import { type Provider } from '../types'; import { Button } from './ui/button'; import { Input } from './ui/input'; @@ -14,10 +14,10 @@ import { Spinner } from './ui/spinner'; import { BUSY_HOLD_MS, CLEAR_BUSY_MS } from '@/lib/activityConstants'; import { CornerDownLeft } from 'lucide-react'; import { TooltipProvider, Tooltip, TooltipTrigger, TooltipContent } from './ui/tooltip'; -import { useAutoScrollOnWorkspaceSwitch } from '@/hooks/useAutoScrollOnWorkspaceSwitch'; +import { useAutoScrollOnTaskSwitch } from '@/hooks/useAutoScrollOnTaskSwitch'; interface Props { - workspace: Workspace; + task: Task; projectName: string; projectId: string; } @@ -31,16 +31,16 @@ type Variant = { worktreeId: string; }; -const MultiAgentWorkspace: React.FC = ({ workspace }) => { +const MultiAgentTask: React.FC = ({ task }) => { const { effectiveTheme } = useTheme(); const [prompt, setPrompt] = useState(''); const [activeTabIndex, setActiveTabIndex] = useState(0); const [variantBusy, setVariantBusy] = useState>({}); - const multi = workspace.metadata?.multiAgent; + const multi = task.metadata?.multiAgent; const variants = (multi?.variants || []) as Variant[]; - // Auto-scroll to bottom when this workspace becomes active - const { scrollToBottom } = useAutoScrollOnWorkspaceSwitch(true, workspace.id); + // Auto-scroll to bottom when this task becomes active + const { scrollToBottom } = useAutoScrollOnTaskSwitch(true, task.id); // Helper to generate display label with instance number if needed const getVariantDisplayLabel = (variant: Variant): string => { @@ -56,7 +56,7 @@ const MultiAgentWorkspace: React.FC = ({ workspace }) => { } // Multiple instances: extract instance number from variant name - // variant.name format: "workspace-provider-1", "workspace-provider-2", etc. + // variant.name format: "task-provider-1", "task-provider-2", etc. const match = variant.name.match(/-(\d+)$/); const instanceNum = match ? match[1] : String(providerVariants.indexOf(variant) + 1); @@ -65,7 +65,7 @@ const MultiAgentWorkspace: React.FC = ({ workspace }) => { // Build initial issue context (feature parity with single-agent ChatInterface) const initialInjection: string | null = useMemo(() => { - const md: any = workspace.metadata || null; + const md: any = task.metadata || null; if (!md) return null; const p = (md.initialPrompt || '').trim(); if (p) return p; @@ -154,7 +154,7 @@ const MultiAgentWorkspace: React.FC = ({ workspace }) => { return lines.join('\n'); } return null; - }, [workspace.metadata]); + }, [task.metadata]); // Robust prompt injection modeled after useInitialPromptInjection, without one-shot gating const injectPrompt = async (ptyId: string, provider: Provider, text: string) => { @@ -345,8 +345,8 @@ const MultiAgentWorkspace: React.FC = ({ workspace }) => { // Sync variant busy state to activityStore for sidebar indicator useEffect(() => { const anyBusy = Object.values(variantBusy).some(Boolean); - activityStore.setWorkspaceBusy(workspace.id, anyBusy); - }, [variantBusy, workspace.id]); + activityStore.setTaskBusy(task.id, anyBusy); + }, [variantBusy, task.id]); // Scroll to bottom when active tab changes useEffect(() => { @@ -362,7 +362,7 @@ const MultiAgentWorkspace: React.FC = ({ workspace }) => { if (!multi?.enabled || variants.length === 0) { return (
- Multi-agent config missing for this workspace. + Multi-agent config missing for this task.
); } @@ -444,12 +444,12 @@ const MultiAgentWorkspace: React.FC = ({ workspace }) => { cwd={v.path} shell={providerMeta[v.provider].cli} autoApprove={ - Boolean(workspace.metadata?.autoApprove) && + Boolean(task.metadata?.autoApprove) && Boolean(providerMeta[v.provider]?.autoApproveFlag) } initialPrompt={ providerMeta[v.provider]?.initialPromptFlag !== undefined && - !workspace.metadata?.initialInjectionSent + !task.metadata?.initialInjectionSent ? (initialInjection ?? undefined) : undefined } @@ -465,17 +465,17 @@ const MultiAgentWorkspace: React.FC = ({ workspace }) => { // For providers WITHOUT CLI flag support, use keystroke injection if ( initialInjection && - !workspace.metadata?.initialInjectionSent && + !task.metadata?.initialInjectionSent && providerMeta[v.provider]?.initialPromptFlag === undefined ) { void injectPrompt(`${v.worktreeId}-main`, v.provider, initialInjection); } // Mark initial injection as sent so it won't re-run on restart - if (initialInjection && !workspace.metadata?.initialInjectionSent) { - void window.electronAPI.saveWorkspace({ - ...workspace, + if (initialInjection && !task.metadata?.initialInjectionSent) { + void window.electronAPI.saveTask({ + ...task, metadata: { - ...workspace.metadata, + ...task.metadata, initialInjectionSent: true, }, }); @@ -526,4 +526,4 @@ const MultiAgentWorkspace: React.FC = ({ workspace }) => { ); }; -export default MultiAgentWorkspace; +export default MultiAgentTask; diff --git a/src/renderer/components/ProjectDeleteButton.tsx b/src/renderer/components/ProjectDeleteButton.tsx index d19ca248..74a4619b 100644 --- a/src/renderer/components/ProjectDeleteButton.tsx +++ b/src/renderer/components/ProjectDeleteButton.tsx @@ -18,11 +18,11 @@ import { cn } from '@/lib/utils'; import { useDeleteRisks } from '../hooks/useDeleteRisks'; import DeletePrNotice from './DeletePrNotice'; import { isActivePr } from '../lib/prStatus'; -import type { Workspace } from '../types/app'; +import type { Task } from '../types/chat'; type Props = { projectName: string; - workspaces?: Workspace[]; + tasks?: Task[]; onConfirm: () => void | Promise; className?: string; 'aria-label'?: string; @@ -31,7 +31,7 @@ type Props = { export const ProjectDeleteButton: React.FC = ({ projectName, - workspaces = [], + tasks = [], onConfirm, className, 'aria-label': ariaLabel = 'Delete project', @@ -41,14 +41,14 @@ export const ProjectDeleteButton: React.FC = ({ const [acknowledge, setAcknowledge] = React.useState(false); const targets = useMemo( - () => workspaces.map((ws) => ({ id: ws.id, name: ws.name, path: ws.path })), - [workspaces] + () => tasks.map((ws) => ({ id: ws.id, name: ws.name, path: ws.path })), + [tasks] ); const { risks, loading, hasData } = useDeleteRisks(targets, open); - // Workspaces with uncommitted/unpushed changes BUT NO PR - const workspacesWithUncommittedWork = workspaces.filter((ws) => { + // Tasks with uncommitted/unpushed changes BUT NO PR + const tasksWithUncommittedWork = tasks.filter((ws) => { const status = risks[ws.id]; if (!status) return false; const hasUncommittedWork = @@ -58,13 +58,13 @@ export const ProjectDeleteButton: React.FC = ({ return hasUncommittedWork && !hasPR; }); - // Workspaces with PRs (may or may not have uncommitted work) - const workspacesWithPRs = workspaces.filter((ws) => { + // Tasks with PRs (may or may not have uncommitted work) + const tasksWithPRs = tasks.filter((ws) => { const status = risks[ws.id]; return status?.pr && isActivePr(status.pr); }); - const hasRisks = workspacesWithUncommittedWork.length > 0 || workspacesWithPRs.length > 0; + const hasRisks = tasksWithUncommittedWork.length > 0 || tasksWithPRs.length > 0; const disableDelete = Boolean(isDeleting || loading) || (hasRisks && !acknowledge); React.useEffect(() => { @@ -108,7 +108,7 @@ export const ProjectDeleteButton: React.FC = ({ Delete project? - {`This removes "${projectName}" from Emdash, including its saved workspaces and conversations. Files on disk are not deleted.`} + {`This removes "${projectName}" from Emdash, including its saved tasks and conversations. Files on disk are not deleted.`} @@ -127,7 +127,7 @@ export const ProjectDeleteButton: React.FC = ({
Please wait... - Scanning workspaces for uncommitted changes and open pull requests + Scanning tasks for uncommitted changes and open pull requests
@@ -135,7 +135,7 @@ export const ProjectDeleteButton: React.FC = ({ - {!loading && workspacesWithUncommittedWork.length > 0 ? ( + {!loading && tasksWithUncommittedWork.length > 0 ? ( = ({ className="space-y-2 rounded-md border border-amber-300/60 bg-amber-50 px-3 py-2 text-amber-900 dark:border-amber-500/40 dark:bg-amber-500/10 dark:text-amber-50" >

- {workspacesWithUncommittedWork.length === 1 + {tasksWithUncommittedWork.length === 1 ? 'Unmerged or unpushed work detected in 1 task' - : `Unmerged or unpushed work detected in ${workspacesWithUncommittedWork.length} tasks`} + : `Unmerged or unpushed work detected in ${tasksWithUncommittedWork.length} tasks`}

    - {workspacesWithUncommittedWork.map((ws) => { + {tasksWithUncommittedWork.map((ws) => { const status = risks[ws.id]; if (!status) return null; const summary = [ @@ -188,7 +188,7 @@ export const ProjectDeleteButton: React.FC = ({ - {!loading && workspacesWithPRs.length > 0 ? ( + {!loading && tasksWithPRs.length > 0 ? ( = ({ transition={{ duration: 0.18, ease: 'easeOut', delay: 0.02 }} > { const pr = risks[ws.id]?.pr; return pr && isActivePr(pr) ? { name: ws.name, pr } : null; @@ -257,7 +257,7 @@ export const ProjectDeleteButton: React.FC = ({ {disableDelete && !isDeleting ? ( {loading - ? 'Checking workspaces...' + ? 'Checking tasks...' : hasRisks && !acknowledge ? 'Acknowledge the risks to delete' : 'Delete is disabled'} diff --git a/src/renderer/components/ProjectMainView.tsx b/src/renderer/components/ProjectMainView.tsx index 6dc19350..843ee163 100644 --- a/src/renderer/components/ProjectMainView.tsx +++ b/src/renderer/components/ProjectMainView.tsx @@ -5,10 +5,10 @@ import { AnimatePresence, motion } from 'motion/react'; import { Separator } from './ui/separator'; import { Alert, AlertDescription, AlertTitle } from './ui/alert'; import { usePrStatus } from '../hooks/usePrStatus'; -import { useWorkspaceChanges } from '../hooks/useWorkspaceChanges'; -import { ChangesBadge } from './WorkspaceChanges'; +import { useTaskChanges } from '../hooks/useTaskChanges'; +import { ChangesBadge } from './TaskChanges'; import { Spinner } from './ui/spinner'; -import WorkspaceDeleteButton from './WorkspaceDeleteButton'; +import TaskDeleteButton from './TaskDeleteButton'; import ProjectDeleteButton from './ProjectDeleteButton'; import { AlertDialog, @@ -24,20 +24,20 @@ import { Checkbox } from './ui/checkbox'; import BaseBranchControls, { RemoteBranchOption } from './BaseBranchControls'; import { useToast } from '../hooks/use-toast'; import ContainerStatusBadge from './ContainerStatusBadge'; -import WorkspacePorts from './WorkspacePorts'; +import TaskPorts from './TaskPorts'; import { TooltipProvider, Tooltip, TooltipTrigger, TooltipContent } from './ui/tooltip'; import dockerLogo from '../../assets/images/docker.png'; import DeletePrNotice from './DeletePrNotice'; import { getContainerRunState, startContainerRun, - subscribeToWorkspaceRunState, + subscribeToTaskRunState, type ContainerRunState, } from '@/lib/containerRuns'; import { activityStore } from '../lib/activityStore'; import PrPreviewTooltip from './PrPreviewTooltip'; import { isActivePr, PrInfo } from '../lib/prStatus'; -import type { Project, Workspace } from '../types/app'; +import type { Project, Task } from '../types/app'; const normalizeBaseRef = (ref?: string | null): string | undefined => { if (!ref) return undefined; @@ -45,7 +45,7 @@ const normalizeBaseRef = (ref?: string | null): string | undefined => { return trimmed.length > 0 ? trimmed : undefined; }; -function WorkspaceRow({ +function TaskRow({ ws, active, onClick, @@ -54,7 +54,7 @@ function WorkspaceRow({ isSelected, onToggleSelect, }: { - ws: Workspace; + ws: Task; active: boolean; onClick: () => void; onDelete: () => void | Promise; @@ -66,7 +66,7 @@ function WorkspaceRow({ const [isRunning, setIsRunning] = useState(false); const [isDeleting, setIsDeleting] = useState(false); const { pr } = usePrStatus(ws.path); - const { totalAdditions, totalDeletions, isLoading } = useWorkspaceChanges(ws.path, ws.id); + const { totalAdditions, totalDeletions, isLoading } = useTaskChanges(ws.path, ws.id); const [containerState, setContainerState] = useState(() => getContainerRunState(ws.id) ); @@ -127,7 +127,7 @@ function WorkspaceRow({ }, [ws.id]); useEffect(() => { - const off = subscribeToWorkspaceRunState(ws.id, (state) => setContainerState(state)); + const off = subscribeToTaskRunState(ws.id, (state) => setContainerState(state)); return () => { off?.(); }; @@ -138,7 +138,7 @@ function WorkspaceRow({ (async () => { try { const mod = await import('@/lib/containerRuns'); - await mod.refreshWorkspaceRunState(ws.id); + await mod.refreshTaskRunState(ws.id); } catch {} })(); }, [ws.id]); @@ -148,8 +148,8 @@ function WorkspaceRow({ try { setIsStartingContainer(true); const res = await startContainerRun({ - workspaceId: ws.id, - workspacePath: ws.path, + taskId: ws.id, + taskPath: ws.path, mode: 'container', }); if (res?.ok !== true) { @@ -256,14 +256,14 @@ function WorkspaceRow({ type="button" disabled className="inline-flex h-8 cursor-not-allowed items-center justify-center rounded-md border border-border/70 bg-background px-2.5 text-xs font-medium opacity-50" - aria-label="Connect disabled for multi-agent workspaces" + aria-label="Connect disabled for multi-agent tasks" > Docker Connect - Docker containerization is not available for multi-agent workspaces. + Docker containerization is not available for multi-agent tasks. @@ -295,7 +295,7 @@ function WorkspaceRow({ stoppingAction={isStoppingContainer} onStart={handleStartContainer} onStop={handleStopContainer} - workspacePath={ws.path} + taskPath={ws.path} /> )} {containerActive ? ( @@ -347,10 +347,10 @@ function WorkspaceRow({ className="h-4 w-4 rounded border-muted-foreground/50 data-[state=checked]:border-muted-foreground data-[state=checked]:bg-muted-foreground" /> ) : ( - { try { setIsDeleting(true); @@ -360,7 +360,7 @@ function WorkspaceRow({ } }} isDeleting={isDeleting} - aria-label={`Delete workspace ${ws.name}`} + aria-label={`Delete task ${ws.name}`} className="inline-flex items-center justify-center rounded p-2 text-muted-foreground hover:bg-transparent focus-visible:ring-0" /> )} @@ -369,10 +369,10 @@ function WorkspaceRow({ {containerActive && expanded ? ( - void; - activeWorkspace: Workspace | null; - onSelectWorkspace: (workspace: Workspace) => void; - onDeleteWorkspace: ( + onCreateTask: () => void; + activeTask: Task | null; + onSelectTask: (task: Task) => void; + onDeleteTask: ( project: Project, - workspace: Workspace, + task: Task, options?: { silent?: boolean } ) => void | Promise; - isCreatingWorkspace?: boolean; + isCreatingTask?: boolean; onDeleteProject?: (project: Project) => void | Promise; } const ProjectMainView: React.FC = ({ project, - onCreateWorkspace, - activeWorkspace, - onSelectWorkspace, - onDeleteWorkspace, - isCreatingWorkspace = false, + onCreateTask, + activeTask, + onSelectTask, + onDeleteTask, + isCreatingTask = false, onDeleteProject, }) => { const { toast } = useToast(); @@ -423,11 +423,11 @@ const ProjectMainView: React.FC = ({ const [isDeleting, setIsDeleting] = useState(false); const [acknowledgeDirtyDelete, setAcknowledgeDirtyDelete] = useState(false); - const workspaces = project.workspaces ?? []; + const tasksInProject = project.tasks ?? []; const selectedCount = selectedIds.size; - const selectedWorkspaces = useMemo( - () => workspaces.filter((ws) => selectedIds.has(ws.id)), - [selectedIds, workspaces] + const selectedTasks = useMemo( + () => tasksInProject.filter((ws) => selectedIds.has(ws.id)), + [selectedIds, tasksInProject] ); const [deleteStatus, setDeleteStatus] = useState< Record< @@ -447,7 +447,7 @@ const ProjectMainView: React.FC = ({ const deleteRisks = useMemo(() => { const riskyIds = new Set(); const summaries: Record = {}; - for (const ws of selectedWorkspaces) { + for (const ws of selectedTasks) { const status = deleteStatus[ws.id]; if (!status) continue; const dirty = @@ -476,7 +476,7 @@ const ProjectMainView: React.FC = ({ } } return { riskyIds, summaries }; - }, [deleteStatus, selectedWorkspaces]); + }, [deleteStatus, selectedTasks]); const deleteDisabled: boolean = Boolean(isDeleting || deleteStatusLoading) || (deleteRisks.riskyIds.size > 0 && acknowledgeDirtyDelete !== true); @@ -499,7 +499,7 @@ const ProjectMainView: React.FC = ({ }; const handleBulkDelete = async () => { - const toDelete = workspaces.filter((ws) => selectedIds.has(ws.id)); + const toDelete = tasksInProject.filter((ws) => selectedIds.has(ws.id)); if (toDelete.length === 0) return; setIsDeleting(true); @@ -508,12 +508,12 @@ const ProjectMainView: React.FC = ({ const deletedNames: string[] = []; for (const ws of toDelete) { try { - const result = await onDeleteWorkspace(project, ws, { silent: true }); + const result = await onDeleteTask(project, ws, { silent: true }); if (result !== false) { deletedNames.push(ws.name); } } catch { - // Continue deleting remaining workspaces + // Continue deleting remaining tasks } } @@ -554,12 +554,12 @@ const ProjectMainView: React.FC = ({ setDeleteStatusLoading(true); const next: typeof deleteStatus = {}; - for (const ws of selectedWorkspaces) { + for (const ws of selectedTasks) { try { const [statusRes, infoRes, prRes] = await Promise.allSettled([ window.electronAPI.getGitStatus(ws.path), window.electronAPI.getGitInfo(ws.path), - window.electronAPI.getPrStatus({ workspacePath: ws.path }), + window.electronAPI.getPrStatus({ taskPath: ws.path }), ]); let staged = 0; @@ -627,7 +627,7 @@ const ProjectMainView: React.FC = ({ return () => { cancelled = true; }; - }, [showDeleteDialog, selectedWorkspaces]); + }, [showDeleteDialog, selectedTasks]); useEffect(() => { let cancelled = false; @@ -747,7 +747,7 @@ const ProjectMainView: React.FC = ({ {onDeleteProject ? ( onDeleteProject?.(project)} aria-label={`Delete project ${project.name}`} /> @@ -781,7 +781,7 @@ const ProjectMainView: React.FC = ({

    Tasks

    - Spin up a fresh, isolated workspace for this project. + Spin up a fresh, isolated task for this project.

    {!isSelectMode && ( @@ -789,11 +789,11 @@ const ProjectMainView: React.FC = ({ variant="default" size="sm" className="h-9 px-4 text-sm font-semibold shadow-sm" - onClick={onCreateWorkspace} - disabled={isCreatingWorkspace} - aria-busy={isCreatingWorkspace} + onClick={onCreateTask} + disabled={isCreatingTask} + aria-busy={isCreatingTask} > - {isCreatingWorkspace ? ( + {isCreatingTask ? ( <> Starting… @@ -807,7 +807,7 @@ const ProjectMainView: React.FC = ({ )}
- {workspaces.length > 0 && ( + {tasksInProject.length > 0 && (
{isSelectMode && selectedCount > 0 && (
)}
- {workspaces.map((ws) => ( - ( + toggleSelect(ws.id)} - active={activeWorkspace?.id === ws.id} - onClick={() => onSelectWorkspace(ws)} - onDelete={() => onDeleteWorkspace(project, ws)} + active={activeTask?.id === ws.id} + onClick={() => onSelectTask(ws)} + onDelete={() => onDeleteTask(project, ws)} /> ))}
- {(!project.workspaces || project.workspaces.length === 0) && ( + {(!project.tasks || project.tasks.length === 0) && ( What's a task? @@ -880,7 +880,7 @@ const ProjectMainView: React.FC = ({
{(() => { - const workspacesWithUncommittedWorkOnly = selectedWorkspaces.filter((ws) => { + const tasksWithUncommittedWorkOnly = selectedTasks.filter((ws) => { const summary = deleteRisks.summaries[ws.id]; const status = deleteStatus[ws.id]; if (!summary && !status?.error) return false; @@ -888,7 +888,7 @@ const ProjectMainView: React.FC = ({ return true; }); - return workspacesWithUncommittedWorkOnly.length > 0 ? ( + return tasksWithUncommittedWorkOnly.length > 0 ? ( = ({ >

Unmerged or unpushed work detected

    - {workspacesWithUncommittedWorkOnly.map((ws) => { + {tasksWithUncommittedWorkOnly.map((ws) => { const summary = deleteRisks.summaries[ws.id]; const status = deleteStatus[ws.id]; return ( @@ -922,10 +922,10 @@ const ProjectMainView: React.FC = ({ {(() => { - const prWorkspaces = selectedWorkspaces + const prTasks = selectedTasks .map((ws) => ({ name: ws.name, pr: deleteStatus[ws.id]?.pr })) .filter((w) => w.pr && isActivePr(w.pr)); - return prWorkspaces.length ? ( + return prTasks.length ? ( = ({ exit={{ opacity: 0, y: 6, scale: 0.99 }} transition={{ duration: 0.2, ease: 'easeOut', delay: 0.02 }} > - + ) : null; })()} diff --git a/src/renderer/components/ProviderBar.tsx b/src/renderer/components/ProviderBar.tsx index 45099e64..86055fcb 100644 --- a/src/renderer/components/ProviderBar.tsx +++ b/src/renderer/components/ProviderBar.tsx @@ -36,7 +36,7 @@ import { getContext7InvocationForProvider } from '../mcp/context7'; type Props = { provider: Provider; - workspaceId: string; + taskId: string; linearIssue?: LinearIssueSummary | null; githubIssue?: GitHubIssueSummary | null; jiraIssue?: JiraIssueSummary | null; @@ -48,7 +48,7 @@ type Props = { export const ProviderBar: React.FC = ({ provider, - workspaceId, + taskId, linearIssue, githubIssue, jiraIssue, @@ -59,7 +59,7 @@ export const ProviderBar: React.FC = ({ }) => { const [c7Enabled, setC7Enabled] = React.useState(false); const [c7Busy, setC7Busy] = React.useState(false); - const [c7WorkspaceEnabled, setC7WorkspaceEnabled] = React.useState(false); + const [c7TaskEnabled, setC7TaskEnabled] = React.useState(false); React.useEffect(() => { let cancelled = false; @@ -76,38 +76,38 @@ export const ProviderBar: React.FC = ({ }; }, []); - // Per-workspace default OFF + // Per-task default OFF React.useEffect(() => { try { - const key = `c7:ws:${workspaceId}`; - setC7WorkspaceEnabled(localStorage.getItem(key) === '1'); + const key = `c7:ws:${taskId}`; + setC7TaskEnabled(localStorage.getItem(key) === '1'); } catch { - setC7WorkspaceEnabled(false); + setC7TaskEnabled(false); } - }, [workspaceId]); + }, [taskId]); const handleContext7Click = async () => { setC7Busy(true); try { if (!c7Enabled) return; - if (!c7WorkspaceEnabled) { - // Enable for this workspace and send invocation once + if (!c7TaskEnabled) { + // Enable for this task and send invocation once try { - localStorage.setItem(`c7:ws:${workspaceId}`, '1'); + localStorage.setItem(`c7:ws:${taskId}`, '1'); } catch {} - setC7WorkspaceEnabled(true); + setC7TaskEnabled(true); const isTerminal = providerMeta[provider]?.terminalOnly === true; if (!isTerminal) return; const phrase = getContext7InvocationForProvider(provider) || 'use context7'; - const ptyId = `${provider}-main-${workspaceId}`; + const ptyId = `${provider}-main-${taskId}`; (window as any).electronAPI?.ptyInput?.({ id: ptyId, data: `${phrase}\n` }); } else { try { - localStorage.removeItem(`c7:ws:${workspaceId}`); + localStorage.removeItem(`c7:ws:${taskId}`); } catch {} - setC7WorkspaceEnabled(false); + setC7TaskEnabled(false); } } finally { setC7Busy(false); @@ -401,15 +401,15 @@ export const ProviderBar: React.FC = ({ disabled={c7Busy || !c7Enabled} className={[ 'inline-flex h-7 items-center gap-1.5 rounded-md border px-2 text-xs', - c7WorkspaceEnabled + c7TaskEnabled ? 'border-emerald-500/50 bg-emerald-500/10 text-foreground' : 'border-gray-200 bg-gray-100 text-foreground dark:border-gray-700 dark:bg-gray-700', ].join(' ')} title={ c7Enabled - ? c7WorkspaceEnabled - ? 'Disable Context7 for this workspace' - : 'Enable for this workspace & send to terminal' + ? c7TaskEnabled + ? 'Disable Context7 for this task' + : 'Enable for this task & send to terminal' : 'Enable Context7 in Settings → MCP to use here' } > @@ -431,7 +431,7 @@ export const ProviderBar: React.FC = ({ - + diff --git a/src/renderer/components/RightSidebar.tsx b/src/renderer/components/RightSidebar.tsx index 93d3627a..8e2ab6f4 100644 --- a/src/renderer/components/RightSidebar.tsx +++ b/src/renderer/components/RightSidebar.tsx @@ -2,13 +2,13 @@ import React from 'react'; import { cn } from '@/lib/utils'; import FileChangesPanel from './FileChangesPanel'; import { useFileChanges } from '@/hooks/useFileChanges'; -import WorkspaceTerminalPanel from './WorkspaceTerminalPanel'; +import TaskTerminalPanel from './TaskTerminalPanel'; import { useRightSidebar } from './ui/right-sidebar'; import { providerAssets } from '@/providers/assets'; import { providerMeta } from '@/providers/meta'; import type { Provider } from '../types'; -export interface RightSidebarWorkspace { +export interface RightSidebarTask { id: string; name: string; branch: string; @@ -19,22 +19,17 @@ export interface RightSidebarWorkspace { } interface RightSidebarProps extends React.HTMLAttributes { - workspace: RightSidebarWorkspace | null; + task: RightSidebarTask | null; projectPath?: string | null; } -const RightSidebar: React.FC = ({ - workspace, - projectPath, - className, - ...rest -}) => { +const RightSidebar: React.FC = ({ task, projectPath, className, ...rest }) => { const { collapsed } = useRightSidebar(); - // Detect multi-agent variants in workspace metadata + // Detect multi-agent variants in task metadata const variants: Array<{ provider: Provider; name: string; path: string }> = (() => { try { - const v = workspace?.metadata?.multiAgent?.variants || []; + const v = task?.metadata?.multiAgent?.variants || []; if (Array.isArray(v)) return v .map((x: any) => ({ provider: x?.provider as Provider, name: x?.name, path: x?.path })) @@ -58,7 +53,7 @@ const RightSidebar: React.FC = ({ } // Multiple instances: extract instance number from variant name - // variant.name format: "workspace-provider-1", "workspace-provider-2", etc. + // variant.name format: "task-provider-1", "task-provider-2", etc. const match = variant.name.match(/-(\d+)$/); const instanceNum = match ? match[1] @@ -79,9 +74,9 @@ const RightSidebar: React.FC = ({ {...rest} >
    - {workspace || projectPath ? ( + {task || projectPath ? (
    - {workspace && variants.length > 1 ? ( + {task && variants.length > 1 ? (
    {variants.map((v, i) => (
    = ({
    ))}
    - ) : workspace && variants.length === 1 ? ( + ) : task && variants.length === 1 ? ( (() => { const v = variants[0]; const derived = { - ...workspace, + ...task, path: v.path, - name: v.name || workspace.name, + name: v.name || task.name, } as any; return ( <> @@ -133,25 +128,25 @@ const RightSidebar: React.FC = ({ path={v.path} className="min-h-0 flex-1 border-b border-border" /> - ); })() - ) : workspace ? ( + ) : task ? ( <> - @@ -167,8 +162,8 @@ const RightSidebar: React.FC = ({
    - = ({ }) => { const { fileChanges } = useFileChanges(path); if (!fileChanges || fileChanges.length === 0) return null; - return ; + return ; }; diff --git a/src/renderer/components/WorkspaceChanges.tsx b/src/renderer/components/TaskChanges.tsx similarity index 100% rename from src/renderer/components/WorkspaceChanges.tsx rename to src/renderer/components/TaskChanges.tsx diff --git a/src/renderer/components/WorkspaceDeleteButton.tsx b/src/renderer/components/TaskDeleteButton.tsx similarity index 92% rename from src/renderer/components/WorkspaceDeleteButton.tsx rename to src/renderer/components/TaskDeleteButton.tsx index c603ddb4..12ac4aef 100644 --- a/src/renderer/components/WorkspaceDeleteButton.tsx +++ b/src/renderer/components/TaskDeleteButton.tsx @@ -20,19 +20,19 @@ import DeletePrNotice from './DeletePrNotice'; import { isActivePr } from '../lib/prStatus'; type Props = { - workspaceName: string; - workspaceId: string; - workspacePath: string; + taskName: string; + taskId: string; + taskPath: string; onConfirm: () => void | Promise; className?: string; 'aria-label'?: string; isDeleting?: boolean; }; -export const WorkspaceDeleteButton: React.FC = ({ - workspaceName, - workspaceId, - workspacePath, +export const TaskDeleteButton: React.FC = ({ + taskName, + taskId, + taskPath, onConfirm, className, 'aria-label': ariaLabel = 'Delete Task', @@ -41,11 +41,11 @@ export const WorkspaceDeleteButton: React.FC = ({ const [open, setOpen] = React.useState(false); const [acknowledge, setAcknowledge] = React.useState(false); const targets = useMemo( - () => [{ id: workspaceId, name: workspaceName, path: workspacePath }], - [workspaceId, workspaceName, workspacePath] + () => [{ id: taskId, name: taskName, path: taskPath }], + [taskId, taskName, taskPath] ); const { risks, loading, hasData } = useDeleteRisks(targets, open); - const status = risks[workspaceId] || { + const status = risks[taskId] || { staged: 0, unstaged: 0, untracked: 0, @@ -123,7 +123,7 @@ export const WorkspaceDeleteButton: React.FC = ({

    Unmerged or unpushed work detected

    - {workspaceName} + {taskName} {[ @@ -150,7 +150,7 @@ export const WorkspaceDeleteButton: React.FC = ({
    {status.pr && isActivePr(status.pr) ? ( - + ) : null} ) : null} @@ -198,4 +198,4 @@ export const WorkspaceDeleteButton: React.FC = ({ ); }; -export default WorkspaceDeleteButton; +export default TaskDeleteButton; diff --git a/src/renderer/components/WorkspaceItem.tsx b/src/renderer/components/TaskItem.tsx similarity index 74% rename from src/renderer/components/WorkspaceItem.tsx rename to src/renderer/components/TaskItem.tsx index 92eed736..2e763849 100644 --- a/src/renderer/components/WorkspaceItem.tsx +++ b/src/renderer/components/TaskItem.tsx @@ -1,14 +1,14 @@ import React from 'react'; import { GitBranch, ArrowUpRight } from 'lucide-react'; -import WorkspaceDeleteButton from './WorkspaceDeleteButton'; -import { useWorkspaceChanges } from '../hooks/useWorkspaceChanges'; -import { ChangesBadge } from './WorkspaceChanges'; +import TaskDeleteButton from './TaskDeleteButton'; +import { useTaskChanges } from '../hooks/useTaskChanges'; +import { ChangesBadge } from './TaskChanges'; import { Spinner } from './ui/spinner'; import { usePrStatus } from '../hooks/usePrStatus'; -import { useWorkspaceBusy } from '../hooks/useWorkspaceBusy'; +import { useTaskBusy } from '../hooks/useTaskBusy'; import PrPreviewTooltip from './PrPreviewTooltip'; -interface Workspace { +interface Task { id: string; name: string; branch: string; @@ -17,42 +17,35 @@ interface Workspace { agentId?: string; } -interface WorkspaceItemProps { - workspace: Workspace; +interface TaskItemProps { + task: Task; onDelete?: () => void | Promise; showDelete?: boolean; } -export const WorkspaceItem: React.FC = ({ - workspace, - onDelete, - showDelete, -}) => { - const { totalAdditions, totalDeletions, isLoading } = useWorkspaceChanges( - workspace.path, - workspace.id - ); - const { pr } = usePrStatus(workspace.path); - const isRunning = useWorkspaceBusy(workspace.id); +export const TaskItem: React.FC = ({ task, onDelete, showDelete }) => { + const { totalAdditions, totalDeletions, isLoading } = useTaskChanges(task.path, task.id); + const { pr } = usePrStatus(task.path); + const isRunning = useTaskBusy(task.id); const [isDeleting, setIsDeleting] = React.useState(false); return (
    - {isRunning || workspace.status === 'running' ? ( + {isRunning || task.status === 'running' ? ( ) : ( )} - {workspace.name} + {task.name}
    {showDelete && onDelete ? ( - { try { setIsDeleting(true); @@ -62,9 +55,9 @@ export const WorkspaceItem: React.FC = ({ } }} isDeleting={isDeleting} - aria-label={`Delete Task ${workspace.name}`} + aria-label={`Delete Task ${task.name}`} className={`absolute left-0 inline-flex h-5 w-5 items-center justify-center rounded p-0.5 text-muted-foreground transition-opacity duration-150 hover:bg-muted focus:opacity-100 focus-visible:opacity-100 ${ - isDeleting ? 'opacity-100' : 'opacity-0 group-hover/workspace:opacity-100' + isDeleting ? 'opacity-100' : 'opacity-0 group-hover/task:opacity-100' }`} /> ) : null} diff --git a/src/renderer/components/WorkspaceList.tsx b/src/renderer/components/TaskList.tsx similarity index 63% rename from src/renderer/components/WorkspaceList.tsx rename to src/renderer/components/TaskList.tsx index d7e39cc4..5dc1fd9b 100644 --- a/src/renderer/components/WorkspaceList.tsx +++ b/src/renderer/components/TaskList.tsx @@ -4,7 +4,7 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from './ui/ import { Spinner } from './ui/spinner'; import { GitBranch, Bot, Play, Pause, Plus } from 'lucide-react'; -interface Workspace { +interface Task { id: string; name: string; branch: string; @@ -13,19 +13,19 @@ interface Workspace { } interface Props { - workspaces: Workspace[]; - activeWorkspace: Workspace | null; - onSelectWorkspace: (workspace: Workspace) => void; - onCreateWorkspace: () => void; - isCreatingWorkspace?: boolean; + tasks: Task[]; + activeTask: Task | null; + onSelectTask: (task: Task) => void; + onCreateTask: () => void; + isCreatingTask?: boolean; } -export const WorkspaceList: React.FC = ({ - workspaces, - activeWorkspace, - onSelectWorkspace, - onCreateWorkspace, - isCreatingWorkspace = false, +export const TaskList: React.FC = ({ + tasks, + activeTask, + onSelectTask, + onCreateTask, + isCreatingTask = false, }) => { const getStatusIcon = (status: string) => { switch (status) { @@ -41,14 +41,9 @@ export const WorkspaceList: React.FC = ({ return (
    -

    Workspaces

    -
    - {workspaces.length === 0 ? ( + {tasks.length === 0 ? (
    -

    No workspaces yet. Create one to get started!

    +

    No tasks yet. Create one to get started!

    ) : (
    - {workspaces.map((workspace) => ( + {tasks.map((task) => ( onSelectWorkspace(workspace)} + onClick={() => onSelectTask(task)} > - {getStatusIcon(workspace.status)} - {workspace.name} + {getStatusIcon(task.status)} + {task.name} - {workspace.branch} + {task.branch} -

    Status: {workspace.status}

    +

    Status: {task.status}

    ))} @@ -99,4 +94,4 @@ export const WorkspaceList: React.FC = ({ ); }; -export default WorkspaceList; +export default TaskList; diff --git a/src/renderer/components/WorkspaceModal.tsx b/src/renderer/components/TaskModal.tsx similarity index 94% rename from src/renderer/components/WorkspaceModal.tsx rename to src/renderer/components/TaskModal.tsx index d726675f..f096d9bf 100644 --- a/src/renderer/components/WorkspaceModal.tsx +++ b/src/renderer/components/TaskModal.tsx @@ -18,10 +18,10 @@ import { isValidProviderId } from '@shared/providers/registry'; import { type LinearIssueSummary } from '../types/linear'; import { type GitHubIssueSummary } from '../types/github'; import { - generateFriendlyWorkspaceName, - normalizeWorkspaceName, - MAX_WORKSPACE_NAME_LENGTH, -} from '../lib/workspaceNames'; + generateFriendlyTaskName, + normalizeTaskName, + MAX_TASK_NAME_LENGTH, +} from '../lib/taskNames'; const DEFAULT_PROVIDER: Provider = 'claude'; @@ -33,10 +33,10 @@ import JiraIssueSelector from './JiraIssueSelector'; import { type JiraIssueSummary } from '../types/jira'; import { useGithubAuth } from '../hooks/useGithubAuth'; -interface WorkspaceModalProps { +interface TaskModalProps { isOpen: boolean; onClose: () => void; - onCreateWorkspace: ( + onCreateTask: ( name: string, initialPrompt?: string, providerRuns?: ProviderRun[], @@ -51,16 +51,16 @@ interface WorkspaceModalProps { projectPath?: string; } -const WorkspaceModal: React.FC = ({ +const TaskModal: React.FC = ({ isOpen, onClose, - onCreateWorkspace, + onCreateTask, projectName, defaultBranch, existingNames = [], projectPath, }) => { - const [workspaceName, setWorkspaceName] = useState(''); + const [taskName, setTaskName] = useState(''); const [defaultProviderFromSettings, setDefaultProviderFromSettings] = useState(DEFAULT_PROVIDER); const [providerRuns, setProviderRuns] = useState([ @@ -124,19 +124,19 @@ const WorkspaceModal: React.FC = ({ }, [hasInitialPromptSupport]); const normalizedExisting = useMemo( - () => existingNames.map((n) => normalizeWorkspaceName(n)).filter(Boolean), + () => existingNames.map((n) => normalizeTaskName(n)).filter(Boolean), [existingNames] ); const validate = useCallback( (value: string): string | null => { - const normalized = normalizeWorkspaceName(value); + const normalized = normalizeTaskName(value); if (!normalized) return 'Please enter a Task name.'; if (normalizedExisting.includes(normalized)) { return 'A Task with this name already exists.'; } - if (normalized.length > MAX_WORKSPACE_NAME_LENGTH) { - return `Task name is too long (max ${MAX_WORKSPACE_NAME_LENGTH} characters).`; + if (normalized.length > MAX_TASK_NAME_LENGTH) { + return `Task name is too long (max ${MAX_TASK_NAME_LENGTH} characters).`; } return null; }, @@ -144,7 +144,7 @@ const WorkspaceModal: React.FC = ({ ); const onChange = (val: string) => { - setWorkspaceName(val); + setTaskName(val); setError(validate(val)); // Track that user has manually edited the name (not during initial setup) if (!isInitialSetupRef.current) { @@ -156,12 +156,12 @@ const WorkspaceModal: React.FC = ({ autoGeneratedName && val !== autoGeneratedName && val.trim() && - normalizeWorkspaceName(val) !== normalizeWorkspaceName(autoGeneratedName) + normalizeTaskName(val) !== normalizeTaskName(autoGeneratedName) ) { setCustomNameTracked(true); void (async () => { const { captureTelemetry } = await import('../lib/telemetryClient'); - captureTelemetry('workspace_custom_named', { custom_name: 'true' }); + captureTelemetry('task_custom_named', { custom_name: 'true' }); })(); } }; @@ -170,7 +170,7 @@ const WorkspaceModal: React.FC = ({ if (!isOpen) return; // Reset all form state on open - setWorkspaceName(''); + setTaskName(''); setAutoGeneratedName(''); setError(null); setTouched(false); @@ -195,9 +195,9 @@ const WorkspaceModal: React.FC = ({ // Generate auto-generated name synchronously to avoid race condition // Default assumption: autoGenerateName is true (matches DEFAULT_SETTINGS) - const suggested = generateFriendlyWorkspaceName(normalizedExisting); + const suggested = generateFriendlyTaskName(normalizedExisting); setAutoGeneratedName(suggested); - setWorkspaceName(suggested); + setTaskName(suggested); setError(validate(suggested)); // Mark initial setup as complete after state updates @@ -230,11 +230,11 @@ const WorkspaceModal: React.FC = ({ if (!autoGenerate) { // User disabled auto-generate, clear the name setAutoGeneratedName(''); - setWorkspaceName(''); + setTaskName(''); setError(null); } else { // Ensure auto-generated name is set (may have been cleared by user typing) - setWorkspaceName(suggested); + setTaskName(suggested); setError(validate(suggested)); } } @@ -406,7 +406,7 @@ const WorkspaceModal: React.FC = ({ onSubmit={(e) => { e.preventDefault(); setTouched(true); - const err = validate(workspaceName); + const err = validate(taskName); if (err) { setError(err); return; @@ -415,8 +415,8 @@ const WorkspaceModal: React.FC = ({ (async () => { try { const trimmedPrompt = initialPrompt.trim(); - await onCreateWorkspace( - normalizeWorkspaceName(workspaceName), + await onCreateTask( + normalizeTaskName(taskName), hasInitialPromptSupport && trimmedPrompt ? trimmedPrompt : undefined, providerRuns, selectedLinearIssue, @@ -426,7 +426,7 @@ const WorkspaceModal: React.FC = ({ ); onClose(); } catch (error) { - console.error('Failed to create workspace:', error); + console.error('Failed to create task:', error); } finally { setIsCreating(false); } @@ -435,27 +435,27 @@ const WorkspaceModal: React.FC = ({ className="space-y-4" >
    -
    - {workspaceName && ( + {taskName && (
    - {normalizeWorkspaceName(workspaceName)} + {normalizeTaskName(taskName)}
    )} @@ -488,7 +488,7 @@ const WorkspaceModal: React.FC = ({ if (wasClosed) { void (async () => { const { captureTelemetry } = await import('../lib/telemetryClient'); - captureTelemetry('workspace_advanced_options_opened'); + captureTelemetry('task_advanced_options_opened'); })(); } }} @@ -500,7 +500,7 @@ const WorkspaceModal: React.FC = ({ if (wasClosed) { void (async () => { const { captureTelemetry } = await import('../lib/telemetryClient'); - captureTelemetry('workspace_advanced_options_opened'); + captureTelemetry('task_advanced_options_opened'); })(); } } @@ -511,7 +511,7 @@ const WorkspaceModal: React.FC = ({ Advanced options - +
    {hasAutoApproveSupport ? (
    @@ -772,7 +772,7 @@ const WorkspaceModal: React.FC = ({
    - @@ -204,11 +204,11 @@ const WorkspaceTerminalPanelComponent: React.FC = ({ type="button" className={cn( 'rounded px-2 py-1 text-[11px] font-semibold transition-colors', - mode === 'workspace' + mode === 'task' ? 'bg-background text-foreground shadow-sm' : 'text-muted-foreground hover:bg-background/70' )} - onClick={() => setMode('workspace')} + onClick={() => setMode('task')} > Worktree @@ -278,12 +278,12 @@ const WorkspaceTerminalPanelComponent: React.FC = ({ captureTelemetry('terminal_new_terminal_created', { scope: mode }); })(); createTerminal({ - cwd: mode === 'global' ? projectPath : workspace?.path, + cwd: mode === 'global' ? projectPath : task?.path, }); }} className="ml-2 flex h-6 w-6 items-center justify-center rounded border border-transparent text-muted-foreground transition hover:border-border hover:bg-background dark:hover:bg-gray-800" - title={mode === 'global' ? 'New global terminal' : 'New workspace terminal'} - disabled={mode === 'workspace' && !workspace} + title={mode === 'global' ? 'New global terminal' : 'New task terminal'} + disabled={mode === 'task' && !task} > @@ -302,7 +302,7 @@ const WorkspaceTerminalPanelComponent: React.FC = ({ {terminals.map((terminal) => { const cwd = terminal.cwd || - (mode === 'global' ? projectPath || terminal.cwd : workspace?.path || terminal.cwd); + (mode === 'global' ? projectPath || terminal.cwd : task?.path || terminal.cwd); return (
    = ({
    ); }; -export const WorkspaceTerminalPanel = React.memo(WorkspaceTerminalPanelComponent); +export const TaskTerminalPanel = React.memo(TaskTerminalPanelComponent); -export default WorkspaceTerminalPanel; +export default TaskTerminalPanel; diff --git a/src/renderer/components/TerminalPane.tsx b/src/renderer/components/TerminalPane.tsx index ab5572b4..2db0cb6a 100644 --- a/src/renderer/components/TerminalPane.tsx +++ b/src/renderer/components/TerminalPane.tsx @@ -59,7 +59,7 @@ const TerminalPaneComponent: React.FC = ({ if (!container) return; const session = terminalSessionRegistry.attach({ - workspaceId: id, + taskId: id, container, cwd, shell, diff --git a/src/renderer/components/kanban/KanbanBoard.tsx b/src/renderer/components/kanban/KanbanBoard.tsx index e0498ac6..f662a246 100644 --- a/src/renderer/components/kanban/KanbanBoard.tsx +++ b/src/renderer/components/kanban/KanbanBoard.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import type { Project, Workspace } from '../../types/app'; +import type { Project, Task } from '../../types/app'; import KanbanColumn from './KanbanColumn'; import KanbanCard from './KanbanCard'; import { Button } from '../ui/button'; @@ -7,10 +7,10 @@ import { Inbox, Plus } from 'lucide-react'; import { getAll, setStatus, type KanbanStatus } from '../../lib/kanbanStore'; import { subscribeDerivedStatus, - watchWorkspacePty, - watchWorkspaceContainers, - watchWorkspaceActivity, -} from '../../lib/workspaceStatus'; + watchTaskPty, + watchTaskContainers, + watchTaskActivity, +} from '../../lib/taskStatus'; import { activityStore } from '../../lib/activityStore'; const order: KanbanStatus[] = ['todo', 'in-progress', 'done']; @@ -22,9 +22,9 @@ const titles: Record = { const KanbanBoard: React.FC<{ project: Project; - onOpenWorkspace?: (ws: Workspace) => void; - onCreateWorkspace?: () => void; -}> = ({ project, onOpenWorkspace, onCreateWorkspace }) => { + onOpenTask?: (ws: Task) => void; + onCreateTask?: () => void; +}> = ({ project, onOpenTask, onCreateTask }) => { const [statusMap, setStatusMap] = React.useState>({}); React.useEffect(() => { @@ -35,14 +35,14 @@ const KanbanBoard: React.FC<{ React.useEffect(() => { const offs: Array<() => void> = []; const idleTimers = new Map>(); - const wsList = project.workspaces || []; + const wsList = project.tasks || []; for (const ws of wsList) { // Watch PTY output to capture terminal-based providers as activity - offs.push(watchWorkspacePty(ws.id)); + offs.push(watchTaskPty(ws.id)); // Watch container run state as another activity source (build/start/ready) - offs.push(watchWorkspaceContainers(ws.id)); + offs.push(watchTaskContainers(ws.id)); // Watch app-wide activity classification (matches left sidebar spinner) - offs.push(watchWorkspaceActivity(ws.id)); + offs.push(watchTaskActivity(ws.id)); const off = subscribeDerivedStatus(ws.id, (derived) => { if (derived !== 'busy') return; setStatusMap((prev) => { @@ -79,7 +79,7 @@ const KanbanBoard: React.FC<{ offs.push(un); } - // Per-ws: when the PTY exits and workspace is not busy anymore, move to Done + // Per-task: when the PTY exits and task is not busy anymore, move to Done for (const ws of wsList) { try { const offExit = (window as any).electronAPI.onPtyExit?.( @@ -104,12 +104,12 @@ const KanbanBoard: React.FC<{ } return () => offs.forEach((f) => f()); // eslint-disable-next-line react-hooks/exhaustive-deps - }, [project.id, project.workspaces?.length]); + }, [project.id, project.tasks?.length]); - // Promote any workspace with local changes directly to "Ready for review" (done) + // Promote any task with local changes directly to "Ready for review" (done) React.useEffect(() => { let cancelled = false; - const wsList = project.workspaces || []; + const wsList = project.tasks || []; const check = async () => { for (const ws of wsList) { const variantPaths: string[] = (() => { @@ -145,7 +145,7 @@ const KanbanBoard: React.FC<{ }); } } catch { - // ignore per‑workspace errors + // ignore per-task errors } } }; @@ -155,12 +155,12 @@ const KanbanBoard: React.FC<{ cancelled = true; window.clearInterval(id); }; - }, [project.id, project.workspaces?.length]); + }, [project.id, project.tasks?.length]); - // Promote any workspace with an open PR to "Ready for review" (done) + // Promote any task with an open PR to "Ready for review" (done) React.useEffect(() => { let cancelled = false; - const wsList = project.workspaces || []; + const wsList = project.tasks || []; const check = async () => { for (const ws of wsList) { const variantPaths: string[] = (() => { @@ -175,7 +175,7 @@ const KanbanBoard: React.FC<{ try { let hasPr = false; for (const p of paths) { - const res = await (window as any).electronAPI?.getPrStatus?.({ workspacePath: p }); + const res = await (window as any).electronAPI?.getPrStatus?.({ taskPath: p }); if (res?.success && res?.pr) { hasPr = true; break; @@ -206,11 +206,11 @@ const KanbanBoard: React.FC<{ cancelled = true; window.clearInterval(id); }; - }, [project.id, project.workspaces?.length]); + }, [project.id, project.tasks?.length]); React.useEffect(() => { let cancelled = false; - const wsList = project.workspaces || []; + const wsList = project.tasks || []; const check = async () => { for (const ws of wsList) { const variantPaths: string[] = (() => { @@ -225,7 +225,7 @@ const KanbanBoard: React.FC<{ try { let ahead = 0; for (const p of paths) { - const res = await (window as any).electronAPI?.getBranchStatus?.({ workspacePath: p }); + const res = await (window as any).electronAPI?.getBranchStatus?.({ taskPath: p }); if (res?.success) { const a = Number(res?.ahead ?? 0); if (a > 0) { @@ -258,18 +258,18 @@ const KanbanBoard: React.FC<{ cancelled = true; window.clearInterval(id); }; - }, [project.id, project.workspaces?.length]); + }, [project.id, project.tasks?.length]); - const byStatus: Record = { todo: [], 'in-progress': [], done: [] }; - for (const ws of project.workspaces || []) { + const byStatus: Record = { todo: [], 'in-progress': [], done: [] }; + for (const ws of project.tasks || []) { const s = statusMap[ws.id] || 'todo'; byStatus[s].push(ws); } - const hasAny = (project.workspaces?.length ?? 0) > 0; + const hasAny = (project.tasks?.length ?? 0) > 0; - const handleDrop = (target: KanbanStatus, workspaceId: string) => { - setStatus(workspaceId, target); - setStatusMap({ ...statusMap, [workspaceId]: target }); + const handleDrop = (target: KanbanStatus, taskId: string) => { + setStatus(taskId, target); + setStatusMap({ ...statusMap, [taskId]: target }); }; return ( @@ -281,12 +281,12 @@ const KanbanBoard: React.FC<{ count={byStatus[s].length} onDropCard={(id) => handleDrop(s, id)} action={ - s === 'todo' && onCreateWorkspace ? ( + s === 'todo' && onCreateTask ? ( @@ -321,14 +321,14 @@ const KanbanBoard: React.FC<{ ) : ( <> {byStatus[s].map((ws) => ( - + ))} - {s === 'todo' && onCreateWorkspace ? ( + {s === 'todo' && onCreateTask ? (