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
13 changes: 13 additions & 0 deletions apps/ade-cli/src/adeRpcServer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -495,6 +495,14 @@ function createRuntime() {
lastActivityAt: "2026-03-17T19:00:00.000Z",
createdAt: "2026-03-17T19:00:00.000Z",
})),
getTurnStatus: vi.fn(async (sessionId: string) => ({
sessionId,
phase: "idle",
activity: "idle",
live: false,
hasAsk: false,
queued: 0,
})),
createScheduledWork: vi.fn(async ({ sessionId, cron, runAt, prompt, recurring = true }: {
sessionId: string;
cron?: string;
Expand Down Expand Up @@ -3480,6 +3488,11 @@ describe("adeRpcServer", () => {
expect(getSessionSummary).toMatchObject({
input: expect.stringContaining("scalar sessionId"),
});
const getTurnStatus = chatActions.structuredContent.actions.find((entry: { action: string }) => entry.action === "getTurnStatus");
expect(getTurnStatus).toMatchObject({
input: expect.stringContaining("scalar sessionId"),
example: expect.stringContaining("chat.getTurnStatus"),
});
const smartLinkPreview = chatActions.structuredContent.actions.find(
(entry: { action: string }) => entry.action === "resolveSmartLinkPreview",
);
Expand Down
26 changes: 24 additions & 2 deletions apps/ade-cli/src/cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4909,7 +4909,7 @@ describe("ADE CLI", () => {
expect(text).toContain("assistant 2026-06-29T12:00:01.000Z");
});

it("builds chat show/status as positional session summary calls", () => {
it("builds chat show as a session summary and chat status as turn status", () => {
const show = buildCliPlan(["chat", "show", "chat-1"]);
expect(show.kind).toBe("execute");
if (show.kind !== "execute") return;
Expand All @@ -4925,14 +4925,36 @@ describe("ADE CLI", () => {
const status = buildCliPlan(["chat", "status", "--session-id", "chat-2"]);
expect(status.kind).toBe("execute");
if (status.kind !== "execute") return;
expect(status.formatter).toBe("chat-status");
expect(status.steps[0]?.params).toEqual({
name: "run_ade_action",
arguments: {
domain: "chat",
action: "getSessionSummary",
action: "getTurnStatus",
argsList: ["chat-2"],
},
});
expect(status.exitCodeFromResult?.({ phase: "running", sessionId: "chat-2" })).toBe(0);
expect(status.exitCodeFromResult?.({ phase: "idle", sessionId: "chat-2" })).toBe(1);
expect(status.exitCodeFromResult?.({ phase: "blocked", sessionId: "chat-2" })).toBe(2);
});

it("formats chat turn status as a RUNNING/BLOCKED/IDLE tree", () => {
const text = formatOutput(
{
sessionId: "chat-1",
phase: "running",
turnElapsedMs: 12_000,
lastActivityMsAgo: 1_000,
queuedMessageCount: 0,
currentTool: { name: "Bash" },
subagents: [],
},
{ ...baseResolveOpts(), projectRoot: null, workspaceRoot: null, text: true },
"chat-status",
);
expect(text).toContain("RUNNING");
expect(text).toContain("Bash");
});

it("maps chat list filters to the listSessions action", () => {
Expand Down
48 changes: 45 additions & 3 deletions apps/ade-cli/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,12 @@ import {
type AgentChatDroidPermissionMode,
} from "../../desktop/src/shared/types/chat";
import type { AgentChatDispatchSteerMode } from "../../desktop/src/shared/types/chat";
import {
chatTurnStatusExitCode,
formatChatTurnStatus,
type ChatTurnStatusPhase,
type ChatTurnStatusSnapshot,
} from "../../desktop/src/shared/chatTurnStatus";
import type { TerminalSessionSummary } from "../../desktop/src/shared/types/sessions";
import {
formatWorkingDuration,
Expand Down Expand Up @@ -328,6 +334,7 @@ type FormatterId =
| "pr-comments"
| "chat-list"
| "chat-read"
| "chat-status"
| "session-lifecycle"
| "lane-drift"
| "scheduled-work-create"
Expand Down Expand Up @@ -2097,14 +2104,17 @@ const HELP_BY_COMMAND: Record<string, string> = {
already carries. Add --arg strictMcpConfig=false to also load the
user's own MCP config (personal chats withhold it by default), or
--arg strictMcpConfig=true to withhold it on a project chat.
Read mcpCapability on the created session (ade chat status
Read mcpCapability on the created session (ade chat show
<session> --personal --json) for what the provider could honor:
only level "enforced" means the caller's servers are the whole
surface. A server the provider cannot carry fails the create.
$ ade chat create --lane <lane> --provider claude --model anthropic/claude-opus-5 --no-parent --prompt "fix the tests"
$ ade chat create --from-linear-issue ENG-431 --parent <session> --type subagent
Start a child chat with an attached issue + kickoff (alias: --linear-issue-json)
$ ade chat send <session> --text "next step" Send a message; steers automatically if the turn is active
$ ade chat show <session> Session summary (title, provider, model)
$ ade chat status <session> Live turn status: RUNNING / BLOCKED / IDLE
Exit 0 running, 1 idle, 2 blocked. Use --text.
$ ade chat note "testing desktop auth fallback" # Update the Work status line (aim for ${STATUS_NOTE_GUIDELINE_WORDS} words or fewer; truncated past ${MAX_STATUS_NOTE_CHARACTERS} characters)
$ ade chat ask "Which account should I use?" Escalate a blocking question to the user
'note' and 'ask' default to the caller and accept --session <id>.
Expand Down Expand Up @@ -7633,16 +7643,36 @@ function buildChatPlan(args: string[]): CliPlan {
],
};
}
if (sub === "show" || sub === "status")
if (sub === "show")
return {
kind: "execute",
label: "chat status",
label: "chat show",
steps: [
actionArgsListStep("result", "chat", "getSessionSummary", [
requireValue(sessionId, "sessionId"),
]),
],
};
if (sub === "status")
return {
kind: "execute",
label: "chat status",
formatter: "chat-status",
steps: [
actionArgsListStep("result", "chat", "getTurnStatus", [
requireValue(sessionId, "sessionId"),
]),
],
exitCodeFromResult: (result) => {
const record = firstRecord(result, ["result", "status"])
?? (isRecord(result) ? result : {});
const phase = asString(record.phase) as ChatTurnStatusPhase | undefined;
if (phase === "running" || phase === "idle" || phase === "blocked") {
return chatTurnStatusExitCode(phase);
}
return 1;
},
};
if (sub === "read" || sub === "messages" || sub === "transcript") {
const targetSession = requireValue(sessionId, "sessionId");
const limit = readIntOption(args, ["--limit"], 50);
Expand Down Expand Up @@ -20570,6 +20600,15 @@ function formatExternalSessions(value: unknown): string {
);
}

function formatChatStatus(value: unknown): string {
const record = firstRecord(value, ["result", "status"])
?? (isRecord(value) ? value : null);
if (!record || typeof record.sessionId !== "string" || typeof record.phase !== "string") {
return "ADE chat status\n(no session)";
}
return formatChatTurnStatus(record as ChatTurnStatusSnapshot);
}

function formatChatList(value: unknown): string {
const sessions = firstArray(value, ["sessions", "chats", "items"]);
return renderTable(
Expand Down Expand Up @@ -22000,6 +22039,8 @@ function formatTextOutput(
return formatPrComments(value);
case "chat-list":
return formatChatList(value);
case "chat-status":
return formatChatStatus(value);
case "chat-read":
return formatChatRead(value);
case "session-lifecycle":
Expand Down Expand Up @@ -22149,6 +22190,7 @@ function inferFormatter(
if (label === "pr checks") return "pr-checks";
if (label === "pr comments") return "pr-comments";
if (label === "chat list") return "chat-list";
if (label === "chat status") return "chat-status";
if (label === "test runs") return "tests-runs";
if (label === "proof list") return "proof-list";
if (label === "ios simulator status") return "ios-sim-status";
Expand Down
19 changes: 19 additions & 0 deletions apps/ade-cli/src/tuiClient/__tests__/adeApi.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1061,6 +1061,25 @@ describe("discoverProjectSlashCommands", () => {
]));
});

it("hides Claude terminal-only slash commands from the TUI catalog", () => {
const projectRoot = makeTmpRoot("ade-code-terminal-slash-");
const commandsDir = path.join(projectRoot, ".claude", "commands");
fs.mkdirSync(commandsDir, { recursive: true });
fs.writeFileSync(path.join(commandsDir, "exit.md"), "Exit the CLI.\n");
fs.writeFileSync(path.join(commandsDir, "quit.md"), "Quit.\n");
fs.writeFileSync(path.join(commandsDir, "statusline.md"), "Status line.\n");
fs.writeFileSync(path.join(commandsDir, "compact.md"), "Compact.\n");

const commands = discoverProjectSlashCommands(projectRoot);
const names = commands.map((command) => command.name.toLowerCase());
expect(names).not.toContain("/exit");
expect(names).not.toContain("/quit");
expect(names).not.toContain("/statusline");
expect(commands).toEqual(expect.arrayContaining([
expect.objectContaining({ name: "/compact" }),
]));
});

it("includes Cursor command files and subagents", () => {
const projectRoot = makeTmpRoot("ade-code-cursor-command-");
const commandsDir = path.join(projectRoot, ".cursor", "commands");
Expand Down
12 changes: 12 additions & 0 deletions apps/desktop/src/main/services/adeActions/registry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -605,6 +605,10 @@
expect(getAdeActionInputContract("chat", "getSessionSummary")).toMatchObject({
input: expect.stringContaining("scalar sessionId"),
});
expect(getAdeActionInputContract("chat", "getTurnStatus")).toMatchObject({
description: expect.stringContaining("RUNNING"),
input: expect.stringContaining("scalar sessionId"),
});
expect(getAdeActionInputContract("chat", "readTranscript")).toMatchObject({
input: expect.stringContaining("limit"),
});
Expand Down Expand Up @@ -765,6 +769,7 @@
const createSession = vi.fn(async (args?: unknown) => ({ sessionId: "chat-new", args }));
const getAvailableModels = vi.fn(async (args: { provider?: string }) => [{ id: args.provider ?? "any" }]);
const getSessionSummary = vi.fn(async (sessionId: string) => ({ sessionId }));
const getTurnStatus = vi.fn(async (sessionId: string) => ({ sessionId, phase: "idle" }));
const readTranscript = vi.fn(async (sessionId: string, limit?: number, since?: string) => ([
{ role: "user", text: sessionId, timestamp: since ?? "now", limit },
]));
Expand All @@ -783,6 +788,7 @@
createSession,
getAvailableModels,
getSessionSummary,
getTurnStatus,
readTranscript,
getChatEventHistory,
getChatEventHistoryPage,
Expand All @@ -795,6 +801,7 @@
createSession?: (args?: unknown) => Promise<unknown>;
getAvailableModels?: (args?: unknown) => Promise<unknown>;
getSessionSummary?: (args?: unknown) => Promise<unknown>;
getTurnStatus?: (args?: unknown) => Promise<unknown>;
readTranscript?: (args?: unknown) => Promise<unknown>;
getChatEventHistory?: (args?: unknown, options?: unknown) => Promise<unknown>;
getChatEventHistoryPage?: (args?: unknown, options?: unknown) => Promise<unknown>;
Expand All @@ -810,6 +817,11 @@
expect(getSessionSummary).toHaveBeenNthCalledWith(1, "chat-1");
expect(getSessionSummary).toHaveBeenNthCalledWith(2, "chat-2");

await expect(chat.getTurnStatus?.({ sessionId: " chat-1 " })).resolves.toEqual({ sessionId: "chat-1", phase: "idle" });
await expect(chat.getTurnStatus?.("chat-2")).resolves.toEqual({ sessionId: "chat-2", phase: "idle" });
expect(getTurnStatus).toHaveBeenNthCalledWith(1, "chat-1");
expect(getTurnStatus).toHaveBeenNthCalledWith(2, "chat-2");

await expect(chat.createSession?.({
laneId: "lane-1",
provider: "codex",
Expand Down Expand Up @@ -2052,7 +2064,7 @@

it("does not pretend a native CLI prompt was dismissed while its process is still blocked", async () => {
const settleSession = vi.fn(() => true);
const settleSessionReportingAbort = vi.fn(() => ({ found: true, settled: true }));

Check warning on line 2067 in apps/desktop/src/main/services/adeActions/registry.test.ts

View workflow job for this annotation

GitHub Actions / lint-desktop

'settleSessionReportingAbort' is assigned a value but never used. Allowed unused vars must match /^_/u
const setSessionRuntimeState = vi.fn(() => true);
const runtime = {
sessionService: {
Expand Down
10 changes: 10 additions & 0 deletions apps/desktop/src/main/services/adeActions/registry.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import fs from "node:fs";
import path from "node:path";

Check warning on line 2 in apps/desktop/src/main/services/adeActions/registry.ts

View workflow job for this annotation

GitHub Actions / lint-desktop

'path' is defined but never used. Allowed unused vars must match /^_/u
import { randomUUID } from "node:crypto";
import type { AdeRuntime } from "../../../../../ade-cli/src/bootstrap";
import {
Expand Down Expand Up @@ -603,6 +603,7 @@
"listClaudeOutputStyles",
"getSessionCapabilities",
"getSessionSummary",
"getTurnStatus",
"getSlashCommands",
"getTurnFileDiff",
"getParallelLaunchState",
Expand Down Expand Up @@ -1117,6 +1118,11 @@
input: "scalar sessionId string, positional argsList [sessionId], or object { sessionId }",
example: "ade actions run chat.getSessionSummary --scalar chat-123",
},
getTurnStatus: {
description: "Read live turn status for one chat: RUNNING, BLOCKED, or IDLE.",
input: "scalar sessionId string, positional argsList [sessionId], or object { sessionId }",
example: "ade actions run chat.getTurnStatus --scalar chat-123",
},
createScheduledWork: {
description: "Create durable scheduled work for an eligible chat or tracked provider CLI session. Use delaySeconds or runAt for one-shot wakeups; five-field cron uses the ADE brain machine's local timezone.",
input: "object { sessionId?: string, prompt: string, exactly one of cron?: string | runAt?: ISO 8601 string with offset/Z | delaySeconds?: positive integer, recurring?: boolean, reason?: string }",
Expand Down Expand Up @@ -1896,6 +1902,10 @@
service.getSessionSummary = (args?: unknown) =>
agentChatService.getSessionSummary(readStringActionArg(args, "sessionId"));
}
if (typeof base.getTurnStatus === "function") {
service.getTurnStatus = (args?: unknown) =>
agentChatService.getTurnStatus(readStringActionArg(args, "sessionId"));
}
if (typeof base.listScheduledWork === "function") {
service.listScheduledWork = (args?: unknown) => {
const record = args === undefined
Expand Down
Loading
Loading