Merge branch 'main' into dev

Resolve 38 conflicts preserving both sides' work:
- Server: DB-based registries (dev) + WorkspaceGitService/DaemonConfigStore (main)
- App: script proxy/voice MCP (dev) + pin/hide tabs/bootstrap boundary (main)
- Shared: WorkspaceScriptPayload (dev) + git/GitHub runtime schemas (main)
- Deleted workspace-registry-bootstrap.ts (dev's DB approach supersedes)
This commit is contained in:
Mohamed Boudra
2026-04-14 03:05:45 +07:00
333 changed files with 13490 additions and 6317 deletions
+38 -29
View File
@@ -2,6 +2,7 @@ import type { Page } from "@playwright/test";
import path from "node:path";
import { pathToFileURL } from "node:url";
import { randomUUID } from "node:crypto";
import { createNodeWebSocketFactory, type NodeWebSocketFactory } from "./node-ws-factory";
import { buildHostWorkspaceRoute } from "../../src/utils/host-routes";
export type TerminalPerfDaemonClient = {
@@ -49,29 +50,36 @@ function getServerId(): string {
return serverId;
}
type TerminalPerfDaemonClientConfig = {
url: string;
clientId: string;
clientType: "cli";
webSocketFactory?: NodeWebSocketFactory;
};
async function loadDaemonClientConstructor(): Promise<
new (config: { url: string; clientId: string; clientType: "cli" }) => TerminalPerfDaemonClient
new (
config: TerminalPerfDaemonClientConfig,
) => TerminalPerfDaemonClient
> {
const repoRoot = path.resolve(process.cwd(), "../..");
const repoRoot = path.resolve(__dirname, "../../../../");
const moduleUrl = pathToFileURL(
path.join(repoRoot, "packages/server/dist/server/server/exports.js"),
).href;
const mod = (await import(moduleUrl)) as {
DaemonClient: new (config: {
url: string;
clientId: string;
clientType: "cli";
}) => TerminalPerfDaemonClient;
DaemonClient: new (config: TerminalPerfDaemonClientConfig) => TerminalPerfDaemonClient;
};
return mod.DaemonClient;
}
export async function connectTerminalClient(): Promise<TerminalPerfDaemonClient> {
const DaemonClient = await loadDaemonClientConstructor();
const webSocketFactory = createNodeWebSocketFactory();
const client = new DaemonClient({
url: getDaemonWsUrl(),
clientId: `terminal-perf-${randomUUID()}`,
clientType: "cli",
webSocketFactory,
});
await client.connect();
return client;
@@ -83,6 +91,10 @@ export function buildTerminalWorkspaceUrl(cwd: string, terminalId: string): stri
return `${route}?open=${encodeURIComponent(`terminal:${terminalId}`)}`;
}
function buildWorkspaceUrl(cwd: string): string {
return buildHostWorkspaceRoute(getServerId(), cwd);
}
export async function getTerminalBufferText(page: Page): Promise<string> {
return page.evaluate(() => {
const term = (window as any).__paseoTerminal;
@@ -124,33 +136,29 @@ export async function navigateToTerminal(
// Boot the app at the workspace route directly.
// The fixtures.ts beforeEach addInitScript seeds localStorage on every navigation,
// so the daemon registry is already configured when the app starts.
const workspaceRoute = buildHostWorkspaceRoute(getServerId(), input.cwd);
const workspaceRoute = buildTerminalWorkspaceUrl(input.cwd, input.terminalId);
await page.goto(workspaceRoute);
// The workspace layout consumes `?open=...`, returns null during the effect,
// then replaces the URL with the clean workspace route after preparing the tab.
const cleanWorkspaceRoute = buildWorkspaceUrl(input.cwd);
await page.waitForURL(
(url) => url.pathname === cleanWorkspaceRoute && !url.searchParams.has("open"),
{ timeout: 15_000 },
);
// Wait for daemon connection (sidebar shows host label)
await page.getByText("localhost", { exact: true }).first().waitFor({ state: "visible", timeout: 15_000 });
await page
.getByText("localhost", { exact: true })
.first()
.waitFor({ state: "visible", timeout: 15_000 });
// The open intent should have prepared and focused the exact pre-created terminal tab.
const terminalTab = page.locator(`[data-testid="workspace-tab-terminal_${input.terminalId}"]`);
await terminalTab.waitFor({ state: "visible", timeout: 15_000 });
await terminalTab.click();
// The workspace should now query listTerminals and discover our terminal.
// Click the terminal tab if it auto-appeared, or wait for it.
const terminalSurface = page.locator('[data-testid="terminal-surface"]');
const surfaceVisible = await terminalSurface.isVisible().catch(() => false);
if (!surfaceVisible) {
// Terminal tab might not be focused — look for it in the tab row and click it
const terminalTab = page.locator(`[data-testid="workspace-tab-terminal:${input.terminalId}"]`);
const tabExists = await terminalTab.isVisible({ timeout: 5_000 }).catch(() => false);
if (tabExists) {
await terminalTab.click();
} else {
// Terminal tab not yet created — click "New terminal tab" to create one through the UI
const newTerminalBtn = page.getByRole("button", { name: "New terminal tab" });
await newTerminalBtn.waitFor({ state: "visible", timeout: 10_000 });
await newTerminalBtn.click();
}
}
// Wait for terminal surface to be visible
await terminalSurface.waitFor({ state: "visible", timeout: 15_000 });
// Wait for loading overlay to disappear (terminal attached)
@@ -161,6 +169,7 @@ export async function navigateToTerminal(
// overlay may never appear if attachment is instant
});
await terminalSurface.scrollIntoViewIfNeeded();
await terminalSurface.click();
}