Working on Llooma
SvelteKit with Svelte 5 runes, TypeScript, Tailwind, and SQLite (via node:sqlite) in server
mode. Node 26 and pnpm.
pnpm installpnpm run devThe one seam that matters
Section titled “The one seam that matters”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.
The other seam
Section titled “The other seam”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.
Layout
Section titled “Layout”| 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 |
Checks
Section titled “Checks”pnpm run lintnpx svelte-check --tsconfig ./tsconfig.jsonpnpm run buildpnpm run i18n:statusnode scripts/check-api-docs.mjsThe end-to-end suite
Section titled “The end-to-end suite”pnpm testFive 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:
pnpm run screenshotsIt 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.
Adding an API endpoint
Section titled “Adding an API endpoint”CI fails if a route exists that docs/openapi.yaml does not describe. After adding a
+server.ts:
node scripts/check-api-docs.mjs --listand 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.
Conventions
Section titled “Conventions”- 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.

