Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -14,46 +14,23 @@ const workspaceInputSchema = z.object({
});

const schema = z.object({
projectId: z.string(),
workspaces: z.array(workspaceInputSchema).min(1).max(5),
});

interface CreatedWorkspace {
workspaceId: string;
workspaceName: string;
branch: string;
worktreePath: string;
wasExisting: boolean;
}

async function execute(
params: z.infer<typeof schema>,
ctx: ToolContext,
): Promise<CommandResult> {
// Derive projectId from current workspace or use the only available project
const workspaces = ctx.getWorkspaces();
if (!workspaces || workspaces.length === 0) {
return { success: false, error: "No workspaces available" };
}

// Try to get from current workspace first
let projectId: string | null = null;
const activeWorkspaceId = ctx.getActiveWorkspaceId();
if (activeWorkspaceId) {
const activeWorkspace = workspaces.find(
(ws) => ws.id === activeWorkspaceId,
);
if (activeWorkspace) {
projectId = activeWorkspace.projectId;
}
}

// Fall back to the most recently used workspace's project
if (!projectId) {
const sorted = [...workspaces].sort(
(a, b) => (b.lastOpenedAt ?? 0) - (a.lastOpenedAt ?? 0),
);
projectId = sorted[0].projectId;
}

const { projectId } = params;
const created: CreatedWorkspace[] = [];
const errors: BulkItemError[] = [];

Expand All @@ -70,6 +47,7 @@ async function execute(
workspaceId: result.workspace.id,
workspaceName: result.workspace.name,
branch: result.workspace.branch,
worktreePath: result.worktreePath,
wasExisting: result.wasExisting,
});
} catch (error) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,59 +5,42 @@ import type { CommandResult, ToolContext, ToolDefinition } from "./types";
const schema = z.object({
command: z.string(),
name: z.string(),
workspaceId: z.string(),
});

async function execute(
params: z.infer<typeof schema>,
ctx: ToolContext,
): Promise<CommandResult> {
// 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;
const workspace = workspaces.find((ws) => ws.id === params.workspaceId);
if (!workspace) {
return {
success: false,
error: `Workspace not found: ${params.workspaceId}`,
};
}

try {
// 2. Create workspace
const result = await ctx.createWorktree.mutateAsync({
projectId,
name: params.name,
branchName: params.name,
});

// 3. Append command to pending terminal setup
// Append command to pending terminal setup for the existing workspace
const store = useWorkspaceInitStore.getState();
const pending = store.pendingTerminalSetups[result.workspace.id];
const pending = store.pendingTerminalSetups[workspace.id];
store.addPendingTerminalSetup({
workspaceId: result.workspace.id,
projectId: pending?.projectId ?? projectId,
workspaceId: workspace.id,
projectId: pending?.projectId ?? workspace.projectId,
initialCommands: [...(pending?.initialCommands ?? []), params.command],
defaultPreset: pending?.defaultPreset ?? null,
});

return {
success: true,
data: {
workspaceId: result.workspace.id,
branch: result.workspace.branch,
workspaceId: workspace.id,
branch: workspace.branch,
},
};
} catch (error) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export function register(server: McpServer) {
"Create one or more workspaces (git worktrees) on a device. Use this when the user asks to create worktrees or workspaces.",
inputSchema: {
deviceId: z.string().describe("Target device ID"),
projectId: z.string().describe("Project ID to create workspaces in"),
workspaces: z
.array(workspaceInputSchema)
.min(1)
Expand All @@ -35,13 +36,19 @@ export function register(server: McpServer) {
async (args, extra) => {
const ctx = getMcpContext(extra);
const deviceId = args.deviceId as string;
const projectId = args.projectId as string;
const workspaces = args.workspaces as z.infer<
typeof workspaceInputSchema
>[];

if (!deviceId) {
if (!deviceId || !projectId) {
return {
content: [{ type: "text", text: "Error: deviceId is required" }],
content: [
{
type: "text",
text: "Error: deviceId and projectId are required",
},
],
isError: true,
};
}
Expand All @@ -50,7 +57,7 @@ export function register(server: McpServer) {
ctx,
deviceId,
tool: "create_workspace",
params: { workspaces },
params: { projectId, workspaces },
});
},
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,19 @@ async function fetchTask({
return task ?? null;
}

function validateArgs(args: Record<string, unknown>): {
function validateSessionArgs(args: Record<string, unknown>): {
deviceId: string;
taskId: string;
workspaceId: string;
} | null {
const deviceId = args.deviceId as string;
const taskId = args.taskId as string;
const workspaceId = args.workspaceId as string;
if (!deviceId || !taskId || !workspaceId) return null;
return { deviceId, taskId, workspaceId };
}

function validateSubagentArgs(args: Record<string, unknown>): {
deviceId: string;
taskId: string;
} | null {
Expand All @@ -91,9 +103,22 @@ function validateArgs(args: Record<string, unknown>): {
return { deviceId, taskId };
}

const ERROR_DEVICE_AND_TASK_REQUIRED = {
const ERROR_SESSION_ARGS_REQUIRED = {
content: [
{
type: "text" as const,
text: "Error: deviceId, taskId, and workspaceId are required",
},
],
isError: true,
};

const ERROR_SUBAGENT_ARGS_REQUIRED = {
content: [
{ type: "text" as const, text: "Error: deviceId and taskId are required" },
{
type: "text" as const,
text: "Error: deviceId and taskId are required",
},
],
isError: true,
};
Expand All @@ -108,16 +133,21 @@ export function register(server: McpServer) {
"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.",
"Start an autonomous Claude Code session for a task in an existing workspace. Launches Claude with the task context in the specified workspace.",
inputSchema: {
deviceId: z.string().describe("Target device ID"),
taskId: z.string().describe("Task ID to work on"),
workspaceId: z
.string()
.describe(
"Workspace ID to run the session in (from create_workspace)",
),
},
},
async (args, extra) => {
const ctx = getMcpContext(extra);
const validated = validateArgs(args);
if (!validated) return ERROR_DEVICE_AND_TASK_REQUIRED;
const validated = validateSessionArgs(args);
if (!validated) return ERROR_SESSION_ARGS_REQUIRED;

const task = await fetchTask({
taskId: validated.taskId,
Expand All @@ -129,7 +159,11 @@ export function register(server: McpServer) {
ctx,
deviceId: validated.deviceId,
tool: "start_claude_session",
params: { command: buildCommand(task), name: task.slug },
params: {
command: buildCommand(task),
name: task.slug,
workspaceId: validated.workspaceId,
},
});
},
);
Expand All @@ -146,8 +180,8 @@ export function register(server: McpServer) {
},
async (args, extra) => {
const ctx = getMcpContext(extra);
const validated = validateArgs(args);
if (!validated) return ERROR_DEVICE_AND_TASK_REQUIRED;
const validated = validateSubagentArgs(args);
if (!validated) return ERROR_SUBAGENT_ARGS_REQUIRED;

const task = await fetchTask({
taskId: validated.taskId,
Expand Down