From 9c1f3eab9df70c64509ed40d27bf4a386288924f Mon Sep 17 00:00:00 2001 From: Pradeep Sahu Date: Wed, 8 Jul 2026 22:29:04 +0530 Subject: [PATCH 1/4] Navigate back to orchestrator after kill --- .../renderer/components/ShellTopbar.test.tsx | 69 ++++++++++++++++++- .../src/renderer/components/ShellTopbar.tsx | 12 ++++ package.json | 3 +- 3 files changed, 82 insertions(+), 2 deletions(-) diff --git a/frontend/src/renderer/components/ShellTopbar.test.tsx b/frontend/src/renderer/components/ShellTopbar.test.tsx index 3b2c9d4190..93b6339be2 100644 --- a/frontend/src/renderer/components/ShellTopbar.test.tsx +++ b/frontend/src/renderer/components/ShellTopbar.test.tsx @@ -5,10 +5,25 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; import type { WorkspaceSession } from "../types/workspace"; import { TopbarKillButton } from "./ShellTopbar"; -const { postMock } = vi.hoisted(() => ({ +const { navigateMock, postMock, useWorkspaceQueryMock } = vi.hoisted(() => ({ + navigateMock: vi.fn(), postMock: vi.fn(), + useWorkspaceQueryMock: vi.fn(), })); +vi.mock("@tanstack/react-router", () => ({ + useNavigate: () => navigateMock, + useParams: () => ({}), +})); + +vi.mock("../hooks/useWorkspaceQuery", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + useWorkspaceQuery: () => useWorkspaceQueryMock(), + }; +}); + vi.mock("../lib/api-client", () => ({ apiClient: { POST: postMock, @@ -35,6 +50,19 @@ const worker: WorkspaceSession = { prs: [], }; +const orchestrator: WorkspaceSession = { + id: "orch-1", + workspaceId: "proj-1", + workspaceName: "my-app", + title: "orchestrator", + provider: "claude-code", + kind: "orchestrator", + branch: "main", + status: "working", + updatedAt: "2026-06-10T00:00:00Z", + prs: [], +}; + function renderKill(session: WorkspaceSession = worker) { const queryClient = new QueryClient({ defaultOptions: { @@ -51,8 +79,14 @@ function renderKill(session: WorkspaceSession = worker) { } beforeEach(() => { + navigateMock.mockReset(); postMock.mockReset(); postMock.mockResolvedValue({ data: { ok: true, sessionId: "sess-1" }, error: undefined }); + useWorkspaceQueryMock.mockReturnValue({ + data: [{ id: "proj-1", name: "my-app", path: "/p/my-app", type: "main", sessions: [] }], + isLoading: false, + isError: false, + }); }); describe("TopbarKillButton", () => { @@ -89,4 +123,37 @@ describe("TopbarKillButton", () => { expect(await screen.findByText("session not found")).toBeInTheDocument(); }); + + it("navigates back to the project orchestrator after a successful kill", async () => { + useWorkspaceQueryMock.mockReturnValue({ + data: [{ id: "proj-1", name: "my-app", path: "/p/my-app", type: "main", sessions: [worker, orchestrator] }], + isLoading: false, + isError: false, + }); + renderKill(); + + await userEvent.click(screen.getByRole("button", { name: "Kill session" })); + await userEvent.click(screen.getByRole("button", { name: "Confirm kill" })); + + await waitFor(() => { + expect(navigateMock).toHaveBeenCalledWith({ + to: "/projects/$projectId/sessions/$sessionId", + params: { projectId: "proj-1", sessionId: "orch-1" }, + }); + }); + }); + + it("falls back to the project board when no orchestrator is available", async () => { + renderKill(); + + await userEvent.click(screen.getByRole("button", { name: "Kill session" })); + await userEvent.click(screen.getByRole("button", { name: "Confirm kill" })); + + await waitFor(() => { + expect(navigateMock).toHaveBeenCalledWith({ + to: "/projects/$projectId", + params: { projectId: "proj-1" }, + }); + }); + }); }); diff --git a/frontend/src/renderer/components/ShellTopbar.tsx b/frontend/src/renderer/components/ShellTopbar.tsx index 892fcabbd5..8af6f78120 100644 --- a/frontend/src/renderer/components/ShellTopbar.tsx +++ b/frontend/src/renderer/components/ShellTopbar.tsx @@ -247,6 +247,8 @@ export function ShellTopbar() { // terminated group. export function TopbarKillButton({ session }: { session: WorkspaceSession }) { const queryClient = useQueryClient(); + const navigate = useNavigate(); + const all = useWorkspaceQuery().data ?? []; const [confirming, setConfirming] = useState(false); const [error, setError] = useState(null); @@ -262,6 +264,16 @@ export function TopbarKillButton({ session }: { session: WorkspaceSession }) { void captureRendererEvent("ao.renderer.session_kill_succeeded", { project_id: session.workspaceId }); setConfirming(false); void queryClient.invalidateQueries({ queryKey: workspaceQueryKey }); + const projectId = session.workspaceId; + const orchestrator = projectId ? findProjectOrchestrator(all, projectId) : undefined; + if (orchestrator && projectId) { + void navigate({ + to: "/projects/$projectId/sessions/$sessionId", + params: { projectId, sessionId: orchestrator.id }, + }); + } else if (projectId) { + void navigate({ to: "/projects/$projectId", params: { projectId } }); + } }, onError: (e) => { void captureRendererEvent("ao.renderer.session_kill_failed", { project_id: session.workspaceId }); diff --git a/package.json b/package.json index fa4a701a64..2b519ad766 100644 --- a/package.json +++ b/package.json @@ -11,5 +11,6 @@ }, "devDependencies": { "openapi-typescript": "7.4.4" - } + }, + "packageManager": "pnpm@9.15.0+sha512.76e2379760a4328ec4415815bcd6628dee727af3779aaa4c914e3944156c4299921a89f976381ee107d41f12cfa4b66681ca9c718f0668fa0831ed4c6d8ba56c" } From eca49bd4220a7eaaec8d565a86057df278ef7e44 Mon Sep 17 00:00:00 2001 From: Pradeep Sahu Date: Wed, 8 Jul 2026 22:32:11 +0530 Subject: [PATCH 2/4] Remove accidental packageManager change --- package.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/package.json b/package.json index 2b519ad766..fa4a701a64 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,5 @@ }, "devDependencies": { "openapi-typescript": "7.4.4" - }, - "packageManager": "pnpm@9.15.0+sha512.76e2379760a4328ec4415815bcd6628dee727af3779aaa4c914e3944156c4299921a89f976381ee107d41f12cfa4b66681ca9c718f0668fa0831ed4c6d8ba56c" + } } From 2f3d69fdcf46931cd3436b5dea3d5605d8dec966 Mon Sep 17 00:00:00 2001 From: Pradeep Sahu Date: Thu, 9 Jul 2026 09:54:24 +0530 Subject: [PATCH 3/4] Refactor TopbarKillButton to use parent-owned navigation --- .../src/renderer/components/ShellTopbar.tsx | 40 ++++++++++++------- 1 file changed, 26 insertions(+), 14 deletions(-) diff --git a/frontend/src/renderer/components/ShellTopbar.tsx b/frontend/src/renderer/components/ShellTopbar.tsx index 8af6f78120..d1e4310e15 100644 --- a/frontend/src/renderer/components/ShellTopbar.tsx +++ b/frontend/src/renderer/components/ShellTopbar.tsx @@ -195,7 +195,22 @@ export function ShellTopbar() { ) : null} {/* Kill control sits beside the orchestrator link for active workers — moved here from the inspector's Summary "Danger zone". */} - {!isOrchestrator && session && sessionIsActive(session) ? : null} + {!isOrchestrator && session && sessionIsActive(session) ? ( + { + if (orchestratorId) { + void navigate({ + to: "/projects/$projectId/sessions/$sessionId", + params: { projectId: workspaceId, sessionId: orchestratorId }, + }); + return; + } + void navigate({ to: "/projects/$projectId", params: { projectId: workspaceId } }); + }} + /> + ) : null} {!isOrchestrator && (