Skip to content

Add manual SSH reconnect: in-pane 'r' prompt + Reconnect Pane menu - #7250

Merged
austinywang merged 18 commits into
manaflow-ai:mainfrom
0xJord4n:feat/manual-reconnect-ssh
Jul 6, 2026
Merged

austinywang merged 18 commits into
manaflow-ai:mainfrom
0xJord4n:feat/manual-reconnect-ssh

Conversation

@0xJord4n

@0xJord4n 0xJord4n commented Jul 3, 2026 •

Copy link
Copy Markdown
Contributor

What

Adds a way to manually reconnect a failed SSH pane, instead of only being able to close it.

Two entrypoints:

  1. In-pane r prompt — When the automatic reconnect loop gives up (or hits a non-retryable exit), the SSH hold prompt now offers r + Enter to re-run the connect loop with a fresh attempt counter. Enter / q / EOF still dismisses and closes the pane as before.

  2. "Reconnect Pane" context-menu item — The terminal pane's right-click menu gains a "Reconnect Pane" action (shown only for remote panes) that reconnects the focused surface via reconnectRemoteConnection(surfaceId:).

Why

Previously a dropped/paused/destroyed remote VM left the pane showing only "press Enter to close this pane" — the only recovery was to close and manually recreate it. This lets the user retry in place.

Changes

  • CLI/cmux.swift — wrap the reconnect loop + hold prompt in an outer session loop; r re-enters the connect loop.
  • Sources/GhosttyTerminalView.swift — "Reconnect Pane" item in menu(for:), gated on the surface belonging to a remote workspace.
  • Resources/Localizable.xcstrings — localize "Reconnect Pane" (en / ja / ko).
  • cmuxTests/SSHStartupSignalLifecycleTests.swift — testSSHStartupManualReconnectReentersConnectLoop (fake ssh fails once then succeeds; r\n re-enters the loop → status 0, 2 attempts, session-end once) plus updated banner-wording / session-end-once assertions.

Testing

  • xcodebuild test (cmux-unit scheme): both SSH-startup lifecycle tests pass (2 tests, 0 failures).
  • Built via tagged reload.sh; verified live — r + Enter reconnects, and the "Reconnect Pane" menu item reconnects the focused pane.

Notes

  • Uses the shared reconnectRemoteConnection action path (per cmux shared-behavior policy) — no duplicated reconnect logic per entrypoint.
  • Localization audit: "Reconnect Pane" added to Localizable.xcstrings for en/ja/ko (matching the existing terminalContextMenu.* sibling keys).

View with Codesmith Autofix with Codesmith
Need help on this PR? Tag /codesmith with what you need. Autofix is disabled.


Summary by cubic

Adds manual SSH reconnect so users can retry a failed remote pane without closing it. Press r then Enter in the pane or use the Reconnect Pane menu; only ended/inactive panes are eligible, and reconnects are blocked while the workspace is connecting or reconnecting.

  • New Features

    • In-pane prompt: press r then Enter (r/R/retry/reconnect) to re-run the SSH connect loop with a fresh attempt counter; Enter/EOF closes.
    • Context menu: new Reconnect Pane appears only for ended/inactive remote panes; sends "r" + Enter to trigger the in-pane reconnect.
    • Localization: added localized auto-reconnect note, manual reconnect prompt, and "Reconnect Pane".
  • Bug Fixes

    • Lifecycle: SSH session end is reported before the manual retry prompt; choosing r clears the ended flag and re-enters the loop; cleanup runs on final exit.
    • Guards: validate surface IDs; show/hide the menu correctly (hidden during connecting/reconnecting, allowed while connected); re-track ended panes before workspace-level guards so pane retries proceed even if the workspace is already connected or a reconnect is in flight; no-op if workspace/surface is missing.
    • Tests: fixed missing CmuxCore import and qualified static helpers in SSH manual reconnect tests.

Written for commit 7931e79. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • New Features

    • Added a terminal context menu option to reconnect a remote pane.
    • Improved SSH reconnect flow with clearer prompts and automatic retry handling.
  • Bug Fixes

    • SSH sessions now exit cleanly when the remote command finishes successfully.
    • Reconnect is now limited to eligible remote panes and avoids duplicate reconnect attempts.

- SSH startup wrapper: when the auto-reconnect loop gives up, the hold
  prompt now offers 'r' + Enter to re-run the connect loop (fresh attempt
  counter) instead of only closing the pane. Enter/q/EOF dismisses.
- Terminal pane right-click menu: add 'Reconnect Pane' (remote panes only)
  that reconnects the focused surface via reconnectRemoteConnection(surfaceId:).
- Localize 'Reconnect Pane' (en/ja/ko).
- Tests: manual-retry re-enters connect loop; session-end runs once.
@vercel

vercel Bot commented Jul 3, 2026

Copy link
Copy Markdown

@0xJord4n is attempting to deploy a commit to the Manaflow Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai

coderabbitai Bot commented Jul 3, 2026 •

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The generated SSH startup script now loops on reconnect: successful SSH exits break immediately, while failures prompt for retry (r/reconnect) via a new bash helper invoking a workspace.remote.reconnect RPC. A "Reconnect Pane" context menu item and matching Workspace gating logic were added, with localization strings and tests.

Changes

Manual SSH pane reconnect

Layer / File(s) Summary
Session loop and manual retry prompt
CLI/cmux.swift
Wraps reconnect logic in an outer session loop resetting retry state, breaks immediately on successful SSH exit, and on failure prompts for a dismiss/retry key that either re-enters the loop via cmux_ssh_remote_reconnect or tears down the session.
Reconnect RPC shell helper and localized prompts
CLI/CMUXCLI+SSHReconnectPrompt.swift
Adds localized ANSI-colored auto-reconnect/manual-reconnect prompt formatters and a bash function generator that resolves the CLI path and invokes workspace.remote.reconnect with workspace/surface IDs.
Reconnect Pane menu action
Sources/GhosttyNSView+RemoteReconnect.swift, Sources/GhosttyTerminalView.swift, Sources/Workspace.swift, Resources/Localizable.xcstrings
Adds a conditional "Reconnect Pane" context menu item gated by remote workspace/connection state and placeholder panel lookup, removes the obsolete reset-terminal handler, tightens reconnectRemoteConnection gating, and adds related localization strings.
Xcode project wiring
cmux.xcodeproj/project.pbxproj
Registers the new reconnect prompt, remote reconnect menu, and test source files across build phases and groups.
Reconnect behavior test coverage
cmuxTests/SSHStartupManualReconnectTests.swift
Adds an integration test simulating manual retry against mock cmux/ssh scripts and a JSON-RPC server, plus Workspace-level reconnect unit tests and supporting subprocess/socket/JSON utilities.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant cmuxSwift as cmux.swift generated script
  participant SSH
  participant ReconnectHelper as cmux_ssh_remote_reconnect (RPC helper)

  cmuxSwift->>SSH: start background SSH command
  SSH-->>cmuxSwift: exit status
  alt status == 0
    cmuxSwift->>cmuxSwift: break session loop
  else non-zero exit
    cmuxSwift->>User: show manual reconnect prompt
    alt user enters r/reconnect
      cmuxSwift->>ReconnectHelper: cmux_ssh_remote_reconnect
      ReconnectHelper->>ReconnectHelper: rpc workspace.remote.reconnect
      cmuxSwift->>cmuxSwift: reset retry counter, re-enter outer loop
    else user dismisses
      cmuxSwift->>cmuxSwift: cmux_ssh_session_end teardown, exit
    end
  end
Loading
sequenceDiagram
  participant User
  participant GhosttyNSView
  participant Workspace
  participant TerminalPanel

  User->>GhosttyNSView: right-click terminal
  GhosttyNSView->>Workspace: remoteWorkspaceForCurrentSurface()
  GhosttyNSView->>Workspace: canReconnectRemotePane(state)
  GhosttyNSView->>TerminalPanel: remoteReconnectablePanel(in:)
  GhosttyNSView-->>User: show "Reconnect Pane" menu item
  User->>GhosttyNSView: select Reconnect Pane
  GhosttyNSView->>Workspace: re-validate reconnect eligibility
  GhosttyNSView->>TerminalPanel: send input "r\r"
Loading

Possibly related issues

Possibly related PRs

  • manaflow-ai/cmux#3995: Both PRs modify SSH lifecycle/signal handling in CLI/cmux.swift, overlapping in session end/trap wiring paths.

Important

Pre-merge checks failed

Please resolve all errors before merging. Addressing warnings is optional.

❌ Failed checks (1 error, 1 inconclusive)

Check name Status Explanation Resolution
Cmux Architecture Rethink ❌ Error The new AppKit menu path injects r\\r into the terminal instead of calling Workspace.reconnectRemoteConnection, splitting reconnect ownership across a view side-channel and the workspace model. Route the menu action through the workspace reconnect API (the existing source of truth) and keep the shell prompt as the only keystroke-based entrypoint.
Cmux Swift @Concurrent ❓ Inconclusive The accessible diff only shows submodule pointer updates; the Swift files from the PR aren’t present to verify @concurrent usage. Provide the actual Swift-file diff or checkout the PR commit so the concurrency-annotation rule can be checked against the changed code.
✅ Passed checks (23 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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.
Cmux Swift Actor Isolation ✅ Passed No new Swift 6 isolation debt: the new UI code stays on the NSView/Workspace main-actor path, and the CLI helper is on a plain struct.
Cmux Swift Blocking Runtime ✅ Passed PASS: The reconnect PR adds no new Swift semaphores/locks/sleeps; blocking waits are in tests only, and the shell prompt already had read/sleep before the feature.
Cmux Browser Automation Off-Main ✅ Passed PASS: The actual submodule diff is only in Zig files (include/ghostty.h, src/Surface.zig, src/apprt/embedded.zig); no browser.* routing or policy-test files changed.
Cmux Expensive Synchronous Load ✅ Passed The PR only adds reconnect UI/flow code; no agent-history loader, transcript parse, or SharedLiveAgentIndex call is introduced on main/interactive paths.
Cmux Cache Substitution Correctness ✅ Passed No stale-cache swap in a persistence/history/snapshot path; new cached cwd/stop-state paths are freshness-checked or documented and cleaned up.
Cmux No Hacky Sleeps ✅ Passed No new fixed sleep/delay was introduced; the only sleep is the existing reconnect backoff in generated shell, and new tests use bounded timeouts only.
Cmux Algorithmic Complexity ✅ Passed New code adds one linear tab lookup plus O(1) set/dictionary checks; no nested rescans, repeated filtering/sorting, or batch rescans in hot paths.
Cmux Swift Concurrency ✅ Passed PASS: No new production background queues, Combine state, completion-handler APIs, or fire-and-forget Tasks; only an AppKit main-queue hop and test-only sync scaffolding appear.
Cmux Swift File And Package Boundaries ✅ Passed Touched production Swift changes are small glue/focused fixes; the only 462-line new file is test code, and oversized app files were already huge with tiny additions.
Cmux Swiftpm Lockfiles ✅ Passed PASS: ios/cmuxPackage/Package.swift’s new dependency is paired with ios/cmuxPackage/Package.resolved, and cmux.xcodeproj diff only adds sources, not package refs.
Cmux Swift Logging ✅ Passed No new runtime Swift logging violations: app sources add only menu/reconnect plumbing, and the CLI change emits intended user-facing SSH prompts.
Cmux User-Facing Error Privacy ✅ Passed New user-facing copy stays generic (“ssh exited…”, “Reconnect Pane”) and the reconnect helper suppresses internal env/details; no forbidden leakage found.
Cmux Full Internationalization ✅ Passed New UI text uses String(localized:defaultValue:) and all 6 new keys have translations for every existing locale in Localizable.xcstrings.
Cmux Swiftui State Layout ✅ Passed No new SwiftUI state/layout patterns were introduced; the reconnect UI is in AppKit bridge code and CLI/tests, with no GeometryReader, lazy rows, or render-time state writes.
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed PR only changes CLI, menu, localization, and tests; no new or materially changed standalone NSWindow/NSPanel/NSWindowController/WindowGroup code or cmuxAuxiliaryWindowIdentifiers wiring.
Cmux Source Artifacts ✅ Passed Changed paths are source/test/localization/project files and submodule pointers; no logs, caches, build output, or scratch dirs are introduced.
Cmux No Test Or Debug Seam In Production Source ✅ Passed PASS: The touched Sources files add only production reconnect UI/script helpers; no new #if DEBUG or *ForTesting/*debug seam was added, and the menu helper has a production caller.
Cmux No Ambient Global State ✅ Passed The new reconnect helpers live on CMUXCLI/GhosttyNSView extensions as instance methods; no new file-scope API, mutable global, or singleton state was introduced.
Title check ✅ Passed The title is concise and clearly describes the main change: manual SSH reconnect via an in-pane prompt and Reconnect Pane menu.
Description check ✅ Passed The description includes the change summary, motivation, implementation notes, and testing details, so it covers the required content well.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@greptile-apps

greptile-apps Bot commented Jul 3, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds a manual SSH reconnect mechanism with two entry points: an in-pane r + Enter prompt that replaces the existing "press Enter to close" hold, and a "Reconnect Pane" context-menu item. The shell startup script is restructured into an outer session loop that gates the manual-reconnect prompt, while Workspace.reconnectRemoteConnection is updated to also accept surfaces tracked in pendingRemoteTerminalChildExitSurfaceIds and to guard against triggering a workspace-level reconnect when the workspace is already .connected or already mid-reconnect.

  • The shell script wraps the existing auto-retry inner loop in an outer session loop; after exhausting retries (or a non-retryable exit), it shows the manual prompt and either dismisses or resets CMUX_SSH_SESSION_ENDED and continues the outer loop to start a fresh inner loop.
  • GhosttyNSView+RemoteReconnect.swift adds the context-menu action gated on remoteReconnectablePanel(in:) — requiring membership in the "ended" surface-ID sets — and sends "r\r" directly into the pane, keeping the in-pane prompt as the single reconnect code path.
  • Localization for all new user-visible strings is complete across all 20 supported locales.

Confidence Score: 5/5

Safe to merge — the new reconnect paths are well-guarded, thoroughly tested, and localized across all supported locales.

The outer session-loop restructure in the shell script, the reconnectRemoteConnection guard additions, and the remoteReconnectablePanel gate in the context-menu extension all work correctly and are validated by four new integration tests. Signal handling during read degrades to the same close-pane behavior as before this PR. The pendingRemoteTerminalChildExitSurfaceIds cleanup goes through trackRemoteTerminalSurface, which removes the ID as confirmed in the source. No correctness-critical paths are left unguarded.

Sources/Workspace.swift — the new reconnectRemoteConnection one-liner is the only code that benefits from a second look, purely for future maintainability.

Important Files Changed

Filename Overview
CLI/cmux.swift Outer session loop added around the auto-retry inner loop; manual reconnect prompt replaces the old hold prompt; dead cmux_ssh_retry=0 initializer correctly removed
CLI/CMUXCLI+SSHReconnectPrompt.swift New file extracting SSH prompt format strings (fully localized) and the cmux_ssh_remote_reconnect shell function; all three string keys use String(localized:defaultValue:) correctly
Sources/GhosttyNSView+RemoteReconnect.swift New extension adding the Reconnect Pane context-menu item; correctly gated on surface membership in ended-pane sets; sends r\r to delegate reconnect to the in-pane prompt
Sources/Workspace.swift reconnectRemoteConnection now also accepts surfaces from pendingRemoteTerminalChildExitSurfaceIds, adds guards for already-connected/connecting/reconnecting states; the new conditional logic is correct but the long flatMap one-liner reduces readability
Sources/GhosttyTerminalView.swift Single-line addition calling appendReconnectRemotePaneMenuItem at the correct position in the context-menu builder
Resources/Localizable.xcstrings Adds five new localization keys across 20 locales — complete and consistent with existing sibling keys
cmuxTests/SSHStartupManualReconnectTests.swift Four new tests covering manual reconnect re-entry, rejection of non-ended surface IDs, connected-workspace pane retry, and in-flight reconnect deferral

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant PTY as Terminal PTY
    participant Shell as SSH Shell Script
    participant App as cmux App (Workspace)
    participant SSH as ssh process

    Shell->>SSH: "launch ssh <&0 &"
    SSH-->>Shell: exit non-zero (non-retryable)
    Shell->>App: cmux_ssh_session_end (RPC: ssh-session-end)
    Note over App: surface → pendingRemoteTerminalChildExitSurfaceIds
    Shell->>PTY: printf manual reconnect prompt
    
    alt User presses r + Enter (in-pane)
        PTY->>Shell: r\n via stdin read
        Shell->>App: cmux_ssh_remote_reconnect (RPC: workspace.remote.reconnect)
        App->>App: reconnectRemoteConnection(surfaceId:)
        Shell->>Shell: "CMUX_SSH_SESSION_ENDED=0 && continue outer loop"
        Shell->>SSH: launch ssh again (fresh inner loop)
        SSH-->>Shell: exit 0 (success)
        Shell->>App: cmux_ssh_session_end (2nd call)
    else User presses Enter / EOF
        PTY->>Shell: empty line or EOF
        Shell->>Shell: break → exit $cmux_ssh_status
    else Context-menu Reconnect Pane
        App->>App: remoteReconnectablePanel() guards check
        App->>PTY: panel.sendInput(r\r)
        PTY->>Shell: r\n via stdin read
        Note over Shell,App: same r+Enter path as above
    end
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant PTY as Terminal PTY
    participant Shell as SSH Shell Script
    participant App as cmux App (Workspace)
    participant SSH as ssh process

    Shell->>SSH: "launch ssh <&0 &"
    SSH-->>Shell: exit non-zero (non-retryable)
    Shell->>App: cmux_ssh_session_end (RPC: ssh-session-end)
    Note over App: surface → pendingRemoteTerminalChildExitSurfaceIds
    Shell->>PTY: printf manual reconnect prompt
    
    alt User presses r + Enter (in-pane)
        PTY->>Shell: r\n via stdin read
        Shell->>App: cmux_ssh_remote_reconnect (RPC: workspace.remote.reconnect)
        App->>App: reconnectRemoteConnection(surfaceId:)
        Shell->>Shell: "CMUX_SSH_SESSION_ENDED=0 && continue outer loop"
        Shell->>SSH: launch ssh again (fresh inner loop)
        SSH-->>Shell: exit 0 (success)
        Shell->>App: cmux_ssh_session_end (2nd call)
    else User presses Enter / EOF
        PTY->>Shell: empty line or EOF
        Shell->>Shell: break → exit $cmux_ssh_status
    else Context-menu Reconnect Pane
        App->>App: remoteReconnectablePanel() guards check
        App->>PTY: panel.sendInput(r\r)
        PTY->>Shell: r\n via stdin read
        Note over Shell,App: same r+Enter path as above
    end
Loading

Reviews (15): Last reviewed commit: "Qualify static test helpers in SSH manua..." | Re-trigger Greptile

Comment thread Sources/GhosttyTerminalView.swift Outdated

@cubic-dev-ai cubic-dev-ai 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.

No issues found across 4 files

Re-trigger cubic

0xJord4n and others added 2 commits July 3, 2026 16:46
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
The outer session loop resets cmux_ssh_retry=0 unconditionally at the top
of every iteration (including the first), so the pre-loop initializer was
dead code introduced by the manual-reconnect change. Flagged in PR review.
@vercel

vercel Bot commented Jul 3, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
cmux Ready Ready Preview, Comment Jul 4, 2026 4:43am

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

Caution

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

⚠️ Outside diff range comments (1)
Sources/Workspace.swift (1)

5892-5906: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Placeholder/tracking side effects run before the new connecting/reconnecting guard, leaving state desynced on early return.

reconnectRemoteConnection(surfaceId:) removes the surface from remoteDisconnectPlaceholderPanelIds and calls trackRemoteTerminalSurface(_:) unconditionally (lines 5900-5903) before the guard at line 5904 checks remoteConnectionState. The PR's stated intent for widening this guard is to prevent reconnect during an active attempt (e.g. user clicks "Reconnect Pane" while an automatic retry is already .reconnecting/.connecting) — meaning this early-return path is now expected to be hit in real usage, not just as a defensive/unreachable check.

When that happens, the surface is already unmarked as a disconnect placeholder and marked as a tracked/live remote terminal surface, even though configureRemoteConnection was never called and no actual reconnect occurred. This leaves the pane's placeholder/tracking state out of sync with the real connection state.

Move the guard to the top of the function so the placeholder/tracking mutation only happens when a reconnect will actually be attempted.

🐛 Proposed fix: check the guard before mutating placeholder/tracking state
     func reconnectRemoteConnection(surfaceId: UUID? = nil) {
+        guard let configuration = remoteConfiguration,
+              remoteConnectionState != .connecting,
+              remoteConnectionState != .reconnecting else { return }
         let reconnectingPlaceholderSurfaceId = surfaceId.flatMap { candidate -> UUID? in
             guard remoteDisconnectPlaceholderPanelIds.contains(candidate),
                   panels[candidate] is TerminalPanel else {
                 return nil
             }
             return candidate
         }
         if let reconnectingPlaceholderSurfaceId {
             remoteDisconnectPlaceholderPanelIds.remove(reconnectingPlaceholderSurfaceId)
             trackRemoteTerminalSurface(reconnectingPlaceholderSurfaceId)
         }
-        guard let configuration = remoteConfiguration, remoteConnectionState != .connecting, remoteConnectionState != .reconnecting else { return }
         configureRemoteConnection(configuration, autoConnect: true)
     }
🤖 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 `@Sources/Workspace.swift` around lines 5892 - 5906, In
reconnectRemoteConnection(surfaceId:), the placeholder/tracking mutation happens
before the new remoteConnectionState guard, so early returns can desync state.
Move the guard that blocks .connecting and .reconnecting to the top of the
function, before removing from remoteDisconnectPlaceholderPanelIds or calling
trackRemoteTerminalSurface(_:), so those side effects only occur when
configureRemoteConnection(_:autoConnect:) will actually run.
🤖 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 `@Sources/GhosttyNSView`+RemoteReconnect.swift:
- Around line 45-50: The reconnect action in reconnectRemotePane(_:) is
currently sending a bare Enter through remoteReconnectPlaceholderPanel, which
only follows the dismiss flow instead of manually reconnecting. Update this path
to invoke the same reconnect helper used elsewhere in the remote reconnect flow,
using the workspace/panel context already gathered, so the menu item actually
triggers pane reconnection rather than submitting an empty line.

---

Outside diff comments:
In `@Sources/Workspace.swift`:
- Around line 5892-5906: In reconnectRemoteConnection(surfaceId:), the
placeholder/tracking mutation happens before the new remoteConnectionState
guard, so early returns can desync state. Move the guard that blocks .connecting
and .reconnecting to the top of the function, before removing from
remoteDisconnectPlaceholderPanelIds or calling trackRemoteTerminalSurface(_:),
so those side effects only occur when configureRemoteConnection(_:autoConnect:)
will actually run.
🪄 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: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 4e120679-4d67-41f0-8665-c05dd5aac56e

📥 Commits

Reviewing files that changed from the base of the PR and between 2f0d3cb and b7242f3.

📒 Files selected for processing (9)
  • CLI/CMUXCLI+SSHReconnectPrompt.swift
  • CLI/cmux.swift
  • Resources/Localizable.xcstrings
  • Sources/GhosttyNSView+RemoteReconnect.swift
  • Sources/GhosttyTerminalView.swift
  • Sources/Workspace.swift
  • cmux.xcodeproj/project.pbxproj
  • cmuxTests/SSHStartupManualReconnectTests.swift
  • cmuxTests/SSHStartupSignalLifecycleTests.swift

Comment thread Sources/GhosttyNSView+RemoteReconnect.swift
@greptile-apps

greptile-apps Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Want your agent to iterate on Greptile's feedback? Try greploops.

guard let workspace = remoteWorkspaceForCurrentSurface(),
canReconnectRemotePane(in: workspace),
let panel = remoteReconnectPlaceholderPanel(in: workspace) else { return }
panel.sendInput("\r")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 The "Reconnect Pane" menu item sends " " (a bare Enter/carriage-return) to the panel. The shell's IFS= read -r _cmux_dismiss_key waits for a newline-terminated line; with ICRNL active, is translated to , so _cmux_dismiss_key is set to the empty string. The case in the shell script only matches r|R|retry|reconnect — an empty string falls through, hits the break, and the pane closes without reconnecting. The test correctly feeds "r " to reproduce the reconnect path; the menu item needs to send "r" followed by a line-ending to match. Using "r " triggers the same ICRNL translation (r + Enter) that the keyboard path uses.

Suggested change
panel.sendInput("\r")
panel.sendInput("r\r")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Fixed — the menu item now sends "r\r" (commit dc81c14 "Send reconnect input from pane menu"), which takes the same r + Enter path as typing in the pane. manualReconnectReentersConnectLoop covers the resulting reconnect re-entry end-to-end (fake ssh + fake CLI, asserting the second connect attempt and the session-end accounting).

austinywang and others added 2 commits July 5, 2026 16:38
Pin the deliberate mutation-before-guard semantics of
reconnectRemoteConnection(surfaceId:): a valid ended pane is re-tracked
even when the workspace-level reconfigure is skipped (workspace already
connected, or a reconnect already in flight), because the pane's shell
loop re-runs ssh regardless of which internal path the RPC took.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@austinywang

Copy link
Copy Markdown
Contributor

Re the CodeRabbit outside-diff finding on Sources/Workspace.swift reconnectRemoteConnection(surfaceId:) (placeholder/tracking mutation before the connecting/reconnecting guard):

The ordering is deliberate. The pane's shell loop re-runs ssh regardless of which internal path the RPC took (cmux_ssh_remote_reconnect && CMUX_SSH_SESSION_ENDED=0 && continue), so the app must re-track the pane as a live remote surface even when it skips the workspace-level configureRemoteConnection (workspace already connected, or a reconnect already in flight). Hoisting the guard above the mutation would leave a pane actively re-running ssh while still marked as a disconnect placeholder — desynced in the opposite direction.

Invalid/unended surface ids still return before any mutation (if surfaceId != nil, reconnectingSurfaceId == nil { return }), covered by reconnectRejectsUnendedTerminalSurfaceId. The two cases this finding is about are now pinned by dedicated tests in SSHStartupManualReconnectTests (added in 221a444): reconnectKeepsConnectedWorkspaceForEndedPaneRetry and reconnectDefersToInFlightReconnectForEndedPaneRetry — both assert the pane is re-tracked and the workspace connection state is left untouched.

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

Caution

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

⚠️ Outside diff range comments (1)
CLI/cmux.swift (1)

9829-9838: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Keep the reconnect prompt open on reconnect RPC failure. cmux_ssh_remote_reconnect() already returns success when it can’t even attempt a reconnect, so the only failure path here is an actual workspace.remote.reconnect error. In that case the r branch still falls through to break, closing the pane without telling the user the reconnect failed. Re-prompt instead.

🤖 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 `@CLI/cmux.swift` around lines 9829 - 9838, The reconnect prompt flow in the
cmux SSH loop should stay open when `cmux_ssh_remote_reconnect()` fails on an
actual `workspace.remote.reconnect` error. Update the `r|R|retry|reconnect`
branch in the SSH reconnect prompt logic to detect a failed reconnect attempt
and re-display the prompt instead of falling through to `break`, while keeping
the existing success path that clears `CMUX_SSH_SESSION_ENDED`.
🤖 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.

Outside diff comments:
In `@CLI/cmux.swift`:
- Around line 9829-9838: The reconnect prompt flow in the cmux SSH loop should
stay open when `cmux_ssh_remote_reconnect()` fails on an actual
`workspace.remote.reconnect` error. Update the `r|R|retry|reconnect` branch in
the SSH reconnect prompt logic to detect a failed reconnect attempt and
re-display the prompt instead of falling through to `break`, while keeping the
existing success path that clears `CMUX_SSH_SESSION_ENDED`.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 233468cf-bd68-4c62-8132-cd0581926e28

📥 Commits

Reviewing files that changed from the base of the PR and between b7242f3 and 221a444.

📒 Files selected for processing (8)
  • CLI/CMUXCLI+SSHReconnectPrompt.swift
  • CLI/cmux.swift
  • Resources/Localizable.xcstrings
  • Sources/GhosttyNSView+RemoteReconnect.swift
  • Sources/GhosttyTerminalView.swift
  • Sources/Workspace.swift
  • cmux.xcodeproj/project.pbxproj
  • cmuxTests/SSHStartupManualReconnectTests.swift
💤 Files with no reviewable changes (1)
  • cmuxTests/SSHStartupManualReconnectTests.swift

austinywang and others added 2 commits July 5, 2026 17:15
WorkspaceRemoteConfiguration is a CmuxCore type; the suite referenced it
without importing the package. Earlier PR CI runs were cancelled before
this file ever compiled, so the first full app-host compile surfaced it
across all four shards.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
writeShellFile and runProcess are private static helpers called from the
instance-context @test func; Swift requires Self-qualification there.
Latent since the suite predates any completed CI compile of this file.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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.

2 participants