Archived
feat(sessions): add Reload action to refresh session from disk
Sessiond caches the in-memory SessionManager and never re-reads the session file. When the same session is also being edited by another process (e.g. the pi CLI), new entries on disk are invisible to the web UI \u2014 the tail of the conversation gets cut. Add a manual Reload action in the session three-dot menu: - Server: PiSessionService.reload(sessionId) closes the active session and re-opens it from disk, then publishes a fresh status. Exposed as POST /api/.../sessions/:sessionId/reload. - Client: api.reloadSession, SessionController.reloadSession which discards the cached transcript and re-runs selectSession so the history page is re-fetched. - ChatTranscriptStore gains discard(sessionId) and the history cache adapter gains optional remove(sessionId). - SessionList shows a Reload entry for non-archived, non-cached sessions; plumbed through AppNavigationPanel and PiWebApp. Note: pi-web-sessiond.service must be restarted manually after this change since the session daemon code path is affected.
This commit is contained in:
committed by
Federico Jaramillo Martinez
parent
ca30c970a7
commit
ea1ec1b595
@@ -10,6 +10,7 @@ import {
|
||||
parseCommandResult,
|
||||
parseDeleted,
|
||||
parseDetached,
|
||||
parseReloaded,
|
||||
parseFileContentResponse,
|
||||
parseFileSuggestion,
|
||||
parseFileTreeResponse,
|
||||
@@ -143,6 +144,7 @@ export const sessionsApi = {
|
||||
restore: (session: SessionLookup, machineId = "local") => request(sessionUrl(session, "restore", machineId), parseRestored, { method: "POST", body: sessionBody(session) }),
|
||||
deleteArchived: (session: SessionLookup, machineId = "local") => request(sessionBaseQueryUrl(session, machineId), parseDeleted, { method: "DELETE" }),
|
||||
detachParent: (session: SessionLookup, machineId = "local") => request(sessionUrl(session, "detach-parent", machineId), parseDetached, { method: "POST", body: sessionBody(session) }),
|
||||
reloadSession: (session: SessionLookup, machineId = "local") => request(sessionUrl(session, "reload", machineId), parseReloaded, { method: "POST", body: sessionBody(session) }),
|
||||
authProviders: (options?: { mode?: "login" | "logout"; authType?: "oauth" | "api_key"; machineId?: string }) => {
|
||||
const params = new URLSearchParams();
|
||||
if (options?.mode !== undefined) params.set("mode", options.mode);
|
||||
|
||||
@@ -717,6 +717,12 @@ export function parseDetached(value: unknown): { detached: true } {
|
||||
return { detached: true };
|
||||
}
|
||||
|
||||
export function parseReloaded(value: unknown): { reloaded: true } {
|
||||
const record = requireRecord(value);
|
||||
if (record["reloaded"] !== true) throw new Error("Expected reloaded response");
|
||||
return { reloaded: true };
|
||||
}
|
||||
|
||||
function optionalNumber(record: Record<string, unknown>, key: string): number | undefined {
|
||||
const value = record[key];
|
||||
if (value === undefined) return undefined;
|
||||
|
||||
@@ -35,6 +35,14 @@ export function writeChatHistoryCache(sessionId: string, page: RawMessagePage):
|
||||
}
|
||||
}
|
||||
|
||||
export function removeChatHistoryCache(sessionId: string): void {
|
||||
try {
|
||||
sessionStorage.removeItem(cacheKey(sessionId));
|
||||
} catch {
|
||||
// Ignore storage access errors; cache may simply be unavailable.
|
||||
}
|
||||
}
|
||||
|
||||
export function mergeChatHistory(existing: RawMessagePage | undefined, incoming: RawMessagePage): RawMessagePage {
|
||||
if (existing === undefined || !isValidMessagePage(existing)) return incoming;
|
||||
if (!isValidMessagePage(incoming)) return existing;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { normalizeMessages } from "./chatMessages";
|
||||
import { applyTranscriptEvent } from "./chatTranscript";
|
||||
import { mergeChatHistory, readChatHistoryCache, writeChatHistoryCache, type RawMessagePage } from "./chatHistoryCache";
|
||||
import { mergeChatHistory, readChatHistoryCache, removeChatHistoryCache, writeChatHistoryCache, type RawMessagePage } from "./chatHistoryCache";
|
||||
import type { ChatLine } from "./components/shared";
|
||||
import type { SessionUiEvent } from "./sessionSocket";
|
||||
|
||||
@@ -16,11 +16,13 @@ export interface ChatTranscriptView {
|
||||
export interface ChatHistoryCacheAdapter {
|
||||
read(sessionId: string): RawMessagePage | undefined;
|
||||
write(sessionId: string, page: RawMessagePage): void;
|
||||
remove?(sessionId: string): void;
|
||||
}
|
||||
|
||||
const browserChatHistoryCache: ChatHistoryCacheAdapter = {
|
||||
read: readChatHistoryCache,
|
||||
write: writeChatHistoryCache,
|
||||
remove: removeChatHistoryCache,
|
||||
};
|
||||
|
||||
export class ChatTranscriptStore {
|
||||
@@ -43,6 +45,11 @@ export class ChatTranscriptStore {
|
||||
return applyTranscriptEvent(messages, event);
|
||||
}
|
||||
|
||||
discard(sessionId: string): void {
|
||||
this.rawHistoryPages.delete(sessionId);
|
||||
this.cache.remove?.(sessionId);
|
||||
}
|
||||
|
||||
rawHistoryPage(sessionId: string): RawMessagePage | undefined {
|
||||
const cached = this.rawHistoryPages.get(sessionId) ?? this.cache.read(sessionId);
|
||||
if (cached !== undefined) this.rawHistoryPages.set(sessionId, cached);
|
||||
|
||||
@@ -1060,6 +1060,7 @@ export class PiWebApp extends LitElement {
|
||||
.onDeleteArchivedSession=${(session: SessionInfo) => this.sessions.deleteArchivedSessions([session])}
|
||||
.onDeleteArchivedSessions=${(sessions: SessionInfo[]) => this.sessions.deleteArchivedSessions(sessions)}
|
||||
.onDetachParentSession=${(session: SessionInfo) => this.sessions.detachParent(session)}
|
||||
.onReloadSession=${(session: SessionInfo) => this.sessions.reloadSession(session)}
|
||||
.onFocusNavigationTarget=${(target: NavigationFocusTarget) => { void this.focusNavigationTarget(target); }}
|
||||
.onCancelKeyboardNavigation=${() => { void this.focusChatComposer(); }}
|
||||
></app-navigation-panel>
|
||||
|
||||
@@ -49,6 +49,7 @@ export class SessionList extends LitElement implements KeyboardNavigableSection
|
||||
@property({ attribute: false }) onDeleteArchived?: (session: SessionInfo) => void | Promise<void>;
|
||||
@property({ attribute: false }) onDeleteArchivedMany?: (sessions: SessionInfo[]) => void | Promise<void>;
|
||||
@property({ attribute: false }) onDetachParent?: (session: SessionInfo) => void;
|
||||
@property({ attribute: false }) onReload?: (session: SessionInfo) => void;
|
||||
|
||||
@state() private openMenuSessionId: string | undefined;
|
||||
@state() private menuStyle = "";
|
||||
@@ -226,6 +227,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`
|
||||
<button title="Reload session from disk" @click=${() => { this.openMenuSessionId = undefined; this.onReload?.(session); }}>Reload</button>
|
||||
${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}
|
||||
|
||||
@@ -61,6 +61,7 @@ export class AppNavigationPanel extends LitElement {
|
||||
@property({ attribute: false }) onDeleteArchivedSession?: (session: SessionInfo) => void | Promise<void>;
|
||||
@property({ attribute: false }) onDeleteArchivedSessions?: (sessions: SessionInfo[]) => void | Promise<void>;
|
||||
@property({ attribute: false }) onDetachParentSession?: (session: SessionInfo) => void | Promise<void>;
|
||||
@property({ attribute: false }) onReloadSession?: (session: SessionInfo) => void | Promise<void>;
|
||||
@property({ attribute: false }) onArchivedCollapsed?: () => void | Promise<void>;
|
||||
@property({ attribute: false }) onSelectMachine?: (machine: Machine) => void | Promise<void>;
|
||||
@property({ attribute: false }) onRemoveMachine?: (machine: Machine) => void | Promise<void>;
|
||||
@@ -171,6 +172,7 @@ export class AppNavigationPanel extends LitElement {
|
||||
.onDeleteArchived=${(session: SessionInfo) => this.onDeleteArchivedSession?.(session)}
|
||||
.onDeleteArchivedMany=${(sessions: SessionInfo[]) => this.onDeleteArchivedSessions?.(sessions)}
|
||||
.onDetachParent=${(session: SessionInfo) => this.onDetachParentSession?.(session)}
|
||||
.onReload=${(session: SessionInfo) => this.onReloadSession?.(session)}
|
||||
.onFocusPreviousSection=${() => { this.focusPreviousFrom("sessions"); }}
|
||||
.onFocusNextSection=${() => { this.focusNextFrom("sessions"); }}
|
||||
.onCancelKeyboardNavigation=${() => { this.cancelKeyboardNavigation(); }}
|
||||
|
||||
@@ -379,6 +379,19 @@ export class SessionController {
|
||||
}
|
||||
}
|
||||
|
||||
async reloadSession(session = this.getState().selectedSession) {
|
||||
if (session === undefined) return;
|
||||
try {
|
||||
await this.api.reloadSession(session.id, selectedMachineId(this.getState()));
|
||||
this.transcripts.discard(this.sessionCacheKey(session.id));
|
||||
if (this.getState().selectedSession?.id === session.id) {
|
||||
await this.selectSession(session, { updateUrl: false });
|
||||
}
|
||||
} catch (error) {
|
||||
this.setState({ error: String(error) });
|
||||
}
|
||||
}
|
||||
|
||||
async detachParent(session = this.getState().selectedSession) {
|
||||
if (session?.parentSessionPath === undefined) return;
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user