Skip to content

Accept Voice Memos drags and add batch file transcription - #716

Open
shreeraman96 wants to merge 17 commits into
mainfrom
feat/219-voicememo-batch
Open

Accept Voice Memos drags and add batch file transcription#716
shreeraman96 wants to merge 17 commits into
mainfrom
feat/219-voicememo-batch

Conversation

@shreeraman96

@shreeraman96 shreeraman96 commented Jul 26, 2026

Copy link
Copy Markdown
Collaborator

Description

Dragging a recording from Apple Voice Memos into File Transcription did nothing, and only one file could be transcribed at a time. This PR makes both work:

  • Promise-aware drop target (PromiseAwareDropView): SwiftUI's .onDrop can never see Voice Memos drags — they are file promises with no public.file-url (open Apple gap, FB13583826). An AppKit NSViewRepresentable overlay now accepts both concrete URLs and file promises. Resolution runs entirely off the main thread (verified live: any main-thread pasteboard read can deadlock intermittently and freezes the system-wide drag session) through three racing paths: modern NSFilePromiseReceiver (completion-callback-gated, so a stalled iCloud download is never delivered truncated) → raw pasteboard data (what Voice Memos actually serves, ~100 ms) → deprecated legacy promise API as a lazy last resort (an eager legacy request holds Voice Memos' promise machinery hostage for 35–80 s and blocks its next drag).
  • Batch transcription (BatchTranscriptionCoordinator): sequential per-item state machine — per-file status/progress/error, "no speech" detection, cancellation (pending items stop instantly; the in-flight native transcription runs out, shown as "Cancelling…"), and staging-dir cleanup only after each item finishes.
  • App-level session (FileTranscriptionSession): coordinator + service moved out of the view so in-flight work survives sidebar navigation.
  • Shared-model arbitration: dictation refuses to start while a batch is transcribing, and batch items wait for live dictation to end — both paths drive the same ASR model. Intent is claimed synchronously at every entry point (dictate/prompt via beginDictationRecording, command and rewrite at their callbacks, which start ASR directly) and released only once the stop path finishes — ASRService.stop() clears isRunning before the final transcription pass — with stopWithoutTranscription releasing it for every cancel path.
  • Multi-select file picker and multi-file Finder drops (the old handler silently kept only providers.first).
  • Two Task.checkCancellation() points in MeetingTranscriptionService's chunk loop so a cancelled batch stops mid-file on the chunked path.

Staging design: each promised file gets its own temp subdirectory (two memos titled "New Recording.m4a" never collide), duplicates from losing delivery paths are swept — except dirs whose promise is still pending, because deleting a promise's destination mid-write leaves the source app's drag session unresolved and Voice Memos then refuses all further drags until restarted (verified live; cleaned up on a deferred task instead).

Type of Change

  • 🐞 Bug fix
  • ✨ New feature
  • 💥 Breaking change
  • 🧹 Chore
  • 📝 Documentation update

Related Issue or Discussion

Closes #219

Testing

  • Tested on Intel Mac
  • Tested on Apple Silicon Mac
  • Tested on macOS version: 26.5.1
  • Ran linter locally: swiftlint --strict --config .swiftlint.yml Sources Tests — 0 violations, 151 files
  • Ran formatter locally: not run repo-wide — swiftformat reflows ~3k unrelated pre-existing lines on main; new files follow the existing style
  • Ran tests locally: 85/85 on the suites covering this PR (PromiseDropSupportTests, BatchTranscriptionCoordinatorTests, HotkeyShortcutTests); 35 new unit tests total. Note: a full-target run currently stops early inside DictationE2ETests.testDictationEndToEnd_whisperTiny_transcribesFixture (Whisper/ggml model load) while still reporting TEST SUCCEEDED — pre-existing, unrelated to this PR, but it means a whole-suite number is not trustworthy right now. Was 208/208 pass (35 new unit tests: coordinator state machine, cancellation, staging cleanup + session-root pruning, drop strategy selection, delivery dedup/sweep, format filtering, promise-name sanitization, alternate-vs-item delivery bounds)

Live end-to-end verification on this machine (Voice Memos → FluidVoice):

  • Single memo drop: promise resolved in ~0.5 s, 34-min recording transcribed in ~86 s, history entry written
  • Second memo dropped while the first was transcribing: queued and transcribed ("Transcribing 1 of 2" → both complete); Voice Memos stays responsive (the eager-legacy freeze this PR avoids was reproduced and eliminated)
  • Navigating Settings → back mid-batch: batch card intact, work uninterrupted
  • Non-audio promises (e.g. Photos images) are refused at the drag affordance

Screenshots / Video

Batch card after a drop (per-item status, header summary, Done):

Batch transcription card

Notes

  • Second review round (8e3f499): delivery now relocates every file into its own staging dir, so the coordinator's one-item-one-dir ownership assumption holds for all three paths (the raw-data and legacy fallbacks share a dir by construction, and a receiver carrying several fileNames fills one dir too). Fell out of that: the legacy dir is now held back from the delivery sweep while its writer is in flight, and a receiver's in-flight token is released once after its last file callback instead of once per file — the over-decrement left hasInFlightWork false and disabled the guard against delivering a half-written sweep.
  • Review round (bf2e542, ebe47e7): promise-supplied file names are reduced to a single path component (separators/.. let the raw-data fallback write outside staging); selectDelivery is bounded by the pasteboard item count, since the fallback renames same-named items ("New Recording 2.m4a") and that name matched no modern name, so it was delivered as a phantom extra file; and the dictation-intent flag is now claimed and released on every start, stop, and cancel path.
  • The modern NSFilePromiseReceiver path is unreliable for Voice Memos (a Catalyst app): called on the main thread it fails instantly with Cocoa 3072; on a background thread it completes only sometimes. The raw pasteboard data path is what reliably delivers Voice Memos audio; the receiver + legacy paths cover other promise sources (Photos, Mail).
  • Reading the drag pasteboard from background threads after performDragOperation returns is outside AppKit's documented lifetime contract — it is the only arrangement found that neither deadlocks nor loses drops; commented in code with the poll-timeout safety net.
  • The issue's option 3 (an in-app Voice Memos library browser) was deliberately not pursued: the group container is TCC/Full-Disk-Access protected on modern macOS and CloudRecordings.db is an undocumented schema.
  • Watch/iCloud memos not yet downloaded locally are untested (none available); a promise that never resolves surfaces as a per-item error after the poll deadline rather than a hang.

@github-actions github-actions Bot added the needs screenshots Pull request needs screenshot or video evidence. label Jul 26, 2026
@github-actions

Copy link
Copy Markdown

The PR Policy check is blocking this PR because required template information is missing.

Please update the PR description with:

  • Screenshots / Video

Visual files detected:

  • Sources/Fluid/ContentView.swift
  • Sources/Fluid/UI/MeetingTranscriptionView.swift
  • Sources/Fluid/UI/PromiseAwareDropView.swift
  • Sources/Fluid/UI/PromiseDropSupport.swift

Screenshots or video are required for UI, UX, settings, onboarding, overlay, menu bar, or visual behavior changes. If this PR has no visual changes, check the no-visual-change box in the template.

If this remains incomplete for 48 hours after opening, the PR may be closed.

@github-actions github-actions Bot removed the needs screenshots Pull request needs screenshot or video evidence. label Jul 26, 2026
@shreeraman96
shreeraman96 force-pushed the feat/219-voicememo-batch branch from 9f6032a to d6c509d Compare August 1, 2026 00:56
@shreeraman96
shreeraman96 marked this pull request as ready for review August 1, 2026 01:00
@greptile-apps

greptile-apps Bot commented Aug 1, 2026

Copy link
Copy Markdown

Greptile Summary

This PR adds promise-aware Voice Memos drops and persistent sequential batch transcription.

  • Resolves concrete URLs and file promises into isolated staging directories with delivery deduplication and cleanup.
  • Adds multi-file picking, per-item batch status, cancellation, no-speech handling, and persistent app-level session ownership.
  • Adds cancellation checks to native and chunked file transcription.
  • Adds shared-model arbitration to the primary, prompt, command-hotkey, and rewrite-hotkey recording entry points.

Confidence Score: 4/5

The PR is not yet safe to merge because command and rewrite sidebar recording can still overlap an active file-transcription batch on the shared ASR provider.

The previously reported sidebar entry points still call the shared ASRService directly without checking batch state or claiming dictation intent, while the batch only checks live-ASR state before beginning each file and cannot stop a later sidebar start.

Files Needing Attention: Sources/Fluid/UI/CommandModeView.swift, Sources/Fluid/UI/RewriteModeView.swift, Sources/Fluid/Services/FileTranscriptionSession.swift

Reviews (11): Last reviewed commit: "fix: shorten the delivery wait once a re..." | Re-trigger Greptile

Comment thread Sources/Fluid/UI/PromiseAwareDropView.swift
Comment thread Sources/Fluid/ContentView.swift Outdated

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d6c509d639

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread Sources/Fluid/ContentView.swift Outdated
private func stopAndProcessTranscription(route: DictationOutputRoute = .normal) async {
// Recording session is ending: release dictation intent so the batch arbiter
// (and any dictation restart) no longer sees dictation as active.
FileTranscriptionSession.shared.endDictationIntent()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Keep dictation intent until ASR stop finishes

For recordings that set dictationIntent, clearing it at the start of the stop path opens a concurrency window: ASRService.stop() sets asr.isRunning to false before it runs the final transcription pass, so a file drop during the visible “Transcribing” phase can enter MeetingTranscriptionService.transcribeFile while the dictation final pass is still using the shared provider. Move this release until after await asr.stop(...) returns (or otherwise keep the batch arbiter locked through the final ASR transcription).

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in ebe47e7. stopAndProcessTranscription now releases via defer at function scope instead of clearing at entry, so the flag survives the final transcription pass (and the early returns for empty text/cancellation/failure).

Comment thread Sources/Fluid/ContentView.swift
Comment thread Sources/Fluid/ContentView.swift Outdated
Comment thread Sources/Fluid/UI/PromiseDropSupport.swift
@github-actions

github-actions Bot commented Aug 1, 2026

Copy link
Copy Markdown

FluidVoice PR build ready

Download FluidVoice-PR-716-c44affd667f8

The artifact contains the ad-hoc-signed app ZIP, Xcode archive, build manifest, and installation instructions. It expires 5 days after the build.

Install the app

  1. Extract the downloaded artifact, then extract FluidVoice-PR-716.app.zip.

  2. Move FluidVoice Accept Voice Memos drags and add batch file transcription #716.app into the /Applications folder.

  3. Open Terminal and remove the download quarantine marker:

    xattr -dr com.apple.quarantine "/Applications/FluidVoice #716.app"
    
  4. In Applications, Control-click FluidVoice Accept Voice Memos drags and add batch file transcription #716.app and choose Open.

  5. If macOS still blocks it, open System Settings → Privacy & Security, click Open Anyway, and confirm.

This build has its own app identity, so its permissions are separate from the release version of FluidVoice.

View workflow run

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: bf2e5424a7

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread Sources/Fluid/UI/PromiseAwareDropView.swift Outdated
@greptile-apps

greptile-apps Bot commented Aug 2, 2026

Copy link
Copy Markdown

Want your agent to iterate on Greptile's feedback? Start a greploop in Codex and it will work through the open comments and keep going until this PR reviews clean.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 8e3f4992e2

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread Sources/Fluid/Services/MeetingTranscriptionService.swift

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 61eb88887e

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread Sources/Fluid/UI/PromiseAwareDropView.swift Outdated
// Relocation empties the legacy dir, so a sweep would see it as undelivered
// and delete a destination the OS may still be writing into.
pendingFallbackDirs: state.hasInFlightWork ? [legacyDir].compactMap(\.self) : [],
totalExpected: max(receiverDirs.count, state.payloadCountValue(), 1),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Count promised filenames rather than receiver dirs

When a single NSFilePromiseReceiver advertises multiple fileNames and the modern path fails while the legacy/data fallback writes those files, totalExpected is capped at receiverDirs.count (1). That value is passed to selectDelivery(... expectedItemCount:), so only the first fallback file is delivered and the rest of the promised files are silently dropped; track the promised filename count instead of the number of staging dirs.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Good catch — fixed in 33bd0f6. This was a bug I introduced with the expectedItemCount bound two commits earlier, and it is the exact mirror of the phantom-extra-file case that bound was added to fix.

ResolutionState now accumulates each receiver's fileNames.count as it registers, and totalExpected uses that instead of receiverDirs.count:

totalExpected: max(state.promisedFileCountValue(), state.payloadCountValue(), 1)

Receiver dirs are still counted separately for allReceiversResolved and the pending-dir logic, which are genuinely per-dir concerns.

Regression test asserts a single receiver promising three names delivers all three from the fallback, and pins the failure the receiver-count bound produced (capped at 1, two files silently dropped).

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 8fcc29c2f1

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread Sources/Fluid/UI/PromiseAwareDropView.swift Outdated
Comment thread Sources/Fluid/UI/PromiseAwareDropView.swift Outdated

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5618d0cfba

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread Sources/Fluid/ContentView.swift

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a907cd5683

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread Sources/Fluid/UI/PromiseAwareDropView.swift Outdated
Comment thread Sources/Fluid/ContentView.swift

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 370aa57949

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread Sources/Fluid/Services/FileTranscriptionSession.swift
Comment thread Sources/Fluid/UI/PromiseAwareDropView.swift
- AppKit promise-aware drop target (SwiftUI onDrop cannot receive file
  promises): concrete URLs + file promises, resolved off-main via modern
  receiver, legacy promise API, and raw pasteboard data fallbacks
- BatchTranscriptionCoordinator: sequential per-item state machine with
  cancellation, staging cleanup, and batch/dictation arbitration
- App-level FileTranscriptionSession so in-flight work survives navigation
- Multi-select file picker; batch progress UI with per-item status
NSPasteboard is not thread-safe: our worker's per-item reads raced the
promise receivers' internal pasteboard access and crashed in the type
cache (NSInternalInconsistencyException -> ggml terminate handler ->
abort). All app-side reads now happen first on one thread, receivers
start only afterwards on a serial queue, and every pasteboard call is
wrapped in an ObjC exception catcher so a future race logs instead of
killing the app.
- Single MainActor arbiter for the shared ASR model: dictation intent flag
  set synchronously before any await closes the TOCTOU gap; batch items wait
  on dictation intent, live capture, and in-flight single-file transcription
- Batch guard moved to every hotkey entry point (dictate/prompt/command/
  rewrite), with an audible cue when dictation is blocked mid-batch
- Coordinator: pending-driven processing (completed items never re-run when
  enqueueing after an undismissed batch) and self-draining restart (items
  enqueued during the cancel window are picked up, not stranded)
- CancellationError rethrown unwrapped from transcribeFile (no false failure
  analytics)
- Drop resolver: empty-delivery path no longer deletes pending receiver dirs;
  receivers hold in-flight tokens so slow (iCloud) promises aren't abandoned
  at the 30s soft timeout; failed receivers count as resolved so partial
  failures deliver promptly; partial-failure accounting includes raw-data
  item counts
- Single-file drops route into the batch when anything is in flight or a
  finished batch is still displayed; drop errors auto-dismiss
- 7 new regression tests (182 total)
Review findings on the single-model arbiter: a batch could start concurrent
inference, or wait forever on a dictation that had already ended.

- Claim intent at the command/rewrite callbacks and in beginDictationRecording,
  which start ASR directly and previously left both arbitration flags false
  until isRunning flipped.
- Release it after stopAndProcessTranscription completes rather than at entry;
  ASRService.stop() clears isRunning before the final transcription pass, so an
  early release let a drop run during the visible "Transcribing" phase.
- Release it in ASRService.stopWithoutTranscription, which every cancel path
  (Escape, overlay cancel, settings window teardown) funnels through, above the
  isRunning guard so a cancel while starting counts too.
- Claim it after the already-running guard in startRecording, so an ignored
  start cannot latch a flag it never releases.
- Reduce a provider-supplied promise name to one path component; separators or
  ".." let the raw-data fallback write outside the staging directory.
- Bound selectDelivery by the pasteboard item count: the raw-data path renames
  same-named items ("New Recording 2.m4a"), which matched no modern name and was
  delivered as a phantom extra file for a two-memo drag.
- Bind items.last instead of force-unwrapping it (swiftlint --strict).
The coordinator deletes an item's staging dir as soon as that item finishes,
so it assumes one item owns one dir. Delivery broke that assumption: the
raw-data fallback writes every item into one shared dir, the legacy API writes
every file into one destination dir, and a receiver carrying several file names
fills one dir — and stagingDir was derived from the file's parent path, so N
files claimed the same dir. The first item to finish destroyed files the queued
items still needed; cancel() hit the same path while an item was transcribing.

- Relocate each delivered file into a fresh item dir at delivery, reporting that
  dir as its owner. A failed move reports no owner rather than a shared dir.
- Treat the legacy dir as pending while its writer is in flight: relocation
  empties it, so the sweep would otherwise delete a destination the OS is still
  writing into.
- Release a receiver's in-flight token once, after its last file callback.
  receivePromisedFiles fires once per name, so the token was over-decremented,
  leaving hasInFlightWork false and disabling both the soft-timeout hold and the
  guard against delivering a half-written multi-item sweep.
- Document staging-dir ownership on Request/Item.
Cancellation was only checked before the transcription await, so a provider
that returns normally after a cancel still reached the completion writes: the
result landed in FileTranscriptionHistoryStore and fired success analytics
while the batch card showed the item as cancelled.

- Recheck after the native path's transcribeFile, which has no interruption
  point at all — the UI's "Cancelling…" state exists precisely because that
  call runs to completion.
- Recheck after the chunked loop, which only checks between chunks, so a cancel
  during the final chunk (up to 20 minutes of audio) reached the writes too.

Both throw into the coordinator's existing CancellationError path.
Branch-added comments were 19% of added lines against a repo norm nearer 8%.
Collapses prose blocks to single lines and drops change narration, keeping the
live-verified AppKit facts that are not inferable from the code: pasteboard
main-thread deadlocks, read ordering vs the NSPasteboard type-cache crash, the
legacy promise API's 35-80s block, the pending-dir sweep hazard, Cocoa 3072 on
Catalyst sources, and the timeout rationale. 404 comment lines to 265.

Also moves a dismissBatch() doc comment that had drifted onto showDropError.
totalExpected counted receiver dirs, but one NSFilePromiseReceiver can promise
several files. Bounding selectDelivery by that count meant a single receiver
advertising three names whose modern path failed delivered only the first
fallback file and silently dropped the rest — the mirror of the phantom-extra
-file bug the bound was added to fix.

ResolutionState now accumulates each receiver's fileNames count; receiver dirs
are still counted separately for the resolved/unresolved checks, which are
genuinely per-dir.
Second pass over the drop/batch files: deletes comments that restate the code,
compresses prose to single lines, and drops doc comments that only repeat a
well-named symbol. Every live-verified fact is kept — pasteboard main-thread
deadlocks, read ordering vs the NSPasteboard type-cache crash, the legacy
promise API's 35-80s block, the pending-dir sweep hazard, staging-dir ownership,
and the dictationIntent TOCTOU window.

265 comment lines to 78 across the six files.
A Voice Memos promise can carry metadata the OS refuses to decode; NSFilePromiseReceiver
raises rather than reporting an error, and an uncaught NSException aborts the process.
Guard both decode points and degrade to the raw-data fallback.

Delivery also waited on a receiver Voice Memos only cancels when the next drag starts,
so a lone drop sat until the 120s hard timeout. An unresolved receiver that has written
nothing past the grace no longer holds delivery open, and receivers get their own
in-flight counter so an uncancelled one cannot gate the fallback writers.
A receiver promising several files can fail on one and still have written the rest,
but any failure marked the whole dir failed, and failed dirs are never delivered and
are swept at once. Only a dir that produced nothing counts as failed now.

The legacy path's returned names were logged and discarded, so a legacy-only multi-file
drop left totalExpected at 1 and selectDelivery dropped every file after the first.
A command or edit hotkey press during a file batch flipped activeRecordingMode and
re-skinned the overlay before the batch guard returned, leaving the app in that mode
with nothing recording and isCommandRecordingProvider reporting it to the hotkey
manager. The rewrite path also captured the selection and started the rewrite service.

The batch guard now runs first, so its blocked cue still plays while a recording is
live. The isRunningOrStarting guard stays below the mutations: those mutations are how
a cross-mode hotkey press switches an in-flight recording without restarting ASR.
Comment-only. Trims the branch's own comments toward the density target, keeping the
why and dropping restatement. Pre-existing comments in shared files are untouched.
Giving receivers their own in-flight counter left hasInFlightWork blind to them, so the
30s soft timeout could fire while a promise receiver was mid-transfer; the timeout path
delivers only completed dirs, so that file was reported unreadable. Providers have been
seen taking 80s.

The loop now holds past the soft timeout while an unresolved receiver has written files,
reusing the discriminator the stall fix already relies on, so an idle receiver still
cannot gate delivery.
@shreeraman96
shreeraman96 force-pushed the feat/219-voicememo-batch branch from 370aa57 to 5cb1d04 Compare August 2, 2026 21:47

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5cb1d04e74

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread Sources/Fluid/UI/PromiseAwareDropView.swift
A drop with several receivers where one completes and another stalls without
writing satisfies neither early exit: modernComplete needs every receiver
resolved, and the fallback path needs no modern wins at all. It waited the full
30s soft timeout to deliver files that were stable seconds earlier.

The soft timeout is now 8s once any receiver has completed and 30s while none
has, reusing the existing break rather than adding a second notion of settled:
that break already holds delivery open for a receiver still writing, and cannot
sweep a legacy dir early.
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.

[✨ FEATURE] Please provide ability to transcribe multiple voice memos all at once (voice memos.app)

1 participant