Archived
Cache loaded chat history in browser
This commit is contained in:
@@ -0,0 +1,71 @@
|
|||||||
|
const CACHE_PREFIX = "pi-web:chat-history:";
|
||||||
|
const CACHE_TTL_MS = 30 * 60 * 1000;
|
||||||
|
|
||||||
|
export interface RawMessagePage {
|
||||||
|
messages: unknown[];
|
||||||
|
start: number;
|
||||||
|
total: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CachedChatHistory extends RawMessagePage {
|
||||||
|
savedAt: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function readChatHistoryCache(sessionId: string): RawMessagePage | undefined {
|
||||||
|
try {
|
||||||
|
const raw = sessionStorage.getItem(cacheKey(sessionId));
|
||||||
|
if (raw === null || raw === "") return undefined;
|
||||||
|
const parsed: unknown = JSON.parse(raw);
|
||||||
|
if (!isCachedHistory(parsed)) return undefined;
|
||||||
|
if (Date.now() - parsed.savedAt > CACHE_TTL_MS) {
|
||||||
|
sessionStorage.removeItem(cacheKey(sessionId));
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return { messages: parsed.messages, start: parsed.start, total: parsed.total };
|
||||||
|
} catch {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function writeChatHistoryCache(sessionId: string, page: RawMessagePage): void {
|
||||||
|
try {
|
||||||
|
sessionStorage.setItem(cacheKey(sessionId), JSON.stringify({ ...page, savedAt: Date.now() }));
|
||||||
|
} catch {
|
||||||
|
// Ignore quota/private-mode failures; history paging still works without cache.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function mergeChatHistory(existing: RawMessagePage | undefined, incoming: RawMessagePage): RawMessagePage {
|
||||||
|
if (!existing || existing.total !== incoming.total) return incoming;
|
||||||
|
|
||||||
|
const start = Math.min(existing.start, incoming.start);
|
||||||
|
const end = Math.max(existing.start + existing.messages.length, incoming.start + incoming.messages.length);
|
||||||
|
const messages = new Array<unknown>(end - start);
|
||||||
|
copyInto(messages, start, existing);
|
||||||
|
copyInto(messages, start, incoming);
|
||||||
|
|
||||||
|
return { start, total: incoming.total, messages: messages.filter((message) => message !== undefined) };
|
||||||
|
}
|
||||||
|
|
||||||
|
function copyInto(target: unknown[], targetStart: number, page: RawMessagePage): void {
|
||||||
|
page.messages.forEach((message, index) => {
|
||||||
|
target[page.start - targetStart + index] = message;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function cacheKey(sessionId: string): string {
|
||||||
|
return `${CACHE_PREFIX}${sessionId}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isCachedHistory(value: unknown): value is CachedChatHistory {
|
||||||
|
return typeof value === "object"
|
||||||
|
&& value !== null
|
||||||
|
&& "messages" in value
|
||||||
|
&& "start" in value
|
||||||
|
&& "total" in value
|
||||||
|
&& "savedAt" in value
|
||||||
|
&& Array.isArray(value.messages)
|
||||||
|
&& typeof value.start === "number"
|
||||||
|
&& typeof value.total === "number"
|
||||||
|
&& typeof value.savedAt === "number";
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ import { api, type CommandResult, type SessionActivity, type SessionInfo, type S
|
|||||||
|
|
||||||
const MESSAGE_PAGE_SIZE = 100;
|
const MESSAGE_PAGE_SIZE = 100;
|
||||||
import { normalizeMessages, textMessage } from "../chatMessages";
|
import { normalizeMessages, textMessage } from "../chatMessages";
|
||||||
|
import { readChatHistoryCache, mergeChatHistory, writeChatHistoryCache, type RawMessagePage } from "../chatHistoryCache";
|
||||||
import { applyTranscriptEvent } from "../chatTranscript";
|
import { applyTranscriptEvent } from "../chatTranscript";
|
||||||
import { isShellInput } from "../inputModes";
|
import { isShellInput } from "../inputModes";
|
||||||
import { GlobalSessionSocket, SessionSocket, type SessionUiEvent } from "../sessionSocket";
|
import { GlobalSessionSocket, SessionSocket, type SessionUiEvent } from "../sessionSocket";
|
||||||
@@ -48,7 +49,8 @@ export class SessionController {
|
|||||||
const buffered: SessionUiEvent[] = [];
|
const buffered: SessionUiEvent[] = [];
|
||||||
this.socket.connect(session.id, (event) => buffered.push(event));
|
this.socket.connect(session.id, (event) => buffered.push(event));
|
||||||
const [page, status] = await Promise.all([api.messages(session.id, { limit: MESSAGE_PAGE_SIZE }), api.status(session.id)]);
|
const [page, status] = await Promise.all([api.messages(session.id, { limit: MESSAGE_PAGE_SIZE }), api.status(session.id)]);
|
||||||
this.setState({ selectedSession: session, messages: normalizeMessages(page.messages), messagePageStart: page.start, messagePageTotal: page.total, isLoadingEarlierMessages: false, status });
|
const history = this.mergeAndCacheHistory(session.id, page);
|
||||||
|
this.setState({ selectedSession: session, messages: normalizeMessages(history.messages), messagePageStart: history.start, messagePageTotal: history.total, isLoadingEarlierMessages: false, status });
|
||||||
this.applyStatus(status);
|
this.applyStatus(status);
|
||||||
for (const event of buffered) this.applyEvent(event);
|
for (const event of buffered) this.applyEvent(event);
|
||||||
this.socket.setHandler((event) => { this.applyEvent(event); });
|
this.socket.setHandler((event) => { this.applyEvent(event); });
|
||||||
@@ -66,10 +68,11 @@ export class SessionController {
|
|||||||
try {
|
try {
|
||||||
const page = await api.messages(session.id, { before: state.messagePageStart, limit: MESSAGE_PAGE_SIZE });
|
const page = await api.messages(session.id, { before: state.messagePageStart, limit: MESSAGE_PAGE_SIZE });
|
||||||
if (this.getState().selectedSession?.id !== session.id) return;
|
if (this.getState().selectedSession?.id !== session.id) return;
|
||||||
|
const history = this.mergeAndCacheHistory(session.id, page);
|
||||||
this.setState({
|
this.setState({
|
||||||
messages: [...normalizeMessages(page.messages), ...this.getState().messages],
|
messages: normalizeMessages(history.messages),
|
||||||
messagePageStart: page.start,
|
messagePageStart: history.start,
|
||||||
messagePageTotal: page.total,
|
messagePageTotal: history.total,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.setState({ error: String(error) });
|
this.setState({ error: String(error) });
|
||||||
@@ -142,6 +145,12 @@ export class SessionController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private mergeAndCacheHistory(sessionId: string, page: RawMessagePage): RawMessagePage {
|
||||||
|
const history = mergeChatHistory(readChatHistoryCache(sessionId), page);
|
||||||
|
writeChatHistoryCache(sessionId, history);
|
||||||
|
return history;
|
||||||
|
}
|
||||||
|
|
||||||
private applyCommandResult(result: CommandResult) {
|
private applyCommandResult(result: CommandResult) {
|
||||||
if (result.type === "select") {
|
if (result.type === "select") {
|
||||||
this.setState({ commandDialog: result });
|
this.setState({ commandDialog: result });
|
||||||
|
|||||||
Reference in New Issue
Block a user