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
2 changes: 1 addition & 1 deletion apps/ade-cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -239,7 +239,7 @@ ade git push --lane lane-id
ade git branches --lane lane-id --text
ade git user-identity --lane lane-id --text
ade diff patch --lane lane-id --path src/file.ts --text
ade prs create --lane lane-id --base main --title "Fix checkout flow"
ade prs create --lane lane-id --base main --title "Fix checkout flow" --text # prints GitHub + ADE PR URLs
ade prs create --lane lane-id --base main --close-linear-issue-on-merge
ade prs list-open --text
ade prs github-snapshot --include-external-closed
Expand Down
45 changes: 44 additions & 1 deletion apps/ade-cli/src/adeRpcServer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -435,7 +435,16 @@ function createRuntime() {
createQueuePrs: vi.fn(async () => ({ groupId: "group-1", prs: [] })),
createIntegrationPr: vi.fn(async () => ({ prId: "pr-int-1", url: "https://github.com/pr/1" })),
draftDescription: vi.fn(async () => ({ title: "Drafted PR", body: "Drafted body" })),
createFromLane: vi.fn(async () => ({ id: "pr-new", laneId: "lane-1", title: "New PR", status: "open" })),
createFromLane: vi.fn(async () => ({
id: "pr-new",
laneId: "lane-1",
repoOwner: "acme",
repoName: "ade",
githubPrNumber: 42,
githubUrl: "https://github.com/acme/ade/pull/42",
title: "New PR",
status: "open",
})),
getPrHealth: vi.fn(async (prId: string) => ({ prId, healthy: true, checks: "pass", reviews: "approved" })),
landQueueNext: vi.fn(async () => ({ landed: true, prId: "pr-1", sha: "def456" })),
getChecks: vi.fn(async () => [
Expand Down Expand Up @@ -4538,6 +4547,10 @@ describe("adeRpcServer", () => {
closeLinearIssueOnMerge: true,
});
expect(created?.isError).toBeUndefined();
expect(created?.structuredContent).toMatchObject({
githubUrl: "https://github.com/acme/ade/pull/42",
adeUrl: "https://ade.app/open?type=pr&repo=acme%2Fade&number=42",
});
expect(fixture.runtime.prService.createFromLane).toHaveBeenCalledWith({
laneId: "lane-1",
baseBranch: "main",
Expand Down Expand Up @@ -4570,6 +4583,36 @@ describe("adeRpcServer", () => {
expect(fixture.runtime.prService.addComment).toHaveBeenCalledWith({ prId: "pr-1", body: "Looks good" });
});

it("synthesizes PR browser links from repo metadata when RPC PR creation omits githubUrl", async () => {
const fixture = createRuntime();
fixture.runtime.prService.createFromLane = vi.fn(async () => ({
id: "pr-new",
laneId: "lane-1",
repoOwner: "acme",
repoName: "ade",
githubPrNumber: 42,
title: "New PR",
status: "open",
})) as any;
const handler = createAdeRpcRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
await initialize(handler, { callerId: "agent-1", role: "agent" });

const created = await callTool(handler, "create_pr_from_lane", {
laneId: "lane-1",
baseBranch: "main",
title: "My PR",
body: "Body text",
draft: true,
closeLinearIssueOnMerge: true,
});

expect(created?.isError).toBeUndefined();
expect(created?.structuredContent).toMatchObject({
githubUrl: "https://github.com/acme/ade/pull/42",
adeUrl: "https://ade.app/open?type=pr&repo=acme%2Fade&number=42",
});
});

it("lists ADE actions across runtime domains", async () => {
const fixture = createRuntime();
const handler = createAdeRpcRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
Expand Down
33 changes: 31 additions & 2 deletions apps/ade-cli/src/adeRpcServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import { runGit } from "../../desktop/src/main/services/git/git";
import { resolvePathWithinRoot } from "../../desktop/src/main/services/shared/utils";
import { getDefaultModelDescriptor } from "../../desktop/src/shared/modelRegistry";
import { buildAdeCliInlineGuidance } from "../../desktop/src/shared/adeCliGuidance";
import { buildDeeplink } from "../../desktop/src/shared/deeplinks";
import {
ADE_AGENT_SKILLS_DIRS_ENV,
getAdeAgentSkillRootsForPrompt,
Expand Down Expand Up @@ -1187,7 +1188,7 @@ const TOOL_SPECS: ToolSpec[] = [
},
{
name: "create_pr_from_lane",
description: "Create a PR from a lane branch. Drafts a title/body from ADE context when omitted.",
description: "Create a PR from a lane branch. Drafts a title/body from ADE context when omitted. Returns GitHub and ADE PR URLs when available.",
inputSchema: {
type: "object",
required: ["laneId"],
Expand Down Expand Up @@ -2387,6 +2388,34 @@ function asBoolean(value: unknown, fallback = false): boolean {
return typeof value === "boolean" ? value : fallback;
}

function asPositiveInteger(value: unknown): number | null {
let parsed = NaN;
if (typeof value === "number") parsed = value;
else if (typeof value === "string") parsed = Number(value);
return Number.isInteger(parsed) && parsed > 0 ? parsed : null;
}

function prLinkUrls(pr: unknown): { githubUrl?: string; adeUrl?: string } {
if (!isRecord(pr)) return {};
const githubUrl = asOptionalTrimmedString(pr.githubUrl);
const repoOwner = asOptionalTrimmedString(pr.repoOwner);
const repoName = asOptionalTrimmedString(pr.repoName);
const prNumber = asPositiveInteger(
pr.githubPrNumber ?? pr.prNumber ?? pr.number,
);
const derivedGithubUrl = repoOwner && repoName && prNumber
? `https://github.com/${repoOwner}/${repoName}/pull/${prNumber}`
: null;
const adeUrl = repoOwner && repoName && prNumber
? buildDeeplink({ kind: "pr", repoOwner, repoName, prNumber })
: null;
const resolvedGithubUrl = githubUrl ?? derivedGithubUrl;
return {
...(resolvedGithubUrl ? { githubUrl: resolvedGithubUrl } : {}),
...(adeUrl ? { adeUrl } : {}),
};
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

function asNumber(value: unknown, fallback: number): number {
return typeof value === "number" && Number.isFinite(value) ? value : fallback;
}
Expand Down Expand Up @@ -6791,7 +6820,7 @@ async function runTool(args: {
...(baseBranch ? { baseBranch } : {}),
...(closeLinearIssueOnMerge ? { closeLinearIssueOnMerge } : {}),
});
return { pr };
return { pr, ...prLinkUrls(pr) };
}

if (name === "pr_update_title") {
Expand Down
11 changes: 10 additions & 1 deletion apps/ade-cli/src/bootstrap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,12 @@ import { getSharedModelPickerStore } from "./services/modelPickerStore";
import type { createAutomationIngressService } from "../../desktop/src/main/services/automations/automationIngressService";
import type { createGithubService } from "../../desktop/src/main/services/github/githubService";
import { createFeedbackReporterService } from "../../desktop/src/main/services/feedback/feedbackReporterService";
import { ADE_AGENT_SKILLS_DIRS_ENV, joinAdeAgentSkillRoots, splitAdeAgentSkillRoots } from "../../desktop/src/shared/agentSkillRoots";
import {
ADE_AGENT_SKILLS_DIRS_ENV,
getAdeAgentSkillRootsForPrompt,
joinAdeAgentSkillRoots,
splitAdeAgentSkillRoots,
} from "../../desktop/src/shared/agentSkillRoots";
import { createUsageTrackingService } from "../../desktop/src/main/services/usage/usageTrackingService";
import { createBudgetCapService } from "../../desktop/src/main/services/usage/budgetCapService";
import { createSessionDeltaService } from "../../desktop/src/main/services/sessions/sessionDeltaService";
Expand Down Expand Up @@ -373,6 +378,10 @@ function createHeadlessAdeCliAgentEnv(baseEnv: NodeJS.ProcessEnv = process.env):
next[ADE_AGENT_SKILLS_DIRS_ENV],
inferAgentSkillsRootForCliEntry(cliEntry),
);
next[ADE_AGENT_SKILLS_DIRS_ENV] = joinAdeAgentSkillRoots(getAdeAgentSkillRootsForPrompt({
env: next,
cwd: process.cwd(),
}));
return next;
}

Expand Down
45 changes: 45 additions & 0 deletions apps/ade-cli/src/cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2175,6 +2175,51 @@ describe("ADE CLI", () => {
});
});

it("summarizes PR create output with GitHub and ADE links", () => {
const connection = {
mode: "headless" as const,
projectRoot: "/tmp/project",
workspaceRoot: "/tmp/project",
socketPath: "/tmp/project/.ade/ade.sock",
request: async () => null,
close: () => {},
};
const summarized = summarizeExecution({
plan: { kind: "execute", label: "PR create", steps: [] },
connection,
values: {
result: {
pr: {
id: "pr-42",
laneId: "lane-1",
repoOwner: "acme",
repoName: "ade",
githubPrNumber: 42,
githubUrl: "https://github.com/acme/ade/pull/42",
title: "Add PR deeplinks",
state: "open",
},
},
},
} as any);

expect(summarized).toMatchObject({
githubUrl: "https://github.com/acme/ade/pull/42",
adeUrl: "https://ade.app/open?type=pr&repo=acme%2Fade&number=42",
});

const text = formatOutput(
summarized,
{ ...baseResolveOpts(), projectRoot: null, workspaceRoot: null, text: true },
"pr-create",
);
expect(text).toContain("ADE pull request created");
expect(text).toContain("GitHub URL");
expect(text).toContain("https://github.com/acme/ade/pull/42");
expect(text).toContain("ADE URL");
expect(text).toContain("https://ade.app/open?type=pr&repo=acme%2Fade&number=42");
});

it("maps lane create Linear issue JSON to the typed RPC tool", () => {
const plan = buildCliPlan([
"lanes",
Expand Down
66 changes: 65 additions & 1 deletion apps/ade-cli/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
CliDeeplinkUsageError,
runDeeplinkCommand,
} from "./commands/deeplinks";
import { buildDeeplink } from "../../desktop/src/shared/deeplinks";
import { resolveMachineAdeLayout } from "./services/projects/machineLayout";
import {
findAdeManagedWorktreeRoot,
Expand Down Expand Up @@ -83,6 +84,7 @@ type FormatterId =
| "files-tree"
| "files-search"
| "prs-list"
| "pr-create"
| "pr-detail"
| "pr-checks"
| "pr-comments"
Expand Down Expand Up @@ -998,7 +1000,7 @@ const HELP_BY_COMMAND: Record<string, string> = {

$ ade prs list --text List PRs known to ADE
$ ade prs list-open --text List every open GitHub PR in the repo, keyed by head branch
$ ade prs create --lane <lane> --base main Open and map a GitHub PR from a lane
$ ade prs create --lane <lane> --base main Open and map a GitHub PR; prints GitHub + ADE URLs
$ ade prs create --lane <lane> --close-linear-issue-on-merge
$ ade prs link --lane <lane> --url <pr-url> Map an existing GitHub PR to a lane
$ ade prs checks <pr> --text Show check status
Expand Down Expand Up @@ -11564,6 +11566,47 @@ function cell(value: unknown, width = 42): string {
return truncateCell(String(value), width);
}

function positiveInteger(value: unknown): number | null {
let parsed = NaN;
if (typeof value === "number") parsed = value;
else if (typeof value === "string") parsed = Number(value);
return Number.isInteger(parsed) && parsed > 0 ? parsed : null;
}

function getPrCreateLinks(value: unknown): {
pr: JsonObject;
githubUrl: string | null;
adeUrl: string | null;
} {
const result = unwrapActionEnvelope(value);
const root = isRecord(result) ? result : {};
const pr = firstRecord(root, ["pr"]) ?? root;
const githubUrl =
asString(root.githubUrl) ??
asString(root.githubPrUrl) ??
asString(pr.githubUrl) ??
asString(pr.url);
const explicitAdeUrl = asString(root.adeUrl) ?? asString(root.adePrUrl);
const repoOwner = asString(pr.repoOwner);
const repoName = asString(pr.repoName);
const prNumber = positiveInteger(pr.githubPrNumber ?? pr.prNumber ?? pr.number);
const derivedAdeUrl = repoOwner && repoName && prNumber
? buildDeeplink({ kind: "pr", repoOwner, repoName, prNumber })
: null;
return { pr, githubUrl, adeUrl: explicitAdeUrl ?? derivedAdeUrl };
}

function summarizePrCreateResult(value: unknown): unknown {
const result = unwrapActionEnvelope(value);
if (!isRecord(result)) return result;
const { githubUrl, adeUrl } = getPrCreateLinks(result);
return {
...result,
...(githubUrl ? { githubUrl } : {}),
...(adeUrl ? { adeUrl } : {}),
};
}

function formatAutomationRunDetail(value: unknown): string {
if (!isRecord(value)) return JSON.stringify(value, null, 2);
const run = isRecord(value.run) ? value.run : value;
Expand Down Expand Up @@ -11768,6 +11811,20 @@ function formatPrList(value: unknown): string {
);
}

function formatPrCreate(value: unknown): string {
const { pr, githubUrl, adeUrl } = getPrCreateLinks(value);
const prNumber = positiveInteger(pr.githubPrNumber ?? pr.prNumber ?? pr.number);
return renderKeyValues("ADE pull request created", [
["id", pr.id],
["number", prNumber ? `#${prNumber}` : null],
["title", pr.title],
["state", pr.state ?? pr.status],
["lane", pr.laneId],
["GitHub URL", githubUrl],
["ADE URL", adeUrl],
]);
}

function formatPrChecks(value: unknown): string {
const checks = firstArray(value, ["checks", "items"]);
const summary = isRecord(value) ? value.summary : null;
Expand Down Expand Up @@ -12895,6 +12952,8 @@ function formatTextOutput(
return formatFilesSearch(value);
case "prs-list":
return formatPrList(value);
case "pr-create":
return formatPrCreate(value);
case "pr-detail":
return renderKeyValues(
"ADE pull request",
Expand Down Expand Up @@ -12999,6 +13058,7 @@ function inferFormatter(
if (label === "file search" || label === "file quick-open")
return "files-search";
if (label === "pr list" || label === "pr list open") return "prs-list";
if (label === "pr create") return "pr-create";
if (label === "pr detail" || label === "pr health") return "pr-detail";
if (label === "pr checks") return "pr-checks";
if (label === "pr comments") return "pr-comments";
Expand Down Expand Up @@ -13176,6 +13236,10 @@ function summarizeExecution(args: {
};
}

if (plan.label === "PR create") {
return summarizePrCreateResult(values.result ?? values);
}

const result = values.result ?? values;
if (
isRecord(result) &&
Expand Down
10 changes: 7 additions & 3 deletions apps/ade-cli/src/tuiClient/__tests__/commands.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ describe("commands", () => {
}));
});

it("includes Phase 4 Claude parity builtins", () => {
it("includes Claude parity builtins plus provider-agnostic skills", () => {
const rows = paletteCommands("/", [], { provider: "claude" });
for (const name of ["/agents", "/skills", "/init"]) {
expect(rows).toContainEqual(expect.objectContaining({ name, source: "ade" }));
Expand All @@ -155,11 +155,15 @@ describe("commands", () => {
}
});

it("filters Phase 4 Claude-only builtins outside Claude chats", () => {
it("keeps provider-agnostic skills visible outside Claude chats", () => {
const rows = paletteCommands("/", [], { provider: "codex" });
for (const name of ["/agents", "/skills", "/init", "/usage", "/insights", "/fast"]) {
for (const name of ["/agents", "/init", "/usage", "/insights", "/fast"]) {
expect(rows).not.toContainEqual(expect.objectContaining({ name }));
}
expect(rows).toContainEqual(expect.objectContaining({
name: "/skills",
description: "List agent skills from project, user, and ADE bundled roots",
}));
expect(rows).toContainEqual(expect.objectContaining({
name: "/compact",
description: "Compact the active chat context",
Expand Down
Loading
Loading