Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
127 changes: 127 additions & 0 deletions .agents/skills/ade-web/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
---
name: ade-web
description: >-
Launch the ADE desktop app's renderer as a standalone web app (Vite-only preview)
seeded with real data from the ADE database. Works from any lane worktree without
interfering with running ADE sockets or runtimes. Use when asked to start, run, or
preview the ADE desktop web renderer, open the ADE web app, or view ADE UI in a browser.
metadata:
author: ADE
version: 0.1.0
---

# ade-web — Launch the ADE Desktop Web Renderer

Starts the ADE desktop renderer as a browser-accessible web app on `http://localhost:5173`,
seeded with a snapshot of the real ADE database. Safe to run alongside the ADE beta or
any other running ADE runtime — it does **not** touch sockets or start new runtimes.

## When to use

- User asks to run, start, preview, or open the ADE web app / desktop web renderer
- User wants to visually inspect or iterate on ADE desktop UI changes in a browser
- User asks to launch ADE web from a specific lane or worktree

## Procedure

### 1. Resolve the workspace root

The web renderer must run from the **current lane's worktree**, not the main project checkout.

```
WORKTREE_ROOT="$(pwd)"
```

If `pwd` is not already inside `.ade/worktrees/<lane>/`, resolve it:

```
# If inside a worktree, pwd is already correct.
# If at the project root, there is no lane context — ask the user which lane.
```

Confirm the desktop app exists at `$WORKTREE_ROOT/apps/desktop/package.json`.

### 2. Kill any stale Vite on port 5173

```bash
lsof -ti :5173 2>/dev/null | xargs kill 2>/dev/null
```

Do **not** kill processes on any other port. Do **not** touch ADE runtime sockets
(`/tmp/ade-runtime-dev.sock`, `~/.ade-beta/sock/ade.sock`, etc.).

### 3. Seed the database snapshot

Export real data from the global ADE database into the browser mock:

```bash
cd "$WORKTREE_ROOT/apps/desktop" && node ./scripts/export-browser-mock-ade-snapshot.mjs
```

This reads `.ade/ade.db` from the primary project root (auto-detected even from worktrees)
and writes `src/renderer/browser-mock-ade-snapshot.generated.json`.

If this fails with "No database", the user hasn't opened the project in ADE desktop yet.
The renderer will still work with built-in demo data.

### 4. Start the Vite dev server

```bash
cd "$WORKTREE_ROOT/apps/desktop" && npm run dev:vite
```

This runs `vite --port 5173 --strictPort`. The `predev:vite` hook re-exports the
snapshot automatically, so step 3 is optional if you go straight here.

Wait for the `VITE ready` message confirming it's listening.

### 5. Open in the ADE browser (optional)

If the user wants it in ADE's built-in browser:

```bash
ade actions run built_in_browser createTab --socket --text --arg url=http://localhost:5173/work
```

Or navigate an existing tab:

```bash
ade actions run built_in_browser navigate --socket --text --arg url=http://localhost:5173/work
```

Use `--socket` to communicate with the running ADE instance. This does **not** start a
new runtime or interfere with the existing socket.

## Important constraints

- **Never start a runtime or bridge.** Do not run `dev:vite:live`, `dev:browser-bridge`,
or `ensureRuntime`. These may detect version mismatches and restart the user's running
ADE beta/dev runtime.
- **Never start or manage the ADE socket directly.** The Vite-only preview uses
`browserMock.ts` to stub `window.ade` — it does not need a runtime connection.
The `--socket` flag on `ade actions run` above is fine; it connects to the
running desktop instance rather than managing the socket itself.
- **Always run from the worktree.** All `cd` commands, file reads, and file edits must
target paths under `$WORKTREE_ROOT`, never the main project checkout. When `grep` or
`find` returns absolute paths rooted at the main checkout, translate them to the
worktree before editing.
- **Port 5173 only.** Do not change the port. The desktop app's Vite config uses
`--strictPort` so it will error if the port is taken rather than silently picking another.

## Cleanup

When done, kill the Vite server:

```bash
lsof -ti :5173 2>/dev/null | xargs kill 2>/dev/null
```

## Troubleshooting

| Problem | Fix |
|---------|-----|
| `Port 5173 is already in use` | Kill the stale process: `lsof -ti :5173 \| xargs kill` |
| `No database at ...` | Run `export-browser-mock-ade-snapshot.mjs` with `ADE_PROJECT_ROOT=/path/to/ADE` pointing at the main checkout |
| `ERR_CONNECTION_REFUSED` in ADE browser | Vite died — restart with `npm run dev:vite` from the worktree |
| Mock data instead of real data | Re-run the export script, then restart Vite or hard-refresh the browser |
| `proxy error: /health ECONNREFUSED 127.0.0.1:18765` | Expected — this is the browser bridge port. Vite-only mode doesn't use it. Ignore. |
16 changes: 16 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,22 @@ Desktop release:
- Validation commands are documented in the "Validation" section above.
- The desktop test suite is large; CI shards it. For local iteration, run a single file or one CI-style shard rather than the full suite.

### Working in ADE lanes (worktrees)

- When an agent session runs inside an ADE lane, its working directory is the lane's worktree (e.g. `/path/to/ADE/.ade/worktrees/<lane-slug>/`). **All file reads, edits, and writes MUST target paths under that worktree, never under the main project-root checkout.**
- `grep`, `find`, and Explore agents may return absolute paths rooted at the main checkout. Before editing, translate those paths to the worktree: replace the project root prefix with the worktree root. For example, `/Users/admin/Projects/ADE/apps/desktop/src/foo.ts` becomes `<worktree>/apps/desktop/src/foo.ts`.
- Use relative paths from your working directory whenever possible — they resolve to the worktree automatically.
- If `ADE_REPO_ROOT` is set in the environment, use it as the canonical base for all file operations.
- When launching dev servers (Vite, Electron, etc.) for a lane, run them from the worktree, not the main checkout: `cd <worktree>/apps/desktop && npm run dev:vite`.

### Running the ADE desktop web renderer (Vite-only preview)

- The desktop renderer can run standalone in a browser without Electron via `npm run dev:vite` in `apps/desktop`. This starts Vite on port 5173 with a browser mock for `window.ade`.
- To seed the mock with real data from the ADE database, run `npm run export:browser-mock-ade` in `apps/desktop` first, or let the `predev:vite` hook do it automatically. The export script reads `.ade/ade.db` from the primary project root and writes a snapshot to `src/renderer/browser-mock-ade-snapshot.generated.json`.
- This works from any lane worktree: `cd <worktree>/apps/desktop && npm run dev:vite`. The export script detects worktree paths and resolves the `.ade/ade.db` location from the parent project root.
- For live data (connected to the ADE runtime socket instead of mock data), use `npm run dev:vite:live`. This starts both Vite and a browser-runtime bridge. Note: this calls `ensureRuntime` which may restart a stale dev runtime — avoid if the ADE beta or another runtime is already running on the target socket.
- Open `http://localhost:5173/work` in a browser or ADE's built-in browser to view the Work tab.

### Inspecting the local Electron desktop app with Codex Computer Use on macOS

- To inspect ADE desktop parity locally with Codex Computer Use, launch the dev app from the worktree with `npm run dev` in `apps/desktop`.
Expand Down
2 changes: 2 additions & 0 deletions apps/ade-cli/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10515,6 +10515,8 @@ async function spawnMachineRuntimeDaemon(
}
if (runtimeBuildHash) {
env.ADE_RUNTIME_BUILD_HASH = runtimeBuildHash;
} else {
delete env.ADE_RUNTIME_BUILD_HASH;
}

const child = spawn(serviceCommand.command, args, {
Expand Down
1 change: 1 addition & 0 deletions apps/ade-cli/src/stdioRpcDaemon.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -416,6 +416,7 @@ describe("ade rpc --stdio daemon bridge", () => {
ADE_HOME: adeHome,
NODE_OPTIONS: withTsxNodeOptions(process.env.NODE_OPTIONS),
ADE_CLI_VERSION: "2.0.0",
ADE_RUNTIME_BUILD_HASH: "",
};
const tcpDaemon = startServeProcess({
cliPath,
Expand Down
5 changes: 3 additions & 2 deletions apps/ade-cli/src/tuiClient/connection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -454,12 +454,13 @@ function spawnDaemon(socketPath: string): boolean {
const daemonArgs = cliEntrypoint
? [cliEntrypoint, "serve", "--socket", socketPath]
: ["serve", "--socket", socketPath];
const env = {
const env: NodeJS.ProcessEnv = {
...process.env,
ADE_DEFAULT_ROLE: "cto",
ADE_RPC_SOCKET_PATH: socketPath,
...(buildHash ? { ADE_RUNTIME_BUILD_HASH: buildHash } : {}),
};
if (buildHash) env.ADE_RUNTIME_BUILD_HASH = buildHash;
else delete env.ADE_RUNTIME_BUILD_HASH;
const child = spawn(
process.execPath,
daemonArgs,
Expand Down
14 changes: 14 additions & 0 deletions apps/desktop/scripts/export-browser-mock-ade-snapshot.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,18 @@ const args = process.argv.slice(2);
const optional = args.includes("--optional");
const positionalRoot = args.find((arg) => !arg.startsWith("-"));

function resolveWorktreeParentRoot(dir) {
const sep = path.sep;
const parts = dir.split(sep);
for (let i = parts.length - 1; i >= 0; i -= 1) {
if (parts[i] === "worktrees" && i > 0 && parts[i - 1] === ".ade") {
const root = parts.slice(0, i - 1).join(sep) || sep;
return path.resolve(root);
}
}
return null;
}

function resolveProjectRoot() {
if (process.env.ADE_PROJECT_ROOT) {
return path.resolve(process.env.ADE_PROJECT_ROOT);
Expand All @@ -43,6 +55,8 @@ function resolveProjectRoot() {
path.resolve(cwd, "../../.."),
REPO_ROOT_FROM_SCRIPT,
];
const worktreeParent = resolveWorktreeParentRoot(cwd) ?? resolveWorktreeParentRoot(REPO_ROOT_FROM_SCRIPT);
if (worktreeParent) candidates.push(worktreeParent);
for (const candidate of candidates) {
if (existsSync(path.join(candidate, ".ade", "ade.db"))) {
return candidate;
Expand Down
12 changes: 11 additions & 1 deletion apps/desktop/src/main/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1119,7 +1119,17 @@ app.whenReady().then(async () => {

const builtInBrowserService = createBuiltInBrowserService({
getLogger: () => getActiveContext().logger,
onEvent: (payload) => broadcast(IPC.builtInBrowserEvent, payload),
onEvent: (payload, targetWindow) => {
if (targetWindow && !targetWindow.isDestroyed()) {
try {
targetWindow.webContents.send(IPC.builtInBrowserEvent, payload);
} catch {
// ignore stale window sends
}
return;
}
broadcast(IPC.builtInBrowserEvent, payload);
},
});

// Side-channel JSON-RPC server that lets the runtime daemon proxy
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -246,9 +246,12 @@ function captureStatusEvents(): {
};
}

let fakeWindowId = 1;

function fakeBrowserWindow() {
const children: unknown[] = [];
return {
id: fakeWindowId++,
isDestroyed: () => false,
contentView: {
children,
Expand All @@ -270,6 +273,7 @@ describe("createBuiltInBrowserService — bounds and status dedupe", () => {

beforeEach(() => {
collector = captureStatusEvents();
fakeWindowId = 1;
fakes.clearWebContentsInstances();
fakes.clearBeforeSendHeadersHandlers();
fakes.clearPermissionHandlers();
Expand Down Expand Up @@ -374,6 +378,78 @@ describe("createBuiltInBrowserService — bounds and status dedupe", () => {
expect(wc?.audioMutedCalls.at(-1)).toBe(true);
});

it("keeps a visible browser view attached to its owner window when another ADE window focuses", async () => {
const service = createBuiltInBrowserService({ onEvent: collector.onEvent });
const winA = fakeBrowserWindow();
const winB = fakeBrowserWindow();
const browserWinA = winA as unknown as Parameters<typeof service.attachToWindow>[0];
const browserWinB = winB as unknown as Parameters<typeof service.attachToWindow>[0];

service.attachToWindow(browserWinA);
await service.createTab({ url: "https://a.example.test", activate: true }, browserWinA);
await service.setBounds({ x: 12, y: 24, width: 640, height: 360, visible: true }, browserWinA);

expect(winA.contentView.children).toHaveLength(1);
expect(winB.contentView.children).toHaveLength(0);
expect(service.getStatus(browserWinA).visible).toBe(true);

service.attachToWindow(browserWinB);

expect(winA.contentView.children).toHaveLength(1);
expect(winB.contentView.children).toHaveLength(0);
expect(service.getStatus(browserWinA).visible).toBe(true);
expect(service.getStatus(browserWinB).visible).toBe(false);
expect(service.getStatus(browserWinB).tabs).toEqual([]);
});

it("scopes browser tabs and commands to the sender window", async () => {
const service = createBuiltInBrowserService({ onEvent: collector.onEvent });
const winA = fakeBrowserWindow();
const winB = fakeBrowserWindow();
const browserWinA = winA as unknown as Parameters<typeof service.attachToWindow>[0];
const browserWinB = winB as unknown as Parameters<typeof service.attachToWindow>[0];

service.attachToWindow(browserWinA);
await service.createTab({ url: "https://a.example.test", activate: true }, browserWinA);
service.attachToWindow(browserWinB);
await service.createTab({ url: "https://b.example.test", activate: true }, browserWinB);

expect(service.getStatus(browserWinA).tabs).toHaveLength(1);
expect(service.getStatus(browserWinA).url).toBe("https://a.example.test/");
expect(service.getStatus(browserWinB).tabs).toHaveLength(1);
expect(service.getStatus(browserWinB).url).toBe("https://b.example.test/");

await service.navigate({ url: "https://b-2.example.test" }, browserWinB);

expect(service.getStatus(browserWinA).url).toBe("https://a.example.test/");
expect(service.getStatus(browserWinB).url).toBe("https://b-2.example.test/");
});

it("targets browser events to the owning ADE window", async () => {
const targetedEvents: Array<{ payload: BuiltInBrowserEventPayload; targetWindow: unknown }> = [];
const service = createBuiltInBrowserService({
onEvent: (payload, targetWindow) => targetedEvents.push({ payload, targetWindow }),
});
const winA = fakeBrowserWindow();
const winB = fakeBrowserWindow();
const browserWinA = winA as unknown as Parameters<typeof service.attachToWindow>[0];
const browserWinB = winB as unknown as Parameters<typeof service.attachToWindow>[0];

service.attachToWindow(browserWinA);
targetedEvents.length = 0;
await service.createTab({ url: "https://a.example.test", activate: true }, browserWinA);

expect(targetedEvents.length).toBeGreaterThan(0);
expect(targetedEvents.every((event) => event.targetWindow === browserWinA)).toBe(true);

service.attachToWindow(browserWinB);
targetedEvents.length = 0;
await service.createTab({ url: "https://b.example.test", activate: true }, browserWinB);

expect(targetedEvents.length).toBeGreaterThan(0);
expect(targetedEvents.every((event) => event.targetWindow === browserWinB)).toBe(true);
});

it("keeps Google account sign-in inside ADE browser tabs", async () => {
const service = createBuiltInBrowserService({ onEvent: collector.onEvent });
const googleAuthUrl = "https://accounts.google.com/o/oauth2/v2/auth?client_id=test";
Expand Down
Loading
Loading