Getting Started
Install
Section titled “Install”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.
Build from source
Section titled “Build from source”You need Rust (1.87 or newer), Node 22+ and pnpm.
git clone https://github.com/lacodda/kilna.gitcd kilnapnpm installpnpm tauri devpnpm 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:
$ npx shadcn@latest add https://lacodda.github.io/dowel/r/button.jsonTwo 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
selectableon anything that is genuinely text someone may want to copy - a version body, a note, a path, a diff. Inputs andcontenteditablekeep it automatically.Markdownalready 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-autoalone 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:
cd src-tauricargo testcargo clippy -- -D warningsFirst run
Section titled “First run”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.
What a workspace is
Section titled “What a workspace is”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.
Where it lives
Section titled “Where it lives”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.
Put your first work in
Section titled “Put your first work in”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.
Where next
Section titled “Where next”- 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.