Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
85 commits
Select commit Hold shift + click to select a range
cb91f11
Add On Deck sidebar mockup (working set brainstorm)
badcuban Jul 22, 2026
35337b9
Add On Deck working-set section above the sidebar project tree
badcuban Jul 22, 2026
495cfca
Add browser panel and sidebar layout design explorations
badcuban Jul 23, 2026
6290e92
Show source control change counts in browser panel header
badcuban Jul 23, 2026
341a9f6
Collapse the sidebar to a deck rail instead of off-canvas
badcuban Jul 26, 2026
f0d3e95
Show the working-tree diffstat on a closed source control toggle
badcuban Jul 26, 2026
b7deb72
Stop listing a thread in the project tree while it is on deck
badcuban Jul 26, 2026
526bb3e
Give general chats a destination instead of a project row
badcuban Jul 26, 2026
825a026
Restore truncated thread detail with a hover card
badcuban Jul 26, 2026
abe82c2
Group hover cards and close off the destination band
badcuban Jul 26, 2026
4d382bd
Keep general chats off the deck and settle the band's spacing
badcuban Jul 26, 2026
c58eecb
Present chats as their own mode, not a project in disguise
badcuban Jul 26, 2026
7956f39
Restore the chat terminal and name the destination "General chats"
badcuban Jul 26, 2026
c814fcc
Show projects on the rail and keep the titlebar one colour
badcuban Jul 26, 2026
0cc8f9f
Lift the rail off the window controls properly
badcuban Jul 26, 2026
d298ec5
Give the chats list its own head and drop the rail's false grip
badcuban Jul 26, 2026
a165d16
Describe projects on hover and finish the rail's top edge
badcuban Jul 26, 2026
1aa2005
Close the divider's corner and warm the whole sidebar's hover group
badcuban Jul 26, 2026
78c71a0
Add browser panel state, keyed per thread
badcuban Jul 26, 2026
fe6f11a
Split the centre for an in-app browser
badcuban Jul 26, 2026
17b985b
Run the preview in its own browser session, and enforce it
badcuban Jul 26, 2026
4ee71fd
Attach CDP to preview tabs and collect their diagnostics
badcuban Jul 26, 2026
31d5436
Keep Electron's own warnings out of the page's diagnostics
badcuban Jul 26, 2026
276ed23
Report the address of a request that failed below HTTP
badcuban Jul 26, 2026
fec0742
Let the agent see a page and act on what it saw
badcuban Jul 26, 2026
b9205da
Offer what is listening instead of an empty address bar
badcuban Jul 26, 2026
484ac3e
Give the browser panel tabs
badcuban Jul 26, 2026
672d242
Let the browser change where it sits, and add its overflow menu
badcuban Jul 26, 2026
0e41d70
Match the preview to the app's appearance, and keep it in the sidebar
badcuban Jul 26, 2026
244b0f7
List servers that serve pages, not every open socket
badcuban Jul 26, 2026
4e0d9d9
Move device sizing out of the toolbar and into a device row
badcuban Jul 26, 2026
47d18c6
Make the device frame draggable, and tell the page it resized
badcuban Jul 26, 2026
3cbc92e
Let the device row shed controls before it overflows
badcuban Jul 26, 2026
83d1f3f
Draw the rotate control as a device with arrows around it
badcuban Jul 26, 2026
90d4d03
Space the diffstat toggle, and open the address elsewhere
badcuban Jul 26, 2026
df3300b
Place the device frame at computed coordinates and fit it to the panel
badcuban Jul 26, 2026
0459076
Let the user point at an element and hand it to the agent
badcuban Jul 26, 2026
854a211
Carry a picked element as a composer context, not prompt text
badcuban Jul 26, 2026
525fae6
Make a borderless element's bounds visible while picking
badcuban Jul 26, 2026
2947ae7
Draw the pick highlight ourselves instead of using DevTools'
badcuban Jul 26, 2026
731529e
Name a picked element by what it says, and point at it with one cursor
badcuban Jul 26, 2026
f9d6f7d
Draw the pick cursor smaller, themed, and in the toolbar's arrow
badcuban Jul 26, 2026
883f7ca
Click a picked-element chip to find it again in the page
badcuban Jul 26, 2026
f4a6595
Ask what is wrong with an element while picking it
badcuban Jul 26, 2026
4823256
Try a size or colour on the element and send it as a proposal
badcuban Jul 26, 2026
4d57870
Put the style controls behind a toggle and give them a real list
badcuban Jul 26, 2026
1ec3607
Make the style panel hide, read real colours, and step in theme
badcuban Jul 26, 2026
ebbde96
Shrink the annotation card, which two components were inflating
badcuban Jul 26, 2026
061dadd
Put the page controls beside the address, and close tabs individually
badcuban Jul 27, 2026
e08be2d
Select a region of the page, and send screenshots to the composer
badcuban Jul 27, 2026
31777ec
Draw on the page, and erase a stroke you did not mean
badcuban Jul 27, 2026
7ba3cfa
Put the four page tools behind one control
badcuban Jul 27, 2026
7ea34fe
Carry a browser call from the agent to the tab the user has open
badcuban Jul 27, 2026
9d87848
Give the browser panel a way to answer the agent
badcuban Jul 27, 2026
a46e836
Attach a drawing, and put one word in the tool menu
badcuban Jul 27, 2026
36a1520
Make a drawing one thing in the composer
badcuban Jul 27, 2026
b6538f3
Re-attach the debugger, and set a drawing aside instead of binning it
badcuban Jul 27, 2026
23e89df
Hand the browser to the agent
badcuban Jul 27, 2026
7269b70
Make the browser tools work for the agent, not just exist
badcuban Jul 27, 2026
7d02260
Show the agent's hand on the page
badcuban Jul 27, 2026
8dd69ab
Let the turn changes header wrap
badcuban Jul 27, 2026
2726e83
Vendor Playwright's element engine, where a bad upgrade is loud
badcuban Jul 27, 2026
6ac5a70
Give the agent Playwright's eyes
badcuban Jul 27, 2026
6989a42
Catch an engine upgrade that renames an option
badcuban Jul 27, 2026
bfcd431
Clear the wreckage a bad regex left behind
badcuban Jul 27, 2026
3dcc77c
Merge main into the browser branch
badcuban Jul 27, 2026
ba88600
Let the model see the screenshot
badcuban Jul 27, 2026
5bab28c
Pin the agent to a tab, and say what it is doing
badcuban Jul 27, 2026
b25da88
Fold the sidebar to its rail when both panels are open
badcuban Jul 27, 2026
b38b463
Tell Codex which browser the user means
badcuban Jul 27, 2026
c853e59
Name the browser tools where Codex is still listening
badcuban Jul 27, 2026
dbb0bfc
Answer a notification with 202, not an empty 200
badcuban Jul 27, 2026
ad05807
Ship the vendored engine next to the bundle that reads it
badcuban Jul 27, 2026
23498f5
Let the agent's pointer stay, and travel
badcuban Jul 27, 2026
a7df0c6
Give the agent a mouse to move, and a button to hold
badcuban Jul 27, 2026
50d30d8
Put the highlight on the element, not beside it
badcuban Jul 27, 2026
5d0c533
Let the agent see every tab, and follow the one you are on
badcuban Jul 27, 2026
1859ac9
Let the mark fade when the page goes, rather than blink out
badcuban Jul 27, 2026
aed98e5
Move the mark at a speed worth watching
badcuban Jul 27, 2026
acab4b3
Let the pointer go to a place, not only to a thing
badcuban Jul 28, 2026
c0a9f96
Finish the address you have typed before
badcuban Jul 28, 2026
817c962
Give the agent its own tab, and open onto the whole screen
badcuban Jul 28, 2026
2e1dc5e
Test the deck the sidebar actually has
badcuban Jul 28, 2026
ff1081d
Merge branch 'main' into feature/deck-sidebar
badcuban Jul 28, 2026
49cf521
Merge branch 'main' into feature/deck-sidebar
badcuban Jul 28, 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
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@ jobs:
- name: Lint
run: vp exec vp lint --report-unused-disable-directives

- name: Vendored Playwright injected script is current
run: node scripts/extract-playwright-injected.ts --check

- name: Typecheck
run: vp exec vp run -r --concurrency-limit 2 typecheck

Expand Down
1 change: 1 addition & 0 deletions .oxfmtrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"packages/effect-codex-app-server/src/_generated/schema.gen.ts",
"apps/web/public/mockServiceWorker.js",
"apps/web/src/lib/vendor/qrcodegen.ts",
"apps/desktop/src/preview/vendor/**",
"*.icon/**"
],
"sortPackageJson": {},
Expand Down
10 changes: 6 additions & 4 deletions apps/desktop/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,16 @@
"type": "module",
"main": "dist-electron/main.cjs",
"scripts": {
"build": "vp pack && node scripts/preview-vendor.mjs",
"dev": "node scripts/dev.mjs",
"dev:server-bundle": "vp run --filter @threadlines/server dev:bundle",
"dev:bundle": "vp pack --watch",
"dev:electron": "node scripts/dev-electron.mjs",
"build": "vp pack",
"dev:server-bundle": "vp run --filter @threadlines/server dev:bundle",
"smoke-test": "node scripts/smoke-test.mjs",
"start": "node scripts/start-electron.mjs",
"typecheck": "tsc --noEmit",
"test": "vp test run --passWithNoTests",
"smoke-test": "node scripts/smoke-test.mjs"
"typecheck": "tsc --noEmit",
"vendor:playwright": "node ../../scripts/extract-playwright-injected.ts"
},
"dependencies": {
"@effect/platform-node": "catalog:",
Expand All @@ -30,6 +31,7 @@
"@effect/vitest": "catalog:",
"@types/node": "catalog:",
"electron-builder": "^26.15.3",
"playwright-core": "^1.61.1",
"typescript": "catalog:",
"vite": "catalog:",
"vite-plus": "catalog:",
Expand Down
6 changes: 6 additions & 0 deletions apps/desktop/scripts/dev-electron.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { unwatchFile, watch, watchFile } from "node:fs";
import { join } from "node:path";

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

const devServerUrl = process.env.VITE_DEV_SERVER_URL?.trim();
Expand Down Expand Up @@ -182,6 +183,11 @@ function startApp() {
return;
}

// Every launch, not once at startup: `vp pack --watch` clears its output
// directory, and a restart triggered by that rebuild would otherwise start an
// Electron whose vendored assets had just been deleted.
copyPreviewVendor();

const launchArgs = [
"--threadlines-dev-root=" + devProcessIdentity,
...(desktopUserDataDirectory === null ? [] : ["--user-data-dir=" + desktopUserDataDirectory]),
Expand Down
48 changes: 48 additions & 0 deletions apps/desktop/scripts/preview-vendor.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/**
* Puts the vendored Playwright script next to the bundle that reads it.
*
* `vp pack` bundles code and nothing else, so a static asset under `src/` never
* reaches `dist-electron/`. The bundled main resolves the script relative to
* itself -- correct in a packaged app, where `src/` does not exist -- so without
* this copy the file is only ever where the runtime is not looking.
*
* The symptom is quiet, which is the reason this exists as its own step rather
* than a line in a build script: `browser_snapshot` fails with ENOENT, the model
* shrugs and falls back to screenshots, and the agent still looks like it works.
*/
import { copyFileSync, existsSync, mkdirSync } from "node:fs";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";

import {
VENDOR_DIRECTORY_NAME,
VENDORED_INJECTED_MANIFEST_FILENAME,
VENDORED_INJECTED_SCRIPT_FILENAME,
} from "@threadlines/shared/previewInjectedScript";

const desktopDir = dirname(dirname(fileURLToPath(import.meta.url)));

/** The manifest travels with the script so a shipped build can say which
* playwright-core it came from. */
const assets = [VENDORED_INJECTED_SCRIPT_FILENAME, VENDORED_INJECTED_MANIFEST_FILENAME];

export function copyPreviewVendor() {
const from = join(desktopDir, "src", "preview", VENDOR_DIRECTORY_NAME);
const to = join(desktopDir, "dist-electron", VENDOR_DIRECTORY_NAME);

mkdirSync(to, { recursive: true });
for (const asset of assets) {
const source = join(from, asset);
if (!existsSync(source)) {
throw new Error(
`Missing vendored preview asset ${source}. ` +
`Run 'vp run --filter @threadlines/desktop vendor:playwright' to regenerate it.`,
);
}
copyFileSync(source, join(to, asset));
}
}

if (process.argv[1] === fileURLToPath(import.meta.url)) {
copyPreviewVendor();
}
8 changes: 8 additions & 0 deletions apps/desktop/src/electron/ElectronWindow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,13 @@ export class ElectronWindowCreateError extends Data.TaggedError("ElectronWindowC
}

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

return ElectronWindow.of({
workAreaSize: Effect.sync(() => Electron.screen.getPrimaryDisplay().workAreaSize),
create: (options) =>
Effect.try({
try: () => new Electron.BrowserWindow(options),
Expand Down
51 changes: 51 additions & 0 deletions apps/desktop/src/ipc/DesktopIpcHandlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,32 @@ import {
setTheme,
showContextMenu,
} from "./methods/window.ts";
import {
previewAttach,
previewClick,
previewDrag,
previewDetach,
previewEvaluate,
previewLocalServers,
previewMove,
previewClearBrowsingData,
previewClearCache,
previewOpenDevTools,
previewScreenshot,
previewCancelPick,
previewPickElement,
previewPress,
previewAwaitDrawing,
previewSetAnnotationMode,
previewRevealElement,
previewScroll,
previewSetColorScheme,
previewSetViewport,
previewSnapshot,
previewStatus,
previewType,
previewWaitFor,
} from "./methods/preview.ts";

export const installDesktopIpcHandlers = Effect.gen(function* () {
const ipc = yield* DesktopIpc.DesktopIpc;
Expand All @@ -72,6 +98,31 @@ export const installDesktopIpcHandlers = Effect.gen(function* () {
yield* ipc.handle(issueSshWebSocketToken);
yield* ipc.handle(resolveSshPasswordPrompt);

yield* ipc.handle(previewAttach);
yield* ipc.handle(previewDetach);
yield* ipc.handle(previewStatus);
yield* ipc.handle(previewEvaluate);
yield* ipc.handle(previewSnapshot);
yield* ipc.handle(previewClick);
yield* ipc.handle(previewMove);
yield* ipc.handle(previewDrag);
yield* ipc.handle(previewType);
yield* ipc.handle(previewPress);
yield* ipc.handle(previewScroll);
yield* ipc.handle(previewWaitFor);
yield* ipc.handle(previewLocalServers);
yield* ipc.handle(previewScreenshot);
yield* ipc.handle(previewOpenDevTools);
yield* ipc.handle(previewSetColorScheme);
yield* ipc.handle(previewPickElement);
yield* ipc.handle(previewSetAnnotationMode);
yield* ipc.handle(previewAwaitDrawing);
yield* ipc.handle(previewCancelPick);
yield* ipc.handle(previewRevealElement);
yield* ipc.handle(previewSetViewport);
yield* ipc.handle(previewClearBrowsingData);
yield* ipc.handle(previewClearCache);

yield* ipc.handle(getServerExposureState);
yield* ipc.handle(setServerExposureMode);
yield* ipc.handle(setTailscaleServeEnabled);
Expand Down
24 changes: 24 additions & 0 deletions apps/desktop/src/ipc/channels.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,30 @@
export const PICK_FOLDER_CHANNEL = "desktop:pick-folder";
export const CONFIRM_CHANNEL = "desktop:confirm";
export const CAPTURE_SCREENSHOT_CHANNEL = "desktop:capture-screenshot";
export const PREVIEW_ATTACH_CHANNEL = "desktop:preview-attach";
export const PREVIEW_DETACH_CHANNEL = "desktop:preview-detach";
export const PREVIEW_STATUS_CHANNEL = "desktop:preview-status";
export const PREVIEW_EVALUATE_CHANNEL = "desktop:preview-evaluate";
export const PREVIEW_SNAPSHOT_CHANNEL = "desktop:preview-snapshot";
export const PREVIEW_CLICK_CHANNEL = "desktop:preview-click";
export const PREVIEW_MOVE_CHANNEL = "desktop:preview-move";
export const PREVIEW_DRAG_CHANNEL = "desktop:preview-drag";
export const PREVIEW_TYPE_CHANNEL = "desktop:preview-type";
export const PREVIEW_PRESS_CHANNEL = "desktop:preview-press";
export const PREVIEW_SCROLL_CHANNEL = "desktop:preview-scroll";
export const PREVIEW_WAIT_FOR_CHANNEL = "desktop:preview-wait-for";
export const PREVIEW_LOCAL_SERVERS_CHANNEL = "desktop:preview-local-servers";
export const PREVIEW_SCREENSHOT_CHANNEL = "desktop:preview-screenshot";
export const PREVIEW_OPEN_DEVTOOLS_CHANNEL = "desktop:preview-open-devtools";
export const PREVIEW_SET_COLOR_SCHEME_CHANNEL = "desktop:preview-set-color-scheme";
export const PREVIEW_PICK_ELEMENT_CHANNEL = "desktop:preview-pick-element";
export const PREVIEW_CANCEL_PICK_CHANNEL = "desktop:preview-cancel-pick";
export const PREVIEW_SET_ANNOTATION_MODE_CHANNEL = "desktop:preview-set-annotation-mode";
export const PREVIEW_AWAIT_DRAWING_CHANNEL = "desktop:preview-await-drawing";
export const PREVIEW_REVEAL_ELEMENT_CHANNEL = "desktop:preview-reveal-element";
export const PREVIEW_SET_VIEWPORT_CHANNEL = "desktop:preview-set-viewport";
export const PREVIEW_CLEAR_BROWSING_DATA_CHANNEL = "desktop:preview-clear-browsing-data";
export const PREVIEW_CLEAR_CACHE_CHANNEL = "desktop:preview-clear-cache";
export const SET_THEME_CHANNEL = "desktop:set-theme";
export const SET_TASKBAR_STATUS_CHANNEL = "desktop:set-taskbar-status";
export const CONTEXT_MENU_CHANNEL = "desktop:context-menu";
Expand Down
Loading
Loading