Wave is a lightweight macOS dictation app. Press a shortcut, speak, and your words are transcribed and pasted at the cursor. It supports on-device Whisper and Groq cloud transcription, plus an AI Mode that sends the transcription to an LLM and pastes the response.
- Single source of truth. All app state lives in one
@Observable @MainActorclass (AppState). Do not create additional observable singletons or pass state through multiple layers. - Services are owned by
AppState, not created in views. Views call methods on the state object; they never instantiate services directly. - All persistent settings are stored in
UserDefaultsviadidSeton the property. No other persistence mechanism. - Views are display-only. Business logic belongs in
AppStateor a service. If a view is doing more than formatting and rendering, move the logic out.
Follow these rules consistently. Do not invent new patterns when an existing one covers the case.
Typography
- Section headers: size 11, medium weight, secondary color, uppercase
- Row labels / body: size 13
- Secondary detail: size 12, secondary color
- Monospaced inputs (API keys, code): size 12, monospaced design
- Captions / metadata: size 11
Spacing
- Page padding: 16pt on all sides
- Between sections: 20pt
- Within a section: 8pt
Backgrounds
- Card / row:
.quaternary.opacity(0.5)withRoundedRectangle(cornerRadius: 8) - Standard button:
.quaternarywithRoundedRectangle(cornerRadius: 6) - Primary action button:
Color.brand.opacity(0.15)background,Color.brandforeground,RoundedRectangle(cornerRadius: 7). Brand color is#958BF9, defined asColor.brandinColor+Brand.swift— always useColor.brand, never.blue.
Buttons
- Always
.buttonStyle(.plain) - Font: size 12, medium weight, rounded design
- Standard padding: 10pt horizontal, 4pt vertical
- Primary padding: 10pt horizontal, 5pt vertical
Sections
Every settings section follows this exact structure: uppercase label at size 11 in secondary color, followed by the content at 8pt spacing. Reuse the existing section(_:content:) helper found in each settings view — do not create a new one.
The sidebar is a NavigationSplitView with a fixed-width column. Items are grouped into sections. The detail pane renders the selected view.
- Settings pages (General, Shortcut, Models) are not scrollable — use a plain
VStackwith padding. - The window size is fixed. Do not make it resizable or change its dimensions without explicit instruction.
- Sidebar icons use SF Symbols via
Label("Title", systemImage: "symbol.name").
- Distinguish Left vs Right modifier keys explicitly — flag bitmasks alone (
maskAlternate,maskCommand) are identical for both sides. Track the exact keyCodes being held to tell them apart. - The shortcut recorder persists the pressed combination (
savedCombo) after the user releases keys, so Enter can confirm it even after release. Regular key presses save immediately without needing Enter.
Keep the menu bar menu lean and in this order:
- Status (idle / recording / transcribing / error)
- Recent Transcriptions submenu (max 7, copy on click, truncate long text)
- Check for Updates
- Settings (⌘,)
- Quit (⌘Q)
Dividers between logical groups, not between every item.
- No docstrings or comments on code you didn't write or change.
- No error handling for internal code paths — only at system boundaries (user input, external APIs).
- No helpers or abstractions for one-off operations.
- No speculative features, flags, or "future-proofing."
- Prefer editing existing files over creating new ones.
- If something is unused, delete it — don't rename it with a leading underscore or leave a comment.
- Do not add
ScrollViewto settings pages. - Do not mock services in a way that diverges from production behavior.
- Do not re-add
CommandGroup(replacing: .undoRedo) {}— it breaks Cmd+Z in text fields. - Do not remove the menu-cleaning logic in
applicationDidBecomeActive— it removes ghost headers left by SwiftUICommandGroupreplacements. - Do not push to main or create PRs without being explicitly asked.