Skip to content

Add option to directly transcribe to file#188

Open
atkinchris wants to merge 3 commits intokitlangton:mainfrom
atkinchris:main
Open

Add option to directly transcribe to file#188
atkinchris wants to merge 3 commits intokitlangton:mainfrom
atkinchris:main

Conversation

@atkinchris
Copy link
Copy Markdown

@atkinchris atkinchris commented Mar 13, 2026

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

  • New Features
    • Added a new Transcription Output setting allowing users to choose between pasting transcriptions into the focused app or writing to a text file.
    • Added file selection UI with custom file path and default location options.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 13, 2026

📝 Walkthrough

Walkthrough

This 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 TranscriptionOutputMode enum with associated settings properties, updated transcription handling logic, and corresponding localization strings.

Changes

Cohort / File(s) Summary
Settings & Configuration
Hex/Features/Settings/GeneralSectionView.swift, HexCore/Sources/HexCore/Settings/HexSettings.swift
Added new Transcription Output section with UI picker for output mode selection. Conditionally renders file path controls when appendToFile mode is active. New TranscriptionOutputMode enum with pasteIntoFocusedApp and appendToFile cases, plus transcriptionOutputFilePath property. Updated HexSettingKey and Settings schema for persistence.
Transcription Logic
Hex/Features/Transcription/TranscriptionFeature.swift
Replaced direct paste with mode-based output handling. Added appendTranscriptToOutputFile() helper that resolves destination path, ensures parent directory exists, and appends timestamped transcript. Updated finalizeRecordingAndStoreTranscript to route output based on selected mode and log destinations.
Logging
HexCore/Sources/HexCore/Logging.swift
Added new "Output" logging category to HexLog.Category enum and exposed public HexLog.output logger instance.
Testing & Localization
HexCore/Tests/HexCoreTests/HexSettingsMigrationTests.swift, Localizable.xcstrings
Extended migration test to validate transcriptionOutputMode and transcriptionOutputFilePath defaults. Added 8 new localization keys for UI strings including "Transcription Output", "Append to file", "Insert in focused app", and file selection prompts.

Sequence Diagram

sequenceDiagram
    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
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related issues

  • Transcribe directly to a file #189: Implements the requested "transcribe directly to a file" feature with new TranscriptionOutputMode enum, file append logic, and corresponding UI controls for mode selection and file path configuration.

Poem

🐰 A file path now flows where paste once did go,
With a picker to choose: app or text file, you know?
Timestamped lines gather in folders so neat,
The transcription output feature is now complete! ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Add option to directly transcribe to file' accurately captures the main feature introduced in this PR, which adds the ability to write transcriptions directly to a file as an alternative to pasting into the focused app.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
📝 Coding Plan
  • Generate coding plan for human review comments

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.

Tip

CodeRabbit can generate a title for your PR based on the changes.

Add @coderabbitai placeholder anywhere in the title of your PR and CodeRabbit will replace it with a title based on the changes in the PR. You can change the placeholder by changing the reviews.auto_title_placeholder setting.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

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

📥 Commits

Reviewing files that changed from the base of the PR and between f29125d and cbd10b7.

📒 Files selected for processing (6)
  • Hex/Features/Settings/GeneralSectionView.swift
  • Hex/Features/Transcription/TranscriptionFeature.swift
  • HexCore/Sources/HexCore/Logging.swift
  • HexCore/Sources/HexCore/Settings/HexSettings.swift
  • HexCore/Tests/HexCoreTests/HexSettingsMigrationTests.swift
  • Localizable.xcstrings

Comment on lines +126 to +127
panel.title = "Select Transcription Output File"
panel.prompt = "Choose"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

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.

Suggested change
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.

Comment on lines +538 to +557
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)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 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.entitlements

Repository: kitlangton/Hex

Length of output: 977


🏁 Script executed:

rg -n -B5 -A5 'configuredPath' Hex/Features/Transcription/TranscriptionFeature.swift

Repository: 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 swift

Repository: 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:

  1. Store bookmarkData(options: .withSecurityScope) from the selected URL instead of the path string
  2. Resolve the bookmark on access with URL(resolvingBookmarkData:options:).url
  3. 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.

Comment thread Localizable.xcstrings
Comment on lines +70 to 72
"Append to file" : {

},
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

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.

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