Skip to content

render tmux windows as live multi-pane canvases - #49

Merged
h3nock merged 24 commits into
mainfrom
feature/pane-composite-canvas
Aug 4, 2026
Merged

render tmux windows as live multi-pane canvases#49
h3nock merged 24 commits into
mainfrom
feature/pane-composite-canvas

Conversation

@h3nock

@h3nock h3nock commented Aug 4, 2026

Copy link
Copy Markdown
Owner

Summary

Render tmux windows as live multi-pane canvases in Remux and add pane-aware controls, previews, input routing, zoom handling, and deletion reliability.

Previously, Remux handled multipane windows by automatically zooming the active tmux pane and presenting only that pane. This change presents the complete pane layout by default and makes tmux zoom an explicit user action.

This change retains one terminal renderer per hydrated pane and composes those renderers using the authoritative geometry reported by tmux.

What changed

Multi-pane presentation

  • Render every pane in the active tmux window simultaneously.
  • Position panes using tmux’s authoritative window and pane geometry.
  • Use the same canvas implementation for single-pane and multi-pane windows.
  • Draw neutral internal separators and highlight the segments belonging to the selected pane.
  • Keep terminal content, selection, scrolling, mouse input, and keyboard input pane-local.
  • Maintain a stable keyboard responder while changing the logically selected pane.

Pane controls

  • Redesign the Panes sheet around the actual relative tmux layout.
  • Show a preview for every pane.
  • Add direct Split Right and Split Down actions.
  • Add a per-window Zoom toggle.
  • Support tile-local long-press deletion.
  • Remove Remux pane numbering.
  • Show the current pane count on the existing pane toolbar control.

Window controls

  • Improve window previews using the same per-pane capture path.
  • Make long-press deletion local to the pressed window card.
  • Serialize destructive topology changes so subsequent commands do not target stale pane or window state.
  • Present command failures non-modally inside the relevant sheet.

Zoom behavior

  • Reflect the tmux window’s actual server-side zoom state.
  • Preserve zoom when navigating between windows.
  • Preserve zoom while Remux is backgrounded.
  • Split with zoom enabled by using tmux’s zoom-preserving split behavior.
  • Preserve the existing zoomed pane when deleting an inactive pane.
  • Let tmux choose a successor and zoom it when deleting the active zoomed pane.
  • Turn Zoom off naturally when only one pane remains.
  • Track zooms created by Remux during the current app session so they can be cleaned up on normal session shutdown.

Remux makes a best-effort cleanup attempt for zooms it created when the actual Remux session shuts down. iOS does not guarantee execution when the app is force-killed, so server zoom can remain in that case and may need to be cleared from another tmux client using the tmux prefix followed by z—usually Ctrl-b, then z.

Preview capture

  • Use one per-pane preview capture path for both pane and window presentations.
  • Capture useful context around the terminal cursor.
  • Fall back to the center of the visible viewport when cursor geometry is unavailable.
  • Capture additional context and downsample it with medium-quality interpolation for readable thumbnails.
  • Read terminal cell and cursor geometry through generic libghostty host APIs.

Validation completed

  • Targeted tmux controller and foreground lifecycle tests pass.
  • Pane deletion was manually verified on a physical iPhone and simulator.
  • Window deletion was manually verified on a physical iPhone and simulator.
  • Horizontal, vertical, and nested separator geometry was manually exercised.
  • Pane selection and keyboard routing were manually exercised.
  • Pane and window preview framing was manually reviewed on-device.

Screenshots

    
    

Summary by CodeRabbit

  • New Features

    • Added multi-pane viewport rendering for simultaneous display of multiple tmux panes.
    • Added zoom controls for focused panes.
    • Enhanced pane selection with visual separators, inline actions, and removal controls.
  • Improvements

    • Improved keyboard focus and input routing across panes.
    • Enhanced renderer recovery with snapshot overlays.
    • Improved pane preview sizing, cropping, and rendering.
    • Added command-failure messaging and refined terminal theme separator colors.
    • Improved session shutdown reliability.

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Aug 4, 2026

Copy link
Copy Markdown

Deploying remux with  Cloudflare Pages  Cloudflare Pages

Latest commit: d4fb711
Status: ✅  Deploy successful!
Preview URL: https://d580899f.remux-agx.pages.dev
Branch Preview URL: https://feature-pane-composite-canva.remux-agx.pages.dev

View logs

@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@h3nock, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 21 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 251fd587-95e2-488f-a99e-10227e7b14e0

📥 Commits

Reviewing files that changed from the base of the PR and between 40c8123 and d4fb711.

📒 Files selected for processing (6)
  • RemuxApp/Sources/Ghostty/GhosttySurfaceSelectionSheet.swift
  • RemuxApp/Sources/Ghostty/GhosttyTerminalPresentationProjector.swift
  • RemuxApp/Sources/Tmux/TmuxTerminalScreenAdapter.swift
  • RemuxAppTests/GhosttyTerminalPresentationProjectorTests.swift
  • RemuxAppTests/TmuxSessionLinkWriteFailureTests.swift
  • RemuxAppUITests/RemuxAppUITests.swift
📝 Walkthrough

Walkthrough

This PR changes Remux from single-pane terminal presentation to topology-aware composite rendering. It adds pane-map geometry, zoom-aware tmux commands, renderer recovery snapshots, cursor-centered previews, measured viewport updates, updated selection sheets, and corresponding unit and UI test coverage.

Changes

Composite terminal presentation

Layer / File(s) Summary
Viewport geometry and preview contracts
RemuxApp/Sources/Ghostty/*, RemuxApp/Sources/Domain/TerminalSettings.swift
Viewport measurements, pane-map geometry, separator projections, theme colors, and cropped image generation define composite rendering and preview data.
Multi-pane surface rendering and interaction
RemuxApp/Sources/Tmux/TmuxTerminalSession.swift, TmuxTerminalScreenAdapter.swift, RemuxApp/Sources/Ghostty/GhosttySingleViewportView.swift, GhosttySurfaceScreen.swift, GhosttySurfaceSelectionSheet.swift
The terminal retains one managed surface per pane. Composite views render visible panes, route input by surface ID, draw focused separators, support zoom controls, and display command-failure banners.
Renderer recovery and pane previews
RemuxApp/Sources/Ghostty/GhosttyManagedSurface.swift, GhosttyPaneScrollContainerView.swift, RemuxApp/Sources/Tmux/TmuxPaneSurface.swift
Renderer replacement exposes availability and recovery snapshots. Input is blocked during recovery. Preview capture supports pane geometry, viewport geometry, cursor-centered cropping, frame reuse, and bounded waits.
Viewport preparation, zoom, refresh, and transport shutdown
RemuxApp/Sources/Tmux/TmuxScreenModel.swift, TmuxSessionController.swift, TmuxSessionLink.swift
Viewport preparation uses detailed measurements. Navigation supports zoom and multi-window unzoom operations. Refresh requests are coalesced without client-size metadata. Shutdown drains outbound writes with a timeout.
Behavior validation and release update
RemuxAppTests/*, RemuxAppUITests/RemuxAppUITests.swift, scripts/fetch_ghosttykit.sh
Tests cover composite geometry, pane maps, previews, zoom commands, topology updates, transport draining, and dynamic picker tiles. The GhosttyKit pin is updated to the ghosttykit-20260804 release.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant GhosttyCompositeViewportView
  participant GhosttySurfaceScreen
  participant TmuxTerminalScreenAdapter
  participant TmuxSessionController
  participant TmuxSessionLink

  User->>GhosttyCompositeViewportView: Tap visible pane
  GhosttyCompositeViewportView->>GhosttySurfaceScreen: Send surface ID and keyboard preference
  GhosttySurfaceScreen->>TmuxTerminalScreenAdapter: Focus pane
  TmuxTerminalScreenAdapter->>TmuxSessionController: Submit pane selection or zoom request
  TmuxSessionController->>TmuxSessionLink: Send tmux commands
  TmuxSessionLink-->>TmuxSessionController: Deliver output and topology updates
  TmuxTerminalScreenAdapter-->>GhosttyCompositeViewportView: Update pane projection
Loading

Possibly related PRs

  • h3nock/remux#20: Shares terminal presentation and surface lifecycle changes.
  • h3nock/remux#21: Shares pane selection sheets, presentation projection, and preview layout changes.
  • h3nock/remux#27: Shares tmux session and controller changes for pane selection and zoom behavior.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 6.48% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: rendering tmux windows as live multi-pane canvases.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/pane-composite-canvas

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Aug 4, 2026

Copy link
Copy Markdown

Deploying getremux with  Cloudflare Pages  Cloudflare Pages

Latest commit: d4fb711
Status: ✅  Deploy successful!
Preview URL: https://f10b4bfc.getremux.pages.dev
Branch Preview URL: https://feature-pane-composite-canva.getremux.pages.dev

View logs

@h3nock
h3nock marked this pull request as ready for review August 4, 2026 10:20

@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: a42fbadcc8

ℹ️ 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".

Comment on lines +1063 to +1065
viewport: selectedTopLevelID == topLevelID
? terminalViewportPresentationProjection
: nil

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 Preserve pane-map geometry after active-window changes

If tmux changes the active window externally while this pane sheet is open, the sheet's original topLevelID still exists, so paneSelectionSheetTopologyProjection does not dismiss it, but this conditional now passes nil as its viewport. The projector consequently returns no pane frames or window grid, and the visible sheet becomes a blank fallback even though all of that window's panes still exist. Derive the requested window's geometry from latestTopology, or dismiss/switch the sheet when its top level stops being selected.

Useful? React with 👍 / 👎.

@coderabbitai coderabbitai 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.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
RemuxApp/Sources/Tmux/TmuxPaneSurface.swift (1)

472-478: 🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

Renderer recovery has no terminal state and no retry, so a pane can stay permanently non-interactive. rendererDidFail() marks the pane unavailable and sets rendererFailureReported = true before recovery begins. Only a successful replacement clears that flag. Every path that ends recovery without success therefore parks the pane behind the recovery overlay with input disabled and no way back.

  • RemuxApp/Sources/Tmux/TmuxPaneSurface.swift#L472-L478: call finishRendererReplacement on every exit of the replacement flow, including the generation-mismatch return at Line 543-546, and clear rendererFailureReported (or expose a retry entry point) when the replacement fails so a later attempt is possible.
  • RemuxApp/Sources/Tmux/TmuxTerminalSession.swift#L260-L266: when the guard rejects the failure because presentationMetrics(for:in:) returns nil, record the pane as needing a replacement, then retry it from handleTopology and updateViewportMeasurement once topology and viewport metrics resolve.
🤖 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/TmuxPaneSurface.swift` around lines 472 - 478, Ensure
the renderer replacement flow always calls finishRendererReplacement on every
exit, including generation-mismatch handling, and clears rendererFailureReported
or otherwise exposes retry when replacement fails so recovery can be attempted
again. In RemuxApp/Sources/Tmux/TmuxPaneSurface.swift lines 472-478, update the
replacement failure path; also update the generation-mismatch path in that file
around lines 543-546. In RemuxApp/Sources/Tmux/TmuxTerminalSession.swift lines
260-266, record panes whose failure handling is skipped because
presentationMetrics(for:in:) is nil, then retry replacement from handleTopology
and updateViewportMeasurement once metrics become available.
🧹 Nitpick comments (14)
RemuxApp/Sources/Tmux/TmuxScreenModel.swift (1)

313-316: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider a separate re-measure entry point.

applyTerminalSettings now calls prepareInitialViewport. That method reports .disconnected(Self.initialViewportFailureReason) and sets startupFailure when measurement throws. A live, attached session then surfaces a startup-style failure after a theme change. The name also no longer matches the behavior, because the path runs after the initial connect.

Consider extracting the shared measurement into a remeasureViewport(size:scale:) helper and reporting a settings-application failure on this path instead of the initial-viewport reason.

🤖 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/TmuxScreenModel.swift` around lines 313 - 316, Separate
the post-connection measurement path in applyTerminalSettings from
prepareInitialViewport by extracting shared measurement into a
remeasureViewport(size:scale:) helper. Ensure remeasurement reports a
settings-application failure rather than
.disconnected(Self.initialViewportFailureReason) or setting startupFailure,
while preserving the initial-connect behavior of prepareInitialViewport.
RemuxApp/Sources/Tmux/TmuxSessionController.swift (1)

1106-1123: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extract the zoom-aware pane-selection submission.

Lines 1106-1123 and lines 1141-1158 build the same command and choose between submitPanePresentationCommandOnWriter and submitCommandOnWriter with the same condition. Only the Request value differs. One helper removes the duplicated policy and keeps both call sites in step when the zoom rules change.

♻️ Proposed helper
private func submitPaneSelection(
    paneID: TmuxPaneID,
    zoomed: Bool,
    request: Request,
    drainOutbound: Bool
) {
    let command = zoomed
        ? "select-pane -Z -t %\(paneID.rawValue)"
        : "select-pane -t %\(paneID.rawValue)"
    if zoomed {
        submitPanePresentationCommandOnWriter(
            command: command,
            request: request,
            paneID: paneID,
            drainOutbound: drainOutbound
        )
    } else {
        submitCommandOnWriter(
            command: command,
            request: request,
            drainOutbound: drainOutbound
        )
    }
}
🤖 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 1106 - 1123,
Extract the duplicated pane-selection logic that appears in two locations into a
new private helper method named submitPaneSelection. The helper should accept
paneID, zoomed, request, and drainOutbound parameters, build the zoom-aware
select-pane command once, and conditionally route to either
submitPanePresentationCommandOnWriter or submitCommandOnWriter based on the
zoomed flag. Replace both occurrences of this pattern (lines 1106-1123 and lines
1141-1158) with calls to this helper, passing the appropriate Request value to
each call site. This ensures the zoom-selection logic and command structure are
defined once and both call sites stay synchronized.
RemuxApp/Sources/Ghostty/GhosttyKitControlSurface.swift (1)

351-357: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

A failed cursor query invalidates the whole control surface.

report(result) calls fail(.native(result)) for any non-OK result. fail sets invalidated = true and calls onFailure, which reaches TmuxPaneSurface.rendererDidFail() and triggers a full renderer replacement. cursorGeometry() is only used to pick a preview crop anchor in TmuxPaneSurface.capturePickerPreview, so a transient GHOSTTY_TERMINAL_SURFACE_RESULT_FAILED from a cosmetic query would tear down and rebuild a working renderer.

selectionSnapshot() handles the same native result class through selectionOutcome, which retries with ghostty_terminal_surface_terminal_changed instead of failing. Consider treating a cursor-geometry failure as "no anchor" and returning nil without escalating.

♻️ Suggested handling
     func cursorGeometry() -> CGRect? {
         guard !invalidated else { return nil }
         var geometry = ghostty_terminal_surface_cell_geometry_s()
         let result = ghostty_terminal_surface_cursor_geometry(handle, &geometry)
-        guard report(result) else { return nil }
+        guard result == GHOSTTY_TERMINAL_SURFACE_RESULT_OK else { return nil }
         return ghosttyTerminalCellFrame(geometry, scaleFactor: scaleFactor)
     }

Confirm which native results ghostty_terminal_surface_cursor_geometry can return before choosing the policy.

🤖 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/GhosttyKitControlSurface.swift` around lines 351 -
357, Update cursorGeometry() so failures from
ghostty_terminal_surface_cursor_geometry are handled as a missing preview anchor
rather than passed to report(result), which invalidates the control surface.
Confirm the native result values this query can return, and return nil for the
transient/failure result without invoking fail or onFailure; preserve the
existing invalidated guard and successful geometry conversion.
RemuxApp/Sources/Ghostty/TerminalSelectionSheetStyle.swift (1)

199-229: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Remove the redundant clipShape before glassEffect.

glassEffect(..., in: shape) already clips to Capsule(), so the prior .clipShape(shape) is unnecessary. Keep it only if the developer specifically wants to avoid downstream overflow artifacts outside the effect path.

🤖 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/TerminalSelectionSheetStyle.swift` around lines 199
- 229, Remove the redundant .clipShape(shape) call from the iOS 26 branch of
terminalSelectionSheetControlGroupSurface, leaving glassEffect(..., in: shape)
responsible for the capsule clipping. Keep the existing overlay and fallback
branch unchanged.
RemuxApp/Sources/Tmux/TmuxTerminalScreenAdapter.swift (2)

73-93: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Do not capture session strongly inside the topology subscription.

The adapter holds session as weak on purpose. This closure captures the activate(session:) parameter strongly, and subscriptions retains the closure, so the adapter keeps the session alive until invalidate() runs. Read the stored weak reference instead so the lifetime contract stays in one place.

♻️ Proposed fix
                 self.rebuildTopologySnapshot()
                 self.reconcileManagedSurfaces(
-                    sessionSurfaces: session.surfacesByPaneID,
+                    sessionSurfaces: self.session?.surfacesByPaneID ?? [:],
                     topology: topology
                 )
🤖 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/TmuxTerminalScreenAdapter.swift` around lines 73 - 93,
Update the topology subscription in activate(session:) to avoid capturing its
session parameter strongly; use the adapter’s stored weak session reference when
accessing surfacesByPaneID, while preserving the existing topology
reconciliation behavior.

110-129: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Bind self once in the failure sink.

The closure mixes self?. calls with an inner let self binding for the .closePane branch. Bind self at the top of the closure and use direct member access for every branch. This keeps the four failure branches symmetric.

🤖 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/TmuxTerminalScreenAdapter.swift` around lines 110 -
129, Update the lastFailedRequest sink closure to bind self once at its start
with a guard, then use direct member access throughout the selectPane, zoomPane,
closePane, and presentCommandFailure branches. Remove the inner let self binding
while preserving the existing request handling behavior.
RemuxApp/Sources/Ghostty/PanePreviewLayout.swift (1)

135-146: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Compute the scaled side once.

paneMapPhysicalPixelBudget evaluates (side * safeScale).rounded(.up) twice for a square budget. Bind it to one constant to make the square intent explicit.

♻️ Proposed refactor
         let safeScale = max(scale, 1)
         let side = min(availableWidth, maximumHeight)
-        return (
-            clampUInt32((side * safeScale).rounded(.up)),
-            clampUInt32((side * safeScale).rounded(.up))
-        )
+        let sidePx = clampUInt32((side * safeScale).rounded(.up))
+        return (sidePx, sidePx)
🤖 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/PanePreviewLayout.swift` around lines 135 - 146, In
paneMapPhysicalPixelBudget, compute the rounded scaled side once in a local
constant, then pass that value to clampUInt32 for both the width and height
tuple fields while preserving the existing square budget behavior.
RemuxApp/Sources/Ghostty/GhosttySingleViewportView.swift (1)

5-7: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Rename the file to match its primary type.

This file now defines GhosttyCompositeViewportView and GhosttyCompositeViewportContainerView, but the filename is still GhosttySingleViewportView.swift. Rename it to GhosttyCompositeViewportView.swift so the file matches the type it declares.

🤖 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/GhosttySingleViewportView.swift` around lines 5 - 7,
Rename GhosttySingleViewportView.swift to GhosttyCompositeViewportView.swift so
the filename matches the primary GhosttyCompositeViewportView type declared in
the file; leave the view implementations unchanged.
RemuxApp/Sources/Ghostty/GhosttySurfaceScreen.swift (1)

2072-2078: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Share the pane-map fallback height with the sheet.

This detent uses 160 when windowGrid or the pane-map metrics are unavailable. GhosttyPaneSelectionSheet.paneMap uses a separate minHeight: 160 literal for the same fallback. If one literal changes, the detent no longer matches the rendered content. Expose one constant, for example on PanePreviewLayout, and read it in both places.

🤖 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/GhosttySurfaceScreen.swift` around lines 2072 -
2078, Define a shared pane-map fallback height constant on PanePreviewLayout,
then replace the 160 fallback in the .panes detent calculation and the
minHeight: 160 value in GhosttyPaneSelectionSheet.paneMap with that constant so
both paths remain synchronized.
RemuxAppTests/TmuxTerminalScreenAdapterTests.swift (2)

389-399: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Assert the byte accounting after replacement.

TmuxPanePreviewImageCache.store subtracts the replaced entry's byteCost before adding the new one. This test checks the image identity and the entry count, but not totalByteCost. A regression that skips the subtraction would keep both assertions green and then over-count the cache, causing premature eviction.

Add the byte-cost assertion.

♻️ Proposed fix
         XCTAssertTrue(cache.preview(for: 1)?.image === replacement)
         XCTAssertEqual(cache.entries.count, 1)
+        XCTAssertEqual(
+            cache.totalByteCost,
+            replacement.bytesPerRow * replacement.height
+        )
🤖 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/TmuxTerminalScreenAdapterTests.swift` around lines 389 - 399,
Extend testPanePreviewCacheReplacesThePreviousImageForAPane to assert that
cache.totalByteCost equals the replacement preview’s byte cost after storing the
second image, confirming the replaced entry’s cost was removed before the new
cost was added.

190-200: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Guard the pane indexes before subscripting.

This test subscripts viewport.panes[0] and viewport.panes[1] without asserting the count first. If the projection returns fewer panes, the subscript traps and aborts the test process instead of reporting a failure. The sibling test at Line 146 asserts the count before mapping.

Assert the count first, and use XCTAssertNil for the hidden pane.

♻️ Proposed fix
         let viewport = adapter.terminalScreenPresentationProjection.viewport
         XCTAssertTrue(viewport.isServerZoomed)
-        XCTAssertEqual(viewport.panes[0].visibleFrame, nil)
+        XCTAssertEqual(viewport.panes.count, 2)
+        guard viewport.panes.count == 2 else { return }
+        XCTAssertNil(viewport.panes[0].visibleFrame)
         XCTAssertEqual(
             viewport.panes[1].visibleFrame,
             .init(x: 0, y: 0, columns: 80, rows: 24)
         )
🤖 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/TmuxTerminalScreenAdapterTests.swift` around lines 190 - 200,
Update the test around terminalScreenPresentationProjection to assert
viewport.panes contains the expected number of panes before subscripting it,
matching the sibling test’s guard pattern. Replace the equality assertion for
viewport.panes[0].visibleFrame with XCTAssertNil while preserving the existing
pane frame expectations.
RemuxAppTests/GhosttyTerminalPresentationProjectorTests.swift (1)

481-494: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

The viewport assertion now restates the test input.

The test passes focusedSurfaceID: surfaceInstanceID into viewportProjection, then asserts projection.viewport.focusedSurfaceID == surfaceInstanceID and != paneID. Both assertions hold for any pass-through implementation, so they no longer prove that the projector separates surface instance identity from pane identity. Only the selectedActiveLeafID assertion still exercises derivation.

Make the input distinguish the two identities. For example, include the pane surface ID in panes while focusedSurfaceID stays the surface instance, then assert which value each output field takes.

🤖 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/GhosttyTerminalPresentationProjectorTests.swift` around lines
481 - 494, Update the test’s viewportProjection setup to provide distinct pane
and surface-instance identities, such as including paneID in panes while
retaining surfaceInstanceID as focusedSurfaceID. Revise the assertions around
the projector result to verify viewport.focusedSurfaceID preserves the surface
instance and interaction.selectedActiveLeafID derives the expected pane
identity, rather than asserting only pass-through values.
RemuxAppUITests/RemuxAppUITests.swift (1)

1170-1176: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Assert which pane was removed, not just the count.

The previous code waited for a specific tile identifier to disappear. The new assertion only checks that the tile count drops to 1, so removing the wrong pane still passes. This test records no pane-count or pane-capture expectation afterwards, so nothing else catches a wrong-pane removal. The same pattern appears at Lines 1599-1604 in testLiveSSHBackgroundForegroundRetainsTerminalWhenConfigured.

Capture the identifier before removal and assert that exact tile disappears, in addition to the count.

♻️ Proposed fix
         openPanesSheet()
         XCTAssertTrue(waitForPanePickerTileCount(2, timeout: 10))
-        removePanePickerItem(panePickerTiles()[1])
+        let removedTile = panePickerTiles()[1]
+        let removedIdentifier = removedTile.identifier
+        removePanePickerItem(removedTile)
+        XCTAssertTrue(
+            waitForElementToDisappear(app.buttons[removedIdentifier], timeout: 10),
+            "The removed pane tile should disappear from the picker."
+        )
         XCTAssertTrue(
             waitForPanePickerTileCount(1, timeout: 10),
             "The removed pane should disappear from the picker."
         )
🤖 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 `@RemuxAppUITests/RemuxAppUITests.swift` around lines 1170 - 1176, Update the
pane-removal test around openPanesSheet and removePanePickerItem to capture the
selected pane’s identifier before removal, then assert that exact identifier
disappears while retaining the existing tile-count assertion. Apply the same
change in testLiveSSHBackgroundForegroundRetainsTerminalWhenConfigured, using
the existing pane-picker tile identifier and disappearance-wait helpers.
RemuxAppTests/GhosttyPanePreviewSessionTests.swift (1)

231-257: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add coverage for .paneMap preview sizing.

GhosttyPanePreviewSession.pixelBudget() branches on previewSizing, and paneMapForCurrentScreen uses .paneMap(...), but GhosttyPanePreviewSessionTests.swift only exercises .windowGrid(...). Add one test that mirrors testWindowGridUsesWindowPreviewPixelBudget and asserts .paneMap(...) requests PanePreviewLayout.paneMapPhysicalPixelBudget dimensions.

🤖 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/GhosttyPanePreviewSessionTests.swift` around lines 231 - 257,
Add a test alongside testWindowGridUsesWindowPreviewPixelBudget that constructs
GhosttyPanePreviewSession with .paneMap sizing, starts refreshing, waits for one
capture, and asserts the request budget matches
PanePreviewLayout.paneMapPhysicalPixelBudget dimensions for the same width and
scale. Resolve the harness request as the existing test does.
🤖 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 `@RemuxApp/Sources/Ghostty/GhosttySurfaceSelectionSheet.swift`:
- Around line 311-347: The pane tile accessibility identifier is attached to the
outer ZStack instead of the tappable control. Move
terminal.pane.tile.\(pane.id.uuidString) from the ZStack to the pane Button,
matching the window tile’s identifier placement and preserving the existing
identifier value.

In `@RemuxApp/Sources/Ghostty/GhosttyTerminalPresentationProjector.swift`:
- Around line 730-732: Update the viewportPanesByID construction in
paneSelectionSheetRenderProjection to use the non-trapping Dictionary
initializer with a duplicate-key resolution closure, preserving one pane for
each ID instead of crashing when viewport.panes contains duplicates.

In `@RemuxApp/Sources/Tmux/TmuxTerminalSession.swift`:
- Around line 256-258: The handleActivePaneChanged method must reconcile
presentation activity when the native active-pane event occurs, rather than only
refreshing the affected surface’s interaction state. Update
handleActivePaneChanged to invoke the existing reconcilePresentationActivity
flow (using the current topology/state inputs) so display and keyboard focus are
reassigned consistently even without a topology callback.

In `@RemuxAppTests/TmuxSessionLinkWriteFailureTests.swift`:
- Around line 78-81: Update the outbound assertion in
TmuxSessionLinkWriteFailureTests to compare the received byte data directly
against the expected “new-window\n” UTF-8 bytes, removing the
String(decoding:as:) conversion while preserving the current contains behavior.

In `@RemuxAppUITests/RemuxAppUITests.swift`:
- Around line 3709-3720: The removePanePickerItem helper currently stops after
opening the pane removal confirmation dialog. After tapPickerButton invokes
terminal.pane.remove.<pane id> or its fallback label, add a tap for
terminal.pane.remove.confirm to complete the confirmed removal.

---

Outside diff comments:
In `@RemuxApp/Sources/Tmux/TmuxPaneSurface.swift`:
- Around line 472-478: Ensure the renderer replacement flow always calls
finishRendererReplacement on every exit, including generation-mismatch handling,
and clears rendererFailureReported or otherwise exposes retry when replacement
fails so recovery can be attempted again. In
RemuxApp/Sources/Tmux/TmuxPaneSurface.swift lines 472-478, update the
replacement failure path; also update the generation-mismatch path in that file
around lines 543-546. In RemuxApp/Sources/Tmux/TmuxTerminalSession.swift lines
260-266, record panes whose failure handling is skipped because
presentationMetrics(for:in:) is nil, then retry replacement from handleTopology
and updateViewportMeasurement once metrics become available.

---

Nitpick comments:
In `@RemuxApp/Sources/Ghostty/GhosttyKitControlSurface.swift`:
- Around line 351-357: Update cursorGeometry() so failures from
ghostty_terminal_surface_cursor_geometry are handled as a missing preview anchor
rather than passed to report(result), which invalidates the control surface.
Confirm the native result values this query can return, and return nil for the
transient/failure result without invoking fail or onFailure; preserve the
existing invalidated guard and successful geometry conversion.

In `@RemuxApp/Sources/Ghostty/GhosttySingleViewportView.swift`:
- Around line 5-7: Rename GhosttySingleViewportView.swift to
GhosttyCompositeViewportView.swift so the filename matches the primary
GhosttyCompositeViewportView type declared in the file; leave the view
implementations unchanged.

In `@RemuxApp/Sources/Ghostty/GhosttySurfaceScreen.swift`:
- Around line 2072-2078: Define a shared pane-map fallback height constant on
PanePreviewLayout, then replace the 160 fallback in the .panes detent
calculation and the minHeight: 160 value in GhosttyPaneSelectionSheet.paneMap
with that constant so both paths remain synchronized.

In `@RemuxApp/Sources/Ghostty/PanePreviewLayout.swift`:
- Around line 135-146: In paneMapPhysicalPixelBudget, compute the rounded scaled
side once in a local constant, then pass that value to clampUInt32 for both the
width and height tuple fields while preserving the existing square budget
behavior.

In `@RemuxApp/Sources/Ghostty/TerminalSelectionSheetStyle.swift`:
- Around line 199-229: Remove the redundant .clipShape(shape) call from the iOS
26 branch of terminalSelectionSheetControlGroupSurface, leaving glassEffect(...,
in: shape) responsible for the capsule clipping. Keep the existing overlay and
fallback branch unchanged.

In `@RemuxApp/Sources/Tmux/TmuxScreenModel.swift`:
- Around line 313-316: Separate the post-connection measurement path in
applyTerminalSettings from prepareInitialViewport by extracting shared
measurement into a remeasureViewport(size:scale:) helper. Ensure remeasurement
reports a settings-application failure rather than
.disconnected(Self.initialViewportFailureReason) or setting startupFailure,
while preserving the initial-connect behavior of prepareInitialViewport.

In `@RemuxApp/Sources/Tmux/TmuxSessionController.swift`:
- Around line 1106-1123: Extract the duplicated pane-selection logic that
appears in two locations into a new private helper method named
submitPaneSelection. The helper should accept paneID, zoomed, request, and
drainOutbound parameters, build the zoom-aware select-pane command once, and
conditionally route to either submitPanePresentationCommandOnWriter or
submitCommandOnWriter based on the zoomed flag. Replace both occurrences of this
pattern (lines 1106-1123 and lines 1141-1158) with calls to this helper, passing
the appropriate Request value to each call site. This ensures the zoom-selection
logic and command structure are defined once and both call sites stay
synchronized.

In `@RemuxApp/Sources/Tmux/TmuxTerminalScreenAdapter.swift`:
- Around line 73-93: Update the topology subscription in activate(session:) to
avoid capturing its session parameter strongly; use the adapter’s stored weak
session reference when accessing surfacesByPaneID, while preserving the existing
topology reconciliation behavior.
- Around line 110-129: Update the lastFailedRequest sink closure to bind self
once at its start with a guard, then use direct member access throughout the
selectPane, zoomPane, closePane, and presentCommandFailure branches. Remove the
inner let self binding while preserving the existing request handling behavior.

In `@RemuxAppTests/GhosttyPanePreviewSessionTests.swift`:
- Around line 231-257: Add a test alongside
testWindowGridUsesWindowPreviewPixelBudget that constructs
GhosttyPanePreviewSession with .paneMap sizing, starts refreshing, waits for one
capture, and asserts the request budget matches
PanePreviewLayout.paneMapPhysicalPixelBudget dimensions for the same width and
scale. Resolve the harness request as the existing test does.

In `@RemuxAppTests/GhosttyTerminalPresentationProjectorTests.swift`:
- Around line 481-494: Update the test’s viewportProjection setup to provide
distinct pane and surface-instance identities, such as including paneID in panes
while retaining surfaceInstanceID as focusedSurfaceID. Revise the assertions
around the projector result to verify viewport.focusedSurfaceID preserves the
surface instance and interaction.selectedActiveLeafID derives the expected pane
identity, rather than asserting only pass-through values.

In `@RemuxAppTests/TmuxTerminalScreenAdapterTests.swift`:
- Around line 389-399: Extend
testPanePreviewCacheReplacesThePreviousImageForAPane to assert that
cache.totalByteCost equals the replacement preview’s byte cost after storing the
second image, confirming the replaced entry’s cost was removed before the new
cost was added.
- Around line 190-200: Update the test around
terminalScreenPresentationProjection to assert viewport.panes contains the
expected number of panes before subscripting it, matching the sibling test’s
guard pattern. Replace the equality assertion for viewport.panes[0].visibleFrame
with XCTAssertNil while preserving the existing pane frame expectations.

In `@RemuxAppUITests/RemuxAppUITests.swift`:
- Around line 1170-1176: Update the pane-removal test around openPanesSheet and
removePanePickerItem to capture the selected pane’s identifier before removal,
then assert that exact identifier disappears while retaining the existing
tile-count assertion. Apply the same change in
testLiveSSHBackgroundForegroundRetainsTerminalWhenConfigured, using the existing
pane-picker tile identifier and disappearance-wait helpers.
🪄 Autofix

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: cc24deac-ed68-41c2-ba52-d7557909c556

📥 Commits

Reviewing files that changed from the base of the PR and between c4a76fd and a42fbad.

📒 Files selected for processing (33)
  • RemuxApp/Sources/Domain/TerminalSettings.swift
  • RemuxApp/Sources/Ghostty/GhosttyIOSurfaceFrame.swift
  • RemuxApp/Sources/Ghostty/GhosttyKeyboardChrome.swift
  • RemuxApp/Sources/Ghostty/GhosttyKitControlSurface.swift
  • RemuxApp/Sources/Ghostty/GhosttyKitRuntime.swift
  • RemuxApp/Sources/Ghostty/GhosttyManagedSurface.swift
  • RemuxApp/Sources/Ghostty/GhosttyPanePreviewSession.swift
  • RemuxApp/Sources/Ghostty/GhosttyPaneScrollContainerView.swift
  • RemuxApp/Sources/Ghostty/GhosttySingleViewportView.swift
  • RemuxApp/Sources/Ghostty/GhosttySurfaceScreen.swift
  • RemuxApp/Sources/Ghostty/GhosttySurfaceSelectionSheet.swift
  • RemuxApp/Sources/Ghostty/GhosttyTerminalPresentationProjector.swift
  • RemuxApp/Sources/Ghostty/GhosttyTerminalScreenModeling.swift
  • RemuxApp/Sources/Ghostty/PanePreviewLayout.swift
  • RemuxApp/Sources/Ghostty/TerminalSelectionSheetStyle.swift
  • RemuxApp/Sources/Tmux/TmuxPanePreviewImageCache.swift
  • RemuxApp/Sources/Tmux/TmuxPaneSurface.swift
  • RemuxApp/Sources/Tmux/TmuxScreenModel.swift
  • RemuxApp/Sources/Tmux/TmuxSessionController.swift
  • RemuxApp/Sources/Tmux/TmuxSessionLink.swift
  • RemuxApp/Sources/Tmux/TmuxTerminalScreenAdapter.swift
  • RemuxApp/Sources/Tmux/TmuxTerminalSession.swift
  • RemuxAppTests/GhosttyKitControlSurfaceTests.swift
  • RemuxAppTests/GhosttyKitRuntimeTests.swift
  • RemuxAppTests/GhosttyPanePreviewSessionTests.swift
  • RemuxAppTests/GhosttyTerminalPresentationProjectorTests.swift
  • RemuxAppTests/PanePreviewLayoutTests.swift
  • RemuxAppTests/TmuxSessionControllerClientSizeTests.swift
  • RemuxAppTests/TmuxSessionLinkWriteFailureTests.swift
  • RemuxAppTests/TmuxTerminalScreenAdapterTests.swift
  • RemuxAppTests/TmuxTerminalSessionShutdownDrainTests.swift
  • RemuxAppUITests/RemuxAppUITests.swift
  • scripts/fetch_ghosttykit.sh
💤 Files with no reviewable changes (1)
  • RemuxAppTests/TmuxTerminalSessionShutdownDrainTests.swift

Comment thread RemuxApp/Sources/Ghostty/GhosttySurfaceSelectionSheet.swift Outdated
Comment on lines +730 to +732
let viewportPanesByID = Dictionary(
uniqueKeysWithValues: (viewport?.panes ?? []).map { ($0.id, $0) }
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Avoid a trapping dictionary initializer for viewport panes.

Dictionary(uniqueKeysWithValues:) traps at runtime if two entries in viewport.panes share an id. paneSelectionSheetRenderProjection is a static entry point that accepts any caller-supplied projection, so a duplicate pane identity crashes the sheet instead of degrading to a missing frame. Use Dictionary(_:uniquingKeysWith:) so the projection stays defined for every input.

🛡️ Proposed fix
-        let viewportPanesByID = Dictionary(
-            uniqueKeysWithValues: (viewport?.panes ?? []).map { ($0.id, $0) }
-        )
+        let viewportPanesByID = Dictionary(
+            (viewport?.panes ?? []).map { ($0.id, $0) },
+            uniquingKeysWith: { first, _ in first }
+        )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
let viewportPanesByID = Dictionary(
uniqueKeysWithValues: (viewport?.panes ?? []).map { ($0.id, $0) }
)
let viewportPanesByID = Dictionary(
(viewport?.panes ?? []).map { ($0.id, $0) },
uniquingKeysWith: { first, _ in first }
)
🤖 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/GhosttyTerminalPresentationProjector.swift` around
lines 730 - 732, Update the viewportPanesByID construction in
paneSelectionSheetRenderProjection to use the non-trapping Dictionary
initializer with a duplicate-key resolution closure, preserving one pane for
each ID instead of crashing when viewport.panes contains duplicates.

Comment on lines 256 to 258
private func handleActivePaneChanged(_ paneID: TmuxPaneID) {
guard paneSurface?.paneID == paneID else { return }
paneSurface?.refreshInteractionState()
surfacesByPaneID[paneID]?.refreshInteractionState()
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check whether the controller publishes topology alongside active-pane changes.
set -euo pipefail
rg -n -C 6 'onActivePaneChanged' RemuxApp/Sources/Tmux/TmuxSessionController.swift
rg -n -C 4 'activePaneID' RemuxApp/Sources/Tmux/TmuxSessionController.swift

Repository: h3nock/remux

Length of output: 7085


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate and inspect the relevant terminal session handling without running repository code.
printf '--- TmuxTerminalSession outline ---\n'
ast-grep outline RemuxApp/Sources/Tmux/TmuxTerminalSession.swift || true

printf '\n--- Relevant TmuxTerminalSession sections ---\n'
sed -n '1,140p' RemuxApp/Sources/Tmux/TmuxTerminalSession.swift
printf '\n--- Section around 200-290 ---\n'
sed -n '200,290p' RemuxApp/Sources/Tmux/TmuxTerminalSession.swift
printf '\n--- Section around 140-220 for reconcilePresentationActivity ---\n'
rg -n -C 12 'reconcilePresentationActivity|focus|display|activePaneID|topology' RemuxApp/Sources/Tmux/TmuxTerminalSession.swift

printf '\n--- Relevant TmuxSessionController topology callbacks ---\n'
sed -n '1280,1420p' RemuxApp/Sources/Tmux/TmuxSessionController.swift

Repository: h3nock/remux

Length of output: 241


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path
import re

controller = Path("RemuxApp/Sources/Tmux/TmuxSessionController.swift").read_text()
session = Path("RemuxApp/Sources/Tmux/TmuxTerminalSession.swift").read_text()

# Extract callback publication snippets around onTopology and onActivePaneChanged.
for name in ["onTopology", "onActivePaneChanged"]:
    print(f"--- {name} occurrences ---")
    for m in re.finditer(rf'self\.callbacks\.{name}[^;\n]*;', controller):
        start=max(0, controller.rfind("\n", 0, m.start()) - 1200)
        end=min(len(controller), controller.find("\n", m.end()) + 800)
        print(controller[start:end])
        print()

# Behavioral/data-shape check: whether each activePaneChanged publication site in refreshStateByPaneID
# is textually preceded by a topology callback publication in the same function.
for match in re.finditer(r'func handle.*refreshStateByPaneID\s*:', controller):
    func_start = match.start()
    func_end = controller.find("\n}\n", func_start)
    if func_end == -1:
        func_end = len(controller)
    body = controller[func_start:func_end]
    on_topology = [controller.count(s, 0, i) for i, s in enumerate(body) if s == "\n"]  # placeholder/no-op
    print("refreshStateByPaneID function section present:", bool(body))
    if body:
        print(body[:800])
        print("contains onTopology call:", bool(re.search(r'self\.callbacks\.onTopology\s*\(', body)))
        if "activePaneID(in: topology) == paneID" in body:
            print("contains activePaneID(in: topology) == paneID:", True)

print("Terminal session handles reference topology?", "topology" in session)
print("Terminal session sets focused from reconcilePresentationActivity?", "setFocused(true)" in session)
PY

Repository: h3nock/remux

Length of output: 343


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "--- TmuxTerminalSession outline ---"
ast-grep outline RemuxApp/Sources/Tmux/TmuxTerminalSession.swift || true

echo
echo "--- TmuxTerminalSession relevant sections ---"
sed -n '1,140p' RemuxApp/Sources/Tmux/TmuxTerminalSession.swift
sed -n '140,290p' RemuxApp/Sources/Tmux/TmuxTerminalSession.swift

echo
echo "--- TmuxSessionController active-pane callback publication sites ---"
rg -n -C 8 'callbacks\.onActivePaneChanged|callbacks\.onTopology' RemuxApp/Sources/Tmux/TmuxSessionController.swift

echo
echo "--- TmuxSessionController refreshStateByPaneID helper sections ---"
rg -n -C 25 'func handle.*refreshStateByPaneID|activePaneID\(in: topology\) == paneID|onTopology' RemuxApp/Sources/Tmux/TmuxSessionController.swift

Repository: h3nock/remux

Length of output: 21374


Reconcile focus in handleActivePaneChanged.

onActivePaneChanged is only used by handleActivePaneChanged, which refreshes interaction state. Display and keyboard focus are reconciled by reconcilePresentationActivity through handleTopology; if native active-pane events are not always accompanied by a topology callback, the currently focused pane stays focused without any focus recipient being told it stopped receiving focus.

🤖 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/TmuxTerminalSession.swift` around lines 256 - 258, The
handleActivePaneChanged method must reconcile presentation activity when the
native active-pane event occurs, rather than only refreshing the affected
surface’s interaction state. Update handleActivePaneChanged to invoke the
existing reconcilePresentationActivity flow (using the current topology/state
inputs) so display and keyboard focus are reassigned consistently even without a
topology callback.

Comment thread RemuxAppTests/TmuxSessionLinkWriteFailureTests.swift
Comment on lines +3709 to +3720
private func removePanePickerItem(_ tile: XCUIElement) {
let tilePrefix = "terminal.pane.tile."
XCTAssertTrue(tile.identifier.hasPrefix(tilePrefix))
let paneIdentifier = String(tile.identifier.dropFirst(tilePrefix.count))
XCTAssertFalse(paneIdentifier.isEmpty)

tile.press(forDuration: 1.0)
tapPickerButton(
identifier: "terminal.pane.remove.\(paneIdentifier)",
fallbackLabel: "Remove Pane"
)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Inspect pane removal identifiers, labels, and any confirmation control in the selection sheet.
set -uo pipefail

fd -t f 'GhosttySurfaceSelectionSheet.swift' --exec rg -n -C 6 'terminal\.pane\.(tile|remove)|Remove Pane|remove\.confirm' {} \;
rg -n -C 4 'terminal\.pane\.remove|terminal\.pane\.tile' --glob '*.swift'

Repository: h3nock/remux

Length of output: 3300


🏁 Script executed:

#!/bin/bash
# Read the relevant GhosttySurfaceSelectionSheet section without relying on fd's exit status.
set -u

file="$(fd -t f 'GhosttySurfaceSelectionSheet.swift' | head -n 1)"
if [ -n "${file:-}" ]; then
  sed -n '300,360p' "$file" | cat -n
  sed -n '255,284p' "$file" | cat -n
fi

Repository: h3nock/remux

Length of output: 5202


Add the pane removal confirmation tap.

The pane removal action button uses terminal.pane.remove.<pane id> and accessibility label Remove Pane, but it opens a confirmation dialog. Add a tap on terminal.pane.remove.confirm after tapping the removal action.

🤖 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 `@RemuxAppUITests/RemuxAppUITests.swift` around lines 3709 - 3720, The
removePanePickerItem helper currently stops after opening the pane removal
confirmation dialog. After tapPickerButton invokes terminal.pane.remove.<pane
id> or its fallback label, add a tap for terminal.pane.remove.confirm to
complete the confirmed removal.

@h3nock
h3nock merged commit 4d80b7d into main Aug 4, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant