Repository navigation
Review and simplify the commits since 2026-10-02 - #249
Merged
Merged
Conversation
Cleanups from a /simplify pass over a3aab4c..HEAD. Intended behaviour is unchanged, except that the keyboard's release and cancel commands no longer carry the field's text, which the app only reads on a press. iPhone app and keyboard: - One phase mapping. VoiceState.Phase is now PhaseSnapshot.State, the OverlayUIState -> state/message mapping lives once in BlurtiOSCore (it was written out in both DictationCoordinator and VoiceState), and isSettled is defined once on the state. - keyTerms() calls a new KeyTerms.normalized(_:) (split out of parse) instead of repeating its trim-and-dedupe loop. - TermPack.termCap removed in favour of KeyTerms.termCap; staleness constants derive from SyncSTTLimits.maxAudioSeconds and the notice dwell instead of restating 120 s and 2 s. - UtteranceFeed.level(of:) uses MicCapture.linearLevel instead of a copy of the Mac meter's -50 dB mapping. - trimmedNonEmpty() instead of hand-rolled trim + isEmpty; Duration ratios instead of rebuilding seconds from attoseconds; one endTermField() for the four term-field resets; one raw-value choice accessor in SharedStore and a shared defaultThemeID. - The capture delegate skips resampling while no utterance is open (the window holds the mic open for minutes between utterances). - Release and cancel commands no longer copy the field's text across the App Group; only a press reads it. Scripts: ios_require_device and IOS_DEFAULT_DEVICE in ios-lib.sh replace the device-picking prologue copied into seven scripts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01X84Vo9pry7SANcbu86Jjrh
- Hold-to-ask: SelectionSpeechRouting now tracks whether the session is busy (sessionPhaseChanged, fed every phase). A hold while an earlier dictation is still finishing waits for it and then opens the mic, instead of reading that dictation's .pasted as the request failing and resetting the gate mid-hold. Letting go before the mic opened reads the selection and cancels nothing. - SelectionCopy: a ⌘C answered after the 100 ms timeout no longer leaves the selection on the user's clipboard. The first change within a further second is restored, behind the press. - Hold-to-ask is off by default, like skip-jargon: it sends the selection and the request to the LLM Gateway, so it is its own opt-in. - Overlay: a fade-out's completion only dismisses the panel if no later setVisible superseded it (a generation, not the alpha), so the resize path's alpha = 0 can't let a stale fade order out the silent-mic notice. - iPhone: a keyboard press parks its keyboard and field text, and the session's frontmost hook claims them only for a press it accepts. A refused press can no longer redirect the dictation in flight. - AssemblyAITranscriber reads the enhanced-transcripts setting once per request, so rewrite_mode and the pasted transcript always agree. - The developer log records rewrite_mode (fast/smart; absent with the rewrite off). - ListeningWindow's heartbeat no longer holds self across its sleep. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01X84Vo9pry7SANcbu86Jjrh
…lime and Zed The editability check gets a third answer, FocusedEditability.cannotTell: a system-wide focus query that fails with cannotComplete, or a bare AXWindow with no text signal - what editors that draw their own text view report. hasEditableFocusedElement pastes on it, so the custom-drawn editor bundle-ID list, its wrappers and its tests go. Electron and browsers keep their app-identity exemption, since they answer AX with a definite but wrong no. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01X84Vo9pry7SANcbu86Jjrh
The silent mic is an error that writes its reason on the pill, not a separate state: .error(message:notice:) with an optional PillNotice. The notice's words are MicCaptureError's, and its errorDescription is built from the same pair, so the sentence and the pill can't drift apart. Hosts with no pill (the iPhone) go back to matching .error alone. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01X84Vo9pry7SANcbu86Jjrh
On-device phrase expansion runs on the text the API returned and puts nothing on the wire, so it is Blurt's text processing, not part of the API client. TextShortcut and TextShortcutExpander move to Sources/BlurtEngine/Pipeline; imports of AssemblyAI that existed only for them go, files that use them on the iPhone import BlurtEngine, and the mutation-test target, READMEs and AGENTS.md follow. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01X84Vo9pry7SANcbu86Jjrh
…ngIPC DictationSession.contextQueue and offPool were the engine's one home for blocking cross-process work, but read-aloud's selection read and clipboard copy reached into the dictation actor for them. BlockingIPC.queue and BlockingIPC.run hold them now, named for what they run, and the press path, SelectionSpeaker and SelectionCopy all use it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01X84Vo9pry7SANcbu86Jjrh
DictationConfig.rewriteMode was a String?, so the SDK accepted any word and the documented values lived in BlurtEngine. DictationRewriteMode (fast, smart) is the SDK's now; the engine's RewriteMode maps to it in wireValue, still omitting the key for Fast. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01X84Vo9pry7SANcbu86Jjrh
- PhaseSnapshot.State's init(_:) and message(for:) are nonisolated: the iPhone core defaults to the main actor, and VoiceState calls them from a nonisolated init. isSettled moves beside them, which also brings SharedState.swift back under the 400-line limit. - The last OverlayUIState test matching .error with one binding matches both values. - The enhanced-transcripts read-once test moves to RewriteModeTests, and two comments shorten, so AssemblyAITranscriber.swift and its test suite are back within file_length / type_body_length. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01X84Vo9pry7SANcbu86Jjrh
The Mac and iPhone each carried a copy of the same @observable model, kept in step by a comment. It moves to Sources/BlurtEngine/Config as a public @mainactor type with the Mac's full surface (current, storage, save), and its tests move to the engine suite. Observation is a toolchain module, like Synchronization, so the dependency-free rule holds. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01X84Vo9pry7SANcbu86Jjrh
STTPrompt, TranscriptionContext and DictationSession imported AssemblyAI only for TextShortcut; TextShortcutExpander has no caller outside the engine, so it is internal (periphery's redundant-public check). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01X84Vo9pry7SANcbu86Jjrh
main's #248 made the same APIKeyModel merge this branch did (one public @mainactor @observable class in BlurtEngine, the Mac project's four references dropped); its versions of APIKeyModel.swift and APIKeyModelTests.swift are taken. jscpd finds no clones in the merge. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01X84Vo9pry7SANcbu86Jjrh
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What & why
A
/code-reviewand a/simplifypass over the 16 commits since 2026-10-02 (a3aab4c..9405b76). After those, five structural refactors, then fixes from CI.fix:code-review findingsSelectionSpeechRoutingnow tracks whether the session is busy: the app passes every phase tosessionPhaseChanged, which replacessessionReachedTerminalPhase. A hold over a selection while the earlier dictation is still transcribing now waits for that dictation to end, then opens the mic. Before, it read that dictation's.pastedas the request failing and reset the gate while the key was still held. Letting go before the mic opened reads the selection and cancels nothing.SelectionCopyclobbering the clipboard. If ⌘C was answered after the 100 ms timeout, the selection stayed on the user's clipboard. Now the first clipboard change within a further second is restored, in the background so the press doesn't wait.setVisiblecall superseded it. It checks a generation counter instead of the alpha value, so the resize path settingalphaValue = 0can't let a stale fade hide the silent-mic notice.frontmosthook claims them, and that hook runs only for a press the session accepts.AssemblyAITranscriberreads the enhanced-transcripts setting once per request, sorewrite_modeand the pasted transcript always agree.rewrite_mode(fastorsmart; absent with the rewrite off).ListeningWindow's heartbeat no longer holdsselfstrongly across its 5 s sleep.refactor:cleanups (intended behaviour unchanged, except one item)VoiceState.Phaseis a typealias ofPhaseSnapshot.State.OverlayUIStateto state/message mapping lives once inBlurtiOSCore.isSettledis defined once, on the state.KeyTerms.normalized(_:)is split out ofparseand used by the iPhone'skeyTerms().TermPack.termCapis replaced byKeyTerms.termCap.SyncSTTLimits.maxAudioSecondsand the notice dwell. Its level meter usesMicCapture.linearLevel, now public.trimmedNonEmpty(),Durationratios instead of attosecond arithmetic, oneendTermField(), and one raw-value accessor inSharedStoreplusdefaultThemeID.ios_require_deviceandIOS_DEFAULT_DEVICEinios-lib.shreplace the device-picking code copied into seven scripts.Structural refactors
FocusCapture.FocusedEditabilityadds a third answer,.cannotTell. It covers a system-wide focus query that fails withcannotComplete, and a bareAXWindowwith no text signal, which is what editors that draw their own text view report.hasEditableFocusedElementpastes on it, so the Sublime/Zed bundle-ID list goes away..inputSilentfolds into.error(message:notice:), with an optionalPillNotice. Its wording is built once inMicCaptureError.TextShortcutandTextShortcutExpandermove from theAssemblyAISDK to the engine. They do on-device text processing and put nothing on the wire.BlockingIPC.queueandBlockingIPC.runreplaceDictationSession.contextQueueandoffPool, which read-aloud had been reaching into.rewrite_modeis typed:DictationRewriteModereplacesString?.APIKeyModelmerge is nowmain's. Gate on copy/paste detection (jscpd) and remove every clone it found #248 merged the two copies into one engine type independently, so this branch takesmain's version.How it was tested
Authored in a Linux sandbox with no Swift toolchain, so CI on macOS is the only Swift build and test run.
scripts/check.sh --portablepasses, including the new jscpd gate (0 clones).1a5e46a, every CI job passed (compile, the iOS build, both iOS sanitizers, the engine tests) exceptcheck. Its two lint failures, unused imports and a redundantpublic, are fixed in49be1fb.New tests:
rewrite_modein the log and on the wire.KeyTerms.normalized.UtteranceFeed.isCapturing.FocusedEditability.The overlay generation counter and the iPhone press claim have no unit tests.
scripts/check.shpasses (or CI will, if I'm not on a Mac)🤖 Generated with Claude Code
https://claude.ai/code/session_01X84Vo9pry7SANcbu86Jjrh