feat: replace launcher panel with direct tab creation, use directory-based workspace IDs, and add backward-compatible schema fields
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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}`);
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user