Skip to content

Commit a17e0d0

Browse files
authored
Tui Perf And Fucntilnaity Pass (#325)
* ship: prepare lane for review * fix: address pr loop feedback
1 parent ae3255a commit a17e0d0

70 files changed

Lines changed: 4745 additions & 447 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.

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ ade actions list --text # discover every service action
128128

129129
## Architecture
130130

131-
Local-first, on purpose. The center of ADE is the **runtime daemon** — a single per-machine `ade` service that owns projects, lanes, chats, processes, sync, and proof artifacts. Desktop, the terminal client, the iOS app, and SSH-attached desktop windows all attach to it as clients. Runtime state lives under `.ade/` inside each project (SQLite db, worktree checkouts, proof artifacts, encrypted secrets) and the machine-wide socket lives under `~/.ade/sock/ade.sock`.
131+
Local-first, on purpose. The center of ADE is the **runtime daemon** — a single per-machine `ade` service that owns projects, lanes, chats, processes, sync, and proof artifacts. Desktop, the terminal client, the iOS app, and SSH-attached desktop windows all attach to it as clients. Runtime state lives under `.ade/` inside each project (SQLite db, worktree checkouts, proof artifacts, encrypted secrets) and the machine-wide socket lives under `~/.ade/sock/ade.sock`. When desktop is running, its Electron main process also hosts a **bridge socket** at `~/.ade/sock/desktop-bridge.sock` (override: `ADE_DESKTOP_BRIDGE_SOCKET_PATH`) so the headless daemon can proxy `ade browser …` calls into the Electron-only `WebContentsView` APIs it can't reach under `ELECTRON_RUN_AS_NODE=1`.
132132

133133
```text
134134
apps/ade-cli ADE runtime daemon (`ade serve`) + `ade` CLI + `ade code` terminal client
@@ -189,6 +189,7 @@ Override it when needed:
189189
npm run dev:desktop -- --socket /tmp/my-ade-dev.sock
190190
npm run dev:code -- --socket /tmp/my-ade-dev.sock
191191
ADE_DEV_RUNTIME_SOCKET_PATH=/tmp/my-ade-dev.sock npm run dev:runtime
192+
ADE_DESKTOP_BRIDGE_SOCKET_PATH=/tmp/my-bridge.sock npm run dev:desktop
192193
```
193194

194195
To test auto-runtime creation, use the `:auto`/default commands after stopping the dev runtime:

apps/ade-cli/pnpm-workspace.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
allowBuilds:
22
esbuild: set this to true or false
33
node-pty: set this to true or false
4+
opencode-ai: set this to true or false
45
sqlite3: set this to true or false

apps/ade-cli/src/bootstrap.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,11 @@ import {
8989
} from "../../desktop/src/main/services/appControl/appControlService";
9090
import { createMacosVmService } from "../../desktop/src/main/services/macosVm/macosVmService";
9191
import type { BuiltInBrowserService } from "../../desktop/src/main/services/builtInBrowser/builtInBrowserService";
92+
import {
93+
createBuiltInBrowserDesktopBridgeClient,
94+
type BuiltInBrowserDesktopBridgeClient,
95+
} from "./services/builtInBrowser/desktopBridgeClient";
96+
import { resolveMachineAdeLayout } from "./services/projects/machineLayout";
9297
import type { createFileService } from "../../desktop/src/main/services/files/fileService";
9398
import type { AppNavigationRequest, AppNavigationResult, PortLease } from "../../desktop/src/shared/types";
9499
import {
@@ -841,6 +846,21 @@ export async function createAdeRuntime(args: {
841846
}),
842847
});
843848

849+
// `built_in_browser` is hosted by the desktop's Electron main process (the
850+
// browser pane owns a WebContentsView). The runtime daemon proxies calls
851+
// through `<adeHome>/sock/desktop-bridge.sock`; if no desktop is running,
852+
// individual calls fail clearly. Override the socket path with
853+
// `ADE_DESKTOP_BRIDGE_SOCKET_PATH` for dev launches that use a non-default
854+
// ADE home.
855+
const builtInBrowserBridge: BuiltInBrowserDesktopBridgeClient | null = chatOnlyRuntime
856+
? null
857+
: createBuiltInBrowserDesktopBridgeClient({
858+
socketPath:
859+
process.env.ADE_DESKTOP_BRIDGE_SOCKET_PATH?.trim()
860+
|| resolveMachineAdeLayout().desktopBridgeSocketPath,
861+
logger,
862+
});
863+
844864
const aiOrchestratorService = createAiOrchestratorService({
845865
db,
846866
logger,
@@ -1187,6 +1207,7 @@ export async function createAdeRuntime(args: {
11871207
computerUseArtifactBrokerService,
11881208
iosSimulatorService,
11891209
appControlService,
1210+
builtInBrowserService: builtInBrowserBridge as unknown as BuiltInBrowserService | null,
11901211
macosVmService,
11911212
orchestratorService,
11921213
aiOrchestratorService,
@@ -1205,6 +1226,7 @@ export async function createAdeRuntime(args: {
12051226
swallow(() => portAllocationService.dispose());
12061227
swallow(() => iosSimulatorService?.dispose());
12071228
swallow(() => appControlService?.dispose());
1229+
swallow(() => builtInBrowserBridge?.dispose());
12081230
swallow(() => macosVmService?.dispose());
12091231
swallow(() => linearOAuthService.dispose());
12101232
swallow(() => headlessLinearServices.dispose());

apps/ade-cli/src/cli.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3521,7 +3521,6 @@ function buildPrPlan(args: string[]): CliPlan {
35213521
status: "getStatus",
35223522
files: "getFiles",
35233523
"action-runs": "getActionRuns",
3524-
activity: "getActivity",
35253524
reviews: "getReviews",
35263525
threads: "getReviewThreads",
35273526
deployments: "getDeployments",

apps/ade-cli/src/multiProjectRpcServer.test.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ function createRegistry() {
1919
secretsDir: path.join(root, "home", "secrets"),
2020
sockDir: path.join(root, "home", "sock"),
2121
socketPath: path.join(root, "home", "sock", "ade.sock"),
22+
desktopBridgeSocketPath: path.join(root, "home", "sock", "desktop-bridge.sock"),
2223
binDir: path.join(root, "home", "bin"),
2324
runtimeDir: path.join(root, "home", "runtime"),
2425
});
@@ -34,6 +35,9 @@ function makeRuntime(label: string) {
3435
laneService: {
3536
list: vi.fn(async () => [{ id: `${label}-lane`, name: label }]),
3637
},
38+
sessionService: {
39+
get: vi.fn(() => null),
40+
},
3741
syncService: {
3842
getStatus: vi.fn(async () => ({ role: "brain", label })),
3943
},
Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
import fs from "node:fs";
2+
import net from "node:net";
3+
import os from "node:os";
4+
import path from "node:path";
5+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
6+
7+
import {
8+
startJsonRpcServer,
9+
type JsonRpcRequest,
10+
type JsonRpcTransport,
11+
} from "../../jsonrpc";
12+
import { createBuiltInBrowserDesktopBridgeClient } from "./desktopBridgeClient";
13+
14+
function silentLogger() {
15+
return {
16+
debug: () => {},
17+
info: () => {},
18+
warn: () => {},
19+
error: () => {},
20+
};
21+
}
22+
23+
type ServerHandle = {
24+
socketPath: string;
25+
close: () => Promise<void>;
26+
};
27+
28+
async function startBridgeServer(
29+
handler: (request: JsonRpcRequest) => Promise<unknown>,
30+
): Promise<ServerHandle> {
31+
const socketPath = path.join(
32+
fs.mkdtempSync(path.join(os.tmpdir(), "ade-bridge-test-")),
33+
"bridge.sock",
34+
);
35+
const stopHandles = new Set<() => void>();
36+
const sockets = new Set<net.Socket>();
37+
const server = net.createServer((conn) => {
38+
sockets.add(conn);
39+
const transport: JsonRpcTransport = {
40+
onData: (callback) => conn.on("data", callback),
41+
write: (data) => conn.write(data),
42+
close: () => {
43+
if (!conn.destroyed) conn.destroy();
44+
},
45+
};
46+
const stop = startJsonRpcServer(handler, transport, { nonFatal: true });
47+
stopHandles.add(stop);
48+
conn.on("close", () => {
49+
sockets.delete(conn);
50+
stopHandles.delete(stop);
51+
stop();
52+
});
53+
conn.on("error", () => {});
54+
});
55+
await new Promise<void>((resolve, reject) => {
56+
server.once("error", reject);
57+
server.listen(socketPath, () => resolve());
58+
});
59+
return {
60+
socketPath,
61+
close: () =>
62+
new Promise<void>((resolve) => {
63+
for (const s of sockets) {
64+
try {
65+
s.destroy();
66+
} catch {
67+
// ignore
68+
}
69+
}
70+
for (const stop of stopHandles) {
71+
try {
72+
stop();
73+
} catch {
74+
// ignore
75+
}
76+
}
77+
server.close(() => {
78+
try {
79+
fs.unlinkSync(socketPath);
80+
} catch {
81+
// ignore
82+
}
83+
resolve();
84+
});
85+
}),
86+
};
87+
}
88+
89+
describe("createBuiltInBrowserDesktopBridgeClient", () => {
90+
let server: ServerHandle | null = null;
91+
92+
afterEach(async () => {
93+
if (server) {
94+
await server.close();
95+
server = null;
96+
}
97+
});
98+
99+
it("forwards method + params and resolves the JSON-RPC response", async () => {
100+
const seen: JsonRpcRequest[] = [];
101+
server = await startBridgeServer(async (request) => {
102+
seen.push(request);
103+
if (request.method === "built_in_browser.navigate") {
104+
return { ok: true, url: (request.params as { url: string }).url };
105+
}
106+
throw new Error(`unexpected method: ${request.method}`);
107+
});
108+
const client = createBuiltInBrowserDesktopBridgeClient({
109+
socketPath: server.socketPath,
110+
logger: silentLogger(),
111+
});
112+
const result = await client.navigate({ url: "https://example.com" });
113+
expect(result).toEqual({ ok: true, url: "https://example.com" });
114+
expect(seen).toHaveLength(1);
115+
expect(seen[0]?.method).toBe("built_in_browser.navigate");
116+
expect(seen[0]?.params).toEqual({ url: "https://example.com" });
117+
client.dispose();
118+
});
119+
120+
it("dispatches no-arg methods without params field", async () => {
121+
const recorded: JsonRpcRequest[] = [];
122+
server = await startBridgeServer(async (request) => {
123+
recorded.push(request);
124+
return { tabs: [] };
125+
});
126+
const client = createBuiltInBrowserDesktopBridgeClient({
127+
socketPath: server.socketPath,
128+
logger: silentLogger(),
129+
});
130+
await client.getStatus();
131+
expect(recorded[0]?.method).toBe("built_in_browser.getStatus");
132+
expect(recorded[0]?.params).toBeUndefined();
133+
client.dispose();
134+
});
135+
136+
it("surfaces a clear error when the bridge socket does not exist", async () => {
137+
const missingPath = path.join(
138+
fs.mkdtempSync(path.join(os.tmpdir(), "ade-bridge-test-missing-")),
139+
"absent.sock",
140+
);
141+
const client = createBuiltInBrowserDesktopBridgeClient({
142+
socketPath: missingPath,
143+
logger: silentLogger(),
144+
});
145+
await expect(client.getStatus()).rejects.toThrow(
146+
/Desktop browser bridge not running/,
147+
);
148+
client.dispose();
149+
});
150+
151+
it("propagates JSON-RPC server errors", async () => {
152+
server = await startBridgeServer(async () => {
153+
throw new Error("Browser pane is offline");
154+
});
155+
const client = createBuiltInBrowserDesktopBridgeClient({
156+
socketPath: server.socketPath,
157+
logger: silentLogger(),
158+
});
159+
await expect(client.getStatus()).rejects.toThrow(/Browser pane is offline/);
160+
client.dispose();
161+
});
162+
163+
it("reconnects after a transient failure on the next call", async () => {
164+
let callCount = 0;
165+
server = await startBridgeServer(async () => {
166+
callCount += 1;
167+
if (callCount === 1) throw new Error("temporary");
168+
return { ok: true };
169+
});
170+
const client = createBuiltInBrowserDesktopBridgeClient({
171+
socketPath: server.socketPath,
172+
logger: silentLogger(),
173+
});
174+
await expect(client.getStatus()).rejects.toThrow(/temporary/);
175+
const result = await client.getStatus();
176+
expect(result).toEqual({ ok: true });
177+
expect(callCount).toBe(2);
178+
client.dispose();
179+
});
180+
});

0 commit comments

Comments
 (0)