Open workspace files in desktop targets

This commit is contained in:
Mohamed Boudra
2026-06-03 18:34:24 +08:00
committed by GitHub
parent 45f412456c
commit 3681fd132b
30 changed files with 1830 additions and 822 deletions
+50 -1
View File
@@ -1,4 +1,5 @@
import { readFileSync } from "node:fs";
import { appendFile } from "node:fs/promises";
import { expect, type Page } from "@playwright/test";
import { openSettings } from "./app";
import { getE2EDaemonPort } from "./daemon-port";
@@ -64,6 +65,21 @@ export interface DesktopBridgeConfig {
* false so tests that only assert copy don't inadvertently trigger state changes.
*/
confirmShouldAccept?: boolean;
editorTargets?: DesktopEditorTargetConfig[];
editorRecordPath?: string;
}
interface DesktopEditorTargetConfig {
id: string;
label: string;
kind: "editor" | "file-manager";
}
interface DesktopEditorOpenRecord {
editorId: string;
path: string;
cwd?: string;
mode?: "open" | "reveal";
}
export interface ConfirmDialogCall {
@@ -74,6 +90,7 @@ export interface ConfirmDialogCall {
declare global {
interface Window {
__capturedDialogCall: ConfirmDialogCall | undefined;
__recordDesktopEditorOpen?: (input: DesktopEditorOpenRecord) => Promise<void>;
}
}
@@ -87,6 +104,15 @@ declare global {
* can assert dialog copy without depending on window.confirm concatenation.
*/
export async function injectDesktopBridge(page: Page, config: DesktopBridgeConfig): Promise<void> {
if (config.editorRecordPath) {
await page.exposeFunction(
"__recordDesktopEditorOpen",
async (input: DesktopEditorOpenRecord) => {
await appendFile(config.editorRecordPath as string, `${JSON.stringify(input)}\n`, "utf8");
},
);
}
await page.addInitScript((cfg) => {
// Mutable state shared across IPC calls within this page
let manageDaemon = cfg.manageBuiltInDaemon ?? false;
@@ -108,7 +134,19 @@ export async function injectDesktopBridge(page: Page, config: DesktopBridgeConfi
};
}
(window as unknown as { paseoDesktop: unknown }).paseoDesktop = {
const desktopBridge: {
platform: string;
invoke: (command: string, args?: Record<string, unknown>) => Promise<unknown>;
dialog: {
ask: (message: string, options?: Record<string, unknown>) => Promise<boolean>;
};
getPendingOpenProject: () => Promise<string | null>;
events: { on: () => Promise<() => void> };
editor?: {
listTargets: () => Promise<DesktopEditorTargetConfig[]>;
openTarget: (input: DesktopEditorOpenRecord) => Promise<void>;
};
} = {
platform: "darwin",
invoke: async (command: string, args?: Record<string, unknown>) => {
if (command === "check_app_update") {
@@ -202,6 +240,17 @@ export async function injectDesktopBridge(page: Page, config: DesktopBridgeConfi
getPendingOpenProject: async () => null,
events: { on: async () => () => undefined },
};
if (cfg.editorTargets) {
desktopBridge.editor = {
listTargets: async () => cfg.editorTargets ?? [],
openTarget: async (input: DesktopEditorOpenRecord) => {
await window.__recordDesktopEditorOpen?.(input);
},
};
}
(window as unknown as { paseoDesktop: unknown }).paseoDesktop = desktopBridge;
}, config);
}
@@ -5,8 +5,10 @@ import { injectDesktopBridge } from "./helpers/desktop-updates";
import { clickSettingsBackToWorkspace } from "./helpers/settings";
interface EditorOpenRecord {
command: string;
args: string[];
editorId: string;
path: string;
cwd?: string;
mode?: "open" | "reveal";
}
function requireE2EEnv(name: string): string {
@@ -43,8 +45,8 @@ async function chooseEditorTarget(page: Page, targetId: "vscode"): Promise<void>
async function expectEditorOpened(input: {
recordPath: string;
command: string;
workspacePath: string;
editorId: string;
path: string;
afterCount: number;
}): Promise<void> {
await expect
@@ -53,10 +55,7 @@ async function expectEditorOpened(input: {
const records = await readEditorOpenRecords(input.recordPath);
return records
.slice(input.afterCount)
.some(
(record) =>
record.command === input.command && record.args.includes(input.workspacePath),
);
.some((record) => record.editorId === input.editorId && record.path === input.path);
},
{ timeout: 30_000 },
)
@@ -73,7 +72,14 @@ test.describe("Workspace open in editor", () => {
const serverId = requireE2EEnv("E2E_SERVER_ID");
const recordPath = requireE2EEnv("E2E_EDITOR_RECORD_PATH");
await rm(recordPath, { force: true });
await injectDesktopBridge(page, { serverId });
await injectDesktopBridge(page, {
serverId,
editorTargets: [
{ id: "cursor", label: "Cursor", kind: "editor" },
{ id: "vscode", label: "VS Code", kind: "editor" },
],
editorRecordPath: recordPath,
});
const workspace = await withWorkspace({ prefix: "workspace-editor-target-" });
await workspace.navigateTo();
@@ -81,8 +87,8 @@ test.describe("Workspace open in editor", () => {
await chooseEditorTarget(page, "vscode");
await expectEditorOpened({
recordPath,
command: "code",
workspacePath: workspace.repoPath,
editorId: "vscode",
path: workspace.repoPath,
afterCount: 0,
});
const recordsAfterSelection = (await readEditorOpenRecords(recordPath)).length;
@@ -94,8 +100,8 @@ test.describe("Workspace open in editor", () => {
await page.getByTestId("workspace-open-in-editor-primary").click();
await expectEditorOpened({
recordPath,
command: "code",
workspacePath: workspace.repoPath,
editorId: "vscode",
path: workspace.repoPath,
afterCount: recordsAfterSelection,
});
const recordsAfterReturnOpen = (await readEditorOpenRecords(recordPath)).length;
@@ -105,8 +111,8 @@ test.describe("Workspace open in editor", () => {
await page.getByTestId("workspace-open-in-editor-primary").click();
await expectEditorOpened({
recordPath,
command: "code",
workspacePath: workspace.repoPath,
editorId: "vscode",
path: workspace.repoPath,
afterCount: recordsAfterReturnOpen,
});
});