feat(coding-agent): add ExtensionAPI.getCommands()

This commit is contained in:
warren 2026-02-03 06:06:29 +01:00 committed by Mario Zechner
parent ff9a3f0660
commit 2613754c47
10 changed files with 156 additions and 45 deletions

View file

@ -67,6 +67,7 @@ import { expandPromptTemplate, type PromptTemplate } from "./prompt-templates.js
import type { ResourceExtensionPaths, ResourceLoader } from "./resource-loader.js";
import type { BranchSummaryEntry, CompactionEntry, SessionManager } from "./session-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 type { BashOperations } from "./tools/bash.js";
import { createAllTools } from "./tools/index.js";
@ -1775,6 +1776,45 @@ export class AgentSession {
}
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(
{
sendMessage: (message, options) => {
@ -1810,6 +1850,7 @@ export class AgentSession {
getActiveTools: () => this.getActiveToolNames(),
getAllTools: () => this.getAllTools(),
setActiveTools: (toolNames) => this.setActiveToolsByName(toolNames),
getCommands,
setModel: async (model) => {
const key = await this.modelRegistry.getApiKey(model);
if (!key) return false;

View file

@ -2,6 +2,7 @@
* Extension system for lifecycle events and custom tools.
*/
export type { SlashCommandInfo, SlashCommandLocation, SlashCommandSource } from "../slash-commands.js";
export {
createExtensionRuntime,
discoverAndLoadExtensions,
@ -68,6 +69,7 @@ export type {
FindToolResultEvent,
GetActiveToolsHandler,
GetAllToolsHandler,
GetCommandsHandler,
GetThinkingLevelHandler,
GrepToolCallEvent,
GrepToolResultEvent,

View file

@ -119,6 +119,7 @@ export function createExtensionRuntime(): ExtensionRuntime {
getActiveTools: notInitialized,
getAllTools: notInitialized,
setActiveTools: notInitialized,
getCommands: notInitialized,
setModel: () => Promise.reject(new Error("Extension runtime not initialized")),
getThinkingLevel: notInitialized,
setThinkingLevel: notInitialized,
@ -228,6 +229,10 @@ function createExtensionAPI(
runtime.setActiveTools(toolNames);
},
getCommands() {
return runtime.getCommands();
},
setModel(model) {
return runtime.setModel(model);
},

View file

@ -200,6 +200,7 @@ export class ExtensionRunner {
this.runtime.getActiveTools = actions.getActiveTools;
this.runtime.getAllTools = actions.getAllTools;
this.runtime.setActiveTools = actions.setActiveTools;
this.runtime.getCommands = actions.getCommands;
this.runtime.setModel = actions.setModel;
this.runtime.getThinkingLevel = actions.getThinkingLevel;
this.runtime.setThinkingLevel = actions.setThinkingLevel;

View file

@ -53,6 +53,7 @@ import type {
SessionEntry,
SessionManager,
} from "../session-manager.js";
import type { SlashCommandInfo } from "../slash-commands.js";
import type { BashOperations } from "../tools/bash.js";
import type { EditToolDetails } from "../tools/edit.js";
import type {
@ -973,6 +974,9 @@ export interface ExtensionAPI {
/** Set the active tools by name. */
setActiveTools(toolNames: string[]): void;
/** Get available slash commands in the current session. */
getCommands(): SlashCommandInfo[];
// =========================================================================
// Model and Thinking Level
// =========================================================================
@ -1154,6 +1158,8 @@ export type ToolInfo = Pick<ToolDefinition, "name" | "description">;
export type GetAllToolsHandler = () => ToolInfo[];
export type GetCommandsHandler = () => SlashCommandInfo[];
export type SetActiveToolsHandler = (toolNames: string[]) => void;
export type SetModelHandler = (model: Model<any>) => Promise<boolean>;
@ -1188,6 +1194,7 @@ export interface ExtensionActions {
getActiveTools: GetActiveToolsHandler;
getAllTools: GetAllToolsHandler;
setActiveTools: SetActiveToolsHandler;
getCommands: GetCommandsHandler;
setModel: SetModelHandler;
getThinkingLevel: GetThinkingLevelHandler;
setThinkingLevel: SetThinkingLevelHandler;

View file

@ -88,6 +88,9 @@ export type {
ExtensionCommandContext,
ExtensionContext,
ExtensionFactory,
SlashCommandInfo,
SlashCommandLocation,
SlashCommandSource,
ToolDefinition,
} from "./extensions/index.js";
export type { PromptTemplate } from "./prompt-templates.js";

View 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" },
];