Skip to content

Improvement v2 - #9

Merged
iamtheorm merged 5 commits into
ormversionfrom
improvement-V2
Jan 18, 2026
Merged

Improvement v2#9
iamtheorm merged 5 commits into
ormversionfrom
improvement-V2

Conversation

@iamtheorm

Copy link
Copy Markdown
Collaborator

No description provided.

@iamtheorm
iamtheorm merged commit f9c4be7 into ormversion Jan 18, 2026
aschwarzkopf added a commit to aschwarzkopf/speaktype that referenced this pull request Apr 24, 2026
Full code audit surfaced 15 bugs across 4 severity tiers; this commit
addresses 14 of them (karansinghgit#15 deferred — Polar.sh handles server-side). It
also introduces MiniRecorderViewModel, which fixes a subscription-timing
race that caused "box appears, disappears, no audio" when pressing the
hotkey.

All 58 unit tests pass. 2 pre-existing UI test failures are unrelated
(sidebar link lookups).

CRITICAL

karansinghgit#1 AudioRecordingService writer race — replaced `isStopping: Bool` with a
   RecordingStatus enum gated on audioQueue (RosyWriter pattern). All
   writer-state mutation now happens on audioQueue; concurrent/double
   stopRecording calls are safe.
karansinghgit#2 MiniRecorderView NSEvent monitor leak + duplicate Escape handling —
   deleted the global/local NSEvent monitors; introduced
   KeyableMiniRecorderPanel (canBecomeKey=true) so the existing
   KeyEventHandlerView becomes the sole Escape path.
karansinghgit#3 MiniRecorder state hygiene — fixed via the VM refactor below.

HIGH

karansinghgit#4 ModelDownloadService retry bounds — added `maxDownloadAttempts = 2`,
   `defer { activeTasks[variant] = nil }` for exactly-once handle cleanup.
karansinghgit#5 AIModel.recommendedModel force unwrap — replaced with precondition +
   guarded fallback to smallest model.
karansinghgit#6 TranscribeAudioView security-scoped resource — applied Apple's `defer`
   pattern, UUID-prefixed temp names, dropped unsound fallback to scoped
   URL after access was released.
karansinghgit#7 AudioPlayerService silent load failure — loadAudio now async throws;
   added @published var lastError for SwiftUI .alert(item:) observation;
   made the service @mainactor; updated 2 callers.
karansinghgit#8 LicenseManager race — added @published isValidatingLicense flag;
   network/transport failures no longer revoke cached license (only
   explicit server-confirmed invalid responses do), per RevenueCat /
   StoreKit 2 pattern.

MEDIUM

karansinghgit#9  WhisperService loadModel cancellation — pre-guard and post-guard
    Task.checkCancellation() per WWDC21 guidance; `defer` guarantees
    isLoading resets; late-cancelled pipe is discarded.
karansinghgit#10 ModelDownloadService deleteModel — dropped /tmp and ~/.cache paths;
    replaced substring `contains` match with exact last-path-component
    equality; extracted static `shouldDelete(fileName:patterns:)` for
    testability.
karansinghgit#11 UpdateService stale state — on network error, clear availableUpdate
    to avoid showing outdated update banner; added clearAvailableUpdate()
    helper.
karansinghgit#12 MiniRecorderView.barHeight clamp — defensive min/max on audioLevel
    before sqrt() to guard against upstream bugs producing NaN.

LOW

karansinghgit#13 HistoryService deleteItem — filter out-of-bounds offsets before
    `remove(atOffsets:)` to tolerate stale IndexSet during rapid deletes.
karansinghgit#14 MiniRecorderView debugLog — moved off world-readable /tmp to
    ~/Library/Application Support/SpeakType/Logs; gated behind #if DEBUG
    so release builds use print (unified logging) instead.

VM REFACTOR (Option D from research)

Fixes the "no audio on Fn press" bug introduced by karansinghgit#3's initial attempt
to use `.id(sessionID)` for SwiftUI @State reset. The .id() rebuild
forced SwiftUI to teardown/rebuild the view, but notifications posted
synchronously after the swap hit an empty subscriber list — recording
was never triggered.

- New MiniRecorderViewModel (@mainactor, ObservableObject) owns all
  session-lifecycle state: isListening, isProcessing, isWarmingUp,
  statusMessage, cancelCommit, sessionID, pendingAction
- Controller drives the VM directly via startSession/requestStop/
  requestCancel — no more NotificationCenter round-trip, no more .id()
  churn
- View observes via @ObservedObject; three .onReceive blocks collapsed
  into one .onChange(of: viewModel.pendingAction) draining consumeAction()
- SwiftUI guarantees @published subscribers are attached before any
  mutation is observed, eliminating the timing race

Test additions:
- 3 audio concurrency tests, 3 panel, 5 controller, 4 AIModel, 3 audio
  player, 2 license, 5 exact-match deletion + 1 retry bound, 1 Whisper
  cancellation, 2 update service, 2 history offset, 12 VM contract tests

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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