Files
2026-09-14 10:32:21 +00:00

153 lines
5.1 KiB
TypeScript

import type {
QuickChatSelection,
QuickChatSelectionByWorkspace,
QuickChatSelectionOrder,
QuickChatSessionKind,
} from "@/lib/state/slices/ui/types";
export const ANONYMOUS_QUICK_CHAT_SELECTION_IDENTITY = "anonymous";
const STORAGE_PREFIX = "kandev.quick-chat.selection.v1.";
const STORAGE_VERSION = 1;
const MAX_WORKSPACES = 200;
export type QuickChatSelectionIdentity = string | null;
export type QuickChatSelectionStorageResult = {
selections: QuickChatSelectionByWorkspace;
order: QuickChatSelectionOrder;
};
type StoredSelectionEntry = {
workspaceId: string;
chat?: string;
config?: string;
lastSelectedAt?: number;
};
type StoredSelectionDocument = {
version: number;
entries: StoredSelectionEntry[];
};
type AuthIdentityState = {
mode: "disabled" | "setup" | "enabled";
authenticated: boolean;
user: { id: string } | null;
};
function storageKey(identity: QuickChatSelectionIdentity): string | null {
return identity === null ? null : `${STORAGE_PREFIX}${encodeURIComponent(identity)}`;
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
function isSessionId(value: unknown): value is string {
return typeof value === "string" && value.length > 0;
}
function readEntry(value: unknown): StoredSelectionEntry | null {
if (!isRecord(value) || typeof value.workspaceId !== "string" || !value.workspaceId) {
return null;
}
const entry: StoredSelectionEntry = { workspaceId: value.workspaceId };
for (const kind of ["chat", "config"] as const) {
if (isSessionId(value[kind])) entry[kind] = value[kind];
}
if (typeof value.lastSelectedAt === "number" && Number.isFinite(value.lastSelectedAt)) {
entry.lastSelectedAt = value.lastSelectedAt;
}
return entry.chat || entry.config ? entry : null;
}
function emptySelection(): QuickChatSelectionStorageResult {
return { selections: {}, order: [] };
}
/** Returns the browser-storage identity for the current authentication mode. */
export function getQuickChatSelectionIdentity(auth: AuthIdentityState): QuickChatSelectionIdentity {
if (auth.mode === "disabled") return ANONYMOUS_QUICK_CHAT_SELECTION_IDENTITY;
if (auth.mode !== "enabled" || !auth.authenticated || !auth.user?.id) return null;
return auth.user.id;
}
/** Reads and validates remembered Quick Chat selections for one identity. */
export function loadQuickChatSelection(
identity: QuickChatSelectionIdentity,
): QuickChatSelectionStorageResult {
const key = storageKey(identity);
if (!key || typeof window === "undefined") return emptySelection();
try {
const raw = window.localStorage.getItem(key);
if (!raw) return emptySelection();
const parsed: unknown = JSON.parse(raw);
if (!isRecord(parsed) || parsed.version !== STORAGE_VERSION || !Array.isArray(parsed.entries)) {
return emptySelection();
}
const entries = parsed.entries
.map(readEntry)
.filter((entry): entry is StoredSelectionEntry => entry !== null)
.map((entry, index) => ({ entry, index }))
.sort(
(left, right) =>
(right.entry.lastSelectedAt ?? 0) - (left.entry.lastSelectedAt ?? 0) ||
left.index - right.index,
);
const selections: QuickChatSelectionByWorkspace = {};
const order: QuickChatSelectionOrder = [];
for (const { entry } of entries) {
if (order.length >= MAX_WORKSPACES || selections[entry.workspaceId]) continue;
const selection: QuickChatSelection = {};
if (entry.chat) selection.chat = entry.chat;
if (entry.config) selection.config = entry.config;
selections[entry.workspaceId] = selection;
order.push(entry.workspaceId);
}
return { selections, order };
} catch {
return emptySelection();
}
}
/** Writes the bounded remembered-selection map. Storage failures are harmless. */
export function persistQuickChatSelection(
identity: QuickChatSelectionIdentity,
selections: QuickChatSelectionByWorkspace,
order: QuickChatSelectionOrder,
): void {
const key = storageKey(identity);
if (!key || typeof window === "undefined") return;
const orderedWorkspaceIds = [
...order,
...Object.keys(selections).filter((workspaceId) => !order.includes(workspaceId)),
].filter((workspaceId, index, all) => all.indexOf(workspaceId) === index);
const entries: StoredSelectionEntry[] = orderedWorkspaceIds
.filter((workspaceId) => selections[workspaceId])
.slice(0, MAX_WORKSPACES)
.map((workspaceId, index) => {
const selection = selections[workspaceId];
return {
workspaceId,
...(selection.chat ? { chat: selection.chat } : {}),
...(selection.config ? { config: selection.config } : {}),
lastSelectedAt: Date.now() - index,
};
});
try {
window.localStorage.setItem(
key,
JSON.stringify({ version: STORAGE_VERSION, entries } satisfies StoredSelectionDocument),
);
} catch {
// Browser storage can be disabled or full. The in-memory selection remains valid.
}
}
export const QUICK_CHAT_SELECTION_MAX_WORKSPACES = MAX_WORKSPACES;
export type { QuickChatSessionKind };