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 @@ -2,6 +2,7 @@ import { Badge } from "@superset/ui/badge";
import { ScrollArea } from "@superset/ui/scroll-area";
import type { TaskWithStatus } from "../../../components/TasksView/hooks/useTasksTable";
import { AssigneeProperty } from "./components/AssigneeProperty";
import { OpenInWorkspace } from "./components/OpenInWorkspace";
import { PriorityProperty } from "./components/PriorityProperty";
import { StatusProperty } from "./components/StatusProperty";

Expand Down Expand Up @@ -41,6 +42,8 @@ export function PropertiesSidebar({ task }: PropertiesSidebarProps) {
<span className="text-sm text-muted-foreground">No labels</span>
)}
</div>

<OpenInWorkspace task={task} />
</div>
</ScrollArea>
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
import { Button } from "@superset/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@superset/ui/dropdown-menu";
import { toast } from "@superset/ui/sonner";
import { useEffect, useState } from "react";
import { HiArrowRight, HiChevronDown } from "react-icons/hi2";
import { electronTrpc } from "renderer/lib/electron-trpc";
import { useCreateWorkspace } from "renderer/react-query/workspaces";
import { ProjectThumbnail } from "renderer/screens/main/components/WorkspaceSidebar/ProjectSection/ProjectThumbnail";
import { useWorkspaceInitStore } from "renderer/stores/workspace-init";
import type { TaskWithStatus } from "../../../../../components/TasksView/hooks/useTasksTable";
import { buildClaudeCommand } from "../../../../utils/buildClaudeCommand";
import { deriveBranchName } from "../../../../utils/deriveBranchName";

interface OpenInWorkspaceProps {
task: TaskWithStatus;
}

export function OpenInWorkspace({ task }: OpenInWorkspaceProps) {
const { data: recentProjects = [] } =
electronTrpc.projects.getRecents.useQuery();
const createWorkspace = useCreateWorkspace();
const [selectedProjectId, setSelectedProjectId] = useState<string | null>(
null,
);

// Default to the first recent project
const effectiveProjectId = selectedProjectId ?? recentProjects[0]?.id ?? null;
const selectedProject = recentProjects.find(
(p) => p.id === effectiveProjectId,
);

// Sync default once projects load
useEffect(() => {
if (!selectedProjectId && recentProjects.length > 0) {
setSelectedProjectId(recentProjects[0].id);
}
}, [selectedProjectId, recentProjects]);

const handleOpen = async () => {
if (!effectiveProjectId) return;
await handleSelectProject(effectiveProjectId);
};

const handleSelectProject = async (projectId: string) => {
const branchName = deriveBranchName({
slug: task.slug,
title: task.title,
});

try {
const result = await createWorkspace.mutateAsync({
projectId,
name: task.slug,
branchName,
});

if (!result.wasExisting) {
const command = buildClaudeCommand({
task: {
id: task.id,
slug: task.slug,
title: task.title,
description: task.description,
priority: task.priority,
statusName: task.status.name,
labels: task.labels,
},
randomId: window.crypto.randomUUID(),
});

const store = useWorkspaceInitStore.getState();
const pending = store.pendingTerminalSetups[result.workspace.id];
store.addPendingTerminalSetup({
workspaceId: result.workspace.id,
projectId: result.projectId,
initialCommands: [...(pending?.initialCommands ?? []), command],
defaultPresets: pending?.defaultPresets,
});
}

toast.success(
result.wasExisting ? "Opened existing workspace" : "Workspace created",
);
} catch (err) {
toast.error(
err instanceof Error ? err.message : "Failed to create workspace",
);
}
Comment on lines +89 to +93

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Add console.error logging before the toast.

The catch block displays a user-facing toast but doesn't log the error. This makes debugging harder in production. Per the project guidelines: "Never swallow errors silently; at minimum log errors with context before rethrowing or handling them explicitly."

Proposed fix
 		} catch (err) {
+			console.error("[WorkOnTaskButton/handleSelectProject]", err);
 			toast.error(
 				err instanceof Error ? err.message : "Failed to create workspace",
 			);
 		}
🤖 Prompt for AI Agents
In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/`$taskId/components/WorkOnTaskButton/WorkOnTaskButton.tsx
around lines 89 - 93, In the catch block inside the WorkOnTaskButton component
(the error handler that currently calls toast.error), add a console.error call
before showing the toast to log the full error and contextual info (e.g.,
"WorkOnTaskButton: failed to create workspace" plus the caught err and err.stack
when available) so the error is not swallowed; keep the toast behavior unchanged
after logging.

};

return (
<div className="flex flex-col gap-2">
<span className="text-xs text-muted-foreground">Open in workspace</span>
<div className="flex gap-1.5">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="outline"
size="sm"
className="flex-1 justify-between font-normal h-8 min-w-0"
>
<span className="flex items-center gap-2 truncate">
{selectedProject ? (
<>
<ProjectThumbnail
projectId={selectedProject.id}
projectName={selectedProject.name}
projectColor={selectedProject.color}
githubOwner={selectedProject.githubOwner}
hideImage={selectedProject.hideImage ?? undefined}
iconUrl={selectedProject.iconUrl}
className="size-4"
/>
<span className="truncate">{selectedProject.name}</span>
</>
) : (
<span className="text-muted-foreground">Select project</span>
)}
</span>
<HiChevronDown className="w-3.5 h-3.5 text-muted-foreground shrink-0" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent
align="start"
className="w-[--radix-dropdown-menu-trigger-width]"
>
{recentProjects.length === 0 ? (
<DropdownMenuItem disabled>No projects found</DropdownMenuItem>
) : (
recentProjects
.filter((p) => p.id)
.map((project) => (
<DropdownMenuItem
key={project.id}
onClick={() => setSelectedProjectId(project.id)}
className="flex items-center gap-2"
>
<ProjectThumbnail
projectId={project.id}
projectName={project.name}
projectColor={project.color}
githubOwner={project.githubOwner}
hideImage={project.hideImage ?? undefined}
iconUrl={project.iconUrl}
className="size-4"
/>
{project.name}
</DropdownMenuItem>
))
)}
</DropdownMenuContent>
</DropdownMenu>
<Button
size="icon"
className="h-8 w-8 shrink-0"
disabled={!effectiveProjectId || createWorkspace.isPending}
onClick={handleOpen}
>
<HiArrowRight className="w-3.5 h-3.5" />
</Button>
</div>
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { OpenInWorkspace } from "./OpenInWorkspace";
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { buildClaudeCommand } from "@superset/shared/claude-command";
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { sanitizeSegment } from "shared/utils/branch";

export function deriveBranchName({
slug,
title,
}: {
slug: string;
title: string;
}): string {
const prefix = slug.toLowerCase();
const titleSegment = sanitizeSegment(title, 40);
return titleSegment ? `${prefix}-${titleSegment}` : prefix;
}
1 change: 1 addition & 0 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions packages/mcp/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"dependencies": {
"@modelcontextprotocol/sdk": "^1.25.3",
"@superset/db": "workspace:*",
"@superset/shared": "workspace:*",
"drizzle-orm": "0.45.1",
"zod": "^4.3.5"
},
Expand Down
Original file line number Diff line number Diff line change
@@ -1,54 +1,12 @@
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { db } from "@superset/db/client";
import { taskStatuses, tasks } from "@superset/db/schema";
import { buildClaudeCommand } from "@superset/shared/claude-command";
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<Awaited<ReturnType<typeof fetchTask>>>,
): 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`;

const delimiter = `SUPERSET_PROMPT_${crypto.randomUUID().replaceAll("-", "")}`;

return [
`claude --dangerously-skip-permissions "$(cat <<'${delimiter}'`,
prompt,
delimiter,
')"',
].join("\n");
}

async function fetchTask({
taskId,
organizationId,
Expand Down Expand Up @@ -160,7 +118,7 @@ export function register(server: McpServer) {
deviceId: validated.deviceId,
tool: "start_claude_session",
params: {
command: buildCommand(task),
command: buildClaudeCommand({ task, randomId: crypto.randomUUID() }),
name: task.slug,
workspaceId: validated.workspaceId,
},
Expand Down Expand Up @@ -193,7 +151,9 @@ export function register(server: McpServer) {
ctx,
deviceId: validated.deviceId,
tool: "start_claude_subagent",
params: { command: buildCommand(task) },
params: {
command: buildClaudeCommand({ task, randomId: crypto.randomUUID() }),
},
});
},
);
Expand Down
4 changes: 4 additions & 0 deletions packages/shared/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@
"./auth": {
"types": "./src/auth/index.ts",
"default": "./src/auth/index.ts"
},
"./claude-command": {
"types": "./src/claude-command.ts",
"default": "./src/claude-command.ts"
}
},
"scripts": {
Expand Down
56 changes: 56 additions & 0 deletions packages/shared/src/claude-command.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
interface TaskInput {
id: string;
slug: string;
title: string;
description: string | null;
priority: string;
statusName: string | null;
labels: string[] | null;
}
Comment on lines +1 to +9

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

cat -n packages/shared/src/claude-command.ts

Repository: superset-sh/superset

Length of output: 1997


🏁 Script executed:

rg "import.*TaskInput|from.*claude-command" --type=ts --type=tsx

Repository: superset-sh/superset

Length of output: 91


🏁 Script executed:

rg "TaskInput" --type=ts --type=tsx -B 2 -A 2

Repository: superset-sh/superset

Length of output: 91


🏁 Script executed:

rg "TaskInput" --type=ts -B 2 -A 2

Repository: superset-sh/superset

Length of output: 1317


🏁 Script executed:

fd "\.ts$|\.tsx$" packages/mcp packages/desktop-app 2>/dev/null | head -20

Repository: superset-sh/superset

Length of output: 1171


🏁 Script executed:

rg "import.*from.*claude-command" --type=ts

Repository: superset-sh/superset

Length of output: 209


🏁 Script executed:

cat -n packages/mcp/src/tools/devices/start-claude-session/start-claude-session.ts

Repository: superset-sh/superset

Length of output: 5510


🏁 Script executed:

cat -n packages/mcp/src/tools/tasks/create-task/create-task.ts | head -40

Repository: superset-sh/superset

Length of output: 1699


Export TaskInput from the claude-command module.

buildClaudeCommand is exported, but TaskInput — its primary input type — is not. Consumers in packages/mcp and elsewhere must construct the task object without importing the type, relying on structural typing instead of explicit type safety.

Proposed fix
-interface TaskInput {
+export interface TaskInput {
	id: string;
	slug: string;
	title: string;
	description: string | null;
	priority: string;
	statusName: string | null;
	labels: string[] | null;
}
🤖 Prompt for AI Agents
In `@packages/shared/src/claude-command.ts` around lines 1 - 9, The TaskInput
interface is not exported, so callers cannot import the primary input type for
buildClaudeCommand; update the module to export TaskInput (e.g., change the
declaration to export interface TaskInput or export type TaskInput = { ... }) so
consumers can import TaskInput alongside buildClaudeCommand and get explicit
type safety when constructing task objects.


export function buildClaudeCommand({
task,
randomId,
}: {
task: TaskInput;
randomId: string;
}): 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`;

const delimiter = `SUPERSET_PROMPT_${randomId.replaceAll("-", "")}`;

return [
`claude --dangerously-skip-permissions "$(cat <<'${delimiter}'`,
prompt,
delimiter,
')"',
].join("\n");
}
Loading