feat: replace launcher panel with direct tab creation, use directory-based workspace IDs, and add backward-compatible schema fields

This commit is contained in:
Mohamed Boudra
2026-04-06 18:15:30 +07:00
parent 6e5cad812e
commit 7173342bcb
38 changed files with 1020 additions and 500 deletions
+23 -24
View File
@@ -65,9 +65,16 @@ export async function getActiveTabTestId(page: Page): Promise<string | null> {
// ─── Tab actions ───────────────────────────────────────────────────────────
/** Click the '+' button in the tab bar to open a new launcher tab. */
/** Click the new agent tab button in the tab bar. Creates a draft/chat tab directly. */
export async function clickNewTabButton(page: Page): Promise<void> {
const button = page.getByTestId("workspace-new-tab");
const button = page.getByTestId("workspace-new-agent-tab");
await expect(button).toBeVisible({ timeout: 10_000 });
await button.click();
}
/** Click the new terminal button in the workspace tab bar. Creates a terminal tab directly. */
export async function clickNewTerminalButton(page: Page): Promise<void> {
const button = page.getByTestId("workspace-new-terminal");
await expect(button).toBeVisible({ timeout: 10_000 });
await button.click();
}
@@ -77,41 +84,35 @@ export async function pressNewTabShortcut(page: Page): Promise<void> {
await page.keyboard.press("Meta+t");
}
// ─── Launcher panel assertions ─────────────────────────────────────────────
// ─── Tab bar assertions ───────────────────────────────────────────────────
/** Wait for the launcher panel to render with its primary tiles. */
/** @deprecated The launcher panel was removed. Actions go directly to their target. */
export async function waitForLauncherPanel(page: Page): Promise<void> {
await expect(page.getByRole("button", { name: "New Chat" }).first()).toBeVisible({
timeout: 15_000,
});
await expect(page.getByRole("button", { name: "Terminal" }).first()).toBeVisible({
timeout: 15_000,
});
// No-op: the launcher panel no longer exists.
}
/** Assert the launcher panel has a "New Chat" tile. */
/** Assert the new agent tab button is visible in the tab bar. */
export async function assertNewChatTileVisible(page: Page): Promise<void> {
await expect(page.getByRole("button", { name: "New Chat" }).first()).toBeVisible();
await expect(page.getByTestId("workspace-new-agent-tab").first()).toBeVisible();
}
/** Assert the launcher panel has a "Terminal" tile. */
/** Assert the new terminal button is visible in the tab bar. */
export async function assertTerminalTileVisible(page: Page): Promise<void> {
await expect(page.getByRole("button", { name: "Terminal" }).first()).toBeVisible();
await expect(page.getByTestId("workspace-new-terminal").first()).toBeVisible();
}
// ─── Launcher tile clicks ──────────────────────────────────────────────────
// ─── Tab creation actions ─────────────────────────────────────────────────
/** Click the "New Chat" tile on the launcher panel. */
/** Click the new agent tab button to create a draft/chat tab. */
export async function clickNewChat(page: Page): Promise<void> {
const button = page.getByRole("button", { name: "New Chat" }).first();
const button = page.getByTestId("workspace-new-agent-tab");
await expect(button).toBeVisible({ timeout: 10_000 });
await button.click();
}
/** Click the "Terminal" tile on the launcher panel. */
/** Click the new terminal button to create a terminal tab. */
export async function clickTerminal(page: Page): Promise<void> {
const button = page.getByRole("button", { name: "Terminal", exact: true }).first();
const button = page.getByTestId("workspace-new-terminal");
await expect(button).toBeVisible({ timeout: 10_000 });
await button.click();
}
@@ -134,11 +135,9 @@ export async function waitForTabWithTitle(
).toBeVisible({ timeout });
}
/** Assert the new-tab '+' button is visible and there is only one. */
/** Assert the new agent tab button is visible in the tab bar. */
export async function assertSingleNewTabButton(page: Page): Promise<void> {
const buttons = page.getByTestId("workspace-new-tab");
// There might be multiple panes, each with a "+" button
// But within a single pane there should only be one
const buttons = page.getByTestId("workspace-new-agent-tab");
const count = await buttons.count();
expect(count).toBeGreaterThanOrEqual(1);
}
@@ -2,8 +2,6 @@ import { expect, type Page } from "@playwright/test";
import {
clickNewChat,
clickTerminal,
countTabsOfKind,
getTabTestIds,
} from "./launcher";
import { setupDeterministicPrompt, waitForTerminalContent } from "./terminal-perf";
@@ -25,12 +23,8 @@ export async function expectTerminalCwd(page: Page, expectedPath: string): Promi
}
export async function createStandaloneTerminalFromLauncher(page: Page): Promise<void> {
const tabIdsBefore = await getTabTestIds(page);
const launcherCountBefore = await countTabsOfKind(page, "launcher");
await clickTerminal(page);
await expect(terminalSurface(page)).toBeVisible({ timeout: 20_000 });
await expect.poll(() => countTabsOfKind(page, "launcher")).toBe(launcherCountBefore - 1);
await expect.poll(async () => (await getTabTestIds(page)).length).toBe(tabIdsBefore.length);
}
export async function createAgentChatFromLauncher(page: Page): Promise<void> {
@@ -38,5 +32,4 @@ export async function createAgentChatFromLauncher(page: Page): Promise<void> {
await expect(composerInput(page)).toBeVisible({ timeout: 15_000 });
await expect(composerInput(page)).toBeEditable({ timeout: 15_000 });
await expect(page.getByTestId("agent-loading")).toHaveCount(0);
await expect(page.getByRole("button", { name: "New Chat" })).toHaveCount(0);
}
+26 -10
View File
@@ -1,3 +1,4 @@
import { realpathSync } from "node:fs";
import path from "node:path";
import { randomUUID } from "node:crypto";
import { pathToFileURL } from "node:url";
@@ -147,8 +148,8 @@ export async function createWorkspaceFromSidebar(page: Page, repoPath: string):
await expect(button).toBeVisible({ timeout: 30_000 });
await expect(button).toBeEnabled({ timeout: 30_000 });
await button.click();
await expect(page).toHaveURL(/\/workspace\//, { timeout: 30_000 });
await expect(page.getByTestId("workspace-setup-dialog")).toBeVisible({ timeout: 30_000 });
await expect(page).toHaveURL(/\/new\?/, { timeout: 30_000 });
await expect(page.getByRole("textbox", { name: "Message agent..." }).first()).toBeVisible({ timeout: 30_000 });
}
export async function getCurrentWorkspaceIdFromRoute(page: Page): Promise<string> {
@@ -176,16 +177,16 @@ export async function createChatAgentFromWorkspaceSetup(
page: Page,
input: { message: string },
): Promise<void> {
const dialog = workspaceSetupDialog(page);
await dialog.getByRole("button", { name: /Chat Agent/i }).click();
const messageInput = dialog.getByRole("textbox", { name: "Message agent..." }).first();
const messageInput = page.getByRole("textbox", { name: "Message agent..." }).first();
await expect(messageInput).toBeVisible({ timeout: 15_000 });
await messageInput.fill(input.message);
await dialog.getByRole("button", { name: "Send message" }).click();
await messageInput.press("Enter");
}
/**
* @deprecated The new workspace screen no longer has a standalone terminal button.
* Use the daemon API to create a workspace, then open a terminal from the launcher.
*/
export async function createStandaloneTerminalFromWorkspaceSetup(page: Page): Promise<void> {
await workspaceSetupDialog(page)
.getByRole("button", { name: /^Terminal Create the workspace/i })
@@ -209,7 +210,21 @@ export async function waitForWorkspaceSetupDialogToClose(page: Page, timeoutMs =
}
export async function expectSetupPanel(page: Page): Promise<void> {
await expect(page.getByText("Workspace setup", { exact: true })).toBeVisible({ timeout: 30_000 });
// If the setup panel is already visible (auto-opened), we're done.
const panel = page.getByTestId("workspace-setup-panel");
if (await panel.isVisible().catch(() => false)) {
return;
}
// Otherwise open it manually via workspace header actions menu.
// Use the specific testID to avoid matching the sidebar kebab which shares
// the same "Workspace actions" accessibility label.
const actionsButton = page.getByTestId("workspace-header-menu-trigger");
await expect(actionsButton).toBeVisible({ timeout: 10_000 });
await actionsButton.click();
const showSetup = page.getByTestId("workspace-header-show-setup");
await expect(showSetup).toBeVisible({ timeout: 5_000 });
await showSetup.click();
await expect(panel).toBeVisible({ timeout: 30_000 });
}
export async function expectSetupStatus(
@@ -257,10 +272,11 @@ export async function findWorktreeWorkspaceForProject(
workspaceDirectory: string;
}> {
const payload = await client.fetchWorkspaces();
const normalizedRepoPath = realpathSync(repoPath);
const workspace =
payload.entries.find(
(entry) =>
entry.projectRootPath === repoPath && entry.workspaceDirectory !== repoPath,
entry.projectRootPath === normalizedRepoPath && entry.workspaceDirectory !== normalizedRepoPath,
) ?? null;
if (!workspace) {
throw new Error(`Failed to find created worktree workspace for ${repoPath}`);
+3 -2
View File
@@ -1,5 +1,5 @@
import { execSync } from "node:child_process";
import { mkdtemp, writeFile, rm, mkdir } from "node:fs/promises";
import { mkdtemp, writeFile, rm, mkdir, realpath } from "node:fs/promises";
import { tmpdir } from "node:os";
import path from "node:path";
@@ -17,7 +17,8 @@ export const createTempGitRepo = async (
},
): Promise<TempRepo> => {
// Keep E2E repo paths short so terminal prompt + typed commands stay visible without zsh clipping.
const tempRoot = process.platform === "win32" ? tmpdir() : "/tmp";
// Resolve symlinks (macOS: /tmp → /private/tmp) so paths match the daemon's resolved paths.
const tempRoot = process.platform === "win32" ? tmpdir() : await realpath("/tmp");
const repoPath = await mkdtemp(path.join(tempRoot, prefix));
const withRemote = options?.withRemote ?? false;