import { Signal } from "@char/aftercare"; import type { ImageContent, UserMessage } from "@earendil-works/pi-ai"; import { decodeBase64, encodeBase64 } from "@std/encoding/base64"; import { type DraftImage, imageDraft } from "./image-drafts.ts"; import type { CommandInfo } from "./conversation.tsx"; import type { Place } from "./tree.tsx"; import { computed, keyed, Scope, swap } from "./ui.ts"; type Completion = { value: string; label: string; description: string; directory?: boolean }; type Query = { kind: "command"; prefix: string } | { kind: "reference"; prefix: string; start: number; end: number; }; /** The reference containing the caret, including its unfinished suffix. */ export function referenceAt(text: string, start: number, end = start): Query | null { if (start !== end) return null; const match = text.slice(0, start).match(/(?:^|\s|[([{])@([^\s]*)$/u); if (!match) return null; const prefix = match[1]; return { kind: "reference", prefix, start: start - prefix.length, end: start + (text.slice(start).match(/^[^\s]*/u)?.[0].length ?? 0), }; } const EMPTY: readonly never[] = []; const drafts = new Map(); let nextSendMenu = 0; function savedText(route: string): string { if (drafts.has(route)) return drafts.get(route)!; try { return localStorage.getItem(`berry.draft.${route}`) ?? ""; } catch { return ""; } } function saveText(route: string, text: string) { try { if (text === "") localStorage.removeItem(`berry.draft.${route}`); else localStorage.setItem(`berry.draft.${route}`, text); drafts.delete(route); } catch { // navigation still preserves drafts when browser storage is unavailable drafts.set(route, text); } } /** Text for the composer of the view a choice in the tree opens. */ let handoff: { route: string; text: string } | null = null; /** Show `place`, and its text in its composer. */ export function go(place: Place, setText: (text: string) => void) { const draft = place.at === undefined ? "" : `${place.fork ? "/fork" : ""}@${place.at}`; const route = `#/${place.tid}${draft}`; if (route === location.hash) { if (place.text !== undefined) setText(place.text); return; } handoff = place.text === undefined ? null : { route, text: place.text }; location.hash = route; } /** * `send` takes whatever is not a command that runs in the browser; a failure keeps the text. `abort`, where there is * something to abort, stops the conversation while it is busy. */ export function composer( scope: Scope, route: string, busy: Signal, commands: Signal, send: (text: string, followUp: boolean, images: readonly ImageContent[]) => Promise, notify: (message: string, error?: boolean) => void, abort?: () => Promise, completeReferences?: (prefix: string) => Promise, shellEnabled = new Signal(false), ): { element: Element; setText(text: string): void; restore(content: UserMessage["content"]): Promise; } { const input = (