From fed95db00867ecff753785bf51a3edea5af92a31 Mon Sep 17 00:00:00 2001 From: Satya Patel Date: Thu, 19 Feb 2026 13:11:41 -0800 Subject: [PATCH 1/5] feat(docs): restructure docs site and add workflow, task management, parallel agents pages Inspired by Conductor's documentation architecture: - Add workflow.mdx, task-management.mdx, parallel-agents.mdx - Rewrite overview as card-grid welcome/landing page - Reorganize navigation into 5 sections (Get Started, Core Features, Guides, Tips, Help) - Fix ResourceCard to support internal links - Update MCP docs with get_workspace_details and unified start_claude_session --- .mcp.json | 2 +- .../tools/get-workspace-details.ts | 69 ++++++++++++++ .../hooks/useCommandWatcher/tools/index.ts | 4 +- .../tools/start-claude-session.ts | 28 +++++- .../tools/start-claude-subagent.ts | 42 --------- .../useCommandWatcher/useCommandWatcher.ts | 47 +++++++--- .../CollectionsProvider/collections.ts | 58 ++++++++---- apps/docs/content/docs/agent-integration.mdx | 10 ++- apps/docs/content/docs/mcp.mdx | 4 +- apps/docs/content/docs/meta.json | 12 ++- apps/docs/content/docs/overview.mdx | 89 ++++++++++++++++--- apps/docs/content/docs/parallel-agents.mdx | 61 +++++++++++++ apps/docs/content/docs/task-management.mdx | 53 +++++++++++ apps/docs/content/docs/workflow.mdx | 68 ++++++++++++++ apps/docs/next.config.mjs | 4 +- .../components/ResourceCard/ResourceCard.tsx | 8 +- .../get-app-context/get-app-context.ts | 2 +- .../get-workspace-details.ts | 41 +++++++++ .../devices/get-workspace-details/index.ts | 1 + .../start-claude-session.ts | 73 ++++----------- .../switch-workspace/switch-workspace.ts | 3 +- packages/mcp/src/tools/index.ts | 2 + 22 files changed, 527 insertions(+), 154 deletions(-) create mode 100644 apps/desktop/src/renderer/routes/_authenticated/components/AgentHooks/hooks/useCommandWatcher/tools/get-workspace-details.ts delete mode 100644 apps/desktop/src/renderer/routes/_authenticated/components/AgentHooks/hooks/useCommandWatcher/tools/start-claude-subagent.ts create mode 100644 apps/docs/content/docs/parallel-agents.mdx create mode 100644 apps/docs/content/docs/task-management.mdx create mode 100644 apps/docs/content/docs/workflow.mdx create mode 100644 packages/mcp/src/tools/devices/get-workspace-details/get-workspace-details.ts create mode 100644 packages/mcp/src/tools/devices/get-workspace-details/index.ts diff --git a/.mcp.json b/.mcp.json index 8851a2af1cb..2786a1efacc 100644 --- a/.mcp.json +++ b/.mcp.json @@ -2,7 +2,7 @@ "mcpServers": { "superset": { "type": "http", - "url": "https://api.superset.sh/api/agent/mcp" + "url": "http://localhost:3041/api/agent/mcp" }, "expo-mcp": { "type": "http", diff --git a/apps/desktop/src/renderer/routes/_authenticated/components/AgentHooks/hooks/useCommandWatcher/tools/get-workspace-details.ts b/apps/desktop/src/renderer/routes/_authenticated/components/AgentHooks/hooks/useCommandWatcher/tools/get-workspace-details.ts new file mode 100644 index 00000000000..cd5a40f33aa --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/components/AgentHooks/hooks/useCommandWatcher/tools/get-workspace-details.ts @@ -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, + ctx: ToolContext, +): Promise { + 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 = { + name: "get_workspace_details", + schema, + execute, +}; 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 68742302ef4..85f08e8d7f1 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 @@ -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"; @@ -16,11 +16,11 @@ const tools: ToolDefinition[] = [ createWorkspace, deleteWorkspace, getAppContext, + getWorkspaceDetails, listProjects, listWorkspaces, navigateToWorkspace, startClaudeSession, - startClaudeSubagent, switchWorkspace, updateWorkspace, ]; 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 index 23237fc497d..8a63cad3a76 100644 --- 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 @@ -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"; @@ -6,6 +7,7 @@ const schema = z.object({ command: z.string(), name: z.string(), workspaceId: z.string(), + paneId: z.string().optional(), }); async function execute( @@ -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({ 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 deleted file mode 100644 index 40770300d27..00000000000 --- a/apps/desktop/src/renderer/routes/_authenticated/components/AgentHooks/hooks/useCommandWatcher/tools/start-claude-subagent.ts +++ /dev/null @@ -1,42 +0,0 @@ -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/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 cb5677f2aa0..f1713e5aec5 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 @@ -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 "renderer/routes/_authenticated/providers/CollectionsProvider/collections"; import { executeTool, type ToolContext } from "./tools"; /** Tracks command IDs that have been or are being processed to prevent duplicate execution. */ @@ -86,30 +87,34 @@ export function useCommandWatcher() { console.log(`[command-watcher] Processing: ${commandId} (${tool})`); try { + // Optimistic local updates for intermediate statuses (no HTTP persistence) collections.agentCommands.update(commandId, (draft) => { draft.status = "claimed"; draft.claimedBy = deviceInfo?.deviceId ?? null; draft.claimedAt = new Date(); }); - 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, + result: result.data ?? {}, + executedAt, }); } else { - // Include per-item errors from bulk operations in the error message const itemErrors = ( result.data?.errors as Array<{ error: string }> | undefined ) @@ -119,24 +124,41 @@ 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, + 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, + error: errorMsg, + executedAt, }); } }, @@ -169,6 +191,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; } diff --git a/apps/desktop/src/renderer/routes/_authenticated/providers/CollectionsProvider/collections.ts b/apps/desktop/src/renderer/routes/_authenticated/providers/CollectionsProvider/collections.ts index 40d4ededd04..31b152b4b2d 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/providers/CollectionsProvider/collections.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/providers/CollectionsProvider/collections.ts @@ -1,5 +1,6 @@ import { snakeCamelMapper } from "@electric-sql/client"; import type { + CommandStatus, SelectAgentCommand, SelectChatSession, SelectDevicePresence, @@ -219,21 +220,10 @@ 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) }; + // No-op: command status persistence is handled directly by useCommandWatcher + // via persistCommandStatus() with retry logic. + onUpdate: async () => { + return { txid: Date.now() }; }, }), ); @@ -388,3 +378,41 @@ export function getCollections(organizationId: string) { organizations: organizationsCollection, }; } + +/** + * Persist a command's final status directly to the API with exponential backoff retry. + * Bypasses the collection's onUpdate handler for reliable final-status writes. + */ +export async function persistCommandStatus(params: { + id: string; + status: CommandStatus; + claimedBy?: string; + claimedAt?: Date; + result?: Record; + error?: string; + executedAt?: Date; +}): Promise { + const delays = [500, 1000, 2000]; + let lastError: unknown; + + for (let attempt = 0; attempt <= delays.length; attempt++) { + try { + await apiClient.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, + ); +} diff --git a/apps/docs/content/docs/agent-integration.mdx b/apps/docs/content/docs/agent-integration.mdx index fa13177e97b..18a7497502a 100644 --- a/apps/docs/content/docs/agent-integration.mdx +++ b/apps/docs/content/docs/agent-integration.mdx @@ -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 diff --git a/apps/docs/content/docs/mcp.mdx b/apps/docs/content/docs/mcp.mdx index 99fd2495586..d83d38468df 100644 --- a/apps/docs/content/docs/mcp.mdx +++ b/apps/docs/content/docs/mcp.mdx @@ -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 @@ -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 diff --git a/apps/docs/content/docs/meta.json b/apps/docs/content/docs/meta.json index d8e8b14a6d8..439bef23ea5 100644 --- a/apps/docs/content/docs/meta.json +++ b/apps/docs/content/docs/meta.json @@ -2,23 +2,27 @@ "title": "Documentation", "pages": [ "---Rocket Get Started---", - "installation", "overview", + "installation", "first-workspace", + "workflow", "---Gauge Core Features---", + "parallel-agents", "workspaces", + "task-management", "diff-viewer", "terminal-integration", "ports", - "agent-integration", "mcp", "---BookOpen Guides---", + "agent-integration", "setup-teardown-scripts", "use-with-ide", - "using-monorepos", "terminal-presets", - "keyboard-shortcuts", "customization", + "---Lightbulb Tips---", + "using-monorepos", + "keyboard-shortcuts", "---CircleHelp Help---", "faq" ] diff --git a/apps/docs/content/docs/overview.mdx b/apps/docs/content/docs/overview.mdx index f8da282c654..2c84c52d0c3 100644 --- a/apps/docs/content/docs/overview.mdx +++ b/apps/docs/content/docs/overview.mdx @@ -1,22 +1,87 @@ --- -title: Overview -description: Learn what Superset is and how it works +title: Welcome +description: Superset is a desktop app for running multiple AI coding agents in parallel --- ## What is Superset? -Superset is a desktop app for managing multiple codebases with git worktrees. Work on several branches simultaneously, each in its own isolated workspace. +Superset is a macOS desktop app for running multiple AI coding agents simultaneously. Each agent works in its own isolated workspace (a git worktree), so you can hand off parallel tasks and review results as they come in. -## Key Features +**Wait less, ship more.** -- **Workspaces** - Each branch gets its own directory via git worktrees. No more stashing. -- **Terminal** - Built-in terminal with persistent sessions per workspace. -- **Diff Viewer** - Review changes, stage files, commit, and push. -- **AI Agents** - Run Claude Code or other agents in isolated workspaces. -- **IDE Integration** - Open any workspace in Cursor or VS Code. + -## How It Works +## Get Started -Superset uses [git worktrees](https://git-scm.com/docs/git-worktree) to give each branch its own directory. Switch contexts instantly without losing your place. + -Your data stays local. Superset works offline and syncs when connected. +## Core Features + + + +## Guides + + diff --git a/apps/docs/content/docs/parallel-agents.mdx b/apps/docs/content/docs/parallel-agents.mdx new file mode 100644 index 00000000000..717c368c98e --- /dev/null +++ b/apps/docs/content/docs/parallel-agents.mdx @@ -0,0 +1,61 @@ +--- +title: Parallel Agents +description: Run multiple AI coding agents simultaneously in isolated workspaces +--- + +## Overview + +Run multiple AI agents at the same time, each in its own isolated workspace. This is the core value of Superset — instead of waiting for one agent to finish before starting the next, run them all in parallel. + +## How It Works + +Each workspace is a separate git worktree with its own branch and directory. Agents can't interfere with each other because they're working on different files in different directories. + +1. Create workspace A → launch Claude on task A +2. Create workspace B → launch Claude on task B +3. Create workspace C → launch Claude on task C +4. Monitor all three from the sidebar +5. Review and merge as each finishes + +## Supported Agents + +| Agent | Command | +|-------|---------| +| **Claude Code** | `claude` | +| **Codex** | `codex` | +| **Gemini CLI** | `gemini` | +| **OpenCode** | `opencode` | +| **Any CLI agent** | Works in the terminal | + +## Monitoring + +The workspace sidebar shows live status for every workspace: + +- **Spinning** — agent is actively working +- **Amber** — agent needs input or hit an error +- **Green** — work is done, ready for review + +Audio notifications alert you when agents finish. Configure sounds in Settings → Notifications. + +## Auto-Launch with Presets + +Set up a [terminal preset](/terminal-presets) for your preferred agent and mark it as default. New workspaces will automatically launch the agent when created. + +Quick-add templates are available for Claude, Codex, Gemini, Cursor Agent, and OpenCode. + +## Subagents + +Use the [MCP server](/mcp) `start_claude_session` tool to spawn subagents within an existing workspace. Pass a `paneId` to add a new terminal pane to an existing tab — useful for breaking a large task into parallel subtasks. + +## Tips + +- Start with 2-3 agents and scale up as you get comfortable +- Use descriptive branch names so you can tell workspaces apart at a glance +- Set up [setup scripts](/setup-teardown-scripts) to automate `bun install` and env file copying — each workspace needs its own dependencies +- Delete workspaces after merging to keep things clean + +## Next Steps + +- [Terminal Presets](/terminal-presets) — Auto-launch agents on workspace creation +- [Task Management](/task-management) — Manage what each agent works on +- [Workflow](/workflow) — The full development loop diff --git a/apps/docs/content/docs/task-management.mdx b/apps/docs/content/docs/task-management.mdx new file mode 100644 index 00000000000..8effeba2221 --- /dev/null +++ b/apps/docs/content/docs/task-management.mdx @@ -0,0 +1,53 @@ +--- +title: Task Management +description: Organize, track, and execute tasks from a single interface +--- + +## Overview + +Superset has a built-in task tracker. Create tasks, assign them, set priorities, and launch agents — all without leaving the app. + +## Creating Tasks + +Create tasks from the Tasks view or via the [MCP server](/mcp). Each task has: + +| Field | Description | +|-------|-------------| +| **Title** | Short description of the work | +| **Description** | Detailed context, requirements, or instructions | +| **Status** | Backlog, Todo, In Progress, Done, Cancelled | +| **Priority** | Urgent, High, Medium, Low, None | +| **Labels** | Tags for categorization | +| **Assignee** | Team member responsible | + +## Start Working + +The "Start Working" button on any task: + +1. Creates a new workspace (git worktree + branch) +2. Feeds the task title and description to Claude as context +3. Launches Claude Code in the workspace terminal + +The agent starts working immediately with full context about what needs to be done. + +## Batch Mode + +Select multiple tasks and click "Start Working" to create workspaces for all of them at once. Each gets its own isolated workspace with its own Claude session. + +## MCP Integration + +AI agents can manage tasks programmatically via the [MCP server](/mcp): + +``` +"Create a task for fixing the login validation bug" +"List all my high-priority tasks" +"Update the auth task status to done" +``` + +Available MCP tools: `create_task`, `update_task`, `list_tasks`, `get_task`, `delete_task`, `list_task_statuses`. + +## Next Steps + +- [Workflow](/workflow) — See how tasks fit into the development loop +- [MCP Server](/mcp) — Automate task management with AI agents +- [Parallel Agents](/parallel-agents) — Run multiple tasks simultaneously diff --git a/apps/docs/content/docs/workflow.mdx b/apps/docs/content/docs/workflow.mdx new file mode 100644 index 00000000000..688643dfa96 --- /dev/null +++ b/apps/docs/content/docs/workflow.mdx @@ -0,0 +1,68 @@ +--- +title: Workflow +description: The end-to-end development workflow in Superset +--- + +## The Loop + +Superset is built around a simple loop: **create a workspace, do the work, review, merge, delete**. + +1. **Pick a task** — from your task board, a GitHub issue, or your head +2. **Create a workspace** — `⌘N` spins up an isolated git worktree on a new branch +3. **Work** — code manually, run an AI agent, or both +4. **Review** — check the diff viewer, run tests, iterate +5. **Ship** — commit, push, and create a PR from the sidebar +6. **Clean up** — delete the workspace after merge + +The power comes from running steps 1–6 for multiple tasks **at the same time**. + +## Creating Workspaces + +Every workspace is a git worktree — a real directory with its own branch, terminal sessions, and port range. No stashing, no context switching. + +Create from: +- **New branch** — start fresh from main +- **Existing branch** — pick up where you left off +- **Pull request** — review or continue someone else's work +- **Task** — click "Start Working" on any task to auto-create a workspace with context + +## Running Agents + +Once a workspace is ready, launch an AI agent in the terminal: + +```bash +claude "Implement the auth middleware" +``` + +Or use [terminal presets](/terminal-presets) to auto-launch agents when workspaces are created. + +While one agent works, create another workspace and start a second task. Superset's sidebar shows live status for every workspace — spinning means working, amber means needs attention, green means ready to review. + +See [Parallel Agents](/parallel-agents) for more. + +## Reviewing Changes + +The right sidebar shows all changes in the workspace: + +- **Stage/unstage files** — select exactly what to commit +- **View diffs** — split or unified, with [focus mode](/diff-viewer) for one file at a time +- **Commit and push** — write a message and push in one step +- **Create a PR** — open a pull request directly from Superset + +## From Task to PR + +The fastest path from idea to pull request: + +1. Open the Tasks view and find your task +2. Click **Start Working** — Superset creates a workspace and launches Claude with your task description as context +3. Monitor progress in the sidebar +4. When Claude finishes, review the diff and create a PR +5. Delete the workspace + +Repeat for every task in your backlog. Run as many in parallel as your machine can handle. + +## Next Steps + +- [Parallel Agents](/parallel-agents) — Run multiple agents simultaneously +- [Task Management](/task-management) — Organize and track your work +- [Setup Scripts](/setup-teardown-scripts) — Automate workspace initialization diff --git a/apps/docs/next.config.mjs b/apps/docs/next.config.mjs index 2b0fcb13ec8..c656a361dd4 100644 --- a/apps/docs/next.config.mjs +++ b/apps/docs/next.config.mjs @@ -29,12 +29,12 @@ const config = { return [ { source: "/", - destination: "/installation", + destination: "/overview", permanent: false, }, { source: "/docs", - destination: "/installation", + destination: "/overview", permanent: false, }, ]; diff --git a/apps/docs/src/components/ResourceCard/ResourceCard.tsx b/apps/docs/src/components/ResourceCard/ResourceCard.tsx index 91b9a01579e..e260a4c4268 100644 --- a/apps/docs/src/components/ResourceCard/ResourceCard.tsx +++ b/apps/docs/src/components/ResourceCard/ResourceCard.tsx @@ -16,6 +16,7 @@ export function ResourceCard({ tags, className, }: ResourceCardProps) { + const isExternal = href.startsWith("http"); return (
- +

{title}

diff --git a/packages/mcp/src/tools/devices/get-app-context/get-app-context.ts b/packages/mcp/src/tools/devices/get-app-context/get-app-context.ts index ff6a430c911..80cf61a86b4 100644 --- a/packages/mcp/src/tools/devices/get-app-context/get-app-context.ts +++ b/packages/mcp/src/tools/devices/get-app-context/get-app-context.ts @@ -7,7 +7,7 @@ export function register(server: McpServer) { "get_app_context", { description: - "Get the current app context including pathname and active workspace", + "Get the current app context on a device, including pathname and active workspace. The target device must belong to the current user.", inputSchema: { deviceId: z.string().describe("Target device ID"), }, diff --git a/packages/mcp/src/tools/devices/get-workspace-details/get-workspace-details.ts b/packages/mcp/src/tools/devices/get-workspace-details/get-workspace-details.ts new file mode 100644 index 00000000000..d9e521ebcc1 --- /dev/null +++ b/packages/mcp/src/tools/devices/get-workspace-details/get-workspace-details.ts @@ -0,0 +1,41 @@ +import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { z } from "zod"; +import { executeOnDevice, getMcpContext } from "../../utils"; + +export function register(server: McpServer) { + server.registerTool( + "get_workspace_details", + { + description: + "Get detailed information about a workspace on a device, including its tabs and panes. Use this to discover pane IDs needed for start_claude_session's paneId parameter. The target device must belong to the current user.", + inputSchema: { + deviceId: z.string().describe("Target device ID"), + workspaceId: z.string().describe("Workspace ID to get details for"), + }, + }, + async (args, extra) => { + const ctx = getMcpContext(extra); + const deviceId = args.deviceId as string; + const workspaceId = args.workspaceId as string; + + if (!deviceId || !workspaceId) { + return { + content: [ + { + type: "text", + text: "Error: deviceId and workspaceId are required", + }, + ], + isError: true, + }; + } + + return executeOnDevice({ + ctx, + deviceId, + tool: "get_workspace_details", + params: { workspaceId }, + }); + }, + ); +} diff --git a/packages/mcp/src/tools/devices/get-workspace-details/index.ts b/packages/mcp/src/tools/devices/get-workspace-details/index.ts new file mode 100644 index 00000000000..a167007068c --- /dev/null +++ b/packages/mcp/src/tools/devices/get-workspace-details/index.ts @@ -0,0 +1 @@ +export { register } from "./get-workspace-details"; 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 index d67599021e9..c42db1c70cf 100644 --- 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 @@ -39,29 +39,21 @@ async function fetchTask({ return task ?? null; } -function validateSessionArgs(args: Record): { +function validateArgs(args: Record): { deviceId: string; taskId: string; workspaceId: string; + paneId?: string; } | null { const deviceId = args.deviceId as string; const taskId = args.taskId as string; const workspaceId = args.workspaceId as string; + const paneId = args.paneId as string | undefined; if (!deviceId || !taskId || !workspaceId) return null; - return { deviceId, taskId, workspaceId }; + return { deviceId, taskId, workspaceId, ...(paneId ? { paneId } : {}) }; } -function validateSubagentArgs(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_SESSION_ARGS_REQUIRED = { +const ERROR_ARGS_REQUIRED = { content: [ { type: "text" as const, @@ -71,16 +63,6 @@ const ERROR_SESSION_ARGS_REQUIRED = { isError: true, }; -const ERROR_SUBAGENT_ARGS_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, @@ -91,7 +73,7 @@ export function register(server: McpServer) { "start_claude_session", { description: - "Start an autonomous Claude Code session for a task in an existing workspace. Launches Claude with the task context in the specified workspace. The target device must belong to the current user.", + "Start an autonomous Claude Code session for a task in an existing workspace. 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. The target device must belong to the current user.", inputSchema: { deviceId: z.string().describe("Target device ID"), taskId: z.string().describe("Task ID to work on"), @@ -100,12 +82,18 @@ export function register(server: McpServer) { .describe( "Workspace ID to run the session in (from create_workspace)", ), + paneId: z + .string() + .optional() + .describe( + "Optional pane ID. When provided, adds a new pane to the tab containing this pane instead of initializing the workspace.", + ), }, }, async (args, extra) => { const ctx = getMcpContext(extra); - const validated = validateSessionArgs(args); - if (!validated) return ERROR_SESSION_ARGS_REQUIRED; + const validated = validateArgs(args); + if (!validated) return ERROR_ARGS_REQUIRED; const task = await fetchTask({ taskId: validated.taskId, @@ -121,38 +109,7 @@ export function register(server: McpServer) { command: buildClaudeCommand({ task, randomId: crypto.randomUUID() }), name: task.slug, workspaceId: validated.workspaceId, - }, - }); - }, - ); - - 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. The target device must belong to the current user.", - 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 = validateSubagentArgs(args); - if (!validated) return ERROR_SUBAGENT_ARGS_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: buildClaudeCommand({ task, randomId: crypto.randomUUID() }), + ...(validated.paneId ? { paneId: validated.paneId } : {}), }, }); }, diff --git a/packages/mcp/src/tools/devices/switch-workspace/switch-workspace.ts b/packages/mcp/src/tools/devices/switch-workspace/switch-workspace.ts index 8b216b8c0eb..4e20679b731 100644 --- a/packages/mcp/src/tools/devices/switch-workspace/switch-workspace.ts +++ b/packages/mcp/src/tools/devices/switch-workspace/switch-workspace.ts @@ -6,7 +6,8 @@ export function register(server: McpServer) { server.registerTool( "switch_workspace", { - description: "Switch to a different workspace (git worktree)", + description: + "Switch to a different workspace (git worktree) on a device. The target device must belong to the current user.", inputSchema: { deviceId: z.string().describe("Target device ID"), workspaceId: z diff --git a/packages/mcp/src/tools/index.ts b/packages/mcp/src/tools/index.ts index 8d05dd29c26..2f5e4bde332 100644 --- a/packages/mcp/src/tools/index.ts +++ b/packages/mcp/src/tools/index.ts @@ -2,6 +2,7 @@ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { register as createWorkspace } from "./devices/create-workspace"; import { register as deleteWorkspace } from "./devices/delete-workspace"; import { register as getAppContext } from "./devices/get-app-context"; +import { register as getWorkspaceDetails } from "./devices/get-workspace-details"; import { register as listDevices } from "./devices/list-devices"; import { register as listProjects } from "./devices/list-projects"; import { register as listWorkspaces } from "./devices/list-workspaces"; @@ -29,6 +30,7 @@ const allTools = [ listWorkspaces, listProjects, getAppContext, + getWorkspaceDetails, navigateToWorkspace, createWorkspace, switchWorkspace, From 924ad3940c223af8a4b98ae424c27c1406536a30 Mon Sep 17 00:00:00 2001 From: Satya Patel Date: Thu, 19 Feb 2026 13:13:37 -0800 Subject: [PATCH 2/5] fix: remove accidentally committed .mcp.json --- .mcp.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.mcp.json b/.mcp.json index 2786a1efacc..8851a2af1cb 100644 --- a/.mcp.json +++ b/.mcp.json @@ -2,7 +2,7 @@ "mcpServers": { "superset": { "type": "http", - "url": "http://localhost:3041/api/agent/mcp" + "url": "https://api.superset.sh/api/agent/mcp" }, "expo-mcp": { "type": "http", From 3cc5d614989c217c4c6a12aad68983f5a691c8a4 Mon Sep 17 00:00:00 2001 From: Satya Patel Date: Thu, 19 Feb 2026 13:16:32 -0800 Subject: [PATCH 3/5] refactor: move persistCommandStatus to useCommandWatcher, remove no-op onUpdate --- .../useCommandWatcher/persistCommandStatus.ts | 53 +++++++++++++++++++ .../useCommandWatcher/useCommandWatcher.ts | 2 +- .../CollectionsProvider/collections.ts | 43 --------------- 3 files changed, 54 insertions(+), 44 deletions(-) create mode 100644 apps/desktop/src/renderer/routes/_authenticated/components/AgentHooks/hooks/useCommandWatcher/persistCommandStatus.ts diff --git a/apps/desktop/src/renderer/routes/_authenticated/components/AgentHooks/hooks/useCommandWatcher/persistCommandStatus.ts b/apps/desktop/src/renderer/routes/_authenticated/components/AgentHooks/hooks/useCommandWatcher/persistCommandStatus.ts new file mode 100644 index 00000000000..709f77b6bf5 --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/components/AgentHooks/hooks/useCommandWatcher/persistCommandStatus.ts @@ -0,0 +1,53 @@ +import type { CommandStatus } from "@superset/db/schema"; +import type { AppRouter } from "@superset/trpc"; +import { createTRPCProxyClient, httpBatchLink } from "@trpc/client"; +import { env } from "renderer/env.renderer"; +import { getAuthToken } from "renderer/lib/auth-client"; +import superjson from "superjson"; + +const apiClient = createTRPCProxyClient({ + links: [ + httpBatchLink({ + url: `${env.NEXT_PUBLIC_API_URL}/api/trpc`, + headers: () => { + const token = getAuthToken(); + return token ? { Authorization: `Bearer ${token}` } : {}; + }, + transformer: superjson, + }), + ], +}); + +export async function persistCommandStatus(params: { + id: string; + status: CommandStatus; + claimedBy?: string; + claimedAt?: Date; + result?: Record; + error?: string; + executedAt?: Date; +}): Promise { + const delays = [500, 1000, 2000]; + let lastError: unknown; + + for (let attempt = 0; attempt <= delays.length; attempt++) { + try { + await apiClient.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, + ); +} 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 f1713e5aec5..94f52c490e6 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 @@ -9,7 +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 "renderer/routes/_authenticated/providers/CollectionsProvider/collections"; +import { persistCommandStatus } from "./persistCommandStatus"; import { executeTool, type ToolContext } from "./tools"; /** Tracks command IDs that have been or are being processed to prevent duplicate execution. */ diff --git a/apps/desktop/src/renderer/routes/_authenticated/providers/CollectionsProvider/collections.ts b/apps/desktop/src/renderer/routes/_authenticated/providers/CollectionsProvider/collections.ts index 31b152b4b2d..f9e90d33f12 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/providers/CollectionsProvider/collections.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/providers/CollectionsProvider/collections.ts @@ -1,6 +1,5 @@ import { snakeCamelMapper } from "@electric-sql/client"; import type { - CommandStatus, SelectAgentCommand, SelectChatSession, SelectDevicePresence, @@ -220,11 +219,6 @@ function createOrgCollections(organizationId: string): OrgCollections { columnMapper, }, getKey: (item) => item.id, - // No-op: command status persistence is handled directly by useCommandWatcher - // via persistCommandStatus() with retry logic. - onUpdate: async () => { - return { txid: Date.now() }; - }, }), ); @@ -379,40 +373,3 @@ export function getCollections(organizationId: string) { }; } -/** - * Persist a command's final status directly to the API with exponential backoff retry. - * Bypasses the collection's onUpdate handler for reliable final-status writes. - */ -export async function persistCommandStatus(params: { - id: string; - status: CommandStatus; - claimedBy?: string; - claimedAt?: Date; - result?: Record; - error?: string; - executedAt?: Date; -}): Promise { - const delays = [500, 1000, 2000]; - let lastError: unknown; - - for (let attempt = 0; attempt <= delays.length; attempt++) { - try { - await apiClient.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, - ); -} From 78671ff85dc63bb52a7a743237882296a8ab192d Mon Sep 17 00:00:00 2001 From: Satya Patel Date: Thu, 19 Feb 2026 13:20:51 -0800 Subject: [PATCH 4/5] fix: remove trailing newline (lint) --- .../_authenticated/providers/CollectionsProvider/collections.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/desktop/src/renderer/routes/_authenticated/providers/CollectionsProvider/collections.ts b/apps/desktop/src/renderer/routes/_authenticated/providers/CollectionsProvider/collections.ts index f9e90d33f12..9c1fc56622f 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/providers/CollectionsProvider/collections.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/providers/CollectionsProvider/collections.ts @@ -372,4 +372,3 @@ export function getCollections(organizationId: string) { organizations: organizationsCollection, }; } - From d8dbf40c21c03db4c6a4bca95f10c3cddeff8092 Mon Sep 17 00:00:00 2001 From: Satya Patel Date: Thu, 19 Feb 2026 13:31:57 -0800 Subject: [PATCH 5/5] fix: use shared apiTrpcClient and pass claimedAt to persistCommandStatus --- .../useCommandWatcher/persistCommandStatus.ts | 21 ++----------------- .../useCommandWatcher/useCommandWatcher.ts | 7 +++++-- 2 files changed, 7 insertions(+), 21 deletions(-) diff --git a/apps/desktop/src/renderer/routes/_authenticated/components/AgentHooks/hooks/useCommandWatcher/persistCommandStatus.ts b/apps/desktop/src/renderer/routes/_authenticated/components/AgentHooks/hooks/useCommandWatcher/persistCommandStatus.ts index 709f77b6bf5..66172f1fa6c 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/components/AgentHooks/hooks/useCommandWatcher/persistCommandStatus.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/components/AgentHooks/hooks/useCommandWatcher/persistCommandStatus.ts @@ -1,22 +1,5 @@ import type { CommandStatus } from "@superset/db/schema"; -import type { AppRouter } from "@superset/trpc"; -import { createTRPCProxyClient, httpBatchLink } from "@trpc/client"; -import { env } from "renderer/env.renderer"; -import { getAuthToken } from "renderer/lib/auth-client"; -import superjson from "superjson"; - -const apiClient = createTRPCProxyClient({ - links: [ - httpBatchLink({ - url: `${env.NEXT_PUBLIC_API_URL}/api/trpc`, - headers: () => { - const token = getAuthToken(); - return token ? { Authorization: `Bearer ${token}` } : {}; - }, - transformer: superjson, - }), - ], -}); +import { apiTrpcClient } from "renderer/lib/api-trpc-client"; export async function persistCommandStatus(params: { id: string; @@ -32,7 +15,7 @@ export async function persistCommandStatus(params: { for (let attempt = 0; attempt <= delays.length; attempt++) { try { - await apiClient.agent.updateCommand.mutate(params); + await apiTrpcClient.agent.updateCommand.mutate(params); return; } catch (err) { lastError = err; 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 94f52c490e6..baeada86c18 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 @@ -86,12 +86,12 @@ export function useCommandWatcher() { handledCommands.add(commandId); console.log(`[command-watcher] Processing: ${commandId} (${tool})`); + const claimedAt = new Date(); try { - // Optimistic local updates for intermediate statuses (no HTTP persistence) collections.agentCommands.update(commandId, (draft) => { draft.status = "claimed"; draft.claimedBy = deviceInfo?.deviceId ?? null; - draft.claimedAt = new Date(); + draft.claimedAt = claimedAt; }); collections.agentCommands.update(commandId, (draft) => { @@ -111,6 +111,7 @@ export function useCommandWatcher() { id: commandId, status: "completed", claimedBy: deviceInfo?.deviceId, + claimedAt, result: result.data ?? {}, executedAt, }); @@ -139,6 +140,7 @@ export function useCommandWatcher() { id: commandId, status: "failed", claimedBy: deviceInfo?.deviceId, + claimedAt, error: fullError, executedAt, }); @@ -157,6 +159,7 @@ export function useCommandWatcher() { id: commandId, status: "failed", claimedBy: deviceInfo?.deviceId, + claimedAt, error: errorMsg, executedAt, });