Skip to content

Load existing tmux sessions in the session switcher - #48

Merged
h3nock merged 17 commits into
h3nock:mainfrom
vaayne:sessions/load-existing-tmux-sessions
Aug 12, 2026
Merged

Load existing tmux sessions in the session switcher#48
h3nock merged 17 commits into
h3nock:mainfrom
vaayne:sessions/load-existing-tmux-sessions

Conversation

@vaayne

@vaayne vaayne commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Summary

  • discover existing tmux sessions across saved servers using their configured SSH connections
  • organize the Sessions sheet into Active, Recent, and Available sections
  • keep Recent compact by default with View all and Show fewer controls
  • resume Recent sessions through the existing saved-session connection path
  • attach to discovered Available sessions without requiring users to remember or type their names
  • provide a searchable Available Sessions browser grouped by server
  • refresh discovery from the Sessions sheet while keeping failures isolated to the affected server
  • retry discovery after a previously unreachable server successfully reconnects
  • reconcile Recent sessions against successful discovery so remotely deleted sessions no longer remain visible
  • preserve the existing resizable sheet behavior, pinned New Session action, and terminal chrome

Discovery uses a read-only SSH exec channel to run tmux list-sessions. It does not attach to, resize, switch, create, or otherwise mutate remote tmux sessions.

Testing

  • focused discovery recovery and request-coalescing tests on a physical iPhone
  • signed physical-device build
  • live SSH session-switcher flow covering discovery, refresh, Available search, attaching to a discovered session, Recent session resumption, and Active/Recent reconciliation

Screenshots

Sessions overview Available sessions Session search

Summary by CodeRabbit

  • New Features

    • Added a session switcher for active, recent, and remotely available tmux sessions.
    • Discover available tmux sessions automatically and connect to them directly.
    • Added session search, refresh, server grouping, creation, resumption, and disconnection controls.
    • Added discovery status and error feedback, with workspace preservation during temporary failures.
    • Improved accessibility guidance and session status details.
  • Bug Fixes

    • Improved session ordering, filtering, duplicate handling, and refresh cancellation.
  • Tests

    • Added comprehensive coverage for discovery, switching, failures, cancellation, and live remote session workflows.

@coderabbitai

coderabbitai Bot commented Aug 3, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 95b7c1ba-32ad-412d-a5c3-a76413e42f37

📥 Commits

Reviewing files that changed from the base of the PR and between faefed5 and ef48f04.

📒 Files selected for processing (6)
  • RemuxApp/Sources/App/RemuxRootModel.swift
  • RemuxApp/Sources/App/SessionSwitcherView.swift
  • RemuxApp/Sources/Ghostty/GhosttyKeyboardChrome.swift
  • RemuxApp/Sources/Ghostty/TerminalSelectionSheetStyle.swift
  • RemuxAppTests/SessionSwitcherProjectionTests.swift
  • RemuxAppUITests/RemuxAppUITests.swift
🚧 Files skipped from review as they are similar to previous changes (6)
  • RemuxApp/Sources/Ghostty/GhosttyKeyboardChrome.swift
  • RemuxApp/Sources/Ghostty/TerminalSelectionSheetStyle.swift
  • RemuxApp/Sources/App/SessionSwitcherView.swift
  • RemuxAppUITests/RemuxAppUITests.swift
  • RemuxAppTests/SessionSwitcherProjectionTests.swift
  • RemuxApp/Sources/App/RemuxRootModel.swift

📝 Walkthrough

Walkthrough

The app adds SSH-based tmux session discovery, per-server refresh coordination, workspace reconciliation, and a new session switcher for active, recent, and discovered sessions. It also updates project wiring, accessibility text, terminal-sheet styling, sorting, unit tests, and UI tests.

Changes

Tmux session discovery

Layer / File(s) Summary
Discovery command and parsing
RemuxApp/Sources/SSH/TmuxSessionDiscovery.swift, RemuxApp/Sources/Tmux/SSHTmuxControlCommandBuilder.swift, RemuxAppTests/TmuxSessionDiscoveryTests.swift
Builds a shell-safe tmux listing command and parses valid remote output into ordered, unique session names.
Workspace reconciliation and live dependency wiring
RemuxApp/Sources/App/RemuxAppDependencies.swift, RemuxApp/Sources/Persistence/ConnectionProfileRepository.swift, RemuxApp/Sources/App/RemuxRootModel.swift
Injects live discovery, performs SSH setup and cleanup, centralizes workspace sorting, and reconciles discovered sessions with workspace state.
Per-server refresh orchestration
RemuxApp/Sources/App/RemuxRootModel.swift, RemuxAppTests/RemuxRootModelTests.swift
Adds per-server discovery state, refresh coalescing, cancellation, stale-result rejection, retry handling, and discovered-session connections.
Session switcher integration and projection
RemuxApp/Sources/App/SessionSwitcherView.swift, RemuxApp/Sources/App/RootView.swift, RemuxAppTests/SessionSwitcherProjectionTests.swift, RemuxAppUITests/RemuxAppUITests.swift
Replaces the active-session switcher with a UI for active, recent, and available sessions, including search, refresh, server selection, accessibility, and live SSH coverage.

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

Possibly related PRs

  • h3nock/remux#8: Shares SSH, dependency, root-model, and tmux infrastructure changes.
  • h3nock/remux#21: Provides the active session switcher that this change replaces and extends.
  • h3nock/remux#59: Shares session-switcher, recent-workspace, projection, repository, and UI-test changes.

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant SessionSwitcherView
  participant RemuxRootModel
  participant RemuxAppDependencies
  participant TmuxSessionDiscovery
  participant SSH
  User->>SessionSwitcherView: Refresh available sessions
  SessionSwitcherView->>RemuxRootModel: refreshTmuxSessions(for:)
  RemuxRootModel->>RemuxAppDependencies: discoverTmuxSessions(for:)
  RemuxAppDependencies->>TmuxSessionDiscovery: discover sessions
  TmuxSessionDiscovery->>SSH: Execute tmux list-sessions command
  SSH-->>TmuxSessionDiscovery: Return session output
  TmuxSessionDiscovery-->>RemuxRootModel: Return parsed session names
  RemuxRootModel-->>SessionSwitcherView: Publish discovery state and session rows
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% 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 and concisely describes the main change: discovering and displaying existing tmux sessions in the session switcher.
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
🧪 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.

@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: 896c551248

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

.filter { $0.target.server.id == serverID }
var includedNames = Set<String>()
var items = activeSessions.compactMap { session -> ActiveSessionSwitcherItem? in
guard includedNames.insert(session.target.workspace.sessionName).inserted else { return 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 every active runtime in the switcher

When two saved workspaces on the same server use the same tmux session name, this name-based deduplication drops one active runtime even though activation and persistence distinguish them by workspace ID and the workspace validator permits duplicate names. In particular, if the older duplicate is currently selected, the more recently opened runtime is retained by the display sort and the selected runtime disappears from the sheet, so it cannot be selected or disconnected there. Deduplicate only discovered available sessions, or otherwise ensure every active workspace ID remains represented.

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: 2

🧹 Nitpick comments (9)
RemuxApp/Sources/App/RemuxAppDependencies.swift (1)

495-520: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicates the reconciliation logic from ConnectionProfileRepository.swift.

This is the same check-dedupe-append logic as FileBackedConnectionProfileRepository.reconcileDiscoveredWorkspaces. See the consolidated comment for the shared fix.

🤖 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 495 - 520,
Remove the duplicated check-dedupe-append implementation from
reconcileDiscoveredWorkspaces and reuse the existing reconciliation logic in
FileBackedConnectionProfileRepository, preserving server validation and snapshot
loading behavior.
RemuxApp/Sources/Tmux/SSHTmuxControlCommandBuilder.swift (1)

23-56: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extract the shared script preamble to avoid divergence.

discoveryScript duplicates most of launchScript: the PATH fallback line, the tmux=$(printf %b "$1") resolution, and the not-found/not-executable marker lines are identical between the two arrays. Only the TERM export, the session argument, and the final exec line differ.

Extract the shared lines into one place, parameterized by the final exec statement, so a future change to PATH handling or marker text does not need to be duplicated correctly in two scripts.

♻️ Example refactor
+    private static func resolutionScript(execLine: String, includeTerm: Bool) -> [String] {
+        var lines = [
+            #"PATH="${PATH:+$PATH:}\#(fallbackRemotePath)""#,
+            "export PATH",
+        ]
+        if includeTerm {
+            lines += ["TERM=xterm-256color", "export TERM"]
+        }
+        lines += [
+            #"tmux=$(printf %b "$1")"#,
+        ]
+        return lines
+    }

Compose launchScript and discoveryScript from this shared helper plus their distinct resolution/exec lines and the common not-found/not-executable tail.

🤖 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/SSHTmuxControlCommandBuilder.swift` around lines 23 -
56, Extract the duplicated PATH setup, tmux argument resolution, and
executable/not-found marker handling from launchScript and discoveryScript into
a shared helper parameterized by the command-specific final exec portion.
Rebuild both scripts using that helper while preserving launchScript’s
TERM/session behavior and discoveryScript’s list-sessions behavior.
RemuxApp/Sources/Persistence/ConnectionProfileRepository.swift (1)

172-204: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Reconciliation logic is duplicated across three repository implementations.

This same reconcile-and-dedupe logic (check server exists, filter empty names, dedupe against existing and newly-added names, append with .distantPast) is repeated almost verbatim in RemuxAppDependencies.swift's InMemoryConnectionProfileRepository and in DebugConnectionProfileSeederTests.swift's test double. See the consolidated comment for the shared fix.

🤖 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/Persistence/ConnectionProfileRepository.swift` around lines
172 - 204, Consolidate the reconcile-and-dedupe behavior used by
ConnectionProfileRepository.reconcileDiscoveredWorkspaces into one shared
implementation, and update InMemoryConnectionProfileRepository and the
DebugConnectionProfileSeederTests test double to reuse it. Preserve server
validation, empty-name filtering, deduplication against existing and newly
discovered names, and .distantPast timestamps.
RemuxAppTests/DebugConnectionProfileSeederTests.swift (1)

151-176: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicates the reconciliation logic from ConnectionProfileRepository.swift.

This test double repeats the same check-dedupe-append logic present in FileBackedConnectionProfileRepository and the InMemoryConnectionProfileRepository in RemuxAppDependencies.swift. See the consolidated comment for the shared fix.

🤖 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/DebugConnectionProfileSeederTests.swift` around lines 151 -
176, Update reconcileDiscoveredWorkspaces in the test double to reuse the shared
reconciliation implementation from ConnectionProfileRepository.swift or the
existing repository helper instead of duplicating the server validation,
deduplication, and append logic. Preserve the current snapshot-loading behavior
and repository contract while removing the repeated implementation.
RemuxAppTests/TmuxSessionDiscoveryTests.swift (1)

5-34: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Add coverage for discover()'s exit-status handling.

This file tests parseSessionNames and the command builder, but no test calls TmuxSessionDiscovery.discover itself. Add a test for the zero-sessions case once the fix in TmuxSessionDiscovery.swift lands, and a test confirming a genuine remote failure still throws TmuxSessionDiscoveryError.remoteExit. See the consolidated comment for details.

🤖 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/TmuxSessionDiscoveryTests.swift` around lines 5 - 34, Add tests
invoking TmuxSessionDiscovery.discover for both outcomes: verify a successful
remote command with no sessions returns an empty collection, and verify a
nonzero remote exit status throws TmuxSessionDiscoveryError.remoteExit. Reuse
the existing test doubles or discovery setup used by TmuxSessionDiscovery rather
than testing only parseSessionNames or the command builder.
RemuxApp/Sources/App/RemuxRootModel.swift (2)

263-268: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Merge the two MainActor.assumeIsolated blocks in deinit.

The deinitializer enters the isolation twice. One block keeps teardown ordering explicit and removes the intermediate array.

♻️ Proposed simplification
     deinit {
-        let refreshTasks = MainActor.assumeIsolated {
-            Array(tmuxSessionRefreshTasks.values)
-        }
-        for task in refreshTasks {
-            task.cancel()
-        }
         MainActor.assumeIsolated {
+            for task in tmuxSessionRefreshTasks.values {
+                task.cancel()
+            }
             stopAllTerminalScreenModels()
         }
     }
🤖 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/RemuxRootModel.swift` around lines 263 - 268, In
RemuxRootModel.deinit, merge the refreshTasks snapshot and task cancellation
into a single MainActor.assumeIsolated block. Cancel the values of
tmuxSessionRefreshTasks directly within that block, removing the intermediate
refreshTasks array while preserving the existing teardown order.

1504-1528: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider dropping the injected dependencies parameter.

performTmuxSessionRefresh runs on the main actor and can read self.dependencies directly. The parameter and the capture in refreshTmuxSessions add indirection without changing behavior.

Also applies to: 1539-1560

🤖 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/RemuxRootModel.swift` around lines 1504 - 1528, Remove
the injected dependencies parameter from performTmuxSessionRefresh and have it
use self.dependencies directly for tmux discovery and profile reconciliation.
Update refreshTmuxSessions and every call site, including the related flow
around the reported additional lines, to stop capturing and passing the
redundant parameter while preserving existing refresh behavior.
RemuxApp/Sources/App/ActiveSessionSwitcherView.swift (1)

222-233: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Use a localized plural instead of a manual ternary.

Line 229 builds the plural form in code. ^[\(sessions.count) session](inflect: true) produces the correct form and supports localization later.

🤖 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/ActiveSessionSwitcherView.swift` around lines 222 - 233,
Update the .loaded branch of the context property to use the inflected localized
string format `^[\(sessions.count) session](inflect: true)` instead of manually
selecting “session” or “sessions” with a ternary; leave the other discoveryState
cases unchanged.
RemuxAppTests/ActiveSessionSwitcherProjectionTests.swift (1)

18-33: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Recover the ordering coverage in this test.

The two sessions now belong to different servers, so scoping reduces the result to one item. The test no longer verifies recent-open ordering that its name claims. Add a second session on the selected server, or rename the test to describe scoping and selection only.

🤖 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/ActiveSessionSwitcherProjectionTests.swift` around lines 18 -
33, Update the test around ActiveSessionSwitcherProjection.items so it continues
covering recent-open ordering: add another session associated with
selected.target.server while preserving the existing selected-session assertions
and expected ordering. If ordering is not intended to be tested, instead rename
the test to describe only server scoping and selection.
🤖 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/App/RemuxRootModel.swift`:
- Around line 1529-1536: Update the CancellationError branch in the tmux session
discovery refresh flow to first verify isCurrentTmuxSessionRefresh(server,
refreshID: refreshID), then clear tmuxSessionDiscoveryStates[server.id] before
returning. Preserve the existing behavior for stale or explicitly invalidated
refreshes.

In `@RemuxApp/Sources/SSH/TmuxSessionDiscovery.swift`:
- Around line 21-41: The discover() method should return an empty array when
tmux list-sessions exits with the supported “no server running” status and
verified tmux stderr message; preserve TmuxSessionDiscoveryError.remoteExit for
all other non-zero exits. Confirm the exact message variants and supported tmux
versions before implementing the targeted match.

---

Nitpick comments:
In `@RemuxApp/Sources/App/ActiveSessionSwitcherView.swift`:
- Around line 222-233: Update the .loaded branch of the context property to use
the inflected localized string format `^[\(sessions.count) session](inflect:
true)` instead of manually selecting “session” or “sessions” with a ternary;
leave the other discoveryState cases unchanged.

In `@RemuxApp/Sources/App/RemuxAppDependencies.swift`:
- Around line 495-520: Remove the duplicated check-dedupe-append implementation
from reconcileDiscoveredWorkspaces and reuse the existing reconciliation logic
in FileBackedConnectionProfileRepository, preserving server validation and
snapshot loading behavior.

In `@RemuxApp/Sources/App/RemuxRootModel.swift`:
- Around line 263-268: In RemuxRootModel.deinit, merge the refreshTasks snapshot
and task cancellation into a single MainActor.assumeIsolated block. Cancel the
values of tmuxSessionRefreshTasks directly within that block, removing the
intermediate refreshTasks array while preserving the existing teardown order.
- Around line 1504-1528: Remove the injected dependencies parameter from
performTmuxSessionRefresh and have it use self.dependencies directly for tmux
discovery and profile reconciliation. Update refreshTmuxSessions and every call
site, including the related flow around the reported additional lines, to stop
capturing and passing the redundant parameter while preserving existing refresh
behavior.

In `@RemuxApp/Sources/Persistence/ConnectionProfileRepository.swift`:
- Around line 172-204: Consolidate the reconcile-and-dedupe behavior used by
ConnectionProfileRepository.reconcileDiscoveredWorkspaces into one shared
implementation, and update InMemoryConnectionProfileRepository and the
DebugConnectionProfileSeederTests test double to reuse it. Preserve server
validation, empty-name filtering, deduplication against existing and newly
discovered names, and .distantPast timestamps.

In `@RemuxApp/Sources/Tmux/SSHTmuxControlCommandBuilder.swift`:
- Around line 23-56: Extract the duplicated PATH setup, tmux argument
resolution, and executable/not-found marker handling from launchScript and
discoveryScript into a shared helper parameterized by the command-specific final
exec portion. Rebuild both scripts using that helper while preserving
launchScript’s TERM/session behavior and discoveryScript’s list-sessions
behavior.

In `@RemuxAppTests/ActiveSessionSwitcherProjectionTests.swift`:
- Around line 18-33: Update the test around
ActiveSessionSwitcherProjection.items so it continues covering recent-open
ordering: add another session associated with selected.target.server while
preserving the existing selected-session assertions and expected ordering. If
ordering is not intended to be tested, instead rename the test to describe only
server scoping and selection.

In `@RemuxAppTests/DebugConnectionProfileSeederTests.swift`:
- Around line 151-176: Update reconcileDiscoveredWorkspaces in the test double
to reuse the shared reconciliation implementation from
ConnectionProfileRepository.swift or the existing repository helper instead of
duplicating the server validation, deduplication, and append logic. Preserve the
current snapshot-loading behavior and repository contract while removing the
repeated implementation.

In `@RemuxAppTests/TmuxSessionDiscoveryTests.swift`:
- Around line 5-34: Add tests invoking TmuxSessionDiscovery.discover for both
outcomes: verify a successful remote command with no sessions returns an empty
collection, and verify a nonzero remote exit status throws
TmuxSessionDiscoveryError.remoteExit. Reuse the existing test doubles or
discovery setup used by TmuxSessionDiscovery rather than testing only
parseSessionNames or the command builder.
🪄 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: 35a4d696-0a36-4483-a861-8fe08e279c85

📥 Commits

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

📒 Files selected for processing (13)
  • Remux.xcodeproj/project.pbxproj
  • RemuxApp/Sources/App/ActiveSessionSwitcherView.swift
  • RemuxApp/Sources/App/RemuxAppDependencies.swift
  • RemuxApp/Sources/App/RemuxRootModel.swift
  • RemuxApp/Sources/App/RootView.swift
  • RemuxApp/Sources/Persistence/ConnectionProfileRepository.swift
  • RemuxApp/Sources/SSH/TmuxSessionDiscovery.swift
  • RemuxApp/Sources/Tmux/SSHTmuxControlCommandBuilder.swift
  • RemuxAppTests/ActiveSessionSwitcherProjectionTests.swift
  • RemuxAppTests/ConnectionProfileRepositoryTests.swift
  • RemuxAppTests/DebugConnectionProfileSeederTests.swift
  • RemuxAppTests/RemuxRootModelTests.swift
  • RemuxAppTests/TmuxSessionDiscoveryTests.swift

Comment on lines +1529 to +1536
} catch is CancellationError {
return
} catch {
guard isCurrentTmuxSessionRefresh(server, refreshID: refreshID) else { return }
// Discovery is auxiliary to an already-running terminal. Keep its
// failure inside the sheet rather than replacing the app route.
tmuxSessionDiscoveryStates[server.id] = .failed(error.localizedDescription)
}

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

Clear the loading state when discovery reports cancellation but the refresh is still current.

invalidateTmuxSessionRefresh removes the discovery state, so cancellation caused by edit or delete is handled. If the discoverer itself throws CancellationError while the refresh is still current, the code returns and leaves tmuxSessionDiscoveryStates[server.id] at .loading. The Sessions sheet then shows "Refreshing…" and disables the Refresh button until the sheet is reopened.

🐛 Proposed fix
         } catch is CancellationError {
+            guard isCurrentTmuxSessionRefresh(server, refreshID: refreshID) else { return }
+            tmuxSessionDiscoveryStates[server.id] = .idle
             return
         } catch {
📝 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
} catch is CancellationError {
return
} catch {
guard isCurrentTmuxSessionRefresh(server, refreshID: refreshID) else { return }
// Discovery is auxiliary to an already-running terminal. Keep its
// failure inside the sheet rather than replacing the app route.
tmuxSessionDiscoveryStates[server.id] = .failed(error.localizedDescription)
}
} catch is CancellationError {
guard isCurrentTmuxSessionRefresh(server, refreshID: refreshID) else { return }
tmuxSessionDiscoveryStates[server.id] = .idle
return
} catch {
guard isCurrentTmuxSessionRefresh(server, refreshID: refreshID) else { return }
// Discovery is auxiliary to an already-running terminal. Keep its
// failure inside the sheet rather than replacing the app route.
tmuxSessionDiscoveryStates[server.id] = .failed(error.localizedDescription)
}
🤖 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/RemuxRootModel.swift` around lines 1529 - 1536, Update
the CancellationError branch in the tmux session discovery refresh flow to first
verify isCurrentTmuxSessionRefresh(server, refreshID: refreshID), then clear
tmuxSessionDiscoveryStates[server.id] before returning. Preserve the existing
behavior for stale or explicitly invalidated refreshes.

Comment thread RemuxApp/Sources/SSH/TmuxSessionDiscovery.swift
@h3nock
h3nock force-pushed the sessions/load-existing-tmux-sessions branch from 896c551 to faefed5 Compare August 12, 2026 02:03

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

🧹 Nitpick comments (9)
RemuxApp/Sources/Ghostty/GhosttyKeyboardChrome.swift (1)

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

Mention discovered sessions in the hint.

The sheet now also lists remote tmux sessions under Available and resumes them. The hint covers active, recent, and new sessions only. Consider "Switch active sessions, resume recent or available sessions, or create a new session."

🤖 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/GhosttyKeyboardChrome.swift` at line 213, Update the
accessibilityHint for the session sheet to mention available sessions alongside
active and recent sessions, while retaining the existing new-session guidance.
RemuxApp/Sources/App/RemuxAppDependencies.swift (2)

287-294: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Pass the discovery flow ID into the SSH configuration.

The function builds trace with flowID: "session.discovery.<serverID>", then calls sshConfiguration with traceFlowID: nil. Root-level SSH events therefore lose correlation with the discovery flow. Set traceFlowID: trace.flowID (or the same string) so discovery traces stay joinable.

🤖 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 287 - 294,
Update the sshConfiguration call in the discovery flow to pass trace.flowID as
traceFlowID instead of nil, preserving correlation with the
RemuxTransportStartupTrace created for the target server.

295-297: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Throw instead of crashing when the SSH root key is missing.

liveTmuxSessionDiscoverer is a throws function on an auxiliary path. RemuxRootModel.performTmuxSessionRefresh already contains discovery failures inside the Sessions sheet. A preconditionFailure here terminates the app for a condition that the caller can present as a local failure.

♻️ Proposed change
-        guard let rootKey = configuration.sshRootKey else {
-            preconditionFailure("Tmux discovery requires an SSH root key")
-        }
+        guard let rootKey = configuration.sshRootKey else {
+            throw TmuxSessionDiscoveryError.remoteExit(
+                status: -1,
+                stderr: "Tmux discovery requires an SSH root key."
+            )
+        }

A dedicated error case is preferable to reusing remoteExit. Do you want me to add one?

🤖 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 295 - 297,
Replace the preconditionFailure in liveTmuxSessionDiscoverer with a dedicated
thrown error for a missing SSH root key, and define that error using the
existing error type conventions. Preserve the throws flow so
RemuxRootModel.performTmuxSessionRefresh can present the discovery failure in
the Sessions sheet instead of terminating the app.
RemuxApp/Sources/App/SessionSwitcherView.swift (2)

654-664: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Reuse a single RelativeDateTimeFormatter.

value(for:relativeTo:) allocates and configures a new RelativeDateTimeFormatter on every call. The function runs once per recent row per render pass, and also from RecentSessionSwitcherRow.accessibilityValue and SessionLibraryRow in RemuxApp/Sources/App/RootView.swift. Formatter creation is comparatively expensive.

♻️ Proposed change
 struct SessionLastOpenedText: View {
     let date: Date
 
+    private static let relativeFormatter: RelativeDateTimeFormatter = {
+        let formatter = RelativeDateTimeFormatter()
+        formatter.dateTimeStyle = .named
+        formatter.unitsStyle = .abbreviated
+        return formatter
+    }()
+
     var body: some View {
         Text(Self.value(for: date))
     }
 
     static func value(for date: Date, relativeTo referenceDate: Date = Date()) -> String {
         let elapsed = referenceDate.timeIntervalSince(date)
         if elapsed >= 0, elapsed < 60 {
             return "Opened just now"
         }
 
-        let formatter = RelativeDateTimeFormatter()
-        formatter.dateTimeStyle = .named
-        formatter.unitsStyle = .abbreviated
-        return "Opened \(formatter.localizedString(for: date, relativeTo: referenceDate))"
+        return "Opened \(Self.relativeFormatter.localizedString(for: date, relativeTo: referenceDate))"
     }
 }

SessionLastOpenedText is used from the main actor only, so a static stored formatter is safe here. Confirm that no background caller exists before adopting.

🤖 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/SessionSwitcherView.swift` around lines 654 - 664,
Update SessionLastOpenedText.value(for:relativeTo:) to reuse a single static
RelativeDateTimeFormatter instead of allocating and configuring one per call.
Define and configure the formatter once within SessionLastOpenedText, confirm
its callers remain main-actor-only, and preserve the existing formatting and
“Opened just now” behavior.

69-112: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

The availableSessions filter depends on statement order.

recentIdentities is mutated inside the compactMap that produces recentSessions (lines 78-96) and then read by the availableSessions filter (line 111). The result is correct today only because line 70 executes before line 98. A later reordering of these two assignments would silently let recent sessions reappear under Available.

Compute the recent identity set explicitly before building availableSessions, or add a short comment that records the ordering requirement.

🤖 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/SessionSwitcherView.swift` around lines 69 - 112, Make
the dependency between recent identity collection and available-session
filtering explicit in the SessionSwitcherView initialization: compute/populate
recentIdentities before constructing availableSessions, rather than relying on
the current recentSessions assignment order. Keep the existing deduplication and
filtering behavior unchanged so identities represented by recentSessions remain
excluded from availableSessions.
RemuxApp/Sources/App/RootView.swift (1)

116-140: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

discoveryStates is supplied twice in the same call.

Line 119 passes model.tmuxSessionDiscoveryStates into SessionSwitcherProjection, and line 140 passes the same dictionary to SessionSwitcherView.discoveryStates. The view uses the second copy only to derive isRefreshing, failedServerNames, and hasUndiscoveredServer. Moving that derivation into SessionSwitcherProjection would remove the duplicated input and keep the view's surface smaller.

🤖 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/RootView.swift` around lines 116 - 140, Move the
discovery-state-derived values isRefreshing, failedServerNames, and
hasUndiscoveredServer from SessionSwitcherView into SessionSwitcherProjection,
using its existing discoveryStates input. Remove the separate discoveryStates
parameter from SessionSwitcherView and stop passing it at the call site, while
preserving the derived behavior.
RemuxAppUITests/RemuxAppUITests.swift (1)

3561-3568: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Add a settle interval between swipes.

The loop swipes up to eight times with no pause, then asserts recentRow.isHittable. SwiftUI list scrolling and the disconnect animation need time to settle, so consecutive swipes can overshoot the Recent row or run before it is laid out. Add a short RunLoop.current.run(until:) inside the loop, matching the polling pattern used at lines 709-711 and 722-729.

🤖 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 3561 - 3568, Add a short
RunLoop.current.run(until:) settle interval inside the swipe loop around
sessionList.swipeUp(), matching the existing polling pattern used elsewhere in
the test. Keep the eight-attempt limit and recentRow visibility checks unchanged
so each swipe can settle before the next attempt.
RemuxAppTests/SessionSwitcherProjectionTests.swift (1)

17-23: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

The relative-value assertions depend on the test locale.

XCTAssertFalse(longerValue.contains(",")) and the "sec" check assume an English-language RelativeDateTimeFormatter output. If CI ever runs the unit tests under a different locale, these assertions can fail even though the behavior is correct. Pin the formatter locale in the test, or assert only on the app-supplied "Opened " prefix.

🤖 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/SessionSwitcherProjectionTests.swift` around lines 17 - 23,
Update the relative-value assertions in SessionLastOpenedText.value tests to
avoid locale-dependent checks on commas or “sec”; either configure a fixed
formatter locale for the test or assert only the app-supplied “Opened ” prefix
while preserving the existing date-relative behavior.
RemuxApp/Sources/Ghostty/TerminalSelectionSheetStyle.swift (1)

61-71: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

CompactCircularChromeButtonStyle drops the pressed state.

makeBody ignores configuration.isPressed, so the close button and the Sessions Refresh button show no visual press feedback. The palette gained controlPressedFill at line 8, which suggests a pressed fill was intended.

♻️ Proposed change
     func makeBody(configuration: Configuration) -> some View {
         configuration.label
             .font(.system(size: 15, weight: .semibold))
             .foregroundStyle(TerminalSelectionSheetPalette.primary)
             .frame(width: 36, height: 36)
-            .background(TerminalSelectionSheetPalette.controlFill, in: Circle())
+            .background(
+                configuration.isPressed
+                    ? TerminalSelectionSheetPalette.controlPressedFill
+                    : TerminalSelectionSheetPalette.controlFill,
+                in: Circle()
+            )
             .frame(width: 44, height: 44)
             .contentShape(Rectangle())
     }
🤖 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 61 -
71, Update CompactCircularChromeButtonStyle.makeBody to use
configuration.isPressed when selecting the background fill, applying
TerminalSelectionSheetPalette.controlPressedFill while pressed and retaining
controlFill otherwise. Preserve the existing sizing, shape, and content layout.
🤖 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/App/RemuxRootModel.swift`:
- Around line 1039-1041: Update connectToDiscoveredSession to select the
workspace from the deterministic workspaces(for:) result, preserving the
serverID and sessionName filters, so matching workspaces are ordered by last
opened and the most recently opened one is chosen before falling back to
SavedWorkspace.

In `@RemuxApp/Sources/App/RootView.swift`:
- Around line 114-141: Update the isSessionSwitcherPresented sheet presentation
closure around SessionSwitcherView to trigger model.refreshTmuxSessions when the
sheet opens, while preserving the existing SessionSwitcherView configuration and
callbacks.

---

Nitpick comments:
In `@RemuxApp/Sources/App/RemuxAppDependencies.swift`:
- Around line 287-294: Update the sshConfiguration call in the discovery flow to
pass trace.flowID as traceFlowID instead of nil, preserving correlation with the
RemuxTransportStartupTrace created for the target server.
- Around line 295-297: Replace the preconditionFailure in
liveTmuxSessionDiscoverer with a dedicated thrown error for a missing SSH root
key, and define that error using the existing error type conventions. Preserve
the throws flow so RemuxRootModel.performTmuxSessionRefresh can present the
discovery failure in the Sessions sheet instead of terminating the app.

In `@RemuxApp/Sources/App/RootView.swift`:
- Around line 116-140: Move the discovery-state-derived values isRefreshing,
failedServerNames, and hasUndiscoveredServer from SessionSwitcherView into
SessionSwitcherProjection, using its existing discoveryStates input. Remove the
separate discoveryStates parameter from SessionSwitcherView and stop passing it
at the call site, while preserving the derived behavior.

In `@RemuxApp/Sources/App/SessionSwitcherView.swift`:
- Around line 654-664: Update SessionLastOpenedText.value(for:relativeTo:) to
reuse a single static RelativeDateTimeFormatter instead of allocating and
configuring one per call. Define and configure the formatter once within
SessionLastOpenedText, confirm its callers remain main-actor-only, and preserve
the existing formatting and “Opened just now” behavior.
- Around line 69-112: Make the dependency between recent identity collection and
available-session filtering explicit in the SessionSwitcherView initialization:
compute/populate recentIdentities before constructing availableSessions, rather
than relying on the current recentSessions assignment order. Keep the existing
deduplication and filtering behavior unchanged so identities represented by
recentSessions remain excluded from availableSessions.

In `@RemuxApp/Sources/Ghostty/GhosttyKeyboardChrome.swift`:
- Line 213: Update the accessibilityHint for the session sheet to mention
available sessions alongside active and recent sessions, while retaining the
existing new-session guidance.

In `@RemuxApp/Sources/Ghostty/TerminalSelectionSheetStyle.swift`:
- Around line 61-71: Update CompactCircularChromeButtonStyle.makeBody to use
configuration.isPressed when selecting the background fill, applying
TerminalSelectionSheetPalette.controlPressedFill while pressed and retaining
controlFill otherwise. Preserve the existing sizing, shape, and content layout.

In `@RemuxAppTests/SessionSwitcherProjectionTests.swift`:
- Around line 17-23: Update the relative-value assertions in
SessionLastOpenedText.value tests to avoid locale-dependent checks on commas or
“sec”; either configure a fixed formatter locale for the test or assert only the
app-supplied “Opened ” prefix while preserving the existing date-relative
behavior.

In `@RemuxAppUITests/RemuxAppUITests.swift`:
- Around line 3561-3568: Add a short RunLoop.current.run(until:) settle interval
inside the swipe loop around sessionList.swipeUp(), matching the existing
polling pattern used elsewhere in the test. Keep the eight-attempt limit and
recentRow visibility checks unchanged so each swipe can settle before the next
attempt.
🪄 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: e80594b9-be0b-4a48-902c-a0ae5c529081

📥 Commits

Reviewing files that changed from the base of the PR and between 896c551 and faefed5.

📒 Files selected for processing (16)
  • Remux.xcodeproj/project.pbxproj
  • RemuxApp/Sources/App/ActiveSessionSwitcherView.swift
  • RemuxApp/Sources/App/RemuxAppDependencies.swift
  • RemuxApp/Sources/App/RemuxRootModel.swift
  • RemuxApp/Sources/App/RootView.swift
  • RemuxApp/Sources/App/SessionSwitcherView.swift
  • RemuxApp/Sources/Ghostty/GhosttyKeyboardChrome.swift
  • RemuxApp/Sources/Ghostty/TerminalSelectionSheetStyle.swift
  • RemuxApp/Sources/Persistence/ConnectionProfileRepository.swift
  • RemuxApp/Sources/SSH/TmuxSessionDiscovery.swift
  • RemuxApp/Sources/Tmux/SSHTmuxControlCommandBuilder.swift
  • RemuxAppTests/ActiveSessionSwitcherProjectionTests.swift
  • RemuxAppTests/RemuxRootModelTests.swift
  • RemuxAppTests/SessionSwitcherProjectionTests.swift
  • RemuxAppTests/TmuxSessionDiscoveryTests.swift
  • RemuxAppUITests/RemuxAppUITests.swift
💤 Files with no reviewable changes (2)
  • RemuxAppTests/ActiveSessionSwitcherProjectionTests.swift
  • RemuxApp/Sources/App/ActiveSessionSwitcherView.swift
🚧 Files skipped from review as they are similar to previous changes (2)
  • RemuxApp/Sources/Tmux/SSHTmuxControlCommandBuilder.swift
  • RemuxAppTests/RemuxRootModelTests.swift

Comment thread RemuxApp/Sources/App/RemuxRootModel.swift Outdated
Comment on lines 114 to 141
.sheet(isPresented: $isSessionSwitcherPresented) {
ActiveSessionSwitcherView(
sessions: ActiveSessionSwitcherProjection.items(
sessions: model.activeSessions,
SessionSwitcherView(
projection: SessionSwitcherProjection(
snapshot: model.library,
activeSessions: model.activeSessions,
discoveryStates: model.tmuxSessionDiscoveryStates,
selectedSessionID: selectedTerminalID
),
servers: model.library.servers,
currentServerID: selectedActiveSession?.target.server.id,
onSelectSession: model.showActiveSession,
onSelectActiveSession: model.showActiveSession,
onResumeSession: { workspaceID in
traceSessionOpenTap(workspaceID)
Task { await model.connect(to: workspaceID) }
},
onResumeAvailableSession: { serverID, sessionName in
Task {
await model.connectToDiscoveredSession(
named: sessionName,
on: serverID
)
}
},
onDisconnectSession: model.disconnectActiveSession,
onCreateSession: beginNewWorkspaceFromTerminal
onCreateSession: beginNewWorkspaceFromTerminal,
onRefresh: model.refreshTmuxSessions,
discoveryStates: model.tmuxSessionDiscoveryStates
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

The Sessions sheet does not refresh automatically when it opens.

The PR objective states that the sheet refreshes automatically when opened. This closure only builds SessionSwitcherView; it never calls model.refreshTmuxSessions. Discovery runs at launch and after a runtime reaches .connected. RemuxRootModel.handleAppLifecyclePhase cancels every refresh when the app enters the background, so after a background round trip the sheet shows the previous snapshot until the user taps Refresh.

Add a refresh trigger on presentation.

🐛 Proposed fix
             .terminalSelectionSheetPresentationBackground()
             .ghosttyTerminalChromePresentation(
                 model.terminalSettings.theme.terminalChromeColorScheme,
                 chromeStyle: model.terminalSettings.theme.terminalChromeStyle
             )
+            .task { model.refreshTmuxSessions() }

refreshTmuxSessions(for:) already coalesces per server, so a duplicate request during an in-flight refresh is a no-op.

🤖 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/RootView.swift` around lines 114 - 141, Update the
isSessionSwitcherPresented sheet presentation closure around SessionSwitcherView
to trigger model.refreshTmuxSessions when the sheet opens, while preserving the
existing SessionSwitcherView configuration and callbacks.

@h3nock

h3nock commented Aug 12, 2026

Copy link
Copy Markdown
Owner

thanks @vaayne for starting this and contributing the session discovery work. i polished the ui/ux and added search on top of it. really appreciate the work

@vaayne

vaayne commented Aug 12, 2026

Copy link
Copy Markdown
Contributor Author

@h3nock Thanks,You are awesome.

@h3nock
h3nock merged commit 517f8d2 into h3nock:main Aug 12, 2026
4 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.

2 participants