Archived
feat(sessions): expose reload as a command-palette action and disable when busy
Add a "Reload Session" core action so reload is keyboard-accessible and can be assigned a custom shortcut, gated by the same guards as the menu item (writable session, sessions.reload capability, not currently busy). Disable the Reload menu entry while the session has active work, mirroring the server guard and the archived-delete control, so users get a clear reason instead of an error toast. Co-authored-by: Claude <[email protected]>
This commit is contained in:
co-authored by
Claude
parent
82db15f894
commit
9159da9353
@@ -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);
|
||||
|
||||
@@ -228,7 +228,7 @@ export class SessionList extends LitElement implements KeyboardNavigableSection
|
||||
<button class="danger" title=${this.canDeleteArchived ? "Permanently delete archived session" : this.archivedDeleteUnavailableMessage} ?disabled=${!this.canDeleteArchived} @click=${() => { this.openMenuSessionId = undefined; this.confirmDeleteArchived(session); }}>Delete archived session</button>
|
||||
`
|
||||
: html`
|
||||
${this.canReload ? html`<button title="Reload session from disk" @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" : "Reload session from disk"} ?disabled=${isSessionActive(this.statuses[session.id], this.activities[session.id])} @click=${() => { this.openMenuSessionId = undefined; this.onReload?.(session); }}>Reload</button>` : null}
|
||||
${session.parentSessionPath !== undefined ? html`<button title="Detach from parent" @click=${() => { this.openMenuSessionId = undefined; this.onDetachParent?.(session); }}>Detach from parent</button>` : null}
|
||||
<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}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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<AppState> = {}) {
|
||||
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> = {}): 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" };
|
||||
}
|
||||
|
||||
@@ -106,6 +106,7 @@ export interface PluginRuntimeContext {
|
||||
deleteWorkspace: (workspace?: Workspace) => void | Promise<void>;
|
||||
startSession: () => void | Promise<void>;
|
||||
archiveSession: () => void | Promise<void>;
|
||||
reloadSession: () => void | Promise<void>;
|
||||
deleteCachedNewSession: () => void | Promise<void>;
|
||||
stopActiveWork: () => void | Promise<void>;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user