refactor: centralize webchat refresh persistence

This commit is contained in:
Peter Steinberger
2026-03-24 11:04:10 -07:00
parent d316c2cac0
commit 73422fac4a
6 changed files with 364 additions and 116 deletions

View File

@@ -12,14 +12,63 @@ vi.mock("./app-settings.ts", () => ({
}));
let handleSendChat: typeof import("./app-chat.ts").handleSendChat;
let removeQueuedMessage: typeof import("./app-chat.ts").removeQueuedMessage;
let refreshChatAvatar: typeof import("./app-chat.ts").refreshChatAvatar;
let restorePersistedChatState: typeof import("./app-chat.ts").restorePersistedChatState;
let loadPersistedChatAttachments: typeof import("./storage.ts").loadPersistedChatAttachments;
let loadPersistedChatDraft: typeof import("./storage.ts").loadPersistedChatDraft;
let loadPersistedChatQueue: typeof import("./storage.ts").loadPersistedChatQueue;
let persistChatAttachments: typeof import("./storage.ts").persistChatAttachments;
let persistChatDraft: typeof import("./storage.ts").persistChatDraft;
let persistChatQueue: typeof import("./storage.ts").persistChatQueue;
type TestChatHost = ChatHost & {
toolStreamById: Map<string, unknown>;
toolStreamOrder: string[];
chatToolMessages: unknown[];
chatStreamSegments: Array<{ text: string; ts: number }>;
toolStreamSyncTimer: number | null;
};
function createStorageMock(): Storage {
const store = new Map<string, string>();
return {
get length() {
return store.size;
},
clear() {
store.clear();
},
getItem(key: string) {
return store.get(key) ?? null;
},
key(index: number) {
return Array.from(store.keys())[index] ?? null;
},
removeItem(key: string) {
store.delete(key);
},
setItem(key: string, value: string) {
store.set(key, String(value));
},
};
}
async function loadChatHelpers(): Promise<void> {
vi.resetModules();
({ handleSendChat, refreshChatAvatar } = await import("./app-chat.ts"));
({ handleSendChat, refreshChatAvatar, removeQueuedMessage, restorePersistedChatState } =
await import("./app-chat.ts"));
({
loadPersistedChatAttachments,
loadPersistedChatDraft,
loadPersistedChatQueue,
persistChatAttachments,
persistChatDraft,
persistChatQueue,
} = await import("./storage.ts"));
}
function makeHost(overrides?: Partial<ChatHost>): ChatHost {
function makeHost(overrides?: Partial<TestChatHost>): TestChatHost {
return {
client: null,
chatMessages: [],
@@ -28,6 +77,22 @@ function makeHost(overrides?: Partial<ChatHost>): ChatHost {
chatMessage: "",
chatAttachments: [],
chatQueue: [],
settings: {
gatewayUrl: "wss://gateway.example:8443/openclaw",
token: "",
sessionKey: "agent:main",
lastActiveSessionKey: "agent:main",
theme: "claw",
themeMode: "system",
chatFocusMode: false,
chatShowThinking: true,
chatShowToolCalls: true,
splitRatio: 0.6,
navCollapsed: false,
navWidth: 220,
navGroupsCollapsed: {},
borderRadius: 50,
},
chatRunId: null,
chatSending: false,
lastError: null,
@@ -38,6 +103,11 @@ function makeHost(overrides?: Partial<ChatHost>): ChatHost {
chatModelOverrides: {},
chatModelsLoading: false,
chatModelCatalog: [],
toolStreamById: new Map(),
toolStreamOrder: [],
chatToolMessages: [],
chatStreamSegments: [],
toolStreamSyncTimer: null,
refreshSessionsAfterChat: new Set<string>(),
updateComplete: Promise.resolve(),
...overrides,
@@ -46,6 +116,8 @@ function makeHost(overrides?: Partial<ChatHost>): ChatHost {
describe("refreshChatAvatar", () => {
beforeEach(async () => {
vi.stubGlobal("localStorage", createStorageMock());
vi.stubGlobal("sessionStorage", createStorageMock());
await loadChatHelpers();
});
@@ -91,6 +163,8 @@ describe("refreshChatAvatar", () => {
describe("handleSendChat", () => {
beforeEach(async () => {
setLastActiveSessionKeyMock.mockReset();
vi.stubGlobal("localStorage", createStorageMock());
vi.stubGlobal("sessionStorage", createStorageMock());
await loadChatHelpers();
});
@@ -153,6 +227,126 @@ describe("handleSendChat", () => {
value: "openai/gpt-5-mini",
});
});
it("clears persisted draft and attachments after a successful send", async () => {
const request = vi.fn(async (method: string) => {
if (method === "chat.send") {
return {};
}
throw new Error(`Unexpected request: ${method}`);
});
const host = makeHost({
client: { request } as unknown as ChatHost["client"],
sessionKey: "main",
chatMessage: "hello",
chatAttachments: [{ id: "1", dataUrl: "data:image/png;base64,AA==", mimeType: "image/png" }],
});
persistChatDraft(host.settings.gatewayUrl, host.sessionKey, host.chatMessage);
persistChatAttachments(host.settings.gatewayUrl, host.sessionKey, host.chatAttachments);
await handleSendChat(host);
expect(loadPersistedChatDraft(host.settings.gatewayUrl, host.sessionKey)).toBe("");
expect(loadPersistedChatAttachments(host.settings.gatewayUrl, host.sessionKey)).toEqual([]);
});
it("restores persisted draft and attachments after a failed send", async () => {
const request = vi.fn(async () => {
throw new Error("send failed");
});
const attachments = [{ id: "1", dataUrl: "data:image/png;base64,AA==", mimeType: "image/png" }];
const host = makeHost({
client: { request } as unknown as ChatHost["client"],
sessionKey: "main",
chatMessage: "hello",
chatAttachments: attachments,
});
await handleSendChat(host);
expect(host.chatMessage).toBe("hello");
expect(host.chatAttachments).toEqual(attachments);
expect(loadPersistedChatDraft(host.settings.gatewayUrl, host.sessionKey)).toBe("hello");
expect(loadPersistedChatAttachments(host.settings.gatewayUrl, host.sessionKey)).toEqual(
attachments,
);
});
});
describe("chat persistence helpers", () => {
beforeEach(async () => {
vi.stubGlobal("localStorage", createStorageMock());
vi.stubGlobal("sessionStorage", createStorageMock());
await loadChatHelpers();
});
afterEach(() => {
vi.unstubAllGlobals();
});
it("persists queue removals", () => {
const host = makeHost({
sessionKey: "main",
chatQueue: [
{ id: "keep", text: "keep", createdAt: 1 },
{ id: "drop", text: "drop", createdAt: 2 },
],
});
persistChatQueue(host.settings.gatewayUrl, host.sessionKey, host.chatQueue);
removeQueuedMessage(host, "drop");
expect(loadPersistedChatQueue(host.settings.gatewayUrl, host.sessionKey)).toEqual([
{ id: "keep", text: "keep", createdAt: 1 },
]);
});
it("restores persisted state for the active gateway only", () => {
const host = makeHost({
sessionKey: "main",
settings: {
gatewayUrl: "wss://gateway-b.example:8443/openclaw",
token: "",
sessionKey: "main",
lastActiveSessionKey: "main",
theme: "claw",
themeMode: "system",
chatFocusMode: false,
chatShowThinking: true,
chatShowToolCalls: true,
splitRatio: 0.6,
navCollapsed: false,
navWidth: 220,
navGroupsCollapsed: {},
borderRadius: 50,
},
});
persistChatQueue("wss://gateway-a.example:8443/openclaw", "main", [
{ id: "queue-a", text: "queue-a", createdAt: 1 },
]);
persistChatDraft("wss://gateway-a.example:8443/openclaw", "main", "draft-a");
persistChatAttachments("wss://gateway-a.example:8443/openclaw", "main", [
{ id: "att-a", dataUrl: "data:image/png;base64,AA==", mimeType: "image/png" },
]);
persistChatQueue(host.settings.gatewayUrl, host.sessionKey, [
{ id: "queue-b", text: "queue-b", createdAt: 2 },
]);
persistChatDraft(host.settings.gatewayUrl, host.sessionKey, "draft-b");
persistChatAttachments(host.settings.gatewayUrl, host.sessionKey, [
{ id: "att-b", dataUrl: "data:image/png;base64,AA==", mimeType: "image/png" },
]);
restorePersistedChatState(host);
expect(host.chatQueue).toEqual([{ id: "queue-b", text: "queue-b", createdAt: 2 }]);
expect(host.chatMessage).toBe("draft-b");
expect(host.chatAttachments).toEqual([
{ id: "att-b", dataUrl: "data:image/png;base64,AA==", mimeType: "image/png" },
]);
});
});
afterAll(() => {

View File

@@ -11,11 +11,15 @@ import { loadSessions } from "./controllers/sessions.ts";
import type { GatewayBrowserClient, GatewayHelloOk } from "./gateway.ts";
import { normalizeBasePath } from "./navigation.ts";
import {
clearPersistedChatComposer,
clearPersistedChatQueue,
loadPersistedChatAttachments,
loadPersistedChatDraft,
loadPersistedChatQueue,
persistChatAttachments,
persistChatDraft,
persistChatQueue,
type UiSettings,
} from "./storage.ts";
import type { ChatModelOverride, ModelCatalogEntry } from "./types.ts";
import type { ChatAttachment, ChatQueueItem } from "./ui-types.ts";
@@ -29,6 +33,7 @@ export type ChatHost = {
chatMessage: string;
chatAttachments: ChatAttachment[];
chatQueue: ChatQueueItem[];
settings: UiSettings;
chatRunId: string | null;
chatSending: boolean;
lastError?: string | null;
@@ -45,6 +50,17 @@ export type ChatHost = {
onSlashAction?: (action: string) => void;
};
type ChatSessionStateHost = ChatHost &
Pick<OpenClawApp, "applySettings" | "resetChatScroll" | "resetToolStream"> & {
chatStreamStartedAt: number | null;
};
type PersistedChatState = {
attachments: ChatAttachment[];
draft: string;
queue: ChatQueueItem[];
};
export const CHAT_SESSIONS_ACTIVE_MINUTES = 120;
export function isChatBusy(host: ChatHost) {
@@ -85,10 +101,100 @@ export async function handleAbortChat(host: ChatHost) {
if (!host.connected) {
return;
}
host.chatMessage = "";
clearChatComposerState(host);
await abortChatRun(host as unknown as OpenClawApp);
}
function gatewayUrlForHost(host: ChatHost): string {
return host.settings.gatewayUrl;
}
function persistChatQueueState(host: ChatHost) {
persistChatQueue(gatewayUrlForHost(host), host.sessionKey, host.chatQueue);
}
export function persistChatComposerState(host: ChatHost) {
const gatewayUrl = gatewayUrlForHost(host);
persistChatDraft(gatewayUrl, host.sessionKey, host.chatMessage);
persistChatAttachments(gatewayUrl, host.sessionKey, host.chatAttachments);
}
function clearPersistedChatComposerState(host: ChatHost) {
clearPersistedChatComposer(gatewayUrlForHost(host), host.sessionKey);
}
export function setChatDraft(host: ChatHost, draft: string) {
host.chatMessage = draft;
persistChatComposerState(host);
}
export function setChatAttachments(host: ChatHost, attachments: ChatAttachment[]) {
host.chatAttachments = attachments;
persistChatComposerState(host);
}
function clearChatComposerState(host: ChatHost) {
host.chatMessage = "";
host.chatAttachments = [];
clearPersistedChatComposerState(host);
}
export function loadPersistedChatState(host: ChatHost): PersistedChatState {
const gatewayUrl = gatewayUrlForHost(host);
return {
queue: loadPersistedChatQueue(gatewayUrl, host.sessionKey),
draft: loadPersistedChatDraft(gatewayUrl, host.sessionKey),
attachments: loadPersistedChatAttachments(gatewayUrl, host.sessionKey),
};
}
function applyPersistedChatState(host: ChatHost, state: PersistedChatState) {
host.chatQueue = state.queue;
host.chatMessage = state.draft;
host.chatAttachments = state.attachments;
}
function mergePersistedChatState(host: ChatHost, state: PersistedChatState) {
if (state.queue.length > 0 || host.chatQueue.length === 0) {
host.chatQueue = state.queue;
}
if (state.draft || !host.chatMessage) {
host.chatMessage = state.draft;
}
if (state.attachments.length > 0 || host.chatAttachments.length === 0) {
host.chatAttachments = state.attachments;
}
}
export function restorePersistedChatState(
host: ChatHost,
opts?: { mode?: "merge-if-present" | "replace" },
) {
const persisted = loadPersistedChatState(host);
if (opts?.mode === "replace") {
applyPersistedChatState(host, persisted);
return;
}
mergePersistedChatState(host, persisted);
}
export function switchChatSessionState(host: ChatSessionStateHost, sessionKey: string) {
host.sessionKey = sessionKey;
host.chatStream = null;
host.chatQueue = [];
clearChatComposerState(host);
host.chatStreamStartedAt = null;
host.chatRunId = null;
host.resetToolStream();
host.resetChatScroll();
host.applySettings({
...host.settings,
sessionKey,
lastActiveSessionKey: sessionKey,
});
restorePersistedChatState(host, { mode: "replace" });
}
function enqueueChatMessage(
host: ChatHost,
text: string,
@@ -113,8 +219,7 @@ function enqueueChatMessage(
localCommandName: localCommand?.name,
},
];
// Persist queue to localStorage for recovery after refresh
persistChatQueue(host.sessionKey, host.chatQueue);
persistChatQueueState(host);
}
async function sendChatMessageNow(
@@ -152,6 +257,7 @@ async function sendChatMessageNow(
if (ok && opts?.restoreAttachments && opts.previousAttachments?.length) {
host.chatAttachments = opts.previousAttachments;
}
persistChatComposerState(host);
// Force scroll after sending to ensure viewport is at bottom for incoming stream
scheduleChatScroll(host as unknown as Parameters<typeof scheduleChatScroll>[0], true);
if (ok && !host.chatRunId) {
@@ -169,13 +275,11 @@ async function flushChatQueue(host: ChatHost) {
}
const [next, ...rest] = host.chatQueue;
if (!next) {
// Queue is empty, clear persisted queue
clearPersistedChatQueue(host.sessionKey);
clearPersistedChatQueue(gatewayUrlForHost(host), host.sessionKey);
return;
}
host.chatQueue = rest;
// Persist updated queue (without the message being sent)
persistChatQueue(host.sessionKey, host.chatQueue);
persistChatQueueState(host);
let ok = false;
try {
if (next.localCommandName) {
@@ -192,8 +296,7 @@ async function flushChatQueue(host: ChatHost) {
}
if (!ok) {
host.chatQueue = [next, ...host.chatQueue];
// Restore persisted queue
persistChatQueue(host.sessionKey, host.chatQueue);
persistChatQueueState(host);
} else if (host.chatQueue.length > 0) {
// Continue draining — local commands don't block on server response
void flushChatQueue(host);
@@ -202,6 +305,7 @@ async function flushChatQueue(host: ChatHost) {
export function removeQueuedMessage(host: ChatHost, id: string) {
host.chatQueue = host.chatQueue.filter((item) => item.id !== id);
persistChatQueueState(host);
}
export async function handleSendChat(
@@ -232,8 +336,7 @@ export async function handleSendChat(
if (parsed?.command.executeLocal) {
if (isChatBusy(host) && shouldQueueLocalSlashCommand(parsed.command.name)) {
if (messageOverride == null) {
host.chatMessage = "";
host.chatAttachments = [];
clearChatComposerState(host);
}
enqueueChatMessage(host, message, undefined, isChatResetCommand(message), {
args: parsed.args,
@@ -243,8 +346,7 @@ export async function handleSendChat(
}
const prevDraft = messageOverride == null ? previousDraft : undefined;
if (messageOverride == null) {
host.chatMessage = "";
host.chatAttachments = [];
clearChatComposerState(host);
}
await dispatchSlashCommand(host, parsed.command.name, parsed.args, {
previousDraft: prevDraft,
@@ -255,8 +357,7 @@ export async function handleSendChat(
const refreshSessions = isChatResetCommand(message);
if (messageOverride == null) {
host.chatMessage = "";
host.chatAttachments = [];
clearChatComposerState(host);
}
if (isChatBusy(host)) {
@@ -370,23 +471,7 @@ function injectCommandResult(host: ChatHost, content: string) {
}
export async function refreshChat(host: ChatHost, opts?: { scheduleScroll?: boolean }) {
// Restore persisted chat queue and draft before loading history
const persistedQueue = loadPersistedChatQueue(host.sessionKey);
if (persistedQueue.length > 0) {
host.chatQueue = persistedQueue as ChatQueueItem[];
}
// Restore persisted draft message
const persistedDraft = loadPersistedChatDraft(host.sessionKey);
if (persistedDraft) {
host.chatMessage = persistedDraft;
}
// Restore persisted attachments
const persistedAttachments = loadPersistedChatAttachments(host.sessionKey);
if (persistedAttachments.length > 0) {
host.chatAttachments = persistedAttachments as ChatAttachment[];
}
restorePersistedChatState(host);
await Promise.all([
loadChatHistory(host as unknown as OpenClawApp),

View File

@@ -2,7 +2,7 @@ import { html, nothing } from "lit";
import { repeat } from "lit/directives/repeat.js";
import { parseAgentSessionKey } from "../../../src/sessions/session-key-utils.js";
import { t } from "../i18n/index.ts";
import { refreshChat } from "./app-chat.ts";
import { refreshChat, refreshChatAvatar, switchChatSessionState } from "./app-chat.ts";
import { syncUrlWithSessionKey } from "./app-settings.ts";
import type { AppViewState } from "./app-view-state.ts";
import { OpenClawApp } from "./app.ts";
@@ -40,18 +40,10 @@ function resolveSidebarChatSessionKey(state: AppViewState): string {
}
function resetChatStateForSessionSwitch(state: AppViewState, sessionKey: string) {
state.sessionKey = sessionKey;
state.chatMessage = "";
state.chatStream = null;
(state as unknown as OpenClawApp).chatStreamStartedAt = null;
state.chatRunId = null;
(state as unknown as OpenClawApp).resetToolStream();
(state as unknown as OpenClawApp).resetChatScroll();
state.applySettings({
...state.settings,
switchChatSessionState(
state as unknown as Parameters<typeof switchChatSessionState>[0],
sessionKey,
lastActiveSessionKey: sessionKey,
});
);
}
export function renderTab(state: AppViewState, tab: Tab, opts?: { collapsed?: boolean }) {
@@ -486,20 +478,7 @@ export function renderChatMobileToggle(state: AppViewState) {
}
export function switchChatSession(state: AppViewState, nextSessionKey: string) {
state.sessionKey = nextSessionKey;
state.chatMessage = "";
state.chatStream = null;
// P1: Clear queued chat items from the previous session
(state as unknown as { chatQueue: unknown[] }).chatQueue = [];
(state as unknown as OpenClawApp).chatStreamStartedAt = null;
state.chatRunId = null;
(state as unknown as OpenClawApp).resetToolStream();
(state as unknown as OpenClawApp).resetChatScroll();
state.applySettings({
...state.settings,
sessionKey: nextSessionKey,
lastActiveSessionKey: nextSessionKey,
});
resetChatStateForSessionSwitch(state, nextSessionKey);
void state.loadAssistantIdentity();
syncUrlWithSessionKey(
state as unknown as Parameters<typeof syncUrlWithSessionKey>[0],
@@ -507,6 +486,7 @@ export function switchChatSession(state: AppViewState, nextSessionKey: string) {
true,
);
void loadChatHistory(state as unknown as ChatState);
void refreshChatAvatar(state as unknown as Parameters<typeof refreshChatAvatar>[0]);
void refreshSessionOptions(state);
}

View File

@@ -5,7 +5,7 @@ import {
} from "../../../src/routing/session-key.js";
import { t } from "../i18n/index.ts";
import { getSafeLocalStorage } from "../local-storage.ts";
import { refreshChatAvatar } from "./app-chat.ts";
import { refreshChatAvatar, setChatAttachments, setChatDraft } from "./app-chat.ts";
import { renderUsageTab } from "./app-render-usage-tab.ts";
import {
renderChatControls,
@@ -82,7 +82,6 @@ import { buildExternalLinkRel, EXTERNAL_LINK_TARGET } from "./external-link.ts";
import "./components/dashboard-header.ts";
import { icons } from "./icons.ts";
import { normalizeBasePath, TAB_GROUPS, subtitleForTab, titleForTab } from "./navigation.ts";
import { persistChatAttachments, persistChatDraft } from "./storage.ts";
import { agentLogoUrl } from "./views/agents-utils.ts";
import {
resolveAgentConfig,
@@ -1386,23 +1385,7 @@ export function renderApp(state: AppViewState) {
? renderChat({
sessionKey: state.sessionKey,
onSessionKeyChange: (next) => {
state.sessionKey = next;
state.chatMessage = "";
state.chatAttachments = [];
state.chatStream = null;
state.chatStreamStartedAt = null;
state.chatRunId = null;
state.chatQueue = [];
state.resetToolStream();
state.resetChatScroll();
state.applySettings({
...state.settings,
sessionKey: next,
lastActiveSessionKey: next,
});
void state.loadAssistantIdentity();
void loadChatHistory(state);
void refreshChatAvatar(state);
switchChatSession(state, next);
},
thinkingLevel: state.chatThinkingLevel,
showThinking,
@@ -1440,18 +1423,15 @@ export function renderApp(state: AppViewState) {
},
onChatScroll: (event) => state.handleChatScroll(event),
getDraft: () => state.chatMessage,
onDraftChange: (next) => {
state.chatMessage = next;
// Persist draft to sessionStorage for recovery after refresh
persistChatDraft(state.sessionKey, next);
},
onDraftChange: (next) =>
setChatDraft(state as unknown as Parameters<typeof setChatDraft>[0], next),
onRequestUpdate: requestHostUpdate,
attachments: state.chatAttachments,
onAttachmentsChange: (next) => {
state.chatAttachments = next;
// Persist attachments to sessionStorage for recovery after refresh
persistChatAttachments(state.sessionKey, next);
},
onAttachmentsChange: (next) =>
setChatAttachments(
state as unknown as Parameters<typeof setChatAttachments>[0],
next,
),
onSend: () => state.handleSendChat(),
canAbort: Boolean(state.chatRunId),
onAbort: () => void state.handleAbortChat(),
@@ -1474,17 +1454,8 @@ export function renderApp(state: AppViewState) {
agentsList: state.agentsList,
currentAgentId: resolvedAgentId ?? "main",
onAgentChange: (agentId: string) => {
state.sessionKey = buildAgentMainSessionKey({ agentId });
state.chatMessages = [];
state.chatStream = null;
state.chatRunId = null;
state.applySettings({
...state.settings,
sessionKey: state.sessionKey,
lastActiveSessionKey: state.sessionKey,
});
void loadChatHistory(state);
void state.loadAssistantIdentity();
switchChatSession(state, buildAgentMainSessionKey({ agentId }));
},
onNavigateToAgent: () => {
state.agentsSelectedId = resolvedAgentId;

View File

@@ -214,9 +214,6 @@ export function setThemeMode(
}
export async function refreshActiveTab(host: SettingsHost) {
if (host.tab === "chat") {
await refreshChat(host as unknown as OpenClawApp);
}
if (host.tab === "overview") {
await loadOverview(host);
}

View File

@@ -24,6 +24,7 @@ import { isSupportedLocale } from "../i18n/index.ts";
import { getSafeLocalStorage } from "../local-storage.ts";
import { inferBasePathFromPathname, normalizeBasePath } from "./navigation.ts";
import { parseThemeSelection, type ThemeMode, type ThemeName } from "./theme.ts";
import type { ChatAttachment, ChatQueueItem } from "./ui-types.ts";
export const BORDER_RADIUS_STOPS = [0, 25, 50, 75, 100] as const;
export type BorderRadiusStop = (typeof BORDER_RADIUS_STOPS)[number];
@@ -351,17 +352,25 @@ const CHAT_QUEUE_KEY_PREFIX = "openclaw.control.chat-queue.";
const CHAT_DRAFT_KEY_PREFIX = "openclaw.control.chat-draft.";
const CHAT_ATTACHMENTS_KEY_PREFIX = "openclaw.control.chat-attachments.";
function chatStorageKey(prefix: string, gatewayUrl: string, sessionKey: string): string {
return `${prefix}${normalizeGatewayTokenScope(gatewayUrl)}:${sessionKey.trim() || "main"}`;
}
/**
* Persist the chat message queue to localStorage.
* This allows queued messages to survive browser refreshes.
*/
export function persistChatQueue(sessionKey: string, queue: Array<unknown>): void {
export function persistChatQueue(
gatewayUrl: string,
sessionKey: string,
queue: ChatQueueItem[],
): void {
const storage = getSafeLocalStorage();
if (!storage) {
return;
}
try {
const key = `${CHAT_QUEUE_KEY_PREFIX}${sessionKey}`;
const key = chatStorageKey(CHAT_QUEUE_KEY_PREFIX, gatewayUrl, sessionKey);
if (queue.length === 0) {
storage.removeItem(key);
} else {
@@ -375,13 +384,13 @@ export function persistChatQueue(sessionKey: string, queue: Array<unknown>): voi
/**
* Load persisted chat message queue from localStorage.
*/
export function loadPersistedChatQueue(sessionKey: string): Array<unknown> {
export function loadPersistedChatQueue(gatewayUrl: string, sessionKey: string): ChatQueueItem[] {
const storage = getSafeLocalStorage();
if (!storage) {
return [];
}
try {
const key = `${CHAT_QUEUE_KEY_PREFIX}${sessionKey}`;
const key = chatStorageKey(CHAT_QUEUE_KEY_PREFIX, gatewayUrl, sessionKey);
const raw = storage.getItem(key);
if (!raw) {
return [];
@@ -396,13 +405,13 @@ export function loadPersistedChatQueue(sessionKey: string): Array<unknown> {
/**
* Clear persisted chat queue for a session.
*/
export function clearPersistedChatQueue(sessionKey: string): void {
export function clearPersistedChatQueue(gatewayUrl: string, sessionKey: string): void {
const storage = getSafeLocalStorage();
if (!storage) {
return;
}
try {
storage.removeItem(`${CHAT_QUEUE_KEY_PREFIX}${sessionKey}`);
storage.removeItem(chatStorageKey(CHAT_QUEUE_KEY_PREFIX, gatewayUrl, sessionKey));
} catch {
// best-effort
}
@@ -412,13 +421,13 @@ export function clearPersistedChatQueue(sessionKey: string): void {
* Persist unsent draft message to sessionStorage.
* sessionStorage is used because drafts should not persist across tabs/sessions.
*/
export function persistChatDraft(sessionKey: string, draft: string): void {
export function persistChatDraft(gatewayUrl: string, sessionKey: string, draft: string): void {
const storage = getSessionStorage();
if (!storage) {
return;
}
try {
const key = `${CHAT_DRAFT_KEY_PREFIX}${sessionKey}`;
const key = chatStorageKey(CHAT_DRAFT_KEY_PREFIX, gatewayUrl, sessionKey);
if (!draft) {
storage.removeItem(key);
} else {
@@ -432,13 +441,13 @@ export function persistChatDraft(sessionKey: string, draft: string): void {
/**
* Load persisted draft message from sessionStorage.
*/
export function loadPersistedChatDraft(sessionKey: string): string {
export function loadPersistedChatDraft(gatewayUrl: string, sessionKey: string): string {
const storage = getSessionStorage();
if (!storage) {
return "";
}
try {
const key = `${CHAT_DRAFT_KEY_PREFIX}${sessionKey}`;
const key = chatStorageKey(CHAT_DRAFT_KEY_PREFIX, gatewayUrl, sessionKey);
return storage.getItem(key) ?? "";
} catch {
return "";
@@ -448,13 +457,17 @@ export function loadPersistedChatDraft(sessionKey: string): string {
/**
* Persist unsent attachments to sessionStorage.
*/
export function persistChatAttachments(sessionKey: string, attachments: Array<unknown>): void {
export function persistChatAttachments(
gatewayUrl: string,
sessionKey: string,
attachments: ChatAttachment[],
): void {
const storage = getSessionStorage();
if (!storage) {
return;
}
try {
const key = `${CHAT_ATTACHMENTS_KEY_PREFIX}${sessionKey}`;
const key = chatStorageKey(CHAT_ATTACHMENTS_KEY_PREFIX, gatewayUrl, sessionKey);
if (attachments.length === 0) {
storage.removeItem(key);
} else {
@@ -468,13 +481,16 @@ export function persistChatAttachments(sessionKey: string, attachments: Array<un
/**
* Load persisted attachments from sessionStorage.
*/
export function loadPersistedChatAttachments(sessionKey: string): Array<unknown> {
export function loadPersistedChatAttachments(
gatewayUrl: string,
sessionKey: string,
): ChatAttachment[] {
const storage = getSessionStorage();
if (!storage) {
return [];
}
try {
const key = `${CHAT_ATTACHMENTS_KEY_PREFIX}${sessionKey}`;
const key = chatStorageKey(CHAT_ATTACHMENTS_KEY_PREFIX, gatewayUrl, sessionKey);
const raw = storage.getItem(key);
if (!raw) {
return [];
@@ -485,3 +501,8 @@ export function loadPersistedChatAttachments(sessionKey: string): Array<unknown>
return [];
}
}
export function clearPersistedChatComposer(gatewayUrl: string, sessionKey: string): void {
persistChatDraft(gatewayUrl, sessionKey, "");
persistChatAttachments(gatewayUrl, sessionKey, []);
}