mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-20 19:02:10 +00:00
feat(coding-agent): add ExtensionAPI.getCommands() (#1210)
This commit is contained in:
commit
5a6151ba1e
10 changed files with 156 additions and 45 deletions
|
|
@ -2,6 +2,10 @@
|
||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added `ExtensionAPI.getCommands()` to let extensions list available slash commands (extensions, prompt templates, skills) for invocation via `prompt` ([#1210](https://github.com/badlogic/pi-mono/pull/1210) by [@w-winter](https://github.com/w-winter))
|
||||||
|
|
||||||
## [0.51.2] - 2026-02-03
|
## [0.51.2] - 2026-02-03
|
||||||
|
|
||||||
### New Features
|
### New Features
|
||||||
|
|
|
||||||
|
|
@ -942,6 +942,31 @@ pi.registerCommand("deploy", {
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### pi.getCommands()
|
||||||
|
|
||||||
|
Get the slash commands available for invocation via `prompt` in the current session. Includes extension commands, prompt templates, and skill commands.
|
||||||
|
The list matches the RPC `get_commands` ordering: extensions first, then templates, then skills.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const commands = pi.getCommands();
|
||||||
|
const bySource = commands.filter((command) => command.source === "extension");
|
||||||
|
```
|
||||||
|
|
||||||
|
Each entry has this shape:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
{
|
||||||
|
name: string; // Command name without the leading slash
|
||||||
|
description?: string;
|
||||||
|
source: "extension" | "template" | "skill";
|
||||||
|
location?: "user" | "project" | "path"; // For templates and skills
|
||||||
|
path?: string; // Files backing templates, skills, and extensions
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Built-in interactive commands (like `/model` and `/settings`) are not included here. They are handled only in interactive
|
||||||
|
mode and would not execute if sent via `prompt`.
|
||||||
|
|
||||||
### pi.registerMessageRenderer(customType, renderer)
|
### pi.registerMessageRenderer(customType, renderer)
|
||||||
|
|
||||||
Register a custom TUI renderer for messages with your `customType`. See [Custom UI](#custom-ui).
|
Register a custom TUI renderer for messages with your `customType`. See [Custom UI](#custom-ui).
|
||||||
|
|
|
||||||
|
|
@ -67,6 +67,7 @@ import { expandPromptTemplate, type PromptTemplate } from "./prompt-templates.js
|
||||||
import type { ResourceExtensionPaths, ResourceLoader } from "./resource-loader.js";
|
import type { ResourceExtensionPaths, ResourceLoader } from "./resource-loader.js";
|
||||||
import type { BranchSummaryEntry, CompactionEntry, SessionManager } from "./session-manager.js";
|
import type { BranchSummaryEntry, CompactionEntry, SessionManager } from "./session-manager.js";
|
||||||
import type { SettingsManager } from "./settings-manager.js";
|
import type { SettingsManager } from "./settings-manager.js";
|
||||||
|
import { BUILTIN_SLASH_COMMANDS, type SlashCommandInfo, type SlashCommandLocation } from "./slash-commands.js";
|
||||||
import { buildSystemPrompt } from "./system-prompt.js";
|
import { buildSystemPrompt } from "./system-prompt.js";
|
||||||
import type { BashOperations } from "./tools/bash.js";
|
import type { BashOperations } from "./tools/bash.js";
|
||||||
import { createAllTools } from "./tools/index.js";
|
import { createAllTools } from "./tools/index.js";
|
||||||
|
|
@ -1775,6 +1776,45 @@ export class AgentSession {
|
||||||
}
|
}
|
||||||
|
|
||||||
private _bindExtensionCore(runner: ExtensionRunner): void {
|
private _bindExtensionCore(runner: ExtensionRunner): void {
|
||||||
|
const normalizeLocation = (source: string): SlashCommandLocation | undefined => {
|
||||||
|
if (source === "user" || source === "project" || source === "path") {
|
||||||
|
return source;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
const reservedBuiltins = new Set(BUILTIN_SLASH_COMMANDS.map((command) => command.name));
|
||||||
|
|
||||||
|
const getCommands = (): SlashCommandInfo[] => {
|
||||||
|
const extensionCommands: SlashCommandInfo[] = runner
|
||||||
|
.getRegisteredCommandsWithPaths()
|
||||||
|
.filter(({ command }) => !reservedBuiltins.has(command.name))
|
||||||
|
.map(({ command, extensionPath }) => ({
|
||||||
|
name: command.name,
|
||||||
|
description: command.description,
|
||||||
|
source: "extension",
|
||||||
|
path: extensionPath,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const templates: SlashCommandInfo[] = this.promptTemplates.map((template) => ({
|
||||||
|
name: template.name,
|
||||||
|
description: template.description,
|
||||||
|
source: "template",
|
||||||
|
location: normalizeLocation(template.source),
|
||||||
|
path: template.filePath,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const skills: SlashCommandInfo[] = this._resourceLoader.getSkills().skills.map((skill) => ({
|
||||||
|
name: `skill:${skill.name}`,
|
||||||
|
description: skill.description,
|
||||||
|
source: "skill",
|
||||||
|
location: normalizeLocation(skill.source),
|
||||||
|
path: skill.filePath,
|
||||||
|
}));
|
||||||
|
|
||||||
|
return [...extensionCommands, ...templates, ...skills];
|
||||||
|
};
|
||||||
|
|
||||||
runner.bindCore(
|
runner.bindCore(
|
||||||
{
|
{
|
||||||
sendMessage: (message, options) => {
|
sendMessage: (message, options) => {
|
||||||
|
|
@ -1810,6 +1850,7 @@ export class AgentSession {
|
||||||
getActiveTools: () => this.getActiveToolNames(),
|
getActiveTools: () => this.getActiveToolNames(),
|
||||||
getAllTools: () => this.getAllTools(),
|
getAllTools: () => this.getAllTools(),
|
||||||
setActiveTools: (toolNames) => this.setActiveToolsByName(toolNames),
|
setActiveTools: (toolNames) => this.setActiveToolsByName(toolNames),
|
||||||
|
getCommands,
|
||||||
setModel: async (model) => {
|
setModel: async (model) => {
|
||||||
const key = await this.modelRegistry.getApiKey(model);
|
const key = await this.modelRegistry.getApiKey(model);
|
||||||
if (!key) return false;
|
if (!key) return false;
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
* Extension system for lifecycle events and custom tools.
|
* Extension system for lifecycle events and custom tools.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
export type { SlashCommandInfo, SlashCommandLocation, SlashCommandSource } from "../slash-commands.js";
|
||||||
export {
|
export {
|
||||||
createExtensionRuntime,
|
createExtensionRuntime,
|
||||||
discoverAndLoadExtensions,
|
discoverAndLoadExtensions,
|
||||||
|
|
@ -68,6 +69,7 @@ export type {
|
||||||
FindToolResultEvent,
|
FindToolResultEvent,
|
||||||
GetActiveToolsHandler,
|
GetActiveToolsHandler,
|
||||||
GetAllToolsHandler,
|
GetAllToolsHandler,
|
||||||
|
GetCommandsHandler,
|
||||||
GetThinkingLevelHandler,
|
GetThinkingLevelHandler,
|
||||||
GrepToolCallEvent,
|
GrepToolCallEvent,
|
||||||
GrepToolResultEvent,
|
GrepToolResultEvent,
|
||||||
|
|
|
||||||
|
|
@ -119,6 +119,7 @@ export function createExtensionRuntime(): ExtensionRuntime {
|
||||||
getActiveTools: notInitialized,
|
getActiveTools: notInitialized,
|
||||||
getAllTools: notInitialized,
|
getAllTools: notInitialized,
|
||||||
setActiveTools: notInitialized,
|
setActiveTools: notInitialized,
|
||||||
|
getCommands: notInitialized,
|
||||||
setModel: () => Promise.reject(new Error("Extension runtime not initialized")),
|
setModel: () => Promise.reject(new Error("Extension runtime not initialized")),
|
||||||
getThinkingLevel: notInitialized,
|
getThinkingLevel: notInitialized,
|
||||||
setThinkingLevel: notInitialized,
|
setThinkingLevel: notInitialized,
|
||||||
|
|
@ -228,6 +229,10 @@ function createExtensionAPI(
|
||||||
runtime.setActiveTools(toolNames);
|
runtime.setActiveTools(toolNames);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
getCommands() {
|
||||||
|
return runtime.getCommands();
|
||||||
|
},
|
||||||
|
|
||||||
setModel(model) {
|
setModel(model) {
|
||||||
return runtime.setModel(model);
|
return runtime.setModel(model);
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -200,6 +200,7 @@ export class ExtensionRunner {
|
||||||
this.runtime.getActiveTools = actions.getActiveTools;
|
this.runtime.getActiveTools = actions.getActiveTools;
|
||||||
this.runtime.getAllTools = actions.getAllTools;
|
this.runtime.getAllTools = actions.getAllTools;
|
||||||
this.runtime.setActiveTools = actions.setActiveTools;
|
this.runtime.setActiveTools = actions.setActiveTools;
|
||||||
|
this.runtime.getCommands = actions.getCommands;
|
||||||
this.runtime.setModel = actions.setModel;
|
this.runtime.setModel = actions.setModel;
|
||||||
this.runtime.getThinkingLevel = actions.getThinkingLevel;
|
this.runtime.getThinkingLevel = actions.getThinkingLevel;
|
||||||
this.runtime.setThinkingLevel = actions.setThinkingLevel;
|
this.runtime.setThinkingLevel = actions.setThinkingLevel;
|
||||||
|
|
|
||||||
|
|
@ -53,6 +53,7 @@ import type {
|
||||||
SessionEntry,
|
SessionEntry,
|
||||||
SessionManager,
|
SessionManager,
|
||||||
} from "../session-manager.js";
|
} from "../session-manager.js";
|
||||||
|
import type { SlashCommandInfo } from "../slash-commands.js";
|
||||||
import type { BashOperations } from "../tools/bash.js";
|
import type { BashOperations } from "../tools/bash.js";
|
||||||
import type { EditToolDetails } from "../tools/edit.js";
|
import type { EditToolDetails } from "../tools/edit.js";
|
||||||
import type {
|
import type {
|
||||||
|
|
@ -973,6 +974,9 @@ export interface ExtensionAPI {
|
||||||
/** Set the active tools by name. */
|
/** Set the active tools by name. */
|
||||||
setActiveTools(toolNames: string[]): void;
|
setActiveTools(toolNames: string[]): void;
|
||||||
|
|
||||||
|
/** Get available slash commands in the current session. */
|
||||||
|
getCommands(): SlashCommandInfo[];
|
||||||
|
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
// Model and Thinking Level
|
// Model and Thinking Level
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
|
|
@ -1154,6 +1158,8 @@ export type ToolInfo = Pick<ToolDefinition, "name" | "description">;
|
||||||
|
|
||||||
export type GetAllToolsHandler = () => ToolInfo[];
|
export type GetAllToolsHandler = () => ToolInfo[];
|
||||||
|
|
||||||
|
export type GetCommandsHandler = () => SlashCommandInfo[];
|
||||||
|
|
||||||
export type SetActiveToolsHandler = (toolNames: string[]) => void;
|
export type SetActiveToolsHandler = (toolNames: string[]) => void;
|
||||||
|
|
||||||
export type SetModelHandler = (model: Model<any>) => Promise<boolean>;
|
export type SetModelHandler = (model: Model<any>) => Promise<boolean>;
|
||||||
|
|
@ -1188,6 +1194,7 @@ export interface ExtensionActions {
|
||||||
getActiveTools: GetActiveToolsHandler;
|
getActiveTools: GetActiveToolsHandler;
|
||||||
getAllTools: GetAllToolsHandler;
|
getAllTools: GetAllToolsHandler;
|
||||||
setActiveTools: SetActiveToolsHandler;
|
setActiveTools: SetActiveToolsHandler;
|
||||||
|
getCommands: GetCommandsHandler;
|
||||||
setModel: SetModelHandler;
|
setModel: SetModelHandler;
|
||||||
getThinkingLevel: GetThinkingLevelHandler;
|
getThinkingLevel: GetThinkingLevelHandler;
|
||||||
setThinkingLevel: SetThinkingLevelHandler;
|
setThinkingLevel: SetThinkingLevelHandler;
|
||||||
|
|
|
||||||
|
|
@ -88,6 +88,9 @@ export type {
|
||||||
ExtensionCommandContext,
|
ExtensionCommandContext,
|
||||||
ExtensionContext,
|
ExtensionContext,
|
||||||
ExtensionFactory,
|
ExtensionFactory,
|
||||||
|
SlashCommandInfo,
|
||||||
|
SlashCommandLocation,
|
||||||
|
SlashCommandSource,
|
||||||
ToolDefinition,
|
ToolDefinition,
|
||||||
} from "./extensions/index.js";
|
} from "./extensions/index.js";
|
||||||
export type { PromptTemplate } from "./prompt-templates.js";
|
export type { PromptTemplate } from "./prompt-templates.js";
|
||||||
|
|
|
||||||
37
packages/coding-agent/src/core/slash-commands.ts
Normal file
37
packages/coding-agent/src/core/slash-commands.ts
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
export type SlashCommandSource = "extension" | "template" | "skill";
|
||||||
|
|
||||||
|
export type SlashCommandLocation = "user" | "project" | "path";
|
||||||
|
|
||||||
|
export interface SlashCommandInfo {
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
source: SlashCommandSource;
|
||||||
|
location?: SlashCommandLocation;
|
||||||
|
path?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BuiltinSlashCommand {
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const BUILTIN_SLASH_COMMANDS: ReadonlyArray<BuiltinSlashCommand> = [
|
||||||
|
{ name: "settings", description: "Open settings menu" },
|
||||||
|
{ name: "model", description: "Select model (opens selector UI)" },
|
||||||
|
{ name: "scoped-models", description: "Enable/disable models for Ctrl+P cycling" },
|
||||||
|
{ name: "export", description: "Export session to HTML file" },
|
||||||
|
{ name: "share", description: "Share session as a secret GitHub gist" },
|
||||||
|
{ name: "copy", description: "Copy last agent message to clipboard" },
|
||||||
|
{ name: "name", description: "Set session display name" },
|
||||||
|
{ name: "session", description: "Show session info and stats" },
|
||||||
|
{ name: "changelog", description: "Show changelog entries" },
|
||||||
|
{ name: "hotkeys", description: "Show all keyboard shortcuts" },
|
||||||
|
{ name: "fork", description: "Create a new fork from a previous message" },
|
||||||
|
{ name: "tree", description: "Navigate session tree (switch branches)" },
|
||||||
|
{ name: "login", description: "Login with OAuth provider" },
|
||||||
|
{ name: "logout", description: "Logout from OAuth provider" },
|
||||||
|
{ name: "new", description: "Start a new session" },
|
||||||
|
{ name: "compact", description: "Manually compact the session context" },
|
||||||
|
{ name: "resume", description: "Resume a different session" },
|
||||||
|
{ name: "reload", description: "Reload extensions, skills, prompts, and themes" },
|
||||||
|
];
|
||||||
|
|
@ -66,6 +66,7 @@ import { createCompactionSummaryMessage } from "../../core/messages.js";
|
||||||
import { resolveModelScope } from "../../core/model-resolver.js";
|
import { resolveModelScope } from "../../core/model-resolver.js";
|
||||||
import type { ResourceDiagnostic } from "../../core/resource-loader.js";
|
import type { ResourceDiagnostic } from "../../core/resource-loader.js";
|
||||||
import { type SessionContext, SessionManager } from "../../core/session-manager.js";
|
import { type SessionContext, SessionManager } from "../../core/session-manager.js";
|
||||||
|
import { BUILTIN_SLASH_COMMANDS } from "../../core/slash-commands.js";
|
||||||
import type { TruncationResult } from "../../core/tools/truncate.js";
|
import type { TruncationResult } from "../../core/tools/truncate.js";
|
||||||
import { getChangelogPath, getNewEntries, parseChangelog } from "../../utils/changelog.js";
|
import { getChangelogPath, getNewEntries, parseChangelog } from "../../utils/changelog.js";
|
||||||
import { copyToClipboard } from "../../utils/clipboard.js";
|
import { copyToClipboard } from "../../utils/clipboard.js";
|
||||||
|
|
@ -288,56 +289,41 @@ export class InteractiveMode {
|
||||||
|
|
||||||
private setupAutocomplete(fdPath: string | undefined): void {
|
private setupAutocomplete(fdPath: string | undefined): void {
|
||||||
// Define commands for autocomplete
|
// Define commands for autocomplete
|
||||||
const slashCommands: SlashCommand[] = [
|
const slashCommands: SlashCommand[] = BUILTIN_SLASH_COMMANDS.map((command) => ({
|
||||||
{ name: "settings", description: "Open settings menu" },
|
name: command.name,
|
||||||
{
|
description: command.description,
|
||||||
name: "model",
|
}));
|
||||||
description: "Select model (opens selector UI)",
|
|
||||||
getArgumentCompletions: (prefix: string): AutocompleteItem[] | null => {
|
|
||||||
// Get available models (scoped or from registry)
|
|
||||||
const models =
|
|
||||||
this.session.scopedModels.length > 0
|
|
||||||
? this.session.scopedModels.map((s) => s.model)
|
|
||||||
: this.session.modelRegistry.getAvailable();
|
|
||||||
|
|
||||||
if (models.length === 0) return null;
|
const modelCommand = slashCommands.find((command) => command.name === "model");
|
||||||
|
if (modelCommand) {
|
||||||
|
modelCommand.getArgumentCompletions = (prefix: string): AutocompleteItem[] | null => {
|
||||||
|
// Get available models (scoped or from registry)
|
||||||
|
const models =
|
||||||
|
this.session.scopedModels.length > 0
|
||||||
|
? this.session.scopedModels.map((s) => s.model)
|
||||||
|
: this.session.modelRegistry.getAvailable();
|
||||||
|
|
||||||
// Create items with provider/id format
|
if (models.length === 0) return null;
|
||||||
const items = models.map((m) => ({
|
|
||||||
id: m.id,
|
|
||||||
provider: m.provider,
|
|
||||||
label: `${m.provider}/${m.id}`,
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Fuzzy filter by model ID + provider (allows "opus anthropic" to match)
|
// Create items with provider/id format
|
||||||
const filtered = fuzzyFilter(items, prefix, (item) => `${item.id} ${item.provider}`);
|
const items = models.map((m) => ({
|
||||||
|
id: m.id,
|
||||||
|
provider: m.provider,
|
||||||
|
label: `${m.provider}/${m.id}`,
|
||||||
|
}));
|
||||||
|
|
||||||
if (filtered.length === 0) return null;
|
// Fuzzy filter by model ID + provider (allows "opus anthropic" to match)
|
||||||
|
const filtered = fuzzyFilter(items, prefix, (item) => `${item.id} ${item.provider}`);
|
||||||
|
|
||||||
return filtered.map((item) => ({
|
if (filtered.length === 0) return null;
|
||||||
value: item.label,
|
|
||||||
label: item.id,
|
return filtered.map((item) => ({
|
||||||
description: item.provider,
|
value: item.label,
|
||||||
}));
|
label: item.id,
|
||||||
},
|
description: item.provider,
|
||||||
},
|
}));
|
||||||
{ name: "scoped-models", description: "Enable/disable models for Ctrl+P cycling" },
|
};
|
||||||
{ name: "export", description: "Export session to HTML file" },
|
}
|
||||||
{ name: "share", description: "Share session as a secret GitHub gist" },
|
|
||||||
{ name: "copy", description: "Copy last agent message to clipboard" },
|
|
||||||
{ name: "name", description: "Set session display name" },
|
|
||||||
{ name: "session", description: "Show session info and stats" },
|
|
||||||
{ name: "changelog", description: "Show changelog entries" },
|
|
||||||
{ name: "hotkeys", description: "Show all keyboard shortcuts" },
|
|
||||||
{ name: "fork", description: "Create a new fork from a previous message" },
|
|
||||||
{ name: "tree", description: "Navigate session tree (switch branches)" },
|
|
||||||
{ name: "login", description: "Login with OAuth provider" },
|
|
||||||
{ name: "logout", description: "Logout from OAuth provider" },
|
|
||||||
{ name: "new", description: "Start a new session" },
|
|
||||||
{ name: "compact", description: "Manually compact the session context" },
|
|
||||||
{ name: "resume", description: "Resume a different session" },
|
|
||||||
{ name: "reload", description: "Reload extensions, skills, prompts, and themes" },
|
|
||||||
];
|
|
||||||
|
|
||||||
// Convert prompt templates to SlashCommand format for autocomplete
|
// Convert prompt templates to SlashCommand format for autocomplete
|
||||||
const templateCommands: SlashCommand[] = this.session.promptTemplates.map((cmd) => ({
|
const templateCommands: SlashCommand[] = this.session.promptTemplates.map((cmd) => ({
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue