Something went wrong. Try again.
small gleam coding and persistent agent daemon with a detachable cli and a canvas ui forked from okami.mom/albedo
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105import { SvelteMap } from 'svelte/reactivity';import { marker, withoutMarkers, type Attached } from './attachments';
// What was typed in a chat's composer and not yet sent. The composer is// destroyed with its window when another board is shown or the page goes, so// a draft lives here, by session, and in localStorage. Only the text is kept// there: images are too big for it, so a reload drops them and their markers.
const KEY = 'albedo.draft.v1:';/** A draft untouched this long is not worth keeping; the session may be gone. */const STALE_MS = 30 * 24 * 60 * 60 * 1000;const SAVE_AFTER_MS = 300;
function read(session: string): string { try { const saved = JSON.parse(localStorage.getItem(KEY + session) ?? ''); return typeof saved?.text === 'string' ? withoutMarkers(saved.text) : ''; } catch { return ''; }}
/** Drops the saved drafts nobody came back to. */function prune() { try { for (const key of Object.keys(localStorage).filter((name) => name.startsWith(KEY))) { const saved = JSON.parse(localStorage.getItem(key) ?? 'null'); if (!(Date.now() - saved?.at < STALE_MS)) localStorage.removeItem(key); } } catch {}}
const unsaved = new Set<Draft>();let timer: ReturnType<typeof setTimeout> | undefined;
function flush() { clearTimeout(timer); timer = undefined; for (const draft of unsaved) draft.save(); unsaved.clear();}
export class Draft { readonly images = new SvelteMap<number, Attached>(); /** The last image marker handed out. */ next = 0; #text: string; #session: string;
constructor(session: string) { this.#session = session; this.#text = read(session); }
get text(): string { return this.#text; }
/** Setting the text saves it a moment later, once typing pauses. */ set text(value: string) { if (value === this.#text) return; this.#text = value; unsaved.add(this); timer ??= setTimeout(flush, SAVE_AFTER_MS); }
/** Shows the draft's text in a composer that is open; set by it while it is. */ show: ((text: string) => void) | null = null;
/** Appends cancelled messages after what is typed, each a paragraph, their images renumbered after the draft's. */ restore(sends: { text: string; images: Attached[] }[]) { const parts = this.#text.trim() ? [this.#text.trimEnd()] : []; for (const send of sends) { const offset = this.next; send.images.forEach((image, index) => this.images.set(offset + index + 1, image)); this.next += send.images.length; parts.push(send.text.replace(/\[Image #(\d+)\]/g, (_, n) => marker(Number(n) + offset))); } this.text = parts.join('\n\n'); this.show?.(this.#text); }
save() { try { const text = withoutMarkers(this.#text); if (text.trim()) localStorage.setItem(KEY + this.#session, JSON.stringify({ text, at: Date.now() })); else localStorage.removeItem(KEY + this.#session); } catch {} }}
const drafts = new Map<string, Draft>();
export function draftOf(session: string): Draft { let draft = drafts.get(session); if (!draft) drafts.set(session, (draft = new Draft(session))); return draft;}
if (typeof window !== 'undefined') { prune(); // The last words typed before a reload or a closed tab are still waiting on the timer. window.addEventListener('pagehide', flush);}