fix(onboarding,desktop): don't error on duplicate first agent; wire Switch-server button
First-run project setup surfaced a blocking "Agent with this name already exists" error when creating the default CEO. The default first agent can be created from more than one first-run surface (the unified ModelOnboarding agent step and the project-setup SetupWizard sub-flow), and agent names are unique per store, so the second create returned 409. The step's goal — a first agent exists — was already satisfied, so both onboarding surfaces now treat a name collision as success and advance instead of blocking. The user still creates the agent; they just aren't punished for the flow offering it twice. Separately, the desktop header "Desktop local mode / Switch server" button did nothing: main relays the click as a `shell:open-connection-manager` IPC (webContents.send), but the preload never forwarded it to the `window` DOM event ShellContext listens for, so the signal was dropped. Add the preload bridge so the button opens NativeShellConnectionManager (Local/Remote toggle + remote-server profiles), letting operators switch between running locally, connecting to a remote server, or a different remote. Regression tests: SetupWizard advances on an already-exists agent; preload dispatches the window event on the IPC. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"@runfusion/fusion": patch
|
||||
---
|
||||
|
||||
summary: First-run agent setup no longer errors on a duplicate CEO; desktop Switch-server button now opens the connection menu.
|
||||
category: fix
|
||||
dev: Onboarding agent creation (ModelOnboardingModal + SetupWizardModal) treats a 409 "Agent with this name already exists" as success and advances, since the default CEO can be created from more than one first-run surface. The desktop preload now bridges the `shell:open-connection-manager` IPC (sent by main when the header Switch-server button is clicked) into the `window` DOM event ShellContext listens for, so NativeShellConnectionManager (Local/Remote toggle + remote profiles) actually opens.
|
||||
@@ -1201,6 +1201,19 @@ export function ModelOnboardingModal({
|
||||
await createAgent(buildAgentCreatePayload(agentDraft), targetProjectId);
|
||||
handleNext();
|
||||
} catch (err) {
|
||||
/*
|
||||
* FNXC:Onboarding 2026-07-03-12:10:
|
||||
* The default first agent ("CEO") can already exist by the time this step runs — the operator can
|
||||
* reach agent creation from more than one first-run surface (the project-setup SetupWizard sub-flow
|
||||
* also offers to create the first agent), and agent names are unique per store, so a redundant
|
||||
* create returns 409 "Agent with this name already exists". The step's goal — a first agent exists —
|
||||
* is already satisfied, so treat a name collision as success and advance instead of blocking first
|
||||
* run. The user still creates the agent; they just aren't punished for the flow offering it twice.
|
||||
*/
|
||||
if (err instanceof Error && /already exists/i.test(err.message)) {
|
||||
handleNext();
|
||||
return;
|
||||
}
|
||||
setAgentCreationError(err instanceof Error ? err.message : t("setup.firstAgentCreateError", "Failed to create agent"));
|
||||
} finally {
|
||||
setIsCreatingAgent(false);
|
||||
|
||||
@@ -308,6 +308,22 @@ export function SetupWizardModal({
|
||||
agentOutcome: "created",
|
||||
}));
|
||||
} catch (err) {
|
||||
/*
|
||||
* FNXC:Onboarding 2026-07-03-12:10:
|
||||
* A same-named first agent ("CEO") may already exist — created via another first-run surface
|
||||
* (the unified ModelOnboarding agent step) or a prior attempt; agent names are unique per store.
|
||||
* The desired end state (a first agent exists) already holds, so treat a name collision as success
|
||||
* and complete setup rather than blocking with "Agent with this name already exists".
|
||||
*/
|
||||
if (err instanceof Error && /already exists/i.test(err.message)) {
|
||||
setState((prev) => ({
|
||||
...prev,
|
||||
step: "complete",
|
||||
isCreatingAgent: false,
|
||||
agentOutcome: "created",
|
||||
}));
|
||||
return;
|
||||
}
|
||||
setState((prev) => ({
|
||||
...prev,
|
||||
isCreatingAgent: false,
|
||||
|
||||
@@ -721,6 +721,36 @@ describe("SetupWizardModal", () => {
|
||||
expect(await screen.findByText("You can create agents later from the Agents view.")).toBeDefined();
|
||||
});
|
||||
|
||||
it("treats an already-exists agent as success instead of blocking setup", async () => {
|
||||
// FNXC:Onboarding 2026-07-03: the default "CEO" can already exist when this step runs — reached via
|
||||
// the unified ModelOnboarding agent step or a prior attempt; agent names are unique per store, so a
|
||||
// redundant create returns 409 "already exists". That must NOT block first-run: the desired end
|
||||
// state (a first agent exists) already holds, so the wizard advances to completion.
|
||||
const mockProject = buildMockProject();
|
||||
const onProjectRegistered = vi.fn();
|
||||
mockRegisterProject.mockResolvedValueOnce(mockProject);
|
||||
mockCreateAgent.mockRejectedValueOnce(
|
||||
new Error('Agent with name "CEO" already exists (agentId: agent_ceo)'),
|
||||
);
|
||||
|
||||
render(
|
||||
<SetupWizardModal
|
||||
onProjectRegistered={onProjectRegistered}
|
||||
onClose={vi.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(await registerProjectFromWizard()).toBeDefined();
|
||||
fireEvent.click(screen.getByText("Create Agent"));
|
||||
|
||||
// Advances to the completion step; no blocking error alert.
|
||||
expect(await screen.findByText("Your project is registered and your first agent is ready.")).toBeDefined();
|
||||
expect(screen.queryByRole("alert")).toBeNull();
|
||||
|
||||
fireEvent.click(screen.getByText("Get Started"));
|
||||
expect(onProjectRegistered).toHaveBeenCalledWith(mockProject);
|
||||
});
|
||||
|
||||
it("applies an AI interview draft and waits for explicit creation", async () => {
|
||||
const mockProject = buildMockProject();
|
||||
mockRegisterProject.mockResolvedValueOnce(mockProject);
|
||||
|
||||
@@ -70,6 +70,34 @@ describe("preload", () => {
|
||||
expect(mocks.ipcRenderer.invoke).toHaveBeenCalledWith("shell:openConnectionManager");
|
||||
});
|
||||
|
||||
it("bridges the shell:open-connection-manager IPC into a window DOM event", async () => {
|
||||
// Regression: main sends `shell:open-connection-manager` via webContents.send when the header
|
||||
// "Switch server" button is clicked; the renderer's ShellContext listens via
|
||||
// window.addEventListener. Without the preload forwarding it, clicking did nothing.
|
||||
await importPreloadModule();
|
||||
|
||||
const bridge = mocks.ipcRenderer.on.mock.calls.find(
|
||||
([channel]) => channel === "shell:open-connection-manager",
|
||||
)?.[1] as (() => void) | undefined;
|
||||
expect(bridge).toBeTruthy();
|
||||
|
||||
const dispatched: string[] = [];
|
||||
const priorWindow = (globalThis as { window?: unknown }).window;
|
||||
(globalThis as { window?: unknown }).window = {
|
||||
dispatchEvent: (event: Event) => {
|
||||
dispatched.push(event.type);
|
||||
return true;
|
||||
},
|
||||
};
|
||||
try {
|
||||
bridge?.();
|
||||
} finally {
|
||||
(globalThis as { window?: unknown }).window = priorWindow;
|
||||
}
|
||||
|
||||
expect(dispatched).toContain("shell:open-connection-manager");
|
||||
});
|
||||
|
||||
it("electronAPI exposes update-not-available and update-error listeners", async () => {
|
||||
await importPreloadModule();
|
||||
const api = getExposed<{
|
||||
|
||||
@@ -139,6 +139,23 @@ const fusionShell = {
|
||||
},
|
||||
};
|
||||
|
||||
/*
|
||||
FNXC:DesktopShell 2026-07-03-12:10:
|
||||
Bridge the main-process "open connection manager" request into a window DOM event. The header
|
||||
"Switch server" / "Manage connections" button (ShellConnectionStatus) calls
|
||||
`fusionShell.openConnectionManager()`, which invokes the `shell:openConnectionManager` IPC; main then
|
||||
round-trips back via `webContents.send("shell:open-connection-manager")` (ipc.ts). The renderer's
|
||||
ShellContext listens with `window.addEventListener("shell:open-connection-manager")` to raise
|
||||
`openConnectionManagerSignal` and open NativeShellConnectionManager. Without this forwarder the IPC
|
||||
event was dropped, so clicking "Switch server" in Desktop local mode did nothing. DOM events dispatched
|
||||
on the shared window cross the isolated-preload → page-world boundary, so the page listener fires.
|
||||
Re-registered on every navigation (including the handoff to the http runtime origin) because the
|
||||
preload re-runs per page load.
|
||||
*/
|
||||
ipcRenderer.on("shell:open-connection-manager", () => {
|
||||
window.dispatchEvent(new Event("shell:open-connection-manager"));
|
||||
});
|
||||
|
||||
contextBridge.exposeInMainWorld("electronAPI", electronApi);
|
||||
contextBridge.exposeInMainWorld("fusionAPI", electronApi);
|
||||
contextBridge.exposeInMainWorld("fusionShell", fusionShell);
|
||||
|
||||
Reference in New Issue
Block a user