Add compose bar with voice support - #23
Conversation
|
Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits. |
Deploying getremux with
|
| Latest commit: |
97183c5
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://5d4f3a5d.getremux.pages.dev |
| Branch Preview URL: | https://feature-voice-compose-bar.getremux.pages.dev |
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (1)
🚧 Files skipped from review as they are similar to previous changes (1)
📝 WalkthroughWalkthroughThe pull request adds a SwiftUI composer with attachment handling, dictation, keyboard ownership, completion-aware terminal submission, app integration, permission descriptions, deterministic UI-test transport data, updated GhosttyKit wiring, and unit and UI tests. It removes the legacy attachment tray and pending preview components. ChangesComposer and terminal interaction
Estimated code review effort: 5 (Critical) | ~120 minutes Possibly related PRs
Sequence Diagram(s)sequenceDiagram
participant User
participant GhosttyComposeBar
participant GhosttyComposerModel
participant GhosttyComposerDictationController
participant GhosttySurfaceScreen
participant TmuxTerminalScreenAdapter
participant TmuxSessionController
User->>GhosttyComposeBar: Enter text, attachments, or dictation
GhosttyComposeBar->>GhosttyComposerModel: Update composer state
GhosttyComposerModel->>GhosttyComposerDictationController: Start or stop dictation
GhosttyComposerDictationController-->>GhosttyComposerModel: Publish transcript and dictation state
User->>GhosttyComposeBar: Submit draft
GhosttyComposeBar->>GhosttyComposerModel: Submit
GhosttyComposerModel->>GhosttySurfaceScreen: Build destination and submission
GhosttySurfaceScreen->>TmuxTerminalScreenAdapter: Send paste and Enter
TmuxTerminalScreenAdapter->>TmuxSessionController: Submit tracked tmux input
TmuxSessionController-->>TmuxTerminalScreenAdapter: Return command completion status
TmuxTerminalScreenAdapter-->>GhosttySurfaceScreen: Return delivery status
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
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 |
There was a problem hiding this comment.
Actionable comments posted: 10
🧹 Nitpick comments (8)
RemuxApp/Sources/Ghostty/GhosttyComposeBar.swift (3)
217-226: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winShare one submission-state computation.
GhosttyComposeBar.submissionStateduplicatescomposerSubmissionStateinRemuxApp/Sources/Ghostty/GhosttySurfaceScreen.swift(lines 684-693). Both expressions must stay identical, because the screen uses its copy forisInteractionLockedwhile the bar uses its copy for the Send button. A future change to one send-eligibility rule will silently desynchronize the dock lock from the button state.Expose the computation once, for example as a computed property on
GhosttyComposerModelor a static function onGhosttyComposeBarSubmissionState, and call it from both sites.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@RemuxApp/Sources/Ghostty/GhosttyComposeBar.swift` around lines 217 - 226, Centralize the submission-state computation currently in GhosttyComposeBar.submissionState and GhosttySurfaceScreen.composerSubmissionState into one shared computed property or static helper on GhosttyComposerModel or GhosttyComposeBarSubmissionState. Update both call sites to use that shared implementation while preserving the existing submitting, terminal-input, content, and attachment-readiness rules.
842-860: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick winDecode chip preview images once.
attachmentPreviewcallsUIImage(data:)on each body evaluation for every attachment chip. Draft keystrokes re-render this subtree, so each keystroke re-decodes every chip image on the main thread.Store the decoded
UIImage(or a SwiftUIImage) with the attachment preview payload, or cache it per attachment ID.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@RemuxApp/Sources/Ghostty/GhosttyComposeBar.swift` around lines 842 - 860, Update attachmentPreview(_:) to avoid constructing UIImage(data:) during every SwiftUI body evaluation. Cache the decoded image per attachment ID or store it alongside the preview payload, then reuse that cached value when rendering image attachments while preserving the existing loading and file preview branches.
655-672: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick winAvoid mutating the text view from
sizeThatFits.SwiftUI can call
sizeThatFitsmultiple times with different proposals during layout, so settingtextView.isScrollEnabledhere can make the scroll flag inconsistent with the final applied height. Compute the clamped height insizeThatFitsand apply the scroll flag inupdateUIView, using a stored measurement for the committed layout.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@RemuxApp/Sources/Ghostty/GhosttyComposeBar.swift` around lines 655 - 672, The sizeThatFits method currently mutates textView.isScrollEnabled during measurement. Keep sizeThatFits limited to calculating and returning the clamped height, store the measured-height or overflow state needed for the committed layout, and update isScrollEnabled in updateUIView using that stored measurement and the applied height.RemuxApp/Sources/App/RemuxAppDependencies.swift (1)
328-341: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winDocument the coupling between this transcript and the tmux startup command order.
uiTestingTransportChunks()hardcodes command numbers 1 through 9 and a fixed reply order for the session, window, and pane queries. If the controller's startup sequence gains, removes, or reorders a command, this fixture no longer matches. The failure surfaces as a UI test that waits forterminal.input.readyuntil timeout, which is hard to diagnose.Add a short comment that names the startup sequence this transcript mirrors, so a future change to that sequence points here.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@RemuxApp/Sources/App/RemuxAppDependencies.swift` around lines 328 - 341, The hardcoded transcript in uiTestingTransportChunks() must document its dependency on the controller’s tmux startup command order. Add a short comment beside the transcript identifying the startup sequence it mirrors, including the session, window, and pane query ordering, so future changes direct maintainers to update this fixture.RemuxApp/Sources/Ghostty/GhosttyComposerAudioCaptureSession.swift (1)
160-207: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low valueConfirm that
startis never re-entered on a stopped session.
precondition(state == .idle)traps in release builds. A caller that reuses oneGhosttyComposerAudioCaptureSessionaftercancel()or a completed drain crashes the app. Both current backends create a new session per run, so the contract holds today. Consider throwingGhosttyComposerDictationErrorinstead of trapping, to keep a future reuse from being fatal.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@RemuxApp/Sources/Ghostty/GhosttyComposerAudioCaptureSession.swift` around lines 160 - 207, Update start in GhosttyComposerAudioCaptureSession to replace the fatal precondition on state == .idle with a thrown GhosttyComposerDictationError for any non-idle state. Preserve normal initialization for idle sessions and ensure reuse after cancel or completed drain fails gracefully without trapping.RemuxAppTests/GhosttyPendingAttachmentTests.swift (1)
223-231: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winStrengthen the failed-preparation assertion.
pasteboardImagePlaceholder()has no payload, sotransferSourceis nil before the failure is applied. The test passes even if.faileddoes not suppress the transfer source. Build the attachment from a state that has a payload, then mark it.failed, so the assertion covers the failure gate.♻️ Proposed test change
func testFailedPreparationCannotProduceTransferSource() { let attachment = GhosttyPendingAttachment - .pasteboardImagePlaceholder() + .file(url: URL(fileURLWithPath: "/tmp/report.txt")) .updating(detail: "Couldn’t load", preparationState: .failed)🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@RemuxAppTests/GhosttyPendingAttachmentTests.swift` around lines 223 - 231, Update testFailedPreparationCannotProduceTransferSource to construct the attachment from an initial state containing a valid transfer payload, then apply preparationState: .failed. Keep the existing assertions, ensuring transferSource would be non-nil before failure so its nil result verifies the failed-state suppression.RemuxApp/Sources/Ghostty/GhosttySpeechAnalyzerComposerDictationBackend.swift (1)
209-255: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low valueFinish the input continuation on every abandoned start path.
If
analyzer.start(inputSequence:)throws, or if theisDesired()guards fail after the analyzer starts,inputContinuationis never finished. TheAsyncStreamthen stays open until the run object is released. The explicitinputContinuation.finish()in the capture-startcatchshows the intent. Add the same call to the guard failures and to the outercatch.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@RemuxApp/Sources/Ghostty/GhosttySpeechAnalyzerComposerDictationBackend.swift` around lines 209 - 255, Ensure inputContinuation is finished on every abandoned startup path in the surrounding analyzer-start flow: add inputContinuation.finish() before returning from both activeRun/isDesired() guard failures, and add it in the outer catch that handles analyzer.start(inputSequence:) errors. Preserve the existing captureSession-start catch cleanup.RemuxApp/Sources/Ghostty/GhosttyComposerDictationController.swift (1)
721-731: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick winAdd a deadline for the transcribing phase.
finishcancelsstartDeadlineTaskand moves the controller to.transcribing. After that, no timer exists. If the backend never emits.completedor.recognitionFailed, the composer stays in.transcribingand the user cannot send the draft. The legacy backend reaches this state afterrecognitionTask.finish(), and the analyzer backend reaches it while awaitingfinalizeAndFinishThroughEndOfInput(). Schedule a finalization deadline that fails the session with the existing "Dictation stopped unexpectedly" message.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@RemuxApp/Sources/Ghostty/GhosttyComposerDictationController.swift` around lines 721 - 731, The finish flow in GhosttyComposerDictationController currently enters .transcribing without a fallback deadline. Add a transcribing-phase deadline in finish(afterTranscription:) that replaces the canceled startDeadlineTask and fails the session using the existing “Dictation stopped unexpectedly” handling if .completed or .recognitionFailed is not received; preserve normal backend completion behavior and existing callback cleanup.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@Remux.xcodeproj/project.pbxproj`:
- Line 19: Gate GhosttySpeechAnalyzerComposerDictationBackend.swift with a
compile-time SDK/compiler availability condition so it is not compiled or
registered for targets whose SDK lacks SpeechAnalyzer. Preserve
GhosttyLegacyComposerDictationBackend as the fallback whenever the gated backend
is unavailable, while retaining the SpeechAnalyzer implementation for supported
SDKs.
In `@RemuxApp/Sources/Ghostty/GhosttyComposerModel.swift`:
- Around line 105-111: Replace the isolated deinit declaration in
GhosttyComposerModel with a deinitialization implementation compatible with the
project’s Swift 6.0 setting, or explicitly raise the required Swift/toolchain
version to 6.2+ consistently across the project if isolated deinit is required.
Preserve cancellation of submissionTask and attachmentPreparationJobs plus
GhosttyAttachmentStagingStore.cleanup(attachments).
- Around line 333-348: The performSubmission flow must bound both
destination.sendPaste(message) and destination.sendEnter() awaits with
deadlines. Handle timeout or failed completion by calling finishDelivery with
the appropriate non-submitted result, ensuring isSubmitting and submissionTask
are cleared so later submissions remain available; preserve the existing
successful paste, settlement-delay, and submit behavior.
In
`@RemuxApp/Sources/Ghostty/GhosttySpeechAnalyzerComposerDictationBackend.swift`:
- Around line 39-77: Serialize start, finish, cancel, and cancelAll through a
single ordered command stream instead of creating independent Tasks in each
entry point. Update the surrounding backend state flow so commands invoke the
actor in submission order, ensuring State.finish always observes the preceding
State.start and preserves the expected stop/result behavior.
In `@RemuxApp/Sources/Ghostty/GhosttySurfaceScreen.swift`:
- Around line 407-446: Update the bottom safe-area inset host in the
GhosttySurfaceScreen view to size from the measured expanded chrome height
rather than the fixed GhosttyKeyboardChromeSizing.baselineHeight. Preserve the
existing GhosttyKeyboardChrome layout, padding, and
GhosttyBottomChromeHeightPreferenceKey measurement while ensuring attachments
and expanded draft content increase the inset accordingly.
- Around line 150-170: Make the composer update publisher stable across body
evaluations instead of rebuilding it in the computed composerUpdates property.
Store the merged presented, submitting, and attachments publisher once in
view-owned state or an equivalent stable box, while preserving the existing
isSelected gating and dropFirst behavior; alternatively observe composer
directly with `@ObservedObject` and remove the manual composerRevision
passthrough.
In `@RemuxApp/Sources/Ghostty/GhosttyTerminalInputCoordinator.swift`:
- Around line 43-53: Update toggleKeyboard(owner:isOwnerAvailable:) to validate
ownership and transfer availability before calling showSystemKeyboard,
particularly when the requested owner is .composer and .terminal currently owns
the system keyboard. Prevent the shown transition from being projected unless
ownership can actually move to the requested owner, while preserving the
existing hide behavior for the current owner.
In `@RemuxApp/Sources/Tmux/TmuxPaneSurface.swift`:
- Around line 109-122: Protect trackedWrite access in performTrackedWrite and
the Ghostty write_cb path with MainActor-isolated storage or an equivalent
synchronization boundary, ensuring callbacks cannot concurrently read or clear
the slot while completing tracked input. Replace the precondition(trackedWrite
== nil) overlap crash by completing the new request with false and preserving
the existing trackedWrite until its original send finishes.
In `@RemuxApp/Sources/Tmux/TmuxSessionController.swift`:
- Around line 718-762: The tracked-input flow in sendTrackedInput needs a
timeout fallback. After storing each token in requestsByToken, schedule a
deadline that removes that token only if it is still pending and invokes its
completion with false; cancel or make the timeout harmless when
handleCommandCompletion or session-failure paths have already removed the entry,
preserving existing success and failure handling.
In `@RemuxAppUITests/RemuxAppUITests.swift`:
- Around line 1163-1194: Update the probe-delta calculations around the raw
samples used for dictation and typing so each UInt64 counter is validated as
nondecreasing before performing checked subtraction, reporting an XCTest failure
when a later sample is smaller rather than trapping. Apply this to evals,
barEvals, and passes while preserving the existing metric calculations, then
remove the ineffective typingEvals >= 0 assertion because the raw-sample
monotonicity assertion replaces it.
---
Nitpick comments:
In `@RemuxApp/Sources/App/RemuxAppDependencies.swift`:
- Around line 328-341: The hardcoded transcript in uiTestingTransportChunks()
must document its dependency on the controller’s tmux startup command order. Add
a short comment beside the transcript identifying the startup sequence it
mirrors, including the session, window, and pane query ordering, so future
changes direct maintainers to update this fixture.
In `@RemuxApp/Sources/Ghostty/GhosttyComposeBar.swift`:
- Around line 217-226: Centralize the submission-state computation currently in
GhosttyComposeBar.submissionState and
GhosttySurfaceScreen.composerSubmissionState into one shared computed property
or static helper on GhosttyComposerModel or GhosttyComposeBarSubmissionState.
Update both call sites to use that shared implementation while preserving the
existing submitting, terminal-input, content, and attachment-readiness rules.
- Around line 842-860: Update attachmentPreview(_:) to avoid constructing
UIImage(data:) during every SwiftUI body evaluation. Cache the decoded image per
attachment ID or store it alongside the preview payload, then reuse that cached
value when rendering image attachments while preserving the existing loading and
file preview branches.
- Around line 655-672: The sizeThatFits method currently mutates
textView.isScrollEnabled during measurement. Keep sizeThatFits limited to
calculating and returning the clamped height, store the measured-height or
overflow state needed for the committed layout, and update isScrollEnabled in
updateUIView using that stored measurement and the applied height.
In `@RemuxApp/Sources/Ghostty/GhosttyComposerAudioCaptureSession.swift`:
- Around line 160-207: Update start in GhosttyComposerAudioCaptureSession to
replace the fatal precondition on state == .idle with a thrown
GhosttyComposerDictationError for any non-idle state. Preserve normal
initialization for idle sessions and ensure reuse after cancel or completed
drain fails gracefully without trapping.
In `@RemuxApp/Sources/Ghostty/GhosttyComposerDictationController.swift`:
- Around line 721-731: The finish flow in GhosttyComposerDictationController
currently enters .transcribing without a fallback deadline. Add a
transcribing-phase deadline in finish(afterTranscription:) that replaces the
canceled startDeadlineTask and fails the session using the existing “Dictation
stopped unexpectedly” handling if .completed or .recognitionFailed is not
received; preserve normal backend completion behavior and existing callback
cleanup.
In
`@RemuxApp/Sources/Ghostty/GhosttySpeechAnalyzerComposerDictationBackend.swift`:
- Around line 209-255: Ensure inputContinuation is finished on every abandoned
startup path in the surrounding analyzer-start flow: add
inputContinuation.finish() before returning from both activeRun/isDesired()
guard failures, and add it in the outer catch that handles
analyzer.start(inputSequence:) errors. Preserve the existing
captureSession-start catch cleanup.
In `@RemuxAppTests/GhosttyPendingAttachmentTests.swift`:
- Around line 223-231: Update testFailedPreparationCannotProduceTransferSource
to construct the attachment from an initial state containing a valid transfer
payload, then apply preparationState: .failed. Keep the existing assertions,
ensuring transferSource would be non-nil before failure so its nil result
verifies the failed-state suppression.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: bd476e0b-cb8d-4e56-bc5e-32c6719fa7d2
📒 Files selected for processing (39)
Remux.xcodeproj/project.pbxprojRemuxApp/Info.plistRemuxApp/Sources/App/RemuxAppDependencies.swiftRemuxApp/Sources/App/RootView.swiftRemuxApp/Sources/Ghostty/GhosttyAttachmentNotice.swiftRemuxApp/Sources/Ghostty/GhosttyAttachmentPasteboardSnapshot.swiftRemuxApp/Sources/Ghostty/GhosttyAttachmentPreviewSheet.swiftRemuxApp/Sources/Ghostty/GhosttyAttachmentPreviewStyle.swiftRemuxApp/Sources/Ghostty/GhosttyAttachmentTransfer.swiftRemuxApp/Sources/Ghostty/GhosttyAttachmentTray.swiftRemuxApp/Sources/Ghostty/GhosttyComposeBar.swiftRemuxApp/Sources/Ghostty/GhosttyComposerAudioCaptureSession.swiftRemuxApp/Sources/Ghostty/GhosttyComposerDictationController.swiftRemuxApp/Sources/Ghostty/GhosttyComposerModel.swiftRemuxApp/Sources/Ghostty/GhosttyComposerSubmissionController.swiftRemuxApp/Sources/Ghostty/GhosttyDebugComposerDictationBackend.swiftRemuxApp/Sources/Ghostty/GhosttyKeyboardChrome.swiftRemuxApp/Sources/Ghostty/GhosttyManagedSurface.swiftRemuxApp/Sources/Ghostty/GhosttyPendingAttachment.swiftRemuxApp/Sources/Ghostty/GhosttyPendingAttachmentPreview.swiftRemuxApp/Sources/Ghostty/GhosttySpeechAnalyzerComposerDictationBackend.swiftRemuxApp/Sources/Ghostty/GhosttySurfaceScreen.swiftRemuxApp/Sources/Ghostty/GhosttySurfaceStatusOverlay.swiftRemuxApp/Sources/Ghostty/GhosttyTerminalInputCoordinator.swiftRemuxApp/Sources/Ghostty/GhosttyTerminalResponderFocusPolicy.swiftRemuxApp/Sources/Ghostty/GhosttyTerminalResponderView.swiftRemuxApp/Sources/Ghostty/GhosttyTerminalScreenModeling.swiftRemuxApp/Sources/Ghostty/GhosttyTerminalViewportCoordinator.swiftRemuxApp/Sources/Tmux/TmuxPaneSurface.swiftRemuxApp/Sources/Tmux/TmuxSessionController.swiftRemuxApp/Sources/Tmux/TmuxTerminalScreenAdapter.swiftRemuxAppTests/GhosttyComposerDictationControllerTests.swiftRemuxAppTests/GhosttyComposerSubmissionControllerTests.swiftRemuxAppTests/GhosttyPendingAttachmentTests.swiftRemuxAppTests/GhosttyTerminalInputCoordinatorTests.swiftRemuxAppTests/GhosttyTerminalResponderFocusPolicyTests.swiftRemuxAppTests/TmuxSessionControllerClientSizeTests.swiftRemuxAppUITests/RemuxAppUITests.swiftproject.yml
💤 Files with no reviewable changes (2)
- RemuxApp/Sources/Ghostty/GhosttyAttachmentTray.swift
- RemuxApp/Sources/Ghostty/GhosttyPendingAttachmentPreview.swift
| let didPaste = await destination.sendPaste(message) | ||
| guard didPaste else { | ||
| finishDelivery(.pasteRejected, session: session, destination: destination) | ||
| return | ||
| } | ||
|
|
||
| do { | ||
| try await Task.sleep(for: Self.pasteSettlementDelay) | ||
| } catch { | ||
| finishDelivery(.pastedAwaitingSubmit, session: session, destination: destination) | ||
| return | ||
| } | ||
|
|
||
| let result: GhosttyComposerSubmissionController.DraftResult = | ||
| await destination.sendEnter() ? .submitted : .pastedAwaitingSubmit | ||
| finishDelivery(result, session: session, destination: destination) |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Check whether tracked terminal delivery guarantees completion on failure paths.
rg -n -C8 'sendTrackedInput|sendTrackedLiteralInput|sendComposerEnter|sendTerminalPaste' --glob '*.swift'Repository: h3nock/remux
Length of output: 150
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "Tracked files involving GhosttyComposerModel.swift:"
git ls-files | rg 'GhosttyComposerModel\.swift|Ghostty|Ghostty' || true
file="RemuxApp/Sources/Ghostty/GhosttyComposerModel.swift"
if [ -f "$file" ]; then
echo
echo "Outline:"
ast-grep outline "$file" || true
echo
echo "Relevant section lines 220-370:"
sed -n '220,370p' "$file" | nl -ba -v220
fi
echo
echo "Search for submission/isSubmitting/pasteSettlement/sendPaste/sendEnter/composer:"
rg -n -C4 'isSubmitting|submissionTask|performSubmission|sendPaste|sendEnter|pasteSettlementDelay|finishDelivery|sendComposerEnter|Send|submission' --glob '*.swift' RemuxApp || trueRepository: h3nock/remux
Length of output: 6617
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "Relevant GhosttyComposerModel.swift lines:"
sed -n '1,420p' RemuxApp/Sources/Ghostty/GhosttyComposerModel.swift
echo
echo "GhosttyComposerSubmissionController.swift outline:"
ast-grep outline RemuxApp/Sources/Ghostty/GhosttyComposerSubmissionController.swift || true
echo
echo "GhosttyComposerSubmissionController.swift lines 1-260:"
sed -n '1,260p' RemuxApp/Sources/Ghostty/GhosttyComposerSubmissionController.swiftRepository: h3nock/remux
Length of output: 15845
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "Files containing GhosttyComposerSubmissionDestination:"
rg -n -C6 'GhosttyComposerSubmissionDestination|destination\.sendPaste|destination\.sendEnter|prepareTerminalInput|sendEnter|sendPaste|sendTerminalPaste|composer.*paste|sendComposer' --glob '*.swift' .
echo
echo "Find GhosttyTerminalResponderTextInputShim implementation around paste-enter paths:"
file="RemuxApp/Sources/Ghostty/GhosttyTerminalResponderTextInputShim.swift"
if [ -f "$file" ]; then
sed -n '1,260p' "$file"
fiRepository: h3nock/remux
Length of output: 50368
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "TmuxPaneSurface performInputAwaitingCommandCompletion:"
sed -n '260,340p' RemuxApp/Sources/Tmux/TmuxPaneSurface.swift
echo
echo "GhosttyManagedSurface sendKeyEventAwaitingCommandCompletion/sendKeyEvent:"
sed -n '80,180p' RemuxApp/Sources/Ghostty/GhosttyManagedSurface.swift
echo
echo "GhosttySurfaceScreen sendTerminalText/sendKeyEvent/sendTerminalPaste:"
sed -n '1488,1578p' RemuxApp/Sources/Ghostty/GhosttySurfaceScreen.swift
echo
echo "GhosttyTerminalScreenModeling async awaiting API body candidates:"
file="RemuxApp/Sources/Ghostty/GhosttyTerminalScreenModeling.swift"
sed -n '1,140p' "$file"Repository: h3nock/remux
Length of output: 14633
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "TmuxPaneSurface lifecycle/callback completion paths:"
rg -n -C8 'lifecycle|CallbackBox|performTrackedWrite|prepareForPermanentRemoval|onRendererFailure|rendererDidFail|completion|resume' --glob '*.swift' RemuxApp/Sources/Tmux RemuxApp/Sources/Ghostty | head -n 240
echo
echo "Focused composer status and isSubmitting usages:"
rg -n -C4 'composerStatus|statusMessage|isSubmitting|Submit|sending|progress' --glob '*.swift' RemuxApp/Sources/Ghostty RemuxApp/Sources/Tmux | head -n 220Repository: h3nock/remux
Length of output: 39860
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "TmuxPaneSurface lifecycle enum and performTrackedWrite implementation:"
rg -n -C10 'enum Lifecycle|Lifecycle\.|performTrackedWrite|withCheckedContinuation|prepareForPermanentRemoval|rendererDidFail|onRendererFailure|sendPasteAwaitingCommandCompletion|sendKeyEventAwaitingCommandCompletion' --glob '*.swift' RemuxApp/Sources/Tmux RemuxApp/Sources/GhosttyRepository: h3nock/remux
Length of output: 50368
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "GhosttyKitControlSurface sendPaste/sendKeyEvent implementation:"
sed -n '190,230p' RemuxApp/Sources/Ghostty/GhosttyKitControlSurface.swift
echo
echo "Read-only verifier: composer pending-state transition logic"
python3 - <<'PY'
from pathlib import Path
import re
path = Path("RemuxApp/Sources/Ghostty/GhosttyComposerModel.swift")
src = path.read_text()
patterns = {
"submit_guard_isSubmitting": re.search(r"func submit\(.*?\n\s*guard !isSubmitting\s*\) else\s*\{\s*\n\s*return\s*\n\s*\}", src, re.S),
"isSubmitting_set_before_task": re.search(r"isSubmitting\s*=\s*true[\s\S]{0,800}submissionTask\s*=\s*Task", src),
"finish_before_clears_state": re.search(r"submissionTask\s*=\s*nil[\s\S]{0,300}isSubmitting\s*=\s*false", src),
"finish_delivery_clears_state": re.search(r"submissionTask\s*=\s*nil[\s\S]{0,600}isSubmitting\s*=\s*false", src),
"await_send_paste": re.search(r"let\s+didPaste\s*=.*?destination\.sendPaste\(message\)", src, re.S),
"await_send_enter": re.search(r"await\s+destination\.sendEnter\(\)", src),
}
for name, pat in patterns.items():
print(f"{name}: {pat is not None}")
# Extract the two unchecked awaits and whether they are followed by immediate finishDelivery.
idx_paste = src.find("destination.sendPaste(message)")
idx_enter = src.find("destination.sendEnter()", idx_paste)
print("paste_await_index:", idx_paste)
print("enter_await_index:", idx_enter)
print("enter_is_last_direct_await_before_finish_delivery:", "finishDelivery(result" in src[idx_enter:])
PYRepository: h3nock/remux
Length of output: 1787
Add a bound to the composer input awaits.
performSubmission awaits destination.sendPaste(message) and destination.sendEnter(). These paths wait on tracked tmux command completion, and an unbounded await can leave isSubmitting and submissionTask set indefinitely if the pane becomes unavailable or the completion does not fire. Use a deadline around each await and map expiry to finishDelivery, so normal future submissions remain available.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@RemuxApp/Sources/Ghostty/GhosttyComposerModel.swift` around lines 333 - 348,
The performSubmission flow must bound both destination.sendPaste(message) and
destination.sendEnter() awaits with deadlines. Handle timeout or failed
completion by calling finishDelivery with the appropriate non-submitted result,
ensuring isSubmitting and submissionTask are cleared so later submissions remain
available; preserve the existing successful paste, settlement-delay, and submit
behavior.
There was a problem hiding this comment.
Agreed the await is unbounded in the specific case where tmux stops replying while the transport stays open. Every realistic failure path does resolve it — disconnect and detach complete all outstanding tracked input — and reconnecting recovers today. Tracking a proper bound in #24, with one important amendment to the suggested fix: expiry has to resolve to the unconfirmed outcome rather than a plain failure, because a timed-out send may still land and "safe to retry" semantics would invite a double submission.
There was a problem hiding this comment.
@h3nock, agreed. The finding remains valid, but #24 is the correct scope.
A timeout must produce an unconfirmed delivery outcome. It must keep the draft and avoid retry-safe failure semantics. A timed-out paste or Enter can still reach tmux after the deadline.
🐇
✏️ Learnings added
Learnt from: h3nock
Repo: h3nock/remux PR: 23
File: RemuxApp/Sources/Ghostty/GhosttyComposerModel.swift:333-348
Timestamp: 2026-07-31T12:51:39.768Z
Learning: In `RemuxApp/Sources/Ghostty/GhosttyComposerModel.swift`, tracked tmux input completion normally resolves on disconnect and detach. A remaining unbounded-await case exists when the tmux transport stays connected but tmux stops replying. A deadline for `destination.sendPaste(_:)` or `destination.sendEnter()` must resolve to an unconfirmed-delivery outcome, preserve the draft, and not imply that retrying is safe, because the timed-out input may still reach tmux. This follow-up is tracked in GitHub issue `#24`.
Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.
You are interacting with an AI system.
| private func sendTrackedInput( | ||
| paneID: TmuxPaneID, | ||
| _ bytes: Data, | ||
| transport: TrackedInputTransport, | ||
| completion: @escaping @Sendable (Bool) -> Void | ||
| ) -> Bool { | ||
| guard !bytes.isEmpty else { return false } | ||
| queue.async { [self, bytes] in | ||
| guard let client, outboundSink != nil, !shuttingDown else { | ||
| reportRequestFailure(.sendInput) | ||
| completion(false) | ||
| return | ||
| } | ||
| var token: UInt64 = 0 | ||
| let result = bytes.withUnsafeBytes { buffer in | ||
| let pointer = buffer.bindMemory(to: UInt8.self).baseAddress | ||
| return switch transport { | ||
| case .exact: | ||
| ghostty_tmux_client_send_pane_input_tracked( | ||
| client, | ||
| paneID.rawValue, | ||
| pointer, | ||
| buffer.count, | ||
| &token | ||
| ) | ||
| case .literal: | ||
| ghostty_tmux_client_send_pane_literal_input_tracked( | ||
| client, | ||
| paneID.rawValue, | ||
| pointer, | ||
| buffer.count, | ||
| &token | ||
| ) | ||
| } | ||
| } | ||
| guard result == GHOSTTY_TMUX_RESULT_OK else { | ||
| reportImmediateFailure(result, request: .sendInput) | ||
| completion(false) | ||
| return | ||
| } | ||
| requestsByToken[token] = .trackedInput(completion) | ||
| _ = drainOutbound() | ||
| } | ||
| return true | ||
| } |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift
Bound the tracked-input completion with a timeout.
A tracked input resolves only through handleCommandCompletion or one of the session-failure paths. If tmux accepts the command but never emits %begin/%end for that token while the transport stays open, the stored completion never runs.
The awaiting caller is the composer: GhosttyComposerModel.performSubmission awaits destination.sendPaste(message), keeps isSubmitting true, and GhosttySurfaceScreen locks the dock through isInteractionLocked. A single stalled response therefore leaves the composer and dock locked with no recovery other than disconnecting.
Add a deadline for each tracked token. On expiry, remove the entry and complete with false, so the composer reports the destination-aware failure and unlocks.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@RemuxApp/Sources/Tmux/TmuxSessionController.swift` around lines 718 - 762,
The tracked-input flow in sendTrackedInput needs a timeout fallback. After
storing each token in requestsByToken, schedule a deadline that removes that
token only if it is still pending and invokes its completion with false; cancel
or make the timeout harmless when handleCommandCompletion or session-failure
paths have already removed the entry, preserving existing success and failure
handling.
There was a problem hiding this comment.
Agreed on the gap, with the same caveat as the sibling comment: all realistic failure paths already drain outstanding tracked input (failOutstandingTrackedInput on disconnect and detach), so this only bites when tmux stops replying on an otherwise healthy transport — and reconnecting recovers. Tracking a bounded version in #24; the deadline design there is Enter-leg-only so that expiry maps onto the existing unconfirmed outcome instead of falsely reporting a retry-safe failure.
There was a problem hiding this comment.
@h3nock, agreed. The existing failure paths drain tracked input on disconnect, detach, shutdown, and client failure.
An Enter-leg-only deadline in #24 is the correct boundary. It can release the UI while preserving the unconfirmed outcome when tmux may have accepted the message. I will leave this finding open for the follow-up work.
🐇
✏️ Learnings added
Learnt from: h3nock
Repo: h3nock/remux PR: 23
File: RemuxApp/Sources/Tmux/TmuxSessionController.swift:718-762
Timestamp: 2026-07-31T12:51:26.107Z
Learning: In `RemuxApp/Sources/Tmux/TmuxSessionController.swift`, `failOutstandingTrackedInput()` already resolves pending tracked-input completions during disconnect, detach, shutdown, and client-failure paths. A future bounded deadline should apply only to the Enter leg and map expiry to an unconfirmed outcome, because tmux may have accepted the message and a retry may not be safe.
Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.
You are interacting with an AI system.
Deploying remux with
|
| Latest commit: |
97183c5
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://253bd334.remux-agx.pages.dev |
| Branch Preview URL: | https://feature-voice-compose-bar.remux-agx.pages.dev |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@scripts/fetch_ghosttykit.sh`:
- Around line 8-9: Update scripts/fetch_ghosttykit.sh to persist a marker
containing release_tag and asset_sha256, compare that marker with the pinned
values before the existing early exit, and replace the installed
GhosttyKit.xcframework when they differ. Preserve the no-op behavior for
matching installations and the --force override, and ensure the marker is
updated after a successful installation.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: fc9d593f-f538-4a2d-a076-5df4a6c297cf
📒 Files selected for processing (1)
scripts/fetch_ghosttykit.sh
|
@coderabbitai We've replied to each of your review comments in their threads: two findings are fixed (50478f9, 97183c5), the tracked-write concern is addressed with a contract assertion (fb8f824) plus the analysis in that thread, the unbounded-send pair is tracked in #24, and the remaining threads contain rebuttals with the specific mechanisms. Please take a look at the thread replies and let us know if anything doesn't hold up. |
|
Tip For best results, initiate chat on the files or code changes. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
repo="h3nock/remux"
pr_number="23"
echo "== Pull request review threads =="
gh api graphql \
-F owner="${repo%%/*}" \
-F name="${repo#*/}" \
-F number="$pr_number" \
-f query='
query($owner: String!, $name: String!, $number: Int!) {
repository(owner: $owner, name: $name) {
pullRequest(number: $number) {
reviewThreads(first: 100) {
nodes {
id
isResolved
isOutdated
path
line
comments(first: 30) {
nodes {
author { login }
body
createdAt
url
commit { oid }
}
}
}
}
}
}
}' \
--jq '.data.repository.pullRequest.reviewThreads.nodes[]
| select(any(.comments.nodes[]; .author.login == "coderabbitai"))
| {
id,
isResolved,
isOutdated,
path,
line,
comments: [.comments.nodes[] | {author: .author.login, body, url, commit: .commit.oid}]
}'
echo
echo "== Issue `#24` =="
gh issue view 24 --repo "$repo" --json number,title,state,body,url,author
echo
echo "== Cited commits =="
for sha in 50478f9 97183c5 fb8f824; do
echo "--- $sha ---"
gh api "repos/$repo/commits/$sha" \
--jq '{sha: .sha, message: .commit.message, files: [.files[] | {filename, status, additions, deletions, patch}]}'
done
echo
echo "== Current relevant source outlines =="
for name in TmuxPaneSurface.swift TmuxSessionController.swift GhosttyComposerModel.swift; do
file="$(fd -a -t f "^${name}$" . | head -n 1 || true)"
if [ -n "$file" ]; then
echo "--- $file ---"
ast-grep outline "$file" --items all
fi
doneLength of output: 115016
The two remaining timeout findings are correctly tracked in I do not see any remaining objection that requires a change in this PR. 🐇 You are interacting with an AI system. |
Summary
Adds a compose bar to the terminal dock for writing input before sending it
to the terminal. Supports typed text, dictation, and attachments. Nothing
reaches the pane until Send.
confirmed through tmux command completion.
waveform meter.
their remote paths are inserted into the message.
handling; the dock gains a composer toggle.
How it works
The composer is a single app-scoped model that lives above the terminal
screens. The draft, attachments, dictation state, and any in-flight send
survive switching terminals, reconnects, and screen recreation — the
composer follows you rather than belonging to one terminal.
The send destination is captured once when Send is tapped, as a small value
holding the pane, the transfer service, and the paste/Enter operations. The
composer holds no terminal references and imports no terminal types, so the
whole submission path is covered by unit tests with a mock destination.
The bar observes the composer model directly and the screen subscribes only
to what it renders, so typing and dictation re-render just the bar.
Send behavior
doesn't retarget the message.
retrying (a retry could double-submit) and the status names the
destination window: Couldn’t finish sending. Check “remux: codex”.
targets whatever pane is visible then.
outcome are visible.
Testing
controller against a mock backend.
dictation, and submit end to end, verified by reading a marker back out
of the remote pane, with scripted cleanup of the sessions it creates.
dictation across switches, attachment uploads, sends into live CLI
agents.
Screenshots
Summary by CodeRabbit
New Features
Bug Fixes
Accessibility