diff --git a/client/src/components/lanes/AddLaneModal.tsx b/client/src/components/lanes/AddLaneModal.tsx index 4630cd8..8c0a594 100644 --- a/client/src/components/lanes/AddLaneModal.tsx +++ b/client/src/components/lanes/AddLaneModal.tsx @@ -41,6 +41,11 @@ export function AddLaneModal({ const [branchesError, setBranchesError] = useState(null); const [busy, setBusy] = useState(false); const [error, setError] = useState(null); + const [setupResult, setSetupResult] = useState<{ + profile: "scaffolded" | "skipped" | "failed"; + agents: "ok" | "failed"; + mcp: "ok" | "failed"; + } | null>(null); const reset = () => { setSourceRepo(""); @@ -50,6 +55,7 @@ export function AddLaneModal({ setBranchesError(null); setError(null); setBusy(false); + setSetupResult(null); }; // Look up the repo's branches once the path settles - debounced so every @@ -96,6 +102,32 @@ export function AddLaneModal({ title: name, base: base || undefined, }); + + const [profileOutcome, agentsOutcome, mcpOutcome] = await Promise.allSettled([ + api.lanes.profileInit(result.lane.id), + api.lanes.agentsInstall(result.lane.id), + api.lanes.mcpSync(result.lane.id), + ]); + setSetupResult({ + profile: + profileOutcome.status === "fulfilled" + ? profileOutcome.value.scaffolded + ? "scaffolded" + : "skipped" + : "failed", + agents: agentsOutcome.status === "fulfilled" ? "ok" : "failed", + mcp: mcpOutcome.status === "fulfilled" ? "ok" : "failed", + }); + + if (import.meta.env.DEV) { + console.info("[add-lane] auto-setup result:", { + profile: + profileOutcome.status === "fulfilled" ? profileOutcome.value : profileOutcome.reason, + agents: agentsOutcome.status === "fulfilled" ? agentsOutcome.value : agentsOutcome.reason, + mcp: mcpOutcome.status === "fulfilled" ? mcpOutcome.value : mcpOutcome.reason, + }); + } + reset(); onAdded(result.lane); onClose(); diff --git a/client/src/components/lanes/__tests__/AddLaneModal.test.tsx b/client/src/components/lanes/__tests__/AddLaneModal.test.tsx index a03ece4..27c8897 100644 --- a/client/src/components/lanes/__tests__/AddLaneModal.test.tsx +++ b/client/src/components/lanes/__tests__/AddLaneModal.test.tsx @@ -18,7 +18,15 @@ import type { CwdSuggestion } from "../../../lib/api"; import type { Lane } from "../../../lib/types"; vi.mock("../../../lib/api", () => ({ - api: { lanes: { branches: vi.fn(), worktree: vi.fn() } }, + api: { + lanes: { + branches: vi.fn(), + worktree: vi.fn(), + profileInit: vi.fn(), + agentsInstall: vi.fn(), + mcpSync: vi.fn(), + }, + }, })); function laneFixture(over: Partial = {}): Lane { @@ -76,6 +84,12 @@ async function focusField(user: ReturnType, el: HTMLElem beforeEach(() => { vi.mocked(api.lanes.branches).mockReset(); vi.mocked(api.lanes.worktree).mockReset(); + vi.mocked(api.lanes.profileInit).mockResolvedValue({ + scaffolded: false, + reason: "no detectable Node.js project", + }); + vi.mocked(api.lanes.agentsInstall).mockResolvedValue({ installed: [] }); + vi.mocked(api.lanes.mcpSync).mockResolvedValue({ servers: [], profilesSeeded: [] }); }); describe("AddLaneModal", () => { @@ -169,4 +183,51 @@ describe("AddLaneModal", () => { renderModal({ open: false }); expect(screen.queryByRole("dialog")).toBeNull(); }); + + it("fires profileInit, agentsInstall, and mcpSync after a successful worktree call", async () => { + vi.mocked(api.lanes.branches).mockResolvedValue({ branches: ["main"], current: "main" }); + vi.mocked(api.lanes.worktree).mockResolvedValue({ + lane: { id: 42, title: "demo", cwd: "/lanes/demo", status: "provisioning" } as Lane, + }); + const onAdded = vi.fn(); + renderModal({ onAdded }); + const user = userEvent.setup(); + + const repoField = screen.getByLabelText("Source repository"); + await focusField(user, repoField); + await user.type(repoField, "/Users/tester/projects/repo"); + await screen.findByLabelText("Branch to fork from"); + await user.type(screen.getByLabelText("Title"), "demo"); + await user.click(screen.getByRole("button", { name: "Add lane" })); + + await waitFor(() => expect(api.lanes.worktree).toHaveBeenCalled()); + await waitFor(() => expect(api.lanes.profileInit).toHaveBeenCalledWith(42)); + expect(api.lanes.agentsInstall).toHaveBeenCalledWith(42); + expect(api.lanes.mcpSync).toHaveBeenCalledWith(42); + await waitFor(() => expect(onAdded).toHaveBeenCalled()); + }); + + it("still calls onAdded and closes even when every setup call fails", async () => { + vi.mocked(api.lanes.branches).mockResolvedValue({ branches: ["main"], current: "main" }); + vi.mocked(api.lanes.worktree).mockResolvedValue({ + lane: { id: 43, title: "demo2", cwd: "/lanes/demo2", status: "provisioning" } as Lane, + }); + vi.mocked(api.lanes.profileInit).mockRejectedValue(new Error("boom")); + vi.mocked(api.lanes.agentsInstall).mockRejectedValue(new Error("boom")); + vi.mocked(api.lanes.mcpSync).mockRejectedValue(new Error("boom")); + const onAdded = vi.fn(); + const onClose = vi.fn(); + renderModal({ onAdded, onClose }); + const user = userEvent.setup(); + + const repoField = screen.getByLabelText("Source repository"); + await focusField(user, repoField); + await user.type(repoField, "/Users/tester/projects/repo"); + await screen.findByLabelText("Branch to fork from"); + await user.type(screen.getByLabelText("Title"), "demo2"); + await user.click(screen.getByRole("button", { name: "Add lane" })); + + await waitFor(() => expect(onAdded).toHaveBeenCalled()); + expect(onClose).toHaveBeenCalled(); + }); }); diff --git a/client/src/lib/api.ts b/client/src/lib/api.ts index 576c220..9507698 100644 --- a/client/src/lib/api.ts +++ b/client/src/lib/api.ts @@ -2080,6 +2080,11 @@ export const api = { method: "POST", body: "{}", }), + profileInit: (id: number, force = false) => + request<{ scaffolded: boolean; written?: string[]; todos?: string[]; reason?: string }>( + `/lanes/${id}/profile/init`, + { method: "POST", body: JSON.stringify({ force }) } + ), integration: (id: number, name: string) => request<{ enabled: boolean }>(`/lanes/${id}/integrations/${encodeURIComponent(name)}`), syncBaseCheck: (id: number, branch?: string) =>