Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 16 additions & 1 deletion apps/desktop/src/lib/trpc/routers/ui-state/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,16 @@ const fileViewerStateSchema = z.object({
oldPath: z.string().optional(),
});

const chatMastraLaunchConfigSchema = z.object({
initialPrompt: z.string().optional(),
metadata: z
.object({
model: z.string().optional(),
})
.optional(),
retryCount: z.number().int().min(0).optional(),
});

/**
* Zod schema for Pane
*/
Expand All @@ -48,7 +58,12 @@ const paneSchema = z.object({
cwd: z.string().nullable().optional(),
cwdConfirmed: z.boolean().optional(),
fileViewer: fileViewerStateSchema.optional(),
chatMastra: z.object({ sessionId: z.string().nullable() }).optional(),
chatMastra: z
.object({
sessionId: z.string().nullable(),
launchConfig: chatMastraLaunchConfigSchema.nullable().optional(),
})
.optional(),
browser: z
.object({
currentUrl: z.string(),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
import {
AGENT_PRESET_COMMANDS,
AGENT_TYPES,
buildAgentPromptCommand,
} from "@superset/shared/agent-command";
import {
type AgentLaunchRequest,
STARTABLE_AGENT_TYPES,
type StartableAgentType,
} from "@superset/shared/agent-launch";
import { Dialog, DialogContent } from "@superset/ui/dialog";
import { toast } from "@superset/ui/sonner";
import { useNavigate } from "@tanstack/react-router";
import { useEffect, useMemo, useRef, useState } from "react";
import { launchAgentSession } from "renderer/lib/agent-session-orchestrator";
import { electronTrpc } from "renderer/lib/electron-trpc";
import { launchCommandInPane } from "renderer/lib/terminal/launch-command";
import { resolveEffectiveWorkspaceBaseBranch } from "renderer/lib/workspaceBaseBranch";
import { useOpenProject } from "renderer/react-query/projects";
import { useCreateWorkspace } from "renderer/react-query/workspaces";
Expand All @@ -17,7 +21,6 @@ import {
useNewWorkspaceModalOpen,
usePreSelectedProjectId,
} from "renderer/stores/new-workspace-modal";
import { useTabsStore } from "renderer/stores/tabs/store";
import {
resolveBranchPrefix,
sanitizeBranchNameWithMaxLength,
Expand Down Expand Up @@ -62,7 +65,8 @@ export function NewWorkspaceModal() {
if (typeof window === "undefined") return "none";
const stored = window.localStorage.getItem(WORKSPACE_AGENT_STORAGE_KEY);
if (stored === "none") return "none";
return stored && (AGENT_TYPES as readonly string[]).includes(stored)
return stored &&
(STARTABLE_AGENT_TYPES as readonly string[]).includes(stored)
? (stored as WorkspaceCreateAgent)
: "none";
},
Expand Down Expand Up @@ -99,10 +103,9 @@ export function NewWorkspaceModal() {
resolveInitialCommands: (commands) =>
runSetupScriptRef.current ? commands : null,
});
const addTab = useTabsStore((s) => s.addTab);
const removePane = useTabsStore((s) => s.removePane);
const setTabAutoTitle = useTabsStore((s) => s.setTabAutoTitle);
const { openNew } = useOpenProject();
const selectableAgents =
STARTABLE_AGENT_TYPES as readonly StartableAgentType[];

const resolvedPrefix = useMemo(() => {
const projectOverrides = project?.branchPrefixMode != null;
Expand Down Expand Up @@ -136,6 +139,15 @@ export function NewWorkspaceModal() {
}
}, [isOpen]);

useEffect(() => {
if (selectedAgent === "none") return;
if ((STARTABLE_AGENT_TYPES as readonly string[]).includes(selectedAgent)) {
return;
}
setSelectedAgent("none");
window.localStorage.setItem(WORKSPACE_AGENT_STORAGE_KEY, "none");
}, [selectedAgent]);

const effectiveBaseBranch = resolveEffectiveWorkspaceBaseBranch({
explicitBaseBranch: baseBranch,
workspaceBaseBranch: project?.workspaceBaseBranch,
Expand Down Expand Up @@ -249,23 +261,61 @@ export function NewWorkspaceModal() {
/>
);
const isCreateDisabled = createWorkspace.isPending || isBranchesError;
const buildLaunchRequestForWorkspace = (
workspaceId: string,
prompt: string,
): AgentLaunchRequest | null => {
if (selectedAgent === "none") {
return null;
}

if (selectedAgent === "superset-chat") {
return {
kind: "chat",
workspaceId,
agentType: "superset-chat",
source: "new-workspace",
chat: {
initialPrompt: prompt || undefined,
retryCount: 1,
},
};
}

const command = prompt
? buildAgentPromptCommand({
prompt,
randomId: window.crypto.randomUUID(),
agent: selectedAgent,
})
: (AGENT_PRESET_COMMANDS[selectedAgent][0] ?? null);

if (!command) {
return null;
}

return {
kind: "terminal",
workspaceId,
agentType: selectedAgent,
source: "new-workspace",
terminal: {
command,
name: "Agent",
},
};
};

const handleCreateWorkspace = async () => {
if (!selectedProjectId) return;
// Keep the agent prompt uncapped; only trim surrounding whitespace.
const prompt = title.trim();

const workspaceName = deriveWorkspaceTitleFromPrompt(title) || undefined;
const agentCommand =
selectedAgent === "none"
? null
: prompt
? buildAgentPromptCommand({
prompt,
randomId: window.crypto.randomUUID(),
agent: selectedAgent,
})
: (AGENT_PRESET_COMMANDS[selectedAgent][0] ?? null);
const launchRequestTemplate = buildLaunchRequestForWorkspace(
"pending-workspace",
prompt,
);

closeModal();

Expand All @@ -278,37 +328,34 @@ export function NewWorkspaceModal() {
baseBranch: baseBranch || undefined,
applyPrefix,
},
agentCommand ? { agentCommand } : undefined,
launchRequestTemplate
? { agentLaunchRequest: launchRequestTemplate }
: undefined,
);

if (agentCommand) {
if (result.wasExisting) {
const { tabId, paneId } = addTab(result.workspace.id);
setTabAutoTitle(tabId, "Agent");
try {
await launchCommandInPane({
paneId,
tabId,
workspaceId: result.workspace.id,
command: agentCommand,
createOrAttach: (input) =>
terminalCreateOrAttach.mutateAsync(input),
write: (input) => terminalWrite.mutateAsync(input),
});
} catch (error) {
removePane(paneId);
toast.error("Failed to start agent", {
description:
error instanceof Error
? error.message
: "Failed to start agent terminal session.",
});
return;
const launchRequest = launchRequestTemplate
? {
...launchRequestTemplate,
workspaceId: result.workspace.id,
}
: null;

if (launchRequest && result.wasExisting) {
const launchResult = await launchAgentSession(launchRequest, {
source: "new-workspace",
createOrAttach: (input) => terminalCreateOrAttach.mutateAsync(input),
write: (input) => terminalWrite.mutateAsync(input),
});
if (launchResult.status === "failed") {
toast.error("Failed to start agent", {
description: launchResult.error ?? "Failed to start agent session.",
});
}
}

if (result.isInitializing) {
if (result.wasExisting) {
toast.success("Opened existing workspace");
} else if (result.isInitializing) {
Comment on lines +349 to +358
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Prevent success toast after failed existing-workspace agent launch.

On Line 350, failure only shows an error toast, but execution continues and still emits the success toast on Line 357 for existing workspaces.

🛠️ Suggested fix
 			if (launchRequest && result.wasExisting) {
 				const launchResult = await launchAgentSession(launchRequest, {
 					source: "new-workspace",
 					createOrAttach: (input) =>
 						terminalCreateOrAttach.mutateAsync(input),
 					write: (input) => terminalWrite.mutateAsync(input),
 				});
 				if (launchResult.status === "failed") {
 					toast.error("Failed to start agent", {
 						description: launchResult.error ?? "Failed to start agent session.",
 					});
+					return;
 				}
 			}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (launchResult.status === "failed") {
toast.error("Failed to start agent", {
description: launchResult.error ?? "Failed to start agent session.",
});
}
}
if (result.isInitializing) {
if (result.wasExisting) {
toast.success("Opened existing workspace");
} else if (result.isInitializing) {
if (launchResult.status === "failed") {
toast.error("Failed to start agent", {
description: launchResult.error ?? "Failed to start agent session.",
});
return;
}
}
if (result.wasExisting) {
toast.success("Opened existing workspace");
} else if (result.isInitializing) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/renderer/components/NewWorkspaceModal/NewWorkspaceModal.tsx`
around lines 350 - 359, The success toast for existing workspaces is shown even
when the agent launch failed; update the control flow in NewWorkspaceModal.tsx
so that after detecting launchResult.status === "failed" (where you call
toast.error) you abort further processing for that branch—either return early
from the surrounding async handler or guard the later result.wasExisting toast
with a check that launchResult.status !== "failed". Ensure you reference the
existing launchResult.status check and the result.wasExisting toast.success call
so the success toast is only reachable when the launch succeeded.

toast.success("Workspace created", {
description: "Setting up in the background...",
});
Expand Down Expand Up @@ -383,6 +430,7 @@ export function NewWorkspaceModal() {
<NewWorkspaceCreateFlow
projectSelector={projectSelector}
selectedAgent={selectedAgent}
agentOptions={selectableAgents}
onSelectedAgentChange={handleAgentChange}
title={title}
onTitleChange={setTitle}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import {
AGENT_LABELS,
AGENT_TYPES,
type AgentType,
} from "@superset/shared/agent-command";
STARTABLE_AGENT_LABELS,
type StartableAgentType,
} from "@superset/shared/agent-launch";
import { Button } from "@superset/ui/button";
import { Kbd, KbdGroup } from "@superset/ui/kbd";
import {
Expand All @@ -22,11 +21,12 @@ import {
} from "renderer/assets/app-icons/preset-icons";
import { useHotkeysStore } from "renderer/stores/hotkeys";

export type WorkspaceCreateAgent = AgentType | "none";
export type WorkspaceCreateAgent = StartableAgentType | "none";

interface NewWorkspaceCreateFlowProps {
projectSelector: ReactNode;
selectedAgent: WorkspaceCreateAgent;
agentOptions: readonly StartableAgentType[];
onSelectedAgentChange: (agent: WorkspaceCreateAgent) => void;
title: string;
onTitleChange: (value: string) => void;
Expand All @@ -42,6 +42,7 @@ interface NewWorkspaceCreateFlowProps {
export function NewWorkspaceCreateFlow({
projectSelector,
selectedAgent,
agentOptions,
onSelectedAgentChange,
title,
onTitleChange,
Expand Down Expand Up @@ -80,7 +81,7 @@ export function NewWorkspaceCreateFlow({
</Tooltip>
<SelectContent>
<SelectItem value="none">No agent</SelectItem>
{AGENT_TYPES.map((agent) => {
{agentOptions.map((agent) => {
const icon = getPresetIcon(agent, isDark);
return (
<SelectItem key={agent} value={agent}>
Expand All @@ -92,7 +93,7 @@ export function NewWorkspaceCreateFlow({
className="size-3.5 object-contain"
/>
)}
{AGENT_LABELS[agent]}
{STARTABLE_AGENT_LABELS[agent]}
</span>
</SelectItem>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import type { AgentLaunchRequest } from "@superset/shared/agent-launch";
import type { ChatMastraLaunchConfig } from "shared/tabs-types";
import type { AgentSessionLaunchContext, LaunchResultPayload } from "../types";

type ChatLaunchRequest = Extract<AgentLaunchRequest, { kind: "chat" }>;

function toLaunchConfig(
request: ChatLaunchRequest,
): ChatMastraLaunchConfig | null {
const initialPrompt = request.chat.initialPrompt?.trim();
const model = request.chat.model?.trim();
const retryCount = request.chat.retryCount;

if (!initialPrompt && !model && retryCount === undefined) {
return null;
}

return {
initialPrompt: initialPrompt || undefined,
metadata: model ? { model } : undefined,
retryCount,
};
}

export async function launchChatAdapter(
request: ChatLaunchRequest,
context: AgentSessionLaunchContext,
): Promise<LaunchResultPayload> {
const tabs = context.tabs;
if (!tabs) {
throw new Error("Missing tabs adapter");
}

let tabId: string;
let paneId: string;
const launchConfig = toLaunchConfig(request);

const targetPaneId = request.chat.paneId;
if (targetPaneId) {
const targetPane = tabs.getPane(targetPaneId);
if (!targetPane) {
throw new Error(`Pane not found: ${targetPaneId}`);
}
const tab = tabs.getTab(targetPane.tabId);
if (!tab || tab.workspaceId !== request.workspaceId) {
throw new Error(`Tab not found for pane: ${targetPaneId}`);
}

if (targetPane.type === "chat-mastra") {
tabId = tab.id;
paneId = targetPane.id;
} else {
const nextPaneId = tabs.addChatPane(tab.id, {
launchConfig,
});
tabId = tab.id;
paneId = nextPaneId;
}
} else {
const created = tabs.addChatTab(request.workspaceId, {
launchConfig,
});
tabId = created.tabId;
paneId = created.paneId;
}

tabs.setTabAutoTitle(tabId, "Superset Chat");

const pane = tabs.getPane(paneId);
let sessionId = request.chat.sessionId ?? pane?.chatMastra?.sessionId ?? null;
if (!sessionId) {
sessionId = crypto.randomUUID();
}

if (pane?.chatMastra?.sessionId !== sessionId) {
tabs.switchChatSession(paneId, sessionId);
}

if (launchConfig) {
tabs.setChatLaunchConfig(paneId, launchConfig);
}
Comment on lines +79 to +81
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Always persist launch config (including null) to avoid stale prompts.

When a pane is reused and the new request has no launch config, the current conditional leaves prior launchConfig in place. That can replay stale chat initialization behavior.

💡 Suggested fix
-	if (launchConfig) {
-		tabs.setChatLaunchConfig(paneId, launchConfig);
-	}
+	tabs.setChatLaunchConfig(paneId, launchConfig);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/desktop/src/renderer/lib/agent-session-orchestrator/adapters/chat-adapter.ts`
around lines 77 - 79, The code currently only calls
tabs.setChatLaunchConfig(paneId, launchConfig) when launchConfig is truthy,
leaving previous config intact when launchConfig is null/undefined; change the
logic to always call tabs.setChatLaunchConfig(paneId, launchConfig) (even when
launchConfig is null) so the pane’s stored launch config is explicitly cleared
when no new config is provided, ensuring no stale prompts are reused; update the
block around paneId/launchConfig in chat-adapter.ts to remove the conditional
and always persist the passed launchConfig.


return {
tabId,
paneId,
sessionId,
};
}
Loading