fix: finish new-session opening-message handoff

This commit is contained in:
amosblomqvist
2026-10-27 18:19:38 +02:00
parent 8ca291526c
commit f24fda926a
11 changed files with 689 additions and 107 deletions
+43 -25
View File
@@ -1,41 +1,59 @@
/**
* The opening message rides beside the navigation, never in the URL.
*
* Home and the new-session draft used to push `/chat/<id>?prompt=<text>`.
* Node's default max HTTP header size is 16KB. A long paste (a syllabus)
* makes that GET return 431, and Electron commits an empty document: the
* window stays the native background and looks frozen. The chat record
* already exists, with an empty transcript, because POST /api/chats
* succeeded first.
*
* sessionStorage holds the trimmed text under `lh.openingPrompt.<id>`.
* The push target stays `/chat/<id>`. The chat page still honors a
* `?prompt=` search param for an old link; when that param is absent it
* reads this stash and sends it once. The stash survives until that send
* resolves, so a reload before the POST lands can retry. Absent storage
* (node tests, a thrown access) is a no-op — the short path still returns.
* Same-tab handover of a new session's full opening text. Never put the
* message in the navigation URL: long query payloads can be rejected before
* the chat page loads. Storage is required for automatic delivery, not an
* optional best-effort side effect of navigating away from the source draft.
*/
export const OPENING_PROMPT_FAILURE = "Couldn’t open the session. Your message is still here. Try sending again.";
export interface OpeningPrompt {
text: string;
/** Written BEFORE sending. A later mount restores a draft, never reposts. */
attempted: boolean;
}
const keyFor = (id: string) => `lh.openingPrompt.${id}`;
function store(): Storage | null {
export function stashOpeningPrompt(id: string, text: string, attempted = false): boolean {
try {
if (typeof sessionStorage === "undefined" || sessionStorage == null) return null;
return sessionStorage;
const value = JSON.stringify({ text, attempted } satisfies OpeningPrompt);
// Catch both the storage getter and its methods (quota/security errors).
const storage = globalThis.sessionStorage;
storage.setItem(keyFor(id), value);
return storage.getItem(keyFor(id)) === value;
} catch {
return null;
return false;
}
}
export function stashOpeningPrompt(id: string, text: string): void {
store()?.setItem(keyFor(id), text);
export function readOpeningPromptEntry(id: string): OpeningPrompt | undefined {
try {
const value = globalThis.sessionStorage.getItem(keyFor(id));
if (!value) return undefined;
try {
const entry: unknown = JSON.parse(value);
if (entry && typeof entry === "object" && "text" in entry && typeof entry.text === "string" &&
entry.text.length > 0 && "attempted" in entry && typeof entry.attempted === "boolean") {
return { text: entry.text, attempted: entry.attempted };
}
} catch { /* Old raw-text stashes have no proof that sending never began. */ }
return { text: value, attempted: true };
} catch {
return undefined;
}
}
export function readOpeningPrompt(id: string): string | undefined {
const value = store()?.getItem(keyFor(id));
return value && value.length > 0 ? value : undefined;
return readOpeningPromptEntry(id)?.text;
}
export function clearOpeningPrompt(id: string): void {
store()?.removeItem(keyFor(id));
export function clearOpeningPrompt(id: string): boolean {
try {
globalThis.sessionStorage.removeItem(keyFor(id));
return true;
} catch {
// Canonical conversation/acknowledgment remains the truth. A later
// mount with real conversation can try cleanup again; never crash it.
return false;
}
}