Building per-app dictation on macOS: focus, profiles, and optional send Just Tools LTD, developer of the Mac dictation app Clavio, has detailed how it routes dictation by application identity on macOS, using NSWorkspace activation notifications and bundle identifiers to track the live frontmost app, the last external app, and a per-session target. The app stores writing preferences, listening mode, and optional auto-send actions as separate per-app profile settings, with a resolver that inherits intensity and tone independently rather than treating a profile as a single custom-or-default choice. Dictating a coding prompt and dictating an email can involve the same microphone and speech recognizer. They should not necessarily involve the same writing style—or the same action after the text appears. We build Clavio, hands-free dictation for Mac , at Just Tools LTD. This is a developer's explanation of one part of the app: how we separate application identity, writing preferences, listening mode, and delivery. The implementation details below are based on our Mac code; the short pseudocode is illustrative, not a drop-in library. The video above is our existing walkthrough, including wake-word setup and a separate profile for Claude. On macOS, NSWorkspace exposes the frontmost application and an application-activation notification. That gives a useful starting point: identify an app by its bundle identifier, not its display name. But reading the frontmost app only when transcription finishes is too late. Someone can start dictating in an editor, click the dictation panel, and finish speaking while that panel has focus. The panel should not become the writing-style target merely because it is visible. We distinguish three values: | Value | What it represents | Why keep it? | |---|---|---| | Live frontmost app | The latest activated app, including Clavio | Current UI and focus decisions | | Last external app | The latest activated app other than Clavio | Preserve useful context when our own UI appears | | Session target | The target selected when a recording session starts | Keep processing associated with that session | Clavio seeds the focus cache on launch and updates it from activation notifications. Published UI state is updated on the main queue. This also avoids repeatedly requesting application information in UI hot paths. There is an important edge case: starting dictation inside Clavio must not blindly reactivate an older external app. A remembered target is context, not permission to send text anywhere. Apple documents the underlying application-activation notification https://developer.apple.com/documentation/appkit/nsworkspace/didactivateapplicationnotification . A profile is not just a long prompt. Clavio stores user preferences keyed by the application's bundle ID, including: These choices answer different questions. “Professional tone” describes the output. “Wake word” describes when capture begins. “Press Return” describes a later interaction with the destination app. Combining them into one setting makes surprising behavior harder to explain. The app profile exposes listening choices separately from writing preferences. For example, someone might choose a keyboard-triggered mode and a professional tone for email, then wake-word activation and a direct tone for a coding assistant. Automatic sending remains a separate decision in each app. This is application-level identity. A browser bundle ID alone does not identify Gmail versus another website in a neighboring tab. Website-specific routing is a separate problem; it should not be implied by an app-level profile. An unset override should mean “inherit,” not “erase the default.” Clavio's writing-settings resolver handles intensity and tone independently. For intensity, it checks the explicit app preference, then an app/catalog-derived default, then the user's global preference, and finally a built-in fallback. Tone has its own path: explicit app tone, catalog tone, user tone, or no specific tone. An illustrative version looks like this: intensity = firstAvailable app.intensityOverride, catalogEntry.defaultIntensity, user.defaultIntensity, builtInIntensity tone = firstAvailable app.toneOverride, catalogEntry.defaultTone, user.preferredTone, noSpecificTone Why two paths? A user may want a particular tone while leaving the amount of rewriting inherited. Treating the whole profile as either “custom” or “default” would lose that distinction. When both values have explicit app overrides, the resolver skips the catalog lookup. When a default cannot be obtained, it falls back instead of making dictation depend on a catalog response. That fallback does not make speech transcription offline; it only keeps preference resolution from being an unnecessary blocker. Tone and editing intensity describe different dimensions of the result. The listening-mode resolver checks an explicit per-app override and otherwise inherits the global mode. The recording coordinator consumes that result when application activation changes. The distinction between Wake Word and Always On must remain meaningful. If a wake detector cannot operate, silently treating ordinary speech as an Always On session would change what the user agreed to. The resolver preserves the selected mode; runtime components can suspend capture without relabeling it as another mode. Custom app wake phrases also need a destination check. In Clavio, the detector can listen for a combined set of custom phrases, but the trigger is checked against the current app. The shared brand wake phrases remain separate from those app-specific additions. The overall flow is easier to reason about when its stages stay explicit: Application activation → update focus context Recording starts → choose session target Recording finishes → transcribe and resolve writing settings Text is ready → check delivery destination and insert Post-paste action → perform only the configured action, if any Posting a paste shortcut is not the same as proving that an app accepted the text. Electron apps, native fields, focus changes, and timing can all affect delivery. Clavio keeps delivery diagnostics separate from the act of posting the event, and its insertion paths include checks for security-sensitive destinations such as password fields. The same distinction matters for Return: depending on the destination, it can send a message or simply insert a new line. Automatic sending is useful for some prompting workflows, but users should be able to leave it disabled wherever they want to review text first. These are useful engineering tests even if the recognizer itself is already accurate. A perfect transcript in the wrong app, with an unexpected Return key, is still a poor result. If you want to try the implementation rather than build it, Clavio is available for Apple Silicon Macs running macOS 14.2 or later https://clavioapp.com/?utm source=devto&utm medium=article&utm campaign=per app architecture . Dictation transcription and text polishing use online processing; wake-word and voice-profile functions run on the Mac. The current plans https://clavioapp.com/ plans explain the Free allowance and Pro features, including the advanced controls shown in the walkthrough. For this kind of Mac utility, the design work extends beyond converting speech into text: the app must keep track of which settings apply, which destination belongs to a session, and which actions the user actually selected.