Claude Code guidance

September 21, 2026 · View on GitHub

Macro Deck 3: .NET host (owns state and business logic), an Angular desktop UI and a framework-free web client (REST + JSON WebSocket), Tauri bootstrapper (owns the native window and host process).

Workflow

Branch before working. Prefix feature/, fix/, refactor/, chore/, docs/, ci/ plus a short kebab-case description and the issue number when one exists.

Public SDK and plugin compatibility

A plugin may stay compiled against an old SDK indefinitely. Every non-obsolete public plugin-facing contract is a compatibility commitment: sdk/, ui-model/, protocol/, plugin HTTP/WebSocket behaviour, manifest and package formats, analyzer diagnostic ids, conformance check ids. Source and binary compatibility both count.

If a fix needs to break one, stop and ask before implementing it. Protocol breaks require a new protocol major with the previous version still served.

When these surfaces change, run the compatibility, conformance, deprecation-lifecycle, protocol, and package tests, and add a regression test from the old consumer's point of view.

See docs/ and ADR 0026.

Testing

Derive expectations from the requirement or contract, never from the current implementation. When a test and the implementation disagree, go back to the requirement instead of editing either side.

Prefer a few meaningful scenarios to coverage. Do not test private methods, internal call sequences, trivial accessors, or framework behaviour; mock real external boundaries only.

Localization

No user-facing hardcoded strings. Anything a person can read in the app (labels, headings, buttons, tooltips, aria-labels, placeholders, dialog and toast text, validation and error messages, empty states, status text, integration action and parameter names, native menu and dialog text) comes from a localization resource, and ships with a translation in every language the app already carries. English is the default and the fallback; a new key missing a value in any other shipped language (German, Italian, Czech, Polish, Spanish, French) is an incomplete change, not a follow-up.

Internal strings are exempt and stay as they are: ILogger messages, exception text that is only ever diagnostic, protocol and enum values, error codes, identifiers, config keys, routes, storage keys, CSS class names, icon names, and test ids.

New keys go in macrodeck.app (host/src/MacroDeckHost.Localization/Localization/), reached through the generated AppStrings in C#, ClientAppStrings from @macro-deck/runtime in the web client, and AppStrings / 'macrodeck.app:Key' | translate in Angular. The web client compiles in only the slice of the catalog it can paint - the whole thing is half a megabyte of strings a deck never shows - so a key it needs must sit under one of the namespaces listed in GeneratedTypeScriptDriftTests, or that list grows and the modules are regenerated. Only add to macrodeck (sdk/src/MacroDeck.Localization/Resources/) when the string is genuinely reusable by plugins: that catalog is published in the SDK, and its keys are a frozen, additive-only contract.

Reuse an existing key rather than minting a duplicate, keep one key per sentence with {placeholders} for values instead of concatenating translated fragments, and use a [plural] family for anything that counts.

Translate for meaning, not word by word: a translated label may be phrased differently from the English one where that reads better. Each shipped language has its own established register, kept consistent across the whole catalog:

  • German, Italian, Spanish, French: informal address (du/tu//tu), imperatives without a pronoun (Wähle ein Ereignis, not Wählen Sie ein Ereignis), lowercase mid-sentence for German.
  • Czech, Polish: impersonal phrasing, meaning imperative verb forms for actions and impersonal statuses/errors rather than direct ty address, matching each language's own desktop-software convention.

Czech and Polish also need a plural-form adjustment the other languages don't: the localization compiler only distinguishes count == 1 from every other count (see the localization guide), which is grammatically exact for German/Italian/Spanish/French but not for Czech/Polish's few/many forms. Phrase a Czech or Polish Other form to avoid noun-count agreement (a count-agnostic label rather than a declined noun) so it stays grammatical for every count.

After changing a resource, regenerate the checked-in TypeScript and Rust catalogs:

MACRODECK_UPDATE_GENERATED=1 dotnet test sdk/tests/MacroDeck.Localization.Tests.UnitTests

See docs/src/content/docs/features/localization.md.

Icons

App icons come from Lucide, which includes the Feather set. When Lucide has no icon for the concept, draw one in the same style: 24x24 viewBox, no fill, stroke width 2, round caps and joins. Mask icons in ui/runtime/styles/icons/ use stroke="#000"; icons shown as images set the colour they need.

The only exceptions are logos of third-party products (take them from Dashboard Icons or Simple Icons, otherwise the vendor's own logo), the Macro Deck logo and branding, the weather widget's icons and the music player's animated playing badge. Every icon file is either attributed in third-party/attributions.yml or listed as first-party in third-party/config.yml; the license tool fails otherwise (see third-party notices).

Published ui.icon names keep their meaning: an icon may be redrawn in this style, never replaced by a different concept (ADR 0084).

Code comments

Internal implementation code normally carries no comments and no XML documentation. Comment only for a reason the code cannot express: a security or trust assumption, a compatibility or protocol constraint, a platform workaround, an ordering or race requirement, deliberately surprising behaviour, an external constraint.

Such a comment is a // line comment of at most two lines. No block comments and no JSDoc in implementation code, in any language: no /* */, no /** */, no <!-- -->. If two lines cannot carry the reason, the reason is too large for a comment and belongs in engineering/ or an ADR that the code names. Two short comments stacked to dodge the limit is the same thing as one long one.

Do not use em dashes or backticks in comments. A colon, a full stop or a plain hyphen carries the same break, and a bare identifier reads the same as a quoted one.

Public SDK/plugin members are the one exception, because their XML docs are a published contract: they get concise /// docs for non-obvious contract semantics, lifecycle, side effects, and failure behaviour, never a restatement of the signature.

Documentation

docs/ is the published Astro/Starlight site: a user guide under guide/ for people using Macro Deck, and public plugin, SDK, protocol, and compatibility documentation for developers. No standalone internal files. engineering/ is internal architecture, workflow, and ADRs. ADRs use 0000-template.md and exist only for project-wide decisions that are costly to reverse.

Link to code, config, or schemas instead of restating them: if a document would have to change whenever an implementation detail or CI job name changes, reference the source instead.

Update developer documentation only for a change to a public developer contract, the security/trust model, a contributor workflow, a release operation, or an architecture decision.

Keep the user guide (docs/src/content/docs/guide/) current in the same change: when you change something a user sees or does (installation and packages, supported platforms and devices, settings and their labels, setup steps, limitations, troubleshooting symptoms), update the affected guide pages and, if the UI changed visibly, their screenshots under docs/src/assets/guide/. Take screenshots from a disposable instance as described in docs/README.md.

Package-local README.md files are NuGet PackageReadmeFile assets. Do not remove, rename, merge, or move them during documentation cleanup unless you update package metadata and verify the resulting .nupkg readme.

Common commands

dotnet build MacroDeck.slnx -c Release -warnaserror
dotnet test MacroDeck.slnx -c Release

ui/angular/: npm run build:prod, npm test. desktop-ui is the only Angular project; it consumes ui/runtime/ from its build output, which the repo npm scripts build first.

ui/web-client/: npm run build, npm test. The public client is framework-free and built rather than served; it and desktop-ui both consume ui/runtime/, which builds first.

ui/bootstrapper/: cargo fmt --check, cargo clippy --all-targets -- -D warnings, cargo test.

More: engineering/README.md, docs/README.md.