diff --git a/packages/coding-agent/docs/extensions.md b/packages/coding-agent/docs/extensions.md index 09cae0a8..0db7ceab 100644 --- a/packages/coding-agent/docs/extensions.md +++ b/packages/coding-agent/docs/extensions.md @@ -771,6 +771,62 @@ Options: - `replaceInstructions`: If true, `customInstructions` replaces the default prompt instead of being appended - `label`: Label to attach to the branch summary entry (or target entry if not summarizing) +### ctx.reload() + +Run the same reload flow as `/reload`. + +```typescript +pi.registerCommand("reload-runtime", { + description: "Reload extensions, skills, prompts, and themes", + handler: async (_args, ctx) => { + await ctx.reload(); + return; + }, +}); +``` + +Important behavior: +- `await ctx.reload()` emits `session_shutdown` for the current extension runtime +- It then reloads resources and emits `session_start` (and `resources_discover` with reason `"reload"`) for the new runtime +- The currently running command handler still continues in the old call frame +- Code after `await ctx.reload()` still runs from the pre-reload version +- Code after `await ctx.reload()` must not assume old in-memory extension state is still valid +- After the handler returns, future commands/events/tool calls use the new extension version + +For predictable behavior, treat reload as terminal for that handler (`await ctx.reload(); return;`). + +Tools run with `ExtensionContext`, so they cannot call `ctx.reload()` directly. Use a command as the reload entrypoint, then expose a tool that queues that command as a follow-up user message. + +Example tool the LLM can call to trigger reload: + +```typescript +import type { ExtensionAPI } from "@mariozechner/pi-coding-agent"; +import { Type } from "@sinclair/typebox"; + +export default function (pi: ExtensionAPI) { + pi.registerCommand("reload-runtime", { + description: "Reload extensions, skills, prompts, and themes", + handler: async (_args, ctx) => { + await ctx.reload(); + return; + }, + }); + + pi.registerTool({ + name: "reload_runtime", + label: "Reload Runtime", + description: "Reload extensions, skills, prompts, and themes", + parameters: Type.Object({}), + async execute() { + pi.sendUserMessage("/reload-runtime", { deliverAs: "followUp" }); + return { + content: [{ type: "text", text: "Queued /reload-runtime as a follow-up command." }], + }; + }, + }); +} +``` + ## ExtensionAPI Methods ### pi.on(event, handler) @@ -1778,6 +1834,7 @@ All examples in [examples/extensions/](../examples/extensions/). | `handoff.ts` | Cross-provider model handoff | `registerCommand`, `ui.editor`, `ui.custom` | | `qna.ts` | Q&A with custom UI | `registerCommand`, `ui.custom`, `setEditorText` | | `send-user-message.ts` | Inject user messages | `registerCommand`, `sendUserMessage` | +| `reload-runtime.ts` | Reload command and LLM tool handoff | `registerCommand`, `ctx.reload()`, `sendUserMessage` | | `shutdown-command.ts` | Graceful shutdown command | `registerCommand`, `shutdown()` | | **Events & Gates** ||| | `permission-gate.ts` | Block dangerous commands | `on("tool_call")`, `ui.confirm` | diff --git a/packages/coding-agent/examples/extensions/README.md b/packages/coding-agent/examples/extensions/README.md index 52a7be37..18cf40ca 100644 --- a/packages/coding-agent/examples/extensions/README.md +++ b/packages/coding-agent/examples/extensions/README.md @@ -66,6 +66,7 @@ cp permission-gate.ts ~/.pi/agent/extensions/ | `overlay-qa-tests.ts` | Comprehensive overlay QA tests: anchors, margins, stacking, overflow, animation | | `doom-overlay/` | DOOM game running as an overlay at 35 FPS (demonstrates real-time game rendering) | | `shutdown-command.ts` | Adds `/quit` command demonstrating `ctx.shutdown()` | +| `reload-runtime.ts` | Adds `/reload-runtime` and `reload_runtime` tool showing safe reload flow | | `interactive-shell.ts` | Run interactive commands (vim, htop) with full terminal via `user_bash` hook | | `inline-bash.ts` | Expands `!{command}` patterns in prompts via `input` event transformation | diff --git a/packages/coding-agent/examples/extensions/reload-runtime.ts b/packages/coding-agent/examples/extensions/reload-runtime.ts new file mode 100644 index 00000000..e7b41e76 --- /dev/null +++ b/packages/coding-agent/examples/extensions/reload-runtime.ts @@ -0,0 +1,37 @@ +/** + * Reload Runtime Extension + * + * Demonstrates ctx.reload() from ExtensionCommandContext and an LLM-callable + * tool that queues a follow-up command to trigger reload. + */ + +import type { ExtensionAPI } from "@mariozechner/pi-coding-agent"; +import { Type } from "@sinclair/typebox"; + +export default function (pi: ExtensionAPI) { + // Command entrypoint for reload. + // Treat reload as terminal for this handler. + pi.registerCommand("reload-runtime", { + description: "Reload extensions, skills, prompts, and themes", + handler: async (_args, ctx) => { + await ctx.reload(); + return; + }, + }); + + // LLM-callable tool. Tools get ExtensionContext, so they cannot call ctx.reload() directly. + // Instead, queue a follow-up user command that executes the command above. + pi.registerTool({ + name: "reload_runtime", + label: "Reload Runtime", + description: "Reload extensions, skills, prompts, and themes", + parameters: Type.Object({}), + async execute() { + pi.sendUserMessage("/reload-runtime", { deliverAs: "followUp" }); + return { + content: [{ type: "text", text: "Queued /reload-runtime as a follow-up command." }], + details: {}, + }; + }, + }); +} diff --git a/packages/coding-agent/src/core/extensions/runner.ts b/packages/coding-agent/src/core/extensions/runner.ts index 44bb323e..e32b1e7b 100644 --- a/packages/coding-agent/src/core/extensions/runner.ts +++ b/packages/coding-agent/src/core/extensions/runner.ts @@ -147,6 +147,8 @@ export type NavigateTreeHandler = ( export type SwitchSessionHandler = (sessionPath: string) => Promise<{ cancelled: boolean }>; +export type ReloadHandler = () => Promise; + export type ShutdownHandler = () => void; /** @@ -210,6 +212,7 @@ export class ExtensionRunner { private forkHandler: ForkHandler = async () => ({ cancelled: false }); private navigateTreeHandler: NavigateTreeHandler = async () => ({ cancelled: false }); private switchSessionHandler: SwitchSessionHandler = async () => ({ cancelled: false }); + private reloadHandler: ReloadHandler = async () => {}; private shutdownHandler: ShutdownHandler = () => {}; private shortcutDiagnostics: ResourceDiagnostic[] = []; private commandDiagnostics: ResourceDiagnostic[] = []; @@ -269,6 +272,7 @@ export class ExtensionRunner { this.forkHandler = actions.fork; this.navigateTreeHandler = actions.navigateTree; this.switchSessionHandler = actions.switchSession; + this.reloadHandler = actions.reload; return; } @@ -277,6 +281,7 @@ export class ExtensionRunner { this.forkHandler = async () => ({ cancelled: false }); this.navigateTreeHandler = async () => ({ cancelled: false }); this.switchSessionHandler = async () => ({ cancelled: false }); + this.reloadHandler = async () => {}; } setUIContext(uiContext?: ExtensionUIContext): void { @@ -501,6 +506,7 @@ export class ExtensionRunner { fork: (entryId) => this.forkHandler(entryId), navigateTree: (targetId, options) => this.navigateTreeHandler(targetId, options), switchSession: (sessionPath) => this.switchSessionHandler(sessionPath), + reload: () => this.reloadHandler(), }; } diff --git a/packages/coding-agent/src/core/extensions/types.ts b/packages/coding-agent/src/core/extensions/types.ts index 4362b6af..855dbde0 100644 --- a/packages/coding-agent/src/core/extensions/types.ts +++ b/packages/coding-agent/src/core/extensions/types.ts @@ -306,6 +306,9 @@ export interface ExtensionCommandContext extends ExtensionContext { /** Switch to a different session file. */ switchSession(sessionPath: string): Promise<{ cancelled: boolean }>; + + /** Reload extensions, skills, prompts, and themes. */ + reload(): Promise; } // ============================================================================ @@ -1234,6 +1237,7 @@ export interface ExtensionCommandContextActions { options?: { summarize?: boolean; customInstructions?: string; replaceInstructions?: boolean; label?: string }, ) => Promise<{ cancelled: boolean }>; switchSession: (sessionPath: string) => Promise<{ cancelled: boolean }>; + reload: () => Promise; } /** diff --git a/packages/coding-agent/src/modes/interactive/interactive-mode.ts b/packages/coding-agent/src/modes/interactive/interactive-mode.ts index b325c59e..4245c78c 100644 --- a/packages/coding-agent/src/modes/interactive/interactive-mode.ts +++ b/packages/coding-agent/src/modes/interactive/interactive-mode.ts @@ -1068,6 +1068,9 @@ export class InteractiveMode { await this.handleResumeSession(sessionPath); return { cancelled: false }; }, + reload: async () => { + await this.handleReloadCommand(); + }, }, shutdownHandler: () => { this.shutdownRequested = true; diff --git a/packages/coding-agent/src/modes/print-mode.ts b/packages/coding-agent/src/modes/print-mode.ts index 834954db..8b1628c0 100644 --- a/packages/coding-agent/src/modes/print-mode.ts +++ b/packages/coding-agent/src/modes/print-mode.ts @@ -63,6 +63,9 @@ export async function runPrintMode(session: AgentSession, options: PrintModeOpti const success = await session.switchSession(sessionPath); return { cancelled: !success }; }, + reload: async () => { + await session.reload(); + }, }, onError: (err) => { console.error(`Extension error (${err.extensionPath}): ${err.error}`); diff --git a/packages/coding-agent/src/modes/rpc/rpc-mode.ts b/packages/coding-agent/src/modes/rpc/rpc-mode.ts index 4ab920ce..af5a2085 100644 --- a/packages/coding-agent/src/modes/rpc/rpc-mode.ts +++ b/packages/coding-agent/src/modes/rpc/rpc-mode.ts @@ -295,6 +295,9 @@ export async function runRpcMode(session: AgentSession): Promise { const success = await session.switchSession(sessionPath); return { cancelled: !success }; }, + reload: async () => { + await session.reload(); + }, }, shutdownHandler: () => { shutdownRequested = true;