Skip to content

Getting Started

Grab a build from the Releases page: an .msi or .exe for Windows, a .dmg for macOS (Apple silicon), and a .deb, .rpm or .AppImage for Linux. Nothing is signed yet, so both Windows and macOS will warn you about an unidentified developer.

You need Rust (1.87 or newer), Node 22+ and pnpm.

Terminal window
git clone https://github.com/lacodda/kilna.git
cd kilna
pnpm install
pnpm tauri dev

pnpm tauri dev compiles the Rust backend and opens the app window. The first build takes a while; later ones are incremental.

Development builds show one extra sidebar entry, Styleguide — the living inventory of the design system. Screens take their controls from that page and only from there; it is not part of the released app.

The folders say what a file is for (ADR 0036). src/app/ is the window’s root and the one list of screens (screens.tsx) that the router, the rail, the title bar and the shortcut sheet all read; src/shell/ is the frame around every screen; src/features/<area>/ is a screen and what only it uses, with the open work’s tabs under features/work/tabs/, listed once in features/work/tabs.ts; src/components/ is what several features share; and src/lib/ is logic with no screen of its own. A new screen is one entry in screens.tsx and one place in the smoke test, and a new tab one entry in tabs.ts and one body in TabBody.tsx - the tests fail until each is there.

The design system itself is dowel, shared across the lacodda line. The theme arrives as a package and the primitives are copied in from its registry, so they are kilna’s own files to edit:

Terminal window
$ npx shadcn@latest add https://lacodda.github.io/dowel/r/button.json

Two rules come with it and run in pnpm lint: a component names a colour from the vocabulary and never writes one down, so the theme can swap it underneath; and no screen uses a native <select>, whose popup the browser draws in the operating system’s own chrome where no stylesheet reaches it.

There are no eslint-disable comments in this codebase. Where a rule genuinely does not apply, eslint.config.js turns it off for the named file and says why - src/lib/cover.ts owns its palette rather than following the theme, and src/lib/releaseIcon.tsx looks a component up in a table fixed at module level, which the React rules read as building one during render. A rule silenced per file is one a reviewer can find; a comment silencing it inline is one they cannot.

The window is a window, not a page. styles.css seals the document (overflow: hidden on html, body, #root) and switches text selection off across the shell; the content area beside the rail clips, and each screen is a <Screen> that scrolls inside itself. Two consequences when you write a screen:

  • Selection is handed back, never assumed. Put selectable on anything that is genuinely text someone may want to copy - a version body, a note, a path, a diff. Inputs and contenteditable keep it automatically. Markdown already carries it, so prose rendered through it needs nothing.
  • Do not add a second scroller unless the thing inside it really scrolls on its own. If you do, clip the axis you are not using: overflow-y-auto alone leaves the horizontal axis scrollable too, which is how a trackpad swipe used to slide whole screens sideways.

pnpm test fails if any of that goes. The smoke test holds every screen and every tab of a card to it on what they actually draw: the content area clips, each screen is a <Screen> that either scrolls itself or holds the window’s height, and the card’s header stands still. What no render shows - the sealed document, selection handed back to text, radii from the scale - is read from the source by src/test/source.test.ts, which finds the stylesheet through the window’s entry rather than by a path.

components/ui/ holds the registry and nothing else. Every file there - button.tsx, dialog.tsx, toast.tsx - is a copy from dowel’s registry: it is never edited, so it stays byte-identical to upstream and can be updated by re-running shadcn add. The application’s own shared components - AppDialog.tsx, RowMenu.tsx, DatePicker.tsx - live one level up, in components/. pnpm registry (part of pnpm lint) holds every file in components/ui/ to its twin in the installed dowel-ui: a copy edited in place, one left behind by an upgrade, or a file of kilna’s own put there all fail it. pnpm exec dowel diff <name> shows the lines; a fix belongs in dowel, and comes back with the next copy.

AppDialog and AppSelect are the shapes kilna actually uses, written over dowel’s parts: a heading, a sentence and Cancel beside one affirmative button; a flat list of options with an optional “any” entry. dowel says what a dialog is; these say what this application’s dialogs look like - how wide (size), where a deletion goes (aside, at the start of the row), which field has the cursor when it opens, and that a dialog typed into is not closed by a stray click (dirty for changes a text field does not see).

The window is compact. <html data-density="compact"> puts every control of dowel on the compact rows: 32px for a field and a button, 28px for the small size. A control of kilna’s own takes its height from the same rows - h-control, h-control-sm - rather than from a number, so the density reaches it too.

The frontend has two test runs, and pnpm test runs both: the pure logic in src/lib under Node, and components and hooks (*.test.tsx) under jsdom, inside the app’s own providers and against a mocked backend (src/test/). src/app/smoke.test.tsx opens every screen and every tab of a card on a small invented studio and fails on a question the test backend does not answer, on the crash panel, and on anything said to the console. The studio speaks the shipped profiles as the window receives them: src/test/fixtures/profiles/ is written by the backend, and cargo test fails when a profile changes until the copies are written again with KILNA_BLESS=1 cargo test --test profile_fixtures.

To check the backend on its own, without the UI:

Terminal window
cd src-tauri
cargo test
cargo clippy -- -D warnings

On first launch kilna creates a fresh workspace and seeds it with the four built-in profiles — Music, Novel, Podcast and Blog — with Music active by default. Nothing else is pre-populated: no sample works, no demo data.

Switch profiles from the picker at the bottom of the sidebar at any time. Switching doesn’t lose anything — works keep the kind and status they were given, even if you later edit the vocabulary that named them.

kilna is a studio tool, so it is dark by default — more precisely, it follows your system’s theme until you say otherwise. The theme button in the sidebar footer cycles through system, light and dark, and the choice is remembered across launches. See The interface for the full tour of the frame.

A workspace is one SQLite database holding everything kilna knows: profiles, works, versions, scores, releases, notes and chat history with the AI panel. Media — audio, video, images — lives as plain files on disk; the database holds only the path to each one. See Local-first storage for the reasoning.

The workspace file is created under the platform’s application data directory the first time kilna runs — there is no way to point it elsewhere yet. From inside the app, the workspace path command shows you exactly where the file is, so you can find it or back it up directly.

Schema changes ship as versioned migrations under src-tauri/migrations/; the schema is never edited in place, so an existing workspace upgrades safely when you update kilna.

Create a work, give it a title and a kind (a song, if you’re still on the Music profile), and add a version — the body of a draft, kept whole rather than as a diff. A song keeps lyrics and style as separate version roles; other profiles use different roles for the same idea.

  • The loop — work, versions, score, slot, shipped, and why each step exists.
  • Profiles — how Music, Novel, Podcast and Blog share one schema.
  • Scoring a work — a walkthrough.
  • Data — export, backup, and where the workspace file lives.