Skip to content

Working on Llooma

SvelteKit with Svelte 5 runes, TypeScript, Tailwind, and SQLite (via node:sqlite) in server mode. Node 26 and pnpm.

Terminal window
pnpm install
pnpm run dev

Where data lives is confined to a repository: src/lib/data/ exports a repository backed by HTTP calls to /api/data, and everything above it (components, stores, features) is written once against that interface rather than against storage.

The seam earned its keep when the browser-only mode was retired: what had two implementations went back to having one, and nothing above the interface had to be told.

Everything the app knows about a specific provider lives in one file under src/lib/providers/, and nothing else in the application names one. Endpoints, key help links, whether tool calling works, what a provider calls a portrait, which models take reference pictures: all of it is data, kept per provider, because all of it changes on somebody else’s schedule rather than on ours.

The list is a convenience and never a gate. An undescribed provider still works through the OpenAI-compatible entry and only loses conveniences. That is what makes it safe to open up, and why a pull request adding or fixing a provider is a small, reviewable change: the reviewer needs to know the provider, not this codebase.

If you find yourself editing a component to special-case a vendor, the descriptor is missing something, and closing that gap is the more useful patch. See Adding a provider.

Path What lives there
src/lib/chat/ The conversation: strategies, the run orchestrator, titles, compaction, context accounting
src/lib/providers/ One file per provider, see Adding a provider
src/lib/data/ The repository seam: the local and API implementations
src/lib/server/ Everything that only ever runs on the server: database, migrations, auth, resolvers
src/lib/components/ Shared components, provider-agnostic and route-agnostic
src/routes/api/ The HTTP API, see the reference
src/i18n/ typesafe-i18n dictionaries, see Translations
Terminal window
pnpm run lint
npx svelte-check --tsconfig ./tsconfig.json
pnpm run build
pnpm run i18n:status
node scripts/check-api-docs.mjs
Terminal window
pnpm test

Five tests, half a minute. They cover a turn from the composer to the answer on screen, a conversation surviving a reload, the phone interface being offered to a phone and taken back from a wider window, and an administrator publishing a setting to everyone.

It is small on purpose. The suite it replaced had a hundred tests written against the browser-only app, and thirteen of its eighteen test ids no longer existed: it was not broken, it was obsolete.

Two things are worth knowing before adding to it.

The turn runs in the server now, so page.route() intercepts nothing: the request the app makes never passes through the page. Tests start a real OpenAI-compatible endpoint instead (tests/fake-provider.ts) and hand the instance a connection to it.

The run gets an instance of its own, configured in playwright.config.ts: no way to sign in, so nothing is asked at the door, a throwaway DATA_DIR, and the welcome tour switched off. A lone owner is an administrator, which is why connections are created through /api/admin/servers.

Screenshots are not tests and never run with the suite:

Terminal window
pnpm run screenshots

It drives the app to known states and photographs them into static/screenshots, the documentation and the manifest. It writes into source, which is why it only runs when asked.

CI fails if a route exists that docs/openapi.yaml does not describe. After adding a +server.ts:

Terminal window
node scripts/check-api-docs.mjs --list

and add the path and its methods to the spec. The check compares the surface, paths and methods, not response shapes, so it will not tell you a field was renamed. That part is still on you.

  • Commit messages follow Conventional Commits. They are the input to the release, so they matter: see Releases.
  • Issues are for bugs. Feature requests go to Discussions first.
  • English in code and comments. The interface is translated, the source is not.
  • Documentation lives in docs/ in this repository, so a change to the app and the change to its documentation land in the same commit. That is the only thing that actually keeps the two in step.