Skip to content

Review and simplify the commits since 2026-10-02 - #249

Merged
alexkroman merged 11 commits into
mainfrom
claude/review-simplify-commits-i5t564
Oct 15, 2026
Merged

alexkroman merged 11 commits into
mainfrom
claude/review-simplify-commits-i5t564

Conversation

@alexkroman

@alexkroman alexkroman commented Oct 15, 2026 •

Copy link
Copy Markdown
Collaborator

What & why

A /code-review and a /simplify pass over the 16 commits since 2026-10-02 (a3aab4c..9405b76). After those, five structural refactors, then fixes from CI.

fix: code-review findings

  • Hold-to-ask while a dictation is still finishing. SelectionSpeechRouting now tracks whether the session is busy: the app passes every phase to sessionPhaseChanged, which replaces sessionReachedTerminalPhase. 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 .pasted as 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.
  • SelectionCopy clobbering 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.
  • Hold-to-ask is now off by default, like skip-jargon: it sends the selection and the spoken request to the LLM Gateway, so it gets its own opt-in. ⚠️ This changes behaviour for anyone who never touched the toggle.
  • Overlay fade race. A fade-out's completion handler now dismisses the panel only if no later setVisible call superseded it. It checks a generation counter instead of the alpha value, so the resize path setting alphaValue = 0 can't let a stale fade hide the silent-mic notice.
  • iPhone: a refused press redirected the dictation in flight. A keyboard press now parks its keyboard and field text in a pending slot. The session's frontmost hook claims them, and that hook runs only for a press the session accepts.
  • 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 or smart; absent with the rewrite off).
  • ListeningWindow's heartbeat no longer holds self strongly across its 5 s sleep.

refactor: cleanups (intended behaviour unchanged, except one item)

  • iPhone phase mapping. It was written out in three places; now there is one:
    • VoiceState.Phase is a typealias of PhaseSnapshot.State.
    • The OverlayUIState to state/message mapping lives once in BlurtiOSCore.
    • isSettled is defined once, on the state.
  • Key terms. KeyTerms.normalized(_:) is split out of parse and used by the iPhone's keyTerms(). TermPack.termCap is replaced by KeyTerms.termCap.
  • Restated constants. The iPhone's staleness limits are derived from SyncSTTLimits.maxAudioSeconds and the notice dwell. Its level meter uses MicCapture.linearLevel, now public.
  • Small reuse: trimmedNonEmpty(), Duration ratios instead of attosecond arithmetic, one endTermField(), and one raw-value accessor in SharedStore plus defaultThemeID.
  • Wasted work on the iPhone:
    • The capture delegate skips resampling while no utterance is open.
    • The one behaviour change: release and cancel commands no longer copy the field's text across the App Group, since the app only reads it on a press.
  • Scripts: ios_require_device and IOS_DEFAULT_DEVICE in ios-lib.sh replace the device-picking code copied into seven scripts.

Structural refactors

  • Pasting where Accessibility can't tell. FocusCapture.FocusedEditability adds a third answer, .cannotTell. It covers a system-wide focus query that fails with cannotComplete, and a bare AXWindow with no text signal, which is what editors that draw their own text view report. hasEditableFocusedElement pastes on it, so the Sublime/Zed bundle-ID list goes away. ⚠️ Worth checking on hardware: a native app whose focused element is a bare window with no text field now beeps on ⌘V instead of copying.
  • .inputSilent folds into .error(message:notice:), with an optional PillNotice. Its wording is built once in MicCaptureError.
  • TextShortcut and TextShortcutExpander move from the AssemblyAI SDK to the engine. They do on-device text processing and put nothing on the wire.
  • BlockingIPC.queue and BlockingIPC.run replace DictationSession.contextQueue and offPool, which read-aloud had been reaching into.
  • The SDK's rewrite_mode is typed: DictationRewriteMode replaces String?.
  • The APIKeyModel merge is now main'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 takes main'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 --portable passes, including the new jscpd gate (0 clones).
  • On head 1a5e46a, every CI job passed (compile, the iOS build, both iOS sanitizers, the engine tests) except check. Its two lint failures, unused imports and a redundant public, are fixed in 49be1fb.

New tests:

  • Hold-to-ask routing during an earlier dictation.
  • A late ⌘C copy restoring the clipboard.
  • The hold-to-ask default.
  • The once-per-request setting read.
  • rewrite_mode in 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.sh passes (or CI will, if I'm not on a Mac)
  • I read AGENTS.md and this doesn't reintroduce anything deliberately removed
  • Docs updated if behavior changed

🤖 Generated with Claude Code

https://claude.ai/code/session_01X84Vo9pry7SANcbu86Jjrh

claude added 11 commits October 15, 2026 01:37
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
@alexkroman
alexkroman merged commit 49451ea into main Oct 15, 2026
14 checks passed
@alexkroman
alexkroman deleted the claude/review-simplify-commits-i5t564 branch October 15, 2026 03:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants