feat(rpc): expose slash command metadata
This commit is contained in:
@@ -56,7 +56,7 @@ import {
|
||||
} from "../../extensibility/extensions";
|
||||
import { runExtensionCompact } from "../../extensibility/extensions/compact-handler";
|
||||
import { getSessionSlashCommands } from "../../extensibility/extensions/get-commands-handler";
|
||||
import { buildSkillPromptMessage, getSkillSlashCommandName } from "../../extensibility/skills";
|
||||
import { buildSkillPromptMessage } from "../../extensibility/skills";
|
||||
import { loadSlashCommands } from "../../extensibility/slash-commands";
|
||||
import { resolveLocalUrlToPath } from "../../internal-urls";
|
||||
import { MCPManager } from "../../mcp/manager";
|
||||
@@ -71,12 +71,8 @@ import {
|
||||
type SessionInfo as StoredSessionInfo,
|
||||
type UsageStatistics,
|
||||
} from "../../session/session-manager";
|
||||
import {
|
||||
ACP_BUILTIN_RESERVED_NAMES,
|
||||
ACP_BUILTIN_SLASH_COMMANDS,
|
||||
executeAcpBuiltinSlashCommand,
|
||||
isAcpBuiltinShadowedName,
|
||||
} from "../../slash-commands/acp-builtins";
|
||||
import { executeAcpBuiltinSlashCommand } from "../../slash-commands/acp-builtins";
|
||||
import { buildAvailableSlashCommands, toAcpAvailableCommands } from "../../slash-commands/available-commands";
|
||||
import { AUTO_THINKING, parseConfiguredThinkingLevel } from "../../thinking";
|
||||
import { normalizeLocalScheme } from "../../tools/path-utils";
|
||||
import { runResolveInvocation } from "../../tools/resolve";
|
||||
@@ -1662,66 +1658,7 @@ export class AcpAgent implements Agent {
|
||||
}
|
||||
|
||||
async #buildAvailableCommands(session: AgentSession): Promise<AvailableCommand[]> {
|
||||
const commands: AvailableCommand[] = [];
|
||||
const seenNames = new Set<string>();
|
||||
const appendCommand = (command: AvailableCommand): void => {
|
||||
if (seenNames.has(command.name)) {
|
||||
return;
|
||||
}
|
||||
seenNames.add(command.name);
|
||||
commands.push(command);
|
||||
};
|
||||
|
||||
// Advertise in the order dispatch resolves them (mirrors AgentSession
|
||||
// dispatch: builtins → skills → extensions → custom TS → file-based).
|
||||
// `appendCommand` dedupes by name so earlier entries win; extension
|
||||
// commands therefore correctly shadow custom TS commands of the same
|
||||
// name, matching the runtime behaviour of #tryExecuteExtensionCommand
|
||||
// running before #tryExecuteCustomCommand.
|
||||
for (const command of ACP_BUILTIN_SLASH_COMMANDS) {
|
||||
appendCommand(command);
|
||||
}
|
||||
|
||||
if (session.skillsSettings?.enableSkillCommands) {
|
||||
for (const skill of session.skills) {
|
||||
appendCommand({
|
||||
name: getSkillSlashCommandName(skill),
|
||||
description: skill.description || `Run ${skill.name} skill`,
|
||||
input: { hint: "arguments" },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
for (const command of session.extensionRunner?.getRegisteredCommands(ACP_BUILTIN_RESERVED_NAMES) ?? []) {
|
||||
// Reserved-set filtering in getRegisteredCommands only covers exact
|
||||
// names; colon-namespaced names whose prefix is a builtin (e.g.
|
||||
// `model:foo`) would still dispatch to the builtin in ACP.
|
||||
if (isAcpBuiltinShadowedName(command.name)) {
|
||||
continue;
|
||||
}
|
||||
appendCommand({
|
||||
name: command.name,
|
||||
description: command.description ?? "(extension command)",
|
||||
input: { hint: "arguments" },
|
||||
});
|
||||
}
|
||||
|
||||
for (const command of session.customCommands) {
|
||||
appendCommand({
|
||||
name: command.command.name,
|
||||
description: command.command.description,
|
||||
input: { hint: "arguments" },
|
||||
});
|
||||
}
|
||||
|
||||
for (const command of await loadSlashCommands({ cwd: session.sessionManager.getCwd() })) {
|
||||
appendCommand({
|
||||
name: command.name,
|
||||
description: command.description,
|
||||
});
|
||||
}
|
||||
|
||||
return commands;
|
||||
return toAcpAvailableCommands(await buildAvailableSlashCommands(session));
|
||||
}
|
||||
|
||||
#toSessionInfo(session: StoredSessionInfo): SessionInfo {
|
||||
|
||||
@@ -13,6 +13,7 @@ import type { FileSink } from "bun";
|
||||
import type { BashResult } from "../../exec/bash-executor";
|
||||
import type { AgentSessionEvent, SessionStats } from "../../session/agent-session";
|
||||
import type {
|
||||
RpcAvailableSlashCommand,
|
||||
RpcCommand,
|
||||
RpcExtensionUIRequest,
|
||||
RpcHandoffResult,
|
||||
@@ -511,6 +512,14 @@ export class RpcClient {
|
||||
return this.#getData<{ models: ModelInfo[] }>(response).models;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get list of available slash commands.
|
||||
*/
|
||||
async getAvailableCommands(): Promise<RpcAvailableSlashCommand[]> {
|
||||
const response = await this.#send({ type: "get_available_commands" });
|
||||
return this.#getData<{ commands: RpcAvailableSlashCommand[] }>(response).commands;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set thinking level.
|
||||
*/
|
||||
|
||||
@@ -21,6 +21,8 @@ import {
|
||||
} from "../../extensibility/extensions";
|
||||
import { type Theme, theme } from "../../modes/theme/theme";
|
||||
import type { AgentSession } from "../../session/agent-session";
|
||||
import { executeAcpBuiltinSlashCommand } from "../../slash-commands/acp-builtins";
|
||||
import { buildAvailableSlashCommands } from "../../slash-commands/available-commands";
|
||||
import type { EventBus } from "../../utils/event-bus";
|
||||
import { initializeExtensions } from "../runtime-init";
|
||||
import { isRpcHostToolResult, isRpcHostToolUpdate, RpcHostToolBridge } from "./host-tools";
|
||||
@@ -511,6 +513,12 @@ export async function runRpcMode(
|
||||
output(event);
|
||||
});
|
||||
|
||||
const getAvailableCommands = async () => buildAvailableSlashCommands(session);
|
||||
const emitAvailableCommandsUpdate = async () => {
|
||||
output({ type: "available_commands_update", commands: await getAvailableCommands() });
|
||||
};
|
||||
await emitAvailableCommandsUpdate();
|
||||
|
||||
// Handle a single command
|
||||
const handleCommand = async (command: RpcCommand): Promise<RpcResponse> => {
|
||||
const id = command.id;
|
||||
@@ -521,6 +529,26 @@ export async function runRpcMode(
|
||||
// =================================================================
|
||||
|
||||
case "prompt": {
|
||||
const builtinResult = await executeAcpBuiltinSlashCommand(command.message, {
|
||||
session,
|
||||
sessionManager: session.sessionManager,
|
||||
settings: session.settings,
|
||||
cwd: session.sessionManager.getCwd(),
|
||||
output: text => output({ type: "command_output", text }),
|
||||
refreshCommands: emitAvailableCommandsUpdate,
|
||||
reloadPlugins: async () => {},
|
||||
notifyTitleChanged: async () => {},
|
||||
notifyConfigChanged: async () => {},
|
||||
});
|
||||
if (builtinResult !== false) {
|
||||
if ("prompt" in builtinResult) {
|
||||
session
|
||||
.prompt(builtinResult.prompt, { images: command.images })
|
||||
.catch(e => output(error(id, "prompt", e.message)));
|
||||
}
|
||||
return success(id, "prompt");
|
||||
}
|
||||
|
||||
// Don't await - events will stream
|
||||
// Extension commands are executed immediately, file prompt templates are expanded
|
||||
// If streaming and streamingBehavior specified, queues via steer/followUp
|
||||
@@ -592,6 +620,10 @@ export async function runRpcMode(
|
||||
return success(id, "get_state", state);
|
||||
}
|
||||
|
||||
case "get_available_commands": {
|
||||
return success(id, "get_available_commands", { commands: await getAvailableCommands() });
|
||||
}
|
||||
|
||||
case "set_todos": {
|
||||
session.setTodoPhases(command.phases);
|
||||
return success(id, "set_todos", { todoPhases: session.getTodoPhases() });
|
||||
|
||||
@@ -11,6 +11,7 @@ import type { BashResult } from "../../exec/bash-executor";
|
||||
import type { ContextUsage } from "../../extensibility/extensions/types";
|
||||
import type { AgentSessionEvent, SessionStats } from "../../session/agent-session";
|
||||
import type { FileEntry } from "../../session/session-manager";
|
||||
import type { AvailableSlashCommandSource } from "../../slash-commands/available-commands";
|
||||
import type {
|
||||
AgentProgress,
|
||||
SubagentEventPayload,
|
||||
@@ -34,6 +35,7 @@ export type RpcCommand =
|
||||
|
||||
// State
|
||||
| { id?: string; type: "get_state" }
|
||||
| { id?: string; type: "get_available_commands" }
|
||||
| { id?: string; type: "set_todos"; phases: TodoPhase[] }
|
||||
| { id?: string; type: "set_host_tools"; tools: RpcHostToolDefinition[] }
|
||||
| { id?: string; type: "set_host_uri_schemes"; schemes: RpcHostUriSchemeDefinition[] }
|
||||
@@ -110,6 +112,15 @@ export interface RpcSessionState {
|
||||
contextUsage?: ContextUsage;
|
||||
}
|
||||
|
||||
export interface RpcAvailableSlashCommand {
|
||||
name: string;
|
||||
aliases?: string[];
|
||||
description?: string;
|
||||
input?: { hint?: string };
|
||||
subcommands?: Array<{ name: string; description?: string; usage?: string }>;
|
||||
source: AvailableSlashCommandSource;
|
||||
}
|
||||
|
||||
export interface RpcHandoffResult {
|
||||
savedPath?: string;
|
||||
}
|
||||
@@ -156,6 +167,13 @@ export type RpcResponse =
|
||||
|
||||
// State
|
||||
| { id?: string; type: "response"; command: "get_state"; success: true; data: RpcSessionState }
|
||||
| {
|
||||
id?: string;
|
||||
type: "response";
|
||||
command: "get_available_commands";
|
||||
success: true;
|
||||
data: { commands: RpcAvailableSlashCommand[] };
|
||||
}
|
||||
| { id?: string; type: "response"; command: "set_todos"; success: true; data: { todoPhases: TodoPhase[] } }
|
||||
| { id?: string; type: "response"; command: "set_host_tools"; success: true; data: { toolNames: string[] } }
|
||||
| { id?: string; type: "response"; command: "set_host_uri_schemes"; success: true; data: { schemes: string[] } }
|
||||
|
||||
Reference in New Issue
Block a user