# One developer, a Rust core, three native UIs: building Dimmy with Claude Code

> How a voice dictation and meeting app is built by one person and an AI agent: the architecture, the playbook the agent reads, the incidents behind its rules, and the numbers.

Source: https://dimmy.app/blog/building-dimmy-with-claude-code/

blog

# One developer, a Rust core, three native UIs: building Dimmy with Claude Code

8 November 2026

Dimmy is a voice app for the desktop. You hold a hotkey, talk, and the text lands where your cursor is. It also records meetings and writes a recap. One person builds it, and almost every commit in its history was made together with Claude Code. This post is about how that works in practice: the architecture, the rules the agent follows, the mistakes that produced those rules, and the parts I still do myself.

There has been a wave of open-source desktop apps, many of them in Rust, whose authors say openly that an AI agent wrote much of the code. This is one more data point, with the numbers and the failures included.

## The problem

Dictation is a simple job: sound in, text out. Most tools that do it well send your audio to a server and charge per month. Meanwhile, local speech models became good enough to run on an ordinary laptop. Dimmy can transcribe on your machine with whisper.cpp, NVIDIA's Parakeet and a few other engines, or with a cloud provider when you pick one and bring your own key. An optional LLM pass removes filler words and fixes punctuation, again locally with llama.cpp or with a provider you choose.

Meetings are the harder half. Dimmy records the microphone and the system audio of a call, with no bot joining it, transcribes it and writes a recap. The recording stays on your computer. Privacy is not a setting I can add later; it decides the architecture. If audio is processed locally, the app has to carry the speech and LLM runtimes, the GPU code paths and the model downloads itself.

## The architecture

Dimmy is a shared Rust core with one native UI per operating system: WinUI 3 and C# on Windows, SwiftUI on macOS, GTK4 and libadwaita on Linux. The core does everything that is not drawing pixels: audio capture, echo cancellation, voice activity detection, transcription, LLM calls, meeting recording, SQLite history, encrypted key storage and licence checks. Windows and macOS call it through a C ABI. The Linux UI is written in Rust and links the core as a crate.

Why not Electron? The interesting parts of this app are specific to each OS: a small always-on-top overlay, global hotkeys, reading the selected text through UI Automation on Windows and the Accessibility API on macOS, capturing the audio that a call app plays. Each of those needs native APIs anyway. With a web shell I would write that native glue and ship a browser on top of it. With native UIs I write the glue and a thin view layer that uses each platform's own controls.

The cost is real: three UIs means every feature exists three times. The rule in the repository is strict parity. A feature works the same on every OS, or it does not ship. The list of cloud models, for example, is one JSON file compiled into the core, and the C# and Swift UIs read the same bytes, so the lists cannot drift apart. The C boundary has its own guard: a test compares the exported symbols with a snapshot file, so a renamed or dropped function fails CI instead of crashing a UI on its first call. Windows and macOS are what I ship; the GTK4 UI stayed an experiment.

## Working with an agent: the playbook

The most important file in the repository is not code. It is [CLAUDE.md](https://github.com/KonradDallaOrg/dimmy/blob/main/CLAUDE.md), a playbook that Claude Code reads at the start of every session. It describes the architecture, where each kind of change goes, the build commands with their exact feature flags, and a list of invariants. Many of those invariants have a date next to them and the word "burned". Each one is a mistake that happened once and that the playbook now prevents.

The playbook also sets a style. Functions assert their preconditions and postconditions in production code, with `assert!` and not `debug_assert!`: a crash with a clear message is better than silent corruption, and an agent that breaks an assumption hits a wall at once. Every bug fix starts with a test that reproduces the failure. Here are three of the rules and the incidents behind them.

### Nothing may stand between captured audio and the disk

On 2 September 2026 a 34-minute meeting produced an 11-minute file. The logs showed that capture was healthy the whole time. The problem was the worker that wrote audio to disk: it also ran transcription and called back into the UI, and one iteration of its loop stalled for 22 minutes. It was not stuck in the speech model; no transcription call was logged in that window. When stop was pressed, the join timed out after 120 seconds and the audio still in memory was lost. 23 minutes of a conversation, gone. A capture-ratio alarm existed, but it ran only at stop, and stop had already given up.

The obvious fix, "make transcription faster", would not have helped. The real fix was structural. The capture loop now does three things: read the shared buffer, write the audio tracks, move its cursor. Transcription, model loads, network calls and UI callbacks live on a second thread, reached through a bounded queue with `try_send`. If the queue is full, the transcript for that window is dropped and the user is told once; the audio is never held back. At stop, the audio files are finalized before the app waits for the transcriber. The playbook calls this THE AUDIO RULE, and it asks one question of any change to the capture loop: not "is this fast?" but "can this ever block?". A test module, `audio_never_blocked`, guards it.

### The feature set is frozen

The Windows core library is built with five cargo features: whisper.cpp on Vulkan, Parakeet, llama.cpp on Vulkan, a noise suppressor and Qwen3-ASR. Twice on 7 May 2026 a session rebuilt it with fewer features, because the change "did not touch" Parakeet or the recap was "cloud only". Both builds were green. But the user's runtime config decides which code path runs, not the diff. The first time, meetings came back with an empty transcript. The second time, dictation did, with the same error repeated for every audio chunk.

The rule now: the feature set is frozen, the build command is written out in full, and dropping a feature that nobody asked to remove counts as a regression. It sounds obvious once it is written down. It was not obvious to a session that was trying to make the build faster.

### rc10 is lower than rc8

SemVer compares a pre-release identifier that contains letters character by character. So `0.7.0-rc10` sorts below `0.7.0-rc8`, because "1" comes before "8". The 0.7.0 line went past rc9 without a dot and published two release candidates that no updater would ever offer. It had happened once before, on an earlier line, and nobody had written it down.

Tags now use `-rc.N` with a dot, which makes the number an identifier of its own, compared as a number. A script, `check-release-tag.sh`, runs in the release workflows before anything is built and checks the tag shape and the order inside each release track. A rule in prose was not enough. A rule that CI enforces is.

## What the agent is bad at, and where I stay in charge

The burned list shows the same few patterns again and again:

* It trusts a green build. CI compiled the Mac app but never launched it. A stale runtime self-test shipped in v0.6.39-rc1 and crashed on first launch; that release was deleted and cut again. The Mac pre-flight script now builds the app and launches it for five seconds. On Windows, three rebuilds in a row went to the wrong output folder while the app kept loading the old DLL.
* It works from the context it has. A branch made from a local copy of staging that was 7 commits behind produced a build where the previous night's work simply did not exist. The playbook now says: fetch first, branch from `origin/staging`, and ask when the base is not clear.
* It reaches for the local fix. "Make transcription faster" and "drop the feature this diff does not use" are both reasonable, local and wrong.
* It likes to build. The playbook has a filter against features that serve completeness instead of users, and since October a plain rule: think before code. A new mechanism or a design choice comes to me first, with the evidence.

Some things stay with me. What to build, and more often what not to build. Anything that depends on real hardware: one crash happened only on Apple Silicon, where a cross-compiled CI build passes and the binary fails at runtime. Releases and tags, because a tag starts a pipeline. Code signing on Windows, which runs on my own machine because the cloud signing key cannot be exported. And the feedback from the people who try the staging builds and tell me what does not work.

The agent writes most of the code. I decide what the code is for, and I read what it produced before it reaches anyone else.

## Numbers

Measured on the `staging` branch on 8 November 2026\. Line counts are plain `wc -l`, so they include comments, blank lines and tests.

* First commit: 23 February 2026\. Commits on staging: 2,133.
* Non-merge commits with a `Co-Authored-By: Claude` trailer: 1,643 of 1,793, about 92%. The trailer means a session took part in the commit, not that the agent wrote every line alone.
* Rust in `core/src`: 89,837 lines in 108 files.
* Exported C functions (`pub extern "C" fn`) in `ffi.rs`: 207.
* Rust tests in the core: 1,554 (`#[test]` plus `#[tokio::test]`).
* `assert!` calls in `core/src`: 1,803.
* Windows UI: about 47,600 lines of C# and 7,000 of XAML. macOS UI: about 47,600 lines of Swift. Linux UI: about 3,900 lines of Rust.
* The playbook, CLAUDE.md: 474 lines.
* Current version in the tree: 0.7.14.

## What's next

More local engines and better ones: the newest is a streaming speech model that types as you speak and labels speakers live in meetings; it runs on the CPU for now. And keeping the playbook short. Every incident wants to add a paragraph, and a playbook that grows without limit stops being read, by people and by agents.

## Try it, read it

Dimmy is open source under the AGPL-3.0\. The code, the docs and the full playbook are on [GitHub](https://github.com/KonradDallaOrg/dimmy). If you build it yourself, it is free with nothing removed; the paid plans buy a signed build, automatic updates and support. Builds for Windows and macOS are on the [download page](https://dimmy.app/#download). Questions and corrections are welcome at [hi@dimmy.app](mailto:hi@dimmy.app).

Konrad, 8 November 2026

[Try Dimmy](https://dimmy.app/#download) [Read the code](https://github.com/KonradDallaOrg/dimmy)

[← All posts](https://dimmy.app/blog/)
