Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
587f5ea
Add shared MCP preview automation
juliusmarminge Jun 12, 2026
a7dc311
Refine collaborative browser preview
juliusmarminge Jun 12, 2026
ac21169
Refactor MCP services into top-level modules
juliusmarminge Jun 12, 2026
16d511f
Refactor desktop preview IPC onto shared manager
juliusmarminge Jun 13, 2026
31920d9
Port preview manager to Effect-based browser sessions
juliusmarminge Jun 13, 2026
76abe50
Scope preview listeners and control sessions
juliusmarminge Jun 13, 2026
0d822db
Add SWR preview session state and resubscribe handling
juliusmarminge Jun 13, 2026
524b5ca
Prevent stale preview snapshots from resurrecting sessions
juliusmarminge Jun 13, 2026
d6ff31e
Route preview automation through live owner streams (#3548)
juliusmarminge Jun 26, 2026
cfea030
[codex] Fix preview automation edge cases (#3561)
juliusmarminge Jun 26, 2026
8b88947
Stabilize preview browser surfaces, automation, and recording (#3565)
juliusmarminge Jun 27, 2026
cfcc0a2
Resolve localhost preview URLs for remote environments (#4011)
juliusmarminge Jul 16, 2026
aea7488
fix(desktop): allow clipboard writes in the preview browser (#3889)
carlosricojr Jul 20, 2026
7653f1d
fix(preview): preserve direct localhost navigation (#3939)
Chrrxs Jul 20, 2026
d143fc1
Add preview color scheme controls and simplify project grouping (#4385)
juliusmarminge Jul 23, 2026
cae61af
feat: port agent-driven browser automation
ronak-guliani Jul 26, 2026
2ee23c5
Fix standalone desktop workspace dependencies (#57)
ronak-guliani Jul 26, 2026
18e5b64
Fix Windows test harness spawning (#63)
ronak-guliani Jul 27, 2026
c50fbbf
Merge main into agent browser automation port
ronak-guliani Jul 27, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
520 changes: 520 additions & 0 deletions .plans/shared-http-mcp-server-with-preview-automation-revised.md

Large diffs are not rendered by default.

670 changes: 670 additions & 0 deletions .plans/visible-preview-browser-automation-via-cdp-mcp.md

Large diffs are not rendered by default.

13 changes: 9 additions & 4 deletions apps/desktop/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,21 @@
"start": "node scripts/start-electron.mjs",
"typecheck": "tsc --noEmit",
"test": "vp test run --passWithNoTests",
"smoke-test": "node scripts/smoke-test.mjs"
"smoke-test": "node scripts/smoke-test.mjs",
"build:preview-annotation-css": "node scripts/build-preview-annotation-css.mjs"
},
"dependencies": {
"@effect/platform-node": "catalog:",
"@t3tools/contracts": "workspace:*",
"@t3tools/shared": "workspace:*",
"effect": "catalog:",
"electron": "40.6.0",
"electron-updater": "^6.6.2"
"electron-updater": "^6.6.2",
"playwright-core": "1.60.0",
"react-grab": "^0.1.32"
},
"devDependencies": {
"@t3tools/contracts": "workspace:*",
"@t3tools/shared": "workspace:*",
"@effect/vitest": "catalog:",
"@types/node": "catalog:",
"effect-acp": "workspace:*",
"electron-builder": "^26.15.3",
Expand Down
40 changes: 40 additions & 0 deletions apps/desktop/scripts/build-preview-annotation-css.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import * as NodeFSP from "node:fs/promises";
import * as NodeModule from "node:module";
import * as NodePath from "node:path";
import * as NodeURL from "node:url";

import { compile } from "tailwindcss";

const directory = NodePath.dirname(NodeURL.fileURLToPath(import.meta.url));
const appRoot = NodePath.join(directory, "..");
const sourcePath = NodePath.join(appRoot, "src", "preview", "Annotation.css");
const preloadPath = NodePath.join(appRoot, "src", "preview", "PickPreload.ts");
const outputPath = NodePath.join(appRoot, "src", "preview", "AnnotationStyles.generated.ts");
const require = NodeModule.createRequire(import.meta.url);
const tailwindRoot = NodePath.dirname(require.resolve("tailwindcss/package.json"));

const [annotationSource, preloadSource, themeSource, preflightSource] = await Promise.all([
NodeFSP.readFile(sourcePath, "utf8"),
NodeFSP.readFile(preloadPath, "utf8"),
NodeFSP.readFile(NodePath.join(tailwindRoot, "theme.css"), "utf8"),
NodeFSP.readFile(NodePath.join(tailwindRoot, "preflight.css"), "utf8"),
]);

const candidates = new Set(
Array.from(preloadSource.matchAll(/!?-?[A-Za-z0-9_:@/.[\]()%,-]+/g), (match) => match[0]),
);
const compilerInput = [
themeSource,
preflightSource,
annotationSource.replace('@import "tailwindcss";', "@tailwind utilities;"),
].join("\n");
const compiler = await compile(compilerInput, { base: appRoot });
const css = compiler.build([...candidates]);
const encodedCss = `'${css
.replaceAll("\\", "\\\\")
.replaceAll("'", "\\'")
.replaceAll("\r", "\\r")
.replaceAll("\n", "\\n")}'`;
const moduleSource = `// Generated by scripts/build-preview-annotation-css.mjs. Do not edit.\nexport const previewAnnotationStyles =\n ${encodedCss};\n`;

await NodeFSP.writeFile(outputPath, moduleSource);
79 changes: 79 additions & 0 deletions apps/desktop/src/ipc/DesktopIpc.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { assert, describe, it } from "@effect/vitest";
import * as Cause from "effect/Cause";
import * as Effect from "effect/Effect";
import { vi } from "vite-plus/test";

import * as DesktopIpc from "./DesktopIpc.ts";

const invokeMethod: DesktopIpc.DesktopIpcMethod<never, never> = {
channel: "desktop.test.invoke",
handler: () => Effect.void,
};

const syncMethod: DesktopIpc.DesktopSyncIpcMethod<never, never> = {
channel: "desktop.test.sync",
handler: () => Effect.void,
};

function makeIpcMain(
overrides: Partial<DesktopIpc.DesktopIpcMain> = {},
): DesktopIpc.DesktopIpcMain {
return {
removeHandler: vi.fn(),
handle: vi.fn(),
removeAllListeners: vi.fn(),
on: vi.fn(),
...overrides,
};
}

describe("DesktopIpc", () => {
it.effect("preserves invoke registration context and cause", () =>
Effect.gen(function* () {
const cause = new Error("invoke registration failed");
const ipcMain = makeIpcMain({
handle: () => {
throw cause;
},
});
const ipc = DesktopIpc.make(ipcMain);

const error = yield* Effect.flip(Effect.scoped(ipc.handle(invokeMethod)));

assert.instanceOf(error, DesktopIpc.DesktopIpcRegistrationError);
assert.isTrue(DesktopIpc.isDesktopIpcError(error));
assert.strictEqual(error.handlerKind, "invoke");
assert.strictEqual(error.channel, invokeMethod.channel);
assert.strictEqual(error.cause, cause);
assert.include(error.message, "invoke");
assert.include(error.message, invokeMethod.channel);
assert.notInclude(error.message, cause.message);
}),
);

it.effect("preserves sync unregistration context and cause in the finalizer defect", () =>
Effect.gen(function* () {
const cause = new Error("sync unregistration failed");
let removeCount = 0;
const ipcMain = makeIpcMain({
removeAllListeners: () => {
removeCount += 1;
if (removeCount === 2) throw cause;
},
});
const ipc = DesktopIpc.make(ipcMain);

const exit = yield* Effect.exit(Effect.scoped(ipc.handleSync(syncMethod)));

assert.isTrue(exit._tag === "Failure");
if (exit._tag === "Success") return;
const error = Cause.squash(exit.cause);
assert.instanceOf(error, DesktopIpc.DesktopIpcUnregistrationError);
assert.isTrue(DesktopIpc.isDesktopIpcError(error));
assert.strictEqual(error.handlerKind, "sync");
assert.strictEqual(error.channel, syncMethod.channel);
assert.strictEqual(error.cause, cause);
assert.notInclude(error.message, cause.message);
}),
);
});
Loading
Loading