Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
1 change: 0 additions & 1 deletion .ade/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ local.secret.yaml
ade.db
ade.db-*
ade.db-wal
*.bak
embeddings.db
mcp.sock
artifacts/
Expand Down
7 changes: 2 additions & 5 deletions .ade/cto/identity.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
name: CTO
version: 2
version: 1
persona: >-
You are the CTO for this project inside ADE.

Expand Down Expand Up @@ -28,7 +28,4 @@ openclawContextPolicy:
- secret
- token
- system_prompt
onboardingState:
completedSteps: []
dismissedAt: 2026-04-01T23:35:05.209Z
updatedAt: 2026-04-01T23:35:05.211Z
updatedAt: 1970-01-01T00:00:00.000Z
19 changes: 19 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,25 @@ All notable changes to ADE will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Added

- **Packed session grid** — resizable tile layout for the Work view with per-session column/row spans, drag-handle resizing on all edges and corners, and a bin-packing algorithm for compact arrangement (`PackedSessionGrid`, `packedSessionGridMath`)
- **Multi-select agent questions** — `AgentQuestionModal` now supports toggling multiple predefined options per question, with a Markdown/HTML preview pane for selected option descriptions (via `ReactMarkdown` + `rehype-sanitize`)
- **New Chat quick-create** — faster optimistic session opening from the Work view with immediate tab activation before the backend session is ready
- **Turn recap** — `chatTranscriptRows` emits a `turn_recap` summary row at the end of each turn, aggregating tool invocation counts and status
- **Claude tool-use tracking** — per-invocation lifecycle tracking via `toolUseID`; `tool_use_start` and `tool_use_complete` events enable per-tool status indicators in the work log
- **MCP initialize probe** — Claude runtime pre-checks MCP server availability before starting a session

### Changed

- **Terminal renderer fallback** — simplified from three tiers (WebGL/canvas/DOM) to two (WebGL-first with DOM fallback); added fit recovery with retry on invalid dimensions and `fitRecoveries` health counter
- **Work log headings** — human-readable labels (e.g. "Read utils.ts", "Run shell", "Write index.ts") replace generic tool identifiers; default visible entries increased from 1 to 4
- **Model catalog filtering** — `UnifiedModelSelector` accepts `catalogMode: "available-only"` to restrict the picker to models available via configured providers
- **Git stash actions** — stash pop, drop, and clear now refresh workspace metadata after completion
- **Composer sizing** — new compact and grid-tile sizing modes in `ChatComposerShell`

## [1.0.2] - 2026-03-15

### Added
Expand Down
7 changes: 7 additions & 0 deletions apps/desktop/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/desktop/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
"@radix-ui/react-tabs": "^1.1.13",
"@tanstack/react-virtual": "^3.13.21",
"@xterm/addon-fit": "^0.11.0",
"@xterm/addon-webgl": "^0.19.0",
"@xterm/xterm": "^6.0.0",
"@xyflow/react": "^12.5.0",
"ai": "^6.0.141",
Expand Down
17 changes: 17 additions & 0 deletions apps/desktop/scripts/validate-mac-artifacts.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,23 @@ async function validatePackagedRuntime(appPath, description) {
if (!payload?.proxyProbe?.ok) {
throw new Error("[release:mac] Packaged smoke failed to launch the bundled ADE MCP proxy in probe mode");
}
// Do not rely on probe mode alone here. The regression we fixed still let
// the packaged proxy start, but chat MCP failed once Claude/Codex attempted
// the first initialize handshake through that launch path.
if (!payload?.proxyInitialize?.ok) {
throw new Error(
`[release:mac] Packaged smoke failed to complete MCP initialize through the bundled ADE proxy: ${
String(payload?.proxyInitialize?.error || payload?.proxyInitialize?.stderr || "unknown error")
}`
);
}
if (payload?.proxyInitialize?.response?.result?.serverInfo?.name !== "ade-mcp-server") {
throw new Error(
`[release:mac] Packaged smoke expected ADE MCP initialize to report ade-mcp-server, got ${
JSON.stringify(payload?.proxyInitialize?.response ?? null)
}`
);
}

console.log(`[release:mac] Packaged runtime smoke passed for ${description}: ${path.relative(appPath, nodePtyAddon)}`);
}
Expand Down
71 changes: 70 additions & 1 deletion apps/desktop/src/main/packagedRuntimeSmoke.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import fs from "node:fs";
import path from "node:path";
import { execFile } from "node:child_process";
import { execFile, spawnSync } from "node:child_process";
import { createRequire } from "node:module";
import { promisify } from "node:util";
import { resolveDesktopAdeMcpLaunch } from "./services/runtime/adeMcpLaunch";
Expand Down Expand Up @@ -101,6 +101,64 @@ async function probeClaudeStartup(
}
}

function probeMcpInitialize(args: {
command: string;
cmdArgs: string[];
cwd: string;
env: NodeJS.ProcessEnv;
}): {
ok: boolean;
response: unknown | null;
stderr: string | null;
error: string | null;
} {
// Keep this as a real MCP initialize round-trip instead of another cheap
// "--probe" check. We regressed packaged chats by launching the proxy
// successfully but routing chat MCP through the wrong path, which only
// showed up once the client attempted the first initialize handshake.
const payload = JSON.stringify({
jsonrpc: "2.0",
id: 1,
method: "initialize",
params: {
protocolVersion: "2025-06-18",
clientInfo: {
name: "packaged-runtime-smoke",
version: "1.0.0",
},
capabilities: {},
},
});

const result = spawnSync(args.command, args.cmdArgs, {
cwd: args.cwd,
env: args.env,
input: `${payload}\n`,
encoding: "utf8",
timeout: 5_000,
});

const stdout = (result.stdout ?? "").trim();
const stderr = (result.stderr ?? "").trim();
const error = result.error ? String(result.error.message ?? result.error) : null;

try {
return {
ok: result.status === 0,
response: stdout ? JSON.parse(stdout) : null,
stderr: stderr || null,
error,
};
} catch (parseError) {
return {
ok: false,
response: stdout || null,
stderr: stderr || null,
error: parseError instanceof Error ? parseError.message : String(parseError),
};
}
}

async function main(): Promise<void> {
const pty = await import("node-pty");
const claude = await import("@anthropic-ai/claude-agent-sdk");
Expand Down Expand Up @@ -135,6 +193,16 @@ async function main(): Promise<void> {
proxyProbeResult = proxyProbeStdout;
}

const proxyInitialize = probeMcpInitialize({
command: launch.command,
cmdArgs: launch.cmdArgs,
cwd,
env: {
...process.env,
...launch.env,
},
});

process.stdout.write(JSON.stringify({
ok: true,
nodePty: typeof pty.spawn,
Expand All @@ -149,6 +217,7 @@ async function main(): Promise<void> {
launchEntryPath: launch.entryPath,
launchSocketPath: launch.socketPath,
proxyProbe: proxyProbeResult,
proxyInitialize,
}));
}

Expand Down
62 changes: 39 additions & 23 deletions apps/desktop/src/main/services/ai/authDetector.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
}

vi.mock("node:child_process", async () => {
const actual = await vi.importActual<typeof import("node:child_process")>("node:child_process");

Check warning on line 39 in apps/desktop/src/main/services/ai/authDetector.test.ts

View workflow job for this annotation

GitHub Actions / lint-desktop

`import()` type annotations are forbidden
return {
...actual,
spawn: (...args: unknown[]) => spawnMock(...args),
Expand All @@ -49,9 +49,9 @@
}));

// Import AFTER mocks are set up — must re-import to reset the module-level cache.
let detectAllAuth: typeof import("./authDetector").detectAllAuth;

Check warning on line 52 in apps/desktop/src/main/services/ai/authDetector.test.ts

View workflow job for this annotation

GitHub Actions / lint-desktop

`import()` type annotations are forbidden
let detectCliAuthStatuses: typeof import("./authDetector").detectCliAuthStatuses;

Check warning on line 53 in apps/desktop/src/main/services/ai/authDetector.test.ts

View workflow job for this annotation

GitHub Actions / lint-desktop

`import()` type annotations are forbidden
let verifyProviderApiKey: typeof import("./authDetector").verifyProviderApiKey;

Check warning on line 54 in apps/desktop/src/main/services/ai/authDetector.test.ts

View workflow job for this annotation

GitHub Actions / lint-desktop

`import()` type annotations are forbidden
const originalPlatform = process.platform;

function setPlatform(value: NodeJS.Platform): void {
Expand Down Expand Up @@ -260,38 +260,54 @@
it("finds codex through an npm-global prefix when PATH lookup fails", async () => {
tempHomeDir = fs.mkdtempSync(path.join(os.tmpdir(), "ade-auth-detector-"));
const prefixDir = path.join(tempHomeDir, ".npm-global");
const preferredCodexPath = path.join(prefixDir, "bin", "codex");
fs.mkdirSync(path.join(prefixDir, "bin"), { recursive: true });
fs.writeFileSync(path.join(tempHomeDir, ".npmrc"), "prefix=~/.npm-global\n", "utf8");
fs.writeFileSync(path.join(prefixDir, "bin", "codex"), "#!/bin/sh\nexit 0\n", "utf8");
fs.chmodSync(path.join(prefixDir, "bin", "codex"), 0o755);
fs.writeFileSync(preferredCodexPath, "#!/bin/sh\nexit 0\n", "utf8");
fs.chmodSync(preferredCodexPath, 0o755);
process.env.HOME = tempHomeDir;
process.env.PATH = "/usr/bin:/bin";

spawnMock.mockImplementation((command: string, args: string[] = []) => {
if (args[0] === "--version") {
if (command === "codex") return fakeError();
if (command === path.join(prefixDir, "bin", "codex")) return fakeChild({ status: 0, stdout: "0.105.0\n" });
return fakeError();
const realStatSync = fs.statSync.bind(fs);
const statSpy = vi.spyOn(fs, "statSync").mockImplementation(((candidatePath: fs.PathLike, options?: fs.StatOptions) => {
const resolved = String(candidatePath);
if (resolved.endsWith("/codex") && resolved !== preferredCodexPath) {
const error = new Error(`ENOENT: no such file or directory, stat '${resolved}'`) as NodeJS.ErrnoException;
error.code = "ENOENT";
throw error;
}
if (command === "which") {
return realStatSync(candidatePath, options as fs.StatOptions | undefined);
}) as typeof fs.statSync);

try {
spawnMock.mockImplementation((command: string, args: string[] = []) => {
if (args[0] === "--version") {
if (command === "codex") return fakeError();
if (command === preferredCodexPath) return fakeChild({ status: 0, stdout: "0.105.0\n" });
return fakeError();
}
if (command === "which") {
return fakeChild({ status: 1 });
}
if ((command === "codex" || command.endsWith("/codex")) && args[0] === "login" && args[1] === "status") {
return fakeChild({ status: 0, stdout: "Authenticated as test-user\n" });
}
return fakeChild({ status: 1 });
}
if ((command === "codex" || command.endsWith("/codex")) && args[0] === "login" && args[1] === "status") {
return fakeChild({ status: 0, stdout: "Authenticated as test-user\n" });
}
return fakeChild({ status: 1 });
});
});

const statuses = await detectCliAuthStatuses();
const codex = statuses.find((entry) => entry.cli === "codex");
const statuses = await detectCliAuthStatuses();
const codex = statuses.find((entry) => entry.cli === "codex");

expect(codex).toEqual({
cli: "codex",
installed: true,
path: path.join(prefixDir, "bin", "codex"),
authenticated: true,
verified: true,
});
expect(codex).toEqual({
cli: "codex",
installed: true,
path: preferredCodexPath,
authenticated: true,
verified: true,
});
} finally {
statSpy.mockRestore();
}
});

it("repairs PATH from the interactive shell during a forced refresh", async () => {
Expand Down
Loading
Loading