Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
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: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,12 @@ npm run dev

That aliases to `npm run dev:desktop`: it rebuilds `apps/ade-cli`, launches the Electron desktop app, and points it at the dev runtime socket `/tmp/ade-runtime-dev.sock`. If no dev runtime is listening, desktop is allowed to create it. This is the normal desktop-dev flow.

When these commands are run from an ADE lane worktree under `.ade/worktrees/`,
they still run code from that lane checkout, but they open the primary checkout's
project data by default. For example, running from
`/path/to/ADE/.ade/worktrees/my-lane` opens `/path/to/ADE` as the ADE project
and uses the lane path as the workspace root for `dev:code`.

Dev command matrix:

```bash
Expand Down
5 changes: 5 additions & 0 deletions apps/ade-cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -325,6 +325,11 @@ The dev scripts are the same runtime daemon, just running from source against a
/tmp/ade-runtime-dev.sock
```

From an ADE lane checkout under `.ade/worktrees/`, the dev scripts keep using
that lane's source code, but default the ADE project root back to the primary
checkout. `npm run dev:code` also passes the lane checkout as the workspace root
so initial lane selection matches `ade code` launched directly from the lane.

Full matrix:

```bash
Expand Down
172 changes: 172 additions & 0 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 @@ -28,6 +28,7 @@
"@anthropic-ai/claude-agent-sdk": "^0.2.139",
"@cursor/sdk": "^1.0.9",
"@linear/sdk": "^84.0.0",
"@openai/codex": "0.130.0",
"@opencode-ai/sdk": "^1.4.2",
"@wize-logic/nodejs-rfb": "^4.2.0",
"bonjour-service": "^1.3.0",
Expand Down
11 changes: 11 additions & 0 deletions apps/ade-cli/src/cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -893,6 +893,17 @@ describe("ADE CLI", () => {
});
});

it("rejects --print=value on chat send", () => {
expect(() => buildCliPlan([
"chat",
"send",
"chat-1",
"--print=true",
"--text",
"Hello",
])).toThrow(/--print must be set at session creation time/);
});

it("builds chat show/status as positional session summary calls", () => {
const show = buildCliPlan(["chat", "show", "chat-1"]);
expect(show.kind).toBe("execute");
Expand Down
38 changes: 30 additions & 8 deletions apps/ade-cli/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -173,10 +173,13 @@ type ReadinessCheck = {

declare const __ADE_VERSION__: string | undefined;

const BUNDLED_VERSION =
typeof __ADE_VERSION__ === "string" ? __ADE_VERSION__.trim() : "";
const ENV_VERSION = process.env.ADE_CLI_VERSION?.trim() ?? "";
const VERSION =
typeof __ADE_VERSION__ === "string" && __ADE_VERSION__.trim()
? __ADE_VERSION__
: process.env.ADE_CLI_VERSION?.trim() || "0.0.0";
BUNDLED_VERSION && BUNDLED_VERSION !== "0.0.0"
? BUNDLED_VERSION
: ENV_VERSION || BUNDLED_VERSION || "0.0.0";
const PROTOCOL_VERSION = "2025-06-18";
const SOURCE_FALLBACK_ENV = "ADE_CLI_SOURCE_FALLBACK_ACTIVE";
const CLI_ENTRY_PATH =
Expand Down Expand Up @@ -5099,6 +5102,10 @@ function buildChatPlan(args: string[]): CliPlan {
: standardRequested
? false
: undefined;
// `--print` opts the session's app-server initialize handshake into
// print-mode (suppresses delta notification streams). Must be set at create
// time because the handshake runs once when the runtime starts.
const createRuntimeMode = readFlag(args, ["--print"]) ? "print" : undefined;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

readFlag misses --print=value on chat create

Low Severity

The chat create path uses readFlag(args, ["--print"]) which only matches the exact bare token "--print". The chat send path detects both "--print" and "--print=<value>" via args.some(token => token === "--print" || token.startsWith("--print=")) and rejects with a message directing users to chat create --print. A user who originally wrote --print=true on send would naturally try the same syntax on create, where it silently becomes a no-op — the unconsumed token leaks into collectGenericObjectArgs instead of enabling print mode.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 236177c. Configure here.

return {
kind: "execute",
label: "chat create",
Expand All @@ -5124,12 +5131,28 @@ function buildChatPlan(args: string[]): CliPlan {
title: readValue(args, ["--title"]),
surface: readValue(args, ["--surface"]) ?? "work",
...(codexFastMode !== undefined ? { codexFastMode } : {}),
...(createRuntimeMode ? { runtimeMode: createRuntimeMode } : {}),
}),
),
],
};
}
if (sub === "send")
if (sub === "send") {
const imageUrl = readValue(args, ["--image-url"]);
// `--print` is honored at session create time only — the app-server
// initialize handshake runs once per session, so setting it per-message
// would be a silent no-op. Reject explicitly so users move it to
// `ade chat create --print`.
const hasPrintFlag = args.some((token) => token === "--print" || token.startsWith("--print="));
if (hasPrintFlag) {
throw new CliUsageError(
"--print must be set at session creation time. Use `ade chat create --print ...`.",
);
}
const sendText = requireValue(
readValue(args, ["--text", "--message"]) ?? args.join(" "),
"message text",
);
return {
kind: "execute",
label: "chat send",
Expand All @@ -5140,14 +5163,13 @@ function buildChatPlan(args: string[]): CliPlan {
"sendMessage",
withSession({
sessionId: requireValue(sessionId, "sessionId"),
text: requireValue(
readValue(args, ["--text", "--message"]) ?? args.join(" "),
"message text",
),
text: sendText,
...(imageUrl ? { attachments: [{ type: "image-url", url: imageUrl, path: imageUrl }] } : {}),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Image-URL attachment sets path to URL string

Low Severity

The chat send --image-url attachment object sets path: imageUrl, passing the URL string as the path field. The path field in attachments conventionally represents a local file system path, not a URL. Downstream handlers that attempt file operations on this path value would fail since it's a URL like https://example.com/img.png. The path field could be omitted or set to null instead.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 236177c. Configure here.

}),
),
],
};
}
if (sub === "interrupt")
return {
kind: "execute",
Expand Down
58 changes: 57 additions & 1 deletion apps/ade-cli/src/tuiClient/__tests__/adeApi.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import os from "node:os";
import path from "node:path";
import { afterEach, describe, expect, it, vi } from "vitest";
import type { AgentChatEventEnvelope } from "../../../../desktop/src/shared/types/chat";
import { createChatSession, DEFAULT_CODEX_REASONING_EFFORT, discoverProjectSlashCommands, latestTokenStats, sendChatMessage } from "../adeApi";
import { createChatSession, DEFAULT_CODEX_REASONING_EFFORT, discoverProjectSlashCommands, latestGoal, latestTokenStats, sendChatMessage } from "../adeApi";
import type { AdeCodeConnection } from "../types";

const tmpPaths: string[] = [];
Expand Down Expand Up @@ -102,6 +102,62 @@ describe("latestTokenStats", () => {
];
expect(latestTokenStats(events).percent).toBeNull();
});

it("reads cachedInputTokens / cacheReadTokens from both tokens and codex_token_usage events", () => {
const events = [
envelope(1, {
type: "tokens",
turnId: "turn-1",
inputTokens: 1_000,
outputTokens: 500,
cacheReadTokens: 450,
contextWindow: 10_000,
} as AgentChatEventEnvelope["event"]),
envelope(2, {
type: "codex_token_usage",
usage: {
last: { inputTokens: 2_300, outputTokens: 1_100, cacheReadTokens: 600 },
modelContextWindow: 10_000,
},
} as AgentChatEventEnvelope["event"]),
];
const stats = latestTokenStats(events);
expect(stats.cacheReadTokens).toBe(600);
expect(stats.inputTokens).toBe(2_300);
expect(stats.outputTokens).toBe(1_100);
});

it("reads cachedInputTokens from done usage events", () => {
const events = [
envelope(1, {
type: "done",
turnId: "turn-1",
status: "completed",
usage: { inputTokens: 1_000, outputTokens: 200, cachedInputTokens: 350 },
} as AgentChatEventEnvelope["event"]),
];
const stats = latestTokenStats(events);
expect(stats.cacheReadTokens).toBe(350);
});
});

describe("latestGoal", () => {
it("tracks the most recent updated goal and respects clears", () => {
expect(latestGoal([
envelope(1, {
type: "codex_goal_updated",
goal: { objective: "Refactor middleware", status: "active", tokensUsed: 100, tokenBudget: 5_000 },
} as AgentChatEventEnvelope["event"]),
])?.objective).toBe("Refactor middleware");

expect(latestGoal([
envelope(1, {
type: "codex_goal_updated",
goal: { objective: "Old goal", status: "active" },
} as AgentChatEventEnvelope["event"]),
envelope(2, { type: "codex_goal_cleared" } as AgentChatEventEnvelope["event"]),
])).toBeNull();
});
});

describe("discoverProjectSlashCommands", () => {
Expand Down
Loading
Loading