Repository navigation
Fix Cloud reconnect UI state ownership #12556
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
4e7980c
33a93af
0624a0f
22c7068
73b8e25
b73d223
c30a22e
ae24f62
251bb8d
453eb20
bc5ac8e
e8fdd85
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -7543,20 +7543,43 @@ final class Workspace: Identifiable, ObservableObject, FilePreviewTabMetadataHos | |||||||||
| } | ||||||||||
|
|
||||||||||
| func cloudTerminalReconnectOverlayPresentation(forSurfaceId surfaceId: UUID) -> CloudTerminalReconnectOverlayPolicy.Presentation? { | ||||||||||
| // A native Cloud pane owns its presentation through the attachment | ||||||||||
| // session. Do not let a stale workspace controller state cover a | ||||||||||
| // usable terminal when the catalog projection is being refreshed. | ||||||||||
| if let panel = panels[surfaceId] as? TerminalPanel, | ||||||||||
| let attachment = panel.cloudAttachment { | ||||||||||
| return attachment.presentation | ||||||||||
| } | ||||||||||
| if let failure = cloudMaterializationFailures[surfaceId] { | ||||||||||
| return Self.cloudMaterializationFailurePresentation( | ||||||||||
| detail: failure.detail, | ||||||||||
| reference: failure.reference | ||||||||||
| ) | ||||||||||
| } | ||||||||||
| // A reserved pane still waiting for its terminal shows nothing but its | ||||||||||
| // tab spinner; only a recorded failure (above) puts a card on it. | ||||||||||
| // A reserved pane only shows its tab spinner until creation fails. | ||||||||||
| if cloudPendingCreations[surfaceId] != nil { return nil } | ||||||||||
| if let resource = cloudProjectedResource(forPanel: surfaceId), let machineID = resource.id.machine.cloudMachineID, let session = CmuxTuiSurfaceProviderRegistry.shared.provider(machineID: machineID)?.manualMirrorSessions[surfaceId] { return session.connectionPresentation } | ||||||||||
| let surfaceConnectionState: WorkspaceRemoteConnectionState | ||||||||||
| switch remoteTerminalSessionStatesBySurfaceId[surfaceId]?.phase { | ||||||||||
| case .some(.connected): | ||||||||||
| // A workspace controller can reconnect while this terminal's | ||||||||||
| // established PTY remains usable. Its per-surface liveness owns | ||||||||||
| // the card, so the controller state cannot cover this pane. | ||||||||||
| return nil | ||||||||||
| case .some(.launching): | ||||||||||
| // A workspace can be connected through another pane. Keep this | ||||||||||
| // panel's loading card until its own attach callback arrives. | ||||||||||
| surfaceConnectionState = remoteConnectionState == .connected | ||||||||||
| ? .connecting | ||||||||||
| : remoteConnectionState | ||||||||||
| case .some(.ended): | ||||||||||
| surfaceConnectionState = .disconnected | ||||||||||
| case .none: | ||||||||||
| surfaceConnectionState = remoteConnectionState | ||||||||||
|
Comment on lines
+7576
to
+7577
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win 🔎 Supported by static analysis🏁 Script executed: sed -n '7515,7600p' Sources/Workspace.swift
rg -n -C 3 'remoteTerminalSessionStatesBySurfaceId|markRemoteTerminalSessionConnected|RemoteTerminalSessionState' Sources cmuxTests
sed -n '1,160p' .github/review-bot-rules/reliability-single-source-of-truth.mdRepository: manaflow-ai/cmux Length of output: 44381 🏁 Script executed: #!/bin/bash
set -e
printf '%s\n' '--- liveness implementation ---'
sed -n '1,530p' Sources/Workspace+RemoteTerminalLiveness.swift
printf '%s\n' '--- phase and active-surface references ---'
rg -n -C 4 'activeRemoteTerminalSurfaceIds|remoteTerminalSessionStatesBySurfaceId|clearRemoteTerminalSessionPhase|cloudTerminalReconnectOverlayPresentation|isRemoteTerminalSurface\(' Sources --glob '*.swift'
printf '%s\n' '--- cloud surface creation/restore/transfer references ---'
rg -n -C 5 'legacy|Legacy|cloud.*surface|surface.*cloud|remoteDisconnectPlaceholderPanelIds|cloudPendingCreations|cloudAttachment' Sources --glob '*.swift'Repository: manaflow-ai/cmux Length of output: 50373 🏁 Script executed: #!/bin/bash
set -e
printf '%s\n' '--- Workspace+RemoteTerminalLiveness.swift ---'
cat -n Sources/Workspace+RemoteTerminalLiveness.swift
printf '%s\n' '--- exact phase/active mutations ---'
rg -n 'activeRemoteTerminalSurfaceIds|remoteTerminalSessionStatesBySurfaceId|clearRemoteTerminalSessionPhase|cloudPendingCreations' Sources/Workspace.swift Sources/Workspace+RemoteTerminalLiveness.swift Sources/Surfaces --glob '*.swift'
printf '%s\n' '--- renderability and presentation call sites ---'
rg -n -C 3 'func isRemoteTerminalSurface|isRemoteTerminalSurface\(|cloudTerminalReconnectOverlayPresentation\(' Sources/Workspace.swift Sources --glob '*.swift' --glob '!Sources/Mobile/**'Repository: manaflow-ai/cmux Length of output: 50372 🏁 Script executed: #!/bin/bash
set -e
printf '%s\n' '--- active remote surface lifecycle ---'
sed -n '7180,7290p' Sources/Workspace.swift
sed -n '7340,7485p' Sources/Workspace.swift
sed -n '7690,7785p' Sources/Workspace.swift
sed -n '7970,8050p' Sources/Workspace.swift
printf '%s\n' '--- remote session lifecycle callers ---'
cat -n Sources/Workspace+RemoteSessionLifecycle.swift
printf '%s\n' '--- Cloud creation and manual mirror ---'
cat -n Sources/Surfaces/Workspace+CloudTerminalCreation.swift
cat -n Sources/Surfaces/Workspace+CloudManualMirror.swiftRepository: manaflow-ai/cmux Length of output: 50372 🏁 Script executed: #!/bin/bash
set -e
printf '%s\n' '--- presentation consumer and policy ---'
rg -n -C 5 'cloudTerminalReconnectOverlayPresentation|CloudTerminalReconnectOverlayPolicy' Sources --glob '*.swift'
printf '%s\n' '--- tracking and restoration callers ---'
rg -n -C 6 'trackRemoteTerminalSurface\(|restoreRemoteTerminalSessionPhase\(|markRemoteTerminalSessionLaunching\(' Sources --glob '*.swift' --glob '!Sources/Workspace+RemoteTerminalLiveness.swift'
printf '%s\n' '--- transfer and restore state fields ---'
rg -n -C 5 'remoteTerminalSessionPhase|remoteTerminalAuthority|remoteTerminalLifecycleID|remoteTerminalAttemptID' Sources --glob '*.swift'Repository: manaflow-ai/cmux Length of output: 50372 🏁 Script executed: #!/bin/bash
set -e
printf '%s\n' '--- reconnect policy ---'
cat -n Sources/WorkspaceRemoteReconnectPolicy.swift
printf '%s\n' '--- retirement and respawn ---'
cat -n Sources/Workspace+RemoteTerminalRetirement.swift
sed -n '1,190p' Sources/Workspace+TerminalRespawn.swift
printf '%s\n' '--- overlay consumer ---'
sed -n '10370,10425p' Sources/GhosttyTerminalView.swift
printf '%s\n' '--- transfer implementation around workspace handoff ---'
sed -n '11130,11215p' Sources/Workspace.swiftRepository: manaflow-ai/cmux Length of output: 20923 🏁 Script executed: #!/bin/bash
set -e
printf '%s\n' '--- retirement callers and preserved tracking ---'
rg -n -C 8 'retireRemoteTerminalLifecycle|preservesRemoteTerminalTracking: true|discardClosedPanelLifecycleState\(' Sources --glob '*.swift'
printf '%s\n' '--- overlay contract tests and comments ---'
rg -n -C 8 'cloudTerminalReconnectOverlayPresentation|CloudTerminalReconnectOverlayPolicy|remoteTerminalSessionStatesBySurfaceId.*phase|phase.*nil|fail closed|fallback' cmuxTests Sources --glob '*.swift'Repository: manaflow-ai/cmux Length of output: 50373 🏁 Script executed: #!/bin/bash
set -e
log=/tmp/coderabbit-shell-logs/shell-output-9PvjmH
printf '%s\n' '--- exact retirement matches from prior output ---'
rg -n -C 6 'retireRemoteTerminalLifecycle|preservesRemoteTerminalTracking: true' "$log" | head -n 240
printf '%s\n' '--- exact overlay matches from prior output ---'
rg -n -C 5 'cloudTerminalReconnectOverlayPresentation|CloudTerminalReconnectOverlayPolicy|remoteTerminalSessionStatesBySurfaceId' "$log" | head -n 240Repository: manaflow-ai/cmux Length of output: 26635 🏁 Script executed: #!/bin/bash
set -e
printf '%s\n' '--- panel lifecycle retirement boundary ---'
sed -n '430,535p' Sources/Workspace+PanelLifecycle.swift
printf '%s\n' '--- callers that preserve terminal tracking ---'
rg -n -C 10 'preservesTerminalForTransfer|preservesRemoteTerminalTracking' Sources/Workspace+PanelLifecycle.swift Sources --glob '*.swift' | head -n 320
printf '%s\n' '--- legacy overlay test setup ---'
sed -n '100,140p' cmuxTests/WorkspaceRemoteReconnectPolicyTests.swiftRepository: manaflow-ai/cmux Length of output: 25067 Fail closed when the per-surface session phase is absent. During terminal replacement, the workspace keeps the surface in Do not inherit workspace-wide state when the per-surface phase is missing. Proposed direction- case .none:
- surfaceConnectionState = remoteConnectionState
+ case .none:
+ return nil📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||
| } | ||||||||||
| return CloudTerminalReconnectOverlayPolicy.presentation( | ||||||||||
| isManagedCloudWorkspace: isManagedCloudVMWorkspace, | ||||||||||
| isRemoteTerminalSurface: isRemoteTerminalSurface(surfaceId) || remoteDisconnectPlaceholderPanelIds.contains(surfaceId), | ||||||||||
| connectionState: remoteConnectionState, | ||||||||||
| connectionState: surfaceConnectionState, | ||||||||||
| detail: remoteConnectionDetail | ||||||||||
| ) | ||||||||||
| } | ||||||||||
|
|
||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
📐 Maintainability & Code Quality | 🟠 Major | 🏗️ Heavy lift
Compute the next attachment state once, then publish it once.
publishAttachmentPresentation()updatesattachmentStatusand then callssynchronizeCloudTerminalReconnectOverlay()a second time. The comment says this exists "so a phase didSet callback cannot leave the card on the previous phase." That comment names the actual problem: thephasedidSet(lines 56-65, unchanged) already publishes and reconciles the overlay directly, beforetransition(to:reason:)finishes updatinginterruption,attachAttempts, and beforefinishDiagnosticsfinishes updatingdiagnosticFailureanddiagnosticReference. The code then republishes at the end of each caller to fix the picture, instead of computing the full next state before any published property changes.Today every caller of
finishDiagnosticsalso callstransition(...)afterward in the same synchronous call, so the final publish always wins and the interim one is never observed externally. But this is fragile: it depends on every call site remembering to end with atransition()call, and any future phase-adjacent field added to the presentation risks staying stale if that invariant is not preserved.Single source of truth:
attachmentStatusshould be the only writer of this pane's presentation, updated exactly once per transition from state computed beforephase,interruption,diagnosticFailure, anddiagnosticReferenceare mutated.First migration cut: compute the next
attachmentStateandconnectionPresentationbefore assigningphase, pass that single snapshot into oneattachmentStatus.update(...)call, and removephase'sdidSetdirect call tosynchronizeCloudTerminalReconnectOverlay()(or route it through the same single update path) so one transition means one publish.🤖 Prompt for AI Agents
Source: Coding guidelines