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
4 changes: 2 additions & 2 deletions apps/ade-cli/src/adeRpcServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -332,7 +332,7 @@ const TOOL_SPECS: ToolSpec[] = [
additionalProperties: false,
properties: {
laneId: { type: "string", minLength: 1 },
provider: { type: "string", enum: ["claude", "codex", "cursor", "droid", "opencode", "pi", "shell"] },
provider: { type: "string", enum: ["claude", "codex", "cursor", "droid", "opencode", "pi", "qwen", "kimi", "grok", "copilot", "shell"] },
permissionMode: { type: "string", enum: ["default", "auto", "plan", "edit", "full-auto", "config-toml"], default: "default" },
title: { type: "string" },
initialInput: { type: "string" },
Expand Down Expand Up @@ -1698,7 +1698,7 @@ function parseCliSessionProvider(value: unknown): LaunchProfile {
if (!isLaunchProfile(provider)) {
throw new JsonRpcError(
JsonRpcErrorCode.invalidParams,
"provider must be one of claude, codex, cursor, droid, opencode, pi, or shell",
"provider must be one of claude, codex, cursor, droid, opencode, pi, qwen, kimi, grok, copilot, or shell",
);
}
return provider;
Expand Down
24 changes: 23 additions & 1 deletion apps/ade-cli/src/cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -992,6 +992,8 @@ describe("ADE CLI", () => {
ADE_PACKAGE_CHANNEL: process.env.ADE_PACKAGE_CHANNEL,
ADE_SYNC_HOST_LOCK_PATH: process.env.ADE_SYNC_HOST_LOCK_PATH,
ADE_SYNC_HOST_SINGLETON_TEST_MODE: process.env.ADE_SYNC_HOST_SINGLETON_TEST_MODE,
ADE_DISABLE_RUNTIME_SERVICE_INSTALL: process.env.ADE_DISABLE_RUNTIME_SERVICE_INSTALL,
ADE_DISABLE_TOOLS_FETCH: process.env.ADE_DISABLE_TOOLS_FETCH,
};
const ownerProcess = spawn(process.execPath, ["-e", "setInterval(() => {}, 1000);"], {
stdio: "ignore",
Expand All @@ -1008,6 +1010,8 @@ describe("ADE CLI", () => {
delete process.env.ADE_PACKAGE_CHANNEL;
process.env.ADE_SYNC_HOST_LOCK_PATH = lockPath;
process.env.ADE_SYNC_HOST_SINGLETON_TEST_MODE = "1";
process.env.ADE_DISABLE_RUNTIME_SERVICE_INSTALL = "1";
process.env.ADE_DISABLE_TOOLS_FETCH = "1";
writeSyncHostSingletonLock({
lockPath,
pid: ownerProcess.pid,
Expand All @@ -1031,6 +1035,10 @@ describe("ADE CLI", () => {
else process.env.ADE_SYNC_HOST_LOCK_PATH = originalEnv.ADE_SYNC_HOST_LOCK_PATH;
if (originalEnv.ADE_SYNC_HOST_SINGLETON_TEST_MODE === undefined) delete process.env.ADE_SYNC_HOST_SINGLETON_TEST_MODE;
else process.env.ADE_SYNC_HOST_SINGLETON_TEST_MODE = originalEnv.ADE_SYNC_HOST_SINGLETON_TEST_MODE;
if (originalEnv.ADE_DISABLE_RUNTIME_SERVICE_INSTALL === undefined) delete process.env.ADE_DISABLE_RUNTIME_SERVICE_INSTALL;
else process.env.ADE_DISABLE_RUNTIME_SERVICE_INSTALL = originalEnv.ADE_DISABLE_RUNTIME_SERVICE_INSTALL;
if (originalEnv.ADE_DISABLE_TOOLS_FETCH === undefined) delete process.env.ADE_DISABLE_TOOLS_FETCH;
else process.env.ADE_DISABLE_TOOLS_FETCH = originalEnv.ADE_DISABLE_TOOLS_FETCH;
ownerProcess.kill("SIGKILL");
fs.rmSync(adeHome, { recursive: true, force: true });
}
Expand Down Expand Up @@ -3411,7 +3419,21 @@ describe("ADE CLI", () => {
"--provider",
"mystery",
]),
).toThrow(/Provider must be claude, codex, cursor, droid, opencode, pi, or shell/);
).toThrow(/Provider must be claude, codex, cursor, droid, opencode, pi, qwen, kimi, grok, copilot, or shell/);
});

it("accepts ACP providers for new chat", () => {
for (const provider of ["qwen", "kimi", "grok", "copilot"] as const) {
const plan = buildCliPlan([
"new",
"chat",
"--lane",
"lane-1",
"--provider",
provider,
]);
expect(plan.kind).toBe("execute");
}
});

it("does not treat new --mode values as subcommands", () => {
Expand Down
12 changes: 6 additions & 6 deletions apps/ade-cli/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1763,7 +1763,7 @@ const HELP_BY_COMMAND: Record<string, string> = {
--type <subagent|peer> Required for a parented agent spawn. Use subagent
whenever you will need, join, or review the result;
use peer only for fire-and-forget work.
--provider <name> claude | codex | cursor | droid | opencode | pi. CLI mode also accepts shell.
--provider <name> claude | codex | cursor | droid | opencode | pi | qwen | kimi | grok | copilot. CLI mode also accepts shell.
--model <id> Runtime model id.
--reasoning-effort <v> Reasoning tier. Alias: --effort.
--permissions <mode> default | auto | plan | edit | full-auto | config-toml.
Expand Down Expand Up @@ -2166,7 +2166,7 @@ const HELP_BY_COMMAND: Record<string, string> = {
Start a tracked provider CLI session

Create flags:
--provider <name> claude | codex | cursor | droid | opencode | pi.
--provider <name> claude | codex | cursor | droid | opencode | pi | qwen | kimi | grok | copilot.
--model <id> Model id, also sent as modelId for runtime parity.
--reasoning-effort <v> Reasoning tier when the selected model supports it.
Common tiers: minimal, low, medium, high, xhigh, ultra, ultracode.
Expand Down Expand Up @@ -2226,7 +2226,7 @@ const HELP_BY_COMMAND: Record<string, string> = {
Flags:
--personal Use machine-owned chats instead of a project/lane chat.
--lane <lane> Lane/worktree for the chat.
--provider <name> claude | codex | cursor | droid | opencode | pi.
--provider <name> claude | codex | cursor | droid | opencode | pi | qwen | kimi | grok | copilot.
--model <id> Model id, also sent as modelId for runtime parity.
--reasoning-effort <v> Reasoning tier when supported by the model.
--effort <v> Alias for --reasoning-effort.
Expand Down Expand Up @@ -5006,10 +5006,10 @@ function buildNewChatPlan(args: string[], defaultMode: "chat" | "cli"): CliPlan
const printConfig = readFlag(args, ["--print-config", "--dry-run"]);

if (!isLaunchProfile(provider)) {
throw new CliUsageError("Provider must be claude, codex, cursor, droid, opencode, pi, or shell.");
throw new CliUsageError("Provider must be claude, codex, cursor, droid, opencode, pi, qwen, kimi, grok, copilot, or shell.");
}
if (mode === "chat" && provider === "shell") {
throw new CliUsageError("Chat mode provider must be claude, codex, cursor, droid, opencode, or pi.");
throw new CliUsageError("Chat mode provider must be claude, codex, cursor, droid, opencode, pi, qwen, kimi, grok, or copilot.");
}
if (mode === "cli") {
const effectivePermissionMode = permissionMode ?? "default";
Expand Down Expand Up @@ -6973,7 +6973,7 @@ function buildCliSessionStartPlan(
);
if (!isLaunchProfile(rawProvider)) {
throw new CliUsageError(
"provider must be one of claude, codex, cursor, droid, opencode, pi, or shell.",
"provider must be one of claude, codex, cursor, droid, opencode, pi, qwen, kimi, grok, copilot, or shell.",
);
}
const provider: LaunchProfile = rawProvider;
Expand Down
67 changes: 67 additions & 0 deletions apps/ade-cli/src/services/agentRegistry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,73 @@ export const AGENT_CLI_REGISTRY: AgentCliDescriptor[] = [
/\bfactory(?:_api_key| api key)\b.*\b(invalid|missing|not found|not set|required|unauthorized|must be set)\b/i,
],
},
{
agent: "qwen",
displayName: "Qwen Code",
binaryNames: ["qwen"],
installCommand: npmGlobalInstallCommand("@qwen-code/qwen-code"),
// 0.22.3 removed `qwen auth`. Sign-in is OPENAI_API_KEY / `--auth-type=openai`.
authCommand: "qwen --auth-type=openai",
missingErrorPatterns: [
/\bqwen\b.*\b(command not found|not recognized|not found|enoent)\b/i,
/\bspawn\s+qwen\s+enoent\b/i,
],
notAuthErrorPatterns: [
/\bqwen\b.*\b(not logged in|not authenticated|unauthorized|authentication failed|login required|no api key|api key required|no credentials)\b/i,
/\b(?:dashscope|openai)[_ ]api[_ ]key\b.*\b(invalid|missing|not found|not set|required|unauthorized|must be set)\b/i,
],
},
{
agent: "kimi",
displayName: "Kimi Code",
binaryNames: ["kimi"],
// Kimi ships a native binary rather than an npm package, so there is no
// portable one-liner to print here. Point at the vendor's own installer
// instead of guessing a package name that would fail on paste.
installCommand: "curl -LsSf https://code.kimi.com/kimi-code/install.sh | bash",
authCommand: "kimi login",
missingErrorPatterns: [
/\bkimi\b.*\b(command not found|not recognized|not found|enoent)\b/i,
/\bspawn\s+kimi\s+enoent\b/i,
],
notAuthErrorPatterns: [
/\bkimi\b.*\b(not logged in|not authenticated|unauthorized|authentication failed|login required|no api key|api key required|no credentials)\b/i,
/\brun\s+[`'"]?kimi\s+login[`'"]?/i,
/\bmoonshot[_ ]api[_ ]key\b.*\b(invalid|missing|not found|not set|required|unauthorized|must be set)\b/i,
],
},
{
agent: "grok",
displayName: "Grok CLI",
binaryNames: ["grok"],
installCommand: npmGlobalInstallCommand("@xai-official/grok"),
authCommand: "grok login",
missingErrorPatterns: [
/\bgrok\b.*\b(command not found|not recognized|not found|enoent)\b/i,
/\bspawn\s+grok\s+enoent\b/i,
],
notAuthErrorPatterns: [
/\bgrok\b.*\b(not logged in|not authenticated|unauthorized|authentication failed|login required|no api key|api key required|no credentials)\b/i,
/\brun\s+[`'"]?grok\s+login[`'"]?/i,
/\bxai[_ ]api[_ ]key\b.*\b(invalid|missing|not found|not set|required|unauthorized|must be set)\b/i,
],
},
{
agent: "copilot",
displayName: "GitHub Copilot CLI",
binaryNames: ["copilot"],
installCommand: npmGlobalInstallCommand("@github/copilot"),
authCommand: "copilot login",
missingErrorPatterns: [
/\bcopilot\b.*\b(command not found|not recognized|not found|enoent)\b/i,
/\bspawn\s+copilot\s+enoent\b/i,
],
notAuthErrorPatterns: [
/\bcopilot\b.*\b(not logged in|not authenticated|unauthorized|authentication failed|login required|no credentials)\b/i,
/\brun\s+[`'"]?copilot\s+login[`'"]?/i,
/\bgh[_ ]token\b.*\b(invalid|missing|not found|not set|required|unauthorized|must be set)\b/i,
],
},
];

function descriptorMatchesPreferred(descriptor: AgentCliDescriptor, preferredAgent: string | null | undefined): boolean {
Expand Down
10 changes: 10 additions & 0 deletions apps/ade-cli/src/services/push/attentionItemBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,16 @@ export function providerDisplayName(provider: string | null | undefined): string
return "OpenCode";
case "gemini":
return "Gemini";
case "qwen":
return "Qwen";
case "kimi":
return "Kimi";
case "grok":
return "Grok";
case "copilot":
// The default arm would title-case this to "Copilot"; the product name
// carries the vendor.
return "GitHub Copilot";
default:
return provider.charAt(0).toUpperCase() + provider.slice(1);
}
Expand Down
43 changes: 32 additions & 11 deletions apps/ade-cli/src/services/sync/syncRemoteCommandService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3141,22 +3141,31 @@ function parseChatModelsArgs(value: Record<string, unknown>): {
};
}

/**
* Providers a remote caller may ask the host to re-enumerate. Typed as the
* shared union so a provider added there is accepted here too, instead of being
* silently dropped from the request and leaving the phone's refresh a no-op.
*/
const MODEL_CATALOG_REFRESH_PROVIDERS = new Set<AgentChatModelCatalogRefreshProvider>([
"opencode",
"pi",
"cursor",
"droid",
"lmstudio",
"ollama",
"qwen",
"kimi",
"grok",
"copilot",
]);

function parseChatModelCatalogArgs(value: Record<string, unknown>): AgentChatModelCatalogArgs {
const mode = asTrimmedString(value.mode) as AgentChatModelCatalogMode | null;
const refreshProvider = asTrimmedString(value.refreshProvider) as AgentChatModelCatalogRefreshProvider | null;
const cursorSource = parseCursorModelSource(value.cursorSource);
return {
...(mode === "cached" || mode === "refresh-stale" || mode === "force" ? { mode } : {}),
...(
refreshProvider === "opencode"
|| refreshProvider === "pi"
|| refreshProvider === "cursor"
|| refreshProvider === "droid"
|| refreshProvider === "lmstudio"
|| refreshProvider === "ollama"
? { refreshProvider }
: {}
),
...(refreshProvider && MODEL_CATALOG_REFRESH_PROVIDERS.has(refreshProvider) ? { refreshProvider } : {}),
...(cursorSource ? { cursorSource } : {}),
};
}
Expand Down Expand Up @@ -3661,7 +3670,19 @@ async function resolveChatCreateArgs<T extends AgentChatCreateArgs>(
if (payload.model.trim().length > 0) return payload;
const available = await service.getAvailableModels({
provider: payload.provider,
...(payload.provider === "opencode" || payload.provider === "pi" ? { activateRuntime: true } : {}),
// ACP providers are here for the same reason as OpenCode/Pi: their model
// rows are gated on a CLI auth pass, and `activateRuntime` refreshes that
// pass. It does not spawn an agent for them.
...(
payload.provider === "opencode"
|| payload.provider === "pi"
|| payload.provider === "qwen"
|| payload.provider === "kimi"
|| payload.provider === "grok"
|| payload.provider === "copilot"
? { activateRuntime: true }
: {}
),
});
const chosen = available[0];
if (!chosen) {
Expand Down
29 changes: 27 additions & 2 deletions apps/ade-cli/src/tuiClient/adeApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -423,6 +423,10 @@ const CHAT_BACKED_TERMINAL_TOOL_TYPES = new Set([
"cursor",
"droid-chat",
"pi-chat",
"qwen-chat",
"kimi-chat",
"grok-chat",
"copilot-chat",
]);

const TRACKED_CLI_PROVIDERS = new Set<AdeCodeProvider>([
Expand All @@ -432,6 +436,10 @@ const TRACKED_CLI_PROVIDERS = new Set<AdeCodeProvider>([
"droid",
"opencode",
"pi",
"qwen",
"kimi",
"grok",
"copilot",
]);

/**
Expand All @@ -454,6 +462,10 @@ export function trackedCliTerminalProvider(session: ChatTerminalSession): AdeCod
if (toolType.startsWith("opencode")) return "opencode";
if (toolType.startsWith("pi")) return "pi";
if (toolType.startsWith("claude")) return "claude";
if (toolType.startsWith("qwen")) return "qwen";
if (toolType.startsWith("kimi")) return "kimi";
if (toolType.startsWith("grok")) return "grok";
if (toolType.startsWith("copilot")) return "copilot";
const resumeCommand = typeof session.resumeCommand === "string" ? session.resumeCommand.trim().toLowerCase() : "";
return resumeCommand && /\bclaude\b/.test(resumeCommand) ? "claude" : null;
}
Expand Down Expand Up @@ -508,7 +520,10 @@ export async function signalTerminal(
}

/** Provider CLIs the TUI can launch as tracked terminal sessions. */
export type CliTerminalProvider = Extract<AdeCodeProvider, "claude" | "codex" | "cursor" | "droid" | "opencode" | "pi">;
export type CliTerminalProvider = Extract<
AdeCodeProvider,
"claude" | "codex" | "cursor" | "droid" | "opencode" | "pi" | "qwen" | "kimi" | "grok" | "copilot"
>;

export type StartCliTerminalSessionResult = {
provider: string;
Expand Down Expand Up @@ -739,7 +754,17 @@ export async function getAvailableModels(
// IDs such as `claude-opus-4-6-fast`, not a separate service-tier toggle.
// Codex is intentionally NOT here: its tiers come from the app-server, which
// loadAvailableModels always queries regardless of activateRuntime.
activateRuntime: provider === "cursor" || provider === "droid" || provider === "pi",
// The four ACP providers are here too, but they never spawn an agent for a
// model list: `activateRuntime` only forces a fresh CLI auth pass, which is
// what gates their curated rows (see loadAvailableModels in
// agentChatService).
activateRuntime: provider === "cursor"
|| provider === "droid"
|| provider === "pi"
|| provider === "qwen"
|| provider === "kimi"
|| provider === "grok"
|| provider === "copilot",
...(provider === "cursor" ? { cursorSource } : {}),
});
}
Expand Down
5 changes: 5 additions & 0 deletions apps/ade-cli/src/tuiClient/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2642,6 +2642,11 @@ function loginCommandsForProvider(provider: AdeCodeProvider): ProviderLoginComma
if (provider === "codex") return [{ command: "codex", args: ["login"], label: "codex login" }];
if (provider === "opencode") return [{ command: "opencode", args: ["auth", "login"], label: "opencode auth login" }];
if (provider === "pi") return [{ command: "pi", args: [], label: "pi (then /login)" }];
// 0.22.3 removed `qwen auth`. Sign-in is the OpenAI-compatible key path.
if (provider === "qwen") return [{ command: "qwen", args: ["--auth-type=openai"], label: "qwen --auth-type=openai" }];
if (provider === "kimi") return [{ command: "kimi", args: ["login"], label: "kimi login" }];
if (provider === "grok") return [{ command: "grok", args: ["login"], label: "grok login" }];
if (provider === "copilot") return [{ command: "copilot", args: ["login"], label: "copilot login" }];
return [];
}

Expand Down
14 changes: 10 additions & 4 deletions apps/ade-cli/src/tuiClient/closedCliSessions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ export function terminalSessionResumeProvider(session: ChatTerminalSession | nul
if (toolType.startsWith("opencode")) return "opencode";
if (toolType.startsWith("pi")) return "pi";
if (toolType.startsWith("claude")) return "claude";
if (toolType.startsWith("qwen")) return "qwen";
if (toolType.startsWith("kimi")) return "kimi";
if (toolType.startsWith("grok")) return "grok";
if (toolType.startsWith("copilot")) return "copilot";
return null;
}

Expand All @@ -50,12 +54,14 @@ export function isTerminalSessionResumable(session: ChatTerminalSession | null |
);
}

/** Narrow a terminal session's derived provider to an AgentChatProvider (CLI terminals are always one of the five). */
/**
* Narrow a terminal session's derived provider to an AgentChatProvider. Every
* `AdeCodeProvider` except the two OpenCode-backed local runtimes (Ollama,
* LM Studio) is one, and those two never back a tracked CLI terminal.
*/
function terminalSummaryProvider(session: ChatTerminalSession): AgentChatSessionSummary["provider"] {
const provider = terminalSessionProvider(session);
return provider === "codex" || provider === "claude" || provider === "opencode" || provider === "cursor" || provider === "droid" || provider === "pi"
? provider
: "claude";
return provider && provider !== "ollama" && provider !== "lmstudio" ? provider : "claude";
}

export function terminalSessionToChatSummary(
Expand Down
Loading
Loading