fix(extensibility): added centralized session slash command discovery for extensions
- Added a shared session command helper that aggregates extension, prompt, and skill slash commands. - Updated ACP, extension UI, runtime-init, and task executor extension contexts to return session command data instead of empty arrays.
This commit is contained in:
@@ -0,0 +1,77 @@
|
||||
/**
|
||||
* Helper for wiring the `getCommands` action of {@link ExtensionAPI}.
|
||||
*
|
||||
* Centralizes the union over the three slash-command sources the runtime
|
||||
* exposes so the five wiring sites (interactive UI, ACP, RPC, print, child
|
||||
* task executor) cannot drift:
|
||||
* - extension-registered hook commands (`source: "extension"`)
|
||||
* - prompt commands loaded as `LoadedCustomCommand` — user/project/bundled
|
||||
* custom commands and MCP prompts (`source: "prompt"`)
|
||||
* - skill commands derived from `session.skills`, gated on
|
||||
* `skillsSettings.enableSkillCommands` (`source: "skill"`)
|
||||
*
|
||||
* Built-in slash commands are intentionally excluded; `getCommands()` is the
|
||||
* surface extensions use to discover dynamic commands they did not register
|
||||
* themselves. Each frontend (interactive-mode, ACP) prepends its own builtins.
|
||||
*/
|
||||
import type { SkillsSettings } from "../../config/settings";
|
||||
import type { CustomCommandSource, LoadedCustomCommand } from "../custom-commands";
|
||||
import { getSkillSlashCommandName, type Skill } from "../skills";
|
||||
import type { SlashCommandInfo, SlashCommandLocation } from "../slash-commands";
|
||||
import type { ExtensionRunner } from "./runner";
|
||||
|
||||
interface CommandsCapableSession {
|
||||
readonly extensionRunner?: ExtensionRunner;
|
||||
readonly customCommands: ReadonlyArray<LoadedCustomCommand>;
|
||||
readonly skills: ReadonlyArray<Skill>;
|
||||
readonly skillsSettings?: SkillsSettings;
|
||||
}
|
||||
|
||||
export function getSessionSlashCommands(session: CommandsCapableSession): SlashCommandInfo[] {
|
||||
const out: SlashCommandInfo[] = [];
|
||||
|
||||
const runner = session.extensionRunner;
|
||||
if (runner) {
|
||||
for (const cmd of runner.getRegisteredCommands()) {
|
||||
out.push({
|
||||
name: cmd.name,
|
||||
description: cmd.description,
|
||||
source: "extension",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
for (const cmd of session.customCommands) {
|
||||
out.push({
|
||||
name: cmd.command.name,
|
||||
description: cmd.command.description,
|
||||
source: "prompt",
|
||||
location: customCommandLocation(cmd.source),
|
||||
path: cmd.resolvedPath,
|
||||
});
|
||||
}
|
||||
|
||||
if (session.skillsSettings?.enableSkillCommands) {
|
||||
for (const skill of session.skills) {
|
||||
out.push({
|
||||
name: getSkillSlashCommandName(skill),
|
||||
description: skill.description || undefined,
|
||||
source: "skill",
|
||||
path: skill.filePath,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
function customCommandLocation(source: CustomCommandSource): SlashCommandLocation | undefined {
|
||||
switch (source) {
|
||||
case "user":
|
||||
return "user";
|
||||
case "project":
|
||||
return "project";
|
||||
case "bundled":
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
@@ -49,6 +49,7 @@ import { Settings } from "../../config/settings";
|
||||
import { clearPluginRootsAndCaches, resolveActiveProjectRegistryPath } from "../../discovery/helpers";
|
||||
import type { ExtensionUIContext, ExtensionUIDialogOptions } 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 { loadSlashCommands } from "../../extensibility/slash-commands";
|
||||
import { MCPManager } from "../../mcp/manager";
|
||||
@@ -1741,7 +1742,7 @@ export class AcpAgent implements Agent {
|
||||
getActiveTools: () => record.session.getActiveToolNames(),
|
||||
getAllTools: () => record.session.getAllToolNames(),
|
||||
setActiveTools: toolNames => record.session.setActiveToolsByName(toolNames),
|
||||
getCommands: () => [],
|
||||
getCommands: () => getSessionSlashCommands(record.session),
|
||||
setModel: async model => {
|
||||
const apiKey = await record.session.modelRegistry.getApiKey(model);
|
||||
if (!apiKey) {
|
||||
|
||||
@@ -16,6 +16,7 @@ import type {
|
||||
SendUserMessageHandler,
|
||||
TerminalInputHandler,
|
||||
} from "../../extensibility/extensions";
|
||||
import { getSessionSlashCommands } from "../../extensibility/extensions/get-commands-handler";
|
||||
import { HookEditorComponent } from "../../modes/components/hook-editor";
|
||||
import { HookInputComponent } from "../../modes/components/hook-input";
|
||||
import { HookSelectorComponent } from "../../modes/components/hook-selector";
|
||||
@@ -109,7 +110,7 @@ export class ExtensionUiController {
|
||||
},
|
||||
getThinkingLevel: () => this.ctx.session.thinkingLevel,
|
||||
setThinkingLevel: level => this.ctx.session.setThinkingLevel(level),
|
||||
getCommands: () => [],
|
||||
getCommands: () => getSessionSlashCommands(this.ctx.session),
|
||||
getSessionName: () => this.ctx.sessionManager.getSessionName(),
|
||||
setSessionName: name => this.#updateSessionName(name),
|
||||
};
|
||||
@@ -349,7 +350,7 @@ export class ExtensionUiController {
|
||||
},
|
||||
getThinkingLevel: () => this.ctx.session.thinkingLevel,
|
||||
setThinkingLevel: (level, persist) => this.ctx.session.setThinkingLevel(level, persist),
|
||||
getCommands: () => [],
|
||||
getCommands: () => getSessionSlashCommands(this.ctx.session),
|
||||
getSessionName: () => this.ctx.sessionManager.getSessionName(),
|
||||
setSessionName: name => this.#updateSessionName(name),
|
||||
};
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
* caller-supplied hooks.
|
||||
*/
|
||||
import { runExtensionCompact, runExtensionSetModel } from "../extensibility/extensions/compact-handler";
|
||||
import { getSessionSlashCommands } from "../extensibility/extensions/get-commands-handler";
|
||||
import type { ExtensionError, ExtensionUIContext } from "../extensibility/extensions/types";
|
||||
import type { AgentSession } from "../session/agent-session";
|
||||
|
||||
@@ -59,7 +60,7 @@ export async function initializeExtensions(session: AgentSession, options: Initi
|
||||
getActiveTools: () => session.getActiveToolNames(),
|
||||
getAllTools: () => session.getAllToolNames(),
|
||||
setActiveTools: (toolNames: string[]) => session.setActiveToolsByName(toolNames),
|
||||
getCommands: () => [],
|
||||
getCommands: () => getSessionSlashCommands(session),
|
||||
setModel: model => runExtensionSetModel(session, model),
|
||||
getThinkingLevel: () => session.thinkingLevel,
|
||||
setThinkingLevel: level => session.setThinkingLevel(level),
|
||||
|
||||
@@ -16,6 +16,7 @@ import { Settings } from "../config/settings";
|
||||
import { SETTINGS_SCHEMA, type SettingPath } from "../config/settings-schema";
|
||||
import type { CustomTool } from "../extensibility/custom-tools/types";
|
||||
import { runExtensionCompact, runExtensionSetModel } from "../extensibility/extensions/compact-handler";
|
||||
import { getSessionSlashCommands } from "../extensibility/extensions/get-commands-handler";
|
||||
import type { Skill } from "../extensibility/skills";
|
||||
import type { HindsightSessionState } from "../hindsight/state";
|
||||
import type { LocalProtocolOptions } from "../internal-urls";
|
||||
@@ -1119,7 +1120,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
||||
getAllTools: () => session.getAllToolNames(),
|
||||
setActiveTools: (toolNames: string[]) =>
|
||||
session.setActiveToolsByName(toolNames.filter(name => !parentOwnedToolNames.has(name))),
|
||||
getCommands: () => [],
|
||||
getCommands: () => getSessionSlashCommands(session),
|
||||
setModel: model => runExtensionSetModel(session, model),
|
||||
getThinkingLevel: () => session.thinkingLevel,
|
||||
setThinkingLevel: level => session.setThinkingLevel(level),
|
||||
|
||||
Reference in New Issue
Block a user