Add option to directly transcribe to file#188
Add option to directly transcribe to file#188atkinchris wants to merge 3 commits intokitlangton:mainfrom
Conversation
📝 WalkthroughWalkthroughThis PR introduces a transcription output mode feature that allows users to choose between pasting transcriptions into the focused application or appending them to a file. It includes new UI controls in settings, a new Changes
Sequence DiagramsequenceDiagram
participant User
participant UI as GeneralSectionView
participant Settings as HexSettings
participant TranscriptionFeature
participant FileSystem as File System/<br/>Focused App
User->>UI: Select transcription output mode
UI->>Settings: Save transcriptionOutputMode & filePath
Settings->>Settings: Persist to disk
Note over User,FileSystem: During Transcription
User->>TranscriptionFeature: Complete transcription
TranscriptionFeature->>Settings: Read transcriptionOutputMode
alt outputMode == pasteIntoFocusedApp
TranscriptionFeature->>FileSystem: Paste into focused app
else outputMode == appendToFile
TranscriptionFeature->>FileSystem: Resolve file path<br/>(configured or default)
TranscriptionFeature->>FileSystem: Ensure parent directory exists
TranscriptionFeature->>FileSystem: Append timestamped transcript
end
TranscriptionFeature->>FileSystem: Play paste sound
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Possibly related issues
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
📝 Coding Plan
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. Comment Tip CodeRabbit can generate a title for your PR based on the changes.Add |
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (1)
Hex/Features/Settings/GeneralSectionView.swift (1)
49-50: Avoid duplicating the default output path literal.Line 49 hardcodes the default path string, while the actual write path is computed separately in
Hex/Features/Transcription/TranscriptionFeature.swift(Line 541-543). Prefer deriving display text from the same source to prevent drift.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Hex/Features/Settings/GeneralSectionView.swift` around lines 49 - 50, The UI currently hardcodes the default transcription path in GeneralSectionView (Text(store.hexSettings.transcriptionOutputFilePath ?? "Default: ~/Library/.../transcriptions.txt")); change it to use the same computed default used by the transcription logic in Hex/Features/Transcription/TranscriptionFeature.swift by exposing that value (e.g., a static var or function like TranscriptionFeature.defaultTranscriptionOutputPath or TranscriptionFeature.computeDefaultOutputPath()) and replacing the literal with Text(store.hexSettings.transcriptionOutputFilePath ?? TranscriptionFeature.defaultTranscriptionOutputPath) so the displayed fallback always matches the actual write path.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@Hex/Features/Settings/GeneralSectionView.swift`:
- Around line 126-127: The panel.title and panel.prompt assignments in
GeneralSectionView.swift are hardcoded user-facing strings; replace them with
localized lookups (e.g., use NSLocalizedString or the app's localization helper)
so the title and prompt use localized keys instead of raw English; update the
assignments to call NSLocalizedString("Select Transcription Output File",
comment: "...") and NSLocalizedString("Choose", comment: "...") (or your
project's localization helper) where panel.title and panel.prompt are set.
In `@Hex/Features/Transcription/TranscriptionFeature.swift`:
- Around line 538-557: The current code uses a plain path string for
transcriptionOutputFilePath and opens destinationURL directly, which will fail
under sandbox; change the persisted value to store bookmark data (use
bookmarkData(options: .withSecurityScope) when the user picks the file in
GeneralSectionView), and when accessing the file in TranscriptionFeature (where
destinationURL is computed and used) resolve the bookmark via
URL(resolvingBookmarkData:options:relativeTo:isStale:) to obtain the URL, call
startAccessingSecurityScopedResource() before any FileManager/FileHandle
operations and ensure stopAccessingSecurityScopedResource() in a defer block,
and fall back to the app support default if resolving fails or bookmark is
stale; update places that read/write transcriptionOutputFilePath to handle
bookmark Data instead of a string path.
In `@Localizable.xcstrings`:
- Around line 70-72: The new UI keys (e.g., "Append to file" and the other newly
added keys in this file) are missing German ("de") translations; open
Localizable.xcstrings, locate the new key blocks such as the "Append to file"
entry and the other newly added entries, and add a "de" value for each following
the same format as existing entries (provide the German string for each key),
then save and run the localization/lint checks to ensure no keys are left
without a de translation.
---
Nitpick comments:
In `@Hex/Features/Settings/GeneralSectionView.swift`:
- Around line 49-50: The UI currently hardcodes the default transcription path
in GeneralSectionView (Text(store.hexSettings.transcriptionOutputFilePath ??
"Default: ~/Library/.../transcriptions.txt")); change it to use the same
computed default used by the transcription logic in
Hex/Features/Transcription/TranscriptionFeature.swift by exposing that value
(e.g., a static var or function like
TranscriptionFeature.defaultTranscriptionOutputPath or
TranscriptionFeature.computeDefaultOutputPath()) and replacing the literal with
Text(store.hexSettings.transcriptionOutputFilePath ??
TranscriptionFeature.defaultTranscriptionOutputPath) so the displayed fallback
always matches the actual write path.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 26929163-7e62-4fc8-9aab-a8fb79db8cb0
📒 Files selected for processing (6)
Hex/Features/Settings/GeneralSectionView.swiftHex/Features/Transcription/TranscriptionFeature.swiftHexCore/Sources/HexCore/Logging.swiftHexCore/Sources/HexCore/Settings/HexSettings.swiftHexCore/Tests/HexCoreTests/HexSettingsMigrationTests.swiftLocalizable.xcstrings
| panel.title = "Select Transcription Output File" | ||
| panel.prompt = "Choose" |
There was a problem hiding this comment.
Localize NSOpenPanel title/prompt text.
Line 126 and Line 127 are user-facing strings but currently hardcoded, so they bypass localization.
🌐 Suggested fix
- panel.title = "Select Transcription Output File"
- panel.prompt = "Choose"
+ panel.title = String(localized: "Select Transcription Output File")
+ panel.prompt = String(localized: "Choose")📝 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.
| panel.title = "Select Transcription Output File" | |
| panel.prompt = "Choose" | |
| panel.title = String(localized: "Select Transcription Output File") | |
| panel.prompt = String(localized: "Choose") |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@Hex/Features/Settings/GeneralSectionView.swift` around lines 126 - 127, The
panel.title and panel.prompt assignments in GeneralSectionView.swift are
hardcoded user-facing strings; replace them with localized lookups (e.g., use
NSLocalizedString or the app's localization helper) so the title and prompt use
localized keys instead of raw English; update the assignments to call
NSLocalizedString("Select Transcription Output File", comment: "...") and
NSLocalizedString("Choose", comment: "...") (or your project's localization
helper) where panel.title and panel.prompt are set.
| if let configuredPath, !configuredPath.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { | ||
| destinationURL = URL(fileURLWithPath: configuredPath) | ||
| } else { | ||
| let supportDir = try URL.hexApplicationSupport | ||
| destinationURL = supportDir.appending(component: "transcriptions.txt") | ||
| } | ||
|
|
||
| let parentDir = destinationURL.deletingLastPathComponent() | ||
| try fm.createDirectory(at: parentDir, withIntermediateDirectories: true) | ||
|
|
||
| let timestamp = Date().ISO8601Format() | ||
| let line = "[\(timestamp)] \(text)\n" | ||
| let data = Data(line.utf8) | ||
| if fm.fileExists(atPath: destinationURL.path) { | ||
| let handle = try FileHandle(forWritingTo: destinationURL) | ||
| defer { try? handle.close() } | ||
| try handle.seekToEnd() | ||
| try handle.write(contentsOf: data) | ||
| } else { | ||
| try data.write(to: destinationURL, options: .atomic) |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== Entitlements files =="
fd -g '*.entitlements'
echo
echo "== Sandbox/file-access entitlements =="
rg -n --glob '*.entitlements' 'com\.apple\.security\.(app-sandbox|files\.)'
echo
echo "== Security-scoped bookmark/resource usage =="
rg -n 'startAccessingSecurityScopedResource|stopAccessingSecurityScopedResource|bookmarkData\(|URL\(resolvingBookmarkData'Repository: kitlangton/Hex
Length of output: 342
🏁 Script executed:
cat -n Hex/Hex.entitlementsRepository: kitlangton/Hex
Length of output: 977
🏁 Script executed:
rg -n -B5 -A5 'configuredPath' Hex/Features/Transcription/TranscriptionFeature.swiftRepository: kitlangton/Hex
Length of output: 1009
🏁 Script executed:
rg -n 'transcriptionOutputFilePath' Hex/Features/Transcription/Repository: kitlangton/Hex
Length of output: 179
🏁 Script executed:
rg -n -B3 -A3 'transcriptionOutputFilePath' --type swiftRepository: kitlangton/Hex
Length of output: 7182
Implement security-scoped bookmarks for persisted file access in sandbox.
The stored transcriptionOutputFilePath (set via file picker in GeneralSectionView) is saved as a plain string. While the NSSavePanel grants temporary sandbox access during selection, direct access to that path will fail on subsequent app launches. Replace the string path with security-scoped bookmarks:
- Store
bookmarkData(options: .withSecurityScope)from the selected URL instead of the path string - Resolve the bookmark on access with
URL(resolvingBookmarkData:options:).url - Wrap file operations with
startAccessingSecurityScopedResource()/stopAccessingSecurityScopedResource()
This is required for sandboxed apps to maintain persistent file access across launches.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@Hex/Features/Transcription/TranscriptionFeature.swift` around lines 538 -
557, The current code uses a plain path string for transcriptionOutputFilePath
and opens destinationURL directly, which will fail under sandbox; change the
persisted value to store bookmark data (use bookmarkData(options:
.withSecurityScope) when the user picks the file in GeneralSectionView), and
when accessing the file in TranscriptionFeature (where destinationURL is
computed and used) resolve the bookmark via
URL(resolvingBookmarkData:options:relativeTo:isStale:) to obtain the URL, call
startAccessingSecurityScopedResource() before any FileManager/FileHandle
operations and ensure stopAccessingSecurityScopedResource() in a defer block,
and fall back to the app support default if resolving fails or bookmark is
stale; update places that read/write transcriptionOutputFilePath to handle
bookmark Data instead of a string path.
| "Append to file" : { | ||
|
|
||
| }, |
There was a problem hiding this comment.
New UI keys are missing existing locale translations.
The new user-facing strings added at Line 70-72, Line 149-153, Line 398-400, Line 719-721, and Line 753-755 have no de values, so German UI will partially fall back to English.
Also applies to: 149-153, 398-400, 719-721, 753-755
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@Localizable.xcstrings` around lines 70 - 72, The new UI keys (e.g., "Append
to file" and the other newly added keys in this file) are missing German ("de")
translations; open Localizable.xcstrings, locate the new key blocks such as the
"Append to file" entry and the other newly added entries, and add a "de" value
for each following the same format as existing entries (provide the German
string for each key), then save and run the localization/lint checks to ensure
no keys are left without a de translation.
This adds the option to transcribe directly to a file on disk, rather than into the current program or clipboard. This is primarily to allow capturing quick, accurate voice notes - for reminders, spontaneous thoughts, etc.
Summary by CodeRabbit