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 ad5eec98c42..e114b8304b1 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 @@ -4,6 +4,8 @@ import { getAppContext } from "./get-app-context"; import { listProjects } from "./list-projects"; import { listWorkspaces } from "./list-workspaces"; import { navigateToWorkspace } from "./navigate-to-workspace"; +import { startClaudeSession } from "./start-claude-session"; +import { startClaudeSubagent } from "./start-claude-subagent"; import { switchWorkspace } from "./switch-workspace"; import type { CommandResult, ToolContext, ToolDefinition } from "./types"; @@ -16,6 +18,8 @@ const tools: ToolDefinition[] = [ listProjects, listWorkspaces, navigateToWorkspace, + startClaudeSession, + startClaudeSubagent, switchWorkspace, ]; diff --git a/apps/desktop/src/renderer/routes/_authenticated/components/AgentHooks/hooks/useCommandWatcher/tools/start-claude-session.ts b/apps/desktop/src/renderer/routes/_authenticated/components/AgentHooks/hooks/useCommandWatcher/tools/start-claude-session.ts new file mode 100644 index 00000000000..d8e1201f08d --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/components/AgentHooks/hooks/useCommandWatcher/tools/start-claude-session.ts @@ -0,0 +1,81 @@ +import { useWorkspaceInitStore } from "renderer/stores/workspace-init"; +import { z } from "zod"; +import type { CommandResult, ToolContext, ToolDefinition } from "./types"; + +const schema = z.object({ + command: z.string(), + name: z.string(), +}); + +async function execute( + params: z.infer, + ctx: ToolContext, +): Promise { + // 1. Derive projectId from current workspace or most recent + const workspaces = ctx.getWorkspaces(); + if (!workspaces || workspaces.length === 0) { + return { success: false, error: "No workspaces available" }; + } + + let projectId: string | null = null; + const activeWorkspaceId = ctx.getActiveWorkspaceId(); + if (activeWorkspaceId) { + const activeWorkspace = workspaces.find( + (ws) => ws.id === activeWorkspaceId, + ); + if (activeWorkspace) { + projectId = activeWorkspace.projectId; + } + } + + if (!projectId) { + const sorted = [...workspaces].sort( + (a, b) => (b.lastOpenedAt ?? 0) - (a.lastOpenedAt ?? 0), + ); + projectId = sorted[0].projectId; + } + + try { + // 2. Create workspace + const result = await ctx.createWorktree.mutateAsync({ + projectId, + name: params.name, + branchName: params.name, + }); + + // 3. Append command to pending terminal setup + const store = useWorkspaceInitStore.getState(); + const pending = store.pendingTerminalSetups[result.workspace.id]; + store.addPendingTerminalSetup({ + workspaceId: result.workspace.id, + projectId: pending?.projectId ?? projectId, + initialCommands: [...(pending?.initialCommands ?? []), params.command], + defaultPreset: pending?.defaultPreset ?? null, + }); + + // 4. Navigate + await ctx.navigateToWorkspace(result.workspace.id); + + return { + success: true, + data: { + workspaceId: result.workspace.id, + branch: result.workspace.branch, + }, + }; + } catch (error) { + return { + success: false, + error: + error instanceof Error + ? error.message + : "Failed to start Claude session", + }; + } +} + +export const startClaudeSession: ToolDefinition = { + name: "start_claude_session", + schema, + execute, +}; diff --git a/apps/desktop/src/renderer/routes/_authenticated/components/AgentHooks/hooks/useCommandWatcher/tools/start-claude-subagent.ts b/apps/desktop/src/renderer/routes/_authenticated/components/AgentHooks/hooks/useCommandWatcher/tools/start-claude-subagent.ts new file mode 100644 index 00000000000..40770300d27 --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/components/AgentHooks/hooks/useCommandWatcher/tools/start-claude-subagent.ts @@ -0,0 +1,42 @@ +import { useTabsStore } from "renderer/stores/tabs/store"; +import { z } from "zod"; +import type { CommandResult, ToolContext, ToolDefinition } from "./types"; + +const schema = z.object({ + command: z.string(), +}); + +async function execute( + params: z.infer, + ctx: ToolContext, +): Promise { + const activeWorkspaceId = ctx.getActiveWorkspaceId(); + if (!activeWorkspaceId) { + return { success: false, error: "No active workspace" }; + } + + const tabsStore = useTabsStore.getState(); + const activeTabId = tabsStore.activeTabIds[activeWorkspaceId]; + if (!activeTabId) { + return { success: false, error: "No active tab in workspace" }; + } + + const paneId = tabsStore.addPane(activeTabId, { + initialCommands: [params.command], + }); + + if (!paneId) { + return { success: false, error: "Failed to add pane" }; + } + + return { + success: true, + data: { workspaceId: activeWorkspaceId, paneId }, + }; +} + +export const startClaudeSubagent: ToolDefinition = { + name: "start_claude_subagent", + schema, + execute, +}; diff --git a/packages/mcp/src/tools/devices/start-claude-session/index.ts b/packages/mcp/src/tools/devices/start-claude-session/index.ts new file mode 100644 index 00000000000..4a758bd261c --- /dev/null +++ b/packages/mcp/src/tools/devices/start-claude-session/index.ts @@ -0,0 +1 @@ +export { register } from "./start-claude-session"; diff --git a/packages/mcp/src/tools/devices/start-claude-session/start-claude-session.ts b/packages/mcp/src/tools/devices/start-claude-session/start-claude-session.ts new file mode 100644 index 00000000000..02a145cdbf3 --- /dev/null +++ b/packages/mcp/src/tools/devices/start-claude-session/start-claude-session.ts @@ -0,0 +1,164 @@ +import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { db } from "@superset/db/client"; +import { taskStatuses, tasks } from "@superset/db/schema"; +import { and, eq, isNull } from "drizzle-orm"; +import { alias } from "drizzle-orm/pg-core"; +import { z } from "zod"; +import { executeOnDevice, getMcpContext } from "../../utils"; + +function buildCommand( + task: NonNullable>>, +): string { + const metadata = [ + `Priority: ${task.priority}`, + task.statusName && `Status: ${task.statusName}`, + task.labels?.length && `Labels: ${task.labels.join(", ")}`, + ] + .filter(Boolean) + .join("\n"); + + const prompt = `You are working on task "${task.title}" (${task.slug}). + +${metadata} + +## Task Description + +${task.description || "No description provided."} + +## Instructions + +You are running fully autonomously. Do not ask questions or wait for user feedback — make all decisions independently based on the codebase and task description. + +1. Explore the codebase to understand the relevant code and architecture +2. Create a detailed execution plan for this task including: + - Purpose and scope of the changes + - Key assumptions + - Concrete implementation steps with specific files to modify + - How to validate the changes work correctly +3. Implement the plan +4. Verify your changes work correctly (run relevant tests, typecheck, lint) +5. When done, use the Superset MCP \`update_task\` tool to update task "${task.id}" with a summary of what was done`; + + return [ + "claude --dangerously-skip-permissions \"$(cat <<'SUPERSET_PROMPT'", + prompt, + "SUPERSET_PROMPT", + ')"', + ].join("\n"); +} + +async function fetchTask({ + taskId, + organizationId, +}: { + taskId: string; + organizationId: string; +}) { + const status = alias(taskStatuses, "status"); + const [task] = await db + .select({ + id: tasks.id, + slug: tasks.slug, + title: tasks.title, + description: tasks.description, + priority: tasks.priority, + statusName: status.name, + labels: tasks.labels, + }) + .from(tasks) + .leftJoin(status, eq(tasks.statusId, status.id)) + .where( + and( + eq(tasks.id, taskId), + eq(tasks.organizationId, organizationId), + isNull(tasks.deletedAt), + ), + ) + .limit(1); + + return task ?? null; +} + +function validateArgs(args: Record): { + deviceId: string; + taskId: string; +} | null { + const deviceId = args.deviceId as string; + const taskId = args.taskId as string; + if (!deviceId || !taskId) return null; + return { deviceId, taskId }; +} + +const ERROR_DEVICE_AND_TASK_REQUIRED = { + content: [ + { type: "text" as const, text: "Error: deviceId and taskId are required" }, + ], + isError: true, +}; + +const ERROR_TASK_NOT_FOUND = { + content: [{ type: "text" as const, text: "Error: Task not found" }], + isError: true, +}; + +export function register(server: McpServer) { + server.registerTool( + "start_claude_session", + { + description: + "Start an autonomous Claude Code session for a task. Creates a new workspace with its own git branch and launches Claude with the task context.", + inputSchema: { + deviceId: z.string().describe("Target device ID"), + taskId: z.string().describe("Task ID to work on"), + }, + }, + async (args, extra) => { + const ctx = getMcpContext(extra); + const validated = validateArgs(args); + if (!validated) return ERROR_DEVICE_AND_TASK_REQUIRED; + + const task = await fetchTask({ + taskId: validated.taskId, + organizationId: ctx.organizationId, + }); + if (!task) return ERROR_TASK_NOT_FOUND; + + return executeOnDevice({ + ctx, + deviceId: validated.deviceId, + tool: "start_claude_session", + params: { command: buildCommand(task), name: task.slug }, + }); + }, + ); + + server.registerTool( + "start_claude_subagent", + { + description: + "Start a Claude Code subagent for a task in an existing workspace. Adds a new terminal pane to the active workspace instead of creating a new one. Use this when you want to run Claude alongside your current work.", + inputSchema: { + deviceId: z.string().describe("Target device ID"), + taskId: z.string().describe("Task ID to work on"), + }, + }, + async (args, extra) => { + const ctx = getMcpContext(extra); + const validated = validateArgs(args); + if (!validated) return ERROR_DEVICE_AND_TASK_REQUIRED; + + const task = await fetchTask({ + taskId: validated.taskId, + organizationId: ctx.organizationId, + }); + if (!task) return ERROR_TASK_NOT_FOUND; + + return executeOnDevice({ + ctx, + deviceId: validated.deviceId, + tool: "start_claude_subagent", + params: { command: buildCommand(task) }, + }); + }, + ); +} diff --git a/packages/mcp/src/tools/index.ts b/packages/mcp/src/tools/index.ts index baaaf65dc6d..0e5053d24fb 100644 --- a/packages/mcp/src/tools/index.ts +++ b/packages/mcp/src/tools/index.ts @@ -6,6 +6,7 @@ import { register as listDevices } from "./devices/list-devices"; import { register as listProjects } from "./devices/list-projects"; 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 listMembers } from "./organizations/list-members"; import { register as createTask } from "./tasks/create-task"; @@ -31,6 +32,7 @@ const allTools = [ createWorkspace, switchWorkspace, deleteWorkspace, + startClaudeSession, ]; export function registerTools(server: McpServer) {