From e17388bc417b9058109bbaf3ad32df2ced416c26 Mon Sep 17 00:00:00 2001 From: Satya Patel Date: Thu, 23 Apr 2026 19:14:24 -0700 Subject: [PATCH 1/2] fix(desktop): honor agent selection in new-workspace modal MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The modal's agent picker was cosmetic — `selectedAgent` was persisted to localStorage for the UI but never written to the pending row, so the pending page's `buildForkAgentLaunch` always called `getFallbackAgentId` (which hard-prefers claude). Selecting codex, cursor, etc. silently launched claude instead. Thread the selected agent through the pending row into the launch build; "none" now genuinely skips the agent launch (no silent claude substitution). --- .../$pendingId/buildForkAgentLaunch.test.ts | 1 + .../$pendingId/buildForkAgentLaunch.ts | 21 +++++++++++++++---- .../$pendingId/buildIntentPayload.test.ts | 1 + .../pending/$pendingId/dispatchForkLaunch.ts | 7 ++++++- .../PromptGroup/PromptGroup.tsx | 2 +- .../useSubmitWorkspace/useSubmitWorkspace.ts | 16 ++++++++++++-- .../dashboardSidebarLocal/schema.ts | 4 ++++ 7 files changed, 44 insertions(+), 8 deletions(-) diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/pending/$pendingId/buildForkAgentLaunch.test.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/pending/$pendingId/buildForkAgentLaunch.test.ts index 0c976ea0bf6..50f87fed64a 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/pending/$pendingId/buildForkAgentLaunch.test.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/pending/$pendingId/buildForkAgentLaunch.test.ts @@ -15,6 +15,7 @@ function pendingBase( prompt: "", linkedIssues: [], linkedPR: null, + agentId: null, ...overrides, }; } diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/pending/$pendingId/buildForkAgentLaunch.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/pending/$pendingId/buildForkAgentLaunch.ts index 791e4350d5e..ee67e8a95fb 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/pending/$pendingId/buildForkAgentLaunch.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/pending/$pendingId/buildForkAgentLaunch.ts @@ -1,8 +1,10 @@ -import { isTerminalAgentDefinition } from "@superset/shared/agent-catalog"; +import { + type AgentDefinitionId, + isTerminalAgentDefinition, +} from "@superset/shared/agent-catalog"; import { buildPromptCommandFromAgentConfig, getCommandFromAgentConfig, - getFallbackAgentId, indexResolvedAgentConfigs, type ResolvedAgentConfig, } from "@superset/shared/agent-settings"; @@ -40,7 +42,7 @@ export interface ResolvedPrContent { export interface BuildForkAgentLaunchInputs { pending: Pick< PendingWorkspaceRow, - "projectId" | "prompt" | "linkedIssues" | "linkedPR" + "projectId" | "prompt" | "linkedIssues" | "linkedPR" | "agentId" >; attachments: LoadedAttachment[] | undefined; agentConfigs: ResolvedAgentConfig[]; @@ -124,7 +126,7 @@ export type PendingLaunchBuild = export async function buildForkAgentLaunch( inputs: BuildForkAgentLaunchInputs, ): Promise { - const agentId = getFallbackAgentId(inputs.agentConfigs); + const agentId = resolveAgentId(inputs.pending.agentId, inputs.agentConfigs); if (!agentId) return null; const agentConfig = indexResolvedAgentConfigs(inputs.agentConfigs).get( @@ -162,6 +164,17 @@ export async function buildForkAgentLaunch( return buildChatLaunch(spec, agentConfig); } +function resolveAgentId( + selected: string | null, + configs: ResolvedAgentConfig[], +): AgentDefinitionId | null { + if (!selected || selected === "none") return null; + const match = indexResolvedAgentConfigs(configs).get( + selected as AgentDefinitionId, + ); + return match?.enabled ? match.id : null; +} + // --------------------------------------------------------------------------- // Terminal launch assembly // --------------------------------------------------------------------------- diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/pending/$pendingId/buildIntentPayload.test.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/pending/$pendingId/buildIntentPayload.test.ts index 5ec07b468ff..9d51cc26f84 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/pending/$pendingId/buildIntentPayload.test.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/pending/$pendingId/buildIntentPayload.test.ts @@ -34,6 +34,7 @@ function makePending( runSetupScript: true, terminalLaunch: null, chatLaunch: null, + agentId: null, ...overrides, }; } diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/pending/$pendingId/dispatchForkLaunch.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/pending/$pendingId/dispatchForkLaunch.ts index 6d4decfec26..8fa665f02e1 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/pending/$pendingId/dispatchForkLaunch.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/pending/$pendingId/dispatchForkLaunch.ts @@ -17,7 +17,12 @@ export interface DispatchForkLaunchInputs { workspaceId: string; pending: Pick< PendingWorkspaceRow, - "projectId" | "prompt" | "linkedIssues" | "linkedPR" | "hostTarget" + | "projectId" + | "prompt" + | "linkedIssues" + | "linkedPR" + | "hostTarget" + | "agentId" >; loadedAttachments: LoadedAttachment[] | undefined; agentConfigs: ResolvedAgentConfig[]; diff --git a/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/PromptGroup/PromptGroup.tsx b/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/PromptGroup/PromptGroup.tsx index 598531a803f..3763b5d5e2a 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/PromptGroup/PromptGroup.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/PromptGroup/PromptGroup.tsx @@ -139,7 +139,7 @@ export function PromptGroup({ }); // ── Submit (fork) ──────────────────────────────────────────────── - const createWorkspace = useSubmitWorkspace(projectId); + const createWorkspace = useSubmitWorkspace(projectId, selectedAgent); const handleSubmit = useCallback( (files: SubmitAttachment[] = []) => { if (needsSetup) { diff --git a/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/PromptGroup/hooks/useSubmitWorkspace/useSubmitWorkspace.ts b/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/PromptGroup/hooks/useSubmitWorkspace/useSubmitWorkspace.ts index 8e721bf7c53..cf7c1bba346 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/PromptGroup/hooks/useSubmitWorkspace/useSubmitWorkspace.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/PromptGroup/hooks/useSubmitWorkspace/useSubmitWorkspace.ts @@ -4,6 +4,7 @@ import { useCallback } from "react"; import { storeAttachments } from "renderer/lib/pending-attachment-store"; import { useCollections } from "renderer/routes/_authenticated/providers/CollectionsProvider"; import { useDashboardNewWorkspaceDraft } from "../../../../../DashboardNewWorkspaceDraftContext"; +import type { WorkspaceCreateAgent } from "../../types"; import { resolveNames } from "./resolveNames"; export interface SubmitAttachment { @@ -24,7 +25,10 @@ export interface SubmitAttachment { * the library clears provider state + revokes blob URLs *before* * invoking onSubmit, so the ref is stale by the time we'd see it. */ -export function useSubmitWorkspace(projectId: string | null) { +export function useSubmitWorkspace( + projectId: string | null, + selectedAgent: WorkspaceCreateAgent, +) { const navigate = useNavigate(); const { closeAndResetDraft, draft } = useDashboardNewWorkspaceDraft(); const collections = useCollections(); @@ -83,6 +87,7 @@ export function useSubmitWorkspace(projectId: string | null) { linkedPR: draft.linkedPR, hostTarget: draft.hostTarget, attachmentCount: files.length, + agentId: selectedAgent, status: "creating", error: null, workspaceId: null, @@ -93,6 +98,13 @@ export function useSubmitWorkspace(projectId: string | null) { closeAndResetDraft(); void navigate({ to: `/pending/${pendingId}` as string }); }, - [closeAndResetDraft, collections, draft, navigate, projectId], + [ + closeAndResetDraft, + collections, + draft, + navigate, + projectId, + selectedAgent, + ], ); } diff --git a/apps/desktop/src/renderer/routes/_authenticated/providers/CollectionsProvider/dashboardSidebarLocal/schema.ts b/apps/desktop/src/renderer/routes/_authenticated/providers/CollectionsProvider/dashboardSidebarLocal/schema.ts index dedfac4dd47..41a80b2e969 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/providers/CollectionsProvider/dashboardSidebarLocal/schema.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/providers/CollectionsProvider/dashboardSidebarLocal/schema.ts @@ -185,6 +185,10 @@ export const pendingWorkspaceSchema = z.object({ linkedIssues: z.array(pendingLinkedIssueSchema).default([]), linkedPR: pendingLinkedPRSchema.nullable().default(null), attachmentCount: z.number().int().default(0), + // User-selected agent from the modal. `"none"` = user explicitly chose not + // to launch; any other string = `AgentDefinitionId`; null = legacy rows + // (predating this field), treated as "use fallback". + agentId: z.string().nullable().default(null), // fork + checkout (irrelevant for adopt — worktree already exists). runSetupScript: z.boolean().default(true), From 9bc6c38da4098972f1ffa2ea77da41d2603393b0 Mon Sep 17 00:00:00 2001 From: Satya Patel Date: Thu, 23 Apr 2026 19:24:04 -0700 Subject: [PATCH 2/2] test(desktop): update buildForkAgentLaunch tests for explicit agent selection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Tests were written against the old fallback-to-claude behavior — they passed `agentId: null` and expected Claude to launch. Under the new contract, null is treated as "no selection → no launch". Pass explicit `agentId` in each case. Add coverage for the new null / "none" paths. --- .../$pendingId/buildForkAgentLaunch.test.ts | 42 ++++++++++++++----- 1 file changed, 31 insertions(+), 11 deletions(-) diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/pending/$pendingId/buildForkAgentLaunch.test.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/pending/$pendingId/buildForkAgentLaunch.test.ts index 50f87fed64a..0bcd171395b 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/pending/$pendingId/buildForkAgentLaunch.test.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/pending/$pendingId/buildForkAgentLaunch.test.ts @@ -122,9 +122,12 @@ describe("buildForkAgentLaunch", () => { expect(build).toBeNull(); }); - test("prompt-only → terminal launch via default agent (claude)", async () => { + test("selected claude agent → terminal launch", async () => { const build = await buildForkAgentLaunch({ - pending: pendingBase({ prompt: "refactor the auth middleware" }), + pending: pendingBase({ + prompt: "refactor the auth middleware", + agentId: "claude", + }), attachments: undefined, agentConfigs, }); @@ -141,6 +144,7 @@ describe("buildForkAgentLaunch", () => { const build = await buildForkAgentLaunch({ pending: pendingBase({ prompt: "do it", + agentId: "claude", linkedIssues: [ { source: "internal", @@ -159,7 +163,7 @@ describe("buildForkAgentLaunch", () => { test("attachments produce disk-ready bytes + matching names", async () => { const build = await buildForkAgentLaunch({ - pending: pendingBase({ prompt: "fix" }), + pending: pendingBase({ prompt: "fix", agentId: "claude" }), attachments: [ { data: "data:text/plain;base64,AQID", // [1,2,3] @@ -179,13 +183,11 @@ describe("buildForkAgentLaunch", () => { }); test("chat agent → chat launch with initialPrompt + files", async () => { - const chatOnlyConfigs = agentConfigs.map((c) => - c.id === "superset-chat" - ? { ...c, enabled: true } - : { ...c, enabled: false }, - ); const build = await buildForkAgentLaunch({ - pending: pendingBase({ prompt: "help me refactor" }), + pending: pendingBase({ + prompt: "help me refactor", + agentId: "superset-chat", + }), attachments: [ { data: "data:text/plain;base64,AQID", @@ -193,7 +195,7 @@ describe("buildForkAgentLaunch", () => { filename: "logs.txt", }, ], - agentConfigs: chatOnlyConfigs, + agentConfigs, }); expect(build?.kind).toBe("chat"); if (build?.kind !== "chat") throw new Error("wrong kind"); @@ -208,10 +210,28 @@ describe("buildForkAgentLaunch", () => { test("disabled agent → null", async () => { const disabled = agentConfigs.map((c) => ({ ...c, enabled: false })); const build = await buildForkAgentLaunch({ - pending: pendingBase({ prompt: "hi" }), + pending: pendingBase({ prompt: "hi", agentId: "claude" }), attachments: undefined, agentConfigs: disabled, }); expect(build).toBeNull(); }); + + test("agentId null → null (no user selection, no launch)", async () => { + const build = await buildForkAgentLaunch({ + pending: pendingBase({ prompt: "hi", agentId: null }), + attachments: undefined, + agentConfigs, + }); + expect(build).toBeNull(); + }); + + test('agentId "none" → null (explicit opt-out)', async () => { + const build = await buildForkAgentLaunch({ + pending: pendingBase({ prompt: "hi", agentId: "none" }), + attachments: undefined, + agentConfigs, + }); + expect(build).toBeNull(); + }); });