Skip to content

es#12

Merged
maker-or merged 3 commits into
mainfrom
other
Feb 19, 2026
Merged

es#12
maker-or merged 3 commits into
mainfrom
other

Conversation

@maker-or

@maker-or maker-or commented Feb 18, 2026

Copy link
Copy Markdown
Owner

Summary by CodeRabbit

  • New Features

    • Export transcription history to plain-text files from the app UI (with tracking)
    • Fallback provider selection and per-provider API-key handling
    • Configurable transcription timeout durations
    • Improved app lifecycle handling for more reliable behavior
  • Bug Fixes

    • Better timeout/error messages naming the provider that timed out
    • Safer key storage migration to improve access reliability
  • Documentation

    • Added a comprehensive README with setup, usage, and architecture notes

@coderabbitai

coderabbitai Bot commented Feb 18, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

Adds fallback provider support with timeout and automatic retry, history export to text, keychain accessibility migration, analytics tracking for exports and fallbacks, app lifecycle delegation, Groq client prompt handling, and expanded README documentation. Includes UI for export and fallback configuration.

Changes

Cohort / File(s) Summary
Fallback & Transcription Orchestration
dhavnii/Features/Transcription/TranscriptionProvider.swift, dhavnii/Features/Transcription/TranscriptionService.swift
Adds saved fallback provider and timeout config (UserDefaults), TranscriptionError.timeout case and shouldTryFallback, live provider re-reading, per-attempt timeout orchestration, and primary→fallback retry logic with analytics hooks.
Groq Client
dhavnii/Features/Transcription/GroqAPIClient.swift
Adds transcriptionPrompt, new preserveVerbatim param on transcribe, includes prompt when requested, and decodes GroqTranscriptionResponse.
History Export UI & Logic
dhavnii/Features/History/HistoryExporter.swift, dhavnii/Features/Home/HomeView.swift, dhavnii/Features/Settings/SettingsView.swift
New HistoryExporter that formats and saves transcriptions as .txt via NSSavePanel; Home view adds an Export toolbar/button; Settings adds “Export History” action and tracks export analytics.
Settings → Fallback UI
dhavnii/Features/Settings/SettingsView.swift
Introduces FallbackSettingsCard in Providers settings, per-provider API-key handling, timeout configuration, and reset/auto-clear behavior when primary provider changes.
Security / Keychain Migration
dhavnii/Core/Security/SecureStorage.swift, openwispher/openwispherApp.swift
Changes keychain accessibility use and adds SecureStorage.migrateKeychainAccessibility(); onboarding flow invokes migration once after onboarding completes.
User Feedback & Errors
dhavnii/Core/Feedback/UserFeedbackSystem.swift, dhavnii/Features/Transcription/TranscriptionProvider.swift
Maps new timeout(provider:) error to a user-friendly message and updates error descriptions/shouldTryFallback decisions.
Analytics & App Lifecycle
openwispher/AnalyticsManager.swift, openwispher/openwispherApp.swift
Adds appOpenedCount, validates API key at configure time, new tracking methods (trackHistoryExported, trackFallbackUsed), moves lifecycle handling into AppLifecycleDelegate, and invokes analytics hooks on activation.
Documentation
README.md
Comprehensive README added describing features (activation, fallback, exports), architecture, setup, env vars, and contribution guidance.
Small UI / Wiring
dhavnii/Features/Home/HomeView.swift, dhavnii/Features/Settings/SettingsView.swift
Toolbar and header UI adjustments for export visibility; Settings reset now clears fallback-related keys.
Miscellaneous
dhavnii/Core/Feedback/UserFeedbackSystem.swift
Minor addition to user-facing error message handling for timeout errors.

Sequence Diagram(s)

sequenceDiagram
    autonumber
    participant User
    participant App
    participant Primary as Primary Provider
    participant Fallback as Fallback Provider
    participant History as History & Analytics

    User->>App: Trigger transcription
    App->>App: Refresh selected provider & timeout
    App->>Primary: Transcribe (with timeout)
    alt Primary success
        Primary-->>App: Transcription
        App->>History: deliver (save, clipboard, notify)
    else Primary timeout/error (shouldTryFallback)
        Primary--xApp: Error/Timeout
        App->>Fallback: Transcribe (fallback)
        alt Fallback success
            Fallback-->>App: Transcription
            App->>History: trackFallbackUsed(primary, fallback, reason)
            App->>History: deliver (save, clipboard, notify)
        else Fallback fails
            Fallback--xApp: Error
            App->>App: surfaceError to user
        end
    end
Loading
sequenceDiagram
    autonumber
    participant User
    participant HomeView as Home View
    participant HistoryExporter as HistoryExporter
    participant FileSystem as File System
    participant Analytics as Analytics

    User->>HomeView: Click Export
    HomeView->>HistoryExporter: exportAndTrack(records)
    HistoryExporter->>HistoryExporter: Format records as text
    HistoryExporter->>FileSystem: Present NSSavePanel
    User->>FileSystem: Choose location & confirm
    FileSystem-->>HistoryExporter: URL returned
    HistoryExporter->>FileSystem: Write UTF-8 .txt
    FileSystem-->>HistoryExporter: Success
    HistoryExporter-->>HomeView: Return success
    HomeView->>Analytics: trackHistoryExported(count)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • lang #10: Directly overlaps transcription provider and client changes (providers, Groq client updates).
  • nothing 4a #8: Related analytics and lifecycle modifications touching AnalyticsManager and app activation handling.
  • new board #7: Touches openwispherApp onboarding/lifecycle wiring and HotkeyManager onboarding integration.

Poem

🐰 A timeout hops, a fallback springs,
Two providers sing when the first one rings.
Exports spill tales in plain-text rows,
Keychains migrate while the rabbit glows.
Analytics counts the joyful leaps! 🎧

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 54.05% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'es' is vague and generic, providing no meaningful information about the substantial changes in this pull request. Replace with a descriptive title that summarizes the main changes, such as 'Add fallback provider support, history export, and keychain migration' or similar.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch other

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
dhavnii/Features/Settings/SettingsView.swift (1)

1378-1398: ⚠️ Potential issue | 🟡 Minor

resetApp() does not clear the new fallbackTranscriptionProvider and transcriptionTimeoutSeconds AppStorage keys.

When the user resets the app, the fallback provider and timeout settings will persist, which is inconsistent with the "Clear all settings and data" intent.

🛡️ Proposed fix — clear fallback settings on reset
     private func resetApp() {
         UserDefaults.standard.removeObject(forKey: "hasCompletedOnboarding")
         UserDefaults.standard.removeObject(forKey: "autoLaunchEnabled")
         UserDefaults.standard.removeObject(forKey: "selectedTranscriptionProvider")
+        UserDefaults.standard.removeObject(forKey: "fallbackTranscriptionProvider")
+        UserDefaults.standard.removeObject(forKey: "transcriptionTimeoutSeconds")
         for provider in TranscriptionProviderType.allCases {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@dhavnii/Features/Settings/SettingsView.swift` around lines 1378 - 1398, The
resetApp() function currently omits clearing the AppStorage keys for the
fallback provider and timeout; update resetApp to remove the
UserDefaults/AppStorage entries for "fallbackTranscriptionProvider" and
"transcriptionTimeoutSeconds" (or use the same key constants used by the
AppStorage wrappers) alongside the other removeObject calls, ensure these
removes occur before UserDefaults.standard.synchronize(), and if you maintain
corresponding in-memory state variables update them (e.g., clear any
fallbackTranscriptionProvider and reset transcription timeout in appState) so
the app truly returns to default settings.
🧹 Nitpick comments (5)
dhavnii/Features/Transcription/TranscriptionService.swift (2)

199-217: deliver() is marked throws but never throws.

The function signature is async throws but all code paths either return normally or call resetAfterDelay() without throwing. If the throws is intentional for future use, consider adding a comment; otherwise, remove it to avoid misleading callers.

Proposed fix
-    private func deliver(transcription: String, provider: TranscriptionProviderType) async throws {
+    private func deliver(transcription: String, provider: TranscriptionProviderType) async {

And update the call sites on lines 128 and 149:

-            try await deliver(transcription: transcription, provider: primary)
+            await deliver(transcription: transcription, provider: primary)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@dhavnii/Features/Transcription/TranscriptionService.swift` around lines 199 -
217, The deliver(transcription:provider:) function is declared async throws but
currently never throws; remove the throws from its signature (change to async)
and update all call sites (e.g., the invocations referenced around the previous
call locations) to no longer use try/try? or try await (or remove redundant
do/catch blocks) so callers compile; alternatively, if you intend to keep throws
for planned future errors, add a clear comment above deliver explaining why it
remains throwing — reference the deliver(transcription:provider:) method and the
call sites that currently use try/try await so you can update them accordingly.

239-247: liveSelectedProvider() mutates selectedProvider as a side effect.

This getter-like function silently updates self.selectedProvider on line 245. While it keeps the instance variable in sync, it's a hidden side effect that could surprise future maintainers. Consider either renaming to refreshSelectedProvider() to signal mutation, or separating the read from the update.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@dhavnii/Features/Transcription/TranscriptionService.swift` around lines 239 -
247, The method liveSelectedProvider() currently mutates self.selectedProvider
as a hidden side effect; either rename it to refreshSelectedProvider() to make
the mutation explicit (update the function name and all callers) or remove the
mutation so it becomes a pure getter (have liveSelectedProvider() only read
UserDefaults and return a TranscriptionProviderType, and create a new explicit
mutating method e.g. updateSelectedProviderFromDefaults() that assigns to
selectedProvider when state sync is required); update references to
selectedProvider, liveSelectedProvider(), and any callers to use the new
naming/behavior consistently.
dhavnii/Features/History/HistoryExporter.swift (1)

43-72: Consider including a trailing newline in the exported file.

The formatted output ends with the last record's text, with no final newline. Many text editors and Unix tools expect a trailing newline. Minor nit.

Proposed fix
-        return header + blocks.joined(separator: "\n\n────────────────────────────────────────\n\n")
+        return header + blocks.joined(separator: "\n\n────────────────────────────────────────\n\n") + "\n"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@dhavnii/Features/History/HistoryExporter.swift` around lines 43 - 72, The
formatted(_ records: [TranscriptionRecord]) -> String output lacks a trailing
newline; update the function to append a single trailing newline to the returned
string (e.g., return (header + blocks.joined(...)) + "\n") so the exported file
ends with a newline; ensure you only add one newline regardless of record count.
dhavnii/Features/Home/HomeView.swift (1)

338-344: Duplicated export logic — consider extracting a shared helper.

This export-then-track pattern is duplicated verbatim in SettingsView.swift (lines 1497–1501). Consider extracting a shared static method (e.g., on HistoryExporter) that performs the export and conditionally fires the analytics event, so both call sites stay in sync.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@dhavnii/Features/Home/HomeView.swift` around lines 338 - 344, The
export-then-track logic is duplicated; refactor by adding a shared helper on
HistoryExporter (e.g., a static method exportAndTrack(records: [Transcription])
-> Bool) that calls HistoryExporter.exportAsText(records) and, if true, calls
AnalyticsManager.shared.trackHistoryExported(count: records.count); then update
HomeView.exportHistory() (and the duplicate in SettingsView) to call
HistoryExporter.exportAndTrack(viewModel.transcriptions) instead of calling
exportAsText and track separately.
dhavnii/Features/Settings/SettingsView.swift (1)

1136-1173: Timeout picker is visible even when no fallback is selected.

The subtitle reads "Fallback kicks in if primary exceeds this time", which is confusing when the fallback is set to "None". Consider conditionally showing this section only when !fallbackRaw.isEmpty, or at minimum adjusting the description.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@dhavnii/Features/Settings/SettingsView.swift` around lines 1136 - 1173, The
timeout picker and its subtitle are shown even when no fallback is selected;
wrap the HStack (the timeout UI using timeoutOptions, timeoutSeconds,
timeoutLabel) in a conditional that only renders when fallbackRaw is not empty
(e.g., if !fallbackRaw.isEmpty) or alternatively change the subtitle text to be
conditional based on fallbackRaw (show “Fallback kicks in if primary exceeds
this time” when fallback exists and a neutral/helpful string when fallbackRaw is
empty). Ensure the condition references the existing symbols timeoutOptions,
timeoutSeconds, timeoutLabel and fallbackRaw so the correct view is shown or the
description updated.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@dhavnii/Core/Security/SecureStorage.swift`:
- Around line 128-142: The migration currently marks completion unconditionally
in migrateKeychainAccessibility even though try? storeAPIKey(...) can fail
silently; change this to track success per provider by calling storeAPIKey(for:)
and capturing/propagating errors (or returning Bool) so you can determine
overall success, and only set UserDefaults.standard.set(true, forKey:
migrationKey) if all providers succeeded (or alternatively persist per-provider
migration status); update retrieveAPIKey, storeAPIKey usages in
migrateKeychainAccessibility to handle errors explicitly and use those return
values/errors to decide whether to write the migrationKey flag.

In `@dhavnii/Features/Settings/SettingsView.swift`:
- Around line 1210-1218: In saveFallbackKey(), the call to
SecureStorage.storeAPIKey(apiKeyText, for: type) is using try? which swallows
failures but fallbackHasKey is set to true unconditionally; change to a
do-try-catch (or capture the thrown error) around SecureStorage.storeAPIKey and
only set fallbackHasKey = true, isEditingKey = false and apiKeyText = "" when
the store succeeds; on catch, log or handle the error and do not change
fallbackHasKey (keep isEditingKey true so the UI reflects the failed write).
Ensure you reference the saveFallbackKey function, the fallbackRaw ->
TranscriptionProviderType(rawValue:) conversion, SecureStorage.storeAPIKey, and
the fallbackHasKey/isEditingKey/apiKeyText variables when making the change.

In `@dhavnii/Features/Transcription/GroqAPIClient.swift`:
- Around line 15-16: The systemPrompt string (variable systemPrompt in
GroqAPIClient.swift) is grammatically broken and instructs the model to alter
user speech rather than preserve fidelity; replace it with a clear, grammatical
prompt that only provides style/vocabulary/context hints (e.g., preferred
formatting, domain-specific terms) and removes any instruction to rewrite or
delete utterances, and instead make "cleaned" or "normalized" output an explicit
opt-in behavior exposed via a new flag/parameter (e.g., preserveVerbatim: Bool
or normalizeOutput: Bool) on the Groq API call method so callers choose whether
to request corrections; update any uses of systemPrompt (or rename to
transcriptionPrompt) to reflect the changed semantics and ensure the prompt
aligns with Groq/Whisper guidance (vocabulary/style only).

In `@dhavnii/Features/Transcription/TranscriptionService.swift`:
- Around line 129-157: The fallback logic in TranscriptionService currently
treats any non-TranscriptionError as eligible for fallback by using
(primaryError as? TranscriptionError)?.shouldTryFallback ?? true; change this to
default to false so unknown errors do not trigger retries (e.g., use ?? false)
and explicitly exclude CancellationError (or Task.isCancelled) from attempting
the fallback; update the eligibleForFallback computation in the catch block
around primaryError and keep the rest of the fallback flow (attempt(provider:),
deliver(...), surfaceError(...)) unchanged.

In `@README.md`:
- Around line 81-112: The fenced code blocks in README.md (the project tree
block containing lines like "dhavnii/" and the environment variable block
containing "GROQ_API_KEY=<your key>") are missing language identifiers; update
both fenced code blocks to include a language (e.g., add ```text before the
project structure block that starts with "dhavnii/" and add ```text before the
environment variable block that contains "GROQ_API_KEY=<your key>") so they
conform to markdownlint MD040.
- Around line 132-135: Update the README git clone example to use the real
repository path instead of the placeholder: replace "git clone
https://github.com/<your-org>/dhavnii.git" with the actual repo URL "git clone
https://github.com/maker-or/openwispher.git" so the clone command in the README
(the bash snippet) points to maker-or/openwispher.

---

Outside diff comments:
In `@dhavnii/Features/Settings/SettingsView.swift`:
- Around line 1378-1398: The resetApp() function currently omits clearing the
AppStorage keys for the fallback provider and timeout; update resetApp to remove
the UserDefaults/AppStorage entries for "fallbackTranscriptionProvider" and
"transcriptionTimeoutSeconds" (or use the same key constants used by the
AppStorage wrappers) alongside the other removeObject calls, ensure these
removes occur before UserDefaults.standard.synchronize(), and if you maintain
corresponding in-memory state variables update them (e.g., clear any
fallbackTranscriptionProvider and reset transcription timeout in appState) so
the app truly returns to default settings.

---

Nitpick comments:
In `@dhavnii/Features/History/HistoryExporter.swift`:
- Around line 43-72: The formatted(_ records: [TranscriptionRecord]) -> String
output lacks a trailing newline; update the function to append a single trailing
newline to the returned string (e.g., return (header + blocks.joined(...)) +
"\n") so the exported file ends with a newline; ensure you only add one newline
regardless of record count.

In `@dhavnii/Features/Home/HomeView.swift`:
- Around line 338-344: The export-then-track logic is duplicated; refactor by
adding a shared helper on HistoryExporter (e.g., a static method
exportAndTrack(records: [Transcription]) -> Bool) that calls
HistoryExporter.exportAsText(records) and, if true, calls
AnalyticsManager.shared.trackHistoryExported(count: records.count); then update
HomeView.exportHistory() (and the duplicate in SettingsView) to call
HistoryExporter.exportAndTrack(viewModel.transcriptions) instead of calling
exportAsText and track separately.

In `@dhavnii/Features/Settings/SettingsView.swift`:
- Around line 1136-1173: The timeout picker and its subtitle are shown even when
no fallback is selected; wrap the HStack (the timeout UI using timeoutOptions,
timeoutSeconds, timeoutLabel) in a conditional that only renders when
fallbackRaw is not empty (e.g., if !fallbackRaw.isEmpty) or alternatively change
the subtitle text to be conditional based on fallbackRaw (show “Fallback kicks
in if primary exceeds this time” when fallback exists and a neutral/helpful
string when fallbackRaw is empty). Ensure the condition references the existing
symbols timeoutOptions, timeoutSeconds, timeoutLabel and fallbackRaw so the
correct view is shown or the description updated.

In `@dhavnii/Features/Transcription/TranscriptionService.swift`:
- Around line 199-217: The deliver(transcription:provider:) function is declared
async throws but currently never throws; remove the throws from its signature
(change to async) and update all call sites (e.g., the invocations referenced
around the previous call locations) to no longer use try/try? or try await (or
remove redundant do/catch blocks) so callers compile; alternatively, if you
intend to keep throws for planned future errors, add a clear comment above
deliver explaining why it remains throwing — reference the
deliver(transcription:provider:) method and the call sites that currently use
try/try await so you can update them accordingly.
- Around line 239-247: The method liveSelectedProvider() currently mutates
self.selectedProvider as a hidden side effect; either rename it to
refreshSelectedProvider() to make the mutation explicit (update the function
name and all callers) or remove the mutation so it becomes a pure getter (have
liveSelectedProvider() only read UserDefaults and return a
TranscriptionProviderType, and create a new explicit mutating method e.g.
updateSelectedProviderFromDefaults() that assigns to selectedProvider when state
sync is required); update references to selectedProvider,
liveSelectedProvider(), and any callers to use the new naming/behavior
consistently.

Comment thread dhavnii/Core/Security/SecureStorage.swift
Comment thread dhavnii/Features/Settings/SettingsView.swift
Comment thread dhavnii/Features/Transcription/GroqAPIClient.swift Outdated
Comment on lines +129 to +157
} catch let primaryError {
print("⚠️ Primary provider \(primary.rawValue) failed: \(primaryError)")

// Determine whether to try the fallback
let eligibleForFallback = (primaryError as? TranscriptionError)?.shouldTryFallback ?? true

if eligibleForFallback, let fallback, fallback != primary {
print("🔄 Trying fallback provider: \(fallback.rawValue)")
do {
let transcription = try await attempt(
provider: fallback,
audioData: audioData,
timeoutSeconds: timeoutSeconds
)
// Successful fallback — track analytics silently, no user-facing notice
AnalyticsManager.shared.trackFallbackUsed(
primary: primary,
fallback: fallback,
reason: (primaryError as? TranscriptionError)?.analyticsReason ?? "unknown"
)
try await deliver(transcription: transcription, provider: fallback)
} catch let fallbackError {
print("❌ Fallback provider \(fallback.rawValue) also failed: \(fallbackError)")
surfaceError(fallbackError)
}
} else {
surfaceError(primaryError)
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

?? true default may trigger fallback on unexpected error types (e.g., CancellationError).

Line 133: if the primary error is not a TranscriptionError (e.g., CancellationError from structured concurrency), the cast fails and ?? true causes a fallback attempt. This could be surprising — for instance, task cancellation should not trigger a retry with a different provider.

Consider defaulting to false so only known transient errors trigger fallback:

Proposed fix
-            let eligibleForFallback = (primaryError as? TranscriptionError)?.shouldTryFallback ?? true
+            let eligibleForFallback = (primaryError as? TranscriptionError)?.shouldTryFallback ?? false
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@dhavnii/Features/Transcription/TranscriptionService.swift` around lines 129 -
157, The fallback logic in TranscriptionService currently treats any
non-TranscriptionError as eligible for fallback by using (primaryError as?
TranscriptionError)?.shouldTryFallback ?? true; change this to default to false
so unknown errors do not trigger retries (e.g., use ?? false) and explicitly
exclude CancellationError (or Task.isCancelled) from attempting the fallback;
update the eligibleForFallback computation in the catch block around
primaryError and keep the rest of the fallback flow (attempt(provider:),
deliver(...), surfaceError(...)) unchanged.

Comment thread README.md Outdated
Comment thread README.md

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (2)
dhavnii/Features/Home/HomeView.swift (1)

253-269: contentHeader is dead code — never referenced in body.

HomeContentView.body only renders exportToolbar, emptyStateView, or transcriptionsList. contentHeader is declared but never used.

♻️ Proposed fix — remove unused property
-    // MARK: - Header
-
-    private var contentHeader: some View {
-        HStack {
-            VStack(alignment: .leading, spacing: 4) {
-                Text("Home")
-                    .font(.title2)
-                    .fontWeight(.semibold)
-                    .foregroundStyle(.primary)
-                    .font(.subheadline)
-                    .foregroundStyle(.secondary)
-            }
-
-            Spacer()
-        }
-        .padding(.horizontal, 24)
-        .padding(.vertical, 16)
-        .background(.ultraThinMaterial.opacity(0.5))
-    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@dhavnii/Features/Home/HomeView.swift` around lines 253 - 269, The private
computed view property contentHeader is dead code (never referenced by
HomeContentView.body which only uses exportToolbar, emptyStateView, and
transcriptionsList); remove the unused contentHeader declaration from
HomeView.swift to clean up unused code, or if you intended to show a header
instead of removing it, insert contentHeader into HomeContentView.body in the
appropriate place alongside exportToolbar/transcriptionsList/emptyStateView;
reference the contentHeader symbol and HomeContentView.body when making the
change.
dhavnii/Features/Transcription/TranscriptionService.swift (1)

195-197: Replace force-unwrap with a guard let.

With two tasks always added to the group, group.next() will not return nil on the first call, so the ! is technically safe — but it is still a code smell that can cause a silent crash if the group composition ever changes.

♻️ Proposed fix
-            let result = try await group.next()!
-            group.cancelAll()
-            return result
+            guard let result = try await group.next() else {
+                group.cancelAll()
+                throw TranscriptionError.timeout(provider: providerType.rawValue)
+            }
+            group.cancelAll()
+            return result
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@dhavnii/Features/Transcription/TranscriptionService.swift` around lines 195 -
197, Replace the force-unwrap of group.next() with a safe optional unwrap: call
let resultOptional = try await group.next(); use guard let result =
resultOptional else { handle the unexpected nil (e.g., throw a descriptive error
or return a default/failure) } then call group.cancelAll() and return result;
update the surrounding function (TranscriptionService method using the
TaskGroup) to propagate the thrown error if you choose to throw so callers can
handle the unexpected nil.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@dhavnii/Features/History/HistoryExporter.swift`:
- Line 10: The import declaration in HistoryExporter.swift uses the Swift 6-only
access modifier token "internal import UniformTypeIdentifiers"; remove the
access modifier and replace it with a plain import by deleting the "internal"
keyword so the file simply uses "import UniformTypeIdentifiers" (locate the line
containing "internal import UniformTypeIdentifiers" to change).

In `@dhavnii/Features/Settings/SettingsView.swift`:
- Around line 1227-1231: removeFallbackKey() currently swallows
SecureStorage.deleteAPIKey errors via try? and unconditionally sets
fallbackHasKey = false; change it to attempt deletion with do/catch (or
propagate the thrown error) using SecureStorage.deleteAPIKey(for: type) inside
removeFallbackKey(), only set fallbackHasKey = false on successful deletion, and
handle/log or surface the caught KeychainError (e.g., show an alert or
processLogger.error) so the UI remains consistent; ensure you still guard the
TranscriptionProviderType(rawValue: fallbackRaw) check and reference the same
symbols (removeFallbackKey, fallbackRaw, TranscriptionProviderType,
SecureStorage.deleteAPIKey, fallbackHasKey).
- Around line 1133-1134: The Divider() is rendered unconditionally causing a
stray separator when fallbackRaw is empty; wrap the Divider() in the same
conditional used for the API-key and timeout sections (the guard on fallbackRaw,
e.g., the if !fallbackRaw.isEmpty check) so the divider is only shown when those
sections are present (place the Divider() inside the block that contains the
API-key/timeout UI associated with fallbackRaw).

In `@README.md`:
- Around line 133-135: The README contains an incorrect directory change
command: after the git clone command `git clone
https://github.com/maker-or/openwispher.git` the subsequent `cd dhavnii` is
wrong; update that command to `cd openwispher` so it matches the cloned
repository name and allows subsequent steps to run in the correct project
directory.

---

Nitpick comments:
In `@dhavnii/Features/Home/HomeView.swift`:
- Around line 253-269: The private computed view property contentHeader is dead
code (never referenced by HomeContentView.body which only uses exportToolbar,
emptyStateView, and transcriptionsList); remove the unused contentHeader
declaration from HomeView.swift to clean up unused code, or if you intended to
show a header instead of removing it, insert contentHeader into
HomeContentView.body in the appropriate place alongside
exportToolbar/transcriptionsList/emptyStateView; reference the contentHeader
symbol and HomeContentView.body when making the change.

In `@dhavnii/Features/Transcription/TranscriptionService.swift`:
- Around line 195-197: Replace the force-unwrap of group.next() with a safe
optional unwrap: call let resultOptional = try await group.next(); use guard let
result = resultOptional else { handle the unexpected nil (e.g., throw a
descriptive error or return a default/failure) } then call group.cancelAll() and
return result; update the surrounding function (TranscriptionService method
using the TaskGroup) to propagate the thrown error if you choose to throw so
callers can handle the unexpected nil.


import AppKit
import Foundation
internal import UniformTypeIdentifiers

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

internal import requires Swift 6.0 (or the AccessLevelOnImport experimental flag on Swift 5.x).

SE-0409 adds access-level modifiers on import declarations and is available from Swift 6.0. In Swift 5, this feature is still behind the experimental flag AccessLevelOnImport, which is not enabled by default. Since the README targets Swift 5.9+ (Xcode 15), this line will fail to compile unless the project has adopted Swift 6 language mode or explicitly enabled the experimental flag. A plain import UniformTypeIdentifiers is fully equivalent here (a single-module app target gains nothing from access-controlling the import).

🐛 Proposed fix
-internal import UniformTypeIdentifiers
+import UniformTypeIdentifiers
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
internal import UniformTypeIdentifiers
import UniformTypeIdentifiers
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@dhavnii/Features/History/HistoryExporter.swift` at line 10, The import
declaration in HistoryExporter.swift uses the Swift 6-only access modifier token
"internal import UniformTypeIdentifiers"; remove the access modifier and replace
it with a plain import by deleting the "internal" keyword so the file simply
uses "import UniformTypeIdentifiers" (locate the line containing "internal
import UniformTypeIdentifiers" to change).

Comment on lines +1133 to +1134
Divider()
.padding(.horizontal, 16)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Orphaned Divider visible when no fallback provider is configured.

The Divider() at line 1133 is rendered unconditionally. When fallbackRaw is empty, the API-key section (guarded by if !fallbackRaw.isEmpty at line 1035) and the timeout section (guarded by if !fallbackRaw.isEmpty at line 1137) are both hidden, leaving this divider at the bottom of the card with nothing below it — a stray separator.

🐛 Proposed fix — wrap the divider in the same guard
+        if !fallbackRaw.isEmpty {
             Divider()
                 .padding(.horizontal, 16)
-
-        // Timeout threshold — only relevant when a fallback provider is selected
-        if !fallbackRaw.isEmpty {
             HStack { /* timeout row */ }
                 .padding(16)
         }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
Divider()
.padding(.horizontal, 16)
if !fallbackRaw.isEmpty {
Divider()
.padding(.horizontal, 16)
HStack { /* timeout row */ }
.padding(16)
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@dhavnii/Features/Settings/SettingsView.swift` around lines 1133 - 1134, The
Divider() is rendered unconditionally causing a stray separator when fallbackRaw
is empty; wrap the Divider() in the same conditional used for the API-key and
timeout sections (the guard on fallbackRaw, e.g., the if !fallbackRaw.isEmpty
check) so the divider is only shown when those sections are present (place the
Divider() inside the block that contains the API-key/timeout UI associated with
fallbackRaw).

Comment on lines +1227 to +1231
private func removeFallbackKey() {
guard let type = TranscriptionProviderType(rawValue: fallbackRaw) else { return }
try? SecureStorage.deleteAPIKey(for: type)
fallbackHasKey = false
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

removeFallbackKey() ignores delete errors and unconditionally marks key absent.

try? silently discards any KeychainError, but fallbackHasKey = false is set regardless. If the delete fails, the UI shows "no key configured" while the key remains in the keychain — the same class of inconsistency that was fixed in saveFallbackKey().

🐛 Proposed fix
 private func removeFallbackKey() {
     guard let type = TranscriptionProviderType(rawValue: fallbackRaw) else { return }
-    try? SecureStorage.deleteAPIKey(for: type)
-    fallbackHasKey = false
+    do {
+        try SecureStorage.deleteAPIKey(for: type)
+        fallbackHasKey = false
+    } catch {
+        print("❌ FallbackSettingsCard: failed to remove API key for \(type.rawValue): \(error)")
+    }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@dhavnii/Features/Settings/SettingsView.swift` around lines 1227 - 1231,
removeFallbackKey() currently swallows SecureStorage.deleteAPIKey errors via
try? and unconditionally sets fallbackHasKey = false; change it to attempt
deletion with do/catch (or propagate the thrown error) using
SecureStorage.deleteAPIKey(for: type) inside removeFallbackKey(), only set
fallbackHasKey = false on successful deletion, and handle/log or surface the
caught KeychainError (e.g., show an alert or processLogger.error) so the UI
remains consistent; ensure you still guard the
TranscriptionProviderType(rawValue: fallbackRaw) check and reference the same
symbols (removeFallbackKey, fallbackRaw, TranscriptionProviderType,
SecureStorage.deleteAPIKey, fallbackHasKey).

Comment thread README.md
Comment on lines +133 to +135
git clone https://github.com/maker-or/openwispher.git
cd dhavnii
```

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

cd dhavnii should be cd openwispher after the clone.

The clone command (now correctly pointing to maker-or/openwispher.git) creates a directory named openwispher, but the next line still tries to cd dhavnii.

📝 Proposed fix
 git clone https://github.com/maker-or/openwispher.git
-cd dhavnii
+cd openwispher
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
git clone https://github.com/maker-or/openwispher.git
cd dhavnii
```
git clone https://github.com/maker-or/openwispher.git
cd openwispher
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@README.md` around lines 133 - 135, The README contains an incorrect directory
change command: after the git clone command `git clone
https://github.com/maker-or/openwispher.git` the subsequent `cd dhavnii` is
wrong; update that command to `cd openwispher` so it matches the cloned
repository name and allows subsequent steps to run in the correct project
directory.

@maker-or maker-or merged commit 9ecb3cd into main Feb 19, 2026
1 check passed
@coderabbitai coderabbitai Bot mentioned this pull request Feb 19, 2026
Merged
@coderabbitai coderabbitai Bot mentioned this pull request Mar 6, 2026
@maker-or maker-or deleted the other branch March 6, 2026 12:51
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.

1 participant