Add get_commands RPC for headless clients (#995)

* Add get_commands RPC for headless clients

Headless clients like Emacs can now query which commands are available.
Previously they could only discover file-based prompt templates by
scanning the filesystem; extension commands and skills were invisible.

The response includes each command's name, description, and source
(extension, template, or skill). Commands appear in the same order
as the TUI's autocomplete: extension commands first, then templates,
then skills.

Built-in TUI commands (/settings, /fork, etc.) are excluded since
they require the interactive UI. Commands like /compact have dedicated
RPC equivalents instead.

* Add location and path to get_commands response

Clients can show where commands come from (user/project/path) and
display file paths in tooltips. The data is already available on
templates and skills - just exposing it.
This commit is contained in:
Daniel Nouri 2026-01-28 02:34:15 +01:00 committed by GitHub
parent c67b582fc4
commit 0ad189f12a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 135 additions and 3 deletions

View file

@ -11,7 +11,7 @@ import type { ImageContent } from "@mariozechner/pi-ai";
import type { SessionStats } from "../../core/agent-session.js";
import type { BashResult } from "../../core/bash-executor.js";
import type { CompactionResult } from "../../core/compaction/index.js";
import type { RpcCommand, RpcResponse, RpcSessionState } from "./rpc-types.js";
import type { RpcCommand, RpcResponse, RpcSessionState, RpcSlashCommand } from "./rpc-types.js";
// ============================================================================
// Types
@ -370,6 +370,14 @@ export class RpcClient {
return this.getData<{ messages: AgentMessage[] }>(response).messages;
}
/**
* Get available commands (extension commands, prompt templates, skills).
*/
async getCommands(): Promise<RpcSlashCommand[]> {
const response = await this.send({ type: "get_commands" });
return this.getData<{ commands: RpcSlashCommand[] }>(response).commands;
}
// =========================================================================
// Helpers
// =========================================================================

View file

@ -26,6 +26,7 @@ import type {
RpcExtensionUIResponse,
RpcResponse,
RpcSessionState,
RpcSlashCommand,
} from "./rpc-types.js";
// Re-export types for consumers
@ -497,6 +498,48 @@ export async function runRpcMode(session: AgentSession): Promise<never> {
return success(id, "get_messages", { messages: session.messages });
}
// =================================================================
// Commands (available for invocation via prompt)
// =================================================================
case "get_commands": {
const commands: RpcSlashCommand[] = [];
// Extension commands
for (const { command, extensionPath } of session.extensionRunner?.getRegisteredCommandsWithPaths() ?? []) {
commands.push({
name: command.name,
description: command.description,
source: "extension",
path: extensionPath,
});
}
// Prompt templates (source is always "user" | "project" | "path" in coding-agent)
for (const template of session.promptTemplates) {
commands.push({
name: template.name,
description: template.description,
source: "template",
location: template.source as RpcSlashCommand["location"],
path: template.filePath,
});
}
// Skills (source is always "user" | "project" | "path" in coding-agent)
for (const skill of session.resourceLoader.getSkills().skills) {
commands.push({
name: `skill:${skill.name}`,
description: skill.description,
source: "skill",
location: skill.source as RpcSlashCommand["location"],
path: skill.filePath,
});
}
return success(id, "get_commands", { commands });
}
default: {
const unknownCommand = command as { type: string };
return error(undefined, unknownCommand.type, `Unknown command: ${unknownCommand.type}`);

View file

@ -60,7 +60,28 @@ export type RpcCommand =
| { id?: string; type: "get_last_assistant_text" }
// Messages
| { id?: string; type: "get_messages" };
| { id?: string; type: "get_messages" }
// Commands (available for invocation via prompt)
| { id?: string; type: "get_commands" };
// ============================================================================
// RPC Slash Command (for get_commands response)
// ============================================================================
/** A command available for invocation via prompt */
export interface RpcSlashCommand {
/** Command name (without leading slash) */
name: string;
/** Human-readable description */
description?: string;
/** What kind of command this is */
source: "extension" | "template" | "skill";
/** Where the command was loaded from (undefined for extensions) */
location?: "user" | "project" | "path";
/** File path to the command source */
path?: string;
}
// ============================================================================
// RPC State
@ -168,6 +189,15 @@ export type RpcResponse =
// Messages
| { id?: string; type: "response"; command: "get_messages"; success: true; data: { messages: AgentMessage[] } }
// Commands
| {
id?: string;
type: "response";
command: "get_commands";
success: true;
data: { commands: RpcSlashCommand[] };
}
// Error response (any command can fail)
| { id?: string; type: "response"; command: string; success: false; error: string };