Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
94 changes: 47 additions & 47 deletions apps/ade-cli/package-lock.json

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

2 changes: 1 addition & 1 deletion apps/ade-cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
},
"dependencies": {
"@anthropic-ai/claude-agent-sdk": "^0.2.139",
"@cursor/sdk": "^1.0.9",
"@cursor/sdk": "^1.0.13",
"@linear/sdk": "^84.0.0",
"@openai/codex": "0.130.0",
"@opencode-ai/sdk": "^1.15.5",
Expand Down
33 changes: 33 additions & 0 deletions apps/ade-cli/src/cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1919,6 +1919,39 @@ describe("ADE CLI", () => {
});
});

it("maps existing lane Linear issue linking to the lane action", () => {
const plan = buildCliPlan([
"lanes",
"link-linear-issue",
"lane-1",
"--linear-issue-json",
'{"id":"issue-1","identifier":"ADE-123","title":"Linked lane"}',
"--source",
"manual",
"--no-include-in-pr",
]);

expect(plan.kind).toBe("execute");
if (plan.kind !== "execute") return;
expect(plan.steps[0]?.params).toEqual({
name: "run_ade_action",
arguments: {
domain: "lane",
action: "linkLinearIssues",
args: {
laneId: "lane-1",
issues: [{
id: "issue-1",
identifier: "ADE-123",
title: "Linked lane",
}],
source: "manual",
includeInPr: false,
},
},
});
});

it("maps Linear quick view to the typed RPC tool", () => {
const plan = buildCliPlan(["linear", "quick-view", "--text"]);

Expand Down
38 changes: 38 additions & 0 deletions apps/ade-cli/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -921,6 +921,8 @@ const HELP_BY_COMMAND: Record<string, string> = {
$ ade lanes show <lane> --text Inspect one lane status
$ ade lanes create --name <name> Create a lane from the current project context
$ ade lanes create --linear-issue-json '{...}' Create a lane linked to a Linear issue
$ ade lanes link-linear-issue <lane> --linear-issue-json '{...}'
Link an existing lane to a Linear issue
$ ade lanes create --branch-name <branch> Override the auto-generated branch name
$ ade lanes child --lane <parent> --name <name> Create a child lane under a parent
$ ade lanes import --branch <branch> Register an existing branch/worktree
Expand Down Expand Up @@ -2379,6 +2381,42 @@ function buildLanePlan(args: string[]): CliPlan {
steps: [actionArgsListStep("result", "lane", "getChildren", [laneId])],
};
}
if (sub === "link-linear-issue" || sub === "link-linear" || sub === "linear-link") {
const laneId = requireValue(
readLaneId(args) ?? firstPositional(args),
"laneId",
);
const linearIssueJson = requireValue(
readValue(args, ["--linear-issue-json", "--issue-json"]),
"--linear-issue-json",
);
const parsed = parseJson(linearIssueJson, "--linear-issue-json");
const issues = Array.isArray(parsed) ? parsed : [parsed];
if (issues.length === 0 || issues.some((issue) => !isRecord(issue))) {
throw new CliUsageError("--linear-issue-json must decode to an object or array of objects.");
}
const input: JsonObject = {
laneId,
issues: issues as JsonObject[],
};
maybePut(input, "role", readValue(args, ["--role"]));
maybePut(input, "source", readValue(args, ["--source"]));
if (readFlag(args, ["--no-include-in-pr"])) input.includeInPr = false;
if (readFlag(args, ["--include-in-pr"])) input.includeInPr = true;
if (readFlag(args, ["--close-on-merge"])) input.closeOnMerge = true;
Comment on lines +2404 to +2406
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Reject contradictory include-in-pr flags

Passing both --include-in-pr and --no-include-in-pr currently succeeds and silently picks the later assignment. Please fail fast on conflicting flags to avoid ambiguous CLI behavior.

Suggested fix
-    if (readFlag(args, ["--no-include-in-pr"])) input.includeInPr = false;
-    if (readFlag(args, ["--include-in-pr"])) input.includeInPr = true;
+    const noIncludeInPr = readFlag(args, ["--no-include-in-pr"]);
+    const includeInPr = readFlag(args, ["--include-in-pr"]);
+    if (noIncludeInPr && includeInPr) {
+      throw new CliUsageError(
+        "Use either --include-in-pr or --no-include-in-pr, not both.",
+      );
+    }
+    if (noIncludeInPr) input.includeInPr = false;
+    if (includeInPr) input.includeInPr = true;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (readFlag(args, ["--no-include-in-pr"])) input.includeInPr = false;
if (readFlag(args, ["--include-in-pr"])) input.includeInPr = true;
if (readFlag(args, ["--close-on-merge"])) input.closeOnMerge = true;
const noIncludeInPr = readFlag(args, ["--no-include-in-pr"]);
const includeInPr = readFlag(args, ["--include-in-pr"]);
if (noIncludeInPr && includeInPr) {
throw new CliUsageError(
"Use either --include-in-pr or --no-include-in-pr, not both.",
);
}
if (noIncludeInPr) input.includeInPr = false;
if (includeInPr) input.includeInPr = true;
if (readFlag(args, ["--close-on-merge"])) input.closeOnMerge = true;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/ade-cli/src/cli.ts` around lines 2588 - 2590, The CLI currently allows
both --include-in-pr and --no-include-in-pr to be passed and the later
assignment wins; update the parsing logic around readFlag(args,
["--no-include-in-pr"]), readFlag(args, ["--include-in-pr"]) and
input.includeInPr to detect when both flags are present and fail fast: if both
readFlag(...) calls return true, emit a clear error message and exit with a
non-zero status (or throw an Error) instead of silently overriding; otherwise
continue setting input.includeInPr as currently implemented.

return {
kind: "execute",
label: "lane link Linear issue",
steps: [
actionStep(
"result",
"lane",
"linkLinearIssues",
collectGenericObjectArgs(args, input),
),
],
};
}
if (sub === "stack") {
const laneId = requireValue(
readLaneId(args) ?? firstPositional(args),
Expand Down
47 changes: 46 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 { cancelSteerMessage, createChatSession, DEFAULT_CODEX_REASONING_EFFORT, dispatchSteerMessage, discoverProjectSlashCommands, editSteerMessage, latestGoal, latestTokenStats, listLaneDiffStats, listPrsByLane, listTerminalSessions, sendChatMessage, signalTerminal, startClaudeTerminalSession, steerChatMessage } from "../adeApi";
import { cancelSteerMessage, createChatSession, DEFAULT_CODEX_REASONING_EFFORT, dispatchSteerMessage, discoverProjectSlashCommands, editSteerMessage, getAvailableModels, latestGoal, latestTokenStats, listLaneDiffStats, listPrsByLane, listTerminalSessions, sendChatMessage, signalTerminal, startClaudeTerminalSession, steerChatMessage } from "../adeApi";
import type { AdeCodeConnection } from "../types";

const tmpPaths: string[] = [];
Expand Down Expand Up @@ -232,6 +232,51 @@ describe("discoverProjectSlashCommands", () => {
expect.objectContaining({ name: "/ship" }),
]));
});

it("includes Cursor command files and subagents", () => {
const projectRoot = makeTmpRoot("ade-code-cursor-command-");
const commandsDir = path.join(projectRoot, ".cursor", "commands");
const agentsDir = path.join(projectRoot, ".cursor", "agents");
fs.mkdirSync(commandsDir, { recursive: true });
fs.mkdirSync(agentsDir, { recursive: true });
fs.writeFileSync(path.join(commandsDir, "review-code.md"), "Review code.\n");
fs.writeFileSync(path.join(agentsDir, "verifier.md"), [
"---",
"description: Verify the change",
"---",
"",
"Verify.",
"",
].join("\n"));

const commands = discoverProjectSlashCommands(projectRoot);
expect(commands).toEqual(expect.arrayContaining([
expect.objectContaining({ name: "/review-code", description: "Review code." }),
expect.objectContaining({ name: "/verifier", description: "Verify the change" }),
]));
});
Comment on lines +236 to +257
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Add dual-path ADE CLI verification for these new API behaviors.

These tests validate payload wiring, but they don’t verify the required headless mode and desktop socket-backed ADE RPC paths. Please add one assertion path per mode for these added behaviors to prevent transport-specific regressions.

As per coding guidelines, "For ADE CLI changes, verify both headless mode and the desktop socket-backed ADE RPC path".

Also applies to: 260-279

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/ade-cli/src/tuiClient/__tests__/adeApi.test.ts` around lines 236 - 257,
The test "includes Cursor command files and subagents" currently only asserts
discoverProjectSlashCommands(projectRoot) results; add two assertion paths that
invoke the CLI-layer verification for both headless and desktop RPC transports:
call the ADE CLI verification entry points used in tests (the headless-mode
verifier and the desktop socket-backed ADE RPC verifier — e.g., the functions or
helpers that simulate headless invocation and the desktop socket-backed RPC
client used elsewhere in tests) using the same temporary projectRoot created by
makeTmpRoot, then assert each returns/produces the expected commands (matching
objects with name "/review-code" and "/verifier" and the same descriptions).
Ensure you reference the same discoverProjectSlashCommands-derived expectations
so each transport path (headless and desktop socket-backed ADE RPC) has its own
expect(...).toEqual/arrayContaining checks to prevent transport-specific
regressions.

});

describe("getAvailableModels", () => {
it("activates dynamic Cursor model discovery for TUI model lists", async () => {
const calls: Array<{ domain: string; action: string; args?: Record<string, unknown> }> = [];
const connection = {
action: vi.fn(async (domain: string, action: string, args?: Record<string, unknown>) => {
calls.push({ domain, action, args });
return [];
}),
} as any;

await getAvailableModels(connection, "cursor");

expect(calls).toEqual([
{
domain: "chat",
action: "getAvailableModels",
args: { provider: "cursor", activateRuntime: true },
},
]);
});
});

describe("createChatSession", () => {
Expand Down
25 changes: 3 additions & 22 deletions apps/ade-cli/src/tuiClient/adeApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,7 @@ import type {
PtySendToSessionResult,
TerminalSessionSummary,
} from "../../../desktop/src/shared/types";
import { discoverClaudeSlashCommands } from "../../../desktop/src/main/services/chat/claudeSlashCommandDiscovery";
import { discoverCodexSlashCommands } from "../../../desktop/src/main/services/chat/codexSlashCommandDiscovery";
import { discoverAllProjectSlashCommands } from "../../../desktop/src/main/services/chat/projectSlashCommandDiscovery";
import type { AdeCodeConnection, ChatHistorySnapshot, CreatedChat, NavigateRequest, NavigateResult } from "./types";

export const DEFAULT_CODEX_REASONING_EFFORT = "low";
Expand Down Expand Up @@ -286,26 +285,8 @@ export async function setClaudeOutputStyle(
return await connection.action<AgentChatSession>("chat", "setClaudeOutputStyle", { sessionId, outputStyle });
}

function slashCommandKey(value: string): string {
return value.trim().toLowerCase();
}

export function discoverProjectSlashCommands(workspaceRoot: string): AgentChatSlashCommand[] {
const byName = new Map<string, AgentChatSlashCommand>();
const add = (command: { name: string; description: string; argumentHint?: string }) => {
const key = slashCommandKey(command.name);
if (key === "/login") return;
if (byName.has(key)) return;
byName.set(key, {
name: command.name,
description: command.description,
argumentHint: command.argumentHint,
source: "sdk",
});
};
for (const command of discoverClaudeSlashCommands(workspaceRoot)) add(command);
for (const command of discoverCodexSlashCommands(workspaceRoot)) add(command);
return [...byName.values()].sort((a, b) => a.name.localeCompare(b.name, undefined, { sensitivity: "base" }));
return discoverAllProjectSlashCommands(workspaceRoot);
}

export async function getAvailableModels(
Expand All @@ -314,7 +295,7 @@ export async function getAvailableModels(
): Promise<AgentChatModelInfo[]> {
return await connection.action<AgentChatModelInfo[]>("chat", "getAvailableModels", {
provider,
activateRuntime: false,
activateRuntime: provider === "cursor",
});
}

Expand Down
1 change: 1 addition & 0 deletions apps/desktop/src/main/services/adeActions/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,7 @@ export const ADE_ACTION_ALLOWLIST: Partial<Record<AdeActionDomain, readonly stri
"listRebaseSuggestions",
"listTemplates",
"listUnregisteredWorktrees",
"linkLinearIssues",
"oauthDecodeState",
"oauthEncodeState",
"oauthGenerateRedirectUris",
Expand Down
Loading
Loading