diff --git a/apps/desktop/src/renderer/routes/_authenticated/components/AgentHooks/hooks/useCommandWatcher/tools/create-worktree.ts b/apps/desktop/src/renderer/routes/_authenticated/components/AgentHooks/hooks/useCommandWatcher/tools/create-worktree.ts index 6a7fead8074..4c2b5757371 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/components/AgentHooks/hooks/useCommandWatcher/tools/create-worktree.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/components/AgentHooks/hooks/useCommandWatcher/tools/create-worktree.ts @@ -1,12 +1,28 @@ import { z } from "zod"; -import type { CommandResult, ToolContext, ToolDefinition } from "./types"; +import type { + BulkItemError, + CommandResult, + ToolContext, + ToolDefinition, +} from "./types"; +import { buildBulkResult } from "./types"; -const schema = z.object({ +const workspaceInputSchema = z.object({ name: z.string().optional(), branchName: z.string().optional(), baseBranch: z.string().optional(), }); +const schema = z.object({ + workspaces: z.array(workspaceInputSchema).min(1).max(5), +}); + +interface CreatedWorkspace { + workspaceId: string; + workspaceName: string; + branch: string; +} + async function execute( params: z.infer, ctx: ToolContext, @@ -37,29 +53,41 @@ async function execute( projectId = sorted[0].projectId; } - try { - const result = await ctx.createWorktree.mutateAsync({ - projectId, - name: params.name, - branchName: params.branchName, - baseBranch: params.baseBranch, - }); + const created: CreatedWorkspace[] = []; + const errors: BulkItemError[] = []; + + for (const [i, input] of params.workspaces.entries()) { + try { + const result = await ctx.createWorktree.mutateAsync({ + projectId, + name: input.name, + branchName: input.branchName, + baseBranch: input.baseBranch, + }); - return { - success: true, - data: { + created.push({ workspaceId: result.workspace.id, workspaceName: result.workspace.name, branch: result.workspace.branch, - }, - }; - } catch (error) { - return { - success: false, - error: - error instanceof Error ? error.message : "Failed to create workspace", - }; + }); + } catch (error) { + errors.push({ + index: i, + name: input.name, + branchName: input.branchName, + error: + error instanceof Error ? error.message : "Failed to create workspace", + }); + } } + + return buildBulkResult({ + items: created, + errors, + itemKey: "created", + allFailedMessage: "All workspace creations failed", + total: params.workspaces.length, + }); } export const createWorkspace: ToolDefinition = { diff --git a/apps/desktop/src/renderer/routes/_authenticated/components/AgentHooks/hooks/useCommandWatcher/tools/delete-workspace.ts b/apps/desktop/src/renderer/routes/_authenticated/components/AgentHooks/hooks/useCommandWatcher/tools/delete-workspace.ts index faaeb8b77a2..e34a1c5fc3c 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/components/AgentHooks/hooks/useCommandWatcher/tools/delete-workspace.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/components/AgentHooks/hooks/useCommandWatcher/tools/delete-workspace.ts @@ -1,31 +1,59 @@ import { z } from "zod"; -import type { CommandResult, ToolContext, ToolDefinition } from "./types"; +import type { + BulkItemError, + CommandResult, + ToolContext, + ToolDefinition, +} from "./types"; +import { buildBulkResult } from "./types"; const schema = z.object({ - workspaceId: z.string(), + workspaceIds: z.array(z.string().uuid()).min(1).max(5), }); +interface DeletedWorkspace { + workspaceId: string; +} + async function execute( params: z.infer, ctx: ToolContext, ): Promise { - try { - const result = await ctx.deleteWorkspace.mutateAsync({ - id: params.workspaceId, - }); + const deleted: DeletedWorkspace[] = []; + const errors: BulkItemError[] = []; - if (!result.success) { - return { success: false, error: result.error ?? "Delete failed" }; - } + for (const [i, workspaceId] of params.workspaceIds.entries()) { + try { + const result = await ctx.deleteWorkspace.mutateAsync({ + id: workspaceId, + }); - return { success: true, data: { workspaceId: params.workspaceId } }; - } catch (error) { - return { - success: false, - error: - error instanceof Error ? error.message : "Failed to delete workspace", - }; + if (!result.success) { + errors.push({ + index: i, + workspaceId, + error: result.error ?? "Delete failed", + }); + } else { + deleted.push({ workspaceId }); + } + } catch (error) { + errors.push({ + index: i, + workspaceId, + error: + error instanceof Error ? error.message : "Failed to delete workspace", + }); + } } + + return buildBulkResult({ + items: deleted, + errors, + itemKey: "deleted", + allFailedMessage: "All workspace deletions failed", + total: params.workspaceIds.length, + }); } export const deleteWorkspace: ToolDefinition = { diff --git a/apps/desktop/src/renderer/routes/_authenticated/components/AgentHooks/hooks/useCommandWatcher/tools/index.ts b/apps/desktop/src/renderer/routes/_authenticated/components/AgentHooks/hooks/useCommandWatcher/tools/index.ts index e114b8304b1..68742302ef4 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/components/AgentHooks/hooks/useCommandWatcher/tools/index.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/components/AgentHooks/hooks/useCommandWatcher/tools/index.ts @@ -8,6 +8,7 @@ import { startClaudeSession } from "./start-claude-session"; import { startClaudeSubagent } from "./start-claude-subagent"; import { switchWorkspace } from "./switch-workspace"; import type { CommandResult, ToolContext, ToolDefinition } from "./types"; +import { updateWorkspace } from "./update-workspace"; // Registry of all available tools // biome-ignore lint/suspicious/noExplicitAny: Tool schemas vary @@ -21,6 +22,7 @@ const tools: ToolDefinition[] = [ startClaudeSession, startClaudeSubagent, switchWorkspace, + updateWorkspace, ]; // Map for O(1) lookup by name diff --git a/apps/desktop/src/renderer/routes/_authenticated/components/AgentHooks/hooks/useCommandWatcher/tools/types.ts b/apps/desktop/src/renderer/routes/_authenticated/components/AgentHooks/hooks/useCommandWatcher/tools/types.ts index 5bccfd63f23..4051352f100 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/components/AgentHooks/hooks/useCommandWatcher/tools/types.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/components/AgentHooks/hooks/useCommandWatcher/tools/types.ts @@ -8,6 +8,37 @@ export interface CommandResult { error?: string; } +export interface BulkItemError { + index: number; + error: string; + [key: string]: unknown; +} + +export function buildBulkResult({ + items, + errors, + itemKey, + allFailedMessage, + total, +}: { + items: T[]; + errors: BulkItemError[]; + itemKey: string; + allFailedMessage: string; + total: number; +}): CommandResult { + const data: Record = { + [itemKey]: items, + summary: { total, succeeded: items.length, failed: errors.length }, + }; + if (errors.length > 0) data.errors = errors; + return { + success: items.length > 0, + data, + error: items.length === 0 ? allFailedMessage : undefined, + }; +} + // Available mutations and queries passed to tool handlers export interface ToolContext { // Mutations @@ -16,6 +47,9 @@ export interface ToolContext { deleteWorkspace: ReturnType< typeof electronTrpc.workspaces.delete.useMutation >; + updateWorkspace: ReturnType< + typeof electronTrpc.workspaces.update.useMutation + >; // Query helpers refetchWorkspaces: () => Promise; getWorkspaces: () => SelectWorkspace[] | undefined; diff --git a/apps/desktop/src/renderer/routes/_authenticated/components/AgentHooks/hooks/useCommandWatcher/tools/update-workspace.ts b/apps/desktop/src/renderer/routes/_authenticated/components/AgentHooks/hooks/useCommandWatcher/tools/update-workspace.ts new file mode 100644 index 00000000000..2a6dd12d98b --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/components/AgentHooks/hooks/useCommandWatcher/tools/update-workspace.ts @@ -0,0 +1,65 @@ +import { z } from "zod"; +import type { + BulkItemError, + CommandResult, + ToolContext, + ToolDefinition, +} from "./types"; +import { buildBulkResult } from "./types"; + +const workspaceUpdateSchema = z.object({ + workspaceId: z.string().uuid(), + name: z.string().min(1), +}); + +const schema = z.object({ + updates: z.array(workspaceUpdateSchema).min(1).max(5), +}); + +interface UpdatedWorkspace { + workspaceId: string; + name: string; +} + +async function execute( + params: z.infer, + ctx: ToolContext, +): Promise { + const updated: UpdatedWorkspace[] = []; + const errors: BulkItemError[] = []; + + for (const [i, update] of params.updates.entries()) { + try { + await ctx.updateWorkspace.mutateAsync({ + id: update.workspaceId, + patch: { name: update.name }, + }); + + updated.push({ + workspaceId: update.workspaceId, + name: update.name, + }); + } catch (error) { + errors.push({ + index: i, + workspaceId: update.workspaceId, + error: + error instanceof Error ? error.message : "Failed to update workspace", + }); + } + } + + return buildBulkResult({ + items: updated, + errors, + itemKey: "updated", + allFailedMessage: "All workspace updates failed", + total: params.updates.length, + }); +} + +export const updateWorkspace: ToolDefinition = { + name: "update_workspace", + schema, + execute, +}; diff --git a/apps/desktop/src/renderer/routes/_authenticated/components/AgentHooks/hooks/useCommandWatcher/useCommandWatcher.ts b/apps/desktop/src/renderer/routes/_authenticated/components/AgentHooks/hooks/useCommandWatcher/useCommandWatcher.ts index 7eccb303ccf..ab42178ff6b 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/components/AgentHooks/hooks/useCommandWatcher/useCommandWatcher.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/components/AgentHooks/hooks/useCommandWatcher/useCommandWatcher.ts @@ -6,6 +6,7 @@ import { authClient } from "renderer/lib/auth-client"; import { electronTrpc } from "renderer/lib/electron-trpc"; import { useCreateWorkspace } from "renderer/react-query/workspaces/useCreateWorkspace"; import { useDeleteWorkspace } from "renderer/react-query/workspaces/useDeleteWorkspace"; +import { useUpdateWorkspace } from "renderer/react-query/workspaces/useUpdateWorkspace"; import { navigateToWorkspace } from "renderer/routes/_authenticated/_dashboard/utils/workspace-navigation"; import { useCollections } from "renderer/routes/_authenticated/providers/CollectionsProvider/CollectionsProvider"; import { executeTool, type ToolContext } from "./tools"; @@ -24,6 +25,7 @@ export function useCommandWatcher() { const createWorktree = useCreateWorkspace({ skipNavigation: true }); const setActive = electronTrpc.workspaces.setActive.useMutation(); const deleteWorkspace = useDeleteWorkspace(); + const updateWorkspace = useUpdateWorkspace(); const { data: workspaces, refetch: refetchWorkspaces } = electronTrpc.workspaces.getAll.useQuery(); @@ -41,6 +43,7 @@ export function useCommandWatcher() { createWorktree, setActive, deleteWorkspace, + updateWorkspace, refetchWorkspaces: async () => refetchWorkspaces(), getWorkspaces: () => workspaces, getProjects: () => projects, @@ -52,6 +55,7 @@ export function useCommandWatcher() { createWorktree, setActive, deleteWorkspace, + updateWorkspace, refetchWorkspaces, workspaces, projects, diff --git a/packages/mcp/src/tools/devices/create-workspace/create-workspace.ts b/packages/mcp/src/tools/devices/create-workspace/create-workspace.ts index ebfbf43fe5f..9dd55bd4172 100644 --- a/packages/mcp/src/tools/devices/create-workspace/create-workspace.ts +++ b/packages/mcp/src/tools/devices/create-workspace/create-workspace.ts @@ -2,34 +2,41 @@ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { z } from "zod"; import { executeOnDevice, getMcpContext } from "../../utils"; +const workspaceInputSchema = z.object({ + name: z + .string() + .optional() + .describe("Workspace name (auto-generated if not provided)"), + branchName: z + .string() + .optional() + .describe("Branch name (auto-generated if not provided)"), + baseBranch: z + .string() + .optional() + .describe("Branch to create from (defaults to main)"), +}); + export function register(server: McpServer) { server.registerTool( "create_workspace", { - description: "Create a new git worktree workspace", + description: "Create one or more git worktree workspaces on a device", inputSchema: { deviceId: z.string().describe("Target device ID"), - name: z - .string() - .optional() - .describe("Workspace name (auto-generated if not provided)"), - branchName: z - .string() - .optional() - .describe("Branch name (auto-generated if not provided)"), - baseBranch: z - .string() - .optional() - .describe("Branch to create from (defaults to main)"), - taskId: z - .string() - .optional() - .describe("Task ID to associate with workspace"), + workspaces: z + .array(workspaceInputSchema) + .min(1) + .max(5) + .describe("Array of workspaces to create (1-5)"), }, }, async (args, extra) => { const ctx = getMcpContext(extra); const deviceId = args.deviceId as string; + const workspaces = args.workspaces as z.infer< + typeof workspaceInputSchema + >[]; if (!deviceId) { return { @@ -42,12 +49,7 @@ export function register(server: McpServer) { ctx, deviceId, tool: "create_workspace", - params: { - name: args.name, - branchName: args.branchName, - baseBranch: args.baseBranch, - taskId: args.taskId, - }, + params: { workspaces }, }); }, ); diff --git a/packages/mcp/src/tools/devices/delete-workspace/delete-workspace.ts b/packages/mcp/src/tools/devices/delete-workspace/delete-workspace.ts index cfe639a7f1e..d1eea3e9ecb 100644 --- a/packages/mcp/src/tools/devices/delete-workspace/delete-workspace.ts +++ b/packages/mcp/src/tools/devices/delete-workspace/delete-workspace.ts @@ -6,16 +6,20 @@ export function register(server: McpServer) { server.registerTool( "delete_workspace", { - description: "Delete a workspace", + description: "Delete one or more workspaces on a device", inputSchema: { deviceId: z.string().describe("Target device ID"), - workspaceId: z.string().uuid().describe("Workspace ID to delete"), + workspaceIds: z + .array(z.string().uuid()) + .min(1) + .max(5) + .describe("Array of workspace IDs to delete (1-5)"), }, }, async (args, extra) => { const ctx = getMcpContext(extra); const deviceId = args.deviceId as string; - const workspaceId = args.workspaceId as string; + const workspaceIds = args.workspaceIds as string[]; if (!deviceId) { return { @@ -28,7 +32,7 @@ export function register(server: McpServer) { ctx, deviceId, tool: "delete_workspace", - params: { workspaceId }, + params: { workspaceIds }, }); }, ); diff --git a/packages/mcp/src/tools/devices/update-workspace/index.ts b/packages/mcp/src/tools/devices/update-workspace/index.ts new file mode 100644 index 00000000000..532d18f466c --- /dev/null +++ b/packages/mcp/src/tools/devices/update-workspace/index.ts @@ -0,0 +1 @@ +export { register } from "./update-workspace"; diff --git a/packages/mcp/src/tools/devices/update-workspace/update-workspace.ts b/packages/mcp/src/tools/devices/update-workspace/update-workspace.ts new file mode 100644 index 00000000000..cf3d067dc75 --- /dev/null +++ b/packages/mcp/src/tools/devices/update-workspace/update-workspace.ts @@ -0,0 +1,45 @@ +import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { z } from "zod"; +import { executeOnDevice, getMcpContext } from "../../utils"; + +const workspaceUpdateSchema = z.object({ + workspaceId: z.string().uuid().describe("Workspace ID to update"), + name: z.string().min(1).describe("New workspace name"), +}); + +export function register(server: McpServer) { + server.registerTool( + "update_workspace", + { + description: + "Update one or more workspaces on a device. Currently supports renaming.", + inputSchema: { + deviceId: z.string().describe("Target device ID"), + updates: z + .array(workspaceUpdateSchema) + .min(1) + .max(5) + .describe("Array of workspace updates (1-5)"), + }, + }, + async (args, extra) => { + const ctx = getMcpContext(extra); + const deviceId = args.deviceId as string; + const updates = args.updates as z.infer[]; + + if (!deviceId) { + return { + content: [{ type: "text", text: "Error: deviceId is required" }], + isError: true, + }; + } + + return executeOnDevice({ + ctx, + deviceId, + tool: "update_workspace", + params: { updates }, + }); + }, + ); +} diff --git a/packages/mcp/src/tools/index.ts b/packages/mcp/src/tools/index.ts index 0e5053d24fb..8d05dd29c26 100644 --- a/packages/mcp/src/tools/index.ts +++ b/packages/mcp/src/tools/index.ts @@ -8,6 +8,7 @@ import { register as listWorkspaces } from "./devices/list-workspaces"; import { register as navigateToWorkspace } from "./devices/navigate-to-workspace"; import { register as startClaudeSession } from "./devices/start-claude-session"; import { register as switchWorkspace } from "./devices/switch-workspace"; +import { register as updateWorkspace } from "./devices/update-workspace"; import { register as listMembers } from "./organizations/list-members"; import { register as createTask } from "./tasks/create-task"; import { register as deleteTask } from "./tasks/delete-task"; @@ -32,6 +33,7 @@ const allTools = [ createWorkspace, switchWorkspace, deleteWorkspace, + updateWorkspace, startClaudeSession, ];