Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
80 commits
Select commit Hold shift + click to select a range
3e7078a
fix(remote): harden beta runtime transport
arul28 Jun 4, 2026
46aa78a
fix(remote): reduce idle renderer chatter
arul28 Jun 4, 2026
66741b2
fix(remote): dedupe saved discovery targets
arul28 Jun 4, 2026
728fcfb
fix(remote): harden runtime reconnect and uploads
arul28 Jun 4, 2026
e15b9f5
fix(remote): explain SSH handshake resets
arul28 Jun 4, 2026
f0d1ecb
fix(remote): avoid autoconnecting new targets
arul28 Jun 4, 2026
f5f995f
fix(remote): guard runtime upload ssh channels
arul28 Jun 4, 2026
67e71da
fix(remote): avoid same-version runtime reuploads
arul28 Jun 4, 2026
fb155f4
fix(remote): upload runtimes over sftp
arul28 Jun 4, 2026
738bc23
fix(remote): bound ssh connect failures
arul28 Jun 4, 2026
3142ebf
fix(work): ignore stale session deep links on project switch
arul28 Jun 4, 2026
c0a3aaf
fix(remote): reset remote work state on project open
arul28 Jun 4, 2026
486bb82
fix(remote): mount active remote project surfaces
arul28 Jun 4, 2026
26cac15
fix(lanes): allow clean lane delete without hidden confirm
arul28 Jun 4, 2026
e87e443
fix(remote): preserve bindings in project surfaces
arul28 Jun 4, 2026
cda9e39
fix(remote): memoize project surface bindings
arul28 Jun 4, 2026
0a242dc
fix(prs): tolerate missing remote queue states
arul28 Jun 4, 2026
44489d5
fix(files): respect read-only workspaces
arul28 Jun 4, 2026
5896168
fix(files): forget stale recent paths
arul28 Jun 4, 2026
99d0f09
fix(remote): tunnel preview URLs locally
arul28 Jun 4, 2026
33a485b
fix(work): reduce remote session fanout
arul28 Jun 4, 2026
b3546c1
fix(work): defer model recents hydration
arul28 Jun 4, 2026
d5f0a30
fix(work): lazy load chat file workspaces
arul28 Jun 4, 2026
af9b8b9
fix(work): defer app control drawer status
arul28 Jun 4, 2026
c5f01fb
fix(remote): skip hidden linear checks
arul28 Jun 4, 2026
b6801a1
fix(work): share project config boot cache
arul28 Jun 4, 2026
52c6261
fix(work): cache chat slash commands
arul28 Jun 4, 2026
4806ae1
fix(work): cache chat session lists
arul28 Jun 4, 2026
de43403
fix(work): skip remote focus refetch
arul28 Jun 4, 2026
4f15e98
fix(remote): defer shell github status
arul28 Jun 4, 2026
ce8efdd
fix(work): lazy load remote chat git state
arul28 Jun 4, 2026
948d358
fix(work): defer remote chat delta fetch
arul28 Jun 4, 2026
7eb6b7f
fix(remote): prevent stale chat ai rechecks
arul28 Jun 4, 2026
c470129
fix(remote): scope renderer state to active project root
arul28 Jun 4, 2026
4728b56
fix(work): defer remote recovery checks
arul28 Jun 4, 2026
e78d223
fix(remote): defer hidden chrome hydration
arul28 Jun 4, 2026
612999e
fix(remote): skip event replay on attach
arul28 Jun 4, 2026
7cbbbf6
fix(remote): tolerate optional files capabilities
arul28 Jun 4, 2026
d9bf7b5
fix(remote): pause shell polling off work routes
arul28 Jun 4, 2026
969fac7
fix(work): hide browser view off work routes
arul28 Jun 4, 2026
ef87393
fix(browser): detach hidden browser views
arul28 Jun 4, 2026
3c38239
fix(lanes): dedupe mirrored agent sessions
arul28 Jun 4, 2026
1c0a6cd
fix(graph): refresh active remote graph when document hidden
arul28 Jun 4, 2026
24a0d37
fix(work): batch remote terminal input
arul28 Jun 4, 2026
ea77f96
fix(work): preserve batched terminal input ordering
arul28 Jun 4, 2026
31a10e6
fix(remote): tolerate missing PR queue states
arul28 Jun 4, 2026
6dae4a9
fix(remote): smooth files recents and lane git menus
arul28 Jun 4, 2026
40dbc12
fix(work): tolerate missing active terminal chat args
arul28 Jun 4, 2026
416c827
fix(remote): tighten remote event and chat actions
arul28 Jun 4, 2026
5d442d5
fix(work): hide unsupported remote tool panes
arul28 Jun 4, 2026
2f54d50
fix(work): defer mounted terminal teardown on project clear
arul28 Jun 4, 2026
d07ce44
fix(remote): refresh runtime artifacts and pty host
arul28 Jun 5, 2026
5ca0740
fix(remote): run pty worker from static runtime
arul28 Jun 5, 2026
c325bf5
fix(remote): stabilize runtime version and pty sessions
arul28 Jun 5, 2026
6db1275
fix(remote): prefer tailscale ip discovery routes
arul28 Jun 5, 2026
d0bba1b
fix(remote): bound ssh exec channels
arul28 Jun 5, 2026
363a970
perf(remote): cache runtime artifact hashes
arul28 Jun 5, 2026
67404dc
fix(remote): unwrap chat list session args
arul28 Jun 5, 2026
511bbb2
test(remote): use generic fixture hosts
arul28 Jun 5, 2026
172db16
fix(work): keep local-only tools hidden remotely
arul28 Jun 5, 2026
78471b6
fix(work): swallow failed background session refreshes
arul28 Jun 5, 2026
24cc9fd
perf(remote): batch runtime bootstrap probes
arul28 Jun 5, 2026
7e63c8e
fix(remote): avoid duplicate remote project tabs
arul28 Jun 5, 2026
347c9d5
fix(remote): simplify machine connection panel
arul28 Jun 5, 2026
657918a
fix(remote): make machine edit details collapsible
arul28 Jun 5, 2026
5864173
fix(remote): use edit affordance for detected machines
arul28 Jun 5, 2026
6c2217b
fix(remote): keep direct connect for detected machines
arul28 Jun 5, 2026
ddf62e8
fix(remote): flatten machines panel wrapper
arul28 Jun 5, 2026
8186f18
fix(remote): remove duplicate machines modal header
arul28 Jun 5, 2026
a6d0657
fix(remote): respect manual disconnect intent
arul28 Jun 5, 2026
423d6ae
fix(remote): surface reconnect and stale project state
arul28 Jun 5, 2026
e1b01ed
fix(remote): pause automatic reconnect after repeated failures
arul28 Jun 5, 2026
14e70bb
fix(remote): harden connection lifecycle
arul28 Jun 5, 2026
418e362
chore(remote): sync automation parity docs
arul28 Jun 5, 2026
8ee8199
chore(remote): simplify finalized code paths
arul28 Jun 5, 2026
45c18bc
test(remote): update runtime validation expectations
arul28 Jun 5, 2026
844b572
test(chat): wait for async file navigation
arul28 Jun 5, 2026
89537bc
fix(remote): harden review reconnect edges
arul28 Jun 5, 2026
210770f
fix(remote): address verified review hardening
arul28 Jun 5, 2026
4cf6b26
test(chat): wait for file navigation settle
arul28 Jun 5, 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
1 change: 1 addition & 0 deletions apps/ade-cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,7 @@ ade run start web --lane lane-id
ade shell start --lane lane-id -- npm test
ade shell start-cli codex --lane lane-id --permission-mode edit --message "fix failing tests"
ade shell start-cli --provider claude --lane lane-id --permission-mode default
ade chat list --lane lane-id --include-automation --no-archived --text
ade chat create --lane lane-id --model gpt-5.5
ade code
ade code --embedded
Expand Down
42 changes: 42 additions & 0 deletions apps/ade-cli/scripts/build-static.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ import { promisify } from "node:util";

const execFileAsync = promisify(execFile);
const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
const repoRoot = path.resolve(packageRoot, "..", "..");
const desktopPackageJsonPath = path.join(repoRoot, "apps", "desktop", "package.json");
const cliPackageJsonPath = path.join(packageRoot, "package.json");
const defaultOutDir = path.join(packageRoot, "dist-static");
const fuse = "NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2";

Expand Down Expand Up @@ -89,6 +92,27 @@ async function run(command, args, options = {}) {
if (stderr) process.stderr.write(stderr);
}

async function readPackageVersion(packageJsonPath) {
try {
const raw = await fs.readFile(packageJsonPath, "utf8");
const parsed = JSON.parse(raw);
return typeof parsed.version === "string" ? parsed.version.trim() : "";
} catch {
return "";
}
}

async function resolveRuntimeVersion() {
const explicit = process.env.ADE_CLI_VERSION?.trim();
if (explicit) return explicit;

const cliVersion = await readPackageVersion(cliPackageJsonPath);
if (cliVersion && cliVersion !== "0.0.0") return cliVersion;

const desktopVersion = await readPackageVersion(desktopPackageJsonPath);
return desktopVersion || cliVersion || "0.0.0";
}

async function assertSeaCapableNodeBinary(binaryPath) {
const contents = await fs.readFile(binaryPath);
if (contents.includes(Buffer.from(fuse))) return;
Expand All @@ -112,6 +136,21 @@ async function adHocSignIfNeeded(binaryPath) {
await run("codesign", ["--sign", "-", binaryPath]);
}

async function assertStaticRuntimeVersion(binaryPath, expectedVersion, target) {
if (target !== currentTarget()) return;
const { stdout } = await execFileAsync(binaryPath, ["--version"], {
cwd: packageRoot,
env: {
...process.env,
ADE_CLI_VERSION: "",
},
});
const actual = stdout.trim().replace(/^ade\s+/i, "");
if (actual !== expectedVersion) {
throw new Error(`Static ADE runtime version mismatch: expected ${expectedVersion}, got ${actual || "<empty>"}.`);
}
}

async function writeSeaEntry(workDir) {
const cliPath = path.join(packageRoot, "dist", "cli.cjs");
const seaEntryPath = path.join(workDir, "cli-sea.cjs");
Expand Down Expand Up @@ -221,6 +260,8 @@ async function main() {
const args = parseArgs(process.argv.slice(2));
await assertHostOrExplicitBinary(args.target);
await fs.mkdir(args.outDir, { recursive: true });
const runtimeVersion = await resolveRuntimeVersion();
process.env.ADE_CLI_VERSION = runtimeVersion;

if (!args.skipBuild) {
await run(process.platform === "win32" ? "npm.cmd" : "npm", ["run", "build"]);
Expand Down Expand Up @@ -263,6 +304,7 @@ async function main() {
}
await run(path.join(packageRoot, "node_modules", ".bin", process.platform === "win32" ? "postject.cmd" : "postject"), postjectArgs);
await adHocSignIfNeeded(binaryPath);
await assertStaticRuntimeVersion(binaryPath, runtimeVersion, args.target);

let nativeArchivePath = null;
if (!args.skipNativeDeps) {
Expand Down
37 changes: 37 additions & 0 deletions apps/ade-cli/src/cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,12 @@ describe("ADE CLI", () => {
});
});

it("recognizes the hidden PTY host worker entrypoint", () => {
expect(buildCliPlan(["__ade-pty-host-worker"])).toEqual({
kind: "pty-host-worker",
});
});

it("classifies only ADE temp runtime sockets as ephemeral", () => {
const tempSocket = path.join(os.tmpdir(), "ade-stdio-rpc-test", "sock", "ade.sock");

Expand Down Expand Up @@ -869,6 +875,37 @@ describe("ADE CLI", () => {
});
});

it("maps chat list filters to the listSessions action", () => {
const plan = buildCliPlan([
"chat",
"list",
"--lane",
"lane-1",
"--include-automation",
"--include-identity",
"--no-archived",
]);
expect(plan.kind).toBe("execute");
if (plan.kind !== "execute") return;
expect(plan.steps[0]?.params).toEqual({
name: "run_ade_action",
arguments: {
domain: "chat",
action: "listSessions",
args: {
laneId: "lane-1",
includeAutomation: true,
includeIdentity: true,
includeArchived: false,
},
},
});

expect(() =>
buildCliPlan(["chat", "list", "--include-archived", "--no-archived"]),
).toThrow(/Use either --include-archived or --no-archived/);
});

it("requires a chat session id for chat show", () => {
expect(() => buildCliPlan(["chat", "show"])).toThrow(
/sessionId is required/,
Expand Down
46 changes: 43 additions & 3 deletions apps/ade-cli/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,7 @@ type CliPlan =
| { kind: "runtime"; rest: string[] }
| { kind: "serve"; rest: string[] }
| { kind: "rpc-stdio"; rest: string[] }
| { kind: "pty-host-worker" }
| { kind: "init"; targetPath: string | null }
| { kind: "cursor-cloud"; rest: string[] }
| { kind: "deeplink"; rest: string[] };
Expand Down Expand Up @@ -1243,7 +1244,8 @@ const HELP_BY_COMMAND: Record<string, string> = {
Chat commands use ADE agent chat sessions. Live provider-backed chat normally
requires an attached runtime because the daemon owns provider/session state.

$ ade chat list --text List chat sessions
$ ade chat list --lane <lane> --text List chat sessions
$ ade chat list --include-automation --no-archived --text
$ ade chat create --lane <lane> --provider codex --model <model> [--fast]
$ ade chat create --from-linear-issue ENG-431 Start a chat with an attached issue + kickoff (alias: --linear-issue-json)
$ ade chat send <session> --text "next step" Send a message
Expand Down Expand Up @@ -5759,7 +5761,30 @@ function buildChatPlan(args: string[]): CliPlan {
...base,
...(sessionId ? { sessionId } : {}),
});
if (sub === "list" || sub === "ls")
if (sub === "list" || sub === "ls") {
const includeArchived = readFlag(args, ["--archived", "--include-archived"]);
const excludeArchived = readFlag(args, [
"--active",
"--no-archived",
"--exclude-archived",
]);
if (includeArchived && excludeArchived) {
throw new CliUsageError(
"Use either --include-archived or --no-archived, not both.",
);
}
const laneId = readLaneId(args);
const input = collectGenericObjectArgs(args, {
...(laneId ? { laneId } : {}),
...(includeArchived ? { includeArchived: true } : {}),
...(excludeArchived ? { includeArchived: false } : {}),
...(readFlag(args, ["--automation", "--include-automation"])
? { includeAutomation: true }
: {}),
...(readFlag(args, ["--identity", "--include-identity"])
? { includeIdentity: true }
: {}),
});
return {
kind: "execute",
label: "chat list",
Expand All @@ -5768,10 +5793,11 @@ function buildChatPlan(args: string[]): CliPlan {
"result",
"chat",
"listSessions",
collectGenericObjectArgs(args),
input,
),
],
};
}
if (sub === "show" || sub === "status")
return {
kind: "execute",
Expand Down Expand Up @@ -10422,6 +10448,9 @@ function buildCliPlan(command: string[]): CliPlan {
if (primary === "version" || primary === "--version" || primary === "-v") {
return { kind: "help", text: `ade ${VERSION}\n` };
}
if (primary === "__ade-pty-host-worker") {
return { kind: "pty-host-worker" };
}
if (primary === "code") {
const rest = args;
return { kind: "ade-code", rest };
Expand Down Expand Up @@ -15374,6 +15403,17 @@ async function runCli(
await runNativeRpcStdio(parsed.options);
return { output: "", exitCode: 0 };
}
if (plan.kind === "pty-host-worker") {
await import("../../desktop/src/main/services/pty/ptyHostWorker");
await new Promise<void>((resolve) => {
if (typeof process.send !== "function") {
resolve();
return;
}
process.once("disconnect", resolve);
});
return { output: "", exitCode: 0 };
}
if (plan.kind === "desktop") {
const result = await runDesktopCommand(plan.rest);
return {
Expand Down
10 changes: 5 additions & 5 deletions apps/ade-cli/src/tuiClient/__tests__/adeApi.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,10 +58,10 @@ describe("listLaneDiffStats", () => {

describe("chat session archive helpers", () => {
it("lists chats with archived sessions hidden by default", async () => {
const calls: Array<{ domain: string; action: string; argsList: unknown[] }> = [];
const calls: Array<{ domain: string; action: string; args: Record<string, unknown> | undefined }> = [];
const connection = {
actionList: async (domain: string, action: string, argsList: unknown[]) => {
calls.push({ domain, action, argsList });
action: async (domain: string, action: string, args?: Record<string, unknown>) => {
calls.push({ domain, action, args });
return [];
},
} as unknown as AdeCodeConnection;
Expand All @@ -70,8 +70,8 @@ describe("chat session archive helpers", () => {
await listChatSessions(connection, "lane-1", { includeArchived: true });

expect(calls).toEqual([
{ domain: "chat", action: "listSessions", argsList: [null, { includeArchived: false }] },
{ domain: "chat", action: "listSessions", argsList: ["lane-1", { includeArchived: true }] },
{ domain: "chat", action: "listSessions", args: { includeArchived: false } },
{ domain: "chat", action: "listSessions", args: { laneId: "lane-1", includeArchived: true } },
]);
});

Expand Down
7 changes: 4 additions & 3 deletions apps/ade-cli/src/tuiClient/adeApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,9 +74,10 @@ export async function listChatSessions(
laneId?: string | null,
options: { includeArchived?: boolean } = {},
): Promise<AgentChatSessionSummary[]> {
const listOptions = { includeArchived: options.includeArchived ?? false };
const argsList = laneId ? [laneId, listOptions] : [null, listOptions];
return await connection.actionList<AgentChatSessionSummary[]>("chat", "listSessions", argsList);
return await connection.action<AgentChatSessionSummary[]>("chat", "listSessions", {
...(laneId ? { laneId } : {}),
includeArchived: options.includeArchived ?? false,
});
}

export async function archiveChatSession(
Expand Down
11 changes: 11 additions & 0 deletions apps/desktop/src/main/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1446,6 +1446,8 @@ app.whenReady().then(async () => {
options: { emit?: boolean; foreground?: boolean } = {},
): void => {
const normalizedRoot = projectRoot ? normalizeProjectRoot(projectRoot) : null;
const previousRemoteBinding =
windowId != null ? (windowProjectBindings.get(windowId) ?? null) : null;
if (windowId != null) {
windowProjectRoots.set(windowId, normalizedRoot);
windowProjectBindings.delete(windowId);
Expand Down Expand Up @@ -1477,6 +1479,15 @@ app.whenReady().then(async () => {
});
}
}
if (previousRemoteBinding) {
const remainingRemoteBinding =
Array.from(windowProjectBindings.values()).at(-1) ?? null;
if (remainingRemoteBinding) {
persistLastRemoteProjectBinding(remainingRemoteBinding);
} else {
clearLastRemoteProjectBinding();
}
}
if (options.emit !== false) {
const project = projectForRoot(normalizedRoot);
emitProjectChangedToWindow(windowId, project);
Expand Down
51 changes: 51 additions & 0 deletions apps/desktop/src/main/services/adeActions/registry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,11 @@ describe("isAllowedAdeAction", () => {
expect(isCtoOnlyAdeAction("ios_simulator", "ensurePreviewWorkspace")).toBe(false);
});

it("exposes subagent transcript reads through the chat runtime action surface", () => {
expect(isAllowedAdeAction("chat", "getSubagentTranscript")).toBe(true);
expect(isCtoOnlyAdeAction("chat", "getSubagentTranscript")).toBe(false);
});

it("rejects an unknown action on a known domain", () => {
expect(isAllowedAdeAction("git", "rmRf")).toBe(false);
expect(isAllowedAdeAction("issue", "deleteAllIssues")).toBe(false);
Expand Down Expand Up @@ -343,6 +348,26 @@ describe("ADE_ACTION_ALLOWLIST shape", () => {
expect(getModelCatalog).toHaveBeenCalledWith({ mode: "cached" });
});

it("unwraps chat.listSessions action args before calling the positional chat service API", async () => {
const listSessions = vi.fn(async () => []);
const runtime = {
agentChatService: {
listSessions,
},
} as unknown as Parameters<typeof getAdeActionDomainServices>[0];

const chat = getAdeActionDomainServices(runtime).chat as {
listSessions?: (args?: unknown) => Promise<unknown>;
};

await expect(chat.listSessions?.({
laneId: " lane-1 ",
includeAutomation: true,
})).resolves.toEqual([]);
expect(listAllowedAdeActionNames("chat", chat as Record<string, unknown>)).toContain("listSessions");
expect(listSessions).toHaveBeenCalledWith("lane-1", { includeAutomation: true });
});

it("exposes the browser panel and tab control surface", () => {
const actions = ADE_ACTION_ALLOWLIST.built_in_browser ?? [];
for (const name of ["claim", "startSession", "listSessions", "endSession", "showPanel", "navigate", "createTab", "switchTab", "closeTab", "observe", "getTrace", "click", "typeText", "dispatchKey", "scroll", "fill", "clear", "wait"]) {
Expand Down Expand Up @@ -640,6 +665,32 @@ describe("runtime session actions", () => {
});
});

it("forwards remote subagent transcript reads through chat actions", async () => {
const transcript = [{ role: "assistant", content: "subagent output" }];
const getSubagentTranscript = vi.fn(async () => transcript);
const runtime = {
agentChatService: {
getSubagentTranscript,
},
} as unknown as Parameters<typeof getAdeActionDomainServices>[0];
const chatService = getAdeActionDomainServices(runtime).chat as {
getSubagentTranscript: (args: {
sessionId: string;
subagentId: string;
}) => Promise<Array<{ role: string; content: string }>>;
} & Record<string, unknown>;

expect(listAllowedAdeActionNames("chat", chatService)).toContain("getSubagentTranscript");
await expect(chatService.getSubagentTranscript({
sessionId: "chat-1",
subagentId: "subagent-1",
})).resolves.toEqual(transcript);
expect(getSubagentTranscript).toHaveBeenCalledWith({
sessionId: "chat-1",
subagentId: "subagent-1",
});
});

it("adds getDelta from the runtime session delta service", () => {
const delta = { sessionId: "session-1", filesChanged: 2 };
const runtime = {
Expand Down
16 changes: 16 additions & 0 deletions apps/desktop/src/main/services/adeActions/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -449,6 +449,7 @@ export const ADE_ACTION_ALLOWLIST: Partial<Record<AdeActionDomain, readonly stri
"getSessionCapabilities",
"getSessionSummary",
"getSlashCommands",
"getSubagentTranscript",
"getTurnFileDiff",
"getParallelLaunchState",
"interrupt",
Expand Down Expand Up @@ -1022,6 +1023,21 @@ function buildChatDomainService(runtime: AdeRuntime): OpaqueService | null {
"Terminal service not available.",
),
}),
listSessions: (args?: unknown) => {
const record = asActionRecord(args);
const laneId = typeof record.laneId === "string" && record.laneId.trim()
? record.laneId.trim()
: undefined;
const options = {
...(typeof record.includeArchived === "boolean" ? { includeArchived: record.includeArchived } : {}),
...(typeof record.includeAutomation === "boolean" ? { includeAutomation: record.includeAutomation } : {}),
...(typeof record.includeIdentity === "boolean" ? { includeIdentity: record.includeIdentity } : {}),
};
return agentChatService.listSessions(
laneId,
Object.keys(options).length ? options : undefined,
);
},
modelCatalog: (args?: unknown) =>
agentChatService.getModelCatalog(args && typeof args === "object" ? args as never : undefined),
codexOpenInCli: async (
Expand Down
Loading
Loading