// Mini-apps: small client-side programs that trade typed events over the same // end-to-end-encrypted thread stream chat uses. The server only ever sees // ciphertext, so it can't route or run app logic — an app lives entirely in the // browser and derives its state by folding the thread's event stream. In other // words: the encrypted thread log IS the app's database. // // An app is four small things: // - an events map — `{ "todo.add": (state, event) => state }`. Its KEYS are the // event types the app owns (`todo.add`), carried as `type` inside the payload // exactly like a chat message's type (old clients that don't know the app just // ignore them); its VALUES fold each event into new state. One declaration is // the whole model — the owned types and the reducer are both derived from it, // so there's no separate `types` array to keep in sync. // - an initialState factory — `() => ({ items: [] })`, the empty state. // - commands it registers (`/todo …`) that emit events. // - a surface where its state renders (full-pane for a personal app, a panel // above the feed for a shared one; plus an optional inline stream line). // // State is folded from the messages currently loaded for a thread (the "window" // the client already holds). That's enough to prove the model; a later snapshot // blob can make state survive reload/late-join without re-folding history — the // reducer shape here is deliberately snapshot-ready. import { BOT_COMMANDS } from "./bot-commands.js"; import { parseMessage, renderTree } from "./markdown.js"; const apps = []; const typeOwner = new Map(); // event type → owning app const commandOwner = new Map(); // "/todo" → owning app (app commands; bot commands live in BOT_COMMANDS) // injectStyles adds a