Add pinnable new-tab targets to the tab row and sidebar

Any new-tab target (new agent, terminal, browser, or a terminal profile)
can be pinned as a quick-launch button. Pins are global client app state
(persisted), not daemon config, so the same set shows on every workspace;
profile pins resolve per host and are dropped where that profile is absent.

The sidebar mirror launches via a navigate-then-launch intent store rather
than calling the create handlers directly: terminal creation only works in
the mounted/focused workspace, so the sidebar requests the launch and the
workspace screen consumes it on focus.
This commit is contained in:
Mohamed Boudra
2026-06-15 14:09:29 +07:00
parent 42969e40d5
commit 713a042dc5
22 changed files with 835 additions and 65 deletions
+46
View File
@@ -0,0 +1,46 @@
import { expect, type Page } from "@playwright/test";
import type { PinnedTabTarget } from "../../src/workspace-pins/target";
import { pinnedTargetKey } from "../../src/workspace-pins/target";
// The new-tab dropdown menu items carry no stable web ARIA role for the inline
// pin toggle (it lives inside a reveal-on-hover slot), so the pins flow is
// addressed through the test ids the feature assigns per target key — the same
// escape-hatch convention the sidebar kebab and tab context menus use.
function pinToggle(page: Page, target: PinnedTabTarget) {
return page.getByTestId(`workspace-pin-toggle-${pinnedTargetKey(target)}`);
}
function menuItemFor(page: Page, target: PinnedTabTarget) {
return page.getByTestId(`workspace-new-tab-menu-${target.kind}`);
}
export function tabRowPin(page: Page, target: PinnedTabTarget) {
return page.getByTestId(`workspace-pinned-target-${pinnedTargetKey(target)}`);
}
export function sidebarPin(page: Page, target: PinnedTabTarget) {
return page.getByTestId(`sidebar-pinned-target-${pinnedTargetKey(target)}`);
}
export async function openNewTabMenu(page: Page): Promise<void> {
const trigger = page
.getByTestId("workspace-new-tab-menu-trigger")
.filter({ visible: true })
.first();
await expect(trigger).toBeVisible({ timeout: 10_000 });
await trigger.click();
}
// The pin toggle is hidden behind reveal-on-hover (opacity + pointerEvents) on
// desktop web, so the menu item must be hovered before the toggle is clickable.
export async function togglePinFromMenu(page: Page, target: PinnedTabTarget): Promise<void> {
await openNewTabMenu(page);
const item = menuItemFor(page, target);
await expect(item).toBeVisible({ timeout: 10_000 });
await item.hover();
const toggle = pinToggle(page, target);
await expect(toggle).toBeVisible({ timeout: 10_000 });
await toggle.click();
await page.keyboard.press("Escape");
}
@@ -93,6 +93,15 @@ export async function expectFirstTerminalTabContains(page: Page, text: string):
);
}
export async function expectTerminalTabOpen(
page: Page,
options?: { timeout?: number },
): Promise<void> {
await expect(
page.locator('[data-testid^="workspace-tab-terminal_"]').filter({ visible: true }).first(),
).toBeVisible({ timeout: options?.timeout ?? 30_000 });
}
export async function sampleWorkspaceTabIds(
page: Page,
options: { durationMs?: number; intervalMs?: number } = {},
+67
View File
@@ -0,0 +1,67 @@
import { test, expect } from "./fixtures";
import { gotoWorkspace } from "./helpers/launcher";
import { gotoAppShell } from "./helpers/app";
import { getServerId } from "./helpers/server-id";
import { seedWorkspace, type SeededWorkspace } from "./helpers/seed-client";
import { togglePinFromMenu, tabRowPin, sidebarPin } from "./helpers/pins";
import { expectTerminalTabOpen } from "./helpers/workspace-tabs";
import type { PinnedTabTarget } from "../src/workspace-pins/target";
const TERMINAL_TARGET: PinnedTabTarget = { kind: "terminal" };
let workspace: SeededWorkspace;
test.beforeAll(async () => {
workspace = await seedWorkspace({ repoPrefix: "workspace-pins-e2e-" });
});
test.afterAll(async () => {
await workspace?.cleanup();
});
test.describe("Pinned tab targets", () => {
test("pinning a target from the dropdown adds its quick-launch button to the tab row, unpinning removes it", async ({
page,
}) => {
await gotoWorkspace(page, workspace.workspaceId);
await expect(tabRowPin(page, TERMINAL_TARGET)).toHaveCount(0);
await togglePinFromMenu(page, TERMINAL_TARGET);
await expect(tabRowPin(page, TERMINAL_TARGET)).toBeVisible({ timeout: 10_000 });
await togglePinFromMenu(page, TERMINAL_TARGET);
await expect(tabRowPin(page, TERMINAL_TARGET)).toHaveCount(0, { timeout: 10_000 });
});
test("clicking the pinned quick-launch button in the tab row opens a terminal tab", async ({
page,
}) => {
test.setTimeout(45_000);
await gotoWorkspace(page, workspace.workspaceId);
await togglePinFromMenu(page, TERMINAL_TARGET);
await tabRowPin(page, TERMINAL_TARGET).click();
await expectTerminalTabOpen(page);
});
test("the pinned button is mirrored in the sidebar workspace row and launches a terminal in that workspace", async ({
page,
}) => {
test.setTimeout(45_000);
await gotoWorkspace(page, workspace.workspaceId);
await togglePinFromMenu(page, TERMINAL_TARGET);
await gotoAppShell(page);
const row = page.getByTestId(`sidebar-workspace-row-${getServerId()}:${workspace.workspaceId}`);
await expect(row).toBeVisible({ timeout: 30_000 });
await row.hover();
await expect(sidebarPin(page, TERMINAL_TARGET)).toBeVisible({ timeout: 10_000 });
await sidebarPin(page, TERMINAL_TARGET).click();
await expect(page).toHaveURL(/\/workspace\//, { timeout: 30_000 });
await expectTerminalTabOpen(page);
});
});