Skip to content
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
10841f2
ade code: per-provider Chat/CLI interface choice
arul28 Jul 3, 2026
bbd0545
ade cli: remote/sync reliability hardening (audit batch)
arul28 Jul 3, 2026
7c2a74a
ade code: perf — single-pass rows, memoized children, debounced backg…
arul28 Jul 3, 2026
1f2ddb1
ade code: fix tui bug and parity batch
arul28 Jul 3, 2026
c608bd0
ade code + sync: review-wave fixups (R1-R5, T1-T2)
arul28 Jul 3, 2026
e3b705d
Persist Work Chat/CLI interface choice on iOS (+ desktop verify)
arul28 Jul 3, 2026
9a9f3f6
ade code: interface persistence, closed-session browsing, /secrets, p…
arul28 Jul 3, 2026
580ee3d
Hub composer: reload per-project Chat/CLI mode on project switch
arul28 Jul 3, 2026
5ee4469
ade code: quality synthesis — digit-picker fixes, app.tsx extractions…
arul28 Jul 3, 2026
1d8983d
ade code: closed-session glyph — user-initiated closes are not failures
arul28 Jul 3, 2026
ca03a82
ade cli: parity with hardening branch
arul28 Jul 3, 2026
e524259
docs: sync internal docs with ade-code hardening branch
arul28 Jul 3, 2026
d0d87d0
test: steward pass — prune, consolidate ade-code suite
arul28 Jul 3, 2026
d03bd97
Merge origin/main (#692 mobile hub overhaul + landing perf) into hard…
arul28 Jul 3, 2026
0d80f5d
ship: iteration 1 — address 10 review comments (greptile P1, codex P1…
arul28 Jul 3, 2026
d442d2d
ship: iteration 2 — address 2 codex P3s (grid claudeChrome, CJK trunc…
arul28 Jul 3, 2026
9a50966
ship: iteration 3 — quote-aware VISUAL/EDITOR parsing (codex P3)
arul28 Jul 3, 2026
46153fe
ship: iteration 4 — provider-aware terminal hydrate + local-provider …
arul28 Jul 3, 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
6 changes: 5 additions & 1 deletion apps/ade-cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,8 @@ sync.getPin sync.setPin sync.clearPin
sync.setActiveLanePresence
```

`runtimeEvents.subscribe` returns `eventEpoch`, `nextCursor`, `hasMore`, `gap`, and `oldestCursor`; when `gap` is true, the caller's cursor predates the retained buffer and it should refresh state before resuming from `oldestCursor` / `nextCursor`.

**Project-scoped** — every other request must carry `params.projectId`. `ade/actions/call` (and the legacy ADE action / tool catalog underneath it) is dispatched into the per-project `ProjectScope` returned by `ProjectScopeRegistry.get(projectId)`.

`ade/initialize` advertises `runtimeInfo.multiProject: true` and `capabilities.projects: true`. Clients use that to switch between sending `projectId` per request (multi-project runtime) and the legacy per-process binding (embedded runtime). Sync is owned by the sync service for the most-recently-opened registered project; `ProjectScopeRegistry.ensureSyncHost` refreshes the active sync project when projects are added or removed.
Expand All @@ -217,7 +219,7 @@ ade code --print-state # smoke-test the connection and exit
ade code remote --target mac --project ADE
# attach to a saved desktop remote machine
ade code remote session --target mac --project ADE --session chat-1
# open a remote chat or Claude terminal session
# open a remote chat or provider CLI terminal session
ade --socket /path/to/ade.sock code # attach to a specific local endpoint
ade --project-root /repo code # bind to a specific project root
```
Expand Down Expand Up @@ -291,6 +293,8 @@ ade prs comments pr-id --text
ade run defs --text
ade run start web --lane lane-id
ade shell start --lane lane-id -- npm test
ade terminal list --lane lane-id --text
ade terminal resume --terminal session-id --text
ade new chat --mode chat --lane lane-id --provider codex --model openai/gpt-5.5 --reasoning-effort xhigh --no-fast --permissions full-auto --prompt "fix failing tests"
ade new chat --mode cli --lane lane-id --provider codex --model openai/gpt-5.5 --reasoning-effort xhigh --no-fast --permissions full-auto --prompt "fix failing tests"
ade new chat --mode chat --lane auto --lane-name fix-checkout-flow --prompt "fix failing tests"
Expand Down
19 changes: 5 additions & 14 deletions apps/ade-cli/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions apps/ade-cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
"opencode-ai": "^1.15.5",
"react": "^19.2.7",
"sql.js": "^1.13.0",
"string-width": "^4.2.3",
"ws": "^8.20.0",
"yaml": "^2.8.2",
"zod": "^4.3.6"
Expand Down
4 changes: 3 additions & 1 deletion apps/ade-cli/src/adeRpcServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4844,7 +4844,9 @@ async function runTool(args: {
events: sliced,
nextCursor: result.nextCursor,
hasMore: filtered.length > limit || result.hasMore,
eventEpoch: result.eventEpoch
eventEpoch: result.eventEpoch,
gap: result.gap === true,
oldestCursor: result.oldestCursor ?? null
};
}
return runtime.eventBuffer.drain(cursor, limit);
Expand Down
51 changes: 51 additions & 0 deletions apps/ade-cli/src/bootstrap.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,60 @@ describe("createEventBuffer", () => {
const result = buffer.drain(0);
// Should have IDs 3, 4, 5 (the oldest 1, 2 were evicted)
expect(result.events.map((e) => e.id)).toEqual([3, 4, 5]);
expect(result.gap).toBe(true);
expect(result.oldestCursor).toBe(3);
expect(result.events[0]!.payload).toEqual({ i: 2 });
});

it("reports no gap when the next retained event is contiguous with the cursor", () => {
const buffer = createEventBuffer(3);

for (let i = 0; i < 5; i++) {
buffer.push({ timestamp: `2026-03-01T00:0${i}:00Z`, category: "runtime", payload: { i } });
}

const result = buffer.drain(2);
expect(result.events.map((event) => event.id)).toEqual([3, 4, 5]);
expect(result.gap).toBe(false);
expect(result.oldestCursor).toBe(3);
});

it("evicts by retained byte budget and reports the replay gap", () => {
const buffer = createEventBuffer(10, { maxBytes: 260, maxEventBytes: 260 });

for (let i = 0; i < 4; i++) {
buffer.push({
timestamp: `2026-03-01T00:0${i}:00Z`,
category: "pty",
payload: { data: "x".repeat(90), i },
});
}

const result = buffer.drain(0);
expect(result.events.length).toBeLessThan(4);
expect(result.events.at(-1)?.id).toBe(4);
expect(result.gap).toBe(true);
expect(result.oldestCursor).toBeGreaterThan(1);
});

it("does not retain events larger than the per-event cap but still notifies live subscribers", () => {
const buffer = createEventBuffer(10, { maxBytes: 1024, maxEventBytes: 64 });
const seen: BufferedEvent[] = [];
buffer.subscribe((event) => seen.push(event));

buffer.push({
timestamp: "2026-03-01T00:00:00Z",
category: "pty",
payload: { data: "x".repeat(200) },
});

expect(seen).toHaveLength(1);
const result = buffer.drain(0);
expect(result.events).toEqual([]);
expect(result.gap).toBe(true);
expect(result.nextCursor).toBe(1);
});

it("drains events after cursor", () => {
const buffer = createEventBuffer();

Expand Down
85 changes: 77 additions & 8 deletions apps/ade-cli/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -478,7 +478,8 @@ const TOP_LEVEL_HELP = `${ADE_BANNER}
$ ade prs list | create | show | checks Manage PRs, queues, and GitHub integration
$ ade run defs | ps | start | logs Manage Run tab process definitions and runtime
$ ade shell start | write | resize | close Launch and control tracked shell sessions
$ ade terminal list | read | write | signal Control an attached session terminal
$ ade terminal list | resume | read | write | signal
Control an attached session terminal
$ ade history list | show | commits | export Inspect ADE operation timeline and lane commits
$ ade chat list | create | send | interrupt Work with ADE agent chats
$ ade cto state | chats Operate CTO state and Work chats
Expand Down Expand Up @@ -1151,12 +1152,12 @@ const HELP_BY_COMMAND: Record<string, string> = {
$ ade code remote --target <machine> --project <project>
Launch against a saved desktop remote machine
$ ade code remote session --target <machine> --project <project> --session <session>
Open a specific remote chat or Claude terminal session
Open a specific remote chat or provider CLI terminal session
$ ade code remote --list-targets List saved remote machines
$ ade code remote --target <machine> --list-projects
List ADE projects available on the remote machine
$ ade code remote session --target <machine> --project <project> --list-sessions
List remote chat and Claude terminal sessions
List remote chat and provider CLI terminal sessions
$ ade --project-root <path> code Launch against a specific ADE project

Keys:
Expand Down Expand Up @@ -1389,8 +1390,9 @@ const HELP_BY_COMMAND: Record<string, string> = {
tracked agent CLI session. Use attached runtime mode when you want the same
terminal the app is viewing.

$ ade terminal list --chat-session <owner-session-id> --text List terminals for a session
$ ade terminal list --chat-session <owner-session-id> --text List running and ended terminals for a session
$ ade terminal active --chat-session <owner-session-id> --text Show the active terminal
$ ade terminal resume --terminal <session-id> --text Resume an ended provider CLI terminal
$ ade terminal read --terminal <session-id> --text Read terminal scrollback
$ ade terminal read --pty <pty-id> --text Read by PTY id
$ ade app-control logs --text Read the active App Control launch terminal
Expand Down Expand Up @@ -5918,6 +5920,7 @@ function buildTerminalPlan(args: string[]): CliPlan {
return {
kind: "execute",
label: "terminal list",
formatter: "terminal-list",
steps: [
actionStep(
"result",
Expand All @@ -5932,6 +5935,31 @@ function buildTerminalPlan(args: string[]): CliPlan {
],
};
}
if (sub === "resume" || sub === "reattach") {
const terminal =
readValue(args, ["--terminal", "--terminal-id", "--session", "--session-id"]) ??
firstStandalonePositional(args);
return {
kind: "execute",
label: "terminal resume",
formatter: "pty-create",
steps: [
actionStep(
"result",
"pty",
"resumeSession",
collectGenericObjectArgs(args, {
sessionId: requireValue(terminal, "terminalId"),
cols: readIntOption(args, ["--cols"], 120),
rows: readIntOption(args, ["--rows"], 36),
model: readValue(args, ["--model", "--model-id"]),
reasoningEffort: readValue(args, ["--reasoning", "--reasoning-effort"]),
permissionMode: readValue(args, ["--permission-mode", "--permissions"]),
}),
),
],
};
}
if (sub === "active" || sub === "current") {
return {
kind: "execute",
Expand Down Expand Up @@ -11250,6 +11278,33 @@ function createSocketConnection(socketPath: string): net.Socket {
return net.createConnection(socketPath);
}

async function probeLocalSocketForLiveness(socketPath: string): Promise<"live" | "stale" | "unknown"> {
if (socketPath.startsWith("tcp://") || isAdeRuntimeNamedPipePath(socketPath)) {
return "unknown";
}
return await new Promise((resolve) => {
const socket = net.createConnection(socketPath);
let settled = false;
let timer: ReturnType<typeof setTimeout> | null = null;
const finish = (value: "live" | "stale" | "unknown") => {
if (settled) return;
settled = true;
if (timer) clearTimeout(timer);
socket.destroy();
resolve(value);
};
timer = setTimeout(() => finish("unknown"), 500);
socket.once("connect", () => finish("live"));
socket.once("error", (error: NodeJS.ErrnoException) => {
if (error.code === "ENOENT" || error.code === "ECONNREFUSED") {
finish("stale");
return;
}
finish("unknown");
});
});
}

function isRetryableSocketConnectError(error: NodeJS.ErrnoException): boolean {
return (
error.code === "ENOENT" ||
Expand Down Expand Up @@ -13690,9 +13745,21 @@ async function runServe(
fs.mkdirSync(layout.adeDir, { recursive: true, mode: 0o700 });
if (!isAdeRuntimeNamedPipePath(socketPath)) {
fs.mkdirSync(path.dirname(socketPath), { recursive: true, mode: 0o700 });
try {
fs.unlinkSync(socketPath);
} catch {}
if (fs.existsSync(socketPath)) {
const liveness = await probeLocalSocketForLiveness(socketPath);
if (liveness === "live" || liveness === "unknown") {
throw new CliExecutionError("ADE brain socket is already in use.", {
socketPath,
cause: liveness === "live"
? "Another ADE brain is accepting connections on this socket."
: "ADE could not prove the existing socket is stale.",
nextAction: "Stop the existing ADE brain or choose a different --socket path.",
});
}
try {
fs.unlinkSync(socketPath);
} catch {}
}
}

const socketState = createHeadlessRpcServer(createHandler);
Expand Down Expand Up @@ -15198,13 +15265,15 @@ function formatTerminalList(value: unknown): string {
? [value]
: firstArray(value, ["terminals", "items"]);
return renderTable(
["terminal", "pty", "chat", "status", "runtime", "title"],
["terminal", "pty", "chat", "status", "runtime", "ended", "resume", "title"],
terminals.map((terminal) => [
terminal.terminalId,
terminal.ptyId,
terminal.chatSessionId,
terminal.status,
terminal.runtimeState,
terminal.endedAt,
terminal.endedAt && (terminal.resumeCommand || terminal.resumeMetadata) ? "yes" : "",
terminal.title,
]),
"ADE attached terminals\n(no terminals found)",
Expand Down
Loading