diff --git a/.changeset/session-reload-from-disk.md b/.changeset/session-reload-from-disk.md
index 7f86181..e0e57ac 100644
--- a/.changeset/session-reload-from-disk.md
+++ b/.changeset/session-reload-from-disk.md
@@ -4,6 +4,6 @@
Add a **Reload** action to the session three-dot menu that re-reads the session from disk. The session daemon keeps an in-memory `SessionManager` per session and never re-reads the session file, so when the same session is also driven by another process (for example the `pi` CLI), new on-disk entries were invisible to the web UI and the tail of the conversation appeared truncated. Reloading closes the active session, re-opens it from disk, discards the cached transcript, and re-fetches the history.
-Reload refuses to run while the session has work in progress and on archived (read-only) sessions, and is gated behind a new `sessions.reload` runtime capability so it only appears for machines whose Pi-Web runtime supports it.
+Reload is also available from the command palette as **Reload Session**, so it can be triggered from the keyboard and assigned a custom shortcut. Reload refuses to run while the session has work in progress and on archived (read-only) sessions, and is gated behind a new `sessions.reload` runtime capability so it only appears for machines whose Pi-Web runtime supports it (both the menu item and the palette action are disabled otherwise).
Note: this changes a session daemon code path, so `pi-web-sessiond.service` must be restarted manually for the server side of this change to take effect.
diff --git a/src/client/src/components/PiWebApp.ts b/src/client/src/components/PiWebApp.ts
index db9132e..43dfa04 100644
--- a/src/client/src/components/PiWebApp.ts
+++ b/src/client/src/components/PiWebApp.ts
@@ -1398,6 +1398,7 @@ export class PiWebApp extends LitElement {
deleteWorkspace: (workspace) => this.deleteWorkspace(workspace),
startSession: () => this.withChatScrollTransition(() => this.sessions.startSession()),
archiveSession: () => this.sessions.archiveSession(),
+ reloadSession: () => this.sessions.reloadSession(),
deleteCachedNewSession: () => this.sessions.deleteCachedNewSession(),
stopActiveWork: () => this.sessions.stopActiveWork(),
}, createContext);
diff --git a/src/client/src/components/SessionList.ts b/src/client/src/components/SessionList.ts
index bc78b67..d9229ac 100644
--- a/src/client/src/components/SessionList.ts
+++ b/src/client/src/components/SessionList.ts
@@ -228,7 +228,7 @@ export class SessionList extends LitElement implements KeyboardNavigableSection
`
: html`
- ${this.canReload ? html`` : null}
+ ${this.canReload ? html`` : null}
${session.parentSessionPath !== undefined ? html`` : null}
${descendantCount > 0 ? html`` : null}
diff --git a/src/client/src/plugins/core/actions.ts b/src/client/src/plugins/core/actions.ts
index e46b84f..90f0337 100644
--- a/src/client/src/plugins/core/actions.ts
+++ b/src/client/src/plugins/core/actions.ts
@@ -1,6 +1,8 @@
import { isSessionActive } from "../../../../shared/activity";
+import { PI_WEB_CAPABILITIES, supportsPiWebCapability } from "../../../../shared/capabilities";
import type { AppState } from "../../appState";
import { isCachedNewSessionInfo } from "../../cachedNewSessions";
+import { selectedMachineId } from "../../controllers/types";
import { isWorkspaceDeletionPending } from "../../workspaceDeletion";
import type { PluginAction } from "../types";
@@ -173,6 +175,14 @@ export function createCoreActions(): PluginAction[] {
enabled: hasArchivableSession,
run: (context) => context.archiveSession(),
},
+ {
+ id: "session.reload",
+ title: "Reload Session",
+ description: "Re-read the selected session from disk to pick up entries written by another process",
+ group: "Session",
+ enabled: hasReloadableSession,
+ run: (context) => context.reloadSession(),
+ },
{
id: "session.delete",
title: "Delete New Session",
@@ -213,3 +223,11 @@ function hasArchivableSession(context: { state: AppState }): boolean {
function hasCachedNewSession(context: { state: AppState }): boolean {
return isCachedNewSessionInfo(context.state.selectedSession);
}
+
+function hasReloadableSession(context: { state: AppState }): boolean {
+ const session = context.state.selectedSession;
+ if (session === undefined || session.archived === true || isCachedNewSessionInfo(session)) return false;
+ const runtime = context.state.machineRuntimes[selectedMachineId(context.state)];
+ if (runtime?.ok !== true || !supportsPiWebCapability(runtime, PI_WEB_CAPABILITIES.sessionsReload)) return false;
+ return !isSessionActive(context.state.status, context.state.activity);
+}
diff --git a/src/client/src/plugins/registry.test.ts b/src/client/src/plugins/registry.test.ts
index 3853f5a..7be8258 100644
--- a/src/client/src/plugins/registry.test.ts
+++ b/src/client/src/plugins/registry.test.ts
@@ -1,8 +1,9 @@
import { html } from "lit";
import { describe, expect, it, vi } from "vitest";
-import type { FileContentResponse, SessionInfo, Workspace } from "../api";
+import type { FileContentResponse, SessionInfo, SessionStatus, Workspace } from "../api";
import { initialAppState, type AppState } from "../appState";
import { markCachedNewSessionInfo } from "../cachedNewSessions";
+import { PI_WEB_CAPABILITIES } from "../../../shared/capabilities";
import { machineScopedPluginId } from "../../../shared/machinePluginIds";
import { corePlugin } from "./core";
import { PluginRegistry } from "./registry";
@@ -42,6 +43,7 @@ function createContext(statePatch: Partial = {}) {
deleteWorkspace: vi.fn(() => { calls.push("deleteWorkspace"); }),
startSession: vi.fn(() => { calls.push("startSession"); }),
archiveSession: vi.fn(() => { calls.push("archiveSession"); }),
+ reloadSession: vi.fn(() => { calls.push("reloadSession"); }),
deleteCachedNewSession: vi.fn(() => { calls.push("deleteCachedNewSession"); }),
stopActiveWork: vi.fn(() => { calls.push("stopActiveWork"); }),
};
@@ -149,6 +151,35 @@ 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", () => {
+ 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 noCapability = registry.getActions(createContext({ selectedSession: testSession() }).context);
+ expect(noCapability.find((action) => action.id === "core:session.reload")?.enabled).toBe(false);
+
+ 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);
+
+ const busy = registry.getActions(createContext({ selectedSession: testSession(), machineRuntimes: reloadRuntime, status: testStatus({ isStreaming: true }) }).context);
+ expect(busy.find((action) => action.id === "core:session.reload")?.enabled).toBe(false);
+ });
+
+ it("routes session reload through the runtime context", () => {
+ const registry = new PluginRegistry();
+ registry.register({ id: "core", plugin: corePlugin });
+ const { context, calls } = createContext({ selectedSession: testSession(), machineRuntimes: { local: { machineId: "local", ok: true, checkedAt: "now", capabilities: [PI_WEB_CAPABILITIES.sessionsReload] } } });
+ const action = registry.getActions(context).find((candidate) => candidate.id === "core:session.reload");
+
+ if (action !== undefined) void action.run();
+
+ expect(calls).toEqual(["reloadSession"]);
+ });
+
it("routes browser-cached new session delete through the runtime context", () => {
const registry = new PluginRegistry();
registry.register({ id: "core", plugin: corePlugin });
@@ -568,6 +599,20 @@ function testFileContent(path = "README.md"): FileContentResponse {
};
}
+function testStatus(patch: Partial = {}): SessionStatus {
+ return {
+ sessionId: "s1",
+ isStreaming: false,
+ isCompacting: false,
+ isBashRunning: false,
+ pendingMessageCount: 0,
+ queuedMessages: [],
+ tokens: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
+ cost: 0,
+ ...patch,
+ };
+}
+
function testMachine(id: string) {
return { id, name: id, kind: id === "local" ? "local" as const : "remote" as const, createdAt: "2026-05-20T00:00:00.000Z", updatedAt: "2026-05-20T00:00:00.000Z" };
}
diff --git a/src/client/src/plugins/types.ts b/src/client/src/plugins/types.ts
index 4b954d7..2b44063 100644
--- a/src/client/src/plugins/types.ts
+++ b/src/client/src/plugins/types.ts
@@ -106,6 +106,7 @@ export interface PluginRuntimeContext {
deleteWorkspace: (workspace?: Workspace) => void | Promise;
startSession: () => void | Promise;
archiveSession: () => void | Promise;
+ reloadSession: () => void | Promise;
deleteCachedNewSession: () => void | Promise;
stopActiveWork: () => void | Promise;
}