From 6db7e849250c49e74fedf529f4debf2f9a5f54e5 Mon Sep 17 00:00:00 2001 From: Leo Li Date: Wed, 14 Oct 2026 15:19:14 -0700 Subject: [PATCH 01/18] test: launch window offers Retry Now and Restart cmux-tui for a stuck owner (cx-jniyz.1) Behavior test on the app's real startup path against a stand-in cmux-tui script: Restart is offered only after a plain ensure failed with server.restore_timeout, runs the same ensure with --replace-unresponsive after a confirm, then connects again; a refusal says so and leaves Retry. Co-Authored-By: Claude Opus 5.5 --- .../CmuxNextAppTests/DaemonRestartTests.swift | 214 ++++++++++++++++++ 1 file changed, 214 insertions(+) create mode 100644 Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/DaemonRestartTests.swift diff --git a/Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/DaemonRestartTests.swift b/Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/DaemonRestartTests.swift new file mode 100644 index 000000000000..2067c2188fee --- /dev/null +++ b/Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/DaemonRestartTests.swift @@ -0,0 +1,214 @@ +import AppKit +@testable import CmuxNextApp +import CmuxNextDaemon +import CmuxNextDesign +import Foundation +import Testing + +/// cx-jniyz.1: a cmux-tui owner that accepts connections but never answers leaves the launch +/// window on "cmux-tui is unavailable" with nothing to press. The window offers Retry Now, and, +/// once a plain `server ensure` failed the way a stuck owner makes it fail (the daemon's restore +/// bound passed, or no answer), Restart cmux-tui: after a confirm it runs the same `server ensure` +/// with `--replace-unresponsive`, then tries to connect again. A refusal says so and leaves Retry. +/// +/// The app runs its real startup path against a stand-in `cmux-tui` script (never a real daemon, +/// never a signal): `server status` finds no owner, `server ensure` fails as the test says, and +/// `server ensure --replace-unresponsive` answers `replaced` or a refusal. +@MainActor @Suite(.serialized, .timeLimit(.minutes(2))) +struct DaemonRestartTests { + /// The stand-in's state: its call log and the files that pick its answers. + private struct StandIn { + let root: URL + var log: URL { root.appendingPathComponent("calls.log") } + /// Present: a plain ensure fails with `server.restore_timeout` (else `server.spawn_failed`). + var stuck: URL { root.appendingPathComponent("stuck") } + /// Present: `--replace-unresponsive` is refused (`server.replace_still_running`). + var refuse: URL { root.appendingPathComponent("refuse") } + + /// Each call's arguments, in call order. + func calls() -> [[String]] { + let text = (try? String(contentsOf: log, encoding: .utf8)) ?? "" + return text.split(separator: "\n").map { $0.split(separator: "\u{1F}", omittingEmptySubsequences: false).map(String.init) } + } + + func ensures() -> [[String]] { calls().filter { $0.contains("ensure") } } + + func mark(_ file: URL) { FileManager.default.createFile(atPath: file.path, contents: Data()) } + } + + private struct Launched { + let services: AppServices + let standIn: StandIn + + var view: DaemonConnectingView? { services.windows.controllers.first?.connectingView } + + func stop() { + CmuxDialogCenter.shared.dismissAll() + services.daemon.shutdownConnection() + for controller in services.windows.controllers { controller.window?.close() } + try? FileManager.default.removeItem(at: standIn.root) + } + } + + private static func standInScript(_ standIn: StandIn, session: String) -> String { + """ + #!/bin/sh + line="" + for arg in "$@"; do line="$line$(printf '\\037')$arg"; done + printf '%s\\n' "${line#?}" >> '\(standIn.log.path)' + case " $* " in + *" --version "*) echo 'cmux-tui 0.1.0'; exit 0 ;; + *" server status "*) exit 1 ;; + *" --replace-unresponsive "*) + if [ -e '\(standIn.refuse.path)' ]; then + echo '{"code":"server.replace_still_running","message":"cannot replace the unresponsive session owner","details":{},"retryable":false}' >&2 + exit 3 + fi + echo '{"status":"replaced","session":"\(session)","socket":"\(standIn.root.path)/gone.sock","pid":4242,"generation":"g-2","replaced_pid":4141,"message":"replaced"}' + exit 0 ;; + *" server ensure "*) + if [ -e '\(standIn.stuck.path)' ]; then + echo '{"code":"server.restore_timeout","message":"the session owner is still restoring","details":{},"retryable":false}' >&2 + else + echo '{"code":"server.spawn_failed","message":"could not start the session owner","details":{},"retryable":false}' >&2 + fi + exit 3 ;; + esac + exit 64 + """ + } + + /// The app's startup path (launch window, startup loop, first attempt) on the stand-in. + private static func launch(stuck: Bool, refuse: Bool = false) throws -> Launched { + let root = FileManager.default.temporaryDirectory.appendingPathComponent("cn-restart-\(UUID().uuidString.prefix(8))") + try FileManager.default.createDirectory(at: root, withIntermediateDirectories: true) + let standIn = StandIn(root: root) + if stuck { standIn.mark(standIn.stuck) } + if refuse { standIn.mark(standIn.refuse) } + let session = "cn-restart-\(UUID().uuidString.prefix(8).lowercased())" + let binary = root.appendingPathComponent("cmux-tui") + try standInScript(standIn, session: session).write(to: binary, atomically: true, encoding: .utf8) + try FileManager.default.setAttributes([.posixPermissions: 0o755], ofItemAtPath: binary.path) + // As the app's launcher: bundled (so `--restore-progress`) with page origins (`--ws-loopback`). + let launcher = DaemonLauncher( + configuration: .init(binary: binary, session: session, stateDirectory: root.appendingPathComponent("state"), + binaryIsBundled: true, webSocketPageOrigins: ["http://127.0.0.1:1"]), + environment: { ["HOME": NSHomeDirectory(), "PATH": "/usr/bin:/bin"] }) + _ = NSApplication.shared + let environment = AppEnvironment.current([:]) + let services = AppServices(environment: environment) + services.windows.ordersWindowsIn = false + let prestart = DaemonPrestart(launcher: launcher, configuration: DaemonConnection.Configuration(terminalEnvironment: nil)) + services.daemon.start(launch: environment.launch, terminalEnvironment: [:], terminalEnvironmentProvider: { [:] }, + prestart: prestart) + services.windows.restoreWhenLoaded() + return Launched(services: services, standIn: standIn) + } + + /// Polls until `done` holds or `limit` passes (as `RapidWorkspaceCreateTests` does). + private static func wait(within limit: Duration, until done: () -> Bool) async { + let deadline = ContinuousClock.now + limit + while !done(), ContinuousClock.now < deadline, !Task.isCancelled { + try? await Task.sleep(for: .milliseconds(100)) + } + } + + private static func control(_ identifier: String, in view: NSView?, as type: T.Type) -> T? { + guard let view else { return nil } + if view.accessibilityIdentifier() == identifier, let match = view as? T { return match } + for subview in view.subviews { + if let match = control(identifier, in: subview, as: type) { return match } + } + return nil + } + + private static func shown(_ identifier: String, in view: NSView?) -> NSButton? { + guard let button = control(identifier, in: view, as: NSButton.self), !button.isHiddenOrHasHiddenAncestor else { return nil } + return button + } + + /// The button's own target and action (a click outside an ordered-in window may never dispatch). + private static func press(_ button: NSButton) throws { + let action = try #require(button.action) + _ = (try #require(button.target) as AnyObject).perform(action, with: button) + } + + /// Waits for the launch window's failure, so a fixture that never got there fails here. + private static func unavailableView(_ launched: Launched) async throws -> DaemonConnectingView { + await wait(within: .seconds(40)) { launched.view?.state.isUnavailable == true } + let view = try #require(launched.view, "the launch window never showed the connecting view") + try #require(view.state.isUnavailable, "the launch window never showed the failure") + return view + } + + /// Presses Restart cmux-tui and confirms the question it asks. + private static func restartAndConfirm(_ view: DaemonConnectingView) async throws { + try press(try #require(shown("daemon.restart", in: view))) + await wait(within: .seconds(5)) { CmuxDialogCenter.shared.records.contains { $0.spec.identifier == "cmux.dialog.restartDaemon" } } + let question = try #require(CmuxDialogCenter.shared.records.last { $0.spec.identifier == "cmux.dialog.restartDaemon" }) + #expect(!question.spec.lines.isEmpty) + #expect(question.spec.cancelButton != nil) + #expect(CmuxDialogCenter.shared.press(question.id, button: "restart")) + } + + @Test func restartIsOfferedOnlyAfterAPlainEnsureFailedAndReplacesThenConnectsAgain() async throws { + let launched = try Self.launch(stuck: false) + defer { launched.stop() } + let standIn = launched.standIn + let view = try await Self.unavailableView(launched) + + // A start failure that is not a stuck owner: Retry Now only. + let retry = Self.shown("daemon.retryNow", in: view) + #expect(retry != nil) + #expect(Self.shown("daemon.restart", in: view) == nil) + #expect(standIn.ensures().allSatisfy { !$0.contains("--replace-unresponsive") }) + + // The owner is stuck now: Retry Now asks again, and the failure offers Restart. + standIn.mark(standIn.stuck) + let before = standIn.ensures().count + try Self.press(try #require(retry)) + await Self.wait(within: .seconds(10)) { Self.shown("daemon.restart", in: view) != nil } + #expect(standIn.ensures().count > before) + #expect(Self.shown("daemon.restart", in: view) != nil) + + try await Self.restartAndConfirm(view) + await Self.wait(within: .seconds(10)) { + let ensures = standIn.ensures() + guard let replace = ensures.firstIndex(where: { $0.contains("--replace-unresponsive") }) else { return false } + return ensures[(replace + 1)...].contains { !$0.contains("--replace-unresponsive") } + } + let ensures = standIn.ensures() + let replaceIndex = try #require(ensures.firstIndex { $0.contains("--replace-unresponsive") }) + let plain = try #require(ensures[.. Date: Wed, 14 Oct 2026 15:30:12 -0700 Subject: [PATCH 02/18] feat: Retry Now and Restart cmux-tui for a stuck owner (cx-jniyz.1) The launch window's failure and the App Store's disconnected banner offer Retry Now (fires the service's retryWake) and, once a plain server ensure failed as a stuck owner makes it fail (server.restore_timeout, server.unavailable or no answer in time), Restart cmux-tui. Restart asks first in the cmux dialog (terminals running in a stuck cmux-tui may be lost), then runs the app's own ensure argv plus --replace-unresponsive and fires retryWake. The replace call allows 30 s of stdout silence (10 s probe + 5 s TERM + 5 s KILL) before restore progress applies. A refusal (server.replace_*) shows a plain line and leaves Retry Now. The gate lives on the launcher (EnsureOutcome), so the startup loop, the first attempt and every reconnect feed it. Strings in all 21 languages. Co-Authored-By: Claude Opus 5.5 --- .../CmuxNextApp/Apps/AppsService.swift | 4 +- .../CmuxNextApp/DaemonConnectingView.swift | 24 +- .../CmuxNextApp/DaemonRecoveryControls.swift | 140 +++ .../Resources/Localizable.xcstrings | 924 ++++++++++++++++++ .../Windows/WindowController.swift | 2 +- .../Client/AppsAvailability.swift | 8 + .../CmuxNextApps/Store/AppStoreModel.swift | 4 + .../Store/Views/AppStoreRootView.swift | 4 +- .../Store/Views/AppsDisconnectedBanner.swift | 44 +- .../Launch/DaemonLauncher+Build.swift | 20 + .../Launch/DaemonLauncher+Replace.swift | 118 +++ .../Launch/DaemonLauncher.swift | 24 +- .../DaemonRecovery.swift | 64 ++ .../DaemonService+Endpoint.swift | 11 + .../CmuxNextDaemonService/DaemonService.swift | 10 +- 15 files changed, 1363 insertions(+), 38 deletions(-) create mode 100644 Packages/macOS/CmuxNext/Sources/CmuxNextApp/DaemonRecoveryControls.swift create mode 100644 Packages/macOS/CmuxNext/Sources/CmuxNextDaemon/Launch/DaemonLauncher+Replace.swift create mode 100644 Packages/macOS/CmuxNext/Sources/CmuxNextDaemonService/DaemonRecovery.swift diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Apps/AppsService.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Apps/AppsService.swift index a7755325d3ae..fa9b68b5cd1a 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Apps/AppsService.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Apps/AppsService.swift @@ -2,6 +2,7 @@ import AppKit import CmuxNextActions import CmuxNextApps import CmuxNextControl +import CmuxNextDaemonService import CmuxNextIcons import CmuxNextPages import CmuxNextSettings @@ -34,8 +35,9 @@ final class AppsService { /// The App Store tabs (internal page), one store model per tab over the /// one client. Captures the client, not the service, so a page set that /// outlives the service still makes models. - private(set) lazy var storePages = AppStorePages { [weak self, client = self.client] in + private(set) lazy var storePages = AppStorePages { [weak self, client = self.client, recovery = self.services.daemon.recovery] in let model = AppStoreModel(client: client) + model.recovery = { DaemonRestartPrompt.appsRecovery(recovery) } model.onNavigate = { self?.services.locationTrail.pageHistoryDidChange() } return model } diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextApp/DaemonConnectingView.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextApp/DaemonConnectingView.swift index d0c491ae8cfa..5929b822c684 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextApp/DaemonConnectingView.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextApp/DaemonConnectingView.swift @@ -1,5 +1,6 @@ import AppKit import CmuxNextDaemon +import CmuxNextDaemonService import CmuxNextDesign import CmuxNextWakeups @@ -10,12 +11,14 @@ import CmuxNextWakeups /// "Connecting to cmux-tui" follows after `launchTextDelay`, so a fast /// launch replaces it before either shows. Once the startup deadline /// passes, it shows the failure at once, in plain words, with the raw error -/// behind Copy Details for a bug report. +/// behind Copy Details for a bug report, Retry Now and, for a stuck owner, +/// Restart cmux-tui (`DaemonRecoveryControls`, cx-jniyz.1). final class DaemonConnectingView: NSView { let mark = LaunchMarkView() private let titleLabel = NSTextField(labelWithString: "") private let detailLabel = NSTextField(wrappingLabelWithString: "") private let copyDetailsButton = ThemedBezelButton(title: "", target: nil, action: nil) + private let recoveryControls: DaemonRecoveryControls? /// The failure's raw text, for Copy Details. private var failureDetails = "" /// Copy Details' write (tests read it instead of the clipboard). @@ -33,10 +36,17 @@ final class DaemonConnectingView: NSView { self.init(frame: frame, clock: ContinuousClock()) } - /// - Parameter clock: Times the mark and status delays (tests step it). - init(frame: NSRect, clock: any Clock) { + convenience init(frame: NSRect, recovery: DaemonRecovery) { + self.init(frame: frame, clock: ContinuousClock(), recovery: recovery) + } + + /// - Parameters: + /// - clock: Times the mark and status delays (tests step it). + /// - recovery: The local daemon's Retry Now and Restart; nil shows neither. + init(frame: NSRect, clock: any Clock, recovery: DaemonRecovery? = nil) { markTimer = DemandTimer(owner: "launch.mark", clock: clock) statusTimer = DemandTimer(owner: "launch.status", clock: clock) + recoveryControls = recovery.map(DaemonRecoveryControls.init(recovery:)) super.init(frame: frame) // Layer-backed, so the status line's fade sets its model alpha at once. wantsLayer = true @@ -52,7 +62,9 @@ final class DaemonConnectingView: NSView { copyDetailsButton.target = self copyDetailsButton.action = #selector(copyDetails) copyDetailsButton.isHidden = true - let stack = NSStackView(views: [mark, titleLabel, detailLabel, copyDetailsButton]) + var views: [NSView] = [mark, titleLabel, detailLabel] + if let recoveryControls { views.append(recoveryControls) } + let stack = NSStackView(views: views + [copyDetailsButton]) stack.orientation = .vertical stack.alignment = .centerX stack.spacing = Metrics.space2 @@ -122,6 +134,7 @@ final class DaemonConnectingView: NSView { detailLabel.stringValue = "" detailLabel.isHidden = true copyDetailsButton.isHidden = true + recoveryControls?.isOffered = false staggerIfConnecting() case .unavailable(let error): cancelStagger() @@ -139,6 +152,7 @@ final class DaemonConnectingView: NSView { detailLabel.isHidden = detailLabel.stringValue.isEmpty failureDetails = String(describing: error) copyDetailsButton.isHidden = false + recoveryControls?.isOffered = true } setAccessibilityLabel([titleLabel.stringValue, detailLabel.stringValue].filter { !$0.isEmpty }.joined(separator: ". ")) } @@ -159,6 +173,6 @@ final class DaemonConnectingView: NSView { } @objc private func copyDetails() { - copyToClipboard(failureDetails) + copyToClipboard([failureDetails, recoveryControls?.restartFailure].compactMap { $0 }.joined(separator: "\n")) } } diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextApp/DaemonRecoveryControls.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextApp/DaemonRecoveryControls.swift new file mode 100644 index 000000000000..ae2d05c02fa1 --- /dev/null +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextApp/DaemonRecoveryControls.swift @@ -0,0 +1,140 @@ +import AppKit +import CmuxNextApps +import CmuxNextCompat +import CmuxNextDaemonService +import CmuxNextDesign + +/// Strings of the daemon recovery controls (Resources/Localizable.xcstrings). +enum DaemonRecoveryStrings { + static var retryNow: String { String(localized: "daemon.recovery.retryNow", defaultValue: "Retry Now", bundle: .module) } + static var restart: String { String(localized: "daemon.recovery.restart", defaultValue: "Restart cmux-tui", bundle: .module) } + static var confirmTitle: String { String(localized: "daemon.recovery.confirmTitle", defaultValue: "Restart cmux-tui?", bundle: .module) } + static var confirmBody: String { + String(localized: "daemon.recovery.confirmBody", + defaultValue: "cmux-tui is not responding. Restarting it stops the stuck process and starts a new one. Terminals running in it may be lost.", + bundle: .module) + } + static var confirmButton: String { String(localized: "daemon.recovery.confirmButton", defaultValue: "Restart", bundle: .module) } + static var restarting: String { String(localized: "daemon.recovery.restarting", defaultValue: "Restarting cmux-tui…", bundle: .module) } + static var failed: String { + String(localized: "daemon.recovery.failed", defaultValue: "cmux-tui could not be restarted. Try again in a moment.", bundle: .module) + } +} + +/// Restart cmux-tui's question and the App Store banner's controls, one path +/// for both surfaces (cx-jniyz.1). The question is the cmux dialog every +/// destructive confirm uses, so the debug socket can answer it. +enum DaemonRestartPrompt { + static let confirmID = "restart" + static let identifier = "cmux.dialog.restartDaemon" + + /// Asks before Restart cmux-tui (terminals of a stuck owner may be lost), + /// then runs it. `window` scopes the question; none asks app-wide. + static func confirm(_ recovery: DaemonRecovery, in window: NSWindow?) { + let spec = CmuxDialogSpec(title: DaemonRecoveryStrings.confirmTitle, lines: [DaemonRecoveryStrings.confirmBody], + buttons: [.cancel(), CmuxDialogButton(id: confirmID, title: DaemonRecoveryStrings.confirmButton, role: .default)], + identifier: identifier) + CmuxDialogCenter.shared.present(spec, in: window.map(CmuxDialogScope.window) ?? .app) { answer in + guard answer.button == confirmID else { return } + // task-owner: one restart per confirm; DaemonRecovery ignores another while one runs + Task { await recovery.restart() } + } + } + + /// What the restart shows while it runs or after it failed; nil otherwise. + static func status(_ recovery: DaemonRecovery) -> String? { + if recovery.isRestarting { return DaemonRecoveryStrings.restarting } + return recovery.restartFailure == nil ? nil : DaemonRecoveryStrings.failed + } + + /// The App Store's disconnected banner controls (AppStoreModel.recovery). + static func appsRecovery(_ recovery: DaemonRecovery) -> AppsRecovery { + guard recovery.canRetry else { return AppsRecovery(actions: [], status: nil) } + var actions = [AppsRecoveryAction(id: "retryNow", title: DaemonRecoveryStrings.retryNow, isEnabled: !recovery.isRestarting) { + recovery.retryNow() + }] + if recovery.offersRestart || recovery.isRestarting { + actions.append(AppsRecoveryAction(id: "restart", title: DaemonRecoveryStrings.restart, isEnabled: !recovery.isRestarting) { + confirm(recovery, in: NSApp.keyWindow) + }) + } + return AppsRecovery(actions: actions, status: status(recovery)) + } +} + +/// Retry Now and Restart cmux-tui under the launch window's failure +/// (cx-jniyz.1). Restart shows only once a plain ensure failed as a stuck +/// owner makes it fail (`DaemonRecovery.offersRestart`). +final class DaemonRecoveryControls: NSStackView { + private let recovery: DaemonRecovery + private let statusLabel = NSTextField(wrappingLabelWithString: "") + private let retryButton = ThemedBezelButton(title: "", target: nil, action: nil) + private let restartButton = ThemedBezelButton(title: "", target: nil, action: nil) + private var observation: Task? + /// The window shows a failure: the controls show only then. + var isOffered = false { didSet { refresh() } } + + init(recovery: DaemonRecovery) { + self.recovery = recovery + super.init(frame: .zero) + orientation = .vertical + alignment = .centerX + spacing = Metrics.space2 + statusLabel.font = Typography.caption + statusLabel.alignment = .center + statusLabel.isSelectable = true + statusLabel.setAccessibilityIdentifier("daemon.recoveryStatus") + Self.configure(retryButton, title: DaemonRecoveryStrings.retryNow, identifier: "daemon.retryNow", target: self, action: #selector(retryNow)) + Self.configure(restartButton, title: DaemonRecoveryStrings.restart, identifier: "daemon.restart", target: self, action: #selector(confirmRestart)) + let row = NSStackView(views: [retryButton, restartButton]) + row.orientation = .horizontal + row.spacing = Metrics.space2 + addArrangedSubview(statusLabel) + addArrangedSubview(row) + refresh() + // task-owner: ends after this view is gone (weak self), at the recovery's next change + observation = Task { [weak self, recovery] in + for await _ in ObservationStream({ (recovery.offersRestart, recovery.isRestarting, recovery.restartFailure) }) { + guard let self else { return } + self.refresh() + } + } + } + + @available(*, unavailable) + required init?(coder: NSCoder) { fatalError("init(coder:) is not supported") } + + private static func configure(_ button: NSButton, title: String, identifier: String, target: AnyObject, action: Selector) { + button.title = title + button.controlSize = .small + button.target = target + button.action = action + button.setAccessibilityIdentifier(identifier) + } + + override func viewDidChangeEffectiveAppearance() { + super.viewDidChangeEffectiveAppearance() + performWithTheme { statusLabel.textColor = Palette.textSecondary } + } + + /// The restart's failure, for Copy Details. + var restartFailure: String? { recovery.restartFailure } + + private func refresh() { + let restarting = recovery.isRestarting + isHidden = !isOffered || !recovery.canRetry + retryButton.isEnabled = !restarting + restartButton.isHidden = !(recovery.offersRestart || restarting) + restartButton.isEnabled = !restarting + statusLabel.stringValue = DaemonRestartPrompt.status(recovery) ?? "" + statusLabel.isHidden = statusLabel.stringValue.isEmpty + } + + @objc private func retryNow() { + recovery.retryNow() + } + + @objc private func confirmRestart() { + DaemonRestartPrompt.confirm(recovery, in: window) + } +} diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Resources/Localizable.xcstrings b/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Resources/Localizable.xcstrings index 3054d27b730e..91fd5e1b355a 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Resources/Localizable.xcstrings +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Resources/Localizable.xcstrings @@ -396,6 +396,930 @@ } } }, + "daemon.recovery.retryNow": { + "comment": "Button under the launch window's cmux-tui failure and on the App Store's disconnected banner: try to connect to cmux-tui now instead of at the next retry.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Retry Now" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "今すぐ再試行" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "إعادة المحاولة الآن" + } + }, + "bs": { + "stringUnit": { + "state": "translated", + "value": "Pokušaj ponovo sada" + } + }, + "da": { + "stringUnit": { + "state": "translated", + "value": "Prøv igen nu" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Jetzt erneut versuchen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Reintentar ahora" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Réessayer maintenant" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Riprova ora" + } + }, + "km": { + "stringUnit": { + "state": "translated", + "value": "ព្យាយាមម្ដងទៀតឥឡូវនេះ" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "지금 다시 시도" + } + }, + "nb": { + "stringUnit": { + "state": "translated", + "value": "Prøv igjen nå" + } + }, + "pl": { + "stringUnit": { + "state": "translated", + "value": "Spróbuj teraz" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Tentar agora" + } + }, + "ru": { + "stringUnit": { + "state": "translated", + "value": "Повторить сейчас" + } + }, + "th": { + "stringUnit": { + "state": "translated", + "value": "ลองอีกครั้งตอนนี้" + } + }, + "tr": { + "stringUnit": { + "state": "translated", + "value": "Şimdi Yeniden Dene" + } + }, + "uk": { + "stringUnit": { + "state": "translated", + "value": "Повторити зараз" + } + }, + "vi": { + "stringUnit": { + "state": "translated", + "value": "Thử lại ngay" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "立即重试" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "立即重試" + } + } + } + }, + "daemon.recovery.restart": { + "comment": "Button shown after cmux-tui stopped answering: stops the stuck cmux-tui process and starts a new one (asks first).", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Restart cmux-tui" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "cmux-tui を再起動" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "إعادة تشغيل cmux-tui" + } + }, + "bs": { + "stringUnit": { + "state": "translated", + "value": "Ponovo pokreni cmux-tui" + } + }, + "da": { + "stringUnit": { + "state": "translated", + "value": "Genstart cmux-tui" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "cmux-tui neu starten" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Reiniciar cmux-tui" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Redémarrer cmux-tui" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Riavvia cmux-tui" + } + }, + "km": { + "stringUnit": { + "state": "translated", + "value": "ចាប់ផ្ដើម cmux-tui ឡើងវិញ" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "cmux-tui 재시작" + } + }, + "nb": { + "stringUnit": { + "state": "translated", + "value": "Start cmux-tui på nytt" + } + }, + "pl": { + "stringUnit": { + "state": "translated", + "value": "Uruchom ponownie cmux-tui" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Reiniciar o cmux-tui" + } + }, + "ru": { + "stringUnit": { + "state": "translated", + "value": "Перезапустить cmux-tui" + } + }, + "th": { + "stringUnit": { + "state": "translated", + "value": "รีสตาร์ต cmux-tui" + } + }, + "tr": { + "stringUnit": { + "state": "translated", + "value": "cmux-tui'yi Yeniden Başlat" + } + }, + "uk": { + "stringUnit": { + "state": "translated", + "value": "Перезапустити cmux-tui" + } + }, + "vi": { + "stringUnit": { + "state": "translated", + "value": "Khởi động lại cmux-tui" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "重启 cmux-tui" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "重新啟動 cmux-tui" + } + } + } + }, + "daemon.recovery.confirmTitle": { + "comment": "Title of the question before Restart cmux-tui.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Restart cmux-tui?" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "cmux-tui を再起動しますか?" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "هل تريد إعادة تشغيل cmux-tui؟" + } + }, + "bs": { + "stringUnit": { + "state": "translated", + "value": "Ponovo pokrenuti cmux-tui?" + } + }, + "da": { + "stringUnit": { + "state": "translated", + "value": "Genstart cmux-tui?" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "cmux-tui neu starten?" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "¿Reiniciar cmux-tui?" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Redémarrer cmux-tui ?" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Riavviare cmux-tui?" + } + }, + "km": { + "stringUnit": { + "state": "translated", + "value": "ចាប់ផ្ដើម cmux-tui ឡើងវិញ?" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "cmux-tui를 재시작할까요?" + } + }, + "nb": { + "stringUnit": { + "state": "translated", + "value": "Starte cmux-tui på nytt?" + } + }, + "pl": { + "stringUnit": { + "state": "translated", + "value": "Uruchomić ponownie cmux-tui?" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Reiniciar o cmux-tui?" + } + }, + "ru": { + "stringUnit": { + "state": "translated", + "value": "Перезапустить cmux-tui?" + } + }, + "th": { + "stringUnit": { + "state": "translated", + "value": "รีสตาร์ต cmux-tui ไหม" + } + }, + "tr": { + "stringUnit": { + "state": "translated", + "value": "cmux-tui yeniden başlatılsın mı?" + } + }, + "uk": { + "stringUnit": { + "state": "translated", + "value": "Перезапустити cmux-tui?" + } + }, + "vi": { + "stringUnit": { + "state": "translated", + "value": "Khởi động lại cmux-tui?" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "要重启 cmux-tui 吗?" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "要重新啟動 cmux-tui 嗎?" + } + } + } + }, + "daemon.recovery.confirmBody": { + "comment": "Body of the question before Restart cmux-tui. Terminals usually survive, but a stuck process may lose them, so the text says they may be lost.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "cmux-tui is not responding. Restarting it stops the stuck process and starts a new one. Terminals running in it may be lost." + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "cmux-tui が応答していません。再起動すると、停止したプロセスを終了して新しいプロセスを開始します。その中で実行中のターミナルが失われる可能性があります。" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "cmux-tui لا يستجيب. تؤدي إعادة تشغيله إلى إيقاف العملية العالقة وبدء عملية جديدة. قد تُفقد الطرفيات التي تعمل فيه." + } + }, + "bs": { + "stringUnit": { + "state": "translated", + "value": "cmux-tui ne odgovara. Ponovnim pokretanjem zaustavlja se zaglavljeni proces i pokreće novi. Terminali koji rade u njemu mogu biti izgubljeni." + } + }, + "da": { + "stringUnit": { + "state": "translated", + "value": "cmux-tui svarer ikke. En genstart stopper den fastlåste proces og starter en ny. Terminaler, der kører i den, kan gå tabt." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "cmux-tui reagiert nicht. Ein Neustart beendet den hängenden Prozess und startet einen neuen. Darin laufende Terminals können verloren gehen." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "cmux-tui no responde. Al reiniciarlo se detiene el proceso bloqueado y se inicia uno nuevo. Los terminales que se ejecutan en él podrían perderse." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "cmux-tui ne répond pas. Le redémarrer arrête le processus bloqué et en lance un nouveau. Les terminaux qui y sont ouverts pourraient être perdus." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "cmux-tui non risponde. Il riavvio arresta il processo bloccato e ne avvia uno nuovo. I terminali in esecuzione al suo interno potrebbero andare persi." + } + }, + "km": { + "stringUnit": { + "state": "translated", + "value": "cmux-tui មិនឆ្លើយតបទេ។ ការចាប់ផ្ដើមវាឡើងវិញនឹងបញ្ឈប់ដំណើរការដែលជាប់គាំង ហើយចាប់ផ្ដើមដំណើរការថ្មីមួយ។ ទែមីណាល់ដែលកំពុងដំណើរការនៅក្នុងវាអាចបាត់បង់។" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "cmux-tui가 응답하지 않습니다. 재시작하면 멈춘 프로세스를 중지하고 새 프로세스를 시작합니다. 그 안에서 실행 중인 터미널이 사라질 수 있습니다." + } + }, + "nb": { + "stringUnit": { + "state": "translated", + "value": "cmux-tui svarer ikke. En omstart stopper den fastlåste prosessen og starter en ny. Terminaler som kjører i den, kan gå tapt." + } + }, + "pl": { + "stringUnit": { + "state": "translated", + "value": "cmux-tui nie odpowiada. Ponowne uruchomienie zatrzyma zawieszony proces i uruchomi nowy. Terminale działające w nim mogą zostać utracone." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "O cmux-tui não está respondendo. Reiniciá-lo encerra o processo travado e inicia um novo. Os terminais em execução nele podem ser perdidos." + } + }, + "ru": { + "stringUnit": { + "state": "translated", + "value": "cmux-tui не отвечает. Перезапуск остановит зависший процесс и запустит новый. Терминалы, работающие в нём, могут быть потеряны." + } + }, + "th": { + "stringUnit": { + "state": "translated", + "value": "cmux-tui ไม่ตอบสนอง การรีสตาร์ตจะหยุดโปรเซสที่ค้างและเริ่มโปรเซสใหม่ เทอร์มินัลที่ทำงานอยู่ในนั้นอาจสูญหาย" + } + }, + "tr": { + "stringUnit": { + "state": "translated", + "value": "cmux-tui yanıt vermiyor. Yeniden başlatmak takılan süreci durdurur ve yenisini başlatır. İçinde çalışan terminaller kaybolabilir." + } + }, + "uk": { + "stringUnit": { + "state": "translated", + "value": "cmux-tui не відповідає. Перезапуск зупинить завислий процес і запустить новий. Термінали, що працюють у ньому, можуть бути втрачені." + } + }, + "vi": { + "stringUnit": { + "state": "translated", + "value": "cmux-tui không phản hồi. Khởi động lại sẽ dừng tiến trình bị treo và chạy một tiến trình mới. Các terminal đang chạy trong đó có thể bị mất." + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "cmux-tui 没有响应。重启会停止卡住的进程并启动一个新进程。其中运行的终端可能会丢失。" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "cmux-tui 沒有回應。重新啟動會停止卡住的程序並啟動新的程序。其中執行中的終端機可能會遺失。" + } + } + } + }, + "daemon.recovery.confirmButton": { + "comment": "Confirm button of the Restart cmux-tui question.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Restart" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "再起動" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "إعادة التشغيل" + } + }, + "bs": { + "stringUnit": { + "state": "translated", + "value": "Ponovo pokreni" + } + }, + "da": { + "stringUnit": { + "state": "translated", + "value": "Genstart" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Neu starten" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Reiniciar" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Redémarrer" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Riavvia" + } + }, + "km": { + "stringUnit": { + "state": "translated", + "value": "ចាប់ផ្ដើមឡើងវិញ" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "재시작" + } + }, + "nb": { + "stringUnit": { + "state": "translated", + "value": "Start på nytt" + } + }, + "pl": { + "stringUnit": { + "state": "translated", + "value": "Uruchom ponownie" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Reiniciar" + } + }, + "ru": { + "stringUnit": { + "state": "translated", + "value": "Перезапустить" + } + }, + "th": { + "stringUnit": { + "state": "translated", + "value": "รีสตาร์ต" + } + }, + "tr": { + "stringUnit": { + "state": "translated", + "value": "Yeniden Başlat" + } + }, + "uk": { + "stringUnit": { + "state": "translated", + "value": "Перезапустити" + } + }, + "vi": { + "stringUnit": { + "state": "translated", + "value": "Khởi động lại" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "重启" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "重新啟動" + } + } + } + }, + "daemon.recovery.restarting": { + "comment": "Status line while cmux-tui is being restarted (up to about 20 seconds).", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Restarting cmux-tui…" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "cmux-tui を再起動しています…" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "جارٍ إعادة تشغيل cmux-tui…" + } + }, + "bs": { + "stringUnit": { + "state": "translated", + "value": "Ponovno pokretanje cmux-tui…" + } + }, + "da": { + "stringUnit": { + "state": "translated", + "value": "Genstarter cmux-tui…" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "cmux-tui wird neu gestartet…" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Reiniciando cmux-tui…" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Redémarrage de cmux-tui…" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Riavvio di cmux-tui…" + } + }, + "km": { + "stringUnit": { + "state": "translated", + "value": "កំពុងចាប់ផ្ដើម cmux-tui ឡើងវិញ…" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "cmux-tui 재시작 중…" + } + }, + "nb": { + "stringUnit": { + "state": "translated", + "value": "Starter cmux-tui på nytt…" + } + }, + "pl": { + "stringUnit": { + "state": "translated", + "value": "Ponowne uruchamianie cmux-tui…" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Reiniciando o cmux-tui…" + } + }, + "ru": { + "stringUnit": { + "state": "translated", + "value": "Перезапуск cmux-tui…" + } + }, + "th": { + "stringUnit": { + "state": "translated", + "value": "กำลังรีสตาร์ต cmux-tui…" + } + }, + "tr": { + "stringUnit": { + "state": "translated", + "value": "cmux-tui yeniden başlatılıyor…" + } + }, + "uk": { + "stringUnit": { + "state": "translated", + "value": "Перезапуск cmux-tui…" + } + }, + "vi": { + "stringUnit": { + "state": "translated", + "value": "Đang khởi động lại cmux-tui…" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "正在重启 cmux-tui…" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "正在重新啟動 cmux-tui…" + } + } + } + }, + "daemon.recovery.failed": { + "comment": "Status line after Restart cmux-tui did not work; Retry Now stays next to it.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "cmux-tui could not be restarted. Try again in a moment." + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "cmux-tui を再起動できませんでした。しばらくしてからもう一度お試しください。" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "تعذّرت إعادة تشغيل cmux-tui. حاول مرة أخرى بعد قليل." + } + }, + "bs": { + "stringUnit": { + "state": "translated", + "value": "cmux-tui nije moguće ponovo pokrenuti. Pokušajte ponovo za trenutak." + } + }, + "da": { + "stringUnit": { + "state": "translated", + "value": "cmux-tui kunne ikke genstartes. Prøv igen om lidt." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "cmux-tui konnte nicht neu gestartet werden. Versuche es gleich noch einmal." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "No se pudo reiniciar cmux-tui. Inténtalo de nuevo en un momento." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Impossible de redémarrer cmux-tui. Réessayez dans un instant." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Impossibile riavviare cmux-tui. Riprova tra un momento." + } + }, + "km": { + "stringUnit": { + "state": "translated", + "value": "មិនអាចចាប់ផ្ដើម cmux-tui ឡើងវិញបានទេ។ សូមព្យាយាមម្ដងទៀតបន្តិចទៀត។" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "cmux-tui를 재시작할 수 없습니다. 잠시 후 다시 시도하세요." + } + }, + "nb": { + "stringUnit": { + "state": "translated", + "value": "Kunne ikke starte cmux-tui på nytt. Prøv igjen om litt." + } + }, + "pl": { + "stringUnit": { + "state": "translated", + "value": "Nie udało się ponownie uruchomić cmux-tui. Spróbuj ponownie za chwilę." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Não foi possível reiniciar o cmux-tui. Tente novamente em instantes." + } + }, + "ru": { + "stringUnit": { + "state": "translated", + "value": "Не удалось перезапустить cmux-tui. Повторите попытку чуть позже." + } + }, + "th": { + "stringUnit": { + "state": "translated", + "value": "ไม่สามารถรีสตาร์ต cmux-tui ได้ โปรดลองอีกครั้งในอีกสักครู่" + } + }, + "tr": { + "stringUnit": { + "state": "translated", + "value": "cmux-tui yeniden başlatılamadı. Birazdan tekrar deneyin." + } + }, + "uk": { + "stringUnit": { + "state": "translated", + "value": "Не вдалося перезапустити cmux-tui. Спробуйте ще раз за мить." + } + }, + "vi": { + "stringUnit": { + "state": "translated", + "value": "Không thể khởi động lại cmux-tui. Hãy thử lại sau giây lát." + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "无法重启 cmux-tui。请稍后再试。" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "無法重新啟動 cmux-tui。請稍後再試。" + } + } + } + }, "daemon.reinstall": { "extractionState": "manual", "localizations": { diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Windows/WindowController.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Windows/WindowController.swift index 2f4e6bc6358f..427b3cade5bf 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Windows/WindowController.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Windows/WindowController.swift @@ -179,7 +179,7 @@ final class WindowController: NSWindowController, NSWindowDelegate { /// The connecting (or unavailable) state of the local daemon's first /// connection, until a workspace can be shown. private func showConnecting() { - let view = connectingView ?? DaemonConnectingView(frame: .zero) + let view = connectingView ?? DaemonConnectingView(frame: .zero, recovery: services.daemon.recovery) connectingView = view root.show(view) themeScope.show(nil) diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextApps/Client/AppsAvailability.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextApps/Client/AppsAvailability.swift index cab463761e28..362c20502d30 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextApps/Client/AppsAvailability.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextApps/Client/AppsAvailability.swift @@ -13,6 +13,14 @@ public nonisolated enum AppsUnavailableReason: Sendable, Hashable { /// The reason as shown (banner, app sections, action refusals). public var text: String { AppsStrings.unavailable(self) } + + /// cmux-tui itself is not connected (Retry Now and Restart cmux-tui can help). + public var daemonUnreachable: Bool { + switch self { + case .notConnected, .notReady: true + case .needsNewerDaemon, .turnedOff: false + } + } } /// Whether the supervisor answers. `epoch` changes on every (re)connect: diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextApps/Store/AppStoreModel.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextApps/Store/AppStoreModel.swift index 2bb378dfda75..8463260e28fa 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextApps/Store/AppStoreModel.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextApps/Store/AppStoreModel.swift @@ -53,6 +53,10 @@ public final class AppStoreModel { /// Called after every page navigation (the titlebar arrows re-read /// ``canGoBack`` and ``canGoForward``). @ObservationIgnored public var onNavigate: (() -> Void)? + /// The App's recovery controls for the disconnected banner while + /// cmux-tui is not connected (Retry Now, Restart cmux-tui); read in the + /// banner's body, so it follows what they observe. Nil offers none. + @ObservationIgnored public var recovery: (() -> AppsRecovery)? @ObservationIgnored private let removalTimer = DemandTimer(owner: "AppStoreModel.removal") @ObservationIgnored private var removalWasEnabled = true /// How long a Remove can be undone before it is committed. diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextApps/Store/Views/AppStoreRootView.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextApps/Store/Views/AppStoreRootView.swift index b9f2d9618fa6..36c3e9b246b7 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextApps/Store/Views/AppStoreRootView.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextApps/Store/Views/AppStoreRootView.swift @@ -21,7 +21,9 @@ public struct AppStoreRootView: View { VStack(spacing: 0) { toolbar Rectangle().fill(colors.separator).frame(height: Borders.width(1)) - if let reason = model.client.unavailableReason { AppsDisconnectedBanner(reason: reason) } + if let reason = model.client.unavailableReason { + AppsDisconnectedBanner(reason: reason, recovery: reason.daemonUnreachable ? model.recovery?() : nil) + } switch model.tab { case .discover: AppDiscoverView(model: model) case .installed: AppInstalledView(model: model) diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextApps/Store/Views/AppsDisconnectedBanner.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextApps/Store/Views/AppsDisconnectedBanner.swift index ca925aeb664e..5daba0904eba 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextApps/Store/Views/AppsDisconnectedBanner.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextApps/Store/Views/AppsDisconnectedBanner.swift @@ -3,9 +3,11 @@ import CmuxNextIcons import SwiftUI /// Why nothing can change: the supervisor is unreachable. Every control -/// below is disabled; nothing queues. +/// below is disabled; nothing queues. While cmux-tui itself cannot be reached +/// the banner carries the App's recovery controls (Retry Now, Restart cmux-tui). struct AppsDisconnectedBanner: View { let reason: AppsUnavailableReason + var recovery: AppsRecovery? @Environment(\.appSceneColors) private var colors var body: some View { @@ -14,12 +16,50 @@ struct AppsDisconnectedBanner: View { Text(AppsStrings.unavailable(reason)).font(Font(Typography.bodyEmphasized)) Text(AppsStrings.unavailableHelp).font(Font(Typography.caption)).foregroundStyle(colors.tertiary) Spacer() + if let recovery { + if let status = recovery.status { + Text(status).font(Font(Typography.caption)).foregroundStyle(colors.tertiary) + } + ForEach(recovery.actions) { action in + Button(action.title) { action.run() } + .controlSize(.small) + .disabled(!action.isEnabled) + .accessibilityIdentifier("appStore.disconnected.\(action.id)") + } + } } .foregroundStyle(colors.secondary) .padding(.horizontal, Metrics.space5) .padding(.vertical, Metrics.space2) .background(colors.hover) - .accessibilityElement(children: .combine) + .accessibilityElement(children: .contain) .accessibilityIdentifier("appStore.disconnected") } } + +/// The App's controls on the disconnected banner while cmux-tui cannot be +/// reached (``AppStoreModel/recovery``), and what a restart shows. +public nonisolated struct AppsRecovery { + public var actions: [AppsRecoveryAction] + public var status: String? + + public init(actions: [AppsRecoveryAction], status: String?) { + self.actions = actions + self.status = status + } +} + +/// One control on the disconnected banner. +public nonisolated struct AppsRecoveryAction: Identifiable { + public let id: String + public let title: String + public let isEnabled: Bool + public let run: @MainActor () -> Void + + public init(id: String, title: String, isEnabled: Bool, run: @escaping @MainActor () -> Void) { + self.id = id + self.title = title + self.isEnabled = isEnabled + self.run = run + } +} diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextDaemon/Launch/DaemonLauncher+Build.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextDaemon/Launch/DaemonLauncher+Build.swift index 70db69580067..84747097ef75 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextDaemon/Launch/DaemonLauncher+Build.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextDaemon/Launch/DaemonLauncher+Build.swift @@ -22,4 +22,24 @@ extension DaemonLauncher { } return (session, stateDirectory) } + + /// Build commit of the bundled binary (`cmux 0.1.0 (; ghostty …)`). + public func bundledBuildCommit() async throws -> String? { + let result = try await ProcessRunner.run(executable: configuration.binary, arguments: ["--version"], + environment: nil, timeout: .seconds(5), clock: clock) + return Self.parseBuildCommit(String(decoding: result.stdout, as: UTF8.self)) + } + + static func parseBuildCommit(_ version: String) -> String? { + guard version.contains("(") else { return nil } + let commit = version.drop { $0 != "(" }.dropFirst().prefix { $0.isHexDigit } + return commit.count >= 7 ? String(commit) : nil + } + + /// True when the running daemon was built from a different commit than + /// the bundled binary, so the app should hand off with `restartDaemon`. + public func isStale(_ identity: DaemonIdentity) async -> Bool { + guard let running = identity.buildCommit, let bundled = try? await bundledBuildCommit() else { return false } + return running != bundled + } } diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextDaemon/Launch/DaemonLauncher+Replace.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextDaemon/Launch/DaemonLauncher+Replace.swift new file mode 100644 index 000000000000..3d29f611e46f --- /dev/null +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextDaemon/Launch/DaemonLauncher+Replace.swift @@ -0,0 +1,118 @@ +import CmuxNextCompat +import Foundation + +/// "Restart cmux-tui" (cx-jniyz.1): `server ensure --replace-unresponsive` +/// stops a session owner that accepts connections but never answers, then +/// ensures as usual (status `replaced`, or `running`/`started` when nothing +/// was stuck). The verb cannot tell a stuck owner from one still restoring, +/// so the app offers it only after a plain ensure failed the way a stuck owner +/// makes it fail (``EnsureOutcome``): the daemon's restore bound passed, or no +/// answer. The owner's terminal hosts survive a replace and the next owner +/// adopts them. +extension DaemonLauncher { + /// `server ensure --replace-unresponsive` refused (exit 3, `server.replace_*`): + /// the owner was not stopped. + public struct ReplaceRefusal: Error, Sendable, Equatable, CustomStringConvertible { + public var code: String + public var message: String? + public var description: String { "\(message ?? "replace refused") [\(code)]" } + } + + /// The longest a replace stays silent on stdout before its first line: + /// the 10 s identify probe, 5 s after SIGTERM and 5 s after SIGKILL, plus margin. + static let replaceSilence: Duration = .seconds(30) + + /// Runs the app's `server ensure` with `--replace-unresponsive` and + /// returns its result. Throws ``ReplaceRefusal`` when the owner was not + /// stopped, and the usual ensure errors when the new owner did not start. + @concurrent public func replaceUnresponsive() async throws -> EnsureResult { + async let reapGrace = supportsReapGrace() + let environment = await ensureEnvironment() + let progress = configuration.binaryIsBundled + let arguments = Self.ensureArguments(configuration, reapGrace: await reapGrace, progress: progress) + + ["--replace-unresponsive"] + // No progress line while the old owner is probed and stopped: the + // silence bound covers that, then restore progress bounds the rest. + let result = try await ProcessRunner.run( + executable: configuration.binary, + arguments: arguments, + environment: environment, + stdin: configuration.installKey?.payload, + timeout: Self.ensureRestoreCap + Self.replaceSilence, + progressIdle: progress ? Self.replaceSilence : nil, + clock: clock + ) + if result.status != 0, let failure = Self.ensureFailure(result), failure.code.hasPrefix("server.replace_") { + throw ReplaceRefusal(code: failure.code, message: failure.message) + } + let ensured = try Self.parseEnsure(result) + ensureOutcome.note(nil) + return ensured + } + + /// ``ensure()`` for the endpoint provider, recording how it ended in ``ensureOutcome``. + func ensureRecordingOutcome() async throws -> EnsureResult { + do { + let ensured = try await ensure() + ensureOutcome.note(nil) + return ensured + } catch { + ensureOutcome.note(error) + throw error + } + } + + /// The JSON error of a failed `server ensure` (the last JSON line on stderr). + static func ensureFailure(_ result: ProcessResult) -> EnsureFailure? { + let line = String(decoding: result.stderr, as: UTF8.self) + .split(whereSeparator: \.isNewline) + .last { $0.trimmingCharacters(in: .whitespaces).hasPrefix("{") } + return line.flatMap { try? JSONDecoder().decode(EnsureFailure.self, from: Data($0.utf8)) } + } + + /// True for a plain ensure failure a stuck owner causes: the restore bound + /// passed (`server.restore_timeout`), the ensure itself got no answer in + /// time, or no owner became ready (`server.unavailable`). + static func isStuckOwnerFailure(_ error: any Error) -> Bool { + switch error as? DaemonError { + case .sessionRestoreTimedOut?, .timedOut?: true + case .launchFailed(let detail)?: detail.contains("\"server.unavailable\"") + default: false + } + } +} + +/// Whether the launcher's last `server ensure` failed the way a stuck owner +/// makes it fail (``DaemonLauncher/isStuckOwnerFailure(_:)``), the gate of +/// "Restart cmux-tui". Shared by every copy of one launcher. +public final class EnsureOutcome: Sendable { + private struct State { + var ownerLooksStuck = false + var observer: (@MainActor @Sendable () -> Void)? + } + + private let state = Mutex(State()) + + public init() {} + + /// The last ensure failed as a stuck owner makes it fail; false after one that succeeded. + public var ownerLooksStuck: Bool { state.withLock { $0.ownerLooksStuck } } + + /// Calls `observer` on the main actor after each change of ``ownerLooksStuck``. + public func observe(_ observer: @escaping @MainActor @Sendable () -> Void) { + state.withLock { $0.observer = observer } + } + + /// Records how an ensure ended (nil: it succeeded). + func note(_ error: (any Error)?) { + let stuck = error.map(DaemonLauncher.isStuckOwnerFailure) ?? false + let observer = state.withLock { state -> (@MainActor @Sendable () -> Void)? in + guard state.ownerLooksStuck != stuck else { return nil } + state.ownerLooksStuck = stuck + return state.observer + } + guard let observer else { return } + // task-owner: one main-actor hop per change; the observer reads the current value + Task { @MainActor in observer() } + } +} diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextDaemon/Launch/DaemonLauncher.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextDaemon/Launch/DaemonLauncher.swift index 2d16fb7a5fa3..6820c21b77fa 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextDaemon/Launch/DaemonLauncher.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextDaemon/Launch/DaemonLauncher.swift @@ -99,6 +99,8 @@ public struct DaemonLauncher: Sendable { private let rememberedSocket: RememberedSocket /// Version handoff state (`handOffIfStale`). let handoff = DaemonHandoffState() + /// How the last endpoint `ensure` ended: the gate of Restart cmux-tui (DaemonLauncher+Replace). + public let ensureOutcome = EnsureOutcome() let clock: any Clock private let ensureTimeout: Duration /// Hard cap on one `server ensure`. The daemon bounds a restore itself @@ -417,30 +419,10 @@ public struct DaemonLauncher: Sendable { return DaemonEndpoint(socketPath: path) } } - return try await ensure().endpoint + return try await ensureRecordingOutcome().endpoint } } - /// Build commit of the bundled binary (`cmux 0.1.0 (; ghostty …)`). - public func bundledBuildCommit() async throws -> String? { - let result = try await ProcessRunner.run(executable: configuration.binary, arguments: ["--version"], - environment: nil, timeout: .seconds(5), clock: clock) - return Self.parseBuildCommit(String(decoding: result.stdout, as: UTF8.self)) - } - - static func parseBuildCommit(_ version: String) -> String? { - guard version.contains("(") else { return nil } - let commit = version.drop { $0 != "(" }.dropFirst().prefix { $0.isHexDigit } - return commit.count >= 7 ? String(commit) : nil - } - - /// True when the running daemon was built from a different commit than - /// the bundled binary, so the app should hand off with `restartDaemon`. - public func isStale(_ identity: DaemonIdentity) async -> Bool { - guard let running = identity.buildCommit, let bundled = try? await bundledBuildCommit() else { return false } - return running != bundled - } - /// Asks the running owner to exit (version handoff), then ensures a new /// one from this binary. PTY hosts survive and are adopted. public func restartDaemon(identity: DaemonIdentity, using connection: DaemonConnection) async throws -> EnsureResult { diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextDaemonService/DaemonRecovery.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextDaemonService/DaemonRecovery.swift new file mode 100644 index 000000000000..7d5fc3dabba9 --- /dev/null +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextDaemonService/DaemonRecovery.swift @@ -0,0 +1,64 @@ +import CmuxNextDaemon +import CmuxNextWakeups +public import Observation +import os + +/// The local daemon's recovery while it cannot be reached (cx-jniyz.1): the +/// launch window and the App Store's disconnected banner offer Retry Now and, +/// only after a plain `server ensure` failed as a stuck owner makes it fail +/// (``offersRestart``), Restart cmux-tui. Remote machines have no launcher, +/// so they offer neither. +@MainActor +@Observable +public final class DaemonRecovery { + /// The last `server ensure` failed as a stuck owner makes it fail (the + /// restore bound passed, or no answer): `--replace-unresponsive` cannot + /// tell a stuck owner from a slow restore, so it is offered only then. + public private(set) var offersRestart = false + /// A Restart runs (up to 20 s of probe and stop, then the new owner's start). + public private(set) var isRestarting = false + /// Why the last Restart did not work (the refusal code or error, for Copy + /// Details); nil after Retry Now or a Restart that worked. + public private(set) var restartFailure: String? + @ObservationIgnored private var launcher: DaemonLauncher? + @ObservationIgnored private var wake: RetryWake? + @ObservationIgnored private let logger = Logger(subsystem: "com.cmuxterm.app.next", category: "app.daemon") + + public init() {} + + /// True once the local daemon's launcher is known (`DaemonService.start(launch:)`). + public var canRetry: Bool { wake != nil } + + /// Follows `launcher`'s ensure outcomes; `wake` is the service's retry wake. + func attach(_ launcher: DaemonLauncher, wake: RetryWake) { + self.launcher = launcher + self.wake = wake + let outcome = launcher.ensureOutcome + outcome.observe { [weak self] in self?.offersRestart = outcome.ownerLooksStuck } + offersRestart = outcome.ownerLooksStuck + } + + /// Tries to connect now instead of at the next backoff step. + public func retryNow() { + restartFailure = nil + wake?.fire() + } + + /// Stops the unresponsive owner and starts a new one (`server ensure + /// --replace-unresponsive`), then tries to connect at once. SIGTERM + /// leaves the terminal hosts running and the new owner adopts them. + public func restart() async { + guard let launcher, !isRestarting else { return } + isRestarting = true + restartFailure = nil + do { + let result = try await launcher.replaceUnresponsive() + logger.notice("cmux-tui restart: \(result.status, privacy: .public), pid \(result.pid)") + } catch { + restartFailure = String(describing: error) + logger.error("cmux-tui restart failed: \(String(describing: error), privacy: .public)") + } + isRestarting = false + wake?.fire() + } +} diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextDaemonService/DaemonService+Endpoint.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextDaemonService/DaemonService+Endpoint.swift index 942399c0437e..bb83a49400ca 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextDaemonService/DaemonService+Endpoint.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextDaemonService/DaemonService+Endpoint.swift @@ -1,4 +1,5 @@ public import CmuxNextDaemon +import Foundation /// The daemon's capabilities and its socket, for callers that open their own connection. extension DaemonService { @@ -27,3 +28,13 @@ extension DaemonService { return CreatingTerminalInput(terminalID: id, connection: connection, targetKnown: targetKnown) } } + +/// Which machine this service reaches. +extension DaemonService { + public var isLocal: Bool { machineID == "local" } + + /// Directory new terminals start in: the Mac's home for the local + /// daemon; nil (the machine's own default) on a Cloud machine, where a + /// Mac path does not exist. + public var defaultCwd: String? { isLocal ? NSHomeDirectory() : nil } +} diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextDaemonService/DaemonService.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextDaemonService/DaemonService.swift index 17e2d5dd199f..6c4bc74243d0 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextDaemonService/DaemonService.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextDaemonService/DaemonService.swift @@ -29,6 +29,8 @@ public final class DaemonService { public let store = DaemonStore() /// Set while an administrator turned this machine's feature off: no endpoint, no re-attach. public let policyBlock = PolicyBlock() + /// Retry Now and Restart cmux-tui while the local daemon cannot be reached (cx-jniyz.1). + public let recovery = DaemonRecovery() public private(set) var connection: DaemonConnection? public private(set) var windowState: WindowStateStore? /// The current (or last) daemon's identity. The store owns it and @@ -76,13 +78,6 @@ public final class DaemonService { ) { _ in wake.fire() } } - public var isLocal: Bool { machineID == "local" } - - /// Directory new terminals start in: the Mac's home for the local - /// daemon; nil (the machine's own default) on a Cloud machine, where a - /// Mac path does not exist. - public var defaultCwd: String? { isLocal ? NSHomeDirectory() : nil } - /// How the first connection is going (window connecting state, control /// errors). Becomes `.unavailable` after `startupDeadline` or on an /// incompatible daemon; retrying continues in the background. @@ -134,6 +129,7 @@ public final class DaemonService { } } endpointProvider = launcher.endpointProvider + recovery.attach(launcher, wake: retryWake) // Before the first connection the retry loop has no socket watch of its own (the connection installs one // after its first handshake): watch the socket the last launch used and its ready marker, so a daemon // that finishes a long restore or handoff wakes a loop past its timed budget (cx-4bd8m). From 50786b8de5b725ed2517055c8e36d36ec5d37afd Mon Sep 17 00:00:00 2001 From: Leo Li Date: Wed, 14 Oct 2026 15:58:49 -0700 Subject: [PATCH 03/18] fix: a cancelled ensure leaves the Restart gate alone; recovery clears a stale failure (cx-jniyz.1) Review follow-ups: a torn-down connection's cancelled ensure no longer hides Restart, and an ensure that works clears an earlier Restart's failure so a later disconnect does not show it. Co-Authored-By: Claude Opus 5.5 --- .../CmuxNextDaemon/Launch/DaemonLauncher+Replace.swift | 3 ++- .../Sources/CmuxNextDaemonService/DaemonRecovery.swift | 7 ++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextDaemon/Launch/DaemonLauncher+Replace.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextDaemon/Launch/DaemonLauncher+Replace.swift index 3d29f611e46f..e0de6f6703dd 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextDaemon/Launch/DaemonLauncher+Replace.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextDaemon/Launch/DaemonLauncher+Replace.swift @@ -57,7 +57,8 @@ extension DaemonLauncher { ensureOutcome.note(nil) return ensured } catch { - ensureOutcome.note(error) + // A cancelled ensure (its connection torn down, or shutdown) says nothing about the owner. + if !Task.isCancelled, !(error is CancellationError) { ensureOutcome.note(error) } throw error } } diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextDaemonService/DaemonRecovery.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextDaemonService/DaemonRecovery.swift index 7d5fc3dabba9..7e6190aeebf2 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextDaemonService/DaemonRecovery.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextDaemonService/DaemonRecovery.swift @@ -34,7 +34,12 @@ public final class DaemonRecovery { self.launcher = launcher self.wake = wake let outcome = launcher.ensureOutcome - outcome.observe { [weak self] in self?.offersRestart = outcome.ownerLooksStuck } + outcome.observe { [weak self] in + guard let self else { return } + offersRestart = outcome.ownerLooksStuck + // An ensure that worked (the owner recovered by itself) ends an earlier Restart's failure. + if !offersRestart { restartFailure = nil } + } offersRestart = outcome.ownerLooksStuck } From 0e03c31e7a435476e38f4b0ca71520448f12cc9e Mon Sep 17 00:00:00 2001 From: Leo Li Date: Wed, 14 Oct 2026 21:42:14 -0700 Subject: [PATCH 04/18] refactor: launch screen and daemon recovery controls get their own target (cx-jniyz.1) CmuxNextApp went 156 lines over its size cap with the Restart cmux-tui and Retry Now controls (SW14: new features go in their own target). The new CmuxNextDaemonRecovery target holds DaemonConnectingView, the recovery controls, the App Store banner's recovery actions and their 12 strings; the app keeps the wiring. CmuxNextApp is now 113559 lines, 170 under the base 113729, which also clears the base's +37 over the cap. Co-Authored-By: Claude Opus 5.5 --- Packages/macOS/CmuxNext/Package.swift | 13 +- .../CmuxNextApp/Apps/AppsService.swift | 1 + .../Resources/Localizable.xcstrings | 1579 ---------------- .../Sources/CmuxNextApp/Strings.swift | 10 - .../Windows/WindowController.swift | 4 +- .../DaemonConnectingView.swift | 24 +- .../DaemonRecoveryControls.swift | 20 +- .../Resources/Localizable.xcstrings | 1585 +++++++++++++++++ .../CmuxNextAppTests/DaemonRestartTests.swift | 1 + .../DaemonUnavailableScreenTests.swift | 1 + .../ThemedNoticeControlsTests.swift | 1 + Packages/macOS/CmuxNext/ci-target-graph.json | 15 + 12 files changed, 1644 insertions(+), 1610 deletions(-) rename Packages/macOS/CmuxNext/Sources/{CmuxNextApp => CmuxNextDaemonRecovery}/DaemonConnectingView.swift (92%) rename Packages/macOS/CmuxNext/Sources/{CmuxNextApp => CmuxNextDaemonRecovery}/DaemonRecoveryControls.swift (86%) create mode 100644 Packages/macOS/CmuxNext/Sources/CmuxNextDaemonRecovery/Resources/Localizable.xcstrings diff --git a/Packages/macOS/CmuxNext/Package.swift b/Packages/macOS/CmuxNext/Package.swift index 3f2887c11188..5383810eea4f 100644 --- a/Packages/macOS/CmuxNext/Package.swift +++ b/Packages/macOS/CmuxNext/Package.swift @@ -184,6 +184,7 @@ let package = Package( "CmuxNextFocusUI", "CmuxNextComputerUse", "CmuxNextAgentQuestion", + "CmuxNextDaemonRecovery", .product(name: "CmuxAgentQuestion", package: "CmuxAgentQuestion"), .product(name: "CmuxHomeCore", package: "CmuxHomeCore"), .product(name: "CmuxHomeRender", package: "CmuxHomeRender"), @@ -347,6 +348,16 @@ let package = Package( ), // The agent question card (plans/cmux-next/agent-questions.md): one // AppKit view and layout for Home rows, agent panes and the UI Gallery. + // The launch window's connecting and failure screen with Retry Now and + // Restart cmux-tui, and the App Store banner's recovery actions (cx-jniyz.1). + .target( + name: "CmuxNextDaemonRecovery", + dependencies: ["CmuxNextApps", "CmuxNextCompat", "CmuxNextDaemon", "CmuxNextDaemonService", "CmuxNextDesign", "CmuxNextWakeups"], + resources: [ + .process("Resources"), + ], + swiftSettings: uiSwiftSettings + ), .target( name: "CmuxNextAgentQuestion", dependencies: [ @@ -923,7 +934,7 @@ let package = Package( .testTarget( name: "CmuxNextAppTests", dependencies: ["CmuxNextWakeups", "CmuxNextNowPlaying", "CmuxNextLayout", "CmuxNextTasks", "CmuxNextAgentCursor", "CmuxNextTabs", "CmuxNextProcessEnvironment", "CmuxNextApp", "CmuxNextAgentLanes", "CmuxNextBrowserHistory", "CmuxNextCrashReporting", "CmuxNextActions", "CmuxNextHistory", "CmuxNextCopyMode", - "CmuxNextComputerUse", "CmuxNextDaemon", "CmuxNextDaemonService", "CmuxNextRemoteLocalhostService", "CmuxNextFocusUI", "CmuxNextHome", "CmuxNextHomeEngine", .product(name: "CmuxHomeCore", package: "CmuxHomeCore"), + "CmuxNextComputerUse", "CmuxNextDaemon", "CmuxNextDaemonRecovery", "CmuxNextDaemonService", "CmuxNextRemoteLocalhostService", "CmuxNextFocusUI", "CmuxNextHome", "CmuxNextHomeEngine", .product(name: "CmuxHomeCore", package: "CmuxHomeCore"), .product(name: "CmuxHomeRender", package: "CmuxHomeRender"), .product(name: "CmuxAgentQuestion", package: "CmuxAgentQuestion"), // GhosttyGlassMinimumContrastTests builds a Ghostty config. diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Apps/AppsService.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Apps/AppsService.swift index fa9b68b5cd1a..8372f93a4021 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Apps/AppsService.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Apps/AppsService.swift @@ -2,6 +2,7 @@ import AppKit import CmuxNextActions import CmuxNextApps import CmuxNextControl +import CmuxNextDaemonRecovery import CmuxNextDaemonService import CmuxNextIcons import CmuxNextPages diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Resources/Localizable.xcstrings b/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Resources/Localizable.xcstrings index 43604f5f18fe..9a0d3cbe590f 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Resources/Localizable.xcstrings +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Resources/Localizable.xcstrings @@ -265,1323 +265,6 @@ } } }, - "daemon.copyDetails": { - "extractionState": "manual", - "localizations": { - "en": { - "stringUnit": { - "state": "translated", - "value": "Copy Details" - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "詳細をコピー" - } - }, - "ar": { - "stringUnit": { - "state": "translated", - "value": "نسخ التفاصيل" - } - }, - "bs": { - "stringUnit": { - "state": "translated", - "value": "Kopiraj detalje" - } - }, - "da": { - "stringUnit": { - "state": "translated", - "value": "Kopiér detaljer" - } - }, - "de": { - "stringUnit": { - "state": "translated", - "value": "Details kopieren" - } - }, - "es": { - "stringUnit": { - "state": "translated", - "value": "Copiar detalles" - } - }, - "fr": { - "stringUnit": { - "state": "translated", - "value": "Copier les détails" - } - }, - "it": { - "stringUnit": { - "state": "translated", - "value": "Copia dettagli" - } - }, - "km": { - "stringUnit": { - "state": "translated", - "value": "ចម្លងព័ត៌មានលម្អិត" - } - }, - "ko": { - "stringUnit": { - "state": "translated", - "value": "세부 정보 복사" - } - }, - "nb": { - "stringUnit": { - "state": "translated", - "value": "Kopier detaljer" - } - }, - "pl": { - "stringUnit": { - "state": "translated", - "value": "Kopiuj szczegóły" - } - }, - "pt-BR": { - "stringUnit": { - "state": "translated", - "value": "Copiar detalhes" - } - }, - "ru": { - "stringUnit": { - "state": "translated", - "value": "Скопировать подробности" - } - }, - "th": { - "stringUnit": { - "state": "translated", - "value": "คัดลอกรายละเอียด" - } - }, - "tr": { - "stringUnit": { - "state": "translated", - "value": "Ayrıntıları Kopyala" - } - }, - "uk": { - "stringUnit": { - "state": "translated", - "value": "Скопіювати подробиці" - } - }, - "vi": { - "stringUnit": { - "state": "translated", - "value": "Sao chép chi tiết" - } - }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "复制详细信息" - } - }, - "zh-Hant": { - "stringUnit": { - "state": "translated", - "value": "拷貝詳細資訊" - } - } - } - }, - "daemon.recovery.retryNow": { - "comment": "Button under the launch window's cmux-tui failure and on the App Store's disconnected banner: try to connect to cmux-tui now instead of at the next retry.", - "extractionState": "manual", - "localizations": { - "en": { - "stringUnit": { - "state": "translated", - "value": "Retry Now" - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "今すぐ再試行" - } - }, - "ar": { - "stringUnit": { - "state": "translated", - "value": "إعادة المحاولة الآن" - } - }, - "bs": { - "stringUnit": { - "state": "translated", - "value": "Pokušaj ponovo sada" - } - }, - "da": { - "stringUnit": { - "state": "translated", - "value": "Prøv igen nu" - } - }, - "de": { - "stringUnit": { - "state": "translated", - "value": "Jetzt erneut versuchen" - } - }, - "es": { - "stringUnit": { - "state": "translated", - "value": "Reintentar ahora" - } - }, - "fr": { - "stringUnit": { - "state": "translated", - "value": "Réessayer maintenant" - } - }, - "it": { - "stringUnit": { - "state": "translated", - "value": "Riprova ora" - } - }, - "km": { - "stringUnit": { - "state": "translated", - "value": "ព្យាយាមម្ដងទៀតឥឡូវនេះ" - } - }, - "ko": { - "stringUnit": { - "state": "translated", - "value": "지금 다시 시도" - } - }, - "nb": { - "stringUnit": { - "state": "translated", - "value": "Prøv igjen nå" - } - }, - "pl": { - "stringUnit": { - "state": "translated", - "value": "Spróbuj teraz" - } - }, - "pt-BR": { - "stringUnit": { - "state": "translated", - "value": "Tentar agora" - } - }, - "ru": { - "stringUnit": { - "state": "translated", - "value": "Повторить сейчас" - } - }, - "th": { - "stringUnit": { - "state": "translated", - "value": "ลองอีกครั้งตอนนี้" - } - }, - "tr": { - "stringUnit": { - "state": "translated", - "value": "Şimdi Yeniden Dene" - } - }, - "uk": { - "stringUnit": { - "state": "translated", - "value": "Повторити зараз" - } - }, - "vi": { - "stringUnit": { - "state": "translated", - "value": "Thử lại ngay" - } - }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "立即重试" - } - }, - "zh-Hant": { - "stringUnit": { - "state": "translated", - "value": "立即重試" - } - } - } - }, - "daemon.recovery.restart": { - "comment": "Button shown after cmux-tui stopped answering: stops the stuck cmux-tui process and starts a new one (asks first).", - "extractionState": "manual", - "localizations": { - "en": { - "stringUnit": { - "state": "translated", - "value": "Restart cmux-tui" - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "cmux-tui を再起動" - } - }, - "ar": { - "stringUnit": { - "state": "translated", - "value": "إعادة تشغيل cmux-tui" - } - }, - "bs": { - "stringUnit": { - "state": "translated", - "value": "Ponovo pokreni cmux-tui" - } - }, - "da": { - "stringUnit": { - "state": "translated", - "value": "Genstart cmux-tui" - } - }, - "de": { - "stringUnit": { - "state": "translated", - "value": "cmux-tui neu starten" - } - }, - "es": { - "stringUnit": { - "state": "translated", - "value": "Reiniciar cmux-tui" - } - }, - "fr": { - "stringUnit": { - "state": "translated", - "value": "Redémarrer cmux-tui" - } - }, - "it": { - "stringUnit": { - "state": "translated", - "value": "Riavvia cmux-tui" - } - }, - "km": { - "stringUnit": { - "state": "translated", - "value": "ចាប់ផ្ដើម cmux-tui ឡើងវិញ" - } - }, - "ko": { - "stringUnit": { - "state": "translated", - "value": "cmux-tui 재시작" - } - }, - "nb": { - "stringUnit": { - "state": "translated", - "value": "Start cmux-tui på nytt" - } - }, - "pl": { - "stringUnit": { - "state": "translated", - "value": "Uruchom ponownie cmux-tui" - } - }, - "pt-BR": { - "stringUnit": { - "state": "translated", - "value": "Reiniciar o cmux-tui" - } - }, - "ru": { - "stringUnit": { - "state": "translated", - "value": "Перезапустить cmux-tui" - } - }, - "th": { - "stringUnit": { - "state": "translated", - "value": "รีสตาร์ต cmux-tui" - } - }, - "tr": { - "stringUnit": { - "state": "translated", - "value": "cmux-tui'yi Yeniden Başlat" - } - }, - "uk": { - "stringUnit": { - "state": "translated", - "value": "Перезапустити cmux-tui" - } - }, - "vi": { - "stringUnit": { - "state": "translated", - "value": "Khởi động lại cmux-tui" - } - }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "重启 cmux-tui" - } - }, - "zh-Hant": { - "stringUnit": { - "state": "translated", - "value": "重新啟動 cmux-tui" - } - } - } - }, - "daemon.recovery.confirmTitle": { - "comment": "Title of the question before Restart cmux-tui.", - "extractionState": "manual", - "localizations": { - "en": { - "stringUnit": { - "state": "translated", - "value": "Restart cmux-tui?" - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "cmux-tui を再起動しますか?" - } - }, - "ar": { - "stringUnit": { - "state": "translated", - "value": "هل تريد إعادة تشغيل cmux-tui؟" - } - }, - "bs": { - "stringUnit": { - "state": "translated", - "value": "Ponovo pokrenuti cmux-tui?" - } - }, - "da": { - "stringUnit": { - "state": "translated", - "value": "Genstart cmux-tui?" - } - }, - "de": { - "stringUnit": { - "state": "translated", - "value": "cmux-tui neu starten?" - } - }, - "es": { - "stringUnit": { - "state": "translated", - "value": "¿Reiniciar cmux-tui?" - } - }, - "fr": { - "stringUnit": { - "state": "translated", - "value": "Redémarrer cmux-tui ?" - } - }, - "it": { - "stringUnit": { - "state": "translated", - "value": "Riavviare cmux-tui?" - } - }, - "km": { - "stringUnit": { - "state": "translated", - "value": "ចាប់ផ្ដើម cmux-tui ឡើងវិញ?" - } - }, - "ko": { - "stringUnit": { - "state": "translated", - "value": "cmux-tui를 재시작할까요?" - } - }, - "nb": { - "stringUnit": { - "state": "translated", - "value": "Starte cmux-tui på nytt?" - } - }, - "pl": { - "stringUnit": { - "state": "translated", - "value": "Uruchomić ponownie cmux-tui?" - } - }, - "pt-BR": { - "stringUnit": { - "state": "translated", - "value": "Reiniciar o cmux-tui?" - } - }, - "ru": { - "stringUnit": { - "state": "translated", - "value": "Перезапустить cmux-tui?" - } - }, - "th": { - "stringUnit": { - "state": "translated", - "value": "รีสตาร์ต cmux-tui ไหม" - } - }, - "tr": { - "stringUnit": { - "state": "translated", - "value": "cmux-tui yeniden başlatılsın mı?" - } - }, - "uk": { - "stringUnit": { - "state": "translated", - "value": "Перезапустити cmux-tui?" - } - }, - "vi": { - "stringUnit": { - "state": "translated", - "value": "Khởi động lại cmux-tui?" - } - }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "要重启 cmux-tui 吗?" - } - }, - "zh-Hant": { - "stringUnit": { - "state": "translated", - "value": "要重新啟動 cmux-tui 嗎?" - } - } - } - }, - "daemon.recovery.confirmBody": { - "comment": "Body of the question before Restart cmux-tui. Terminals usually survive, but a stuck process may lose them, so the text says they may be lost.", - "extractionState": "manual", - "localizations": { - "en": { - "stringUnit": { - "state": "translated", - "value": "cmux-tui is not responding. Restarting it stops the stuck process and starts a new one. Terminals running in it may be lost." - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "cmux-tui が応答していません。再起動すると、停止したプロセスを終了して新しいプロセスを開始します。その中で実行中のターミナルが失われる可能性があります。" - } - }, - "ar": { - "stringUnit": { - "state": "translated", - "value": "cmux-tui لا يستجيب. تؤدي إعادة تشغيله إلى إيقاف العملية العالقة وبدء عملية جديدة. قد تُفقد الطرفيات التي تعمل فيه." - } - }, - "bs": { - "stringUnit": { - "state": "translated", - "value": "cmux-tui ne odgovara. Ponovnim pokretanjem zaustavlja se zaglavljeni proces i pokreće novi. Terminali koji rade u njemu mogu biti izgubljeni." - } - }, - "da": { - "stringUnit": { - "state": "translated", - "value": "cmux-tui svarer ikke. En genstart stopper den fastlåste proces og starter en ny. Terminaler, der kører i den, kan gå tabt." - } - }, - "de": { - "stringUnit": { - "state": "translated", - "value": "cmux-tui reagiert nicht. Ein Neustart beendet den hängenden Prozess und startet einen neuen. Darin laufende Terminals können verloren gehen." - } - }, - "es": { - "stringUnit": { - "state": "translated", - "value": "cmux-tui no responde. Al reiniciarlo se detiene el proceso bloqueado y se inicia uno nuevo. Los terminales que se ejecutan en él podrían perderse." - } - }, - "fr": { - "stringUnit": { - "state": "translated", - "value": "cmux-tui ne répond pas. Le redémarrer arrête le processus bloqué et en lance un nouveau. Les terminaux qui y sont ouverts pourraient être perdus." - } - }, - "it": { - "stringUnit": { - "state": "translated", - "value": "cmux-tui non risponde. Il riavvio arresta il processo bloccato e ne avvia uno nuovo. I terminali in esecuzione al suo interno potrebbero andare persi." - } - }, - "km": { - "stringUnit": { - "state": "translated", - "value": "cmux-tui មិនឆ្លើយតបទេ។ ការចាប់ផ្ដើមវាឡើងវិញនឹងបញ្ឈប់ដំណើរការដែលជាប់គាំង ហើយចាប់ផ្ដើមដំណើរការថ្មីមួយ។ ទែមីណាល់ដែលកំពុងដំណើរការនៅក្នុងវាអាចបាត់បង់។" - } - }, - "ko": { - "stringUnit": { - "state": "translated", - "value": "cmux-tui가 응답하지 않습니다. 재시작하면 멈춘 프로세스를 중지하고 새 프로세스를 시작합니다. 그 안에서 실행 중인 터미널이 사라질 수 있습니다." - } - }, - "nb": { - "stringUnit": { - "state": "translated", - "value": "cmux-tui svarer ikke. En omstart stopper den fastlåste prosessen og starter en ny. Terminaler som kjører i den, kan gå tapt." - } - }, - "pl": { - "stringUnit": { - "state": "translated", - "value": "cmux-tui nie odpowiada. Ponowne uruchomienie zatrzyma zawieszony proces i uruchomi nowy. Terminale działające w nim mogą zostać utracone." - } - }, - "pt-BR": { - "stringUnit": { - "state": "translated", - "value": "O cmux-tui não está respondendo. Reiniciá-lo encerra o processo travado e inicia um novo. Os terminais em execução nele podem ser perdidos." - } - }, - "ru": { - "stringUnit": { - "state": "translated", - "value": "cmux-tui не отвечает. Перезапуск остановит зависший процесс и запустит новый. Терминалы, работающие в нём, могут быть потеряны." - } - }, - "th": { - "stringUnit": { - "state": "translated", - "value": "cmux-tui ไม่ตอบสนอง การรีสตาร์ตจะหยุดโปรเซสที่ค้างและเริ่มโปรเซสใหม่ เทอร์มินัลที่ทำงานอยู่ในนั้นอาจสูญหาย" - } - }, - "tr": { - "stringUnit": { - "state": "translated", - "value": "cmux-tui yanıt vermiyor. Yeniden başlatmak takılan süreci durdurur ve yenisini başlatır. İçinde çalışan terminaller kaybolabilir." - } - }, - "uk": { - "stringUnit": { - "state": "translated", - "value": "cmux-tui не відповідає. Перезапуск зупинить завислий процес і запустить новий. Термінали, що працюють у ньому, можуть бути втрачені." - } - }, - "vi": { - "stringUnit": { - "state": "translated", - "value": "cmux-tui không phản hồi. Khởi động lại sẽ dừng tiến trình bị treo và chạy một tiến trình mới. Các terminal đang chạy trong đó có thể bị mất." - } - }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "cmux-tui 没有响应。重启会停止卡住的进程并启动一个新进程。其中运行的终端可能会丢失。" - } - }, - "zh-Hant": { - "stringUnit": { - "state": "translated", - "value": "cmux-tui 沒有回應。重新啟動會停止卡住的程序並啟動新的程序。其中執行中的終端機可能會遺失。" - } - } - } - }, - "daemon.recovery.confirmButton": { - "comment": "Confirm button of the Restart cmux-tui question.", - "extractionState": "manual", - "localizations": { - "en": { - "stringUnit": { - "state": "translated", - "value": "Restart" - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "再起動" - } - }, - "ar": { - "stringUnit": { - "state": "translated", - "value": "إعادة التشغيل" - } - }, - "bs": { - "stringUnit": { - "state": "translated", - "value": "Ponovo pokreni" - } - }, - "da": { - "stringUnit": { - "state": "translated", - "value": "Genstart" - } - }, - "de": { - "stringUnit": { - "state": "translated", - "value": "Neu starten" - } - }, - "es": { - "stringUnit": { - "state": "translated", - "value": "Reiniciar" - } - }, - "fr": { - "stringUnit": { - "state": "translated", - "value": "Redémarrer" - } - }, - "it": { - "stringUnit": { - "state": "translated", - "value": "Riavvia" - } - }, - "km": { - "stringUnit": { - "state": "translated", - "value": "ចាប់ផ្ដើមឡើងវិញ" - } - }, - "ko": { - "stringUnit": { - "state": "translated", - "value": "재시작" - } - }, - "nb": { - "stringUnit": { - "state": "translated", - "value": "Start på nytt" - } - }, - "pl": { - "stringUnit": { - "state": "translated", - "value": "Uruchom ponownie" - } - }, - "pt-BR": { - "stringUnit": { - "state": "translated", - "value": "Reiniciar" - } - }, - "ru": { - "stringUnit": { - "state": "translated", - "value": "Перезапустить" - } - }, - "th": { - "stringUnit": { - "state": "translated", - "value": "รีสตาร์ต" - } - }, - "tr": { - "stringUnit": { - "state": "translated", - "value": "Yeniden Başlat" - } - }, - "uk": { - "stringUnit": { - "state": "translated", - "value": "Перезапустити" - } - }, - "vi": { - "stringUnit": { - "state": "translated", - "value": "Khởi động lại" - } - }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "重启" - } - }, - "zh-Hant": { - "stringUnit": { - "state": "translated", - "value": "重新啟動" - } - } - } - }, - "daemon.recovery.restarting": { - "comment": "Status line while cmux-tui is being restarted (up to about 20 seconds).", - "extractionState": "manual", - "localizations": { - "en": { - "stringUnit": { - "state": "translated", - "value": "Restarting cmux-tui…" - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "cmux-tui を再起動しています…" - } - }, - "ar": { - "stringUnit": { - "state": "translated", - "value": "جارٍ إعادة تشغيل cmux-tui…" - } - }, - "bs": { - "stringUnit": { - "state": "translated", - "value": "Ponovno pokretanje cmux-tui…" - } - }, - "da": { - "stringUnit": { - "state": "translated", - "value": "Genstarter cmux-tui…" - } - }, - "de": { - "stringUnit": { - "state": "translated", - "value": "cmux-tui wird neu gestartet…" - } - }, - "es": { - "stringUnit": { - "state": "translated", - "value": "Reiniciando cmux-tui…" - } - }, - "fr": { - "stringUnit": { - "state": "translated", - "value": "Redémarrage de cmux-tui…" - } - }, - "it": { - "stringUnit": { - "state": "translated", - "value": "Riavvio di cmux-tui…" - } - }, - "km": { - "stringUnit": { - "state": "translated", - "value": "កំពុងចាប់ផ្ដើម cmux-tui ឡើងវិញ…" - } - }, - "ko": { - "stringUnit": { - "state": "translated", - "value": "cmux-tui 재시작 중…" - } - }, - "nb": { - "stringUnit": { - "state": "translated", - "value": "Starter cmux-tui på nytt…" - } - }, - "pl": { - "stringUnit": { - "state": "translated", - "value": "Ponowne uruchamianie cmux-tui…" - } - }, - "pt-BR": { - "stringUnit": { - "state": "translated", - "value": "Reiniciando o cmux-tui…" - } - }, - "ru": { - "stringUnit": { - "state": "translated", - "value": "Перезапуск cmux-tui…" - } - }, - "th": { - "stringUnit": { - "state": "translated", - "value": "กำลังรีสตาร์ต cmux-tui…" - } - }, - "tr": { - "stringUnit": { - "state": "translated", - "value": "cmux-tui yeniden başlatılıyor…" - } - }, - "uk": { - "stringUnit": { - "state": "translated", - "value": "Перезапуск cmux-tui…" - } - }, - "vi": { - "stringUnit": { - "state": "translated", - "value": "Đang khởi động lại cmux-tui…" - } - }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "正在重启 cmux-tui…" - } - }, - "zh-Hant": { - "stringUnit": { - "state": "translated", - "value": "正在重新啟動 cmux-tui…" - } - } - } - }, - "daemon.recovery.failed": { - "comment": "Status line after Restart cmux-tui did not work; Retry Now stays next to it.", - "extractionState": "manual", - "localizations": { - "en": { - "stringUnit": { - "state": "translated", - "value": "cmux-tui could not be restarted. Try again in a moment." - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "cmux-tui を再起動できませんでした。しばらくしてからもう一度お試しください。" - } - }, - "ar": { - "stringUnit": { - "state": "translated", - "value": "تعذّرت إعادة تشغيل cmux-tui. حاول مرة أخرى بعد قليل." - } - }, - "bs": { - "stringUnit": { - "state": "translated", - "value": "cmux-tui nije moguće ponovo pokrenuti. Pokušajte ponovo za trenutak." - } - }, - "da": { - "stringUnit": { - "state": "translated", - "value": "cmux-tui kunne ikke genstartes. Prøv igen om lidt." - } - }, - "de": { - "stringUnit": { - "state": "translated", - "value": "cmux-tui konnte nicht neu gestartet werden. Versuche es gleich noch einmal." - } - }, - "es": { - "stringUnit": { - "state": "translated", - "value": "No se pudo reiniciar cmux-tui. Inténtalo de nuevo en un momento." - } - }, - "fr": { - "stringUnit": { - "state": "translated", - "value": "Impossible de redémarrer cmux-tui. Réessayez dans un instant." - } - }, - "it": { - "stringUnit": { - "state": "translated", - "value": "Impossibile riavviare cmux-tui. Riprova tra un momento." - } - }, - "km": { - "stringUnit": { - "state": "translated", - "value": "មិនអាចចាប់ផ្ដើម cmux-tui ឡើងវិញបានទេ។ សូមព្យាយាមម្ដងទៀតបន្តិចទៀត។" - } - }, - "ko": { - "stringUnit": { - "state": "translated", - "value": "cmux-tui를 재시작할 수 없습니다. 잠시 후 다시 시도하세요." - } - }, - "nb": { - "stringUnit": { - "state": "translated", - "value": "Kunne ikke starte cmux-tui på nytt. Prøv igjen om litt." - } - }, - "pl": { - "stringUnit": { - "state": "translated", - "value": "Nie udało się ponownie uruchomić cmux-tui. Spróbuj ponownie za chwilę." - } - }, - "pt-BR": { - "stringUnit": { - "state": "translated", - "value": "Não foi possível reiniciar o cmux-tui. Tente novamente em instantes." - } - }, - "ru": { - "stringUnit": { - "state": "translated", - "value": "Не удалось перезапустить cmux-tui. Повторите попытку чуть позже." - } - }, - "th": { - "stringUnit": { - "state": "translated", - "value": "ไม่สามารถรีสตาร์ต cmux-tui ได้ โปรดลองอีกครั้งในอีกสักครู่" - } - }, - "tr": { - "stringUnit": { - "state": "translated", - "value": "cmux-tui yeniden başlatılamadı. Birazdan tekrar deneyin." - } - }, - "uk": { - "stringUnit": { - "state": "translated", - "value": "Не вдалося перезапустити cmux-tui. Спробуйте ще раз за мить." - } - }, - "vi": { - "stringUnit": { - "state": "translated", - "value": "Không thể khởi động lại cmux-tui. Hãy thử lại sau giây lát." - } - }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "无法重启 cmux-tui。请稍后再试。" - } - }, - "zh-Hant": { - "stringUnit": { - "state": "translated", - "value": "無法重新啟動 cmux-tui。請稍後再試。" - } - } - } - }, - "daemon.reinstall": { - "extractionState": "manual", - "localizations": { - "en": { - "stringUnit": { - "state": "translated", - "value": "Reinstall cmux to fix this." - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "cmux を再インストールすると解決します。" - } - }, - "ar": { - "stringUnit": { - "state": "translated", - "value": "أعد تثبيت cmux لإصلاح ذلك." - } - }, - "bs": { - "stringUnit": { - "state": "translated", - "value": "Ponovo instalirajte cmux da biste ovo popravili." - } - }, - "da": { - "stringUnit": { - "state": "translated", - "value": "Geninstaller cmux for at løse det." - } - }, - "de": { - "stringUnit": { - "state": "translated", - "value": "Installiere cmux neu, um das zu beheben." - } - }, - "es": { - "stringUnit": { - "state": "translated", - "value": "Reinstala cmux para solucionarlo." - } - }, - "fr": { - "stringUnit": { - "state": "translated", - "value": "Réinstallez cmux pour corriger ce problème." - } - }, - "it": { - "stringUnit": { - "state": "translated", - "value": "Reinstalla cmux per risolvere il problema." - } - }, - "km": { - "stringUnit": { - "state": "translated", - "value": "ដំឡើង cmux ឡើងវិញ ដើម្បីដោះស្រាយបញ្ហានេះ។" - } - }, - "ko": { - "stringUnit": { - "state": "translated", - "value": "cmux를 다시 설치하면 해결됩니다." - } - }, - "nb": { - "stringUnit": { - "state": "translated", - "value": "Installer cmux på nytt for å løse dette." - } - }, - "pl": { - "stringUnit": { - "state": "translated", - "value": "Zainstaluj cmux ponownie, aby to naprawić." - } - }, - "pt-BR": { - "stringUnit": { - "state": "translated", - "value": "Reinstale o cmux para corrigir isso." - } - }, - "ru": { - "stringUnit": { - "state": "translated", - "value": "Переустановите cmux, чтобы это исправить." - } - }, - "th": { - "stringUnit": { - "state": "translated", - "value": "ติดตั้ง cmux ใหม่เพื่อแก้ไขปัญหานี้" - } - }, - "tr": { - "stringUnit": { - "state": "translated", - "value": "Bunu düzeltmek için cmux’u yeniden yükleyin." - } - }, - "uk": { - "stringUnit": { - "state": "translated", - "value": "Перевстановіть cmux, щоб це виправити." - } - }, - "vi": { - "stringUnit": { - "state": "translated", - "value": "Hãy cài đặt lại cmux để khắc phục." - } - }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "重新安装 cmux 即可解决。" - } - }, - "zh-Hant": { - "stringUnit": { - "state": "translated", - "value": "重新安裝 cmux 即可解決。" - } - } - } - }, - "daemon.serviceUnavailable": { - "extractionState": "manual", - "localizations": { - "en": { - "stringUnit": { - "state": "translated", - "value": "cmux couldn’t start its background service." - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "cmux はバックグラウンドサービスを起動できませんでした。" - } - }, - "ar": { - "stringUnit": { - "state": "translated", - "value": "تعذّر على cmux تشغيل خدمته في الخلفية." - } - }, - "bs": { - "stringUnit": { - "state": "translated", - "value": "cmux nije mogao pokrenuti svoj pozadinski servis." - } - }, - "da": { - "stringUnit": { - "state": "translated", - "value": "cmux kunne ikke starte sin baggrundstjeneste." - } - }, - "de": { - "stringUnit": { - "state": "translated", - "value": "cmux konnte seinen Hintergrunddienst nicht starten." - } - }, - "es": { - "stringUnit": { - "state": "translated", - "value": "cmux no pudo iniciar su servicio en segundo plano." - } - }, - "fr": { - "stringUnit": { - "state": "translated", - "value": "cmux n’a pas pu démarrer son service d’arrière-plan." - } - }, - "it": { - "stringUnit": { - "state": "translated", - "value": "cmux non è riuscito ad avviare il suo servizio in background." - } - }, - "km": { - "stringUnit": { - "state": "translated", - "value": "cmux មិនអាចចាប់ផ្តើមសេវាផ្ទៃខាងក្រោយរបស់វាបានទេ។" - } - }, - "ko": { - "stringUnit": { - "state": "translated", - "value": "cmux가 백그라운드 서비스를 시작하지 못했습니다." - } - }, - "nb": { - "stringUnit": { - "state": "translated", - "value": "cmux kunne ikke starte bakgrunnstjenesten sin." - } - }, - "pl": { - "stringUnit": { - "state": "translated", - "value": "cmux nie mógł uruchomić swojej usługi w tle." - } - }, - "pt-BR": { - "stringUnit": { - "state": "translated", - "value": "O cmux não conseguiu iniciar seu serviço em segundo plano." - } - }, - "ru": { - "stringUnit": { - "state": "translated", - "value": "cmux не удалось запустить фоновую службу." - } - }, - "th": { - "stringUnit": { - "state": "translated", - "value": "cmux ไม่สามารถเริ่มบริการเบื้องหลังได้" - } - }, - "tr": { - "stringUnit": { - "state": "translated", - "value": "cmux arka plan hizmetini başlatamadı." - } - }, - "uk": { - "stringUnit": { - "state": "translated", - "value": "cmux не вдалося запустити фонову службу." - } - }, - "vi": { - "stringUnit": { - "state": "translated", - "value": "cmux không thể khởi động dịch vụ nền." - } - }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "cmux 无法启动其后台服务。" - } - }, - "zh-Hant": { - "stringUnit": { - "state": "translated", - "value": "cmux 無法啟動其背景服務。" - } - } - } - }, "feed.bridge.title": { "comment": "A phone banner title for an agent's permission request. %@ is the tool's title or kind (not translated).", "extractionState": "manual", @@ -5388,268 +4071,6 @@ } } }, - "daemon.connecting": { - "extractionState": "manual", - "localizations": { - "en": { - "stringUnit": { - "state": "translated", - "value": "Connecting to cmux-tui…" - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "cmux-tui に接続しています…" - } - }, - "ar": { - "stringUnit": { - "state": "needs_review", - "value": "جارٍ الاتصال بـ cmux-tui…" - } - }, - "bs": { - "stringUnit": { - "state": "needs_review", - "value": "Povezivanje s cmux-tui…" - } - }, - "da": { - "stringUnit": { - "state": "needs_review", - "value": "Opretter forbindelse til cmux-tui…" - } - }, - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Verbindung zu cmux-tui wird hergestellt…" - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Conectando con cmux-tui…" - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Connexion à cmux-tui…" - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Connessione a cmux-tui…" - } - }, - "km": { - "stringUnit": { - "state": "needs_review", - "value": "កំពុងភ្ជាប់ទៅ cmux-tui…" - } - }, - "ko": { - "stringUnit": { - "state": "needs_review", - "value": "cmux-tui에 연결하는 중…" - } - }, - "nb": { - "stringUnit": { - "state": "needs_review", - "value": "Kobler til cmux-tui…" - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Łączenie z cmux-tui…" - } - }, - "pt-BR": { - "stringUnit": { - "state": "needs_review", - "value": "Conectando ao cmux-tui…" - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Подключение к cmux-tui…" - } - }, - "th": { - "stringUnit": { - "state": "needs_review", - "value": "กำลังเชื่อมต่อกับ cmux-tui…" - } - }, - "tr": { - "stringUnit": { - "state": "needs_review", - "value": "cmux-tui'ye bağlanılıyor…" - } - }, - "uk": { - "stringUnit": { - "state": "needs_review", - "value": "Підключення до cmux-tui…" - } - }, - "vi": { - "stringUnit": { - "state": "needs_review", - "value": "Đang kết nối với cmux-tui…" - } - }, - "zh-Hans": { - "stringUnit": { - "state": "needs_review", - "value": "正在连接 cmux-tui…" - } - }, - "zh-Hant": { - "stringUnit": { - "state": "needs_review", - "value": "正在連線 cmux-tui…" - } - } - } - }, - "daemon.retrying": { - "extractionState": "manual", - "localizations": { - "en": { - "stringUnit": { - "state": "translated", - "value": "Still retrying in the background." - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "バックグラウンドで再試行を続けています。" - } - }, - "ar": { - "stringUnit": { - "state": "needs_review", - "value": "لا تزال إعادة المحاولة جارية في الخلفية." - } - }, - "bs": { - "stringUnit": { - "state": "needs_review", - "value": "Ponovni pokušaji se nastavljaju u pozadini." - } - }, - "da": { - "stringUnit": { - "state": "needs_review", - "value": "Prøver stadig igen i baggrunden." - } - }, - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Neue Versuche laufen im Hintergrund weiter." - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Se sigue reintentando en segundo plano." - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Nouvelles tentatives en arrière-plan." - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Nuovi tentativi in corso in background." - } - }, - "km": { - "stringUnit": { - "state": "needs_review", - "value": "នៅតែព្យាយាមម្តងទៀតនៅផ្ទៃខាងក្រោយ។" - } - }, - "ko": { - "stringUnit": { - "state": "needs_review", - "value": "백그라운드에서 계속 재시도하는 중입니다." - } - }, - "nb": { - "stringUnit": { - "state": "needs_review", - "value": "Prøver fortsatt på nytt i bakgrunnen." - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Ponawianie trwa w tle." - } - }, - "pt-BR": { - "stringUnit": { - "state": "needs_review", - "value": "Ainda tentando novamente em segundo plano." - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Повторные попытки продолжаются в фоне." - } - }, - "th": { - "stringUnit": { - "state": "needs_review", - "value": "ยังคงลองใหม่อยู่เบื้องหลัง" - } - }, - "tr": { - "stringUnit": { - "state": "needs_review", - "value": "Arka planda yeniden deneniyor." - } - }, - "uk": { - "stringUnit": { - "state": "needs_review", - "value": "Повторні спроби тривають у фоні." - } - }, - "vi": { - "stringUnit": { - "state": "needs_review", - "value": "Vẫn đang thử lại trong nền." - } - }, - "zh-Hans": { - "stringUnit": { - "state": "needs_review", - "value": "仍在后台重试。" - } - }, - "zh-Hant": { - "stringUnit": { - "state": "needs_review", - "value": "仍在背景重試。" - } - } - } - }, "browser.engine.fallback.notBundled": { "extractionState": "manual", "localizations": { diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Strings.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Strings.swift index 57bbd31dd482..d1e9105ce2c9 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Strings.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Strings.swift @@ -37,13 +37,3 @@ enum Strings { static var renameConfirm: String { String(localized: "rename.confirm", defaultValue: "Rename", bundle: .module) } static var cancel: String { String(localized: "common.cancel", defaultValue: "Cancel", bundle: .module) } } - -extension Strings { - static var daemonConnecting: String { String(localized: "daemon.connecting", defaultValue: "Connecting to cmux-tui…", bundle: .module) } - static var daemonServiceUnavailable: String { - String(localized: "daemon.serviceUnavailable", defaultValue: "cmux couldn’t start its background service.", bundle: .module) - } - static var daemonReinstall: String { String(localized: "daemon.reinstall", defaultValue: "Reinstall cmux to fix this.", bundle: .module) } - static var daemonCopyDetails: String { String(localized: "daemon.copyDetails", defaultValue: "Copy Details", bundle: .module) } - static var daemonRetrying: String { String(localized: "daemon.retrying", defaultValue: "Still retrying in the background.", bundle: .module) } -} diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Windows/WindowController.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Windows/WindowController.swift index 7386ef8843ae..2b37f627c1d5 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Windows/WindowController.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Windows/WindowController.swift @@ -2,6 +2,7 @@ import AppKit import CmuxNextAgentCursor import CmuxNextActions import CmuxNextCompat +import CmuxNextDaemonRecovery import CmuxNextHistory import CmuxNextBridge import CmuxNextBrowser @@ -176,8 +177,7 @@ final class WindowController: NSWindowController, NSWindowDelegate { if content == nil { showConnecting() } } - /// The connecting (or unavailable) state of the local daemon's first - /// connection, until a workspace can be shown. + /// The local daemon's first connection, connecting or unavailable, until a workspace shows. private func showConnecting() { let view = connectingView ?? DaemonConnectingView(frame: .zero, recovery: services.daemon.recovery) connectingView = view diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextApp/DaemonConnectingView.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextDaemonRecovery/DaemonConnectingView.swift similarity index 92% rename from Packages/macOS/CmuxNext/Sources/CmuxNextApp/DaemonConnectingView.swift rename to Packages/macOS/CmuxNext/Sources/CmuxNextDaemonRecovery/DaemonConnectingView.swift index 5929b822c684..172fce627aec 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextApp/DaemonConnectingView.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextDaemonRecovery/DaemonConnectingView.swift @@ -1,6 +1,6 @@ -import AppKit -import CmuxNextDaemon -import CmuxNextDaemonService +public import AppKit +public import CmuxNextDaemon +public import CmuxNextDaemonService import CmuxNextDesign import CmuxNextWakeups @@ -13,7 +13,7 @@ import CmuxNextWakeups /// passes, it shows the failure at once, in plain words, with the raw error /// behind Copy Details for a bug report, Retry Now and, for a stuck owner, /// Restart cmux-tui (`DaemonRecoveryControls`, cx-jniyz.1). -final class DaemonConnectingView: NSView { +public final class DaemonConnectingView: NSView { let mark = LaunchMarkView() private let titleLabel = NSTextField(labelWithString: "") private let detailLabel = NSTextField(wrappingLabelWithString: "") @@ -32,11 +32,11 @@ final class DaemonConnectingView: NSView { /// The status line has waited out its delay (or a failure showed it). private var statusDue = false - convenience override init(frame: NSRect) { + public convenience override init(frame: NSRect) { self.init(frame: frame, clock: ContinuousClock()) } - convenience init(frame: NSRect, recovery: DaemonRecovery) { + public convenience init(frame: NSRect, recovery: DaemonRecovery) { self.init(frame: frame, clock: ContinuousClock(), recovery: recovery) } @@ -57,7 +57,7 @@ final class DaemonConnectingView: NSView { detailLabel.maximumNumberOfLines = 4 detailLabel.isSelectable = true detailLabel.focusRingType = .none - copyDetailsButton.title = Strings.daemonCopyDetails + copyDetailsButton.title = DaemonRecoveryStrings.copyDetails copyDetailsButton.controlSize = .small copyDetailsButton.target = self copyDetailsButton.action = #selector(copyDetails) @@ -125,11 +125,11 @@ final class DaemonConnectingView: NSView { statusTimer.cancel() } - func apply(_ state: DaemonStartupState) { + public func apply(_ state: DaemonStartupState) { self.state = state switch state { case .connecting, .connected: - titleLabel.stringValue = Strings.daemonConnecting + titleLabel.stringValue = DaemonRecoveryStrings.connecting titleLabel.alphaValue = statusDue ? 1 : 0 detailLabel.stringValue = "" detailLabel.isHidden = true @@ -146,7 +146,7 @@ final class DaemonConnectingView: NSView { titleLabel.stringValue = RefusalStrings.daemonRestoreTimedOut detailLabel.stringValue = "" } else { - titleLabel.stringValue = Strings.daemonServiceUnavailable + titleLabel.stringValue = DaemonRecoveryStrings.serviceUnavailable detailLabel.stringValue = Self.plainReason(error) ?? "" } detailLabel.isHidden = detailLabel.stringValue.isEmpty @@ -163,12 +163,12 @@ final class DaemonConnectingView: NSView { /// app serves the socket, a bad launch tag): Copy Details carries the specifics. private static func plainReason(_ error: DaemonError) -> String? { switch error { - case .binaryNotFound: Strings.daemonReinstall + case .binaryNotFound: DaemonRecoveryStrings.reinstall case .unsupportedProtocol: RefusalStrings.restartToUpdateDaemon case .missingCapabilities, .sessionRestoreTimedOut: RefusalStrings.describe(error) case .endpointBlocked(let reason): reason case .wrongApp, .invalidSessionName: nil - default: DaemonStartup.shared.isPermanent(error) ? nil : Strings.daemonRetrying + default: DaemonStartup.shared.isPermanent(error) ? nil : DaemonRecoveryStrings.retrying } } diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextApp/DaemonRecoveryControls.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextDaemonRecovery/DaemonRecoveryControls.swift similarity index 86% rename from Packages/macOS/CmuxNext/Sources/CmuxNextApp/DaemonRecoveryControls.swift rename to Packages/macOS/CmuxNext/Sources/CmuxNextDaemonRecovery/DaemonRecoveryControls.swift index ae2d05c02fa1..ac06c730125e 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextApp/DaemonRecoveryControls.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextDaemonRecovery/DaemonRecoveryControls.swift @@ -1,11 +1,19 @@ -import AppKit -import CmuxNextApps +public import AppKit +public import CmuxNextApps import CmuxNextCompat -import CmuxNextDaemonService +public import CmuxNextDaemonService import CmuxNextDesign -/// Strings of the daemon recovery controls (Resources/Localizable.xcstrings). +/// Strings of the launch window's connecting and failure screen and the +/// daemon recovery controls (Resources/Localizable.xcstrings). enum DaemonRecoveryStrings { + static var connecting: String { String(localized: "daemon.connecting", defaultValue: "Connecting to cmux-tui…", bundle: .module) } + static var serviceUnavailable: String { + String(localized: "daemon.serviceUnavailable", defaultValue: "cmux couldn’t start its background service.", bundle: .module) + } + static var reinstall: String { String(localized: "daemon.reinstall", defaultValue: "Reinstall cmux to fix this.", bundle: .module) } + static var copyDetails: String { String(localized: "daemon.copyDetails", defaultValue: "Copy Details", bundle: .module) } + static var retrying: String { String(localized: "daemon.retrying", defaultValue: "Still retrying in the background.", bundle: .module) } static var retryNow: String { String(localized: "daemon.recovery.retryNow", defaultValue: "Retry Now", bundle: .module) } static var restart: String { String(localized: "daemon.recovery.restart", defaultValue: "Restart cmux-tui", bundle: .module) } static var confirmTitle: String { String(localized: "daemon.recovery.confirmTitle", defaultValue: "Restart cmux-tui?", bundle: .module) } @@ -24,7 +32,7 @@ enum DaemonRecoveryStrings { /// Restart cmux-tui's question and the App Store banner's controls, one path /// for both surfaces (cx-jniyz.1). The question is the cmux dialog every /// destructive confirm uses, so the debug socket can answer it. -enum DaemonRestartPrompt { +public enum DaemonRestartPrompt { static let confirmID = "restart" static let identifier = "cmux.dialog.restartDaemon" @@ -48,7 +56,7 @@ enum DaemonRestartPrompt { } /// The App Store's disconnected banner controls (AppStoreModel.recovery). - static func appsRecovery(_ recovery: DaemonRecovery) -> AppsRecovery { + public static func appsRecovery(_ recovery: DaemonRecovery) -> AppsRecovery { guard recovery.canRetry else { return AppsRecovery(actions: [], status: nil) } var actions = [AppsRecoveryAction(id: "retryNow", title: DaemonRecoveryStrings.retryNow, isEnabled: !recovery.isRestarting) { recovery.retryNow() diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextDaemonRecovery/Resources/Localizable.xcstrings b/Packages/macOS/CmuxNext/Sources/CmuxNextDaemonRecovery/Resources/Localizable.xcstrings new file mode 100644 index 000000000000..4e99ba20865a --- /dev/null +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextDaemonRecovery/Resources/Localizable.xcstrings @@ -0,0 +1,1585 @@ +{ + "sourceLanguage": "en", + "strings": { + "daemon.connecting": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Connecting to cmux-tui…" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "cmux-tui に接続しています…" + } + }, + "ar": { + "stringUnit": { + "state": "needs_review", + "value": "جارٍ الاتصال بـ cmux-tui…" + } + }, + "bs": { + "stringUnit": { + "state": "needs_review", + "value": "Povezivanje s cmux-tui…" + } + }, + "da": { + "stringUnit": { + "state": "needs_review", + "value": "Opretter forbindelse til cmux-tui…" + } + }, + "de": { + "stringUnit": { + "state": "needs_review", + "value": "Verbindung zu cmux-tui wird hergestellt…" + } + }, + "es": { + "stringUnit": { + "state": "needs_review", + "value": "Conectando con cmux-tui…" + } + }, + "fr": { + "stringUnit": { + "state": "needs_review", + "value": "Connexion à cmux-tui…" + } + }, + "it": { + "stringUnit": { + "state": "needs_review", + "value": "Connessione a cmux-tui…" + } + }, + "km": { + "stringUnit": { + "state": "needs_review", + "value": "កំពុងភ្ជាប់ទៅ cmux-tui…" + } + }, + "ko": { + "stringUnit": { + "state": "needs_review", + "value": "cmux-tui에 연결하는 중…" + } + }, + "nb": { + "stringUnit": { + "state": "needs_review", + "value": "Kobler til cmux-tui…" + } + }, + "pl": { + "stringUnit": { + "state": "needs_review", + "value": "Łączenie z cmux-tui…" + } + }, + "pt-BR": { + "stringUnit": { + "state": "needs_review", + "value": "Conectando ao cmux-tui…" + } + }, + "ru": { + "stringUnit": { + "state": "needs_review", + "value": "Подключение к cmux-tui…" + } + }, + "th": { + "stringUnit": { + "state": "needs_review", + "value": "กำลังเชื่อมต่อกับ cmux-tui…" + } + }, + "tr": { + "stringUnit": { + "state": "needs_review", + "value": "cmux-tui'ye bağlanılıyor…" + } + }, + "uk": { + "stringUnit": { + "state": "needs_review", + "value": "Підключення до cmux-tui…" + } + }, + "vi": { + "stringUnit": { + "state": "needs_review", + "value": "Đang kết nối với cmux-tui…" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "needs_review", + "value": "正在连接 cmux-tui…" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "needs_review", + "value": "正在連線 cmux-tui…" + } + } + } + }, + "daemon.serviceUnavailable": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "cmux couldn’t start its background service." + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "cmux はバックグラウンドサービスを起動できませんでした。" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "تعذّر على cmux تشغيل خدمته في الخلفية." + } + }, + "bs": { + "stringUnit": { + "state": "translated", + "value": "cmux nije mogao pokrenuti svoj pozadinski servis." + } + }, + "da": { + "stringUnit": { + "state": "translated", + "value": "cmux kunne ikke starte sin baggrundstjeneste." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "cmux konnte seinen Hintergrunddienst nicht starten." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "cmux no pudo iniciar su servicio en segundo plano." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "cmux n’a pas pu démarrer son service d’arrière-plan." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "cmux non è riuscito ad avviare il suo servizio in background." + } + }, + "km": { + "stringUnit": { + "state": "translated", + "value": "cmux មិនអាចចាប់ផ្តើមសេវាផ្ទៃខាងក្រោយរបស់វាបានទេ។" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "cmux가 백그라운드 서비스를 시작하지 못했습니다." + } + }, + "nb": { + "stringUnit": { + "state": "translated", + "value": "cmux kunne ikke starte bakgrunnstjenesten sin." + } + }, + "pl": { + "stringUnit": { + "state": "translated", + "value": "cmux nie mógł uruchomić swojej usługi w tle." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "O cmux não conseguiu iniciar seu serviço em segundo plano." + } + }, + "ru": { + "stringUnit": { + "state": "translated", + "value": "cmux не удалось запустить фоновую службу." + } + }, + "th": { + "stringUnit": { + "state": "translated", + "value": "cmux ไม่สามารถเริ่มบริการเบื้องหลังได้" + } + }, + "tr": { + "stringUnit": { + "state": "translated", + "value": "cmux arka plan hizmetini başlatamadı." + } + }, + "uk": { + "stringUnit": { + "state": "translated", + "value": "cmux не вдалося запустити фонову службу." + } + }, + "vi": { + "stringUnit": { + "state": "translated", + "value": "cmux không thể khởi động dịch vụ nền." + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "cmux 无法启动其后台服务。" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "cmux 無法啟動其背景服務。" + } + } + } + }, + "daemon.reinstall": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Reinstall cmux to fix this." + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "cmux を再インストールすると解決します。" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "أعد تثبيت cmux لإصلاح ذلك." + } + }, + "bs": { + "stringUnit": { + "state": "translated", + "value": "Ponovo instalirajte cmux da biste ovo popravili." + } + }, + "da": { + "stringUnit": { + "state": "translated", + "value": "Geninstaller cmux for at løse det." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Installiere cmux neu, um das zu beheben." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Reinstala cmux para solucionarlo." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Réinstallez cmux pour corriger ce problème." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Reinstalla cmux per risolvere il problema." + } + }, + "km": { + "stringUnit": { + "state": "translated", + "value": "ដំឡើង cmux ឡើងវិញ ដើម្បីដោះស្រាយបញ្ហានេះ។" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "cmux를 다시 설치하면 해결됩니다." + } + }, + "nb": { + "stringUnit": { + "state": "translated", + "value": "Installer cmux på nytt for å løse dette." + } + }, + "pl": { + "stringUnit": { + "state": "translated", + "value": "Zainstaluj cmux ponownie, aby to naprawić." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Reinstale o cmux para corrigir isso." + } + }, + "ru": { + "stringUnit": { + "state": "translated", + "value": "Переустановите cmux, чтобы это исправить." + } + }, + "th": { + "stringUnit": { + "state": "translated", + "value": "ติดตั้ง cmux ใหม่เพื่อแก้ไขปัญหานี้" + } + }, + "tr": { + "stringUnit": { + "state": "translated", + "value": "Bunu düzeltmek için cmux’u yeniden yükleyin." + } + }, + "uk": { + "stringUnit": { + "state": "translated", + "value": "Перевстановіть cmux, щоб це виправити." + } + }, + "vi": { + "stringUnit": { + "state": "translated", + "value": "Hãy cài đặt lại cmux để khắc phục." + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "重新安装 cmux 即可解决。" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "重新安裝 cmux 即可解決。" + } + } + } + }, + "daemon.copyDetails": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Copy Details" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "詳細をコピー" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "نسخ التفاصيل" + } + }, + "bs": { + "stringUnit": { + "state": "translated", + "value": "Kopiraj detalje" + } + }, + "da": { + "stringUnit": { + "state": "translated", + "value": "Kopiér detaljer" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Details kopieren" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Copiar detalles" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Copier les détails" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Copia dettagli" + } + }, + "km": { + "stringUnit": { + "state": "translated", + "value": "ចម្លងព័ត៌មានលម្អិត" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "세부 정보 복사" + } + }, + "nb": { + "stringUnit": { + "state": "translated", + "value": "Kopier detaljer" + } + }, + "pl": { + "stringUnit": { + "state": "translated", + "value": "Kopiuj szczegóły" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Copiar detalhes" + } + }, + "ru": { + "stringUnit": { + "state": "translated", + "value": "Скопировать подробности" + } + }, + "th": { + "stringUnit": { + "state": "translated", + "value": "คัดลอกรายละเอียด" + } + }, + "tr": { + "stringUnit": { + "state": "translated", + "value": "Ayrıntıları Kopyala" + } + }, + "uk": { + "stringUnit": { + "state": "translated", + "value": "Скопіювати подробиці" + } + }, + "vi": { + "stringUnit": { + "state": "translated", + "value": "Sao chép chi tiết" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "复制详细信息" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "拷貝詳細資訊" + } + } + } + }, + "daemon.retrying": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Still retrying in the background." + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "バックグラウンドで再試行を続けています。" + } + }, + "ar": { + "stringUnit": { + "state": "needs_review", + "value": "لا تزال إعادة المحاولة جارية في الخلفية." + } + }, + "bs": { + "stringUnit": { + "state": "needs_review", + "value": "Ponovni pokušaji se nastavljaju u pozadini." + } + }, + "da": { + "stringUnit": { + "state": "needs_review", + "value": "Prøver stadig igen i baggrunden." + } + }, + "de": { + "stringUnit": { + "state": "needs_review", + "value": "Neue Versuche laufen im Hintergrund weiter." + } + }, + "es": { + "stringUnit": { + "state": "needs_review", + "value": "Se sigue reintentando en segundo plano." + } + }, + "fr": { + "stringUnit": { + "state": "needs_review", + "value": "Nouvelles tentatives en arrière-plan." + } + }, + "it": { + "stringUnit": { + "state": "needs_review", + "value": "Nuovi tentativi in corso in background." + } + }, + "km": { + "stringUnit": { + "state": "needs_review", + "value": "នៅតែព្យាយាមម្តងទៀតនៅផ្ទៃខាងក្រោយ។" + } + }, + "ko": { + "stringUnit": { + "state": "needs_review", + "value": "백그라운드에서 계속 재시도하는 중입니다." + } + }, + "nb": { + "stringUnit": { + "state": "needs_review", + "value": "Prøver fortsatt på nytt i bakgrunnen." + } + }, + "pl": { + "stringUnit": { + "state": "needs_review", + "value": "Ponawianie trwa w tle." + } + }, + "pt-BR": { + "stringUnit": { + "state": "needs_review", + "value": "Ainda tentando novamente em segundo plano." + } + }, + "ru": { + "stringUnit": { + "state": "needs_review", + "value": "Повторные попытки продолжаются в фоне." + } + }, + "th": { + "stringUnit": { + "state": "needs_review", + "value": "ยังคงลองใหม่อยู่เบื้องหลัง" + } + }, + "tr": { + "stringUnit": { + "state": "needs_review", + "value": "Arka planda yeniden deneniyor." + } + }, + "uk": { + "stringUnit": { + "state": "needs_review", + "value": "Повторні спроби тривають у фоні." + } + }, + "vi": { + "stringUnit": { + "state": "needs_review", + "value": "Vẫn đang thử lại trong nền." + } + }, + "zh-Hans": { + "stringUnit": { + "state": "needs_review", + "value": "仍在后台重试。" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "needs_review", + "value": "仍在背景重試。" + } + } + } + }, + "daemon.recovery.retryNow": { + "comment": "Button under the launch window's cmux-tui failure and on the App Store's disconnected banner: try to connect to cmux-tui now instead of at the next retry.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Retry Now" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "今すぐ再試行" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "إعادة المحاولة الآن" + } + }, + "bs": { + "stringUnit": { + "state": "translated", + "value": "Pokušaj ponovo sada" + } + }, + "da": { + "stringUnit": { + "state": "translated", + "value": "Prøv igen nu" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Jetzt erneut versuchen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Reintentar ahora" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Réessayer maintenant" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Riprova ora" + } + }, + "km": { + "stringUnit": { + "state": "translated", + "value": "ព្យាយាមម្ដងទៀតឥឡូវនេះ" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "지금 다시 시도" + } + }, + "nb": { + "stringUnit": { + "state": "translated", + "value": "Prøv igjen nå" + } + }, + "pl": { + "stringUnit": { + "state": "translated", + "value": "Spróbuj teraz" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Tentar agora" + } + }, + "ru": { + "stringUnit": { + "state": "translated", + "value": "Повторить сейчас" + } + }, + "th": { + "stringUnit": { + "state": "translated", + "value": "ลองอีกครั้งตอนนี้" + } + }, + "tr": { + "stringUnit": { + "state": "translated", + "value": "Şimdi Yeniden Dene" + } + }, + "uk": { + "stringUnit": { + "state": "translated", + "value": "Повторити зараз" + } + }, + "vi": { + "stringUnit": { + "state": "translated", + "value": "Thử lại ngay" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "立即重试" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "立即重試" + } + } + } + }, + "daemon.recovery.restart": { + "comment": "Button shown after cmux-tui stopped answering: stops the stuck cmux-tui process and starts a new one (asks first).", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Restart cmux-tui" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "cmux-tui を再起動" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "إعادة تشغيل cmux-tui" + } + }, + "bs": { + "stringUnit": { + "state": "translated", + "value": "Ponovo pokreni cmux-tui" + } + }, + "da": { + "stringUnit": { + "state": "translated", + "value": "Genstart cmux-tui" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "cmux-tui neu starten" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Reiniciar cmux-tui" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Redémarrer cmux-tui" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Riavvia cmux-tui" + } + }, + "km": { + "stringUnit": { + "state": "translated", + "value": "ចាប់ផ្ដើម cmux-tui ឡើងវិញ" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "cmux-tui 재시작" + } + }, + "nb": { + "stringUnit": { + "state": "translated", + "value": "Start cmux-tui på nytt" + } + }, + "pl": { + "stringUnit": { + "state": "translated", + "value": "Uruchom ponownie cmux-tui" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Reiniciar o cmux-tui" + } + }, + "ru": { + "stringUnit": { + "state": "translated", + "value": "Перезапустить cmux-tui" + } + }, + "th": { + "stringUnit": { + "state": "translated", + "value": "รีสตาร์ต cmux-tui" + } + }, + "tr": { + "stringUnit": { + "state": "translated", + "value": "cmux-tui'yi Yeniden Başlat" + } + }, + "uk": { + "stringUnit": { + "state": "translated", + "value": "Перезапустити cmux-tui" + } + }, + "vi": { + "stringUnit": { + "state": "translated", + "value": "Khởi động lại cmux-tui" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "重启 cmux-tui" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "重新啟動 cmux-tui" + } + } + } + }, + "daemon.recovery.confirmTitle": { + "comment": "Title of the question before Restart cmux-tui.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Restart cmux-tui?" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "cmux-tui を再起動しますか?" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "هل تريد إعادة تشغيل cmux-tui؟" + } + }, + "bs": { + "stringUnit": { + "state": "translated", + "value": "Ponovo pokrenuti cmux-tui?" + } + }, + "da": { + "stringUnit": { + "state": "translated", + "value": "Genstart cmux-tui?" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "cmux-tui neu starten?" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "¿Reiniciar cmux-tui?" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Redémarrer cmux-tui ?" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Riavviare cmux-tui?" + } + }, + "km": { + "stringUnit": { + "state": "translated", + "value": "ចាប់ផ្ដើម cmux-tui ឡើងវិញ?" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "cmux-tui를 재시작할까요?" + } + }, + "nb": { + "stringUnit": { + "state": "translated", + "value": "Starte cmux-tui på nytt?" + } + }, + "pl": { + "stringUnit": { + "state": "translated", + "value": "Uruchomić ponownie cmux-tui?" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Reiniciar o cmux-tui?" + } + }, + "ru": { + "stringUnit": { + "state": "translated", + "value": "Перезапустить cmux-tui?" + } + }, + "th": { + "stringUnit": { + "state": "translated", + "value": "รีสตาร์ต cmux-tui ไหม" + } + }, + "tr": { + "stringUnit": { + "state": "translated", + "value": "cmux-tui yeniden başlatılsın mı?" + } + }, + "uk": { + "stringUnit": { + "state": "translated", + "value": "Перезапустити cmux-tui?" + } + }, + "vi": { + "stringUnit": { + "state": "translated", + "value": "Khởi động lại cmux-tui?" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "要重启 cmux-tui 吗?" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "要重新啟動 cmux-tui 嗎?" + } + } + } + }, + "daemon.recovery.confirmBody": { + "comment": "Body of the question before Restart cmux-tui. Terminals usually survive, but a stuck process may lose them, so the text says they may be lost.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "cmux-tui is not responding. Restarting it stops the stuck process and starts a new one. Terminals running in it may be lost." + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "cmux-tui が応答していません。再起動すると、停止したプロセスを終了して新しいプロセスを開始します。その中で実行中のターミナルが失われる可能性があります。" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "cmux-tui لا يستجيب. تؤدي إعادة تشغيله إلى إيقاف العملية العالقة وبدء عملية جديدة. قد تُفقد الطرفيات التي تعمل فيه." + } + }, + "bs": { + "stringUnit": { + "state": "translated", + "value": "cmux-tui ne odgovara. Ponovnim pokretanjem zaustavlja se zaglavljeni proces i pokreće novi. Terminali koji rade u njemu mogu biti izgubljeni." + } + }, + "da": { + "stringUnit": { + "state": "translated", + "value": "cmux-tui svarer ikke. En genstart stopper den fastlåste proces og starter en ny. Terminaler, der kører i den, kan gå tabt." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "cmux-tui reagiert nicht. Ein Neustart beendet den hängenden Prozess und startet einen neuen. Darin laufende Terminals können verloren gehen." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "cmux-tui no responde. Al reiniciarlo se detiene el proceso bloqueado y se inicia uno nuevo. Los terminales que se ejecutan en él podrían perderse." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "cmux-tui ne répond pas. Le redémarrer arrête le processus bloqué et en lance un nouveau. Les terminaux qui y sont ouverts pourraient être perdus." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "cmux-tui non risponde. Il riavvio arresta il processo bloccato e ne avvia uno nuovo. I terminali in esecuzione al suo interno potrebbero andare persi." + } + }, + "km": { + "stringUnit": { + "state": "translated", + "value": "cmux-tui មិនឆ្លើយតបទេ។ ការចាប់ផ្ដើមវាឡើងវិញនឹងបញ្ឈប់ដំណើរការដែលជាប់គាំង ហើយចាប់ផ្ដើមដំណើរការថ្មីមួយ។ ទែមីណាល់ដែលកំពុងដំណើរការនៅក្នុងវាអាចបាត់បង់។" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "cmux-tui가 응답하지 않습니다. 재시작하면 멈춘 프로세스를 중지하고 새 프로세스를 시작합니다. 그 안에서 실행 중인 터미널이 사라질 수 있습니다." + } + }, + "nb": { + "stringUnit": { + "state": "translated", + "value": "cmux-tui svarer ikke. En omstart stopper den fastlåste prosessen og starter en ny. Terminaler som kjører i den, kan gå tapt." + } + }, + "pl": { + "stringUnit": { + "state": "translated", + "value": "cmux-tui nie odpowiada. Ponowne uruchomienie zatrzyma zawieszony proces i uruchomi nowy. Terminale działające w nim mogą zostać utracone." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "O cmux-tui não está respondendo. Reiniciá-lo encerra o processo travado e inicia um novo. Os terminais em execução nele podem ser perdidos." + } + }, + "ru": { + "stringUnit": { + "state": "translated", + "value": "cmux-tui не отвечает. Перезапуск остановит зависший процесс и запустит новый. Терминалы, работающие в нём, могут быть потеряны." + } + }, + "th": { + "stringUnit": { + "state": "translated", + "value": "cmux-tui ไม่ตอบสนอง การรีสตาร์ตจะหยุดโปรเซสที่ค้างและเริ่มโปรเซสใหม่ เทอร์มินัลที่ทำงานอยู่ในนั้นอาจสูญหาย" + } + }, + "tr": { + "stringUnit": { + "state": "translated", + "value": "cmux-tui yanıt vermiyor. Yeniden başlatmak takılan süreci durdurur ve yenisini başlatır. İçinde çalışan terminaller kaybolabilir." + } + }, + "uk": { + "stringUnit": { + "state": "translated", + "value": "cmux-tui не відповідає. Перезапуск зупинить завислий процес і запустить новий. Термінали, що працюють у ньому, можуть бути втрачені." + } + }, + "vi": { + "stringUnit": { + "state": "translated", + "value": "cmux-tui không phản hồi. Khởi động lại sẽ dừng tiến trình bị treo và chạy một tiến trình mới. Các terminal đang chạy trong đó có thể bị mất." + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "cmux-tui 没有响应。重启会停止卡住的进程并启动一个新进程。其中运行的终端可能会丢失。" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "cmux-tui 沒有回應。重新啟動會停止卡住的程序並啟動新的程序。其中執行中的終端機可能會遺失。" + } + } + } + }, + "daemon.recovery.confirmButton": { + "comment": "Confirm button of the Restart cmux-tui question.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Restart" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "再起動" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "إعادة التشغيل" + } + }, + "bs": { + "stringUnit": { + "state": "translated", + "value": "Ponovo pokreni" + } + }, + "da": { + "stringUnit": { + "state": "translated", + "value": "Genstart" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Neu starten" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Reiniciar" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Redémarrer" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Riavvia" + } + }, + "km": { + "stringUnit": { + "state": "translated", + "value": "ចាប់ផ្ដើមឡើងវិញ" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "재시작" + } + }, + "nb": { + "stringUnit": { + "state": "translated", + "value": "Start på nytt" + } + }, + "pl": { + "stringUnit": { + "state": "translated", + "value": "Uruchom ponownie" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Reiniciar" + } + }, + "ru": { + "stringUnit": { + "state": "translated", + "value": "Перезапустить" + } + }, + "th": { + "stringUnit": { + "state": "translated", + "value": "รีสตาร์ต" + } + }, + "tr": { + "stringUnit": { + "state": "translated", + "value": "Yeniden Başlat" + } + }, + "uk": { + "stringUnit": { + "state": "translated", + "value": "Перезапустити" + } + }, + "vi": { + "stringUnit": { + "state": "translated", + "value": "Khởi động lại" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "重启" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "重新啟動" + } + } + } + }, + "daemon.recovery.restarting": { + "comment": "Status line while cmux-tui is being restarted (up to about 20 seconds).", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Restarting cmux-tui…" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "cmux-tui を再起動しています…" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "جارٍ إعادة تشغيل cmux-tui…" + } + }, + "bs": { + "stringUnit": { + "state": "translated", + "value": "Ponovno pokretanje cmux-tui…" + } + }, + "da": { + "stringUnit": { + "state": "translated", + "value": "Genstarter cmux-tui…" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "cmux-tui wird neu gestartet…" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Reiniciando cmux-tui…" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Redémarrage de cmux-tui…" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Riavvio di cmux-tui…" + } + }, + "km": { + "stringUnit": { + "state": "translated", + "value": "កំពុងចាប់ផ្ដើម cmux-tui ឡើងវិញ…" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "cmux-tui 재시작 중…" + } + }, + "nb": { + "stringUnit": { + "state": "translated", + "value": "Starter cmux-tui på nytt…" + } + }, + "pl": { + "stringUnit": { + "state": "translated", + "value": "Ponowne uruchamianie cmux-tui…" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Reiniciando o cmux-tui…" + } + }, + "ru": { + "stringUnit": { + "state": "translated", + "value": "Перезапуск cmux-tui…" + } + }, + "th": { + "stringUnit": { + "state": "translated", + "value": "กำลังรีสตาร์ต cmux-tui…" + } + }, + "tr": { + "stringUnit": { + "state": "translated", + "value": "cmux-tui yeniden başlatılıyor…" + } + }, + "uk": { + "stringUnit": { + "state": "translated", + "value": "Перезапуск cmux-tui…" + } + }, + "vi": { + "stringUnit": { + "state": "translated", + "value": "Đang khởi động lại cmux-tui…" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "正在重启 cmux-tui…" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "正在重新啟動 cmux-tui…" + } + } + } + }, + "daemon.recovery.failed": { + "comment": "Status line after Restart cmux-tui did not work; Retry Now stays next to it.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "cmux-tui could not be restarted. Try again in a moment." + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "cmux-tui を再起動できませんでした。しばらくしてからもう一度お試しください。" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "تعذّرت إعادة تشغيل cmux-tui. حاول مرة أخرى بعد قليل." + } + }, + "bs": { + "stringUnit": { + "state": "translated", + "value": "cmux-tui nije moguće ponovo pokrenuti. Pokušajte ponovo za trenutak." + } + }, + "da": { + "stringUnit": { + "state": "translated", + "value": "cmux-tui kunne ikke genstartes. Prøv igen om lidt." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "cmux-tui konnte nicht neu gestartet werden. Versuche es gleich noch einmal." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "No se pudo reiniciar cmux-tui. Inténtalo de nuevo en un momento." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Impossible de redémarrer cmux-tui. Réessayez dans un instant." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Impossibile riavviare cmux-tui. Riprova tra un momento." + } + }, + "km": { + "stringUnit": { + "state": "translated", + "value": "មិនអាចចាប់ផ្ដើម cmux-tui ឡើងវិញបានទេ។ សូមព្យាយាមម្ដងទៀតបន្តិចទៀត។" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "cmux-tui를 재시작할 수 없습니다. 잠시 후 다시 시도하세요." + } + }, + "nb": { + "stringUnit": { + "state": "translated", + "value": "Kunne ikke starte cmux-tui på nytt. Prøv igjen om litt." + } + }, + "pl": { + "stringUnit": { + "state": "translated", + "value": "Nie udało się ponownie uruchomić cmux-tui. Spróbuj ponownie za chwilę." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Não foi possível reiniciar o cmux-tui. Tente novamente em instantes." + } + }, + "ru": { + "stringUnit": { + "state": "translated", + "value": "Не удалось перезапустить cmux-tui. Повторите попытку чуть позже." + } + }, + "th": { + "stringUnit": { + "state": "translated", + "value": "ไม่สามารถรีสตาร์ต cmux-tui ได้ โปรดลองอีกครั้งในอีกสักครู่" + } + }, + "tr": { + "stringUnit": { + "state": "translated", + "value": "cmux-tui yeniden başlatılamadı. Birazdan tekrar deneyin." + } + }, + "uk": { + "stringUnit": { + "state": "translated", + "value": "Не вдалося перезапустити cmux-tui. Спробуйте ще раз за мить." + } + }, + "vi": { + "stringUnit": { + "state": "translated", + "value": "Không thể khởi động lại cmux-tui. Hãy thử lại sau giây lát." + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "无法重启 cmux-tui。请稍后再试。" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "無法重新啟動 cmux-tui。請稍後再試。" + } + } + } + } + }, + "version": "1.0" +} diff --git a/Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/DaemonRestartTests.swift b/Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/DaemonRestartTests.swift index 2067c2188fee..8eb38cd67fdb 100644 --- a/Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/DaemonRestartTests.swift +++ b/Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/DaemonRestartTests.swift @@ -1,6 +1,7 @@ import AppKit @testable import CmuxNextApp import CmuxNextDaemon +@testable import CmuxNextDaemonRecovery import CmuxNextDesign import Foundation import Testing diff --git a/Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/DaemonUnavailableScreenTests.swift b/Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/DaemonUnavailableScreenTests.swift index 487a273aaf1a..1d4de910ecfc 100644 --- a/Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/DaemonUnavailableScreenTests.swift +++ b/Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/DaemonUnavailableScreenTests.swift @@ -1,6 +1,7 @@ import AppKit @testable import CmuxNextApp import CmuxNextDaemon +@testable import CmuxNextDaemonRecovery import Testing /// The window cmux shows when its background service cannot start says so in plain words and diff --git a/Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/ThemedNoticeControlsTests.swift b/Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/ThemedNoticeControlsTests.swift index ea5a4158fb78..94475b2686ec 100644 --- a/Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/ThemedNoticeControlsTests.swift +++ b/Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/ThemedNoticeControlsTests.swift @@ -2,6 +2,7 @@ import AppKit @testable import CmuxNextApp @testable import CmuxNextBookmarks import CmuxNextDaemon +@testable import CmuxNextDaemonRecovery @testable import CmuxNextDesign @testable import CmuxNextHistory @testable import CmuxNextTerminal diff --git a/Packages/macOS/CmuxNext/ci-target-graph.json b/Packages/macOS/CmuxNext/ci-target-graph.json index eaa8c504f19a..ad3783a4f4ef 100644 --- a/Packages/macOS/CmuxNext/ci-target-graph.json +++ b/Packages/macOS/CmuxNext/ci-target-graph.json @@ -174,6 +174,7 @@ "CmuxNextControl", "CmuxNextCrashReporting", "CmuxNextDaemon", + "CmuxNextDaemonRecovery", "CmuxNextDaemonService", "CmuxNextDesign", "CmuxNextFeed", @@ -241,6 +242,7 @@ "CmuxNextCopyMode", "CmuxNextCrashReporting", "CmuxNextDaemon", + "CmuxNextDaemonRecovery", "CmuxNextDaemonService", "CmuxNextFocusUI", "CmuxNextHistory", @@ -460,6 +462,19 @@ ], "packages": [] }, + "CmuxNextDaemonRecovery": { + "kind": "regular", + "path": "Packages/macOS/CmuxNext/Sources/CmuxNextDaemonRecovery", + "targets": [ + "CmuxNextApps", + "CmuxNextCompat", + "CmuxNextDaemon", + "CmuxNextDaemonService", + "CmuxNextDesign", + "CmuxNextWakeups" + ], + "packages": [] + }, "CmuxNextDaemonService": { "kind": "regular", "path": "Packages/macOS/CmuxNext/Sources/CmuxNextDaemonService", From a4cb5988a0b5fd3ae3ea7cabe3d13ed422880bf9 Mon Sep 17 00:00:00 2001 From: Leo Li Date: Wed, 14 Oct 2026 21:50:16 -0700 Subject: [PATCH 05/18] fix: DaemonConnectingView's overrides are public with the class (cx-jniyz.1) Co-Authored-By: Claude Opus 5.5 --- .../CmuxNextDaemonRecovery/DaemonConnectingView.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextDaemonRecovery/DaemonConnectingView.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextDaemonRecovery/DaemonConnectingView.swift index 172fce627aec..b67831c1f649 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextDaemonRecovery/DaemonConnectingView.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextDaemonRecovery/DaemonConnectingView.swift @@ -82,9 +82,9 @@ public final class DaemonConnectingView: NSView { } @available(*, unavailable) - required init?(coder: NSCoder) { fatalError("init(coder:) is not supported") } + public required init?(coder: NSCoder) { fatalError("init(coder:) is not supported") } - override func viewDidChangeEffectiveAppearance() { + public override func viewDidChangeEffectiveAppearance() { super.viewDidChangeEffectiveAppearance() performWithTheme { titleLabel.textColor = Palette.textSecondary @@ -92,7 +92,7 @@ public final class DaemonConnectingView: NSView { } } - override func viewDidMoveToWindow() { + public override func viewDidMoveToWindow() { super.viewDidMoveToWindow() viewDidChangeEffectiveAppearance() if window == nil { From eb35810bf81fc8aece49a2f4cd52432cfc14fa62 Mon Sep 17 00:00:00 2001 From: Leo Li Date: Wed, 14 Oct 2026 21:57:44 -0700 Subject: [PATCH 06/18] fix: the App Store's recovery actions are a DaemonRecovery extension, not a public namespace enum (cx-jniyz.1) Co-Authored-By: Claude Opus 5.5 --- .../Sources/CmuxNextApp/Apps/AppsService.swift | 2 +- .../DaemonRecoveryControls.swift | 11 ++++++++--- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Apps/AppsService.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Apps/AppsService.swift index 8372f93a4021..c31e4cde36b2 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Apps/AppsService.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Apps/AppsService.swift @@ -38,7 +38,7 @@ final class AppsService { /// outlives the service still makes models. private(set) lazy var storePages = AppStorePages { [weak self, client = self.client, recovery = self.services.daemon.recovery] in let model = AppStoreModel(client: client) - model.recovery = { DaemonRestartPrompt.appsRecovery(recovery) } + model.recovery = { recovery.appsRecovery } model.onNavigate = { self?.services.locationTrail.pageHistoryDidChange() } return model } diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextDaemonRecovery/DaemonRecoveryControls.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextDaemonRecovery/DaemonRecoveryControls.swift index ac06c730125e..fa80a1986653 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextDaemonRecovery/DaemonRecoveryControls.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextDaemonRecovery/DaemonRecoveryControls.swift @@ -1,4 +1,4 @@ -public import AppKit +import AppKit public import CmuxNextApps import CmuxNextCompat public import CmuxNextDaemonService @@ -32,7 +32,7 @@ enum DaemonRecoveryStrings { /// Restart cmux-tui's question and the App Store banner's controls, one path /// for both surfaces (cx-jniyz.1). The question is the cmux dialog every /// destructive confirm uses, so the debug socket can answer it. -public enum DaemonRestartPrompt { +enum DaemonRestartPrompt { static let confirmID = "restart" static let identifier = "cmux.dialog.restartDaemon" @@ -56,7 +56,7 @@ public enum DaemonRestartPrompt { } /// The App Store's disconnected banner controls (AppStoreModel.recovery). - public static func appsRecovery(_ recovery: DaemonRecovery) -> AppsRecovery { + static func appsRecovery(_ recovery: DaemonRecovery) -> AppsRecovery { guard recovery.canRetry else { return AppsRecovery(actions: [], status: nil) } var actions = [AppsRecoveryAction(id: "retryNow", title: DaemonRecoveryStrings.retryNow, isEnabled: !recovery.isRestarting) { recovery.retryNow() @@ -70,6 +70,11 @@ public enum DaemonRestartPrompt { } } +extension DaemonRecovery { + /// The App Store's disconnected banner controls (AppStoreModel.recovery). + public var appsRecovery: AppsRecovery { DaemonRestartPrompt.appsRecovery(self) } +} + /// Retry Now and Restart cmux-tui under the launch window's failure /// (cx-jniyz.1). Restart shows only once a plain ensure failed as a stuck /// owner makes it fail (`DaemonRecovery.offersRestart`). From 930ef820281f021593dbc1125ee92fea2036f912 Mon Sep 17 00:00:00 2001 From: Lawrence Chen <54008264+lawrencecchen@users.noreply.github.com> Date: Sun, 18 Oct 2026 04:44:01 -0700 Subject: [PATCH 07/18] motion-record: strip surface, focus left/right across a scrolling strip of tabbed columns Co-Authored-By: Claude Opus 5.5 --- scripts/cmux-next/motion-record.py | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/scripts/cmux-next/motion-record.py b/scripts/cmux-next/motion-record.py index b07a0e1d6fcc..8e5d1bd5bd41 100644 --- a/scripts/cmux-next/motion-record.py +++ b/scripts/cmux-next/motion-record.py @@ -191,6 +191,30 @@ def focus(): record("focus", "down", lambda: action("focusDown")) +def strip(): + # Focus moves left and right across a scrolling strip of columns, each with two tabs: the camera + # scrolls and every column's tab bar moves with it (Lawrence 2026-10-18: "tabbar when i move + # around left/right in niri is bad"). + action("workspace.newAtBottom") + time.sleep(1.0) # test harness: the workspace settles + for _ in range(3): + action("newColumn") + time.sleep(0.8) # test harness: one column at a time + for _ in range(4): + action("newSurface") + time.sleep(0.4) # test harness: one tab at a time + action("focusLeft") + time.sleep(0.4) # test harness: the focus settles + for _ in range(3): + action("focusRight") + time.sleep(0.6) # test harness: the camera settles + save_layout("strip") + record("strip", "left-1", lambda: action("focusLeft")) + record("strip", "left-2", lambda: action("focusLeft")) + record("strip", "right-1", lambda: action("focusRight")) + record("strip", "right-2", lambda: action("focusRight")) + + def groups(): # A tab group of two tabs collapses into its chip and expands again. action("workspace.newAtBottom") @@ -489,7 +513,7 @@ def hovercard(): -SURFACES = {"tabs": tabs, "panes": panes, "focus": focus, "toasts": toasts, "popups": popups, "hover": hover, "groups": groups, "drag": drag, "dock": dock, "sidebar": sidebar, "palette": palette, "hovercard": hovercard, "panels": panels, "everyday": everyday, "switch": switch} +SURFACES = {"strip": strip, "tabs": tabs, "panes": panes, "focus": focus, "toasts": toasts, "popups": popups, "hover": hover, "groups": groups, "drag": drag, "dock": dock, "sidebar": sidebar, "palette": palette, "hovercard": hovercard, "panels": panels, "everyday": everyday, "switch": switch} def main(): From 6a97d1f7a237807d2dcac62a0575f58d11ca6adc Mon Sep 17 00:00:00 2001 From: Lawrence Chen <54008264+lawrencecchen@users.noreply.github.com> Date: Sun, 18 Oct 2026 05:07:54 -0700 Subject: [PATCH 08/18] cmux-next strip: a column's tab bar lays out for where its pane rests, never pinned to the traffic lights while the camera scrolls The window-controls inset was computed from the strip's presented window frame on every frame of a strip scroll. A column sliding in from under the sidebar overlapped the sidebar's toolbar band and traffic lights, so its tabs were pushed right to clear them and stood still while the column moved (Lawrence 2026-10-18: tabbar bad when moving left/right). Now obstacles count only over the part of the strip its ancestors show, and a strip inside a pane on a layout spring measures where that pane rests (RestingPlacement, set by applyPresentation from the spring target and the camera target), so its layout changes at most once when motion starts and the tabs then move rigidly with the column. Co-Authored-By: Claude Opus 5.5 --- .../CmuxNextDesign/RestingPlacement.swift | 12 +++++ .../CmuxNextLayout/Views/PaneHostView.swift | 5 ++- .../Views/ScreenContentView+Motion.swift | 5 ++- .../TabStripView+WindowControls.swift | 44 ++++++++++++++++--- 4 files changed, 57 insertions(+), 9 deletions(-) create mode 100644 Packages/macOS/CmuxNext/Sources/CmuxNextDesign/RestingPlacement.swift diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextDesign/RestingPlacement.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextDesign/RestingPlacement.swift new file mode 100644 index 000000000000..3ac5d6e9ef0f --- /dev/null +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextDesign/RestingPlacement.swift @@ -0,0 +1,12 @@ +public import AppKit + +/// A view a layout spring shows away from where it will rest: a pane host +/// while the strip camera scrolls, or a split pane sliding in. Window-relative +/// chrome inside it (a tab strip clearing the traffic lights) measures against +/// the rest, so it lays out once, at its final place, and then moves rigidly +/// with the view instead of laying out again on every frame of the motion. +@MainActor +public protocol RestingPlacement: NSView { + /// Where the view will rest minus where it shows now, in its superview's points. + var restOffset: CGVector { get } +} diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextLayout/Views/PaneHostView.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextLayout/Views/PaneHostView.swift index aadd5e491fa2..77391a5abc0a 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextLayout/Views/PaneHostView.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextLayout/Views/PaneHostView.swift @@ -16,7 +16,7 @@ import QuartzCore /// `OverlayPlane`, not in this view, so they draw above content that is a /// child window (Chromium pages); the root keeps them on this view's /// displayed frame. -final class PaneHostView: NSView { +final class PaneHostView: NSView, RestingPlacement { let pane: PaneID let content: NSView let chrome = PaneOverlayView() @@ -59,6 +59,9 @@ final class PaneHostView: NSView { /// What of the host shows while a spring rect reveals it (a split's arriving pane, a zooming /// one; superview coordinates); nil, its whole frame. Panes under it are clipped by this. var revealRect: CGRect? + /// Where the host will rest minus where it shows now (`RestingPlacement`): its spring rect's + /// target and the strip camera's target, against the presented frame. Zero at rest. + var restOffset: CGVector = .zero var isDocked = false { didSet { if isDocked != oldValue { applyDockFill() } } diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextLayout/Views/ScreenContentView+Motion.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextLayout/Views/ScreenContentView+Motion.swift index 8d21a7abf36b..72e1a94ccc3a 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextLayout/Views/ScreenContentView+Motion.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextLayout/Views/ScreenContentView+Motion.swift @@ -42,7 +42,7 @@ extension ScreenContentView { /// New dock columns' panes still growing in from their screen edge (their frames). var growingDocks: [AnimatedFrame] { PaneArrival.growing(arriving, frames: paneFrames, in: geometry, docks: newDockColumns) } func applyPresentation() { - let strip = stripShift + let strip = stripShift, restStrip = geometry.viewShift(offset: isUserScrolling ? scroll.value : scroll.target) let uncovered = uncoveredRect, clip = PaneArrival.stripClip(geometry, docks: growingDocks) var clips: [(host: PaneHostView, scrolls: Bool, revealing: CGRect?)] = [] for (pane, frame) in paneFrames { @@ -61,6 +61,9 @@ extension ScreenContentView { ? SplitMotion.visible(frame.rect, padding: host.padding).offsetBy(dx: shift.dx, dy: shift.dy) : nil let placed = (zoomed ?? held ?? (arriving.contains(pane) ? PaneArrival.presentation(shown: frame.rect, target: frame.targetRect) : frame.rect)) .offsetBy(dx: shift.dx, dy: shift.dy) + // Before the frame moves: chrome that re-checks itself on the move reads the rest. + let rest = frame.targetRect.offsetBy(dx: scrolls ? restStrip : 0, dy: shift.dy) + host.restOffset = CGVector(dx: rest.minX - placed.minX, dy: rest.minY - placed.minY) if host.frame != placed { host.frame = placed } (host.alphaValue, host.isDocked, host.revealRect) = (frame.alpha.value, !scrolls, revealing) clips.append((host, scrolls, revealing)) diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextTabs/TabStripView+WindowControls.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextTabs/TabStripView+WindowControls.swift index 44deb3ef7652..075c14f1465b 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextTabs/TabStripView+WindowControls.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextTabs/TabStripView+WindowControls.swift @@ -3,8 +3,12 @@ import CmuxNextDesign // A strip under the window's traffic lights (minimal titlebar, sidebar // hidden, the top-left pane) starts its tabs after them. Only strips whose -// frame overlaps the traffic lights get the inset, so it follows sidebar -// show and hide, splits, column scrolling and fullscreen. +// shown part overlaps the traffic lights get the inset, so it follows sidebar +// show and hide, splits, column scrolling and fullscreen. A strip in a pane on +// a layout spring (the strip camera scrolling, a split sliding in) measures +// where its pane will rest (`RestingPlacement`): it lays out once, for its +// final place, and its tabs then move with the column, never pinned to the +// traffic lights while the column slides under them or under the sidebar. extension TabStripView { /// Re-checks the traffic lights after the strip moved in its window /// without a resize (the App calls it when the pane's window frame @@ -19,15 +23,41 @@ extension TabStripView { func computeWindowControlsInset() -> CGFloat { guard let window else { return 0 } let host = window as? TitlebarAccessoryHosting - return Self.windowControlsInset(strip: convert(bounds, to: nil), lights: WindowTitlebar.trafficLightsFrame(in: window), accessory: host?.titlebarAccessoryFrame, - padding: metrics.stripHorizontalPadding) + let (strip, shown) = restingWindowRect() + return Self.windowControlsInset(strip: strip, shown: shown, lights: WindowTitlebar.trafficLightsFrame(in: window), + accessory: host?.titlebarAccessoryFrame, padding: metrics.stripHorizontalPadding) + } + + /// The strip's frame in window coordinates where its pane will rest, and the window + /// area its ancestors above that pane show (their bounds, which do not move with it). + private func restingWindowRect() -> (strip: CGRect, shown: CGRect) { + var placement: (any RestingPlacement)? + var ancestor = superview + while let view = ancestor, placement == nil { + placement = view as? RestingPlacement + ancestor = view.superview + } + var strip = convert(bounds, to: nil) + if let placement, let parent = placement.superview { + let now = parent.convert(placement.frame, to: nil) + let rest = parent.convert(placement.frame.offsetBy(dx: placement.restOffset.dx, dy: placement.restOffset.dy), to: nil) + strip = strip.offsetBy(dx: rest.minX - now.minX, dy: rest.minY - now.minY) + } + var shown = CGRect.infinite + while let view = ancestor { + shown = shown.intersection(view.convert(view.bounds, to: nil)) + ancestor = view.superview + } + return (strip, shown) } /// Leading points a strip at `strip` (window coordinates) keeps clear of - /// the traffic lights and the window's titlebar accessory (pure). - static func windowControlsInset(strip: CGRect, lights: CGRect?, accessory: CGRect?, padding: CGFloat) -> CGFloat { + /// the traffic lights and the window's titlebar accessory where they lie + /// over its `shown` part (pure). + static func windowControlsInset(strip: CGRect, shown: CGRect, lights: CGRect?, accessory: CGRect?, padding: CGFloat) -> CGFloat { + let visible = strip.intersection(shown) let obstacles = [lights, accessory].compactMap { $0 }.filter { frame in - strip.minY < frame.maxY && strip.maxY > frame.minY && strip.minX < frame.maxX && strip.maxX > frame.minX + !visible.isNull && visible.minY < frame.maxY && visible.maxY > frame.minY && visible.minX < frame.maxX && visible.maxX > frame.minX } guard let right = obstacles.map(\.maxX).max() else { return 0 } let clear = right + Metrics.space3 - strip.minX - padding From 780f1eccae0ebccdc7f0193d30e159fcb7f77cc8 Mon Sep 17 00:00:00 2001 From: Lawrence Chen <54008264+lawrencecchen@users.noreply.github.com> Date: Sun, 18 Oct 2026 05:07:56 -0700 Subject: [PATCH 09/18] cmux-next strip: terminals in the keep-alive band keep drawing, so a column scrolled in is never blank A paused Ghostty surface releases its swap chain (renderer setVisible false), so a column the strip camera scrolled in showed an empty terminal for about five frames until it resumed and redrew. Terminal keys whose pane is in the keep-alive band now render; browser pages there (unretained, hibernation owns them) stay paused. Co-Authored-By: Claude Opus 5.5 --- .../CmuxNextBridge/SurfaceLedger.swift | 19 ++++++++++++------- .../CmuxNextLayout/Model/PanePresence.swift | 2 +- .../Views/ScreenContentView+Visibility.swift | 2 +- plans/cmux-next/architecture.md | 4 ++-- 4 files changed, 16 insertions(+), 11 deletions(-) diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextBridge/SurfaceLedger.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextBridge/SurfaceLedger.swift index cfc66451bf50..3e81ab0a0d50 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextBridge/SurfaceLedger.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextBridge/SurfaceLedger.swift @@ -13,11 +13,15 @@ /// /// A key renders while its owner is visible. A key whose owner is in the /// keep-alive band (an off-screen strip column within one viewport width, -/// architecture.md 4) is paused but pinned: never evicted, so scrolling back -/// shows it at once. Other keys that stop rendering enter the -/// ``SurfaceRetention`` LRU; an evicted key is destroyed, and if a pane still -/// presents it off screen, that pane is told so it re-presents (and the App -/// re-attaches with a daemon replay) when it comes near the viewport again. +/// architecture.md 4) is pinned: never evicted, and a terminal there keeps +/// drawing, so a column the strip camera scrolls in shows its current frame +/// from its first pixel (a paused Ghostty surface frees its swap chain and +/// shows nothing for several frames after it resumes). A browser page +/// there (`setRetained(false)`: hibernation owns it) stays paused. Other +/// keys that stop rendering enter the ``SurfaceRetention`` LRU; an evicted +/// key is destroyed, and if a pane still presents it off screen, that pane is +/// told so it re-presents (and the App re-attaches with a daemon replay) when +/// it comes near the viewport again. public struct SurfaceLedger: Sendable { /// What the caller must do after a ledger change, in order. public struct Effects: Equatable, Sendable { @@ -162,7 +166,7 @@ public struct SurfaceLedger