Appearance
Architecture
kimtos is a local-first Chrome MV3 new-tab extension: everything you create — notes, todos, PDFs, chats — lives in your browser and never leaves the device. The only server touch is accounts — an optional sign-in (brokered by the marketing site) and Stripe billing that unlock the Pro apps; your content is never uploaded (see the extension's lib/session.ts + lib/entitlement.ts, and the www-backend). This doc describes how that works.
Apps
Every app is a folder under src/apps/<id>/ with a default-export descriptor in index.ts. Apps are auto-discovered (apps/index.ts globs ./*/index.{js,ts}) — there is no central registry to edit.
| File | Role |
|---|---|
meta.ts | Identity — id (= folder name), name, accent, order, plan ('free' | 'pro'), and an optional marketing block. The single source of truth: the descriptor, dock, App Store, onboarding, the marketing site, and AnubisDB all read it. |
index.ts | The descriptor (default export): render(body, ctx) plus the shared conventions — a first-time tip, an MCP-shaped tools array for the AI agent, per-app settings, dialog sizing, home widget/badge. See the app conventions in the root CLAUDE.md. |
<id>.local.ts | Optional on-device data layer — register() REST routes, auto-discovered. Name partitions by app id (see Data layer). |
<id>.css | Optional colocated styles, imported by index.ts. |
Adding a new app — a few lines, not a rewrite:
apps/<id>/meta.ts—id,name,accent,plan,order.apps/<id>/index.ts— the descriptor + theCLAUDE.mdapp conventions (tip, tools, per-app settings, storage prefixing).- The app's icon glyph — see App icons below.
apps/<id>/<id>.local.tsif it stores data.- A docs booklet under
docs/extension/apps/<id>/(seedocs/extension/apps/README.md). - Free apps auto-install; Pro apps (
plan: 'pro') ship uninstalled and are gated bylib/entitlement.ts.
App icons
Every app's icon is an inline SVG line-mark on a squircle tile built from the app's accent — no image files, nothing fetched (it must work offline and read on any wallpaper, light or dark). The whole set lives in one place, @kimtos/icons (shared-modules/icons/src/glyphs.ts): APP_GLYPHS maps each app id to a 24×24 glyph drawn with currentColor, so it inverts cleanly — white-on-gradient in the dock, accent-on-glass elsewhere.
Both surfaces render the identical glyph from this shared module:
- Extension —
paintAppTile()(components/appIcon.ts) paints the accent gradient + glyph in the dock, App Store, and onboarding. A letter monogram is the fallback for any app with no glyph yet. - Marketing site —
www/components/AppGrid.vueusesappTileGradient()+appGlyphSvg()for the same tiles (replacing the old Twemoji PNGs).
To add an icon, add one entry to APP_GLYPHS (and the AppId union) keyed by the app id — a 24×24 line mark: fill="none", stroke-width ≈ 1.9, round caps/joins — then rebuild @kimtos/icons. The dock and the site both pick it up.
Data layer
All persistence goes through a small client-side stack:
| Module | Role |
|---|---|
lib/idb.ts | Zero-dependency IndexedDB wrapper. One DB (kimtos) with stores records, blobs, kv. |
lib/dataStore.ts | CRUD over records (partitioned by collection), blob storage exposed as object URLs, and a kv cache. Also exportAll/importAll for backup. |
lib/localRouter.ts | A tiny REST-style router. Handlers register(method, pattern, fn) and receive { params, query, body, form }. The pseudo-method FILE returns an object URL. |
lib/api.ts | What apps call: getApi/jsonApi/callApi/fileUrl. Dispatches straight to the local router. |
App handlers live next to their app as *.local.ts and are auto-discovered by lib/localRoutes.ts (import.meta.glob('/src/**/*.local.{js,ts}')). To add an app's data layer, register routes in a *.local.ts file — no wiring needed.
apps/pdflib/index.ts → getApi('books') → localRouter.dispatch → apps/pdflib/pdflib.local.tsName partitions by app id. A records collection, blobs namespace, or kv key should be prefixed with the owning app's id — thoth-chunks, thoth:hashes, homeai:downloaded, history:<app>. AnubisDB (apps/anubisdb/, the on-device data explorer) derives app labels from every meta.ts and resolves ownership from that prefix (registry.ts → ownerOf), so a new app's data appears named and grouped with no registry edit. Un-prefixed data lands under "Unmapped data".
On-device compute (televoica / snaptext)
The two compute apps run their models in the browser so audio and images never leave the device:
- snaptext (Image to Text) — OCR via Tesseract.js (
lib/engines/ocr.ts). Worker + core are self-hosted under/ocr(MV3 bans remote worker scripts); English data is bundled. - televoica (Voice to Text) — speech-to-text via
@huggingface/transformersWhisper (lib/engines/transcribe.ts),device: 'wasm',dtype: 'fp32'. The pipeline is disposed after each run to free memory; model files stay cached.
Model files are fetched once and cached. The onboarding overlay (views/onboarding.ts) pre-downloads them on first visit with a progress bar.
Downloads run in the background (consistent everywhere)
Any model/asset download keeps running even if the surface that started it is closed, and closing that surface mid-download raises a snackbar (components/snackbar.ts) — "Download continues in the background." with a Reopen action button — so it never feels cancelled. Reopening (button or manually) re-attaches to the live progress bar (the bar is always kept). This holds for all three download surfaces:
- App Store and Settings (download-affecting setting changes) → the global
lib/installManager.tsalready runs installs/ensureModeldetached; the dialogs fire the snackbar on close viaonClosewhenanyDownloading(). - KimtAI Model Library → a module-scoped
activeDownloadsregistry inapps/homeai/index.tsplays the same role for chat-model downloads.
New download UI should follow this: run the work off the closeable surface, keep a progress bar that can be re-attached, and snackbar on close while it's in flight.
Backup / portability
dataStore.exportAll() serialises every record and blob (blobs as data URLs) into one JSON file; importAll() restores it. This is how a user moves their data between machines — they own the file.
Floating windows (Picture-in-Picture)
Apps that set popout: true (currently Notes and Clipboard) get a pop-out control in their window title bar. Clicking it uses the Document Picture-in-Picture API (lib/pip.ts) to open an always-on-top OS window — the same feature Google Meet uses — and moves the app's DOM into it, leaving a "bring it back" placeholder in the dialog. The PiP window is a same-origin child of the new-tab page, so the moved nodes keep their event listeners and share our IndexedDB/JS — nothing is copied or uploaded; we only mirror the stylesheets so it looks identical.
The orchestration lives in appsController.ts (popOut/canPopOut); the title-bar control is wired through createDialog({ onPopOut }) + setPopOutEnabled(). Each app exposes an on/off Floating window setting plus a Floating window size preset (Compact / Standard / Large → PIP_SIZES in lib/pip.ts, clamped to the screen; the window stays drag-resizable on top of that), both per-app schema → Settings ▸ Apps. A one-time intro banner (pipNoteSeen in the store) tells users the feature exists. Caveat: a Document PiP window only lives as long as the new-tab page does — keep that tab open and browse in other tabs to keep the floating window around.
Manifest notes
permissions:storage,geolocation,unlimitedStorage,identity(the account sign-in useschrome.identity.launchWebAuthFlow),contextMenus.host_permissions:https://*/*— lets import-by-URL and key-free feeds (Open-Meteo, DEV/HN/Lobsters) be fetched directly, bypassing CORS. The same breadth covers the account/entitlements calls to the API.- CSP allows
'wasm-unsafe-eval'(for the WASM engines) andworker-src 'self' blob:.