From 889672ff530b34a530991b1c147de638060c7cfd Mon Sep 17 00:00:00 2001 From: Federico Jaramillo Martinez Date: Wed, 1 Jul 2026 19:32:52 +0200 Subject: [PATCH] feat: add session runtime reload command --- .changeset/session-runtime-reload-command.md | 5 ++++ README.md | 2 ++ docs/config.html | 2 +- docs/config.md | 2 +- docs/plugins.html | 13 +++++---- docs/plugins.md | 4 +-- src/client/src/components/SessionList.ts | 2 +- .../settings/SettingsPackagesPanel.ts | 2 +- .../settings/piPackageSettings.test.ts | 5 ++-- .../components/settings/piPackageSettings.ts | 2 +- .../src/controllers/sessionController.test.ts | 6 ++-- .../src/controllers/sessionController.ts | 2 +- src/client/src/plugins/core/actions.ts | 6 ++-- src/client/src/plugins/registry.test.ts | 9 ++++-- src/server/sessions/builtinCommands.ts | 2 +- src/server/sessions/piSessionService.test.ts | 29 ++++++++++++++++++- src/server/sessions/piSessionService.ts | 18 ++++++++++++ .../sessions/sessionCommandService.test.ts | 24 +++++++++++++++ src/server/sessions/sessionCommandService.ts | 25 +++++++++++++--- 19 files changed, 129 insertions(+), 31 deletions(-) create mode 100644 .changeset/session-runtime-reload-command.md diff --git a/.changeset/session-runtime-reload-command.md b/.changeset/session-runtime-reload-command.md new file mode 100644 index 0000000..c352987 --- /dev/null +++ b/.changeset/session-runtime-reload-command.md @@ -0,0 +1,5 @@ +--- +"@jmfederico/pi-web": patch +--- + +Add `/reload` support for PI WEB sessions so installed Pi package resources can be refreshed in existing sessions without restarting the session daemon, while keeping browser plugin reload guidance separate. diff --git a/README.md b/README.md index b63902a..7f301b9 100644 --- a/README.md +++ b/README.md @@ -107,6 +107,8 @@ PI WEB supports trusted browser-side PI WEB plugins that can add actions, worksp Pi packages are managed separately through Pi's package manager or **Settings → Pi packages**. Use **Settings → PI WEB plugins** only to enable or disable discovered browser plugins. +After installing, updating, or removing a Pi package, type `/reload` in each idle PI WEB session to refresh Pi runtime resources such as extensions, skills, prompt templates, themes, and context/system prompt files. Reload the browser page separately for newly discovered or changed PI WEB plugins. + Read more: [Plugin API](https://pi-web.dev/plugins) ## Configuration diff --git a/docs/config.html b/docs/config.html index ffdb75c..a610588 100644 --- a/docs/config.html +++ b/docs/config.html @@ -143,7 +143,7 @@
  • spawnSessions / subsessions: restart the session daemon.
  • pathAccess: applies on the next request; existing file views may need a browser refresh.
  • plugins: reload the browser tab after changing PI WEB plugin enablement.
  • -
  • Pi package install/remove/update: not a PI WEB config key; after a mutation, reload the browser page for newly discovered PI WEB browser plugins and reload existing Pi sessions (or use /reload in Pi) for session-runtime resources. A routine session daemon restart is not required.
  • +
  • Pi package install/remove/update: not a PI WEB config key; after a mutation, type /reload in each idle PI WEB session to refresh Pi runtime resources such as extensions, skills, prompt templates, themes, and context/system prompt files as supported by Pi. Reload the browser page separately for PI WEB browser plugin changes. A routine session daemon restart is not required.
  • shortcuts: saved settings apply in the browser after config refresh/save.
  • diff --git a/docs/config.md b/docs/config.md index 8e7e468..ef02890 100644 --- a/docs/config.md +++ b/docs/config.md @@ -37,7 +37,7 @@ Process restarts depend on the key: - `pathAccess`: applies on the next request; existing file views may need a browser refresh. - `uploads.defaultFolder`: applies to newly opened Files upload dialogs and new direct drag/drop batches after config/workspace refresh. - `plugins`: reload the browser tab after changing PI WEB plugin enablement. -- Pi package install/remove/update: not a PI WEB config key; after a mutation, reload the browser page for newly discovered PI WEB browser plugins and reload existing Pi sessions (or use `/reload` in Pi) for session-runtime resources. A routine session daemon restart is not required. +- Pi package install/remove/update: not a PI WEB config key; after a mutation, type `/reload` in each idle PI WEB session to refresh Pi runtime resources such as extensions, skills, prompt templates, themes, and context/system prompt files as supported by Pi. Reload the browser page separately for PI WEB browser plugin changes. A routine session daemon restart is not required. - `shortcuts`: saved settings apply in the browser after config refresh/save. ## Global config example diff --git a/docs/plugins.html b/docs/plugins.html index f4b8e2e..278ba9b 100644 --- a/docs/plugins.html +++ b/docs/plugins.html @@ -130,8 +130,8 @@

    Pi packages vs PI WEB plugins

    Pi packages are packages managed by Pi (pi install, pi remove, - pi update). A Pi package can provide extensions, skills, prompt templates, themes, and/or - PI WEB browser plugins. Many Pi packages do not include a PI WEB plugin. + pi update). A Pi package can provide extensions, skills, prompt templates, themes, + context/system prompt files, and/or PI WEB browser plugins. Many Pi packages do not include a PI WEB plugin.

    PI WEB plugins are browser-side UI modules discovered from bundled, local, dev, and @@ -146,10 +146,11 @@

    Use Settings → PI WEB plugins to enable or disable discovered PI WEB browser plugins - before the browser imports them. After installing, removing, or updating a Pi package, reload the browser - page to import newly discovered PI WEB browser plugins. Reload existing Pi sessions, or use - /reload in Pi, so extensions, skills, prompt templates, and themes are rediscovered. A - routine session daemon restart is not required. + before the browser imports them. After installing, removing, or updating a Pi package, type + /reload in each idle PI WEB session to refresh Pi runtime resources such as extensions, + skills, prompt templates, themes, and context/system prompt files as supported by Pi. Reload the browser + page separately for newly discovered or changed PI WEB browser plugins. A routine session daemon restart + is not required.

    diff --git a/docs/plugins.md b/docs/plugins.md index 0cd9e5c..085049e 100644 --- a/docs/plugins.md +++ b/docs/plugins.md @@ -15,13 +15,13 @@ They do **not** run in the session daemon, do not get a server-side hook API, an ## Pi packages vs PI WEB plugins -**Pi packages** are packages managed by Pi (`pi install`, `pi remove`, `pi update`). A Pi package can provide extensions, skills, prompt templates, themes, and/or PI WEB browser plugins. Many Pi packages do not include a PI WEB plugin. +**Pi packages** are packages managed by Pi (`pi install`, `pi remove`, `pi update`). A Pi package can provide extensions, skills, prompt templates, themes, context/system prompt files, and/or PI WEB browser plugins. Many Pi packages do not include a PI WEB plugin. **PI WEB plugins** are browser-side PI WEB UI modules discovered from bundled, local, dev, and installed Pi-package sources. Enabling or disabling a PI WEB plugin is a PI WEB config task; installing, removing, or updating a Pi package is a Pi package-manager task. Use **Settings → Pi packages** to view configured Pi packages or install/remove/update a package. Enter only the package source, such as `npm:@scope/package`, a git/URL source, or a local path. PI WEB uses Pi's default package location, equivalent to `pi install `, and does not ask for an install location. -Use **Settings → PI WEB plugins** to enable or disable discovered PI WEB browser plugins before the browser imports them. After installing, removing, or updating a Pi package, reload the browser page to import newly discovered PI WEB browser plugins. Reload existing Pi sessions, or use `/reload` in Pi, so session-runtime resources such as extensions, skills, prompt templates, and themes are rediscovered. A routine session daemon restart is not required. +Use **Settings → PI WEB plugins** to enable or disable discovered PI WEB browser plugins before the browser imports them. After installing, removing, or updating a Pi package, type `/reload` in each idle PI WEB session to refresh Pi runtime resources such as extensions, skills, prompt templates, themes, and context/system prompt files as supported by Pi. Reload the browser page separately for newly discovered or changed PI WEB browser plugins. A routine session daemon restart is not required. ## Trust model diff --git a/src/client/src/components/SessionList.ts b/src/client/src/components/SessionList.ts index 99ca0b7..c1c34e9 100644 --- a/src/client/src/components/SessionList.ts +++ b/src/client/src/components/SessionList.ts @@ -240,7 +240,7 @@ export class SessionList extends LitElement implements KeyboardNavigableSection ${descendantCount > 0 ? html`` : null} ${session.parentSessionPath !== undefined ? html`` : null} - ${this.canReload ? html`` : null} + ${this.canReload ? html`` : null} `} ` : null} diff --git a/src/client/src/components/settings/SettingsPackagesPanel.ts b/src/client/src/components/settings/SettingsPackagesPanel.ts index efe9794..5ed9577 100644 --- a/src/client/src/components/settings/SettingsPackagesPanel.ts +++ b/src/client/src/components/settings/SettingsPackagesPanel.ts @@ -23,7 +23,7 @@ export class SettingsPackagesPanel extends LitElement {

    Pi packages

    -

    Install, remove, and update packages managed by Pi. Pi packages can provide extensions, skills, prompt templates, themes, and PI WEB browser plugins.

    +

    Install, remove, and update packages managed by Pi. Pi packages can provide extensions, skills, prompt templates, themes, context/system prompt files, and PI WEB browser plugins.

    diff --git a/src/client/src/components/settings/piPackageSettings.test.ts b/src/client/src/components/settings/piPackageSettings.test.ts index f95189f..b699910 100644 --- a/src/client/src/components/settings/piPackageSettings.test.ts +++ b/src/client/src/components/settings/piPackageSettings.test.ts @@ -37,8 +37,9 @@ describe("Pi package settings helpers", () => { it("describes the browser and session reload follow-up without requiring sessiond restarts", () => { const message = piPackageMutationFollowUpMessage("install"); - expect(message).toContain("Reload the browser page"); - expect(message).toContain("Reload existing Pi sessions"); + expect(message).toContain("Type /reload in each idle PI WEB session"); + expect(message).toContain("extensions, skills, prompt templates, themes, and context/system prompt files"); + expect(message).toContain("Reload the browser page separately for PI WEB browser plugin changes"); expect(message).not.toContain("session daemon"); expect(message).not.toContain("sessiond"); }); diff --git a/src/client/src/components/settings/piPackageSettings.ts b/src/client/src/components/settings/piPackageSettings.ts index 7bcad1d..3bb0dc4 100644 --- a/src/client/src/components/settings/piPackageSettings.ts +++ b/src/client/src/components/settings/piPackageSettings.ts @@ -54,5 +54,5 @@ export function isPiPackageOperationPending(operation: PiPackageOperationState | export function piPackageMutationFollowUpMessage(action: PiPackageMutationAction): string { const verb = action === "install" ? "installed" : action === "remove" ? "removed" : "updated"; - return `Pi package ${verb}. Reload the browser page to import newly discovered PI WEB browser plugins. Reload existing Pi sessions, or use /reload in Pi, so extensions, skills, prompt templates, and themes are rediscovered.`; + return `Pi package ${verb}. Type /reload in each idle PI WEB session to rediscover Pi runtime resources: extensions, skills, prompt templates, themes, and context/system prompt files. Reload the browser page separately for PI WEB browser plugin changes.`; } diff --git a/src/client/src/controllers/sessionController.test.ts b/src/client/src/controllers/sessionController.test.ts index f189a2b..e2beef9 100644 --- a/src/client/src/controllers/sessionController.test.ts +++ b/src/client/src/controllers/sessionController.test.ts @@ -879,7 +879,7 @@ describe("SessionController", () => { expect(state.error).toContain("requires an updated Pi-Web runtime"); }); - it("reloads the selected session, discards the cached transcript, and re-fetches history", async () => { + it("reloads the selected session from disk, discards the cached transcript, and re-fetches history", async () => { Object.defineProperty(globalThis, "localStorage", { value: new MemoryStorage(), configurable: true }); const reloadCalls: string[] = []; const messageCalls: string[] = []; @@ -917,7 +917,7 @@ describe("SessionController", () => { expect(state.error).toBe(""); }); - it("does not reload sessions when the selected machine runtime does not support it", async () => { + it("does not reload sessions from disk when the selected machine runtime does not support it", async () => { const reloadCalls: string[] = []; let state: AppState = { ...initialAppState(), @@ -943,7 +943,7 @@ describe("SessionController", () => { await controller.reloadSession(oldSession); expect(reloadCalls).toEqual([]); - expect(state.error).toContain("requires an updated Pi-Web runtime"); + expect(state.error).toContain("Reloading sessions from disk requires an updated Pi-Web runtime"); }); it("forgets archived selections when the archived section collapse clears selection", async () => { diff --git a/src/client/src/controllers/sessionController.ts b/src/client/src/controllers/sessionController.ts index 93533b6..b891227 100644 --- a/src/client/src/controllers/sessionController.ts +++ b/src/client/src/controllers/sessionController.ts @@ -484,7 +484,7 @@ export class SessionController { const machineId = selectedMachineId(this.getState()); const runtime = this.getState().machineRuntimes[machineId]; if (runtime?.ok !== true || !supportsPiWebCapability(runtime, PI_WEB_CAPABILITIES.sessionsReload)) { - this.setState({ error: "Reloading sessions requires an updated Pi-Web runtime on this machine." }); + this.setState({ error: "Reloading sessions from disk requires an updated Pi-Web runtime on this machine." }); return; } try { diff --git a/src/client/src/plugins/core/actions.ts b/src/client/src/plugins/core/actions.ts index e36edb7..6bd7f1a 100644 --- a/src/client/src/plugins/core/actions.ts +++ b/src/client/src/plugins/core/actions.ts @@ -177,8 +177,8 @@ export function createCoreActions(): PluginAction[] { }, { id: "session.reload", - title: "Reload Session", - description: "Re-read the selected session from disk to pick up entries written by another process", + title: "Reload Session from Disk", + description: "Close and re-open the selected session from its session file. Use /reload in the prompt for Pi runtime resources.", group: "Session", enabled: hasReloadableSession, disabledReason: reloadSessionDisabledReason, @@ -236,7 +236,7 @@ function reloadSessionDisabledReason(context: { state: AppState }): string | und const session = context.state.selectedSession; if (session === undefined || session.archived === true || isCachedNewSessionInfo(session)) return undefined; if (isSessionActive(context.state.status, context.state.activity)) return undefined; - return missingCapabilityReason(context.state, PI_WEB_CAPABILITIES.sessionsReload, "reload sessions"); + return missingCapabilityReason(context.state, PI_WEB_CAPABILITIES.sessionsReload, "reload sessions from disk"); } function missingCapabilityReason(state: AppState, capability: PiWebCapability, action: string): string | undefined { diff --git a/src/client/src/plugins/registry.test.ts b/src/client/src/plugins/registry.test.ts index bc40b38..ce3dbfc 100644 --- a/src/client/src/plugins/registry.test.ts +++ b/src/client/src/plugins/registry.test.ts @@ -187,18 +187,21 @@ describe("PluginRegistry", () => { expect(archivedActions.find((action) => action.id === "core:session.delete")?.enabled).toBe(false); }); - it("enables session reload only for a writable session on a capable, idle runtime", () => { + it("enables session disk reload only for a writable session on a capable, idle runtime", () => { const registry = new PluginRegistry(); registry.register({ id: "core", plugin: corePlugin }); const reloadRuntime = { local: { machineId: "local", ok: true as const, checkedAt: "now", capabilities: [PI_WEB_CAPABILITIES.sessionsReload] } }; const reloadable = registry.getActions(createContext({ selectedSession: testSession(), machineRuntimes: reloadRuntime }).context); - expect(reloadable.find((action) => action.id === "core:session.reload")?.enabled).toBe(true); + const reloadableAction = reloadable.find((action) => action.id === "core:session.reload"); + expect(reloadableAction?.enabled).toBe(true); + expect(reloadableAction?.title).toBe("Reload Session from Disk"); + expect(reloadableAction?.description).toContain("Use /reload in the prompt for Pi runtime resources"); const noCapability = registry.getActions(createContext({ selectedSession: testSession() }).context); const noCapabilityReload = noCapability.find((action) => action.id === "core:session.reload"); expect(noCapabilityReload?.enabled).toBe(false); - expect(noCapabilityReload?.disabledReason).toBe("Update and restart Pi-Web on this machine to reload sessions."); + expect(noCapabilityReload?.disabledReason).toBe("Update and restart Pi-Web on this machine to reload sessions from disk."); const archived = registry.getActions(createContext({ selectedSession: { ...testSession(), archived: true, archivedAt: "2026-05-20T00:00:00.000Z" }, machineRuntimes: reloadRuntime }).context); expect(archived.find((action) => action.id === "core:session.reload")?.enabled).toBe(false); diff --git a/src/server/sessions/builtinCommands.ts b/src/server/sessions/builtinCommands.ts index 0a740d0..a56e375 100644 --- a/src/server/sessions/builtinCommands.ts +++ b/src/server/sessions/builtinCommands.ts @@ -20,7 +20,7 @@ export const BUILTIN_COMMANDS: ClientCommand[] = [ { name: "new", description: "Start a new session", source: "builtin" }, { name: "compact", description: "Manually compact session context", source: "builtin" }, { name: "resume", description: "Resume a different session", source: "builtin" }, - { name: "reload", description: "Reload keybindings, extensions, skills, prompts, and themes", source: "builtin" }, + { name: "reload", description: "Reload Pi runtime resources for this session", source: "builtin" }, { name: "quit", description: "Quit pi", source: "builtin" }, ]; diff --git a/src/server/sessions/piSessionService.test.ts b/src/server/sessions/piSessionService.test.ts index b99cbab..3206b6b 100644 --- a/src/server/sessions/piSessionService.test.ts +++ b/src/server/sessions/piSessionService.test.ts @@ -63,7 +63,7 @@ function fakeRuntime(sessionId = "session-1", patch: Partial = {}) const customMessageCalls: { message: { customType: string; content: string; display: boolean; details?: unknown }; options: unknown }[] = []; const bindExtensionCalls: unknown[] = []; const listeners: ((event: unknown) => void)[] = []; - const calls = { abort: 0, bindExtensions: bindExtensionCalls, clearQueue: 0, dispose: 0, prompt: promptCalls, sendCustomMessage: customMessageCalls }; + const calls = { abort: 0, bindExtensions: bindExtensionCalls, clearQueue: 0, dispose: 0, prompt: promptCalls, reload: 0, sendCustomMessage: customMessageCalls }; const session: TestSession = { sessionId, sessionFile: `/tmp/${sessionId}.jsonl`, @@ -94,6 +94,10 @@ function fakeRuntime(sessionId = "session-1", patch: Partial = {}) }, getSessionStats: () => ({ sessionId, totalMessages: 0, userMessages: 0, assistantMessages: 0, toolCalls: 0, tokens: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 }, cost: 0 }), getContextUsage: () => undefined, + reload: () => { + calls.reload += 1; + return Promise.resolve(); + }, prompt: (text: string, options: unknown) => { calls.prompt.push({ text, options }); return Promise.resolve(); @@ -738,6 +742,29 @@ describe("PiSessionService", () => { await service.dispose(); }); + it("runs /reload by refreshing the active runtime resources in place", async () => { + const hub = new CapturingSessionEventHub(); + const fake = fakeRuntime("runtime-reload-session"); + const service = new PiSessionService(hub, { + createAgentRuntime: runtimeCreator(fake.runtime), + sessionManager: sessionGateway([sessionRecord("runtime-reload-session")]), + heartbeatIntervalMs: 60_000, + }); + + await expect(service.runCommand(sessionRef("runtime-reload-session"), "/reload")).resolves.toEqual({ + type: "done", + message: "Session runtime resources reloaded. Extensions, skills, prompt templates, themes, and context/system prompt files are refreshed for this session. Reload the browser page separately for PI WEB browser plugin changes.", + }); + + expect(fake.calls.reload).toBe(1); + expect(fake.calls.abort).toBe(0); + expect(fake.calls.dispose).toBe(0); + expect(hub.globalEvents.some((event) => event.type === "activity.update" && event.activity.sessionId === "runtime-reload-session" && event.activity.label === "resources reloaded")).toBe(true); + expect(hub.globalEvents.some((event) => event.type === "status.update" && event.status.sessionId === "runtime-reload-session")).toBe(true); + + await service.dispose(); + }); + it("reloads a session by closing the active runtime and re-opening it from disk", async () => { const first = fakeRuntime("reload-session"); const second = fakeRuntime("reload-session"); diff --git a/src/server/sessions/piSessionService.ts b/src/server/sessions/piSessionService.ts index 391e6ae..dc507f6 100644 --- a/src/server/sessions/piSessionService.ts +++ b/src/server/sessions/piSessionService.ts @@ -217,6 +217,7 @@ export interface PiAgentSession { compact(instructions?: string): Promise<{ summary: string; tokensBefore: number }>; getUserMessagesForForking(): readonly { entryId: string; text: string }[]; getSessionStats(): { sessionId: string; totalMessages: number; userMessages: number; assistantMessages: number; toolCalls: number; tokens: ClientSessionStatus["tokens"]; cost: number }; + reload(): Promise; getContextUsage(): ClientSessionStatus["contextUsage"] | undefined; prompt(text: string, options?: { streamingBehavior?: "steer" | "followUp"; images?: ImageContent[] }): Promise; sendCustomMessage(message: { customType: string; content: string; display: boolean; details?: unknown }, options?: { triggerTurn?: boolean; deliverAs?: "steer" | "followUp" | "nextTurn" }): Promise; @@ -423,6 +424,7 @@ export class PiSessionService { this.publishActivity(session, result === "success" ? "compaction complete" : "compaction failed", result === "success" ? "idle" : "error", detail); this.publishStatus(session); }, + reloadSession: (session) => this.reloadSessionRuntime(session), }, { listSessionNames: (cwd) => this.listSessionNames(cwd) }, ); @@ -1118,6 +1120,22 @@ export class PiSessionService { return this.commandService.respond(active.runtime.session.sessionId, requestId, value); } + private async reloadSessionRuntime(session: PiAgentSession): Promise { + if (this.hasActiveWork(session)) throw new Error("Stop current session activity before reloading"); + this.publishActivity(session, "reloading resources", "active"); + try { + await session.reload(); + this.publishActivity(session, "resources reloaded", "idle"); + this.publishStatus(session); + } catch (error: unknown) { + const message = error instanceof Error ? error.message : String(error); + this.publishActivity(session, "reload failed", "error", message); + this.events.publish(session.sessionId, { type: "session.error", message }); + this.publishStatus(session); + throw error; + } + } + async archive(ref: PiSessionLookup): Promise { const session = await this.getOrOpen(ref); if (this.hasActiveWork(session)) throw new Error("Stop current session activity before archiving"); diff --git a/src/server/sessions/sessionCommandService.test.ts b/src/server/sessions/sessionCommandService.test.ts index f3a3cb1..7ca503c 100644 --- a/src/server/sessions/sessionCommandService.test.ts +++ b/src/server/sessions/sessionCommandService.test.ts @@ -106,6 +106,30 @@ describe("SessionCommandService", () => { expect(active.runtime.session.compact).toHaveBeenCalledWith("focus on tests"); }); + it("reloads runtime resources through the injected lifecycle callback", async () => { + const active = activeSession(); + const reloadSession = vi.fn(async () => { await Promise.resolve(); }); + const service = new SessionCommandService(() => getActive(active), vi.fn(), eventPublisher(), { reloadSession }); + + await expect(service.run("s1", "/reload")).resolves.toEqual({ + type: "done", + message: "Session runtime resources reloaded. Extensions, skills, prompt templates, themes, and context/system prompt files are refreshed for this session. Reload the browser page separately for PI WEB browser plugin changes.", + }); + expect(reloadSession).toHaveBeenCalledWith(active.runtime.session); + }); + + it("rejects runtime reload while the session has active work", async () => { + const active = activeSession({ isBashRunning: true }); + const reloadSession = vi.fn(async () => { await Promise.resolve(); }); + const service = new SessionCommandService(() => getActive(active), vi.fn(), eventPublisher(), { reloadSession }); + + await expect(service.run("s1", "/reload")).resolves.toEqual({ + type: "unsupported", + message: "Cannot reload while the session is active. Stop current activity before reloading.", + }); + expect(reloadSession).not.toHaveBeenCalled(); + }); + it("creates fork selection requests from newest message to oldest and responds with selected entry", async () => { const active = activeSession({ getUserMessagesForForking: vi.fn(() => [ diff --git a/src/server/sessions/sessionCommandService.ts b/src/server/sessions/sessionCommandService.ts index 2c1f7b6..ad90f51 100644 --- a/src/server/sessions/sessionCommandService.ts +++ b/src/server/sessions/sessionCommandService.ts @@ -47,6 +47,12 @@ export interface CommandEventPublisher { publishGlobal?(event: Extract): void; } +export interface SessionCommandLifecycle { + onCompactionStart?: (session: TSession) => void; + onCompactionEnd?: (session: TSession, result: "success" | "error", detail?: string) => void; + reloadSession?: (session: TSession) => Promise; +} + export interface SessionCommandNaming { listSessionNames?: (cwd: string) => Promise; } @@ -65,10 +71,7 @@ export class SessionCommandService, private readonly prompt: (sessionId: string, text: string) => Promise, private readonly events: CommandEventPublisher, - private readonly lifecycle: { - onCompactionStart?: (session: TSession) => void; - onCompactionEnd?: (session: TSession, result: "success" | "error", detail?: string) => void; - } = {}, + private readonly lifecycle: SessionCommandLifecycle = {}, private readonly naming: SessionCommandNaming = {}, ) {} @@ -93,6 +96,7 @@ export class SessionCommandService { + if (sessionHasActiveWork(session)) return { type: "unsupported", message: "Cannot reload while the session is active. Stop current activity before reloading." }; + if (this.lifecycle.reloadSession === undefined) return { type: "unsupported", message: "/reload is not available for this session runtime." }; + + try { + await this.lifecycle.reloadSession(session); + } catch (error: unknown) { + const message = error instanceof Error ? error.message : String(error); + return { type: "unsupported", message: `Reload failed: ${message}` }; + } + return { type: "done", message: "Session runtime resources reloaded. Extensions, skills, prompt templates, themes, and context/system prompt files are refreshed for this session. Reload the browser page separately for PI WEB browser plugin changes." }; + } + private async clone(active: CommandActiveSession): Promise { if (sessionHasActiveWork(active.runtime.session)) return forkActiveUnsupported("clone"); const leafId = active.runtime.session.sessionManager.getLeafId();