mirror of
https://github.com/getpaseo/paseo.git
synced 2026-10-14 13:22:37 +00:00
* feat(app): open files and subagents in a closable sheet on compact On phones, opening a file, a provider subagent, or a Paseo subagent now shows it in a bottom sheet over the current tab instead of adding a tab, so the tab strip stays for terminals and root agents. Paseo subagents get "Open as tab" to move the same tab (same tabId and state) into a full tab with a composer; agents in a sheet hide the composer. The decision lives in the workspace layout store's openTab, driven by a per-kind `compactSheet` rule in the panel manifest, so every open path (chat links, Explorer, subagents track, notifications, plugins) follows it without call-site branching, and a new kind opts in with one manifest entry. The app shell publishes the layout mode (compact / single-pane / split-panes) once, so callers stop passing isCompact for placement. A sheet holding unsaved edits is never implicitly displaced; later opens go to tabs underneath, and closing it goes through the existing discard guard. The editor host is position: sticky because CodeMirror's scrollIntoView otherwise scrolls the bottom sheet's container and drags the sheet to full height. * test(app): fail the compact sheet snap check when the sheet is oversized --------- Co-authored-by: paseo-bot[bot] <paseo-bot[bot]@users.noreply.github.com>
95 lines
3.3 KiB
TypeScript
95 lines
3.3 KiB
TypeScript
import { expect, type Locator, type Page } from "@playwright/test";
|
|
|
|
/** The compact workspace sheet, named after the tab it hosts. */
|
|
export function workspaceSheet(page: Page, label: string): Locator {
|
|
return page.getByRole("dialog", { name: label });
|
|
}
|
|
|
|
/** Sheet height as a fraction of the viewport; mirrors the host's 92% snap point. */
|
|
const SHEET_SNAP_FRACTION = 0.92;
|
|
/** The dialog starts below the sheet's drag handle. */
|
|
const SHEET_HANDLE_MAX_HEIGHT = 32;
|
|
|
|
/**
|
|
* The sheet rests at its snap point, docked to the bottom edge. A sheet pushed up past it
|
|
* covers the workspace header; a sheet sunk below it hides content.
|
|
*/
|
|
export async function expectWorkspaceSheetAtSnap(page: Page, label: string): Promise<void> {
|
|
const viewportHeight = page.viewportSize()?.height ?? 0;
|
|
const snapTop = Math.round(viewportHeight * (1 - SHEET_SNAP_FRACTION));
|
|
const sheet = workspaceSheet(page, label);
|
|
await expect
|
|
.poll(async () => {
|
|
const box = await sheet.boundingBox();
|
|
if (!box) return null;
|
|
const top = Math.round(box.y);
|
|
return {
|
|
dockedToBottom: Math.round(box.y + box.height) === viewportHeight,
|
|
topAtSnap: top >= snapTop && top <= snapTop + SHEET_HANDLE_MAX_HEIGHT,
|
|
};
|
|
})
|
|
.toEqual({ dockedToBottom: true, topAtSnap: true });
|
|
}
|
|
|
|
export async function expectOpenTabCount(page: Page, count: number): Promise<void> {
|
|
await expect(
|
|
page.getByRole("button", { name: `Switch tabs (${count} open)` }).filter({ visible: true }),
|
|
).toBeVisible();
|
|
}
|
|
|
|
export async function closeWorkspaceSheet(page: Page, label: string): Promise<void> {
|
|
const sheet = workspaceSheet(page, label);
|
|
await sheet.getByRole("button", { name: "Close" }).click();
|
|
await expect(sheet).toHaveCount(0);
|
|
}
|
|
|
|
export async function openWorkspaceSheetAsTab(page: Page, label: string): Promise<void> {
|
|
const sheet = workspaceSheet(page, label);
|
|
await sheet.getByRole("button", { name: "Open as tab" }).click();
|
|
await expect(sheet).toHaveCount(0);
|
|
}
|
|
|
|
export async function replaceWorkspaceSheetFileText(
|
|
page: Page,
|
|
label: string,
|
|
content: string,
|
|
): Promise<void> {
|
|
const editor = workspaceSheet(page, label)
|
|
.getByTestId("file-source-editor")
|
|
.locator(".cm-content");
|
|
await editor.click();
|
|
await editor.press("ControlOrMeta+A");
|
|
await editor.pressSequentially(content);
|
|
}
|
|
|
|
export async function selectTabFromSwitcher(page: Page, label: string): Promise<void> {
|
|
await page
|
|
.getByRole("button", { name: /Switch tabs/ })
|
|
.filter({ visible: true })
|
|
.click();
|
|
const option = page.getByText(label, { exact: true }).filter({ visible: true }).last();
|
|
await expect(option).toBeVisible();
|
|
await option.click();
|
|
await expect(page.getByRole("button", { name: "Bottom sheet backdrop" })).toHaveCount(0);
|
|
}
|
|
|
|
export async function tapWorkspaceSheetBackdrop(page: Page): Promise<void> {
|
|
await page
|
|
.getByRole("button", { name: "Bottom sheet backdrop" })
|
|
.click({ position: { x: 10, y: 10 } });
|
|
}
|
|
|
|
/** Runs a close that hits the unsaved-changes guard, and declines the discard. */
|
|
export async function expectDeclinedDiscardPrompt(
|
|
page: Page,
|
|
close: () => Promise<void>,
|
|
): Promise<void> {
|
|
const prompt = page.waitForEvent("dialog").then(async (dialog) => {
|
|
const message = dialog.message();
|
|
await dialog.dismiss();
|
|
return message;
|
|
});
|
|
await close();
|
|
expect(await prompt).toContain("discard the draft");
|
|
}
|