Skip to content

Frontend with Main feature - #2

Merged
karansinghgit merged 1 commit into
mainfrom
ormversion
Jan 8, 2026
Merged

Frontend with Main feature#2
karansinghgit merged 1 commit into
mainfrom
ormversion

Conversation

@iamtheorm

Copy link
Copy Markdown
Collaborator

Production Readiness Overhaul

This PR transitions the application from a prototype to a production-ready macOS application. It introduces core system-level features and refactors the app architecture to support background execution, global shortcuts, and always-on-top windows.

What’s Included

  • Floating Mini Recorder with global toggle support
  • First-time onboarding flow for permissions
  • Local AI model download and management
  • Persistent transcription history
  • Refactored app lifecycle using AppDelegate
  • Redesigned UI with sidebar navigation and centralized design system

Key Changes

Core Features and Architecture

Mini Recorder

  • Floating, always-on-top recorder window
  • Toggle logic for showing and hiding via global shortcuts
  • Files:
    • MiniRecorderView
    • MiniRecorderWindowController

Application Lifecycle

  • Introduced AppDelegate to manage lifecycle events
  • Prevents the app from terminating when the last window is closed
  • Enables background execution
  • Files:
    • AppDelegate
    • speaktypeApp

Services and Logic

AI Model Management

  • Service for downloading and tracking WhisperKit models
  • Files:
    • ModelDownloadService
    • AIModelsView

Transcription History

  • Persistent storage for all transcription results
  • Files:
    • HistoryService
    • HistoryView

Onboarding and Permissions

  • Structured onboarding flow for first-time users
  • Handles required permissions:
    • Microphone
    • Accessibility
  • Files:
    • OnboardingView
    • PermissionService

UI and UX Improvements

  • Redesigned main dashboard with sidebar-based navigation
  • Added settings for audio input selection and paste behavior
  • Centralized color palette in AppColors.swift

File Summary

Added

  • AppDelegate.swift
  • MiniRecorderWindowController.swift
  • MiniRecorderView.swift
  • ModelDownloadService.swift
  • HistoryService.swift
  • OnboardingView.swift
  • AIModelsView.swift
  • HistoryView.swift

Modified

  • speaktype.entitlements
    • Added Network and Apple Events entitlements
  • WhisperService.swift

Removed

  • HotkeyService.swift

@karansinghgit
karansinghgit merged commit 4bc58a0 into main Jan 8, 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