Skip to content

Commit 7fa4712

Browse files
authored
Merge pull request #88 from Threadlines/feature/deck-sidebar
In-app browser panel with agent control
2 parents 3fd2d61 + 49cf521 commit 7fa4712

130 files changed

Lines changed: 26345 additions & 401 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/ci.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,9 @@ jobs:
4949
- name: Lint
5050
run: vp exec vp lint --report-unused-disable-directives
5151

52+
- name: Vendored Playwright injected script is current
53+
run: node scripts/extract-playwright-injected.ts --check
54+
5255
- name: Typecheck
5356
run: vp exec vp run -r --concurrency-limit 2 typecheck
5457

.oxfmtrc.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
"packages/effect-codex-app-server/src/_generated/schema.gen.ts",
1313
"apps/web/public/mockServiceWorker.js",
1414
"apps/web/src/lib/vendor/qrcodegen.ts",
15+
"apps/desktop/src/preview/vendor/**",
1516
"*.icon/**"
1617
],
1718
"sortPackageJson": {},

apps/desktop/package.json

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,16 @@
66
"type": "module",
77
"main": "dist-electron/main.cjs",
88
"scripts": {
9+
"build": "vp pack && node scripts/preview-vendor.mjs",
910
"dev": "node scripts/dev.mjs",
10-
"dev:server-bundle": "vp run --filter @threadlines/server dev:bundle",
1111
"dev:bundle": "vp pack --watch",
1212
"dev:electron": "node scripts/dev-electron.mjs",
13-
"build": "vp pack",
13+
"dev:server-bundle": "vp run --filter @threadlines/server dev:bundle",
14+
"smoke-test": "node scripts/smoke-test.mjs",
1415
"start": "node scripts/start-electron.mjs",
15-
"typecheck": "tsc --noEmit",
1616
"test": "vp test run --passWithNoTests",
17-
"smoke-test": "node scripts/smoke-test.mjs"
17+
"typecheck": "tsc --noEmit",
18+
"vendor:playwright": "node ../../scripts/extract-playwright-injected.ts"
1819
},
1920
"dependencies": {
2021
"@effect/platform-node": "catalog:",
@@ -30,6 +31,7 @@
3031
"@effect/vitest": "catalog:",
3132
"@types/node": "catalog:",
3233
"electron-builder": "^26.15.3",
34+
"playwright-core": "^1.61.1",
3335
"typescript": "catalog:",
3436
"vite": "catalog:",
3537
"vite-plus": "catalog:",

apps/desktop/scripts/dev-electron.mjs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { unwatchFile, watch, watchFile } from "node:fs";
33
import { join } from "node:path";
44

55
import { desktopDir, resolveElectronPath } from "./electron-launcher.mjs";
6+
import { copyPreviewVendor } from "./preview-vendor.mjs";
67
import { waitForResources } from "./wait-for-resources.mjs";
78

89
const devServerUrl = process.env.VITE_DEV_SERVER_URL?.trim();
@@ -182,6 +183,11 @@ function startApp() {
182183
return;
183184
}
184185

186+
// Every launch, not once at startup: `vp pack --watch` clears its output
187+
// directory, and a restart triggered by that rebuild would otherwise start an
188+
// Electron whose vendored assets had just been deleted.
189+
copyPreviewVendor();
190+
185191
const launchArgs = [
186192
"--threadlines-dev-root=" + devProcessIdentity,
187193
...(desktopUserDataDirectory === null ? [] : ["--user-data-dir=" + desktopUserDataDirectory]),
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
/**
2+
* Puts the vendored Playwright script next to the bundle that reads it.
3+
*
4+
* `vp pack` bundles code and nothing else, so a static asset under `src/` never
5+
* reaches `dist-electron/`. The bundled main resolves the script relative to
6+
* itself -- correct in a packaged app, where `src/` does not exist -- so without
7+
* this copy the file is only ever where the runtime is not looking.
8+
*
9+
* The symptom is quiet, which is the reason this exists as its own step rather
10+
* than a line in a build script: `browser_snapshot` fails with ENOENT, the model
11+
* shrugs and falls back to screenshots, and the agent still looks like it works.
12+
*/
13+
import { copyFileSync, existsSync, mkdirSync } from "node:fs";
14+
import { dirname, join } from "node:path";
15+
import { fileURLToPath } from "node:url";
16+
17+
import {
18+
VENDOR_DIRECTORY_NAME,
19+
VENDORED_INJECTED_MANIFEST_FILENAME,
20+
VENDORED_INJECTED_SCRIPT_FILENAME,
21+
} from "@threadlines/shared/previewInjectedScript";
22+
23+
const desktopDir = dirname(dirname(fileURLToPath(import.meta.url)));
24+
25+
/** The manifest travels with the script so a shipped build can say which
26+
* playwright-core it came from. */
27+
const assets = [VENDORED_INJECTED_SCRIPT_FILENAME, VENDORED_INJECTED_MANIFEST_FILENAME];
28+
29+
export function copyPreviewVendor() {
30+
const from = join(desktopDir, "src", "preview", VENDOR_DIRECTORY_NAME);
31+
const to = join(desktopDir, "dist-electron", VENDOR_DIRECTORY_NAME);
32+
33+
mkdirSync(to, { recursive: true });
34+
for (const asset of assets) {
35+
const source = join(from, asset);
36+
if (!existsSync(source)) {
37+
throw new Error(
38+
`Missing vendored preview asset ${source}. ` +
39+
`Run 'vp run --filter @threadlines/desktop vendor:playwright' to regenerate it.`,
40+
);
41+
}
42+
copyFileSync(source, join(to, asset));
43+
}
44+
}
45+
46+
if (process.argv[1] === fileURLToPath(import.meta.url)) {
47+
copyPreviewVendor();
48+
}

apps/desktop/src/electron/ElectronWindow.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,13 @@ export class ElectronWindowCreateError extends Data.TaggedError("ElectronWindowC
1616
}
1717

1818
export interface ElectronWindowShape {
19+
/**
20+
* The usable area of the primary display, menu bar and dock excluded.
21+
*
22+
* Here rather than read from `Electron.screen` at the call site so window
23+
* sizing stays testable without a display attached.
24+
*/
25+
readonly workAreaSize: Effect.Effect<{ width: number; height: number }>;
1926
readonly create: (
2027
options: Electron.BrowserWindowConstructorOptions,
2128
) => Effect.Effect<Electron.BrowserWindow, ElectronWindowCreateError>;
@@ -65,6 +72,7 @@ const make = Effect.gen(function* () {
6572
);
6673

6774
return ElectronWindow.of({
75+
workAreaSize: Effect.sync(() => Electron.screen.getPrimaryDisplay().workAreaSize),
6876
create: (options) =>
6977
Effect.try({
7078
try: () => new Electron.BrowserWindow(options),

apps/desktop/src/ipc/DesktopIpcHandlers.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,32 @@ import {
4848
setTheme,
4949
showContextMenu,
5050
} from "./methods/window.ts";
51+
import {
52+
previewAttach,
53+
previewClick,
54+
previewDrag,
55+
previewDetach,
56+
previewEvaluate,
57+
previewLocalServers,
58+
previewMove,
59+
previewClearBrowsingData,
60+
previewClearCache,
61+
previewOpenDevTools,
62+
previewScreenshot,
63+
previewCancelPick,
64+
previewPickElement,
65+
previewPress,
66+
previewAwaitDrawing,
67+
previewSetAnnotationMode,
68+
previewRevealElement,
69+
previewScroll,
70+
previewSetColorScheme,
71+
previewSetViewport,
72+
previewSnapshot,
73+
previewStatus,
74+
previewType,
75+
previewWaitFor,
76+
} from "./methods/preview.ts";
5177

5278
export const installDesktopIpcHandlers = Effect.gen(function* () {
5379
const ipc = yield* DesktopIpc.DesktopIpc;
@@ -72,6 +98,31 @@ export const installDesktopIpcHandlers = Effect.gen(function* () {
7298
yield* ipc.handle(issueSshWebSocketToken);
7399
yield* ipc.handle(resolveSshPasswordPrompt);
74100

101+
yield* ipc.handle(previewAttach);
102+
yield* ipc.handle(previewDetach);
103+
yield* ipc.handle(previewStatus);
104+
yield* ipc.handle(previewEvaluate);
105+
yield* ipc.handle(previewSnapshot);
106+
yield* ipc.handle(previewClick);
107+
yield* ipc.handle(previewMove);
108+
yield* ipc.handle(previewDrag);
109+
yield* ipc.handle(previewType);
110+
yield* ipc.handle(previewPress);
111+
yield* ipc.handle(previewScroll);
112+
yield* ipc.handle(previewWaitFor);
113+
yield* ipc.handle(previewLocalServers);
114+
yield* ipc.handle(previewScreenshot);
115+
yield* ipc.handle(previewOpenDevTools);
116+
yield* ipc.handle(previewSetColorScheme);
117+
yield* ipc.handle(previewPickElement);
118+
yield* ipc.handle(previewSetAnnotationMode);
119+
yield* ipc.handle(previewAwaitDrawing);
120+
yield* ipc.handle(previewCancelPick);
121+
yield* ipc.handle(previewRevealElement);
122+
yield* ipc.handle(previewSetViewport);
123+
yield* ipc.handle(previewClearBrowsingData);
124+
yield* ipc.handle(previewClearCache);
125+
75126
yield* ipc.handle(getServerExposureState);
76127
yield* ipc.handle(setServerExposureMode);
77128
yield* ipc.handle(setTailscaleServeEnabled);

apps/desktop/src/ipc/channels.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,30 @@
11
export const PICK_FOLDER_CHANNEL = "desktop:pick-folder";
22
export const CONFIRM_CHANNEL = "desktop:confirm";
33
export const CAPTURE_SCREENSHOT_CHANNEL = "desktop:capture-screenshot";
4+
export const PREVIEW_ATTACH_CHANNEL = "desktop:preview-attach";
5+
export const PREVIEW_DETACH_CHANNEL = "desktop:preview-detach";
6+
export const PREVIEW_STATUS_CHANNEL = "desktop:preview-status";
7+
export const PREVIEW_EVALUATE_CHANNEL = "desktop:preview-evaluate";
8+
export const PREVIEW_SNAPSHOT_CHANNEL = "desktop:preview-snapshot";
9+
export const PREVIEW_CLICK_CHANNEL = "desktop:preview-click";
10+
export const PREVIEW_MOVE_CHANNEL = "desktop:preview-move";
11+
export const PREVIEW_DRAG_CHANNEL = "desktop:preview-drag";
12+
export const PREVIEW_TYPE_CHANNEL = "desktop:preview-type";
13+
export const PREVIEW_PRESS_CHANNEL = "desktop:preview-press";
14+
export const PREVIEW_SCROLL_CHANNEL = "desktop:preview-scroll";
15+
export const PREVIEW_WAIT_FOR_CHANNEL = "desktop:preview-wait-for";
16+
export const PREVIEW_LOCAL_SERVERS_CHANNEL = "desktop:preview-local-servers";
17+
export const PREVIEW_SCREENSHOT_CHANNEL = "desktop:preview-screenshot";
18+
export const PREVIEW_OPEN_DEVTOOLS_CHANNEL = "desktop:preview-open-devtools";
19+
export const PREVIEW_SET_COLOR_SCHEME_CHANNEL = "desktop:preview-set-color-scheme";
20+
export const PREVIEW_PICK_ELEMENT_CHANNEL = "desktop:preview-pick-element";
21+
export const PREVIEW_CANCEL_PICK_CHANNEL = "desktop:preview-cancel-pick";
22+
export const PREVIEW_SET_ANNOTATION_MODE_CHANNEL = "desktop:preview-set-annotation-mode";
23+
export const PREVIEW_AWAIT_DRAWING_CHANNEL = "desktop:preview-await-drawing";
24+
export const PREVIEW_REVEAL_ELEMENT_CHANNEL = "desktop:preview-reveal-element";
25+
export const PREVIEW_SET_VIEWPORT_CHANNEL = "desktop:preview-set-viewport";
26+
export const PREVIEW_CLEAR_BROWSING_DATA_CHANNEL = "desktop:preview-clear-browsing-data";
27+
export const PREVIEW_CLEAR_CACHE_CHANNEL = "desktop:preview-clear-cache";
428
export const SET_THEME_CHANNEL = "desktop:set-theme";
529
export const SET_TASKBAR_STATUS_CHANNEL = "desktop:set-taskbar-status";
630
export const CONTEXT_MENU_CHANNEL = "desktop:context-menu";

0 commit comments

Comments
 (0)