From b44e7b0b9025d7397d6da79cecf70efa3a0ccdb3 Mon Sep 17 00:00:00 2001 From: "paseo-bot[bot]" <266920839+paseo-bot[bot]@users.noreply.github.com> Date: Mon, 28 Sep 2026 17:27:37 +0100 Subject: [PATCH] Open a subagent in the split pane it was opened from (#5451) * fix(app): open a subagent in the pane it was opened from Opening a subagent from an agent's composer placed the new tab in the focused pane. Clicking the subagent row does not focus its pane, so in a split view the subagent landed in whichever pane last had focus. Place an implicit main-location open that has a parent tab in that tab's pane. Refs #5433 * test(app): cover subagent split-pane placement in browser * test(app): use browser repro for subagent pane placement --------- Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com> Co-authored-by: Mohamed Boudra (cherry picked from commit 7bb7d4ed08a2568ff1eed25a94c512e1acb9fd3b) --- .../e2e/browser/subagent-origin-pane.spec.ts | 149 ++++++++++++++++++ .../app/src/workspace-tabs/open-beside.ts | 5 + 2 files changed, 154 insertions(+) create mode 100644 packages/app/e2e/browser/subagent-origin-pane.spec.ts diff --git a/packages/app/e2e/browser/subagent-origin-pane.spec.ts b/packages/app/e2e/browser/subagent-origin-pane.spec.ts new file mode 100644 index 0000000000..47a0120da6 --- /dev/null +++ b/packages/app/e2e/browser/subagent-origin-pane.spec.ts @@ -0,0 +1,149 @@ +import { writeFile } from "node:fs/promises"; +import path from "node:path"; +import { expect, test, type Page } from "../support/fixtures"; +import { runWorkspaceActionFromCommandCenter } from "../support/helpers/command-center-workspace-actions"; +import { openAgentRoute, seedMockAgentWorkspace } from "../support/helpers/mock-agent"; +import { seedWorkspace } from "../support/helpers/seed-client"; +import { openSubagentsTrack, seedParentWithSubagent } from "../support/helpers/subagents"; + +const SETTINGS_KEY = "@paseo:app-settings"; + +async function openParentInRightPane(page: Page, parentId: string, workspaceId: string) { + await page.setViewportSize({ width: 1400, height: 900 }); + await openAgentRoute(page, { workspaceId, agentId: parentId }); + await expect(page.getByTestId(`workspace-tab-agent_${parentId}`)).toBeVisible(); + await page.getByTestId("workspace-pane-main").getByTestId("workspace-new-tab-button").click(); + await page.getByTestId("workspace-new-tab-menu-agent").click(); + await page.getByTestId(`workspace-tab-agent_${parentId}`).click(); + await runWorkspaceActionFromCommandCenter(page, "Split pane right"); + await page + .getByTestId("workspace-pane-main") + .getByTestId(`workspace-tab-agent_${parentId}`) + .click(); + await page.keyboard.press("Meta+Alt+Shift+ArrowRight"); + + const left = page.getByTestId("workspace-pane-main"); + const right = page + .locator('[data-testid^="workspace-pane-"]') + .filter({ has: page.getByTestId(`workspace-tab-agent_${parentId}`) }); + await expect(left).toBeVisible(); + await expect(right).toBeVisible(); + await expect(right).not.toHaveAttribute("data-testid", "workspace-pane-main"); + await left.locator('[data-testid^="workspace-tab-draft_"]').click(); + return { left, right }; +} + +test.beforeEach(async ({ page }) => { + await page.addInitScript(() => { + Object.defineProperty(navigator, "platform", { get: () => "MacIntel" }); + }); +}); + +test("a subagent opened from a right-pane parent stays with its parent when left has focus", async ({ + page, +}) => { + const workspace = await seedWorkspace({ repoPrefix: "subagent-origin-pane-" }); + try { + const pair = await seedParentWithSubagent(workspace, { + parentTitle: "Right parent", + childTitle: "Child agent", + }); + const { left, right } = await openParentInRightPane(page, pair.parent.id, pair.workspaceId); + await openSubagentsTrack(page); + await page.getByTestId(`subagents-track-row-${pair.child.id}`).click(); + + await expect(right.getByTestId(`workspace-tab-agent_${pair.child.id}`)).toBeVisible(); + await expect(left.getByTestId(`workspace-tab-agent_${pair.child.id}`)).toHaveCount(0); + } finally { + await workspace.cleanup(); + } +}); + +test("the side preference opens a right-pane parent's subagent in the side pane", async ({ + page, +}) => { + await page.addInitScript((key) => { + localStorage.setItem(key, JSON.stringify({ openInSidePane: { subagents: true } })); + }, SETTINGS_KEY); + const workspace = await seedWorkspace({ repoPrefix: "subagent-side-preference-" }); + try { + const pair = await seedParentWithSubagent(workspace, { + parentTitle: "Right parent", + childTitle: "Child agent", + }); + const { left, right } = await openParentInRightPane(page, pair.parent.id, pair.workspaceId); + await openSubagentsTrack(page); + await page.getByTestId(`subagents-track-row-${pair.child.id}`).click(); + + const childPane = page + .locator('[data-testid^="workspace-pane-"]') + .filter({ has: page.getByTestId(`workspace-tab-agent_${pair.child.id}`) }); + await expect(childPane).toBeVisible(); + expect(await childPane.getAttribute("data-testid")).not.toBe( + await right.getAttribute("data-testid"), + ); + await expect(childPane).not.toHaveAttribute("data-testid", "workspace-pane-main"); + await expect(left.getByTestId(`workspace-tab-agent_${pair.child.id}`)).toHaveCount(0); + } finally { + await workspace.cleanup(); + } +}); + +test("opening an already-tabbed subagent reveals its left tab without a duplicate", async ({ + page, +}) => { + const workspace = await seedWorkspace({ repoPrefix: "subagent-existing-tab-" }); + try { + const pair = await seedParentWithSubagent(workspace, { + parentTitle: "Right parent", + childTitle: "Child agent", + }); + const { left, right } = await openParentInRightPane(page, pair.parent.id, pair.workspaceId); + await openSubagentsTrack(page); + await page.getByTestId(`subagents-track-row-${pair.child.id}`).click(); + await expect(right.getByTestId(`workspace-tab-agent_${pair.child.id}`)).toBeVisible(); + await page.keyboard.press("Meta+Alt+Shift+ArrowLeft"); + await expect(left.getByTestId(`workspace-tab-agent_${pair.child.id}`)).toBeVisible(); + await right.getByTestId(`workspace-tab-agent_${pair.parent.id}`).click(); + await openSubagentsTrack(page); + await page.getByTestId(`subagents-track-row-${pair.child.id}`).click(); + + await expect(left.getByTestId(`workspace-tab-agent_${pair.child.id}`)).toBeVisible(); + await expect(right.getByTestId(`workspace-tab-agent_${pair.child.id}`)).toHaveCount(0); + await expect(page.getByTestId(`workspace-tab-agent_${pair.child.id}`)).toHaveCount(1); + } finally { + await workspace.cleanup(); + } +}); + +test("a preferred chat file link still opens beside its right-pane agent", async ({ page }) => { + const target = "linked.ts:3"; + const workspace = await seedMockAgentWorkspace({ + repoPrefix: "chat-file-origin-pane-", + title: "File link parent", + initialPrompt: [ + "Generate a title and a git branch name for a coding agent from the user prompt and attachments.", + "Return JSON only with fields 'title' and 'branch'.", + "", + "", + `Open \`${target}\` now`, + "", + ].join("\n"), + }); + try { + await writeFile(path.join(workspace.cwd, "linked.ts"), "one\ntwo\nthree\n", "utf8"); + const { left, right } = await openParentInRightPane( + page, + workspace.agentId, + workspace.workspaceId, + ); + const fileLink = right.getByText(target, { exact: true }); + await expect(fileLink).toBeVisible({ timeout: 15_000 }); + await fileLink.click(); + + await expect(right.getByTestId("workspace-tab-file_linked.ts")).toBeVisible(); + await expect(left.getByTestId("workspace-tab-file_linked.ts")).toHaveCount(0); + } finally { + await workspace.cleanup(); + } +}); diff --git a/packages/app/src/workspace-tabs/open-beside.ts b/packages/app/src/workspace-tabs/open-beside.ts index 7acda716c1..b36d227c3b 100644 --- a/packages/app/src/workspace-tabs/open-beside.ts +++ b/packages/app/src/workspace-tabs/open-beside.ts @@ -4,6 +4,7 @@ import { collectAllPanes, DEFAULT_PANE_ID, findPaneById, + findPaneContainingTab, useWorkspaceLayoutStore, type WorkspaceTabPlacement, } from "@/stores/workspace-layout-store"; @@ -109,6 +110,10 @@ export function openWorkspaceTargetAtLocation( if (shouldOpenBeside && !targetAlreadyExists) { const paneId = store.ensureSidePane(input.workspaceKey); placement = paneId ? { mode: "prefer", paneId } : undefined; + } else if (!shouldOpenBeside && layout && input.parentTabId) { + // A target opened from a tab belongs in that tab's pane, whichever pane has focus. + const parentPane = findPaneContainingTab(layout.root, input.parentTabId); + placement = parentPane ? { mode: "prefer", paneId: parentPane.id } : undefined; } return store.openTab({ workspaceKey: input.workspaceKey,