Archived
feat: add session runtime reload command
This commit is contained in:
@@ -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.
|
||||
@@ -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
|
||||
|
||||
+1
-1
@@ -143,7 +143,7 @@
|
||||
<li><code>spawnSessions</code> / <code>subsessions</code>: restart the session daemon.</li>
|
||||
<li><code>pathAccess</code>: applies on the next request; existing file views may need a browser refresh.</li>
|
||||
<li><code>plugins</code>: reload the browser tab after changing PI WEB plugin enablement.</li>
|
||||
<li>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 <code>/reload</code> in Pi) for session-runtime resources. A routine session daemon restart is not required.</li>
|
||||
<li>Pi package install/remove/update: not a PI WEB config key; after a mutation, type <code>/reload</code> 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.</li>
|
||||
<li><code>shortcuts</code>: saved settings apply in the browser after config refresh/save.</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
+7
-6
@@ -130,8 +130,8 @@
|
||||
<h2>Pi packages vs PI WEB plugins</h2>
|
||||
<p>
|
||||
<strong>Pi packages</strong> are packages managed by Pi (<code>pi install</code>, <code>pi remove</code>,
|
||||
<code>pi update</code>). 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.
|
||||
<code>pi update</code>). 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.
|
||||
</p>
|
||||
<p>
|
||||
<strong>PI WEB plugins</strong> are browser-side UI modules discovered from bundled, local, dev, and
|
||||
@@ -146,10 +146,11 @@
|
||||
</p>
|
||||
<p>
|
||||
Use <strong>Settings → PI WEB plugins</strong> 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
|
||||
<code>/reload</code> 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
|
||||
<code>/reload</code> 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.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
|
||||
+2
-2
@@ -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 <source>`, 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
|
||||
|
||||
|
||||
@@ -240,7 +240,7 @@ export class SessionList extends LitElement implements KeyboardNavigableSection
|
||||
<button title="Archive session" @click=${() => { this.openMenuSessionId = undefined; this.onArchive?.(session); }}>Archive</button>
|
||||
${descendantCount > 0 ? html`<button title="Archive this session and its descendants" @click=${() => { this.openMenuSessionId = undefined; this.confirmArchiveWithDescendants(session, descendantCount); }}>Archive with descendants (${descendantCount})</button>` : null}
|
||||
${session.parentSessionPath !== undefined ? html`<button title="Detach from parent" @click=${() => { this.openMenuSessionId = undefined; this.onDetachParent?.(session); }}>Detach from parent</button>` : null}
|
||||
${this.canReload ? html`<button title=${isSessionActive(this.statuses[session.id], this.activities[session.id]) ? "Stop current session activity before reloading" : "Reload session from disk"} ?disabled=${isSessionActive(this.statuses[session.id], this.activities[session.id])} @click=${() => { this.openMenuSessionId = undefined; this.onReload?.(session); }}>Reload</button>` : null}
|
||||
${this.canReload ? html`<button title=${isSessionActive(this.statuses[session.id], this.activities[session.id]) ? "Stop current session activity before reloading from disk" : "Reload session from disk without refreshing Pi runtime resources"} ?disabled=${isSessionActive(this.statuses[session.id], this.activities[session.id])} @click=${() => { this.openMenuSessionId = undefined; this.onReload?.(session); }}>Reload from disk</button>` : null}
|
||||
`}
|
||||
</div>
|
||||
` : null}
|
||||
|
||||
@@ -23,7 +23,7 @@ export class SettingsPackagesPanel extends LitElement {
|
||||
<div class="section-heading">
|
||||
<div>
|
||||
<h2>Pi packages</h2>
|
||||
<p>Install, remove, and update packages managed by Pi. Pi packages can provide extensions, skills, prompt templates, themes, and PI WEB browser plugins.</p>
|
||||
<p>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.</p>
|
||||
</div>
|
||||
<button class="secondary" ?disabled=${this.loading || this.isOperating} @click=${() => { void this.onReload?.(); }}>Reload</button>
|
||||
</div>
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
|
||||
@@ -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.`;
|
||||
}
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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" },
|
||||
];
|
||||
|
||||
|
||||
@@ -63,7 +63,7 @@ function fakeRuntime(sessionId = "session-1", patch: Partial<TestSession> = {})
|
||||
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<TestSession> = {})
|
||||
},
|
||||
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");
|
||||
|
||||
@@ -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<void>;
|
||||
getContextUsage(): ClientSessionStatus["contextUsage"] | undefined;
|
||||
prompt(text: string, options?: { streamingBehavior?: "steer" | "followUp"; images?: ImageContent[] }): Promise<void>;
|
||||
sendCustomMessage(message: { customType: string; content: string; display: boolean; details?: unknown }, options?: { triggerTurn?: boolean; deliverAs?: "steer" | "followUp" | "nextTurn" }): Promise<void>;
|
||||
@@ -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<void> {
|
||||
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<void> {
|
||||
const session = await this.getOrOpen(ref);
|
||||
if (this.hasActiveWork(session)) throw new Error("Stop current session activity before archiving");
|
||||
|
||||
@@ -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(() => [
|
||||
|
||||
@@ -47,6 +47,12 @@ export interface CommandEventPublisher {
|
||||
publishGlobal?(event: Extract<SessionUiEvent, { type: "session.name" }>): void;
|
||||
}
|
||||
|
||||
export interface SessionCommandLifecycle<TSession extends CommandSession = CommandSession> {
|
||||
onCompactionStart?: (session: TSession) => void;
|
||||
onCompactionEnd?: (session: TSession, result: "success" | "error", detail?: string) => void;
|
||||
reloadSession?: (session: TSession) => Promise<void>;
|
||||
}
|
||||
|
||||
export interface SessionCommandNaming {
|
||||
listSessionNames?: (cwd: string) => Promise<readonly string[]>;
|
||||
}
|
||||
@@ -65,10 +71,7 @@ export class SessionCommandService<TSession extends CommandSession = CommandSess
|
||||
private readonly getActive: GetCommandActiveSession<TSession>,
|
||||
private readonly prompt: (sessionId: string, text: string) => Promise<void>,
|
||||
private readonly events: CommandEventPublisher,
|
||||
private readonly lifecycle: {
|
||||
onCompactionStart?: (session: TSession) => void;
|
||||
onCompactionEnd?: (session: TSession, result: "success" | "error", detail?: string) => void;
|
||||
} = {},
|
||||
private readonly lifecycle: SessionCommandLifecycle<TSession> = {},
|
||||
private readonly naming: SessionCommandNaming = {},
|
||||
) {}
|
||||
|
||||
@@ -93,6 +96,7 @@ export class SessionCommandService<TSession extends CommandSession = CommandSess
|
||||
if (name === "session") return { type: "done", message: formatSessionStats(session) };
|
||||
if (name === "name") return this.nameSession(active, rest);
|
||||
if (name === "compact") return this.compact(session, rest);
|
||||
if (name === "reload") return this.reload(session);
|
||||
if (name === "clone") return this.clone(active);
|
||||
if (name === "fork") return this.fork(active);
|
||||
|
||||
@@ -140,6 +144,19 @@ export class SessionCommandService<TSession extends CommandSession = CommandSess
|
||||
return { type: "done", message: "Compaction started…" };
|
||||
}
|
||||
|
||||
private async reload(session: TSession): Promise<ClientCommandResult> {
|
||||
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<TSession>): Promise<ClientCommandResult> {
|
||||
if (sessionHasActiveWork(active.runtime.session)) return forkActiveUnsupported("clone");
|
||||
const leafId = active.runtime.session.sessionManager.getLeafId();
|
||||
|
||||
Reference in New Issue
Block a user