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
@@ -0,0 +1,36 @@
import type { CommandStatus } from "@superset/db/schema";
import { apiTrpcClient } from "renderer/lib/api-trpc-client";

export async function persistCommandStatus(params: {
id: string;
status: CommandStatus;
claimedBy?: string;
claimedAt?: Date;
result?: Record<string, unknown>;
error?: string;
executedAt?: Date;
}): Promise<void> {
const delays = [500, 1000, 2000];
let lastError: unknown;

for (let attempt = 0; attempt <= delays.length; attempt++) {
try {
await apiTrpcClient.agent.updateCommand.mutate(params);
return;
} catch (err) {
lastError = err;
console.warn(
`[persistCommandStatus] Attempt ${attempt + 1} failed for ${params.id}:`,
err,
);
if (attempt < delays.length) {
await new Promise((resolve) => setTimeout(resolve, delays[attempt]));
}
}
}

console.error(
`[persistCommandStatus] All retries exhausted for ${params.id}:`,
lastError,
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { useTabsStore } from "renderer/stores/tabs/store";
import { z } from "zod";
import type { CommandResult, ToolContext, ToolDefinition } from "./types";

const schema = z.object({
workspaceId: z.string(),
});

async function execute(
params: z.infer<typeof schema>,
ctx: ToolContext,
): Promise<CommandResult> {
const workspaces = ctx.getWorkspaces();
if (!workspaces || workspaces.length === 0) {
return { success: false, error: "No workspaces available" };
}

const workspace = workspaces.find((ws) => ws.id === params.workspaceId);
if (!workspace) {
return {
success: false,
error: `Workspace not found: ${params.workspaceId}`,
};
}

const tabsStore = useTabsStore.getState();
const activeTabId = tabsStore.activeTabIds[workspace.id] ?? null;
const workspaceTabs = tabsStore.tabs.filter(
(t) => t.workspaceId === workspace.id,
);

const tabs = workspaceTabs.map((tab) => {
const tabPanes = Object.entries(tabsStore.panes)
.filter(([, pane]) => pane.tabId === tab.id)
.map(([id, pane]) => ({
id,
type: pane.type,
name: pane.name,
status: pane.status ?? "idle",
}));

return {
id: tab.id,
name: tab.userTitle ?? tab.name,
isActive: tab.id === activeTabId,
panes: tabPanes,
};
});

return {
success: true,
data: {
workspace: {
id: workspace.id,
name: workspace.name,
branch: workspace.branch,
projectId: workspace.projectId,
},
activeTabId,
tabs,
},
};
}

export const getWorkspaceDetails: ToolDefinition<typeof schema> = {
name: "get_workspace_details",
schema,
execute,
};
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { createWorkspace } from "./create-worktree";
import { deleteWorkspace } from "./delete-workspace";
import { getAppContext } from "./get-app-context";
import { getWorkspaceDetails } from "./get-workspace-details";
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";
import { updateWorkspace } from "./update-workspace";
Expand All @@ -16,11 +16,11 @@ const tools: ToolDefinition<any>[] = [
createWorkspace,
deleteWorkspace,
getAppContext,
getWorkspaceDetails,
listProjects,
listWorkspaces,
navigateToWorkspace,
startClaudeSession,
startClaudeSubagent,
switchWorkspace,
updateWorkspace,
];
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { useTabsStore } from "renderer/stores/tabs/store";
import { useWorkspaceInitStore } from "renderer/stores/workspace-init";
import { z } from "zod";
import type { CommandResult, ToolContext, ToolDefinition } from "./types";
Expand All @@ -6,6 +7,7 @@ const schema = z.object({
command: z.string(),
name: z.string(),
workspaceId: z.string(),
paneId: z.string().optional(),
});

async function execute(
Expand All @@ -26,7 +28,31 @@ async function execute(
}

try {
// Append command to pending terminal setup for the existing workspace
if (params.paneId) {
const tabsStore = useTabsStore.getState();
const pane = tabsStore.panes[params.paneId];
if (!pane) {
return {
success: false,
error: `Pane not found: ${params.paneId}`,
};
}

const newPaneId = tabsStore.addPane(pane.tabId, {
initialCommands: [params.command],
});

if (!newPaneId) {
return { success: false, error: "Failed to add pane" };
}

return {
success: true,
data: { workspaceId: workspace.id, paneId: newPaneId },
};
}

// Without paneId: init workspace path
const store = useWorkspaceInitStore.getState();
const pending = store.pendingTerminalSetups[workspace.id];
store.addPendingTerminalSetup({
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { useDeleteWorkspace } from "renderer/react-query/workspaces/useDeleteWor
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 { persistCommandStatus } from "./persistCommandStatus";
import { executeTool, type ToolContext } from "./tools";

/** Tracks command IDs that have been or are being processed to prevent duplicate execution. */
Expand Down Expand Up @@ -85,31 +86,36 @@ export function useCommandWatcher() {
handledCommands.add(commandId);
console.log(`[command-watcher] Processing: ${commandId} (${tool})`);

const claimedAt = new Date();
try {
collections.agentCommands.update(commandId, (draft) => {
draft.status = "claimed";
draft.claimedBy = deviceInfo?.deviceId ?? null;
draft.claimedAt = new Date();
draft.claimedAt = claimedAt;
});

await new Promise((resolve) => setTimeout(resolve, 100));

collections.agentCommands.update(commandId, (draft) => {
draft.status = "executing";
});

const result = await executeTool(tool, params, toolContext);

await new Promise((resolve) => setTimeout(resolve, 100));

if (result.success) {
const executedAt = new Date();
collections.agentCommands.update(commandId, (draft) => {
draft.status = "completed";
draft.result = result.data ?? {};
draft.executedAt = new Date();
draft.executedAt = executedAt;
});
await persistCommandStatus({
id: commandId,
status: "completed",
claimedBy: deviceInfo?.deviceId,
claimedAt,
result: result.data ?? {},
executedAt,
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.
} else {
// Include per-item errors from bulk operations in the error message
const itemErrors = (
result.data?.errors as Array<{ error: string }> | undefined
)
Expand All @@ -119,24 +125,43 @@ export function useCommandWatcher() {
? `${result.error ?? "Unknown error"}: ${itemErrors}`
: (result.error ?? "Unknown error");

const executedAt = new Date();
collections.agentCommands.update(commandId, (draft) => {
draft.status = "failed";
draft.error = fullError;
draft.executedAt = new Date();
draft.executedAt = executedAt;
});
console.error(
`[command-watcher] Failed: ${commandId}`,
fullError,
result.data,
);
await persistCommandStatus({
id: commandId,
status: "failed",
claimedBy: deviceInfo?.deviceId,
claimedAt,
error: fullError,
executedAt,
});
}
} catch (error) {
console.error(`[command-watcher] Error: ${commandId}`, error);
const errorMsg =
error instanceof Error ? error.message : "Execution error";
const executedAt = new Date();
collections.agentCommands.update(commandId, (draft) => {
draft.status = "failed";
draft.error =
error instanceof Error ? error.message : "Execution error";
draft.executedAt = new Date();
draft.error = errorMsg;
draft.executedAt = executedAt;
});
await persistCommandStatus({
id: commandId,
status: "failed",
claimedBy: deviceInfo?.deviceId,
claimedAt,
error: errorMsg,
executedAt,
});
}
},
Expand Down Expand Up @@ -169,6 +194,11 @@ export function useCommandWatcher() {
draft.status = "timeout";
draft.error = "Command expired before execution";
});
persistCommandStatus({
id: cmd.id,
status: "timeout",
error: "Command expired before execution",
});
return false;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -219,22 +219,6 @@ function createOrgCollections(organizationId: string): OrgCollections {
columnMapper,
},
getKey: (item) => item.id,
onUpdate: async ({ transaction }) => {
const { original, changes } = transaction.mutations[0];
if (!changes.status) {
return { txid: Date.now() };
}
const result = await apiClient.agent.updateCommand.mutate({
id: original.id,
status: changes.status,
claimedBy: changes.claimedBy ?? undefined,
claimedAt: changes.claimedAt ?? undefined,
result: changes.result ?? undefined,
error: changes.error ?? undefined,
executedAt: changes.executedAt ?? undefined,
});
return { txid: Number(result.txid) };
},
}),
);

Expand Down
10 changes: 8 additions & 2 deletions apps/docs/content/docs/agent-integration.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,14 @@ claude "Add error handling to fetchUser"

## Parallel Agents

Create multiple workspaces and run an agent in each. Compare results and merge the best solution.
Create multiple workspaces and run an agent in each. Compare results and merge the best solution. See [Parallel Agents](/parallel-agents) for a detailed guide.

## Notifications

Get notified when agents finish. Configure sounds in Settings.
Get notified when agents finish. Configure sounds in Settings → Notifications.

## Next Steps

- [Parallel Agents](/parallel-agents) — Run multiple agents at the same time
- [Terminal Presets](/terminal-presets) — Auto-launch agents on workspace creation
- [MCP Server](/mcp) — Let agents control Superset programmatically
4 changes: 2 additions & 2 deletions apps/docs/content/docs/mcp.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,7 @@ API keys grant full access to your organization. Keep them secret and never comm
| `switch_workspace` | Switch to a different workspace |
| `delete_workspace` | Delete a workspace |
| `list_workspaces` | List all workspaces on a device |
| `get_workspace_details` | Get detailed information about a workspace on a device, including its tabs and panes. Use this to discover pane IDs. |
| `navigate_to_workspace` | Navigate the desktop app to a workspace |

### Device & Organization
Expand All @@ -239,8 +240,7 @@ API keys grant full access to your organization. Keep them secret and never comm

| Tool | Description |
|------|-------------|
| `start_claude_session` | 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_claude_subagent` | Start a Claude Code subagent in an existing workspace. Adds a new terminal pane instead of creating a new workspace. |
| `start_claude_session` | Start an autonomous Claude Code session for a task. Launches Claude with the task context in the specified workspace. When `paneId` is provided, adds a new terminal pane to the tab containing that pane (subagent behavior) instead of initializing the workspace. |

## Example Usage

Expand Down
Loading
Loading