Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
0b5958c
test: cover cloud shortcut failure diagnostics
lawrencecchen Sep 14, 2026
014275c
fix: retain and trace cloud terminal creation failures
lawrencecchen Sep 14, 2026
5d420ee
fix: keep terminal recovery messages accurate
lawrencecchen Sep 14, 2026
2bbaae9
test: cover cloud error card native layering and input
lawrencecchen Sep 14, 2026
8f1b812
fix: host cloud error controls above native surfaces
lawrencecchen Sep 14, 2026
02041c4
test: center cloud failures inside their source terminal
lawrencecchen Sep 15, 2026
1ffc35c
Center cloud failures on their terminal and prototype three designs
lawrencecchen Sep 15, 2026
4ff367b
Keep cloud failure placement tied to the visible source tab
lawrencecchen Sep 15, 2026
b743a66
refine cloud terminal failure card actions
lawrencecchen Sep 16, 2026
ccd3f5a
Merge main and share responsive Cloud failure presentation
lawrencecchen Sep 16, 2026
ebae092
Keep one provider failure classifier after main integration
lawrencecchen Sep 16, 2026
e0bcfab
test: repair cloud ownership test compilation
lawrencecchen Sep 17, 2026
9126888
Merge main and preserve cloud terminal failure overlays
lawrencecchen Sep 17, 2026
340f1df
debug: preview cloud failure styles in selected terminal
lawrencecchen Sep 17, 2026
bfae8dc
Merge remote-tracking branch 'refs/remotes/origin/cloud-conflicts-pr-…
lawrencecchen Sep 17, 2026
7be1862
debug: add bordered compact cloud error variant
lawrencecchen Sep 17, 2026
ec86d2a
ui: flatten bordered compact cloud error card
lawrencecchen Sep 17, 2026
83d1969
Merge remote-tracking branch 'origin/main' into feat-cloud-terminal-e…
lawrencecchen Sep 17, 2026
1023189
test: import pane directions in cloud focus tests
lawrencecchen Sep 17, 2026
52ad0ce
test: expose URL cancellation cleanup for cloud terminals
lawrencecchen Sep 17, 2026
a4d0030
fix: finish cancelled cloud requests and ship the approved flat card
lawrencecchen Sep 17, 2026
67a0eb9
Merge remote-tracking branch 'origin/main' into feat-cloud-terminal-e…
lawrencecchen Sep 17, 2026
99b9ad4
test: locate cloud overlay through the shared window root
lawrencecchen Sep 17, 2026
879c6db
Merge remote-tracking branch 'origin/main' into feat-cloud-terminal-e…
lawrencecchen Sep 17, 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
236 changes: 236 additions & 0 deletions Resources/Localizable.xcstrings
Original file line number Diff line number Diff line change
Expand Up @@ -527767,6 +527767,242 @@
}
}
}
},
"cloudPane.newTerminalFailed.invalidResult": {
"extractionState": "manual",
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "The machine did not return the new terminal. Refresh the machine before trying again."
}
},
"de": {
"stringUnit": {
"state": "translated",
"value": "Die Maschine hat das neue Terminal nicht zurückgegeben. Aktualisiere die Maschine, bevor du es erneut versuchst."
}
},
"fr": {
"stringUnit": {
"state": "translated",
"value": "La machine n’a pas renvoyé le nouveau terminal. Actualisez la machine avant de réessayer."
}
},
"ar": {
"stringUnit": {
"state": "translated",
"value": "لم يُرجع الجهاز الطرفية الجديدة. حدّث الجهاز قبل المحاولة مرة أخرى."
}
},
"es": {
"stringUnit": {
"state": "translated",
"value": "La máquina no devolvió la nueva terminal. Actualiza la máquina antes de volver a intentarlo."
}
},
"zh-Hant": {
"stringUnit": {
"state": "translated",
"value": "機器未傳回新的終端機。請先重新整理機器再試一次。"
}
},
"zh-Hans": {
"stringUnit": {
"state": "translated",
"value": "机器未返回新的终端。请先刷新机器再重试。"
}
},
"ko": {
"stringUnit": {
"state": "translated",
"value": "머신이 새 터미널을 반환하지 않았습니다. 다시 시도하기 전에 머신을 새로 고치세요."
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "マシンから新しいターミナルが返されませんでした。マシンを更新してから、もう一度お試しください。"
}
}
}
},
"cloudPane.newTerminalFailed.noWorkspace": {
"extractionState": "manual",
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "This machine has no available workspace. Refresh the machine and try again."
}
},
"de": {
"stringUnit": {
"state": "translated",
"value": "Auf dieser Maschine ist kein Arbeitsbereich verfügbar. Aktualisiere die Maschine und versuche es erneut."
}
},
"fr": {
"stringUnit": {
"state": "translated",
"value": "Aucun espace de travail n’est disponible sur cette machine. Actualisez la machine et réessayez."
}
},
"ar": {
"stringUnit": {
"state": "translated",
"value": "لا توجد مساحة عمل متاحة على هذا الجهاز. حدّث الجهاز وحاول مرة أخرى."
}
},
"es": {
"stringUnit": {
"state": "translated",
"value": "Esta máquina no tiene un espacio de trabajo disponible. Actualiza la máquina e inténtalo de nuevo."
}
},
"zh-Hant": {
"stringUnit": {
"state": "translated",
"value": "此機器沒有可用的工作區。請重新整理機器後再試一次。"
}
},
"zh-Hans": {
"stringUnit": {
"state": "translated",
"value": "此机器没有可用的工作区。请刷新机器后重试。"
}
},
"ko": {
"stringUnit": {
"state": "translated",
"value": "이 머신에 사용 가능한 작업 공간이 없습니다. 머신을 새로 고친 후 다시 시도하세요."
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "このマシンには利用可能なワークスペースがありません。マシンを更新して、もう一度お試しください。"
}
}
}
},
"cloudPane.newTerminalFailed.stateUnavailable": {
"extractionState": "manual",
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "The machine’s current state could not be loaded. Reconnect and try again."
}
},
"de": {
"stringUnit": {
"state": "translated",
"value": "Der aktuelle Zustand der Maschine konnte nicht geladen werden. Stelle die Verbindung erneut her und versuche es noch einmal."
}
},
"fr": {
"stringUnit": {
"state": "translated",
"value": "Impossible de charger l’état actuel de la machine. Reconnectez-vous et réessayez."
}
},
"ar": {
"stringUnit": {
"state": "translated",
"value": "تعذّر تحميل الحالة الحالية للجهاز. أعد الاتصال وحاول مرة أخرى."
}
},
"es": {
"stringUnit": {
"state": "translated",
"value": "No se pudo cargar el estado actual de la máquina. Vuelve a conectarte e inténtalo de nuevo."
}
},
"zh-Hant": {
"stringUnit": {
"state": "translated",
"value": "無法載入機器的目前狀態。請重新連線後再試一次。"
}
},
"zh-Hans": {
"stringUnit": {
"state": "translated",
"value": "无法加载机器的当前状态。请重新连接后重试。"
}
},
"ko": {
"stringUnit": {
"state": "translated",
"value": "머신의 현재 상태를 불러올 수 없습니다. 다시 연결한 후 시도하세요."
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "マシンの現在の状態を読み込めませんでした。再接続して、もう一度お試しください。"
}
}
}
},
"cloudPane.newTerminalFailed.shortTitle": {
"extractionState": "manual",
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Couldn’t open terminal"
}
},
"de": {
"stringUnit": {
"state": "translated",
"value": "Terminal konnte nicht geöffnet werden"
}
},
"fr": {
"stringUnit": {
"state": "translated",
"value": "Impossible d’ouvrir le terminal"
}
},
"ar": {
"stringUnit": {
"state": "translated",
"value": "تعذّر فتح الطرفية"
}
},
"es": {
"stringUnit": {
"state": "translated",
"value": "No se pudo abrir la terminal"
}
},
"zh-Hant": {
"stringUnit": {
"state": "translated",
"value": "無法開啟終端機"
}
},
"zh-Hans": {
"stringUnit": {
"state": "translated",
"value": "无法打开终端"
}
},
"ko": {
"stringUnit": {
"state": "translated",
"value": "터미널을 열 수 없습니다"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "ターミナルを開けませんでした"
}
}
}
}
},
"version": "1.0"
Expand Down
9 changes: 9 additions & 0 deletions Sources/Cloud/CloudDiagnosticFailure.swift
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,15 @@ enum CloudDiagnosticFailure: String, Codable, Sendable, Error {
}
}
if error is CloudMachineLinkManager.ManagerError { return .connectFailure(error) }
if let error = error as? SurfaceCatalogError {
switch error {
case .unknownResource, .destinationNotFound, .nothingToOpen: return .notFound
case .noProvider, .unavailable: return .network
case .ambiguousRemotePlacement: return .conflict
case .unsupported: return .unsupported
case .partialOperation: return .response
}
}
if error is DecodingError { return .response }
return .unknown
}
Expand Down
79 changes: 57 additions & 22 deletions Sources/CloudTerminalCreationCoordinator.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import Foundation
import os

nonisolated private let cloudTerminalCreationLogger = Logger(subsystem: "com.cmuxterm.app", category: "CloudTerminalCreation")

/// Coordinates one asynchronous Cloud terminal creation behind a reserved pane.
///
Expand All @@ -9,6 +12,7 @@ import Foundation
final class CloudTerminalCreationCoordinator {
typealias Create = @MainActor () async throws -> SurfaceResource
typealias Project = @MainActor (SurfaceResource) async throws -> (projection: SurfaceProjection, reused: Bool)
typealias Failure = @MainActor (Error, CloudOperationContext?) -> Void
typealias DiscardProjection = @MainActor (SurfaceProjection) -> Void

private let create: Create
Expand Down Expand Up @@ -41,6 +45,32 @@ final class CloudTerminalCreationCoordinator {
self.discardProjection = discardProjection
}

/// All Cloud terminal gestures establish a root before reaching the link.
/// Its process/snapshot spans and the copied failure share one Axiom trace.
static func perform<T>(
recorder: CloudOperationRecorder?,
file: StaticString = #fileID,
line: UInt = #line,
onFailure: Failure,
_ work: @MainActor () async throws -> T
) async rethrows -> T {
let context = recorder?.begin(.terminal, foreground: false, file: file, line: line)
return try await CloudOperationContext.$current.withValue(context) {
do {
let value = try await work()
if let context { await context.recorder.finish(context) }
return value
} catch {
if CloudDiagnosticFailure.classify(error) != .cancelled {
Comment thread
lawrencecchen marked this conversation as resolved.
cloudTerminalCreationLogger.error("Terminal creation failed: failure=\(CloudDiagnosticFailure.classify(error).rawValue, privacy: .public) trace=\(context?.traceID ?? "unavailable", privacy: .public) error=\(String(reflecting: error), privacy: .private)")
onFailure(error, context)
}
if let context { await context.recorder.finish(context, error: error) }
throw error
}
}
}

/// Begins creation or retries the last remote resource's local projection.
func start() {
// A repeated retry is still the same intent. Cancelling a create can
Expand All @@ -55,31 +85,36 @@ final class CloudTerminalCreationCoordinator {
if self.generation == operationGeneration { self.task = nil }
}
do {
let resource: SurfaceResource
if let createdResource = self.createdResource {
resource = createdResource
} else {
resource = try await self.create()
guard self.generation == operationGeneration else { return }
self.createdResource = resource
}
try Task.checkCancellation()
let projectionResult = try await self.project(resource)
guard self.generation == operationGeneration,
!Task.isCancelled else {
if !projectionResult.reused {
self.discardProjection(projectionResult.projection)
try await Self.perform(recorder: AppDelegate.shared?.cloudOperations, onFailure: { error, _ in
guard self.generation == operationGeneration, !Task.isCancelled else { return }
self.onFailure(error)
}) {
let resource: SurfaceResource
if let createdResource = self.createdResource {
resource = createdResource
} else {
resource = try await CloudOperationContext.phase(.provider, self.create)
guard self.generation == operationGeneration else { throw CancellationError() }
self.createdResource = resource
}
return
try Task.checkCancellation()
let projectionResult = try await CloudOperationContext.phase(.materialize) { try await self.project(resource) }
guard self.generation == operationGeneration,
!Task.isCancelled else {
if !projectionResult.reused {
self.discardProjection(projectionResult.projection)
}
throw CancellationError()
}
self.onSuccess()
}
self.onSuccess()
} catch is CancellationError {
if self.generation == operationGeneration { self.onCancel() }
return
} catch {
guard self.generation == operationGeneration,
!Task.isCancelled else { return }
self.onFailure(error)
guard self.generation == operationGeneration else { return }
if CloudDiagnosticFailure.classify(error) == .cancelled {
self.onCancel()
}
// perform already delivered non-cancellation failures inside its
// diagnostic context.
}
}
}
Expand Down
Loading
Loading