Improve overlay feedback, Edit Mode capture, and Codex command routing - #408
Improve overlay feedback, Edit Mode capture, and Codex command routing#408tarushvkodes wants to merge 35 commits into
Conversation
|
Update: improved Edit Mode replacement for Google Docs and similar browser editors.\n\n- Captures the original target PID when selected text is captured, so accepting a rewrite can return to the document instead of aiming at the FluidVoice overlay.\n- Routes Edit Mode acceptance through the reliable paste path, which web rich-text editors handle better than synthetic direct typing.\n- Gives clipboard-based selection capture a longer window for Google Docs/browser editors that update the pasteboard more slowly after Cmd+C.\n\nValidated locally:\n- swiftc parse over Sources/Fluid Swift files\n- git diff --check\n- plutil lint for project/Info/entitlements\n- xcodebuild clean build for macOS arm64\n- installed and ad-hoc signed local /Applications/FluidVoice.app |
003a852 to
314f9c7
Compare
|
Update: rebased this branch onto upstream main at v1.6.1 (49e91c0) and reinstalled a fresh local build.\n\nNotes:\n- Dropped the old Package.resolved-only MCP SDK bump during rebase because upstream removed that dependency.\n- Preserved the visualizer threshold, Codex command mode, notch output, and Google Docs/Edit Mode replacement changes.\n- PR head is now 314f9c7 and reports mergeable.\n\nValidated locally:\n- swiftc parse over Sources/Fluid Swift files\n- git diff --check\n- plutil lint for project/Info/entitlements\n- xcodebuild clean build for macOS arm64\n- installed and ad-hoc signed /Applications/FluidVoice.app |
There was a problem hiding this comment.
Pull request overview
This PR improves FluidVoice’s user-facing feedback and reliability across three flows: overlay waveform responsiveness, Edit Mode selection capture + rewrite insertion, and optional Command Mode routing to Codex (notch or app handoff).
Changes:
- Makes the overlay visualizer respond to quieter mic input by lowering the default noise threshold and adds an integration test for the new default.
- Improves Edit Mode reliability by adding clipboard-based selected-text capture fallback and forcing a “reliable paste” insertion path for accepted rewrites, with PID tracking.
- Adds an opt-in Command Mode “Route to Codex” setting with notch/app handoff styles, updates UI controls, and stabilizes expanded notch output behavior.
Reviewed changes
Copilot reviewed 12 out of 12 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| Tests/FluidDictationIntegrationTests/DictationE2ETests.swift | Adds an integration test asserting the new, more sensitive default visualizer noise threshold. |
| Sources/Fluid/Views/CommandModeView.swift | Adds Codex routing UI controls and adjusts readiness/notch-output behavior based on routing settings. |
| Sources/Fluid/UI/SettingsView.swift | Updates the visualizer threshold reset behavior to use the new default constant. |
| Sources/Fluid/Services/TypingService.swift | Introduces a “force reliable paste” path (typeTextReliably) and plumbs it through the insertion pipeline. |
| Sources/Fluid/Services/TextSelectionService.swift | Adds clipboard-preserving Cmd+C fallback for selected-text capture when AX APIs fail. |
| Sources/Fluid/Services/RewriteModeService.swift | Tracks target PID for Edit Mode context and routes accepted rewrites via the reliable paste path. |
| Sources/Fluid/Services/NotchOverlayManager.swift | Stabilizes expanded command-notch presentation state transitions and cleanup behavior. |
| Sources/Fluid/Services/CommandModeService.swift | Adds Codex routing branch and notch sync behavior for Codex “notch” handoff. |
| Sources/Fluid/Services/CodexHandoffService.swift | New service that runs Codex CLI for notch output or activates/pastes into the Codex app. |
| Sources/Fluid/Persistence/SettingsStore+CommandMode.swift | Bypasses command readiness issues when Codex routing is enabled. |
| Sources/Fluid/Persistence/SettingsStore.swift | Adds default visualizer threshold constant and persists Codex routing + handoff-style settings. |
| Sources/Fluid/ContentView.swift | Routes rewrite acceptance through RewriteModeService.acceptRewrite(...) with PID targeting. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| } | ||
|
|
||
| private func pasteAndSubmit(_ text: String) async -> Bool { | ||
| Self.pasteboardSessionSemaphore.wait() |
| pasteboard.clearContents() | ||
| guard pasteboard.setString(text, forType: .string) else { | ||
| self.restorePasteboardSnapshot(snapshot, to: pasteboard) | ||
| Self.pasteboardSessionSemaphore.signal() | ||
| return false | ||
| } | ||
|
|
||
| guard self.sendCommandKey("v") else { | ||
| self.restorePasteboardSnapshot(snapshot, to: pasteboard) | ||
| Self.pasteboardSessionSemaphore.signal() | ||
| return false | ||
| } | ||
|
|
||
| try? await Task.sleep(nanoseconds: 100_000_000) | ||
| guard self.sendReturnKey() else { | ||
| self.restorePasteboardSnapshot(snapshot, to: pasteboard) | ||
| Self.pasteboardSessionSemaphore.signal() | ||
| return false | ||
| } | ||
|
|
||
| try? await Task.sleep(nanoseconds: 1_000_000_000) | ||
| self.restorePasteboardSnapshot(snapshot, to: pasteboard) | ||
| Self.pasteboardSessionSemaphore.signal() |
| let deadline = Date().addingTimeInterval(1.0) | ||
| var copiedText: String? | ||
| repeat { | ||
| if pasteboard.changeCount != clearedChangeCount, | ||
| let text = pasteboard.string(forType: .string), | ||
| !text.isEmpty | ||
| { | ||
| copiedText = text | ||
| break | ||
| } | ||
| usleep(15_000) | ||
| } while Date() < deadline | ||
|
|
||
| self.restorePasteboardSnapshot(snapshot, to: pasteboard) | ||
|
|
||
| guard let copiedText else { | ||
| self.diag("Clipboard fallback failed: clipboard did not receive selected text") | ||
| return nil | ||
| } |
| func captureSelectedText() -> Bool { | ||
| let targetPID = TypingService.captureSystemFocusedPID() | ||
| ?? NSWorkspace.shared.frontmostApplication?.processIdentifier | ||
| if let text = textSelectionService.getSelectedText(), !text.isEmpty { |
| .outputDelivered, | ||
| properties: [ | ||
| "mode": AnalyticsMode.rewrite.rawValue, | ||
| "method": AnalyticsOutputMethod.typed.rawValue, |
| self.asr.typeTextToActiveField( | ||
| self.rewriteModeService.rewrittenText, | ||
| preferredTargetPID: typingTarget.pid | ||
| self.rewriteModeService.acceptRewrite( |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 314f9c7e81
ℹ️ About Codex in GitHub
Your team has set up Codex to 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 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if self.shouldSyncCommandNotchState { | ||
| NotchContentState.shared.addCommandMessage(role: result.success ? .assistant : .status, content: result.message) | ||
| NotchContentState.shared.setCommandProcessing(false) |
There was a problem hiding this comment.
Capture notch sync state for Codex cleanup
When a Codex notch handoff starts, the earlier block can set isCommandProcessing to true, but this completion block recomputes shouldSyncCommandNotchState after the async Codex run. If the user toggles Codex routing off or changes the handoff style while Codex is still running, cleanup is skipped and the expanded notch can remain stuck in a processing/disabled state even though isProcessing is false; capture the initial sync decision or always clear command processing on completion.
Useful? React with 👍 / 👎.
314f9c7 to
8258e3c
Compare
|
Update: rebased this branch onto upstream main at v1.6.2 (941eedb) and reinstalled a fresh local build. Notes:
Validated locally:
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 8258e3c366
ℹ️ About Codex in GitHub
Your team has set up Codex to 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 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| process.terminationHandler = { _ in | ||
| resume(true) | ||
| } |
There was a problem hiding this comment.
Check for already-exited Codex processes
When the Codex CLI exits before this handler is installed (for example because the flags are invalid, auth/config fails, or the executable returns immediately), setting terminationHandler after the fact does not resume the continuation. In that scenario the UI waits for the full 120-second timeout and reports a timeout instead of the real Codex failure; check process.isRunning/terminationStatus before waiting or install the handler before starting the process.
Useful? React with 👍 / 👎.
|
|
||
| keyDown.flags = .maskCommand | ||
| keyUp.flags = .maskCommand | ||
| keyDown.post(tap: .cghidEventTap) |
There was a problem hiding this comment.
Verify Codex focus before pasting
In app handoff, these events are posted globally after only a fixed 250 ms activation delay. On a cold launch or slow activation where Codex is not yet frontmost/focused, Cmd+V and Return go to whichever app still owns focus, leaking/submitting the command text there while this path still reports success; wait until the Codex app/input is actually focused or post the events to the Codex PID.
Useful? React with 👍 / 👎.
|
Update: added one more small Google Docs/Edit Mode guard. When Right Option is already active as a FluidVoice modifier-only hotkey, macOS printable Option chords like Option+R can otherwise leak through to web editors as characters such as ® if the chord misses the exact shortcut route. The new guard only applies while the physical Right Option key is the active FluidVoice modifier-only shortcut, so normal left-Option text input remains untouched. Validated locally with swiftc parse, git diff --check, xcodebuild macOS arm64 build, and a freshly installed/ad-hoc signed /Applications/FluidVoice.app. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: e44aea03ed
ℹ️ About Codex in GitHub
Your team has set up Codex to 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 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| guard let copyDown = CGEvent(keyboardEventSource: nil, virtualKey: 8, keyDown: true), | ||
| let copyUp = CGEvent(keyboardEventSource: nil, virtualKey: 8, keyDown: false) |
There was a problem hiding this comment.
Resolve Copy shortcut using the active keyboard layout
When AX selected-text lookup fails, this fallback now relies on a hard-coded US keyboard key code for Cmd+C. On non-QWERTY layouts that key code can generate a different shortcut, so the clipboard fallback never captures the selection and Edit Mode incorrectly starts without context; resolve the key code from the current layout before posting.
Useful? React with 👍 / 👎.
| try? await Task.sleep(nanoseconds: 1_000_000_000) | ||
| self.restorePasteboardSnapshot(snapshot, to: pasteboard) |
There was a problem hiding this comment.
Preserve clipboard changes during Codex app handoff
When using App handoff, this sleeps for a full second after submitting and then unconditionally restores the pasteboard snapshot. If the user or Codex copies anything during that second, the new clipboard contents are overwritten by FluidVoice's pre-handoff clipboard; check the pasteboard change count/current string before restoring as the typing paste path does.
Useful? React with 👍 / 👎.
e44aea0 to
59dc5cd
Compare
|
Follow-up correction: tightened the Right Option / Edit Mode fix after live Google Docs testing. The final version preserves automatic dictation behavior for Right Option alone. It only prioritizes the configured Edit Mode chord when physical Right Option is already down and the chord matches the Edit Mode shortcut, consuming the paired key-down/key-up before the web editor can receive printable Option text like ®. This keeps the user's push-to-talk/tap-to-continue dictation flow unchanged while preventing Option+R from replacing selected Google Docs text with a literal character. Validated again locally with swiftc parse, git diff --check, xcodebuild macOS arm64 build, and installed/ad-hoc signed /Applications/FluidVoice.app. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 59dc5cd199
ℹ️ About Codex in GitHub
Your team has set up Codex to 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 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if self.shouldPrioritizeRightOptionRewriteShortcut(keyCode: keyCode, modifiers: eventModifiers) { | ||
| self.consumeRightOptionTextInputKey(keyCode) | ||
| DebugLogger.shared.info( | ||
| "Right Option edit shortcut consumed before text input", | ||
| source: "GlobalHotkeyManager" | ||
| ) | ||
| self.handleRewriteModeKeyDown() | ||
| return nil |
There was a problem hiding this comment.
Mark consumed Right Option chords as interruptions
When the primary shortcut is the Right Option modifier-only key and hotkey mode is Toggle, this early return fires before markOtherInputDuringModifierOnly(), so the modifier press remains clean. Pressing Right Option plus the rewrite shortcut triggers Edit mode here, but releasing Right Option then also runs the modifier-only toggle path and starts/stops dictation; the generic suppression branch below has the same clean-press problem for Option text characters. Mark the modifier-only press interrupted before consuming the key.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: b84bd4ec8e
ℹ️ About Codex in GitHub
Your team has set up Codex to 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 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
|
||
| private static let codexBundleID = "com.openai.codex" | ||
| private static let pasteboardSessionSemaphore = DispatchSemaphore(value: 1) | ||
| private static let bundledCodexCLIPath = "/Applications/Codex.app/Contents/Resources/codex" |
There was a problem hiding this comment.
Resolve Codex CLI from the installed app
When Codex.app is installed outside /Applications (for example in ~/Applications or after the user moves/renames the bundle), App handoff can still locate it by bundle ID, but Notch handoff will fail this fixed path check and report “Codex CLI was not found.” Resolve the app URL via Launch Services/NSWorkspace and append Contents/Resources/codex instead of pinning /Applications.
Useful? React with 👍 / 👎.
|
|
||
| private static func keyCode(for character: Character) -> CGKeyCode? { | ||
| switch character.lowercased() { | ||
| case "v": return 9 |
There was a problem hiding this comment.
Resolve paste shortcut from active layout
When App handoff runs on non-QWERTY keyboard layouts, this maps paste to the fixed ANSI V key even though the rest of the typing pipeline resolves the paste key from the current layout. On those layouts Cmd+V can be delivered as a different key equivalent, so pasteAndSubmit can return success after pressing Return while Codex receives no pasted prompt; use the active-layout lookup or the existing reliable paste helper before posting the shortcut.
Useful? React with 👍 / 👎.
This reverts commit 59dc5cd.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 35ce89241a
ℹ️ About Codex in GitHub
Your team has set up Codex to 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 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| } | ||
|
|
||
| if let data = prompt.data(using: .utf8) { | ||
| inputPipe.fileHandleForWriting.write(data) |
There was a problem hiding this comment.
Guard Codex stdin writes after early exits
When the bundled Codex CLI exits immediately during Notch handoff (for example from bad flags or missing auth/config), the pipe's write end can already be closed here; FileHandle.write(_:) on a closed pipe terminates the app instead of returning a HandoffResult. Check process.isRunning/use a throwing write path or feed stdin from a file so early Codex failures are reported rather than crashing FluidVoice.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 6470ab371b
ℹ️ About Codex in GitHub
Your team has set up Codex to 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 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
|
||
| self.currentStatus = "Creating timestamped subtitles..." | ||
| self.progress = 0.5 | ||
| let segments = try await whisperProvider.transcribeTimed(samples) |
There was a problem hiding this comment.
Keep video subtitle transcription chunked
When a Whisper-backed video is longer than the model's safe window, this new video/SRT branch resamples the entire file and sends all samples to transcribeTimed in one request, bypassing the 20-minute chunk loop below that was added to avoid model/window and memory limits. Long MP4/MOV meetings can therefore fail, truncate, or OOM even when the existing audio path would process bounded chunks; generate timed segments per chunk and offset the cue timestamps instead.
Useful? React with 👍 / 👎.
| if !result.subtitleCues.isEmpty { | ||
| HStack(spacing: 10) { | ||
| Button { | ||
| self.exportFormat = .srt |
There was a problem hiding this comment.
Reset the export format after SRT exports
After a user clicks this SRT-only button, exportFormat remains .srt because the exporter completion only clears exportResult, and the generic export buttons do not set the format back to text. The next time the user clicks the normal export icon on a transcript without subtitleCues, TranscriptionDocument still calls exportToSRT and the export fails; reset the format after SRT export or set .text before opening the generic exporter.
Useful? React with 👍 / 👎.
| for segment in timedSegments { | ||
| guard let speakerID = self.bestSpeakerID( | ||
| forStart: segment.startSeconds, | ||
| end: segment.endSeconds, | ||
| diarization: speakerSegments | ||
| ) else { | ||
| continue | ||
| } |
There was a problem hiding this comment.
Silent transcript data loss in Whisper + diarization path
When bestSpeakerID returns nil — which happens whenever a Whisper timed segment falls in a gap between labeled diarizer windows (common at the very start/end of a file, during overlapping speakers, or in brief pauses between turns) — the entire Whisper segment is silently skipped. The function still returns .success with no counter, warning, or partial-result note in the output text. The only safeguard is guard !paragraphs.isEmpty, which only throws when all segments were dropped; any partial drop reaches the caller as a complete transcript. A user whose recording has even a short unmatched region at the edges will get a silently truncated result.
Prompt To Fix With AI
This is a comment left during a code review.
Path: Sources/Fluid/Services/MeetingTranscriptionService.swift
Line: 572-579
Comment:
**Silent transcript data loss in Whisper + diarization path**
When `bestSpeakerID` returns `nil` — which happens whenever a Whisper timed segment falls in a gap between labeled diarizer windows (common at the very start/end of a file, during overlapping speakers, or in brief pauses between turns) — the entire Whisper segment is silently skipped. The function still returns `.success` with no counter, warning, or partial-result note in the output text. The only safeguard is `guard !paragraphs.isEmpty`, which only throws when *all* segments were dropped; any partial drop reaches the caller as a complete transcript. A user whose recording has even a short unmatched region at the edges will get a silently truncated result.
How can I resolve this? If you propose a fix, please make it concise.There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: e6260e768d
ℹ️ About Codex in GitHub
Your team has set up Codex to 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 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| process.arguments = [ | ||
| "-a", "never", | ||
| "exec", | ||
| "--skip-git-repo-check", | ||
| "--color", "never", |
There was a problem hiding this comment.
Give Codex exec a writable sandbox
When Notch handoff is used for commands that need to modify files or run outside read-only constraints, these arguments never set --sandbox; I checked the Codex non-interactive docs, which state codex exec defaults to a read-only sandbox and recommend --sandbox workspace-write for edits. Combined with -a never, default installs cannot escalate for ordinary Command Mode requests like creating or updating files, so the new Notch route reports a Codex error instead of performing the action.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: f9b4a8cf14
ℹ️ About Codex in GitHub
Your team has set up Codex to 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 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if self.overriddenModelDirectory == nil { | ||
| try WhisperModelStorage.migrateLegacyCacheIfNeeded() | ||
| } |
There was a problem hiding this comment.
Clear legacy models when clearing cache
When an existing installation is updated and the user clears the model cache before the next prepare, this migration has not run yet, while clearCache() only deletes from the new Application Support directory. The old ~/Library/Caches/.../WhisperModels files therefore remain (potentially retaining a large Whisper download) even though the UI reports the cache cleared; have the clear path also remove the legacy cache directory.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: fbda4441c2
ℹ️ About Codex in GitHub
Your team has set up Codex to 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 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| let needsRegularNotchCleanup = self.notch != nil | ||
| if needsRegularNotchCleanup { | ||
| self.hide() |
There was a problem hiding this comment.
Preserve Codex processing while replacing the regular notch
When a Codex notch handoff starts while the regular recording notch is visible, processCodexHandoff sets command processing to true and then opens this expanded output. Calling hide() here runs performHideAndWait, which unconditionally calls NotchContentState.shared.setProcessing(false) before the expanded notch appears. The Codex run is still active, but its spinner/disabled input state is cleared, so a user can submit another command and start a concurrent handoff; preserve command processing across this regular-notch cleanup (or restore it immediately afterward).
Useful? React with 👍 / 👎.
…r-threshold # Conflicts: # Sources/Fluid/Services/TypingService.swift
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: a23919648e
ℹ️ About Codex in GitHub
Your team has set up Codex to 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 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| return utType.preferredFilenameExtension | ||
| } | ||
| return Set(extensions) | ||
| return Set(extensions).union(["mkv"]) |
There was a problem hiding this comment.
Route MKV audio through FFmpeg for every model
When speaker identification is off and the selected model is not Whisper (for example the default Parakeet model), the MKV-specific decoder is bypassed and execution falls through to AVAudioFile(forReading:). MKV was manually added because it is not among AVFoundation's decodable types, so these accepted files fail even when FFmpeg is installed; use the extracted WAV for the normal chunked provider path as well.
Useful? React with 👍 / 👎.
| try process.run() | ||
| process.waitUntilExit() |
There was a problem hiding this comment.
Run FFmpeg extraction off the main actor
For an MKV that reaches decodeAudioSamples, this synchronous wait executes directly from the @MainActor transcription flow; declaring the helper nonisolated does not move the call to a background executor. Extracting a long recording can therefore freeze the entire app, including progress updates and cancellation, until FFmpeg exits.
Useful? React with 👍 / 👎.
| if SettingsStore.shared.commandModeRouteToCodex { | ||
| await self.processCodexHandoff(text) | ||
| return |
There was a problem hiding this comment.
Preserve Codex context for notch follow-ups
When Codex routing uses the Notch style, a follow-up is sent through processCodexHandoff exactly like a new request. runCodexInNotch starts a fresh codex exec process containing only that follow-up text and passes neither the displayed conversation history nor a prior session identifier, so contextual inputs such as “now shorten that” cannot refer to the preceding result even though they are submitted through the follow-up UI.
Useful? React with 👍 / 👎.
…r-threshold # Conflicts: # Sources/Fluid/Persistence/FileTranscriptionHistoryStore.swift # Sources/Fluid/Services/CommandModeService.swift # Sources/Fluid/Services/MeetingTranscriptionService.swift # Sources/Fluid/Services/RewriteModeService.swift # Sources/Fluid/Services/TypingService.swift # Sources/Fluid/Services/WhisperProvider.swift
|
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. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 7b941a44e6
ℹ️ About Codex in GitHub
Your team has set up Codex to 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 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| usleep(15_000) | ||
| } while Date() < deadline |
There was a problem hiding this comment.
Move clipboard polling off the main actor
When Edit Mode cannot read a selection through Accessibility—especially when no text is selected in a browser editor—RewriteModeService.captureSelectedText() invokes this synchronous polling loop from the main actor. The repeated usleep calls can block FluidVoice's UI and hotkey handling for the full one-second timeout before Write Mode opens; perform the fallback asynchronously or poll without blocking the main actor.
Useful? React with 👍 / 👎.
| guard provider.isReady else { | ||
| throw TranscriptionError.modelLoadFailed("Transcription provider not ready") | ||
| } | ||
| let provider = try await self.provider(for: model) |
There was a problem hiding this comment.
Attribute file usage to the selected model
When the new file-transcription picker selects a model different from the global dictation model, this correctly prepares model, but the recordUsage call below still records SettingsStore.shared.selectedSpeechModel.analyticsDescriptor. Every such transcription is therefore attributed to the wrong model, corrupting per-model meeting usage and experiment results; pass the file-selected model's descriptor instead.
Useful? React with 👍 / 👎.
| Toggle(isOn: self.$speakerDiarizationEnabled) { | ||
| Label("Identify speakers", systemImage: "person.2.wave.2") | ||
| .font(.subheadline.weight(.medium)) | ||
| } | ||
| .toggleStyle(.switch) | ||
| .disabled(self.transcriptionService.isTranscribing) | ||
|
|
There was a problem hiding this comment.
Use a single speaker-identification setting
Once a file is selected on supported hardware, this new “Identify speakers” toggle is displayed alongside the existing “Label speakers” toggle, but they control independent flags and separate branches in transcribeFile. Turning either control off does not disable diarization when the other remains on (the UI initially even shows the new toggle on and the existing toggle off), so users cannot reliably opt out of the model download and expensive speaker-processing path; consolidate both controls onto one setting and pipeline.
Useful? React with 👍 / 👎.
…r-threshold # Conflicts: # Sources/Fluid/Services/MeetingTranscriptionService.swift # Sources/Fluid/Views/CommandModeView.swift
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: fc82dac737
ℹ️ About Codex in GitHub
Your team has set up Codex to 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 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if SettingsStore.shared.commandModeRouteToCodex { | ||
| await self.processCodexHandoff(text) | ||
| return |
There was a problem hiding this comment.
Record usage before routing commands to Codex
When Codex routing is enabled, this early return bypasses the recordUsage(mode: .command, ...) call below; the equivalent branch in processFollowUpCommand does the same. Consequently, every initial and follow-up Codex command disappears from Command Mode daily totals and core-action activity, undercounting the opted-in cohort. Record the usage before branching, using a Codex descriptor or no AI descriptor.
Useful? React with 👍 / 👎.
| process.standardError = errorPipe | ||
| try process.run() | ||
| process.waitUntilExit() |
There was a problem hiding this comment.
Drain FFmpeg stderr while the process runs
When a malformed MKV causes FFmpeg to emit enough error output to fill the pipe buffer, waitUntilExit() waits for the child while the child waits for this process to drain errorPipe, so transcription hangs indefinitely and never reaches the diagnostic read below. Drain stderr concurrently (or use a file/termination handler) before waiting for process completion.
Useful? React with 👍 / 👎.
Summary
This PR includes a focused set of user-facing improvements to FluidVoice's overlay, Edit Mode, and Command Mode.
Overlay feedback
0.4to0.12.Edit Mode reliability
Cmd+Cfallback afterAXSelectedText/ selected-range capture fails.Command Mode to Codex
CommandModeRouteToCodexsetting, defaulting off, so existing Command Mode behavior remains unchanged unless enabled.Notch: keeps FluidVoice's Command Mode notch UX, runs the bundled Codex CLI in the background, and displays the result in the notch without focusing the Codex app.App: activates the Codex desktop app and pastes/submits the spoken command there.Why
A few real-world workflows were failing or feeling confusing:
The changes are preference-backed and conservative: normal dictation and default Command Mode behavior are preserved unless the user enables the Codex route.
Validation
Validated locally on macOS with Xcode:
swiftc -parse $(rg --files Sources/Fluid -g '*.swift')git diff --checkplutil -lint Fluid.xcodeproj/project.pbxproj Info.plist Fluid.entitlementsxcodebuild clean build -project Fluid.xcodeproj -scheme Fluid -destination 'platform=macOS,arch=arm64' CODE_SIGN_IDENTITY='' CODE_SIGNING_REQUIRED=NO CODE_SIGNING_ALLOWED=NO/Applications/FluidVoice.appNotes
mainatv1.6.1(49e91c0).