Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
63 commits
Select commit Hold shift + click to select a range
97c4673
test: preserve Cloud replay state across hidden restore geometry
austinywang Sep 24, 2026
5dccec0
test: reproduce lost Cloud geometry eligibility after hidden restore
austinywang Sep 24, 2026
b7e457f
fix: retain Cloud geometry claim policy across hidden restores
austinywang Sep 24, 2026
92126bd
test: assert restored Cloud resize dimensions
austinywang Sep 24, 2026
22a187b
fix: import workspace liveness in Codex restore policy
austinywang Sep 24, 2026
827d859
chore: sync Cloud restore test wiring
austinywang Sep 24, 2026
62532fc
fix: remove duplicate Cloud restore test registration
austinywang Sep 24, 2026
8b8c669
test: fence passive Cloud claims with protocol traffic
austinywang Sep 24, 2026
1b288aa
Merge remote-tracking branch 'origin/main' into 14078-cloud-codex-res…
austinywang Sep 24, 2026
53dd942
Read matchingObservation after it is declared in the restore liveness…
azooz2003-bit Sep 24, 2026
ff2694f
refactor: isolate workspace title debug formatting
austinywang Sep 24, 2026
15ba7c4
fix: preserve restore intent before process probing
austinywang Sep 24, 2026
2a2e092
test: reproduce stale Cloud replay cells after restore
austinywang Sep 24, 2026
1a11606
fix: reset Cloud VT state for replacement replays
austinywang Sep 24, 2026
674248c
Merge remote-tracking branch 'origin/main' into 14078-cloud-codex-res…
austinywang Sep 24, 2026
68fb24d
test: keep replay reset marker in restore fixture
austinywang Sep 24, 2026
223eb67
fix: restore debug title formatter linkage
austinywang Sep 24, 2026
4804326
fix: stop passive Cloud mirror claim oscillation
austinywang Sep 24, 2026
2fdaef3
fix: block rejected cross-client Cloud sizing claims
austinywang Sep 24, 2026
f7a3bc7
fix: wait for Cloud resize outcome before claiming
austinywang Sep 24, 2026
5de372f
test: allow automatic restore claim response
austinywang Sep 24, 2026
dce99b4
fix: distinguish passive Cloud lease outcomes
austinywang Sep 24, 2026
509b984
fix: preserve explicit Cloud claim intent
austinywang Sep 24, 2026
4c0fa87
fix: demote Cloud mirror after cross-client rejection
austinywang Sep 24, 2026
99329a1
fix: retain pending Cloud claims through handshake
austinywang Sep 24, 2026
9bd3588
fix: ignore no-op Cloud resize acknowledgements
austinywang Sep 24, 2026
6a36349
fix: defer cross-client Cloud loss until replay state
austinywang Sep 24, 2026
9d90d5e
fix: clear Cloud ownership after replay confirms peer loss
austinywang Sep 24, 2026
a9f6a92
Merge remote-tracking branch 'origin/main' into 14078-cloud-codex-res…
austinywang Sep 24, 2026
82b100a
test: cover legacy applied resize responses
austinywang Sep 24, 2026
a5af809
Merge remote-tracking branch 'origin/main' into issue-14078-cloud-cod…
austinywang Sep 24, 2026
58d4105
test: preserve authored Cloud colors across sidecar-free replay
austinywang Sep 24, 2026
dc9e8af
fix: retain authored colors when Cloud replay omits sidecar
austinywang Sep 24, 2026
bbc466c
test: cover Cloud replay grid alignment
austinywang Sep 24, 2026
8ed24b2
fix: align Cloud replay with remote grid
austinywang Sep 24, 2026
c094d63
Merge remote-tracking branch 'origin/14078-cloud-codex-restore-garble…
austinywang Sep 24, 2026
e568942
fix: keep Cloud replay geometry transient
austinywang Sep 24, 2026
1e6f928
fix: preserve Cloud sizing intent across replay
austinywang Sep 24, 2026
c78ffdc
fix: keep replay sizing helpers in app target
austinywang Sep 24, 2026
cb2dc58
test: reproduce Cloud replay shifting sparse screens with history
austinywang Sep 24, 2026
91601b8
revert: remove speculative Cloud replay grid overrides
austinywang Sep 24, 2026
d2d6aa3
fix: refresh Cloud renderer after replay application
austinywang Sep 24, 2026
90fa111
fix: preserve replay history and protect tagged resources
austinywang Sep 24, 2026
054dc50
style: apply hosted replay formatting
austinywang Sep 24, 2026
6ac603e
fix: use terminal history boundary for replay
austinywang Sep 25, 2026
96d5686
fix: delimit replay rows when scrollback exists
austinywang Sep 25, 2026
a846dfd
Merge branch 'main' of https://github.com/manaflow-ai/cmux into 14078…
austinywang Sep 25, 2026
d081368
Merge origin/main and fix replay API visibility
austinywang Sep 25, 2026
4784eb2
fix: preserve Cloud replay trailing rows
austinywang Sep 25, 2026
4a48e3d
Merge remote-tracking branch 'origin/main' into 14078-cloud-codex-res…
austinywang Sep 25, 2026
cbc0118
ci: pin GhosttyKit for replay fix
austinywang Sep 25, 2026
a157f5c
Merge remote-tracking branch 'origin/main' into 14078-cloud-codex-res…
austinywang Sep 25, 2026
fc2b529
fix: pin attested iroh Swift artifact revision
austinywang Sep 25, 2026
1d9082a
chore: update iroh package lockfiles
austinywang Sep 25, 2026
ddeb03d
fix: pin published iroh Swift release
austinywang Sep 25, 2026
8cda030
Merge remote-tracking branch 'origin/main' into 14078-cloud-codex-res…
austinywang Sep 25, 2026
4a0bd3a
chore: update Xcode package lockfile
austinywang Sep 25, 2026
4f98bd3
fix: align Xcode iroh package requirement
austinywang Sep 25, 2026
ea4242c
Merge remote-tracking branch 'origin/main' into 14078-cloud-codex-res…
austinywang Sep 25, 2026
52020d3
Merge remote-tracking branch 'origin/main' into 14078-cloud-codex-res…
austinywang Sep 25, 2026
86504fb
fix: import Cloud package for team picker
austinywang Sep 25, 2026
9670d83
Merge remote-tracking branch 'origin/main' into 14078-cloud-codex-res…
austinywang Sep 25, 2026
d0f485e
Merge remote-tracking branch 'origin/main' into 14078-cloud-codex-res…
austinywang Sep 25, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import Foundation

/// Resolves diagnostic copy from the shared package's locale catalog.
struct DiagnosticLocalization: Sendable {
private final class BundleFinder {}

let locale: Locale
private let bundle: Bundle

Expand All @@ -23,21 +25,45 @@ struct DiagnosticLocalization: Sendable {
}

private static func bundle(for locale: Locale) -> Bundle {
languageBundle(for: locale) ?? .module
languageBundle(for: locale) ?? packageResourceBundle ?? .main
}

private static func languageBundle(for locale: Locale) -> Bundle? {
guard let packageResourceBundle else { return nil }
let identifiers = Bundle.preferredLocalizations(
from: Bundle.module.localizations,
from: packageResourceBundle.localizations,
forPreferences: [locale.identifier]
)
for identifier in identifiers {
guard let path = Bundle.module.path(
guard let path = packageResourceBundle.path(
forResource: identifier,
ofType: "lproj"
), let bundle = Bundle(path: path) else { continue }
return bundle
}
return nil
}

/// SwiftPM normally synthesizes `Bundle.module` for this lookup. That
/// accessor traps when a tagged app is replaced while its previous process
/// is still starting, because the old process can briefly observe a bundle
/// whose package resources have moved. Keep the lookup optional so
/// diagnostics fall back to their supplied English defaults instead of
/// turning startup telemetry into a process-wide fatal error.
private static let packageResourceBundle: Bundle? = {
let bundleName = "CMUXMobileCore_CMUXMobileCore"
let resourceRoots = [
Bundle.main.resourceURL,
Bundle(for: BundleFinder.self).resourceURL,
Bundle.main.bundleURL,
]
for root in resourceRoots {
guard let root else { continue }
let url = root.appendingPathComponent(bundleName + ".bundle")
if let bundle = Bundle(url: url) {
return bundle
}
}
return nil
}()
}
4 changes: 2 additions & 2 deletions Packages/Shared/CmuxIrohTransport/Package.resolved

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Packages/Shared/CmuxIrohTransport/Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ let package = Package(
.package(path: "../CMUXMobileCore"),
.package(
url: "https://github.com/manaflow-ai/iroh-ffi.git",
exact: "1.0.2-cmux.7.ios17.2"
exact: "1.0.2-cmux.7.ios17.3"
),
],
targets: [
Expand Down
4 changes: 2 additions & 2 deletions Packages/Shared/CmuxIrxTransport/Package.resolved

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Packages/Shared/CmuxIrxTransport/Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ let package = Package(
.package(path: "../CmuxIrohTransport"),
.package(
url: "https://github.com/manaflow-ai/iroh-ffi.git",
exact: "1.0.2-cmux.7.ios17.2"
exact: "1.0.2-cmux.7.ios17.3"
),
],
targets: [
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
public import Foundation
import GhosttyKit

extension TerminalSurface {
/// Enqueues replacement output and refreshes after the parser has applied it.
///
/// A Cloud snapshot is a replacement state, so refreshing when its bytes
/// are merely admitted can present the previous IOSurface contents. The
/// completion runs after the generation FIFO has parsed the bytes.
@MainActor
public func processRemoteReplay(
_ data: Data,
onApplied: @escaping @MainActor @Sendable () -> Void
) {
guard !data.isEmpty,
let surface = liveSurfaceForGhosttyAccess(reason: "remoteReplay") else {
processRemoteOutput(data)
return
}
flushPendingRemoteOutput(to: surface)
remoteOutputLane.enqueue(data, to: surface, onApplied: onApplied)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,11 @@ final class TerminalSurfaceRemoteOutputLane: @unchecked Sendable {
}

/// Enqueues one ordered output batch and its refresh signal.
func enqueue(_ data: Data, to surface: ghostty_surface_t) {
func enqueue(
_ data: Data,
to surface: ghostty_surface_t,
onApplied: (@MainActor @Sendable () -> Void)? = nil
) {
guard !data.isEmpty else { return }
// Raw pointers are represented as bits across the Sendable queue
// boundary; the lane fence owns the native lifetime until this work
Expand All @@ -51,6 +55,9 @@ final class TerminalSurfaceRemoteOutputLane: @unchecked Sendable {
ghostty_surface_process_output(surface, baseAddress, UInt(rawBuffer.count))
}
ghostty_surface_refresh(surface)
if let onApplied {
Task { @MainActor in onApplied() }
}
}
}
}
Expand Down
68 changes: 34 additions & 34 deletions Sources/Cloud/CloudTuiManualMirrorSession.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ final class CloudTuiManualMirrorSession {
private(set) var remoteSurfaceID: UInt64
let inputRouter: CloudTuiManualIOInputRouter
let imagePaste = CloudImagePasteCoordinator()

private let operations: CloudOperationRecorder?
private var diagnosticContext: CloudOperationContext?
private var creationAttachment: CloudCreationAttachment?
Expand All @@ -50,6 +49,9 @@ final class CloudTuiManualMirrorSession {
private var attachResponseReceived = false
private var claimInFlight = false
private var geometryClaimed = false
private var geometryClaimBlockedByPeer = false
private var explicitGeometryClaimPending = false
private var geometryClaimLossPending = false
private var geometryClaimEligible: Bool
/// Older daemons do not know `set-client-sizing`. In that case the
/// recorded `resize-surface` report is still useful, so the scheduler can
Expand All @@ -58,7 +60,6 @@ final class CloudTuiManualMirrorSession {
/// Retained for diagnostics and for a future targeted detach. Closing the
/// socket is still the cleanup fence for peers without lease support.
private var remoteLease: String?
private var replayNeedsReset = false
/// The last sidecar fed to the local surface; the next one is applied as a delta from it.
private var appliedRemoteColors = CloudTuiRemoteColors()
private var hasReceivedRemoteReplay = false
Expand Down Expand Up @@ -240,8 +241,8 @@ final class CloudTuiManualMirrorSession {
)
}
}
geometryClaimed = false
geometryClaimEligible = false
(geometryClaimed, geometryClaimBlockedByPeer, geometryClaimLossPending) = (false, false, false)
explicitGeometryClaimPending = false
claimUnsupported = false
claimInFlight = false
discardPendingSizingRequests()
Expand Down Expand Up @@ -286,9 +287,6 @@ final class CloudTuiManualMirrorSession {
/// a reset screen.
private func tearDownConnection() {
watchdog.cancel()
if hasReceivedRemoteReplay {
replayNeedsReset = true
}
connectTask?.cancel()
connectTask = nil
eventTask?.cancel()
Expand All @@ -300,7 +298,8 @@ final class CloudTuiManualMirrorSession {
pendingRequests.removeAll(keepingCapacity: true)
attachResponseReceived = false
claimInFlight = false
geometryClaimed = false
(geometryClaimed, geometryClaimBlockedByPeer, geometryClaimLossPending) = (false, false, false)
explicitGeometryClaimPending = false
claimUnsupported = false
remoteLease = nil
serverCapabilities.removeAll(keepingCapacity: true)
Expand Down Expand Up @@ -426,10 +425,7 @@ final class CloudTuiManualMirrorSession {
/// is also used by the composed explicit-input callback.
func claimGeometry() {
guard surface?.isRendererPortalVisible == true else { return }
geometryClaimEligible = true
// Another local projection may have claimed the shared terminal since
// our last report. Treat an explicit focus/input edge as a fresh claim
// opportunity instead of trusting the stale local flag.
(geometryClaimEligible, geometryClaimBlockedByPeer, explicitGeometryClaimPending, geometryClaimLossPending) = (true, false, true, false)
geometryClaimed = false
claimUnsupported = false
sendClaimIfNeeded()
Expand Down Expand Up @@ -569,15 +565,17 @@ final class CloudTuiManualMirrorSession {
inputRouter.updateSurfaceID(surfaceID)
}
guard surfaceID == remoteSurfaceID else { return }
applyReplay(bytes, reset: replayNeedsReset)
applyColors(colors)
replayNeedsReset = false
// A snapshot replaces the local VT state. Reset first so cells,
// cursor state, alternate-screen mode, and SGR from a prior
// restore cannot survive where the replacement is shorter.
applyReplay(bytes, colors: colors)
hasReceivedRemoteReplay = true
diagnosticReplayReceived = true
if phase == .attached { finishDiagnostics() }
updatePresentationEpisode()
synchronizePresentation()
lastRemoteGrid = CloudTuiManualIOGrid(columns: columns, rows: rows)
if geometryClaimLossPending { geometryClaimLossPending = false; geometryClaimBlockedByPeer = !explicitGeometryClaimPending && lastRemoteGrid != resizeScheduler.desired; geometryClaimed = geometryClaimed && !geometryClaimBlockedByPeer }
reconcileRemoteGrid()
case let .output(surfaceID, bytes, colors):
guard surfaceID == remoteSurfaceID else { return }
Expand All @@ -588,14 +586,14 @@ final class CloudTuiManualMirrorSession {
// `resized` carries a replacement replay, not an incremental
// output chunk. Resetting first prevents old rows/cursor state from
// surviving a shrink or a reconnect.
applyReplay(bytes, reset: true)
applyColors(colors)
applyReplay(bytes, colors: colors)
hasReceivedRemoteReplay = true
diagnosticReplayReceived = true
if phase == .attached { finishDiagnostics() }
updatePresentationEpisode()
synchronizePresentation()
lastRemoteGrid = CloudTuiManualIOGrid(columns: columns, rows: rows)
if geometryClaimLossPending { geometryClaimLossPending = false; geometryClaimBlockedByPeer = !explicitGeometryClaimPending && lastRemoteGrid != resizeScheduler.desired; geometryClaimed = geometryClaimed && !geometryClaimBlockedByPeer }
reconcileRemoteGrid()
case let .colorsChanged(surfaceID, colors):
guard surfaceID == remoteSurfaceID else { return }
Expand Down Expand Up @@ -623,16 +621,19 @@ final class CloudTuiManualMirrorSession {
break
}
}

private func applyReplay(_ bytes: Data, reset: Bool) {
if reset {
// Drop every remote color before the reset rather than trusting
// RIS to do it: the replay's own sidecar re-applies the authored
// set in full, so the pane ends in the same state either way.
applyColors(CloudTuiRemoteColors())
surface?.processRemoteOutput(Self.replayReset)
private func applyReplay(_ bytes: Data, colors: CloudTuiRemoteColors?) {
// A sidecar replaces authored colors; an absent sidecar preserves them.
// Restore the authoritative set after resetting the replacement VT state.
let replayColors = colors ?? appliedRemoteColors
var replay = CloudTuiRemoteColors().oscDelta(from: appliedRemoteColors)
replay.append(Self.replayReset)
replay.append(bytes)
replay.append(replayColors.oscBytes)
appliedRemoteColors = replayColors
guard let surface else { return }
surface.processRemoteReplay(replay) { [weak surface] in
surface?.forceRefresh(reason: "cloud.replay.applied")
}
surface?.processRemoteOutput(bytes)
}

/// The replay is theme-portable: it carries no palette or default-color
Expand Down Expand Up @@ -829,15 +830,12 @@ final class CloudTuiManualMirrorSession {
transitionToDisconnected(reason: .rejected("attachment superseded"))
return
}
if outcome == "passive" {
// Another view owns this terminal's geometry. Keep the local
// sample, but make the explicit claim the next operation so a
// focused pane can take authority back deterministically.
geometryClaimed = false
if outcome == "passive" || (accepted == false && geometryClaimed && lastRemoteGrid != nil && lastRemoteGrid != requestedGrid) {
(geometryClaimed, geometryClaimBlockedByPeer, geometryClaimLossPending) = (false, !explicitGeometryClaimPending, false)
claimUnsupported = false
} else if accepted == false && geometryClaimed && lastRemoteGrid == nil {
geometryClaimLossPending = true
}
// A report is useful even when it was passive. Hold the newest
// sample while the explicit geometry claim is in flight.
let next = resizeScheduler.acknowledge(
requestedGrid,
canSend: geometryClaimed || claimUnsupported
Expand All @@ -853,6 +851,7 @@ final class CloudTuiManualMirrorSession {
claimInFlight = false
if ok, surface?.isRendererPortalVisible == true {
geometryClaimed = true
explicitGeometryClaimPending = false
claimUnsupported = false
} else if Self.isUnsupportedClaimError(error) {
// Keep compatibility with protocol-v5/v6 peers. Their
Expand Down Expand Up @@ -971,10 +970,11 @@ final class CloudTuiManualMirrorSession {
surface?.isRendererPortalVisible == true,
surface?.isNativeViewInRealWindow == true,
geometryClaimEligible,
!geometryClaimBlockedByPeer,
!geometryClaimed,
!claimUnsupported,
!claimInFlight,
resizeScheduler.inFlight != nil || resizeScheduler.lastAcknowledged != nil,
resizeScheduler.lastAcknowledged != nil,
let connection else { return }
manualMirrorLogger.info("geometry terminal=\(self.terminalID, privacy: .private(mask: .hash)) decision=claim")
claimInFlight = true
Expand Down
10 changes: 5 additions & 5 deletions Sources/Workspace.swift
Original file line number Diff line number Diff line change
Expand Up @@ -639,11 +639,6 @@ extension Workspace {
)
}
guard let effectiveRestorableAgent else { return nil }
let confirmedRuntimeProcessIdentities = confirmedRuntimeAgentProcessIdentities(
for: effectiveRestorableAgent,
panelId: panelId,
currentProcessIdentity: currentAgentProcessIdentity
)
let matchingObservation = restorableAgentObservation?.matchingAgentSession(
kind: effectiveRestorableAgent.kind.rawValue,
sessionId: effectiveRestorableAgent.sessionId
Expand All @@ -655,6 +650,11 @@ extension Workspace {
) {
return true
}
let confirmedRuntimeProcessIdentities = confirmedRuntimeAgentProcessIdentities(
for: effectiveRestorableAgent,
panelId: panelId,
currentProcessIdentity: currentAgentProcessIdentity
)
guard let matchingObservation else { return false }
if let resumeBinding {
return matchingObservation.wasRunningForSnapshot(
Expand Down
Loading
Loading