From f632c2563610f84d56cbd116224aedc0ee40f7bf Mon Sep 17 00:00:00 2001 From: Satya Patel Date: Tue, 5 May 2026 19:30:14 -0700 Subject: [PATCH 1/5] feat(agents): launch Superset Chat sessions from desktop / CLI / SDK / MCP Folds the built-in Superset Chat agent into the existing `agents` sugar so the same one-call surface spawns terminal *and* chat sessions across every entry point. Picker today shows only the host's terminal-config rows; submitting `superset-chat` 404s on the host because `runAgentInWorkspace` only knows how to spawn PTYs. Plumbing changes: - `agents.ts runAgentInWorkspace` branches on chat-builtin ids: mints a sessionId, registers the cloud `chat_sessions` row, and fires the first turn through `ChatRuntimeManager.startTurn`. Returns a `kind`-tagged result so callers materialize the right pane. - `ChatRuntimeManager.startTurn` boots the runtime and queues the message, but returns as soon as streaming begins instead of awaiting the assistant's full reply (~30s -> ~5-8s for headless callers). - `getSnapshot` cold-start fast path: returns an empty skeleton immediately when no runtime is in memory while creation runs in the background. Renderer's "Loading conversation..." clears on the first poll instead of hanging on the harness boot. - Host's API client carries the bound organizationId via x-superset-organization-id. The session-exchanged JWT only ships organizationIds[]; without the header protectedProcedure middleware can't resolve activeOrganizationId and any host->cloud call hits "No active organization selected". Surface updates: - Desktop v2 picker: new useV2AgentChoices hook overlays builtin chat agents on top of the host's terminal rows. Wired into the new workspace modal and the two task launch pickers. - appendLaunchesToPaneLayout seeds a kind: "chat" pane for chat results. - CLI workspaces create --agent --prompt --attachment and agents run --attachment accept local file paths and upload them to the host's attachment store before launch. - agents list includes Superset Chat. - SDK + MCP-v2 result types gain the kind discriminator. JSDoc and tool descriptions document superset-chat as a valid agent value. --- .../renderer/components/AgentSelect/index.ts | 2 +- .../renderer/hooks/useV2AgentChoices/index.ts | 1 + .../useV2AgentChoices/useV2AgentChoices.ts | 45 +++++++++ .../OpenInWorkspaceV2/OpenInWorkspaceV2.tsx | 22 ++--- .../RunInWorkspacePopoverV2.tsx | 22 ++--- .../PromptGroup/PromptGroup.tsx | 25 ++--- .../appendLaunchesToPaneLayout.ts | 56 ++++++++--- bun.lock | 2 + packages/cli/package.json | 2 + .../cli/src/commands/agents/list/command.ts | 16 +++- .../cli/src/commands/agents/run/command.ts | 25 ++++- .../src/commands/workspaces/create/command.ts | 47 +++++++++ packages/cli/src/lib/upload-attachments.ts | 37 ++++++++ .../api/createApiClient/createApiClient.ts | 13 ++- packages/host-service/src/app.ts | 3 +- .../host-service/src/runtime/chat/chat.ts | 90 +++++++++++++++++- .../src/trpc/router/agents/agents.ts | 95 +++++++++++++++++-- packages/mcp-v2/src/tools/agents/run.ts | 11 ++- .../mcp-v2/src/tools/workspaces/create.ts | 7 +- packages/sdk/src/resources/agents.ts | 25 +++-- packages/sdk/src/resources/workspaces.ts | 8 +- 21 files changed, 454 insertions(+), 100 deletions(-) create mode 100644 apps/desktop/src/renderer/hooks/useV2AgentChoices/index.ts create mode 100644 apps/desktop/src/renderer/hooks/useV2AgentChoices/useV2AgentChoices.ts create mode 100644 packages/cli/src/lib/upload-attachments.ts diff --git a/apps/desktop/src/renderer/components/AgentSelect/index.ts b/apps/desktop/src/renderer/components/AgentSelect/index.ts index 18d93f87245..05ff42855b8 100644 --- a/apps/desktop/src/renderer/components/AgentSelect/index.ts +++ b/apps/desktop/src/renderer/components/AgentSelect/index.ts @@ -1 +1 @@ -export { AgentSelect } from "./AgentSelect"; +export { AgentSelect, type AgentSelectAgent } from "./AgentSelect"; diff --git a/apps/desktop/src/renderer/hooks/useV2AgentChoices/index.ts b/apps/desktop/src/renderer/hooks/useV2AgentChoices/index.ts new file mode 100644 index 00000000000..0474d9aab41 --- /dev/null +++ b/apps/desktop/src/renderer/hooks/useV2AgentChoices/index.ts @@ -0,0 +1 @@ +export { useV2AgentChoices } from "./useV2AgentChoices"; diff --git a/apps/desktop/src/renderer/hooks/useV2AgentChoices/useV2AgentChoices.ts b/apps/desktop/src/renderer/hooks/useV2AgentChoices/useV2AgentChoices.ts new file mode 100644 index 00000000000..9fd4ba03d43 --- /dev/null +++ b/apps/desktop/src/renderer/hooks/useV2AgentChoices/useV2AgentChoices.ts @@ -0,0 +1,45 @@ +import { + BUILTIN_AGENT_DEFINITIONS, + isChatAgentDefinition, +} from "@superset/shared/agent-catalog"; +import { useMemo } from "react"; +import type { AgentSelectAgent } from "renderer/components/AgentSelect"; +import { useV2AgentConfigs } from "renderer/hooks/useV2AgentConfigs"; + +interface UseV2AgentChoicesResult { + agents: AgentSelectAgent[]; + isFetched: boolean; +} + +/** + * Combines the host's configured terminal-agent rows with built-in chat + * agents (e.g. Superset Chat) into a single picker list. Chat agents are + * defined in shared (`BUILTIN_AGENT_DEFINITIONS`) and don't live in the + * host's `host_agent_configs` table — they're routed by id inside + * `runAgentInWorkspace`. Appended after the host's configured terminal + * rows so the user's preferred terminal agents stay at the top. + */ +export function useV2AgentChoices( + hostUrl: string | null, +): UseV2AgentChoicesResult { + const query = useV2AgentConfigs(hostUrl); + const agents = useMemo(() => { + const terminalAgents: AgentSelectAgent[] = (query.data ?? []).map( + (config) => ({ + id: config.id, + label: config.label, + iconId: config.presetId, + }), + ); + const chatAgents: AgentSelectAgent[] = BUILTIN_AGENT_DEFINITIONS.filter( + isChatAgentDefinition, + ).map((definition) => ({ + id: definition.id, + label: definition.label, + iconId: definition.id, + })); + return [...terminalAgents, ...chatAgents]; + }, [query.data]); + + return { agents, isFetched: query.isFetched }; +} diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/$taskId/components/PropertiesSidebar/components/OpenInWorkspaceV2/OpenInWorkspaceV2.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/$taskId/components/PropertiesSidebar/components/OpenInWorkspaceV2/OpenInWorkspaceV2.tsx index 0251be13627..d853bf926d9 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/$taskId/components/PropertiesSidebar/components/OpenInWorkspaceV2/OpenInWorkspaceV2.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/$taskId/components/PropertiesSidebar/components/OpenInWorkspaceV2/OpenInWorkspaceV2.tsx @@ -14,7 +14,7 @@ import { HiArrowRight, HiChevronDown } from "react-icons/hi2"; import { AgentSelect } from "renderer/components/AgentSelect"; import { env } from "renderer/env.renderer"; import { useHostUrl } from "renderer/hooks/host-service/useHostTargetUrl"; -import { useV2AgentConfigs } from "renderer/hooks/useV2AgentConfigs"; +import { useV2AgentChoices } from "renderer/hooks/useV2AgentChoices"; import { authClient } from "renderer/lib/auth-client"; import { DevicePicker } from "renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/components/DevicePicker"; import { useWorkspaceHostOptions } from "renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/components/DevicePicker/hooks/useWorkspaceHostOptions"; @@ -118,16 +118,8 @@ export function OpenInWorkspaceV2({ task }: OpenInWorkspaceV2Props) { }, [v2Projects, githubRepositories, setUpProjectIds]); const launchHostUrl = useHostUrl(hostId); - const v2AgentConfigsQuery = useV2AgentConfigs(launchHostUrl); - const v2Agents = useMemo( - () => - (v2AgentConfigsQuery.data ?? []).map((config) => ({ - id: config.id, - label: config.label, - iconId: config.presetId, - })), - [v2AgentConfigsQuery.data], - ); + const { agents: v2Agents, isFetched: v2AgentsFetched } = + useV2AgentChoices(launchHostUrl); const validAgentIds = useMemo( () => new Set(v2Agents.map((agent) => agent.id)), [v2Agents], @@ -154,7 +146,7 @@ export function OpenInWorkspaceV2({ task }: OpenInWorkspaceV2Props) { const [selectedAgent, setSelectedAgentState] = useState(readStoredAgent); useEffect(() => { - if (!v2AgentConfigsQuery.isFetched) return; + if (!v2AgentsFetched) return; if (selectedAgent !== NONE && validAgentIds.has(selectedAgent)) return; const stored = readStoredAgent(); if (stored !== NONE && validAgentIds.has(stored)) { @@ -162,7 +154,7 @@ export function OpenInWorkspaceV2({ task }: OpenInWorkspaceV2Props) { } else if (selectedAgent !== NONE) { setSelectedAgentState(NONE); } - }, [v2AgentConfigsQuery.isFetched, validAgentIds, selectedAgent]); + }, [v2AgentsFetched, validAgentIds, selectedAgent]); const setSelectedAgent = (next: SelectedAgent) => { setSelectedAgentState(next); if (typeof window !== "undefined") { @@ -200,7 +192,7 @@ export function OpenInWorkspaceV2({ task }: OpenInWorkspaceV2Props) { // query resolves and the corrective effect runs — block submission so // we don't send an id this host doesn't recognize. if (selectedAgent !== NONE) { - if (!v2AgentConfigsQuery.isFetched) return "Checking agents…"; + if (!v2AgentsFetched) return "Checking agents…"; if (!validAgentIds.has(selectedAgent)) { return "Selected agent is not available on this host"; } @@ -211,7 +203,7 @@ export function OpenInWorkspaceV2({ task }: OpenInWorkspaceV2Props) { selectedProject?.needsSetup, setUpProjectIds, selectedAgent, - v2AgentConfigsQuery.isFetched, + v2AgentsFetched, validAgentIds, hostId, machineId, diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/TasksTopBar/components/RunInWorkspacePopoverV2/RunInWorkspacePopoverV2.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/TasksTopBar/components/RunInWorkspacePopoverV2/RunInWorkspacePopoverV2.tsx index 31aacfc6791..d50d0dc339c 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/TasksTopBar/components/RunInWorkspacePopoverV2/RunInWorkspacePopoverV2.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/TasksTopBar/components/RunInWorkspacePopoverV2/RunInWorkspacePopoverV2.tsx @@ -17,7 +17,7 @@ import { HiCheck, HiMiniPlay } from "react-icons/hi2"; import { AgentSelect } from "renderer/components/AgentSelect"; import { env } from "renderer/env.renderer"; import { useHostUrl } from "renderer/hooks/host-service/useHostTargetUrl"; -import { useV2AgentConfigs } from "renderer/hooks/useV2AgentConfigs"; +import { useV2AgentChoices } from "renderer/hooks/useV2AgentChoices"; import { authClient } from "renderer/lib/auth-client"; import { DevicePicker } from "renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/components/DevicePicker"; import { useWorkspaceHostOptions } from "renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/components/DevicePicker/hooks/useWorkspaceHostOptions"; @@ -146,16 +146,8 @@ export function RunInWorkspacePopoverV2({ (project) => project.id === selectedProjectId, ); - const v2AgentConfigsQuery = useV2AgentConfigs(launchHostUrl); - const v2Agents = useMemo( - () => - (v2AgentConfigsQuery.data ?? []).map((config) => ({ - id: config.id, - label: config.label, - iconId: config.presetId, - })), - [v2AgentConfigsQuery.data], - ); + const { agents: v2Agents, isFetched: v2AgentsFetched } = + useV2AgentChoices(launchHostUrl); const validAgentIds = useMemo( () => new Set(v2Agents.map((agent) => agent.id)), [v2Agents], @@ -164,7 +156,7 @@ export function RunInWorkspacePopoverV2({ const [selectedAgent, setSelectedAgentState] = useState(readStoredAgent); useEffect(() => { - if (!v2AgentConfigsQuery.isFetched) return; + if (!v2AgentsFetched) return; if (selectedAgent !== NONE && validAgentIds.has(selectedAgent)) return; const stored = readStoredAgent(); if (stored !== NONE && validAgentIds.has(stored)) { @@ -172,7 +164,7 @@ export function RunInWorkspacePopoverV2({ } else if (selectedAgent !== NONE) { setSelectedAgentState(NONE); } - }, [v2AgentConfigsQuery.isFetched, validAgentIds, selectedAgent]); + }, [v2AgentsFetched, validAgentIds, selectedAgent]); const setSelectedAgent = (next: SelectedAgent) => { setSelectedAgentState(next); if (typeof window !== "undefined") { @@ -201,7 +193,7 @@ export function RunInWorkspacePopoverV2({ // Agent UUIDs are host-scoped; block until the host-specific config // query resolves and the selection is verified to exist there. if (selectedAgent !== NONE) { - if (!v2AgentConfigsQuery.isFetched) return "Checking agents…"; + if (!v2AgentsFetched) return "Checking agents…"; if (!validAgentIds.has(selectedAgent)) { return "Selected agent is not available on this host"; } @@ -212,7 +204,7 @@ export function RunInWorkspacePopoverV2({ selectedProject?.needsSetup, setUpProjectIds, selectedAgent, - v2AgentConfigsQuery.isFetched, + v2AgentsFetched, validAgentIds, hostId, machineId, 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 62d38dbb398..ba6ac2046df 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 @@ -25,7 +25,7 @@ import { LinkedIssuePill } from "renderer/components/Chat/ChatInterface/componen import { IssueLinkCommand } from "renderer/components/Chat/ChatInterface/components/IssueLinkCommand"; import { resolveHostUrl } from "renderer/hooks/host-service/useHostTargetUrl"; import { useAgentLaunchPreferences } from "renderer/hooks/useAgentLaunchPreferences"; -import { useV2AgentConfigs } from "renderer/hooks/useV2AgentConfigs"; +import { useV2AgentChoices } from "renderer/hooks/useV2AgentChoices"; import { PLATFORM } from "renderer/hotkeys"; import { authClient } from "renderer/lib/auth-client"; import { useLocalHostService } from "renderer/routes/_authenticated/providers/LocalHostServiceProvider"; @@ -129,16 +129,8 @@ export function PromptGroup({ }) ?? null ); }, [draft.hostId, machineId, activeHostUrl, activeOrganizationId]); - const v2AgentConfigsQuery = useV2AgentConfigs(launchHostUrl); - const v2Agents = useMemo( - () => - (v2AgentConfigsQuery.data ?? []).map((config) => ({ - id: config.id, - label: config.label, - iconId: config.presetId, - })), - [v2AgentConfigsQuery.data], - ); + const { agents: v2Agents, isFetched: v2AgentsFetched } = + useV2AgentChoices(launchHostUrl); const selectableAgentIds = useMemo( () => v2Agents.map((agent) => agent.id), [v2Agents], @@ -149,7 +141,7 @@ export function PromptGroup({ defaultAgent: "none", fallbackAgent: "none", validAgents: ["none", ...selectableAgentIds], - agentsReady: v2AgentConfigsQuery.isFetched, + agentsReady: v2AgentsFetched, }); // Promote the placeholder "none" → first configured agent whenever the @@ -160,7 +152,7 @@ export function PromptGroup({ // useAgentLaunchPreferences resets to "none"). The corrective effect // can't rescue these on its own because "none" is always in validAgents. useEffect(() => { - if (!v2AgentConfigsQuery.isFetched) return; + if (!v2AgentsFetched) return; if (selectedAgent !== "none") return; const stored = typeof window !== "undefined" @@ -169,12 +161,7 @@ export function PromptGroup({ if (stored === "none") return; const first = selectableAgentIds[0]; if (first) setSelectedAgent(first); - }, [ - v2AgentConfigsQuery.isFetched, - selectableAgentIds, - selectedAgent, - setSelectedAgent, - ]); + }, [v2AgentsFetched, selectableAgentIds, selectedAgent, setSelectedAgent]); const branchPreview = branchNameEdited ? sanitizeUserBranchName(branchName) diff --git a/apps/desktop/src/renderer/stores/workspace-creates/appendLaunchesToPaneLayout.ts b/apps/desktop/src/renderer/stores/workspace-creates/appendLaunchesToPaneLayout.ts index 213f6da9eba..c99f0f7f424 100644 --- a/apps/desktop/src/renderer/stores/workspace-creates/appendLaunchesToPaneLayout.ts +++ b/apps/desktop/src/renderer/stores/workspace-creates/appendLaunchesToPaneLayout.ts @@ -1,5 +1,9 @@ import { createWorkspaceStore, type WorkspaceState } from "@superset/panes"; -import type { PaneViewerData } from "renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/types"; +import type { + ChatPaneData, + PaneViewerData, + TerminalPaneData, +} from "renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/types"; const EMPTY_STATE: WorkspaceState = { version: 1, @@ -7,13 +11,21 @@ const EMPTY_STATE: WorkspaceState = { activeTabId: null, }; +type AgentLaunchResult = + | { ok: true; kind: "terminal"; sessionId: string; label: string } + | { ok: true; kind: "chat"; sessionId: string; label: string } + | { ok: false; error: string }; + interface AppendArgs { existing: WorkspaceState | undefined; terminals: Array<{ terminalId: string; label?: string }>; - agents: Array< - | { ok: true; sessionId: string; label: string } - | { ok: false; error: string } - >; + agents: AgentLaunchResult[]; +} + +interface PaneLaunch { + kind: "terminal" | "chat"; + sessionId: string; + label?: string; } export function appendLaunchesToPaneLayout({ @@ -21,12 +33,19 @@ export function appendLaunchesToPaneLayout({ terminals, agents, }: AppendArgs): WorkspaceState { - const launches = [ - ...terminals, - ...agents - .filter((entry): entry is Extract => entry.ok) - .map((entry) => ({ terminalId: entry.sessionId, label: entry.label })), - ]; + const terminalLaunches: PaneLaunch[] = terminals.map((entry) => ({ + kind: "terminal", + sessionId: entry.terminalId, + label: entry.label, + })); + const agentLaunches: PaneLaunch[] = agents + .filter((entry): entry is Extract => entry.ok) + .map((entry) => ({ + kind: entry.kind, + sessionId: entry.sessionId, + label: entry.label, + })); + const launches = [...terminalLaunches, ...agentLaunches]; if (launches.length === 0) { return existing ?? EMPTY_STATE; @@ -40,10 +59,17 @@ export function appendLaunchesToPaneLayout({ store.getState().addTab({ titleOverride: launch.label, panes: [ - { - kind: "terminal", - data: { terminalId: launch.terminalId }, - }, + launch.kind === "chat" + ? { + kind: "chat", + data: { sessionId: launch.sessionId } satisfies ChatPaneData, + } + : { + kind: "terminal", + data: { + terminalId: launch.sessionId, + } satisfies TerminalPaneData, + }, ], }); } diff --git a/bun.lock b/bun.lock index 0415df4561f..01acc759bed 100644 --- a/bun.lock +++ b/bun.lock @@ -687,11 +687,13 @@ "@trpc/client": "^11.7.1", "date-fns": "^4.1.0", "ink": "^7.0.1", + "mime-types": "^3.0.2", "react": "19.2.0", "superjson": "^2.2.5", }, "devDependencies": { "@superset/typescript": "workspace:*", + "@types/mime-types": "^3.0.1", "@types/react": "~19.2.2", "bun-types": "^1.3.1", "typescript": "^5.9.3", diff --git a/packages/cli/package.json b/packages/cli/package.json index 7e1ce8766d5..bc33d9f5230 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -21,11 +21,13 @@ "@trpc/client": "^11.7.1", "date-fns": "^4.1.0", "ink": "^7.0.1", + "mime-types": "^3.0.2", "react": "19.2.0", "superjson": "^2.2.5" }, "devDependencies": { "@superset/typescript": "workspace:*", + "@types/mime-types": "^3.0.1", "@types/react": "~19.2.2", "bun-types": "^1.3.1", "typescript": "^5.9.3" diff --git a/packages/cli/src/commands/agents/list/command.ts b/packages/cli/src/commands/agents/list/command.ts index 464d549c507..fdf5fbff18c 100644 --- a/packages/cli/src/commands/agents/list/command.ts +++ b/packages/cli/src/commands/agents/list/command.ts @@ -1,4 +1,8 @@ import { boolean, CLIError, string, table } from "@superset/cli-framework"; +import { + BUILTIN_AGENT_DEFINITIONS, + isChatAgentDefinition, +} from "@superset/shared/agent-catalog"; import { command } from "../../../lib/command"; import { requireHostTarget, resolveHostTarget } from "../../../lib/host-target"; @@ -31,6 +35,16 @@ export default command({ userJwt: ctx.bearer, }); - return target.client.settings.agentConfigs.list.query(); + const terminalConfigs = + await target.client.settings.agentConfigs.list.query(); + const chatBuiltins = BUILTIN_AGENT_DEFINITIONS.filter( + isChatAgentDefinition, + ).map((definition) => ({ + id: definition.id, + presetId: definition.id, + label: definition.label, + command: "(superset chat runtime)", + })); + return [...terminalConfigs, ...chatBuiltins]; }, }); diff --git a/packages/cli/src/commands/agents/run/command.ts b/packages/cli/src/commands/agents/run/command.ts index 7cc37ce80b6..dc484d5deee 100644 --- a/packages/cli/src/commands/agents/run/command.ts +++ b/packages/cli/src/commands/agents/run/command.ts @@ -1,6 +1,7 @@ import { CLIError, string } from "@superset/cli-framework"; import { command } from "../../../lib/command"; import { resolveHostTarget } from "../../../lib/host-target"; +import { uploadAttachments } from "../../../lib/upload-attachments"; export default command({ description: "Launch an agent inside an existing workspace", @@ -8,11 +9,18 @@ export default command({ workspace: string().required().desc("Workspace ID"), agent: string() .required() - .desc("Agent preset id (e.g. claude) or instance id"), + .desc( + "Agent preset id (e.g. `claude`), HostAgentConfig instance UUID, or `superset-chat` for a Superset Chat session", + ), prompt: string().required().desc("Prompt sent to the agent"), attachmentId: string() .variadic() - .desc("Attachment UUID; pass --attachment-id repeatedly"), + .desc("Pre-uploaded attachment UUID; pass --attachment-id repeatedly"), + attachment: string() + .variadic() + .desc( + "Local file path to upload as an attachment to the host. Repeatable", + ), }, run: async ({ ctx, options }) => { const organizationId = ctx.config.organizationId; @@ -34,16 +42,25 @@ export default command({ userJwt: ctx.bearer, }); + const uploadedIds = options.attachment + ? await uploadAttachments(target.client, options.attachment) + : []; + const attachmentIds = [...(options.attachmentId ?? []), ...uploadedIds]; + const result = await target.client.agents.run.mutate({ workspaceId: options.workspace, agent: options.agent, prompt: options.prompt, - attachmentIds: options.attachmentId, + attachmentIds: attachmentIds.length > 0 ? attachmentIds : undefined, }); + const sessionDescriptor = + result.kind === "chat" + ? `chat session ${result.sessionId}` + : `terminal ${result.sessionId}`; return { data: result, - message: `Launched ${result.label} (terminal ${result.sessionId}) in workspace ${options.workspace}`, + message: `Launched ${result.label} (${sessionDescriptor}) in workspace ${options.workspace}`, }; }, }); diff --git a/packages/cli/src/commands/workspaces/create/command.ts b/packages/cli/src/commands/workspaces/create/command.ts index 8d2d93d2a50..450bc53e58a 100644 --- a/packages/cli/src/commands/workspaces/create/command.ts +++ b/packages/cli/src/commands/workspaces/create/command.ts @@ -1,6 +1,7 @@ import { boolean, CLIError, number, string } from "@superset/cli-framework"; import { command } from "../../../lib/command"; import { requireHostTarget, resolveHostTarget } from "../../../lib/host-target"; +import { uploadAttachments } from "../../../lib/upload-attachments"; export default command({ description: "Create a workspace on a host", @@ -14,6 +15,17 @@ export default command({ baseBranch: string().desc( "Branch to fork from when `branch` does not exist (defaults to project default)", ), + agent: string().desc( + "Agent to spawn after creation. Preset id (`claude`, `codex`, …), HostAgentConfig instance UUID, or `superset-chat`", + ), + prompt: string().desc( + "Initial prompt the agent starts with. Required when --agent is set", + ), + attachment: string() + .variadic() + .desc( + "Local file path to upload as an attachment to the host. Repeatable. Only used when --agent is set", + ), }, run: async ({ ctx, options }) => { const organizationId = ctx.config.organizationId; @@ -28,6 +40,25 @@ export default command({ ); } + if (options.prompt && !options.agent) { + throw new CLIError( + "--prompt requires --agent", + "Pass --agent alongside --prompt", + ); + } + if (options.agent && !options.prompt) { + throw new CLIError( + "--agent requires --prompt", + "Pass --prompt alongside --agent", + ); + } + if (options.attachment && options.attachment.length > 0 && !options.agent) { + throw new CLIError( + "--attachment requires --agent", + "Attachments are only meaningful when launching an agent", + ); + } + const hostId = requireHostTarget({ host: options.host ?? undefined, local: options.local ?? undefined, @@ -39,12 +70,28 @@ export default command({ userJwt: ctx.bearer, }); + const attachmentIds = options.attachment + ? await uploadAttachments(target.client, options.attachment) + : []; + + const agents = + options.agent && options.prompt + ? [ + { + agent: options.agent, + prompt: options.prompt, + ...(attachmentIds.length > 0 ? { attachmentIds } : {}), + }, + ] + : undefined; + const result = await target.client.workspaces.create.mutate({ projectId: options.project, name: options.name, branch: options.branch, pr: options.pr, baseBranch: options.baseBranch, + agents, }); return { diff --git a/packages/cli/src/lib/upload-attachments.ts b/packages/cli/src/lib/upload-attachments.ts new file mode 100644 index 00000000000..7544a28199b --- /dev/null +++ b/packages/cli/src/lib/upload-attachments.ts @@ -0,0 +1,37 @@ +import { readFileSync } from "node:fs"; +import { basename } from "node:path"; +import { CLIError } from "@superset/cli-framework"; +import mimeTypes from "mime-types"; +import type { HostServiceClient } from "./host-target"; + +/** + * Upload local files to the target host's attachment store and return the + * resulting attachment ids in the same order. The host stores bytes per + * org and returns opaque UUIDs the agent runner resolves on the host + * side. CLI never sees the on-disk path. + */ +export async function uploadAttachments( + client: HostServiceClient, + paths: string[], +): Promise { + if (paths.length === 0) return []; + const ids: string[] = []; + for (const path of paths) { + const filename = basename(path); + const mediaType = mimeTypes.lookup(filename); + if (!mediaType) { + throw new CLIError( + `Could not determine media type for attachment: ${path}`, + "Use a recognizable file extension (e.g. .png, .pdf, .md)", + ); + } + const bytes = readFileSync(path); + const result = await client.attachments.upload.mutate({ + data: { kind: "base64", data: bytes.toString("base64") }, + mediaType, + originalFilename: filename, + }); + ids.push(result.attachmentId); + } + return ids; +} diff --git a/packages/host-service/src/api/createApiClient/createApiClient.ts b/packages/host-service/src/api/createApiClient/createApiClient.ts index 27a5c84577b..8f82db20daa 100644 --- a/packages/host-service/src/api/createApiClient/createApiClient.ts +++ b/packages/host-service/src/api/createApiClient/createApiClient.ts @@ -1,3 +1,4 @@ +import { ORGANIZATION_HEADER } from "@superset/shared/constants"; import type { AppRouter } from "@superset/trpc"; import { createTRPCClient, httpBatchLink, type TRPCLink } from "@trpc/client"; import { observable } from "@trpc/server/observable"; @@ -53,6 +54,7 @@ function retryOnUnauthorizedLink( export function createApiClient( baseUrl: string, authProvider: ApiAuthProvider, + organizationId: string, ): ApiClient { return createTRPCClient({ links: [ @@ -61,7 +63,16 @@ export function createApiClient( url: `${baseUrl}/api/trpc`, transformer: SuperJSON, async headers() { - return authProvider.getHeaders(); + // Pin every host→cloud request to this host's bound org. The + // host's session-exchanged JWT (better-auth jwt plugin) only + // carries `organizationIds`, not a singular active org, so + // `protectedProcedure` would otherwise reject any call that + // reads `ctx.activeOrganizationId`. The cloud middleware + // validates membership before honoring this header. + return { + ...(await authProvider.getHeaders()), + [ORGANIZATION_HEADER]: organizationId, + }; }, }), ], diff --git a/packages/host-service/src/app.ts b/packages/host-service/src/app.ts index 47c0ce85a1e..554cb11fe70 100644 --- a/packages/host-service/src/app.ts +++ b/packages/host-service/src/app.ts @@ -61,7 +61,8 @@ export function createApp(options: CreateAppOptions): CreateAppResult { const { config, providers } = options; const api = - options.api ?? createApiClient(config.cloudApiUrl, providers.auth); + options.api ?? + createApiClient(config.cloudApiUrl, providers.auth, config.organizationId); const db = options.db ?? createDb(config.dbPath, config.migrationsFolder); const git = createGitFactory(providers.credentials); const github = diff --git a/packages/host-service/src/runtime/chat/chat.ts b/packages/host-service/src/runtime/chat/chat.ts index a0df961b362..d80ee0c6148 100644 --- a/packages/host-service/src/runtime/chat/chat.ts +++ b/packages/host-service/src/runtime/chat/chat.ts @@ -555,6 +555,36 @@ When you need to ask the user ANY question — including simple yes/no, confirma return promise; } + /** + * Returns the in-memory runtime if one exists, otherwise null. Kicks off + * creation in the background as a side-effect so the next call (typically + * the next snapshot poll) finds it warm. Used by read-only paths that + * shouldn't block on the 3–8s harness boot — chat panes mounting against + * an evicted session, or sessions spawned from CLI/SDK/MCP whose harness + * isn't yet booted on this host. + */ + private peekRuntime( + sessionId: string, + workspaceId: string, + ): RuntimeSession | null { + const existing = this.runtimes.get(sessionId); + if (existing) { + if (existing.workspaceId !== workspaceId) { + throw new Error( + `Session ${sessionId} is already bound to workspace ${existing.workspaceId}`, + ); + } + return existing; + } + + // Trigger background creation but don't await it. The inflight map + // dedupes concurrent peeks, so we won't create multiple runtimes. + void this.getOrCreateRuntime(sessionId, workspaceId).catch(() => { + // Errors surface on the next read path that awaits the promise. + }); + return null; + } + /** * Tear down the in-memory runtime for a session. Aborts any in-flight * work, disconnects MCP servers, removes the runtime from the manager's @@ -686,6 +716,13 @@ When you need to ask the user ANY question — including simple yes/no, confirma * harness state can change between the displayState read and the messages * read. This still removes the *client-side* two-query race, which is the * one that caused mismatched message/display state. + * + * Cold-start fast path: if the runtime isn't yet booted (chat pane + * mounting against a freshly-loaded host, or a session spawned via + * `agents.run` whose harness was evicted) return an empty skeleton + * snapshot immediately and kick off creation in the background. The + * renderer's loading spinner clears after one poll instead of blocking + * on the 3–8s harness boot. The next poll picks up the warm runtime. */ async getSnapshot(input: { sessionId: string; @@ -694,10 +731,19 @@ When you need to ask the user ANY question — including simple yes/no, confirma displayState: ChatDisplayState; messages: RuntimeMessages; }> { - const runtime = await this.getOrCreateRuntime( - input.sessionId, - input.workspaceId, - ); + const runtime = this.peekRuntime(input.sessionId, input.workspaceId); + if (!runtime) { + return { + displayState: { + isRunning: false, + currentMessage: null, + pendingQuestion: null, + errorMessage: null, + } as unknown as ChatDisplayState, + messages: [] as unknown as RuntimeMessages, + }; + } + const displayState = this.buildDisplayState(runtime); const messages = await runtime.harness.listMessages(); // Intentionally no observedAt: when the harness state hasn't changed, @@ -732,6 +778,42 @@ When you need to ask the user ANY question — including simple yes/no, confirma return runtime.harness.sendMessage(input.payload); } + /** + * Boot the runtime and start the turn, but resolve as soon as streaming + * has begun — don't block on the assistant finishing. Used by headless + * launches (`agents.run` from CLI/SDK/MCP) where the caller doesn't have + * a UI to consume the stream and waiting 30s for the turn to end is just + * dead time. The harness keeps streaming in the background; the renderer + * picks up state via the next `getSnapshot` poll when a chat pane opens. + */ + async startTurn(input: ChatSendMessageInput): Promise { + const runtime = await this.getOrCreateRuntime( + input.sessionId, + input.workspaceId, + ); + runtime.lastErrorMessage = null; + + const selectedModel = input.metadata?.model?.trim(); + if (selectedModel) { + await runtime.harness.switchModel({ + modelId: selectedModel, + scope: "thread", + }); + } + + const thinkingLevel = input.metadata?.thinkingLevel; + if (thinkingLevel) { + await runtime.harness.setState({ thinkingLevel }); + } + + // Fire and forget: surface the error onto the runtime so the next + // snapshot reports it via `displayState.errorMessage`, but don't + // reject the calling promise. + void runtime.harness.sendMessage(input.payload).catch((error) => { + runtime.lastErrorMessage = toRuntimeErrorMessage(error); + }); + } + async restartFromMessage(input: RestartPayload): Promise { const runtime = await this.getOrCreateRuntime( input.sessionId, diff --git a/packages/host-service/src/trpc/router/agents/agents.ts b/packages/host-service/src/trpc/router/agents/agents.ts index 09838787f3c..1b47c847fc3 100644 --- a/packages/host-service/src/trpc/router/agents/agents.ts +++ b/packages/host-service/src/trpc/router/agents/agents.ts @@ -1,9 +1,15 @@ +import { readFileSync } from "node:fs"; +import { + BUILTIN_AGENT_DEFINITIONS, + isChatAgentDefinition, +} from "@superset/shared/agent-catalog"; import { TRPCError } from "@trpc/server"; import { asc, eq } from "drizzle-orm"; import { z } from "zod"; import type { HostDb } from "../../../db"; import { hostAgentConfigs } from "../../../db/schema"; import { createTerminalSessionInternal } from "../../../terminal/terminal"; +import type { HostServiceContext } from "../../../types"; import { protectedProcedure, router } from "../../index"; import { resolveAttachmentPath } from "../attachments/storage"; @@ -157,17 +163,70 @@ export interface AgentRunInput { attachmentIds?: string[]; } -export interface AgentRunResult { - sessionId: string; - label: string; +export type AgentRunResult = + | { kind: "terminal"; sessionId: string; label: string } + | { kind: "chat"; sessionId: string; label: string }; + +function resolveChatBuiltin(agent: string) { + const definition = BUILTIN_AGENT_DEFINITIONS.find( + (item) => item.id === agent, + ); + return definition && isChatAgentDefinition(definition) ? definition : null; } -/** - * Launch an agent against a workspace. Pure function over (db, eventBus, - * input) so `workspaces.create` can invoke it directly for the `agents` - * sugar without going back through tRPC. - */ -export async function runAgentInWorkspace( +async function resolveAttachmentsAsFiles( + attachmentIds: string[], +): Promise> { + return attachmentIds.map((attachmentId) => { + const resolved = resolveAttachmentPath(attachmentId); + if (!resolved) { + throw new TRPCError({ + code: "NOT_FOUND", + message: `Attachment not found: ${attachmentId}`, + }); + } + const bytes = readFileSync(resolved.path); + const data = `data:${resolved.metadata.mediaType};base64,${bytes.toString("base64")}`; + return { + data, + mediaType: resolved.metadata.mediaType, + ...(resolved.metadata.originalFilename + ? { filename: resolved.metadata.originalFilename } + : {}), + }; + }); +} + +async function runChatAgent( + ctx: HostServiceContext, + input: AgentRunInput, + label: string, +): Promise { + const sessionId = crypto.randomUUID(); + const files = await resolveAttachmentsAsFiles(input.attachmentIds ?? []); + + await ctx.api.chat.createSession.mutate({ + sessionId, + v2WorkspaceId: input.workspaceId, + }); + + // Fire-and-forget the turn: the caller (CLI/SDK/MCP) gets the sessionId + // back as soon as the runtime is ready, instead of waiting ~30s for the + // assistant stream to finish. The renderer materializes the streaming + // state via `chat.getSnapshot` when a chat pane opens. + await ctx.runtime.chat.startTurn({ + sessionId, + workspaceId: input.workspaceId, + payload: { + content: input.prompt, + ...(files.length > 0 ? { files } : {}), + }, + }); + + return { kind: "chat", sessionId, label }; +} + +async function runTerminalAgent( ctx: { db: HostDb; eventBus: import("../../../events").EventBus }, input: AgentRunInput, ): Promise { @@ -212,11 +271,29 @@ export async function runAgentInWorkspace( } return { + kind: "terminal", sessionId: result.terminalId, label: config.label, }; } +/** + * Launch an agent against a workspace. Routes by agent kind: chat-builtin + * ids fire a first message through the host's ChatRuntimeManager (no PTY); + * everything else resolves to a terminal preset and spawns a PTY. Both + * return a `kind`-tagged result so callers materialize the right pane. + */ +export async function runAgentInWorkspace( + ctx: HostServiceContext, + input: AgentRunInput, +): Promise { + const chatBuiltin = resolveChatBuiltin(input.agent); + if (chatBuiltin) { + return runChatAgent(ctx, input, chatBuiltin.label); + } + return runTerminalAgent(ctx, input); +} + export const agentsRouter = router({ run: protectedProcedure .input( diff --git a/packages/mcp-v2/src/tools/agents/run.ts b/packages/mcp-v2/src/tools/agents/run.ts index e37c8e33a87..65c8e906a95 100644 --- a/packages/mcp-v2/src/tools/agents/run.ts +++ b/packages/mcp-v2/src/tools/agents/run.ts @@ -8,7 +8,7 @@ export function register(server: McpServer): void { defineTool(server, { name: "agents_run", description: - "Launch an agent inside an existing workspace. Resolves the host that owns the workspace, then runs the named agent preset (or HostAgentConfig instance) with the given prompt in a fresh terminal session. Use this to start a second agent in a workspace that already exists; for create-and-spawn in a single call, pass `agents` to workspaces_create instead.", + "Launch an agent inside an existing workspace. Resolves the host that owns the workspace, then runs the named agent preset (or HostAgentConfig instance) with the given prompt. Terminal agents spawn a fresh PTY session; `superset-chat` spawns a Superset Chat session (mastracode runtime) instead. The result's `kind` field discriminates. Use this to start a second agent in a workspace that already exists; for create-and-spawn in a single call, pass `agents` to workspaces_create instead.", inputSchema: { workspaceId: z .string() @@ -18,14 +18,14 @@ export function register(server: McpServer): void { .string() .min(1) .describe( - "Agent preset id (e.g. `claude`, `codex`) or HostAgentConfig instance UUID.", + "Agent preset id (e.g. `claude`, `codex`), HostAgentConfig instance UUID, or `superset-chat` for a Superset Chat session.", ), prompt: z.string().min(1).describe("Prompt sent to the agent."), attachmentIds: z .array(z.string().uuid()) .optional() .describe( - "Host-scoped attachment UUIDs. The host resolves these to absolute paths and appends them to the prompt.", + "Host-scoped attachment UUIDs. For terminal agents the host appends a paths block to the prompt; for `superset-chat` the host inlines the file bytes as base64 data URLs on the chat message.", ), }, handler: async (input, ctx) => { @@ -38,7 +38,10 @@ export function register(server: McpServer): void { throw new Error(`Workspace not found: ${input.workspaceId}`); } - return hostServiceCall<{ sessionId: string; label: string }>( + return hostServiceCall< + | { kind: "terminal"; sessionId: string; label: string } + | { kind: "chat"; sessionId: string; label: string } + >( { relayUrl: ctx.relayUrl, organizationId: ctx.organizationId, diff --git a/packages/mcp-v2/src/tools/workspaces/create.ts b/packages/mcp-v2/src/tools/workspaces/create.ts index ee5c3a601e5..5601c36debe 100644 --- a/packages/mcp-v2/src/tools/workspaces/create.ts +++ b/packages/mcp-v2/src/tools/workspaces/create.ts @@ -8,14 +8,14 @@ const agentLaunchSchema = z.object({ .string() .min(1) .describe( - "Agent preset id (e.g. `claude`, `codex`) or HostAgentConfig instance UUID.", + "Agent preset id (e.g. `claude`, `codex`), HostAgentConfig instance UUID, or `superset-chat` to spawn a Superset Chat session instead of a terminal.", ), prompt: z.string().min(1).describe("Initial prompt the agent starts with."), attachmentIds: z .array(z.string().uuid()) .optional() .describe( - "Host-scoped attachment UUIDs. The host resolves these to absolute paths and appends them to the prompt.", + "Host-scoped attachment UUIDs. For terminal agents the host appends a paths block to the prompt; for `superset-chat` the host inlines the file bytes as base64 data URLs on the chat message.", ), }); @@ -74,7 +74,8 @@ export function register(server: McpServer): void { }; terminals: Array<{ terminalId: string; label?: string }>; agents: Array< - | { ok: true; sessionId: string; label: string } + | { ok: true; kind: "terminal"; sessionId: string; label: string } + | { ok: true; kind: "chat"; sessionId: string; label: string } | { ok: false; error: string } >; alreadyExists: boolean; diff --git a/packages/sdk/src/resources/agents.ts b/packages/sdk/src/resources/agents.ts index 5c90f4a03bd..eb53114ccef 100644 --- a/packages/sdk/src/resources/agents.ts +++ b/packages/sdk/src/resources/agents.ts @@ -99,11 +99,20 @@ export interface AgentListParams { export interface AgentRunParams { /** Workspace UUID to run the agent in. */ workspaceId: string; - /** Agent preset id (e.g. `"claude"`) or HostAgentConfig instance UUID. */ + /** + * Agent preset id (e.g. `"claude"`), HostAgentConfig instance UUID, or + * `"superset-chat"` to spawn a Superset Chat session instead of a + * terminal. Chat sessions live as a host-side mastracode runtime; the + * caller materializes a chat pane against the returned `sessionId`. + */ agent: string; /** Prompt sent to the agent. */ prompt: string; - /** Host-scoped attachment ids; host resolves to absolute paths in the prompt. */ + /** + * Host-scoped attachment ids. For terminal agents the host appends a + * paths block to the prompt; for `superset-chat` the host inlines the + * file bytes as base64 data URLs on the chat message. + */ attachmentIds?: string[]; } @@ -111,10 +120,14 @@ interface HostLookup { hostId: string; } -export interface AgentRunResult { - sessionId: string; - label: string; -} +/** + * Tagged with `kind` so callers know whether they got a terminal session + * (sessionId addresses a PTY on the host) or a chat session (sessionId + * addresses a mastracode runtime in `ChatRuntimeManager`). + */ +export type AgentRunResult = + | { kind: "terminal"; sessionId: string; label: string } + | { kind: "chat"; sessionId: string; label: string }; export declare namespace Agents { export type { diff --git a/packages/sdk/src/resources/workspaces.ts b/packages/sdk/src/resources/workspaces.ts index 936c92d8723..40beb4a89ae 100644 --- a/packages/sdk/src/resources/workspaces.ts +++ b/packages/sdk/src/resources/workspaces.ts @@ -147,7 +147,10 @@ export interface WorkspaceCreateParams { } export interface WorkspaceAgentLaunch { - /** Agent preset id (e.g. `"claude"`) or HostAgentConfig instance id. */ + /** + * Agent preset id (e.g. `"claude"`), HostAgentConfig instance id, or + * `"superset-chat"` for a Superset Chat session. + */ agent: string; /** What to tell the agent. */ prompt: string; @@ -156,7 +159,8 @@ export interface WorkspaceAgentLaunch { } export type WorkspaceCreateAgentResult = - | { ok: true; sessionId: string; label: string } + | { ok: true; kind: "terminal"; sessionId: string; label: string } + | { ok: true; kind: "chat"; sessionId: string; label: string } | { ok: false; error: string }; export interface WorkspaceCreateResult { From b88533f46860b69168857d81d65248290d53c0b7 Mon Sep 17 00:00:00 2001 From: Satya Patel Date: Tue, 5 May 2026 20:08:34 -0700 Subject: [PATCH 2/5] refactor(agents): trim chat launch path, rename superset-chat -> superset - Drop ChatRuntimeManager.startTurn and the cold-start workarounds (peekRuntime, skeleton snapshot). runChatAgent now just void-fires chat.sendMessage and returns the sessionId. CLI agents run goes from ~18s to ~2s; runtime boot finishes async on the host. - Rename superset-chat -> superset (id, type literals, tests, mcp v1, icon map). Label "Superset Chat" -> "Superset" everywhere it appeared (mcp / cli / sdk / agent-card / orchestrator title). - Trim function-level JSDocs that restated the code. --- .../useV2AgentChoices/useV2AgentChoices.ts | 12 +-- .../adapters/chat-adapter.ts | 2 +- .../components/AgentCard/agent-card.utils.ts | 6 +- .../cli/src/commands/agents/list/command.ts | 2 +- .../cli/src/commands/agents/run/command.ts | 2 +- .../src/commands/workspaces/create/command.ts | 2 +- packages/cli/src/lib/upload-attachments.ts | 6 -- .../host-service/src/runtime/chat/chat.ts | 90 +------------------ .../src/trpc/router/agents/agents.ts | 36 ++++---- .../router/settings/agent-configs.test.ts | 6 +- packages/mcp-v2/src/tools/agents/run.ts | 6 +- .../mcp-v2/src/tools/workspaces/create.ts | 4 +- .../devices/start-agent-session/shared.ts | 8 +- .../start-agent-session-with-prompt.ts | 2 +- .../start-agent-session.ts | 2 +- packages/sdk/src/resources/agents.ts | 4 +- packages/sdk/src/resources/workspaces.ts | 2 +- packages/shared/src/agent-catalog.ts | 8 +- .../shared/src/agent-launch-request.test.ts | 8 +- packages/shared/src/agent-launch.test.ts | 2 +- packages/shared/src/agent-launch.ts | 8 +- packages/shared/src/agent-settings.test.ts | 10 +-- packages/shared/src/host-agent-presets.ts | 2 +- .../ui/src/assets/icons/preset-icons/index.ts | 1 - 24 files changed, 68 insertions(+), 163 deletions(-) diff --git a/apps/desktop/src/renderer/hooks/useV2AgentChoices/useV2AgentChoices.ts b/apps/desktop/src/renderer/hooks/useV2AgentChoices/useV2AgentChoices.ts index 9fd4ba03d43..0f136f7006a 100644 --- a/apps/desktop/src/renderer/hooks/useV2AgentChoices/useV2AgentChoices.ts +++ b/apps/desktop/src/renderer/hooks/useV2AgentChoices/useV2AgentChoices.ts @@ -11,14 +11,10 @@ interface UseV2AgentChoicesResult { isFetched: boolean; } -/** - * Combines the host's configured terminal-agent rows with built-in chat - * agents (e.g. Superset Chat) into a single picker list. Chat agents are - * defined in shared (`BUILTIN_AGENT_DEFINITIONS`) and don't live in the - * host's `host_agent_configs` table — they're routed by id inside - * `runAgentInWorkspace`. Appended after the host's configured terminal - * rows so the user's preferred terminal agents stay at the top. - */ +// Built-in chat agents aren't in the host's `host_agent_configs` table — +// they're routed by id inside `runAgentInWorkspace`. Append after the +// host's terminal rows so the user's preferred terminal agents stay on +// top of the picker. export function useV2AgentChoices( hostUrl: string | null, ): UseV2AgentChoicesResult { diff --git a/apps/desktop/src/renderer/lib/agent-session-orchestrator/adapters/chat-adapter.ts b/apps/desktop/src/renderer/lib/agent-session-orchestrator/adapters/chat-adapter.ts index ab0e91b6e95..d290eb2f7ca 100644 --- a/apps/desktop/src/renderer/lib/agent-session-orchestrator/adapters/chat-adapter.ts +++ b/apps/desktop/src/renderer/lib/agent-session-orchestrator/adapters/chat-adapter.ts @@ -75,7 +75,7 @@ export async function launchChatAdapter( paneId = created.paneId; } - tabs.setTabAutoTitle(tabId, "Superset Chat"); + tabs.setTabAutoTitle(tabId, "Superset"); const pane = tabs.getPane(paneId); let sessionId = request.chat.sessionId ?? pane?.chat?.sessionId ?? null; diff --git a/apps/desktop/src/renderer/routes/_authenticated/settings/agents/components/AgentsSettings/components/AgentCard/agent-card.utils.ts b/apps/desktop/src/renderer/routes/_authenticated/settings/agents/components/AgentsSettings/components/AgentCard/agent-card.utils.ts index f54f6462ab5..dd761ac9f13 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/settings/agents/components/AgentsSettings/components/AgentCard/agent-card.utils.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/settings/agents/components/AgentsSettings/components/AgentCard/agent-card.utils.ts @@ -23,7 +23,7 @@ export function getPreviewPrompt(preset: ResolvedAgentConfig): string { export function getPreviewNoPromptCommand(preset: ResolvedAgentConfig): string { if (preset.kind !== "terminal") { - return "Superset Chat opens a chat pane without a shell command."; + return "Superset opens a chat pane without a shell command."; } return preset.command.trim() || "No command configured."; @@ -32,8 +32,8 @@ export function getPreviewNoPromptCommand(preset: ResolvedAgentConfig): string { export function getPreviewTaskCommand(preset: ResolvedAgentConfig): string { if (preset.kind !== "terminal") { return preset.model - ? `Superset Chat opens with model ${preset.model}.` - : "Superset Chat opens with the rendered task prompt."; + ? `Superset opens with model ${preset.model}.` + : "Superset opens with the rendered task prompt."; } return ( diff --git a/packages/cli/src/commands/agents/list/command.ts b/packages/cli/src/commands/agents/list/command.ts index fdf5fbff18c..eb5dd96df9f 100644 --- a/packages/cli/src/commands/agents/list/command.ts +++ b/packages/cli/src/commands/agents/list/command.ts @@ -43,7 +43,7 @@ export default command({ id: definition.id, presetId: definition.id, label: definition.label, - command: "(superset chat runtime)", + command: "(superset runtime)", })); return [...terminalConfigs, ...chatBuiltins]; }, diff --git a/packages/cli/src/commands/agents/run/command.ts b/packages/cli/src/commands/agents/run/command.ts index dc484d5deee..430caadb3eb 100644 --- a/packages/cli/src/commands/agents/run/command.ts +++ b/packages/cli/src/commands/agents/run/command.ts @@ -10,7 +10,7 @@ export default command({ agent: string() .required() .desc( - "Agent preset id (e.g. `claude`), HostAgentConfig instance UUID, or `superset-chat` for a Superset Chat session", + "Agent preset id (e.g. `claude`), HostAgentConfig instance UUID, or `superset` for a Superset session", ), prompt: string().required().desc("Prompt sent to the agent"), attachmentId: string() diff --git a/packages/cli/src/commands/workspaces/create/command.ts b/packages/cli/src/commands/workspaces/create/command.ts index 450bc53e58a..b1e947ba67b 100644 --- a/packages/cli/src/commands/workspaces/create/command.ts +++ b/packages/cli/src/commands/workspaces/create/command.ts @@ -16,7 +16,7 @@ export default command({ "Branch to fork from when `branch` does not exist (defaults to project default)", ), agent: string().desc( - "Agent to spawn after creation. Preset id (`claude`, `codex`, …), HostAgentConfig instance UUID, or `superset-chat`", + "Agent to spawn after creation. Preset id (`claude`, `codex`, …), HostAgentConfig instance UUID, or `superset`", ), prompt: string().desc( "Initial prompt the agent starts with. Required when --agent is set", diff --git a/packages/cli/src/lib/upload-attachments.ts b/packages/cli/src/lib/upload-attachments.ts index 7544a28199b..82778a2142f 100644 --- a/packages/cli/src/lib/upload-attachments.ts +++ b/packages/cli/src/lib/upload-attachments.ts @@ -4,12 +4,6 @@ import { CLIError } from "@superset/cli-framework"; import mimeTypes from "mime-types"; import type { HostServiceClient } from "./host-target"; -/** - * Upload local files to the target host's attachment store and return the - * resulting attachment ids in the same order. The host stores bytes per - * org and returns opaque UUIDs the agent runner resolves on the host - * side. CLI never sees the on-disk path. - */ export async function uploadAttachments( client: HostServiceClient, paths: string[], diff --git a/packages/host-service/src/runtime/chat/chat.ts b/packages/host-service/src/runtime/chat/chat.ts index d80ee0c6148..a0df961b362 100644 --- a/packages/host-service/src/runtime/chat/chat.ts +++ b/packages/host-service/src/runtime/chat/chat.ts @@ -555,36 +555,6 @@ When you need to ask the user ANY question — including simple yes/no, confirma return promise; } - /** - * Returns the in-memory runtime if one exists, otherwise null. Kicks off - * creation in the background as a side-effect so the next call (typically - * the next snapshot poll) finds it warm. Used by read-only paths that - * shouldn't block on the 3–8s harness boot — chat panes mounting against - * an evicted session, or sessions spawned from CLI/SDK/MCP whose harness - * isn't yet booted on this host. - */ - private peekRuntime( - sessionId: string, - workspaceId: string, - ): RuntimeSession | null { - const existing = this.runtimes.get(sessionId); - if (existing) { - if (existing.workspaceId !== workspaceId) { - throw new Error( - `Session ${sessionId} is already bound to workspace ${existing.workspaceId}`, - ); - } - return existing; - } - - // Trigger background creation but don't await it. The inflight map - // dedupes concurrent peeks, so we won't create multiple runtimes. - void this.getOrCreateRuntime(sessionId, workspaceId).catch(() => { - // Errors surface on the next read path that awaits the promise. - }); - return null; - } - /** * Tear down the in-memory runtime for a session. Aborts any in-flight * work, disconnects MCP servers, removes the runtime from the manager's @@ -716,13 +686,6 @@ When you need to ask the user ANY question — including simple yes/no, confirma * harness state can change between the displayState read and the messages * read. This still removes the *client-side* two-query race, which is the * one that caused mismatched message/display state. - * - * Cold-start fast path: if the runtime isn't yet booted (chat pane - * mounting against a freshly-loaded host, or a session spawned via - * `agents.run` whose harness was evicted) return an empty skeleton - * snapshot immediately and kick off creation in the background. The - * renderer's loading spinner clears after one poll instead of blocking - * on the 3–8s harness boot. The next poll picks up the warm runtime. */ async getSnapshot(input: { sessionId: string; @@ -731,19 +694,10 @@ When you need to ask the user ANY question — including simple yes/no, confirma displayState: ChatDisplayState; messages: RuntimeMessages; }> { - const runtime = this.peekRuntime(input.sessionId, input.workspaceId); - if (!runtime) { - return { - displayState: { - isRunning: false, - currentMessage: null, - pendingQuestion: null, - errorMessage: null, - } as unknown as ChatDisplayState, - messages: [] as unknown as RuntimeMessages, - }; - } - + const runtime = await this.getOrCreateRuntime( + input.sessionId, + input.workspaceId, + ); const displayState = this.buildDisplayState(runtime); const messages = await runtime.harness.listMessages(); // Intentionally no observedAt: when the harness state hasn't changed, @@ -778,42 +732,6 @@ When you need to ask the user ANY question — including simple yes/no, confirma return runtime.harness.sendMessage(input.payload); } - /** - * Boot the runtime and start the turn, but resolve as soon as streaming - * has begun — don't block on the assistant finishing. Used by headless - * launches (`agents.run` from CLI/SDK/MCP) where the caller doesn't have - * a UI to consume the stream and waiting 30s for the turn to end is just - * dead time. The harness keeps streaming in the background; the renderer - * picks up state via the next `getSnapshot` poll when a chat pane opens. - */ - async startTurn(input: ChatSendMessageInput): Promise { - const runtime = await this.getOrCreateRuntime( - input.sessionId, - input.workspaceId, - ); - runtime.lastErrorMessage = null; - - const selectedModel = input.metadata?.model?.trim(); - if (selectedModel) { - await runtime.harness.switchModel({ - modelId: selectedModel, - scope: "thread", - }); - } - - const thinkingLevel = input.metadata?.thinkingLevel; - if (thinkingLevel) { - await runtime.harness.setState({ thinkingLevel }); - } - - // Fire and forget: surface the error onto the runtime so the next - // snapshot reports it via `displayState.errorMessage`, but don't - // reject the calling promise. - void runtime.harness.sendMessage(input.payload).catch((error) => { - runtime.lastErrorMessage = toRuntimeErrorMessage(error); - }); - } - async restartFromMessage(input: RestartPayload): Promise { const runtime = await this.getOrCreateRuntime( input.sessionId, diff --git a/packages/host-service/src/trpc/router/agents/agents.ts b/packages/host-service/src/trpc/router/agents/agents.ts index 1b47c847fc3..30c97e85063 100644 --- a/packages/host-service/src/trpc/router/agents/agents.ts +++ b/packages/host-service/src/trpc/router/agents/agents.ts @@ -210,18 +210,24 @@ async function runChatAgent( v2WorkspaceId: input.workspaceId, }); - // Fire-and-forget the turn: the caller (CLI/SDK/MCP) gets the sessionId - // back as soon as the runtime is ready, instead of waiting ~30s for the - // assistant stream to finish. The renderer materializes the streaming - // state via `chat.getSnapshot` when a chat pane opens. - await ctx.runtime.chat.startTurn({ - sessionId, - workspaceId: input.workspaceId, - payload: { - content: input.prompt, - ...(files.length > 0 ? { files } : {}), - }, - }); + // Fire and forget: the caller gets the sessionId without waiting for the + // runtime boot or assistant stream. A chat pane attaching later surfaces + // any error via `getSnapshot.displayState.errorMessage`. + void ctx.runtime.chat + .sendMessage({ + sessionId, + workspaceId: input.workspaceId, + payload: { + content: input.prompt, + ...(files.length > 0 ? { files } : {}), + }, + }) + .catch((error) => { + console.error( + `[runChatAgent] sendMessage failed for ${sessionId}:`, + error, + ); + }); return { kind: "chat", sessionId, label }; } @@ -277,12 +283,6 @@ async function runTerminalAgent( }; } -/** - * Launch an agent against a workspace. Routes by agent kind: chat-builtin - * ids fire a first message through the host's ChatRuntimeManager (no PTY); - * everything else resolves to a terminal preset and spawns a PTY. Both - * return a `kind`-tagged result so callers materialize the right pane. - */ export async function runAgentInWorkspace( ctx: HostServiceContext, input: AgentRunInput, diff --git a/packages/host-service/src/trpc/router/settings/agent-configs.test.ts b/packages/host-service/src/trpc/router/settings/agent-configs.test.ts index 77266a31375..a69c32beded 100644 --- a/packages/host-service/src/trpc/router/settings/agent-configs.test.ts +++ b/packages/host-service/src/trpc/router/settings/agent-configs.test.ts @@ -52,12 +52,10 @@ describe("agentConfigsRouter", () => { expect(result.map((row) => row.order)).toEqual([0, 1, 2, 3, 4]); }); - it("does not seed Superset Chat", async () => { + it("does not seed Superset", async () => { const caller = createCaller(); const result = await caller.list(); - expect( - result.find((row) => row.presetId === "superset-chat"), - ).toBeUndefined(); + expect(result.find((row) => row.presetId === "superset")).toBeUndefined(); }); it("returns existing rows on subsequent calls without re-seeding", async () => { diff --git a/packages/mcp-v2/src/tools/agents/run.ts b/packages/mcp-v2/src/tools/agents/run.ts index 65c8e906a95..56bfc88c8ff 100644 --- a/packages/mcp-v2/src/tools/agents/run.ts +++ b/packages/mcp-v2/src/tools/agents/run.ts @@ -8,7 +8,7 @@ export function register(server: McpServer): void { defineTool(server, { name: "agents_run", description: - "Launch an agent inside an existing workspace. Resolves the host that owns the workspace, then runs the named agent preset (or HostAgentConfig instance) with the given prompt. Terminal agents spawn a fresh PTY session; `superset-chat` spawns a Superset Chat session (mastracode runtime) instead. The result's `kind` field discriminates. Use this to start a second agent in a workspace that already exists; for create-and-spawn in a single call, pass `agents` to workspaces_create instead.", + "Launch an agent inside an existing workspace. Resolves the host that owns the workspace, then runs the named agent preset (or HostAgentConfig instance) with the given prompt. Terminal agents spawn a fresh PTY session; `superset` spawns a Superset session (mastracode runtime) instead. The result's `kind` field discriminates. Use this to start a second agent in a workspace that already exists; for create-and-spawn in a single call, pass `agents` to workspaces_create instead.", inputSchema: { workspaceId: z .string() @@ -18,14 +18,14 @@ export function register(server: McpServer): void { .string() .min(1) .describe( - "Agent preset id (e.g. `claude`, `codex`), HostAgentConfig instance UUID, or `superset-chat` for a Superset Chat session.", + "Agent preset id (e.g. `claude`, `codex`), HostAgentConfig instance UUID, or `superset` for a Superset session.", ), prompt: z.string().min(1).describe("Prompt sent to the agent."), attachmentIds: z .array(z.string().uuid()) .optional() .describe( - "Host-scoped attachment UUIDs. For terminal agents the host appends a paths block to the prompt; for `superset-chat` the host inlines the file bytes as base64 data URLs on the chat message.", + "Host-scoped attachment UUIDs. For terminal agents the host appends a paths block to the prompt; for `superset` the host inlines the file bytes as base64 data URLs on the chat message.", ), }, handler: async (input, ctx) => { diff --git a/packages/mcp-v2/src/tools/workspaces/create.ts b/packages/mcp-v2/src/tools/workspaces/create.ts index 5601c36debe..25a8db9fda0 100644 --- a/packages/mcp-v2/src/tools/workspaces/create.ts +++ b/packages/mcp-v2/src/tools/workspaces/create.ts @@ -8,14 +8,14 @@ const agentLaunchSchema = z.object({ .string() .min(1) .describe( - "Agent preset id (e.g. `claude`, `codex`), HostAgentConfig instance UUID, or `superset-chat` to spawn a Superset Chat session instead of a terminal.", + "Agent preset id (e.g. `claude`, `codex`), HostAgentConfig instance UUID, or `superset` to spawn a Superset session instead of a terminal.", ), prompt: z.string().min(1).describe("Initial prompt the agent starts with."), attachmentIds: z .array(z.string().uuid()) .optional() .describe( - "Host-scoped attachment UUIDs. For terminal agents the host appends a paths block to the prompt; for `superset-chat` the host inlines the file bytes as base64 data URLs on the chat message.", + "Host-scoped attachment UUIDs. For terminal agents the host appends a paths block to the prompt; for `superset` the host inlines the file bytes as base64 data URLs on the chat message.", ), }); diff --git a/packages/mcp/src/tools/devices/start-agent-session/shared.ts b/packages/mcp/src/tools/devices/start-agent-session/shared.ts index a706eb885cf..783b572d26a 100644 --- a/packages/mcp/src/tools/devices/start-agent-session/shared.ts +++ b/packages/mcp/src/tools/devices/start-agent-session/shared.ts @@ -147,11 +147,11 @@ export function buildTaskLaunchRequest({ agent: (typeof STARTABLE_AGENT_TYPES)[number]; task: TaskRecord; }): AgentLaunchRequest { - if (agent === "superset-chat") { + if (agent === "superset") { return { kind: "chat", workspaceId, - agentType: "superset-chat", + agentType: "superset", source: "mcp", chat: { ...(paneId ? { paneId } : {}), @@ -189,11 +189,11 @@ export function buildPromptLaunchRequest({ agent: (typeof STARTABLE_AGENT_TYPES)[number]; prompt: string; }): AgentLaunchRequest { - if (agent === "superset-chat") { + if (agent === "superset") { return { kind: "chat", workspaceId, - agentType: "superset-chat", + agentType: "superset", source: "mcp", chat: { ...(paneId ? { paneId } : {}), diff --git a/packages/mcp/src/tools/devices/start-agent-session/start-agent-session-with-prompt.ts b/packages/mcp/src/tools/devices/start-agent-session/start-agent-session-with-prompt.ts index 1bdae7ef0c8..b5230d6e001 100644 --- a/packages/mcp/src/tools/devices/start-agent-session/start-agent-session-with-prompt.ts +++ b/packages/mcp/src/tools/devices/start-agent-session/start-agent-session-with-prompt.ts @@ -14,7 +14,7 @@ export function registerPromptLaunchTool(server: McpServer) { START_AGENT_SESSION_WITH_PROMPT_TOOL_NAME, { description: - "Start an autonomous AI session in an existing workspace using a direct prompt instead of a task. Supports terminal agents and Superset Chat. When paneId is provided, launch behavior is scoped to the tab containing that pane.", + "Start an autonomous AI session in an existing workspace using a direct prompt instead of a task. Supports terminal agents and Superset. When paneId is provided, launch behavior is scoped to the tab containing that pane.", inputSchema: promptInputSchemaShape, }, async (args, extra) => { diff --git a/packages/mcp/src/tools/devices/start-agent-session/start-agent-session.ts b/packages/mcp/src/tools/devices/start-agent-session/start-agent-session.ts index cbe40fd60a3..c31e60a5e96 100644 --- a/packages/mcp/src/tools/devices/start-agent-session/start-agent-session.ts +++ b/packages/mcp/src/tools/devices/start-agent-session/start-agent-session.ts @@ -16,7 +16,7 @@ export function registerTaskLaunchTool(server: McpServer) { START_AGENT_SESSION_TOOL_NAME, { description: - "Start an autonomous AI session for a task in an existing workspace. Supports terminal agents and Superset Chat. When paneId is provided, launch behavior is scoped to the tab containing that pane.", + "Start an autonomous AI session for a task in an existing workspace. Supports terminal agents and Superset. When paneId is provided, launch behavior is scoped to the tab containing that pane.", inputSchema: taskInputSchemaShape, }, async (args, extra) => { diff --git a/packages/sdk/src/resources/agents.ts b/packages/sdk/src/resources/agents.ts index eb53114ccef..8776778af7d 100644 --- a/packages/sdk/src/resources/agents.ts +++ b/packages/sdk/src/resources/agents.ts @@ -101,7 +101,7 @@ export interface AgentRunParams { workspaceId: string; /** * Agent preset id (e.g. `"claude"`), HostAgentConfig instance UUID, or - * `"superset-chat"` to spawn a Superset Chat session instead of a + * `"superset"` to spawn a Superset session instead of a * terminal. Chat sessions live as a host-side mastracode runtime; the * caller materializes a chat pane against the returned `sessionId`. */ @@ -110,7 +110,7 @@ export interface AgentRunParams { prompt: string; /** * Host-scoped attachment ids. For terminal agents the host appends a - * paths block to the prompt; for `superset-chat` the host inlines the + * paths block to the prompt; for `superset` the host inlines the * file bytes as base64 data URLs on the chat message. */ attachmentIds?: string[]; diff --git a/packages/sdk/src/resources/workspaces.ts b/packages/sdk/src/resources/workspaces.ts index 40beb4a89ae..38fa70464e4 100644 --- a/packages/sdk/src/resources/workspaces.ts +++ b/packages/sdk/src/resources/workspaces.ts @@ -149,7 +149,7 @@ export interface WorkspaceCreateParams { export interface WorkspaceAgentLaunch { /** * Agent preset id (e.g. `"claude"`), HostAgentConfig instance id, or - * `"superset-chat"` for a Superset Chat session. + * `"superset"` for a Superset chat session. */ agent: string; /** What to tell the agent. */ diff --git a/packages/shared/src/agent-catalog.ts b/packages/shared/src/agent-catalog.ts index dba367e6330..723a208a41c 100644 --- a/packages/shared/src/agent-catalog.ts +++ b/packages/shared/src/agent-catalog.ts @@ -17,7 +17,7 @@ import { export const BUILTIN_AGENT_IDS = [ ...BUILTIN_TERMINAL_AGENT_TYPES, - "superset-chat", + "superset", ] as const; export type BuiltinAgentId = (typeof BUILTIN_AGENT_IDS)[number]; @@ -35,14 +35,14 @@ export const BUILTIN_AGENT_LABELS: Record = { ...Object.fromEntries( BUILTIN_TERMINAL_AGENTS.map((agent) => [agent.id, agent.label]), ), - "superset-chat": "Superset Chat", + superset: "Superset", } as Record; const BUILTIN_CHAT_AGENT: ChatAgentDefinition = { - id: "superset-chat", + id: "superset", source: "builtin", kind: "chat", - label: "Superset Chat", + label: "Superset", description: "Superset's built-in workspace chat for project-aware help and task launches.", enabled: true, diff --git a/packages/shared/src/agent-launch-request.test.ts b/packages/shared/src/agent-launch-request.test.ts index 4a1212088db..288cdd2ee9c 100644 --- a/packages/shared/src/agent-launch-request.test.ts +++ b/packages/shared/src/agent-launch-request.test.ts @@ -56,7 +56,7 @@ describe("buildPromptAgentLaunchRequest", () => { const request = buildPromptAgentLaunchRequest({ workspaceId: "workspace-1", source: "new-workspace", - selectedAgent: "superset-chat", + selectedAgent: "superset", prompt: "hello", initialFiles: [ { @@ -71,7 +71,7 @@ describe("buildPromptAgentLaunchRequest", () => { expect(request).toMatchObject({ kind: "chat", - agentType: "superset-chat", + agentType: "superset", chat: { initialPrompt: "hello", initialFiles: [ @@ -130,7 +130,7 @@ describe("buildTaskAgentLaunchRequest", () => { version: 1, presets: [ { - id: "superset-chat", + id: "superset", taskPromptTemplate: "Chat {{title}} / {{slug}}", }, ], @@ -140,7 +140,7 @@ describe("buildTaskAgentLaunchRequest", () => { const request = buildTaskAgentLaunchRequest({ workspaceId: "workspace-1", source: "open-in-workspace", - selectedAgent: "superset-chat", + selectedAgent: "superset", task: TASK, autoRun: true, configsById, diff --git a/packages/shared/src/agent-launch.test.ts b/packages/shared/src/agent-launch.test.ts index 2f38d2440a5..e7099b06031 100644 --- a/packages/shared/src/agent-launch.test.ts +++ b/packages/shared/src/agent-launch.test.ts @@ -59,7 +59,7 @@ describe("normalizeAgentLaunchRequest", () => { expect(normalized).toEqual({ kind: "chat", workspaceId: "ws-1", - agentType: "superset-chat", + agentType: "superset", chat: { paneId: "pane-1", initialPrompt: "summarize this task", diff --git a/packages/shared/src/agent-launch.ts b/packages/shared/src/agent-launch.ts index 3b18695ea4d..d61f9d8be1e 100644 --- a/packages/shared/src/agent-launch.ts +++ b/packages/shared/src/agent-launch.ts @@ -137,7 +137,7 @@ function normalizeLegacyLaunchRequest( ): AgentLaunchRequest { const chatConfig = legacy.chatLaunchConfig; const shouldLaunchChat = - legacy.agentType === "superset-chat" || + legacy.agentType === "superset" || legacy.openChatPane === true || chatConfig !== undefined; @@ -146,7 +146,7 @@ function normalizeLegacyLaunchRequest( kind: "chat", workspaceId: legacy.workspaceId, idempotencyKey: legacy.idempotencyKey, - agentType: "superset-chat", + agentType: "superset", source: legacy.source, chat: { paneId: chatConfig?.paneId ?? legacy.paneId, @@ -211,11 +211,11 @@ export function buildTaskLaunchRequest({ source: AgentLaunchSource; autoExecute?: boolean; }): AgentLaunchRequest { - if (agentType === "superset-chat") { + if (agentType === "superset") { return { kind: "chat", workspaceId, - agentType: "superset-chat", + agentType: "superset", source, chat: { initialPrompt: renderTaskPromptTemplate( diff --git a/packages/shared/src/agent-settings.test.ts b/packages/shared/src/agent-settings.test.ts index a16bccccc38..dfe53741f58 100644 --- a/packages/shared/src/agent-settings.test.ts +++ b/packages/shared/src/agent-settings.test.ts @@ -26,7 +26,7 @@ describe("resolveAgentConfigs", () => { enabled: false, }, { - id: "superset-chat", + id: "superset", taskPromptTemplate: "Chat {{slug}}", }, ], @@ -34,7 +34,7 @@ describe("resolveAgentConfigs", () => { }); const claude = presets.find((preset) => preset.id === "claude"); - const chat = presets.find((preset) => preset.id === "superset-chat"); + const chat = presets.find((preset) => preset.id === "superset"); expect(claude).toMatchObject({ id: "claude", @@ -49,7 +49,7 @@ describe("resolveAgentConfigs", () => { ); expect(chat).toMatchObject({ - id: "superset-chat", + id: "superset", kind: "chat", taskPromptTemplate: "Chat {{slug}}", }); @@ -313,13 +313,13 @@ describe("contextPromptTemplate resolution", () => { version: 1 as const, presets: [ { - id: "superset-chat", + id: "superset", contextPromptTemplateSystem: "custom sys", }, ], }; const chat = resolveAgentConfigs({ overrideEnvelope: override }).find( - (p) => p.id === "superset-chat", + (p) => p.id === "superset", ); expect(chat?.contextPromptTemplateSystem).toBe("custom sys"); }); diff --git a/packages/shared/src/host-agent-presets.ts b/packages/shared/src/host-agent-presets.ts index c95234f3447..e68eb7db8e8 100644 --- a/packages/shared/src/host-agent-presets.ts +++ b/packages/shared/src/host-agent-presets.ts @@ -27,7 +27,7 @@ export interface HostAgentPreset { * not appear in promptless launches. Stdin transport pipes the prompt to * the spawned process's stdin instead of pushing it to argv. * - * Superset Chat is intentionally excluded — its model/provider config + * Superset is intentionally excluded — its model/provider config * lives in chat settings, not in terminal-agent configs. */ export const HOST_AGENT_PRESETS = [ diff --git a/packages/ui/src/assets/icons/preset-icons/index.ts b/packages/ui/src/assets/icons/preset-icons/index.ts index 635e5913794..a34d2db5f41 100644 --- a/packages/ui/src/assets/icons/preset-icons/index.ts +++ b/packages/ui/src/assets/icons/preset-icons/index.ts @@ -27,7 +27,6 @@ export const PRESET_ICONS: Record = { gemini: { light: geminiIcon, dark: geminiIcon }, pi: { light: piIcon, dark: piWhiteIcon }, superset: { light: supersetIcon, dark: supersetIcon }, - "superset-chat": { light: supersetIcon, dark: supersetIcon }, "cursor-agent": { light: cursorAgentIcon, dark: cursorAgentIcon }, mastracode: { light: mastracodeIcon, dark: mastracodeWhiteIcon }, opencode: { light: opencodeIcon, dark: opencodeWhiteIcon }, From 66aa824f4cfe27b09715d8173f2fb68d419b89a8 Mon Sep 17 00:00:00 2001 From: Satya Patel Date: Tue, 5 May 2026 20:12:05 -0700 Subject: [PATCH 3/5] refactor(agents): hardcode 'superset' instead of filtering catalog MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit There's exactly one chat builtin, so iterating BUILTIN_AGENT_DEFINITIONS to filter for chat-kinds is busywork — the id and label are known at the call site. Drops the catalog import + type predicate in CLI agents list, the v2 picker hook, and runAgentInWorkspace. --- .../useV2AgentChoices/useV2AgentChoices.ts | 26 +++++++------------ .../cli/src/commands/agents/list/command.ts | 22 +++++++--------- .../src/trpc/router/agents/agents.ts | 17 +++--------- 3 files changed, 23 insertions(+), 42 deletions(-) diff --git a/apps/desktop/src/renderer/hooks/useV2AgentChoices/useV2AgentChoices.ts b/apps/desktop/src/renderer/hooks/useV2AgentChoices/useV2AgentChoices.ts index 0f136f7006a..e63b924bf4d 100644 --- a/apps/desktop/src/renderer/hooks/useV2AgentChoices/useV2AgentChoices.ts +++ b/apps/desktop/src/renderer/hooks/useV2AgentChoices/useV2AgentChoices.ts @@ -1,7 +1,3 @@ -import { - BUILTIN_AGENT_DEFINITIONS, - isChatAgentDefinition, -} from "@superset/shared/agent-catalog"; import { useMemo } from "react"; import type { AgentSelectAgent } from "renderer/components/AgentSelect"; import { useV2AgentConfigs } from "renderer/hooks/useV2AgentConfigs"; @@ -11,10 +7,15 @@ interface UseV2AgentChoicesResult { isFetched: boolean; } -// Built-in chat agents aren't in the host's `host_agent_configs` table — -// they're routed by id inside `runAgentInWorkspace`. Append after the -// host's terminal rows so the user's preferred terminal agents stay on -// top of the picker. +const SUPERSET_AGENT: AgentSelectAgent = { + id: "superset", + label: "Superset", + iconId: "superset", +}; + +// Superset chat isn't in the host's `host_agent_configs` table — it's +// routed by id inside `runAgentInWorkspace`. Append after the host's +// terminal rows so the user's preferred terminal agents stay on top. export function useV2AgentChoices( hostUrl: string | null, ): UseV2AgentChoicesResult { @@ -27,14 +28,7 @@ export function useV2AgentChoices( iconId: config.presetId, }), ); - const chatAgents: AgentSelectAgent[] = BUILTIN_AGENT_DEFINITIONS.filter( - isChatAgentDefinition, - ).map((definition) => ({ - id: definition.id, - label: definition.label, - iconId: definition.id, - })); - return [...terminalAgents, ...chatAgents]; + return [...terminalAgents, SUPERSET_AGENT]; }, [query.data]); return { agents, isFetched: query.isFetched }; diff --git a/packages/cli/src/commands/agents/list/command.ts b/packages/cli/src/commands/agents/list/command.ts index eb5dd96df9f..aac2343eb1d 100644 --- a/packages/cli/src/commands/agents/list/command.ts +++ b/packages/cli/src/commands/agents/list/command.ts @@ -1,8 +1,4 @@ import { boolean, CLIError, string, table } from "@superset/cli-framework"; -import { - BUILTIN_AGENT_DEFINITIONS, - isChatAgentDefinition, -} from "@superset/shared/agent-catalog"; import { command } from "../../../lib/command"; import { requireHostTarget, resolveHostTarget } from "../../../lib/host-target"; @@ -37,14 +33,14 @@ export default command({ const terminalConfigs = await target.client.settings.agentConfigs.list.query(); - const chatBuiltins = BUILTIN_AGENT_DEFINITIONS.filter( - isChatAgentDefinition, - ).map((definition) => ({ - id: definition.id, - presetId: definition.id, - label: definition.label, - command: "(superset runtime)", - })); - return [...terminalConfigs, ...chatBuiltins]; + return [ + ...terminalConfigs, + { + id: "superset", + presetId: "superset", + label: "Superset", + command: "(superset runtime)", + }, + ]; }, }); diff --git a/packages/host-service/src/trpc/router/agents/agents.ts b/packages/host-service/src/trpc/router/agents/agents.ts index 30c97e85063..7622df57a1a 100644 --- a/packages/host-service/src/trpc/router/agents/agents.ts +++ b/packages/host-service/src/trpc/router/agents/agents.ts @@ -1,8 +1,4 @@ import { readFileSync } from "node:fs"; -import { - BUILTIN_AGENT_DEFINITIONS, - isChatAgentDefinition, -} from "@superset/shared/agent-catalog"; import { TRPCError } from "@trpc/server"; import { asc, eq } from "drizzle-orm"; import { z } from "zod"; @@ -167,12 +163,8 @@ export type AgentRunResult = | { kind: "terminal"; sessionId: string; label: string } | { kind: "chat"; sessionId: string; label: string }; -function resolveChatBuiltin(agent: string) { - const definition = BUILTIN_AGENT_DEFINITIONS.find( - (item) => item.id === agent, - ); - return definition && isChatAgentDefinition(definition) ? definition : null; -} +const SUPERSET_AGENT_ID = "superset"; +const SUPERSET_AGENT_LABEL = "Superset"; async function resolveAttachmentsAsFiles( attachmentIds: string[], @@ -287,9 +279,8 @@ export async function runAgentInWorkspace( ctx: HostServiceContext, input: AgentRunInput, ): Promise { - const chatBuiltin = resolveChatBuiltin(input.agent); - if (chatBuiltin) { - return runChatAgent(ctx, input, chatBuiltin.label); + if (input.agent === SUPERSET_AGENT_ID) { + return runChatAgent(ctx, input, SUPERSET_AGENT_LABEL); } return runTerminalAgent(ctx, input); } From 90a58bdf7db8e6021eb934057b0f4047006b18b0 Mon Sep 17 00:00:00 2001 From: Satya Patel Date: Tue, 5 May 2026 20:14:59 -0700 Subject: [PATCH 4/5] docs(agents): trim verbose mcp / sdk descriptions Restore the original MCP and SDK descriptions and just add `superset` to the e.g. lists rather than spelling out the chat-vs-terminal split in every docstring. --- packages/mcp-v2/src/tools/agents/run.ts | 6 +++--- packages/mcp-v2/src/tools/workspaces/create.ts | 4 ++-- packages/sdk/src/resources/agents.ts | 18 ++---------------- packages/sdk/src/resources/workspaces.ts | 5 +---- 4 files changed, 8 insertions(+), 25 deletions(-) diff --git a/packages/mcp-v2/src/tools/agents/run.ts b/packages/mcp-v2/src/tools/agents/run.ts index 56bfc88c8ff..aa17b0e3367 100644 --- a/packages/mcp-v2/src/tools/agents/run.ts +++ b/packages/mcp-v2/src/tools/agents/run.ts @@ -8,7 +8,7 @@ export function register(server: McpServer): void { defineTool(server, { name: "agents_run", description: - "Launch an agent inside an existing workspace. Resolves the host that owns the workspace, then runs the named agent preset (or HostAgentConfig instance) with the given prompt. Terminal agents spawn a fresh PTY session; `superset` spawns a Superset session (mastracode runtime) instead. The result's `kind` field discriminates. Use this to start a second agent in a workspace that already exists; for create-and-spawn in a single call, pass `agents` to workspaces_create instead.", + "Launch an agent inside an existing workspace. Resolves the host that owns the workspace, then runs the named agent preset (or HostAgentConfig instance) with the given prompt in a fresh terminal session. Use this to start a second agent in a workspace that already exists; for create-and-spawn in a single call, pass `agents` to workspaces_create instead.", inputSchema: { workspaceId: z .string() @@ -18,14 +18,14 @@ export function register(server: McpServer): void { .string() .min(1) .describe( - "Agent preset id (e.g. `claude`, `codex`), HostAgentConfig instance UUID, or `superset` for a Superset session.", + "Agent preset id (e.g. `claude`, `codex`, `superset`) or HostAgentConfig instance UUID.", ), prompt: z.string().min(1).describe("Prompt sent to the agent."), attachmentIds: z .array(z.string().uuid()) .optional() .describe( - "Host-scoped attachment UUIDs. For terminal agents the host appends a paths block to the prompt; for `superset` the host inlines the file bytes as base64 data URLs on the chat message.", + "Host-scoped attachment UUIDs. The host resolves these to absolute paths and appends them to the prompt.", ), }, handler: async (input, ctx) => { diff --git a/packages/mcp-v2/src/tools/workspaces/create.ts b/packages/mcp-v2/src/tools/workspaces/create.ts index 25a8db9fda0..2b10c3c323b 100644 --- a/packages/mcp-v2/src/tools/workspaces/create.ts +++ b/packages/mcp-v2/src/tools/workspaces/create.ts @@ -8,14 +8,14 @@ const agentLaunchSchema = z.object({ .string() .min(1) .describe( - "Agent preset id (e.g. `claude`, `codex`), HostAgentConfig instance UUID, or `superset` to spawn a Superset session instead of a terminal.", + "Agent preset id (e.g. `claude`, `codex`, `superset`) or HostAgentConfig instance UUID.", ), prompt: z.string().min(1).describe("Initial prompt the agent starts with."), attachmentIds: z .array(z.string().uuid()) .optional() .describe( - "Host-scoped attachment UUIDs. For terminal agents the host appends a paths block to the prompt; for `superset` the host inlines the file bytes as base64 data URLs on the chat message.", + "Host-scoped attachment UUIDs. The host resolves these to absolute paths and appends them to the prompt.", ), }); diff --git a/packages/sdk/src/resources/agents.ts b/packages/sdk/src/resources/agents.ts index 8776778af7d..4ded545adec 100644 --- a/packages/sdk/src/resources/agents.ts +++ b/packages/sdk/src/resources/agents.ts @@ -99,20 +99,11 @@ export interface AgentListParams { export interface AgentRunParams { /** Workspace UUID to run the agent in. */ workspaceId: string; - /** - * Agent preset id (e.g. `"claude"`), HostAgentConfig instance UUID, or - * `"superset"` to spawn a Superset session instead of a - * terminal. Chat sessions live as a host-side mastracode runtime; the - * caller materializes a chat pane against the returned `sessionId`. - */ + /** Agent preset id (e.g. `"claude"`, `"superset"`) or HostAgentConfig instance UUID. */ agent: string; /** Prompt sent to the agent. */ prompt: string; - /** - * Host-scoped attachment ids. For terminal agents the host appends a - * paths block to the prompt; for `superset` the host inlines the - * file bytes as base64 data URLs on the chat message. - */ + /** Host-scoped attachment ids; host resolves to absolute paths in the prompt. */ attachmentIds?: string[]; } @@ -120,11 +111,6 @@ interface HostLookup { hostId: string; } -/** - * Tagged with `kind` so callers know whether they got a terminal session - * (sessionId addresses a PTY on the host) or a chat session (sessionId - * addresses a mastracode runtime in `ChatRuntimeManager`). - */ export type AgentRunResult = | { kind: "terminal"; sessionId: string; label: string } | { kind: "chat"; sessionId: string; label: string }; diff --git a/packages/sdk/src/resources/workspaces.ts b/packages/sdk/src/resources/workspaces.ts index 38fa70464e4..006dfb0f758 100644 --- a/packages/sdk/src/resources/workspaces.ts +++ b/packages/sdk/src/resources/workspaces.ts @@ -147,10 +147,7 @@ export interface WorkspaceCreateParams { } export interface WorkspaceAgentLaunch { - /** - * Agent preset id (e.g. `"claude"`), HostAgentConfig instance id, or - * `"superset"` for a Superset chat session. - */ + /** Agent preset id (e.g. `"claude"`, `"superset"`) or HostAgentConfig instance id. */ agent: string; /** What to tell the agent. */ prompt: string; From b909aa3157e47bdb3a53922124edca8148c4ce39 Mon Sep 17 00:00:00 2001 From: Satya Patel Date: Tue, 5 May 2026 20:22:27 -0700 Subject: [PATCH 5/5] docs(agents): trim fire-and-forget comment to just the WHY --- packages/host-service/src/trpc/router/agents/agents.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/host-service/src/trpc/router/agents/agents.ts b/packages/host-service/src/trpc/router/agents/agents.ts index 7622df57a1a..387ed3d2518 100644 --- a/packages/host-service/src/trpc/router/agents/agents.ts +++ b/packages/host-service/src/trpc/router/agents/agents.ts @@ -202,9 +202,8 @@ async function runChatAgent( v2WorkspaceId: input.workspaceId, }); - // Fire and forget: the caller gets the sessionId without waiting for the - // runtime boot or assistant stream. A chat pane attaching later surfaces - // any error via `getSnapshot.displayState.errorMessage`. + // Errors surface via `getSnapshot.displayState.errorMessage` when a + // chat pane attaches. void ctx.runtime.chat .sendMessage({ sessionId,