diff --git a/packages/coding-agent/src/modes/rpc/rpc-client.ts b/packages/coding-agent/src/modes/rpc/rpc-client.ts index 3a5d583c4..9ac6a3bb3 100644 --- a/packages/coding-agent/src/modes/rpc/rpc-client.ts +++ b/packages/coding-agent/src/modes/rpc/rpc-client.ts @@ -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 { + RpcAvailableCommandsUpdateFrame, RpcAvailableSlashCommand, RpcCommand, RpcExtensionUIRequest, @@ -64,6 +65,7 @@ export type RpcSessionEventListener = (event: AgentSessionEvent) => void; export type RpcSubagentLifecycleListener = (payload: RpcSubagentLifecycleFrame["payload"]) => void; export type RpcSubagentProgressListener = (payload: RpcSubagentProgressFrame["payload"]) => void; export type RpcSubagentEventListener = (payload: RpcSubagentEventFrame["payload"]) => void; +export type RpcAvailableCommandsUpdateListener = (commands: RpcAvailableSlashCommand[]) => void; export interface RpcClientToolContext { toolCallId: string; @@ -162,6 +164,11 @@ function isRpcSubagentEventFrame(value: unknown): value is RpcSubagentEventFrame return value.type === "subagent_event" && isRecord(value.payload); } +function isRpcAvailableCommandsUpdateFrame(value: unknown): value is RpcAvailableCommandsUpdateFrame { + if (!isRecord(value)) return false; + return value.type === "available_commands_update" && Array.isArray(value.commands); +} + function isRpcHostToolCallRequest(value: unknown): value is RpcHostToolCallRequest { if (!isRecord(value)) return false; return ( @@ -203,6 +210,7 @@ export class RpcClient { #subagentLifecycleListeners = new Set(); #subagentProgressListeners = new Set(); #subagentEventListeners = new Set(); + #availableCommandsUpdateListeners = new Set(); #pendingRequests: Map void; reject: (error: Error) => void }> = new Map(); #customTools: RpcClientCustomTool[] = []; @@ -378,6 +386,14 @@ export class RpcClient { return () => this.#subagentEventListeners.delete(listener); } + /** + * Subscribe to slash-command availability updates emitted by the RPC server. + */ + onAvailableCommandsUpdate(listener: RpcAvailableCommandsUpdateListener): () => void { + this.#availableCommandsUpdateListeners.add(listener); + return () => this.#availableCommandsUpdateListeners.delete(listener); + } + /** * Get collected stderr output (useful for debugging). */ @@ -834,6 +850,13 @@ export class RpcClient { return; } + if (isRpcAvailableCommandsUpdateFrame(data)) { + for (const listener of this.#availableCommandsUpdateListeners) { + listener(data.commands); + } + return; + } + if (!isAgentSessionEvent(data)) return; for (const listener of this.#sessionEventListeners) { diff --git a/packages/coding-agent/src/modes/rpc/rpc-mode.ts b/packages/coding-agent/src/modes/rpc/rpc-mode.ts index e244a0bfd..56596e0ed 100644 --- a/packages/coding-agent/src/modes/rpc/rpc-mode.ts +++ b/packages/coding-agent/src/modes/rpc/rpc-mode.ts @@ -12,6 +12,8 @@ */ import { getOAuthProviders } from "@oh-my-pi/pi-ai/oauth"; import { $env, readJsonl, Snowflake } from "@oh-my-pi/pi-utils"; +import { reset as resetCapabilities } from "../../capability"; +import { clearPluginRootsAndCaches, resolveActiveProjectRegistryPath } from "../../discovery/helpers"; import { type ExtensionUIContext, type ExtensionUIDialogOptions, @@ -19,6 +21,7 @@ import { type ExtensionWidgetOptions, getExtensionUISelectOptionLabel, } from "../../extensibility/extensions"; +import { loadSlashCommands } from "../../extensibility/slash-commands"; import { type Theme, theme } from "../../modes/theme/theme"; import type { AgentSession } from "../../session/agent-session"; import { executeAcpBuiltinSlashCommand } from "../../slash-commands/acp-builtins"; @@ -514,6 +517,15 @@ export async function runRpcMode( }); const getAvailableCommands = async () => buildAvailableSlashCommands(session); + const reloadPluginState = async () => { + const cwd = session.sessionManager.getCwd(); + const projectPath = await resolveActiveProjectRegistryPath(cwd); + clearPluginRootsAndCaches(projectPath ? [projectPath] : undefined); + resetCapabilities(); + session.setSlashCommands(await loadSlashCommands({ cwd })); + await session.refreshSshTool({ activateIfAvailable: true }); + await emitAvailableCommandsUpdate(); + }; const emitAvailableCommandsUpdate = async () => { output({ type: "available_commands_update", commands: await getAvailableCommands() }); }; @@ -536,7 +548,7 @@ export async function runRpcMode( cwd: session.sessionManager.getCwd(), output: text => output({ type: "command_output", text }), refreshCommands: emitAvailableCommandsUpdate, - reloadPlugins: async () => {}, + reloadPlugins: reloadPluginState, notifyTitleChanged: async () => {}, notifyConfigChanged: async () => {}, }); diff --git a/packages/coding-agent/src/modes/rpc/rpc-types.ts b/packages/coding-agent/src/modes/rpc/rpc-types.ts index 5f4da3879..efbbada43 100644 --- a/packages/coding-agent/src/modes/rpc/rpc-types.ts +++ b/packages/coding-agent/src/modes/rpc/rpc-types.ts @@ -121,6 +121,11 @@ export interface RpcAvailableSlashCommand { source: AvailableSlashCommandSource; } +export interface RpcAvailableCommandsUpdateFrame { + type: "available_commands_update"; + commands: RpcAvailableSlashCommand[]; +} + export interface RpcHandoffResult { savedPath?: string; }