diff --git a/Packages/macOS/CmuxNext/Package.swift b/Packages/macOS/CmuxNext/Package.swift index 024fd12c15d9..9a7cfaf5e013 100644 --- a/Packages/macOS/CmuxNext/Package.swift +++ b/Packages/macOS/CmuxNext/Package.swift @@ -531,11 +531,12 @@ let packageTargets: [Target] = uiLayerPreviewTargets + [ swiftSettings: daemonSwiftSettings ), // The cards' center: the scan schedule, the job runner and the `import.*` actions, for - // Settings › Import (no Home or New Tab suggestion). The App supplies `ImportCardsHost`. + // Settings › Import (no Home or New Tab suggestion), and the sidebar's Conductor note + // (CmuxNextSidebar's notice card). The App supplies `ImportCardsHost`. .target( name: "CmuxNextImportCards", - dependencies: ["CmuxNextActions", "CmuxNextBrowserImport", "CmuxNextCompat", "CmuxNextDesign", - "CmuxNextImportSuggestions", "CmuxNextOnboarding", "CmuxNextSettings"], + dependencies: ["CmuxNextActions", "CmuxNextBrowserImport", "CmuxNextCompat", "CmuxNextDaemon", "CmuxNextDesign", + "CmuxNextImportSuggestions", "CmuxNextOnboarding", "CmuxNextSettings", "CmuxNextSidebar"], swiftSettings: uiSwiftSettings ), .target( diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextApp/AppServices+ConductorImportHost.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextApp/AppServices+ConductorImportHost.swift new file mode 100644 index 000000000000..42844061334d --- /dev/null +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextApp/AppServices+ConductorImportHost.swift @@ -0,0 +1,9 @@ +import CmuxNextAppCore +import CmuxNextDaemon +import CmuxNextImportCards + +extension AppServices: ConductorImportHost { + var conductorDaemon: DaemonConnection? { daemon.connection } + func conductorWorkspaceKey(_ id: String) -> WorkspaceKey? { daemon.store.workspace(id: id)?.key } + func openConductorWorkspace(folder: String, name: String, key: WorkspaceKey) async throws -> String { try await windows.createWorkspace(WorkspaceSpawn(cwd: folder, name: name, key: key)) } +} diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextApp/ChiefMemoryImport/ChiefMemoryImportHandlers.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextApp/ChiefMemoryImport/ChiefMemoryImportHandlers.swift index 92d6dc2a265b..a7a251ffc89d 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextApp/ChiefMemoryImport/ChiefMemoryImportHandlers.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextApp/ChiefMemoryImport/ChiefMemoryImportHandlers.swift @@ -6,6 +6,7 @@ import CmuxNextCompat import CmuxNextDaemon import CmuxNextDesign import CmuxNextHomeEngine +import CmuxNextImportSuggestions import CmuxNextPages import CmuxNextSettings import Foundation @@ -83,6 +84,7 @@ enum ChiefMemoryImportHandlers { return try CmuxNextSettings.JSONValue.parse(Data(output.stdout.utf8)) } provider.restart = { [weak services] in _ = services?.home.brain.ensure(asked: true) } + provider.notice = { ImportScanner(home: FileManager.default.homeDirectoryForCurrentUser).conductorOnboardingLine(now: Date()) } let native = AppPageNativeProvider(services: services, page: .importHistory) let routes = [PageRoute(prefix: "cmux.importHistory.", provider: provider), PageRoute(prefix: "cmux.app.", provider: native)] guard let page = PageWebView(descriptor: .importHistory, routes: routes) else { return nil } diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Handlers/OnboardingHandlers.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Handlers/OnboardingHandlers.swift index 2888fb1789f1..743924e17ae0 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Handlers/OnboardingHandlers.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Handlers/OnboardingHandlers.swift @@ -98,14 +98,15 @@ enum OnboardingHandlers { }) } - /// What the import cards run through the App: the Import from Browser window's import run - /// and Import Workspaces from Classic cmux. + /// What the import cards run through the App: the Import from Browser window's import run, + /// Import Workspaces from Classic cmux and Import from Conductor. static func importCardsHost(_ services: AppServices) -> ImportCardsHost { ImportCardsHost( runImport: { plan, keys, progress in try await AppOnboardingServices(owner: services.onboarding).runImport(plan, keys: keys, progress: progress) }, importedProfiles: { Set(await services.onboarding.importStore.sources().map(\.sourceKey)) }, openSettings: { route in try? services.settingsWindow.show(section: nil, route: route) }, - importClassic: { progress in try await importClassic(services, in: nil, progress: progress)?.opened }) + importClassic: { progress in try await importClassic(services, in: nil, progress: progress)?.opened }, + importConductor: { progress in try await ConductorImport(host: services).run(progress: progress) }) } /// Import Workspaces from Classic cmux as every entrypoint runs it (the action, the cmux diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Onboarding/ClassicImportConfirmation.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Onboarding/ClassicImportConfirmation.swift index ee0175f07baf..a7b44e79d67a 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Onboarding/ClassicImportConfirmation.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Onboarding/ClassicImportConfirmation.swift @@ -1,5 +1,6 @@ import AppKit import CmuxNextDesign +import CmuxNextOnboarding /// The preview and mandatory answer before a classic snapshot is restored. @MainActor diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Onboarding/ClassicMigrationRunState.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Onboarding/ClassicMigrationRunState.swift index 0f4ddb090926..b8e58f82ecf7 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Onboarding/ClassicMigrationRunState.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Onboarding/ClassicMigrationRunState.swift @@ -52,10 +52,3 @@ final class ClassicMigrationRunState { progress?(Double(handledCount) / Double(max(plan.workspaces.count, 1))) } } - -/// Classic's session as read, the save it came from, and the files to back up. -struct ClassicSnapshotRead { - var snapshot: ClassicSessionSnapshot - var source: ClassicMigrationSource - var backups: [URL] -} diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextApp/SidebarBridge+Cards.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextApp/SidebarBridge+Cards.swift index 5002a5599ccf..98181142273f 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextApp/SidebarBridge+Cards.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextApp/SidebarBridge+Cards.swift @@ -2,6 +2,7 @@ import CmuxNextCompat import Foundation import CmuxNextActions import CmuxNextAgentLanes +import CmuxNextImportCards import CmuxNextNowPlaying import CmuxNextPages import CmuxNextSettings @@ -86,6 +87,8 @@ enum SidebarCardFeed { case .setAutomaticUpdates(let on): services.updater.setAutomaticUpdates(on) case .openUpdateLink(let url): openUpdateLink(url, services: services) case .openUpdateChanges: _ = services.updater.openStagedChangelog() + case .noticeAction(let card, let action) where ImportSuggestionsCenter.shared.answerConductorNote(card, action: action): break + case .dismissNotice(let card) where ImportSuggestionsCenter.shared.answerConductorNote(card, action: nil): break // A question waiting on the person (an agent's Cloud machine request, ``SidebarAsks``) takes its answer first. case .noticeAction(let card, let action) where !services.machineAsks.answer(card, action: action): noticeAction(card, action, updater: services.updater, open: { openUpdateLink($0, services: services) }) @@ -168,9 +171,9 @@ enum SidebarCardFeed { /// The shared notice card (Lawrence 2026-10-09), one message at a time: /// the update status (a check's progress and result, a found update), - /// then the active test feed, then the newest announcement, then today's - /// "Did you know" tip. Nil while the staged update card shows; - /// announcements and the tip also wait for the "cmux Updated!" card. + /// then the active test feed, then Import from Conductor's note (until Nov 20), then the newest + /// announcement, then today's "Did you know" tip. Nil while the staged update card shows; + /// the note, announcements and the tip also wait for the "cmux Updated!" card. static func noticeCard(_ updater: UpdaterService, registry: ActionRegistry?) -> SidebarNoticeCard? { guard updater.readyCard == nil else { return nil } if let shown = updater.cardPresentation { return notice(shown, asked: updater.flow.userAsked) } @@ -179,6 +182,7 @@ enum SidebarCardFeed { actions: [SidebarNoticeCard.Action(id: useRealFeedActionID, title: text.useRealFeed)]) } guard !updater.whatsNew.showsUpdatedCard else { return nil } + if let note = ImportSuggestionsCenter.shared.conductorNote { return note } if let item = updater.announcements.first { let actions = item.action.flatMap { id in PageDescriptor.changelogTryItActions.contains(id) ? [SidebarNoticeCard.Action(id: id, title: UpdaterService.announcementActionTitle)] : nil diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextApp/WorkspaceSpawn.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextApp/WorkspaceSpawn.swift index 23a694cfe71f..50cb690a7562 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextApp/WorkspaceSpawn.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextApp/WorkspaceSpawn.swift @@ -50,7 +50,7 @@ struct WorkspaceSpawner { /// sends the daemon's own next default (`SidebarMapping.defaultWorkspaceName`), /// so the row's title is the daemon's from the first frame. func start() -> () async throws -> String { - let key = WorkspaceKey.generate(), daemon = daemon, services = windows.services, machine = MachineID(daemon.machineID) + let key = spawn.key ?? WorkspaceKey.generate(), daemon = daemon, services = windows.services, machine = MachineID(daemon.machineID) if let windowID { windows.claimNew(workspaceID: key.rawValue, window: windowID, frame: frame) } guard daemon.connection != nil else { return { [windows] in throw windows.createFailed(key.rawValue, DaemonError.notConnected) } } var row = spawn.slot == nil ? NewWorkspacePlacements.rule(for: windowID, in: windows) : nil diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextAppCore/WorkspaceSpawn.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextAppCore/WorkspaceSpawn.swift index 0e69a9a7c3a5..d8273e9f5355 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextAppCore/WorkspaceSpawn.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextAppCore/WorkspaceSpawn.swift @@ -18,6 +18,9 @@ public protocol WorkspaceSpawnSidebar: AnyObject { /// name, command, and extra environment through `newTab`'s arguments. public struct WorkspaceSpawn: Sendable { public var cwd: String? + /// The new workspace's key, fixed by a caller that records the workspace before it exists (Import + /// from Conductor, so a run cut off midway knows which workspace it half made); nil: a new key. + public var key: WorkspaceKey? public var inheritedCwd: String? public var name: String? public var command: String? @@ -41,8 +44,9 @@ public struct WorkspaceSpawn: Sendable { public var onFirstTerminal: (@Sendable (DaemonConnection, CreateTerminalResult) async throws -> Void)? public init(cwd: String? = nil, name: String? = nil, command: String? = nil, env: [String: String] = [:], keep: Bool = false, - profile: ProfileID? = nil, newTabPage: Bool = false) { + profile: ProfileID? = nil, newTabPage: Bool = false, key: WorkspaceKey? = nil) { opensNewTabPage = newTabPage + self.key = key self.cwd = cwd self.name = name self.command = command diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextDaemon/Connection/ConductorImportClient.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextDaemon/Connection/ConductorImportClient.swift new file mode 100644 index 000000000000..1ea508f0cc41 --- /dev/null +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextDaemon/Connection/ConductorImportClient.swift @@ -0,0 +1,114 @@ +import Foundation + +/// Import from Conductor on the daemon (`conductor_import.*`, the +/// `cmux-conductor-import` crate): the dry run of what Conductor's repositories +/// and workspaces become here, and the ledger that keeps a rerun from making +/// anything twice. The daemon reads Conductor's files read-only; the app makes +/// the projects and workspaces the person confirms. +public struct ConductorImportClient: Sendable { + public let connection: DaemonConnection + + public init(connection: DaemonConnection) { + self.connection = connection + } + + /// `conductor_import.plan`: what an import would make now. Nil plan when + /// nothing of Conductor's is on this machine. + public func plan() async throws -> ConductorImportReply { + try await connection.resourceRequest({ id in + ResourceRequestEnvelope(id: id, operation: "conductor_import.plan", params: [:], idempotencyKey: nil) + }, as: ConductorImportReply.self) + } + + /// `conductor_import.record`: Conductor workspace `workspace` is being made + /// (`pending`, with the cmux workspace once there is one) or made (`done`). + public func record(workspace: String, stage: ConductorImportStage, cmuxWorkspace: String?, + runCompleted: Bool = false, idempotencyKey: String) async throws { + var params: [String: JSONValue] = ["workspace": .string(workspace), "stage": .string(stage.rawValue)] + if let cmuxWorkspace { params["cmux_workspace"] = .string(cmuxWorkspace) } + if runCompleted { params["run_completed"] = .bool(true) } + let fields = params + _ = try await connection.resourceRequest({ id in + ResourceRequestEnvelope(id: id, operation: "conductor_import.record", params: fields, idempotencyKey: idempotencyKey) + }, as: ResourceMutationResult.self) + } +} + +public enum ConductorImportStage: String, Sendable { + case pending, done +} + +public struct ConductorImportReply: Decodable, Sendable { + public var plan: ConductorImportPlan? + public var error: String? +} + +/// The dry run, as `cmux_conductor_import::plan::Plan` serializes it. +public struct ConductorImportPlan: Decodable, Sendable, Equatable { + public struct Project: Decodable, Sendable, Equatable { + public var repoID: String + public var name: String + public var path: String? + /// Why it is not imported (hidden in Conductor, folder gone). + public var skipped: String? + public var setup: String? + public var teardown: String? + public var worktreeinclude: [String] + public var envNames: [String] + public var workspaces: [Workspace] + + enum CodingKeys: String, CodingKey { + case repoID = "repo_id", name, path, skipped, setup, teardown, worktreeinclude, envNames = "env_names", workspaces + } + } + + public struct Workspace: Decodable, Sendable, Equatable { + public var id: String + public var name: String + public var branch: String? + public var path: String? + public var disposition: Disposition + /// What the import makes without being asked: live workspaces not imported before. + public var checked: Bool + public var chats: Chats? + } + + public struct Disposition: Decodable, Sendable, Equatable { + /// `adopt`, `recoverable`, `already-imported` or `skip`. + public var kind: String + /// The commit or branch a recoverable workspace comes back from. + public var from: String? + public var reason: String? + } + + public struct Chats: Decodable, Sendable, Equatable { + public var transcripts: Int + } + + public struct Warning: Decodable, Sendable, Equatable { + public var code: String + public var detail: String + } + + public var cloudShutdown: String + public var projects: [Project] + /// cmux workspaces a cut-off run half made: closed before they are made again. + public var cleanup: [String] + public var warnings: [Warning] + + enum CodingKeys: String, CodingKey { + case cloudShutdown = "cloud_shutdown", projects, cleanup, warnings + } + + /// The workspaces this run adopts, with their project. + public var adopting: [(project: Project, workspace: Workspace)] { + projects.filter { $0.skipped == nil }.flatMap { project in + project.workspaces.filter { $0.checked && $0.disposition.kind == "adopt" }.map { (project, $0) } + } + } + + /// Archived workspaces whose commit or branch still exists: listed, not made. + public var recoverable: [Workspace] { + projects.flatMap(\.workspaces).filter { $0.disposition.kind == "recoverable" } + } +} diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextImportCards/ConductorImport.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextImportCards/ConductorImport.swift new file mode 100644 index 000000000000..9233ed0d2dbf --- /dev/null +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextImportCards/ConductorImport.swift @@ -0,0 +1,140 @@ +public import CmuxNextDaemon +import AppKit +import CmuxNextDesign +import CmuxNextImportSuggestions +import Foundation + +/// What Import from Conductor needs from the App: this Mac's daemon, and its workspaces. +@MainActor +public protocol ConductorImportHost: AnyObject { + /// This Mac's daemon, which reads Conductor's files and keeps the import's ledger. + var conductorDaemon: DaemonConnection? { get } + /// The key of open workspace `id`, to close one a cut-off run half made. + func conductorWorkspaceKey(_ id: String) -> WorkspaceKey? + /// Opens a workspace at `folder` named `name` with key `key`; returns its id. + func openConductorWorkspace(folder: String, name: String, key: WorkspaceKey) async throws -> String +} + +/// Import from Conductor (the Conductor card in Settings › Import). The daemon reads +/// Conductor's database and folders read-only and plans the import +/// (`conductor_import.plan`); the person sees that plan and confirms it; then each of +/// Conductor's repositories is listed as a project and each live workspace opens as a cmux +/// workspace at its own folder, on its branch, where its chats already are. Archived +/// workspaces are listed as recoverable and not made. Every workspace is recorded in the +/// import's ledger before and after it is made, so a rerun makes nothing twice and a run cut +/// off midway replaces the workspace it half made. +@MainActor +public struct ConductorImport { + static let importButton = "import" + static let identifier = "cmux.dialog.conductorImport" + /// The most workspace names the preview lists before an ellipsis. + static let listedWorkspaces = 8 + + let host: any ConductorImportHost + + public init(host: any ConductorImportHost) { + self.host = host + } + + /// Runs the import; returns how many workspaces it opened, nil when the person declined. + /// A failure shows its toast, then throws. + public func run(progress: (@MainActor (Double) -> Void)? = nil) async throws -> Int? { + do { + return try await importing(progress: progress) + } catch { + Self.toast(ConductorImportStrings.failed) + throw error + } + } + + private func importing(progress: (@MainActor (Double) -> Void)?) async throws -> Int? { + guard let connection = host.conductorDaemon else { throw DaemonError.notConnected } + let client = ConductorImportClient(connection: connection) + let reply = try await client.plan() + if let error = reply.error { throw ConductorImportFailure.plan(error) } + guard let plan = reply.plan, !plan.adopting.isEmpty else { + Self.toast(ConductorImportStrings.allHere) + return 0 + } + guard await Self.confirm(plan) else { return nil } + + // A cut-off run's half-made workspaces close before they are made again. + for key in plan.cleanup.compactMap(host.conductorWorkspaceKey) { + _ = try? await connection.closeWorkspace(key, endTerminals: true) + } + let projects = ProjectStateClient(connection: connection) + for project in plan.projects where project.skipped == nil { + guard let path = project.path else { continue } + try? await projects.add(path: path, idempotencyKey: "conductor-import-project-\(project.repoID)") + } + let adopting = plan.adopting + let run = UUID().uuidString.lowercased() + var opened = 0 + for (index, item) in adopting.enumerated() { + guard let path = item.workspace.path else { continue } + let id = item.workspace.id + // The key is in the ledger before the workspace exists: a run cut off here leaves a + // pending entry naming it, which the next run's plan lists for cleanup. + let key = WorkspaceKey.generate() + try await client.record(workspace: id, stage: .pending, cmuxWorkspace: key.rawValue, + idempotencyKey: "conductor-import-\(run)-\(id)-pending") + let made = try await host.openConductorWorkspace(folder: path, name: item.workspace.name, key: key) + try await client.record(workspace: id, stage: .done, cmuxWorkspace: made, runCompleted: index == adopting.count - 1, + idempotencyKey: "conductor-import-\(run)-\(id)-done") + opened += 1 + progress?(Double(index + 1) / Double(adopting.count)) + } + Self.toast(opened > 0 ? ConductorImportStrings.opened : ConductorImportStrings.allHere) + return opened + } + + private static func confirm(_ plan: ConductorImportPlan) async -> Bool { + let window = NSApp.keyWindow ?? NSApp.mainWindow + let spec = CmuxDialogSpec( + title: ConductorImportStrings.previewTitle, + lines: previewLines(plan), + buttons: [.cancel(), CmuxDialogButton(id: importButton, title: ConductorImportStrings.previewButton, role: .default)], + identifier: identifier + ) + return await CmuxDialogCenter.shared.present(spec, in: window.map { .window($0) } ?? .app).button == importButton + } + + /// Counts first, then each workspace adopted (name and branch), then what does not come + /// over and the warning about archiving in Conductor. + static func previewLines(_ plan: ConductorImportPlan) -> [String] { + let adopting = plan.adopting + var lines = [ + ConductorImportStrings.projects(plan.projects.filter { $0.skipped == nil }.count), + ConductorImportStrings.adopted(adopting.count), + ] + for item in adopting.prefix(listedWorkspaces) { + let name = String(item.workspace.name.prefix(64)) + lines.append(item.workspace.branch.map { "• \(name) (\(String($0.prefix(64))))" } ?? "• \(name)") + } + if adopting.count > listedWorkspaces { lines.append("• …") } + let chats = adopting.compactMap(\.workspace.chats?.transcripts).reduce(0, +) + if chats > 0 { lines.append(ConductorImportStrings.chats(chats)) } + if !plan.recoverable.isEmpty { lines.append(ConductorImportStrings.recoverable(plan.recoverable.count)) } + if !plan.cleanup.isEmpty { lines.append(ConductorImportStrings.cleanup(plan.cleanup.count)) } + let names = Set(plan.projects.filter { $0.skipped == nil }.flatMap(\.envNames)).sorted() + if !names.isEmpty { lines.append(ConductorImportStrings.environment(names.prefix(12).joined(separator: ", "))) } + if plan.warnings.contains(where: { $0.code == "archive-deletes-folder" }) { lines.append(ConductorImportStrings.archiveWarning) } + lines.append(ConductorImportStrings.deadline) + return lines + } + + private static func toast(_ message: String) { + guard let window = NSApp.keyWindow ?? NSApp.mainWindow else { return } + _ = CmuxToastCenter.shared.show(CmuxToast(id: "conductorImport", message: message), in: window) + } +} + +enum ConductorImportFailure: Error, CustomStringConvertible { + case plan(String) + + var description: String { + switch self { + case .plan(let reason): "conductor import plan: \(reason)" + } + } +} diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextImportCards/ImportCardsHost.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextImportCards/ImportCardsHost.swift index 7bac3880882e..9dfe98837a9e 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextImportCards/ImportCardsHost.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextImportCards/ImportCardsHost.swift @@ -19,14 +19,21 @@ public struct ImportCardsHost { /// twice) and its toast. Reports the share of classic workspaces handled, 0...1, on the main /// actor. Returns how many workspaces this run opened, nil when the person declined the preview. public var importClassic: (@escaping @MainActor (Double) -> Void) async throws -> Int? + /// Runs Import from Conductor: the daemon's dry run (`conductor_import.plan`), its preview and + /// confirmation, then each live workspace adopted in place, + /// recorded in the import's ledger (so nothing comes over twice). Reports progress and + /// returns as `importClassic` does. + public var importConductor: (@escaping @MainActor (Double) -> Void) async throws -> Int? public init(runImport: @escaping (ImportPlan, any SafeStorageKeyProviding, @escaping @MainActor (ImportProgress) -> Void) async throws -> ImportSummary, importedProfiles: @escaping () async -> Set, openSettings: @escaping (String) -> Void, - importClassic: @escaping (@escaping @MainActor (Double) -> Void) async throws -> Int?) { + importClassic: @escaping (@escaping @MainActor (Double) -> Void) async throws -> Int?, + importConductor: @escaping (@escaping @MainActor (Double) -> Void) async throws -> Int?) { self.runImport = runImport self.importedProfiles = importedProfiles self.openSettings = openSettings self.importClassic = importClassic + self.importConductor = importConductor } } diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextImportCards/ImportSuggestionsCenter+ConductorNote.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextImportCards/ImportSuggestionsCenter+ConductorNote.swift new file mode 100644 index 000000000000..d28297d9bff6 --- /dev/null +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextImportCards/ImportSuggestionsCenter+ConductorNote.swift @@ -0,0 +1,32 @@ +public import CmuxNextSidebar +import CmuxNextImportSuggestions + +/// The sidebar's one-time Import from Conductor note, in the shared notice card: while this Mac +/// has Conductor, before Conductor Cloud shuts down, until the import starts or the person closes +/// it (``ImportSuggestionState/showsConductorNote(findings:jobs:now:)``). Its button opens +/// Settings › Import; its x closes it for good, kept with the cards' other state. +extension ImportSuggestionsCenter { + public static let conductorNoteID = "conductor-import" + static let conductorNoteOpen = "open-import" + + /// The note while it shows; nil otherwise, and while the cards are off. + public var conductorNote: SidebarNoticeCard? { + guard isEnabled, state.showsConductorNote(findings: findings, jobs: ledger, now: now()) else { return nil } + return SidebarNoticeCard(id: Self.conductorNoteID, symbol: "square.and.arrow.down", title: ConductorImportStrings.noteTitle, + detail: ConductorImportStrings.noteDetail, + actions: [SidebarNoticeCard.Action(id: Self.conductorNoteOpen, title: ConductorImportStrings.noteOpen, prominent: true)], + dismissLabel: ConductorImportStrings.noteClose) + } + + /// A press on notice `card`: the note's button opens Settings › Import, its x (`action` nil) + /// closes it. False when `card` is another owner's. + public func answerConductorNote(_ card: String, action: String?) -> Bool { + guard card == Self.conductorNoteID else { return false } + if let action { + if action == Self.conductorNoteOpen { openSettings() } + } else { + closeConductorNote() + } + return true + } +} diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextImportCards/ImportSuggestionsCenter+Jobs.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextImportCards/ImportSuggestionsCenter+Jobs.swift index 36665354e4c9..5fe67ef9e431 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextImportCards/ImportSuggestionsCenter+Jobs.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextImportCards/ImportSuggestionsCenter+Jobs.swift @@ -4,7 +4,7 @@ import CmuxNextOnboarding import Foundation /// The jobs a card's button runs, each saved after every step: a browser's -/// profiles, iTerm2's theme and cmux Classic's import. +/// profiles, iTerm2's theme, and cmux Classic's and Conductor's imports. extension ImportSuggestionsCenter { /// Waits until card `id`'s import is no longer running. func settled(_ id: String) async { await running[id]?.value } @@ -100,11 +100,22 @@ extension ImportSuggestionsCenter { /// preview removes the job, so the card is as it was before the press. A quit mid-run leaves /// the job saved as running, which the next launch pauses for Resume. func runClassic(_ id: String) { - guard running[id] == nil, let host else { return } + guard let host else { return } + runWorkspaces(id, through: host.importClassic) + } + + /// Runs Import from Conductor as card `id`'s job, as ``runClassic(_:)`` runs Classic's. + func runConductor(_ id: String) { + guard let host else { return } + runWorkspaces(id, through: host.importConductor) + } + + private func runWorkspaces(_ id: String, through run: @escaping (@escaping @MainActor (Double) -> Void) async throws -> Int?) { + guard running[id] == nil else { return } running[id] = Task { [weak self] in let outcome: Result do { - outcome = try await .success(host.importClassic { [weak self] fraction in + outcome = try await .success(run { [weak self] fraction in guard let self else { return } ledger.update(id) { $0.progressed(fraction, now: self.now()) } }) @@ -120,7 +131,7 @@ extension ImportSuggestionsCenter { } saveLedger() running[id] = nil - // What classic has left to bring, now that this run is in. + // What is left to bring, now that this run is in. scan() } } diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextImportCards/ImportSuggestionsCenter.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextImportCards/ImportSuggestionsCenter.swift index fff9f8e152b2..5c7501201f5b 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextImportCards/ImportSuggestionsCenter.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextImportCards/ImportSuggestionsCenter.swift @@ -180,6 +180,7 @@ public final class ImportSuggestionsCenter { static let agentApps = [ "agents.claudeCode": "com.anthropic.claudefordesktop", "agents.codex": "com.openai.codex", "terminal.iterm2": "com.googlecode.iterm2", "terminal.ghostty": "com.mitchellh.ghostty", "cmux.classic": "com.cmuxterm.app", + "agents.conductor": "com.conductor.app", ] /// The cards Settings › Import lists now; empty while the cards are off. @@ -227,6 +228,9 @@ public final class ImportSuggestionsCenter { // Runs the import again; the migration ledger skips what already came over. put(job) runClassic(id) + case .conductor: + put(job) + runConductor(id) case .claudeCode, .codex, .ghostty: finish(job) } case .dismiss: @@ -355,6 +359,9 @@ public final class ImportSuggestionsCenter { // The action's own import, tracked: its preview asks before anything opens. put(ImportJob(finding: finding, now: now())) runClassic(id) + case .conductor: + put(ImportJob(finding: finding, now: now())) + runConductor(id) case .iterm2: run(id, writingTheme: ImportJob(finding: finding, now: now())) case .ghostty: @@ -373,12 +380,18 @@ public final class ImportSuggestionsCenter { if let url = URL(string: "x-apple.systempreferences:com.apple.preference.security?Privacy_AllFiles") { NSWorkspace.shared.openInBackground(url) } - case .cmuxClassic, nil: break + case .cmuxClassic, .conductor, nil: break } } /// Settings › Import. public func openSettings() { host?.openSettings("#/settings/import") } + + /// Closes the sidebar's Import from Conductor note for good (``conductorNote``). + func closeConductorNote() { + state.conductorNoteClosed = true + state.save(to: defaults) + } } /// A refused card button, for `import.card` and the pages. diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextImportSuggestions/ConductorImportStrings.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextImportSuggestions/ConductorImportStrings.swift new file mode 100644 index 000000000000..01a3c2c3f0f1 --- /dev/null +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextImportSuggestions/ConductorImportStrings.swift @@ -0,0 +1,84 @@ +import Foundation + +/// Import from Conductor's preview and toasts (table `ImportSuggestions`). Counts are +/// formatted for the person's locale before they go into the text. +public nonisolated struct ConductorImportStrings: Sendable { + public init() {} + + public static var previewTitle: String { + String(localized: "conductorImport.preview.title", defaultValue: "Import these Conductor workspaces?", table: "ImportSuggestions", bundle: .module) + } + public static var previewButton: String { + String(localized: "conductorImport.preview.button", defaultValue: "Import", table: "ImportSuggestions", bundle: .module) + } + public static func projects(_ count: Int) -> String { + format(String(localized: "conductorImport.preview.projects", defaultValue: "Projects: %@", table: "ImportSuggestions", bundle: .module), count) + } + public static func adopted(_ count: Int) -> String { + format(String(localized: "conductorImport.preview.adopted", defaultValue: "Workspaces opened in place, in their own folders: %@", + table: "ImportSuggestions", bundle: .module), count) + } + public static func chats(_ count: Int) -> String { + format(String(localized: "conductorImport.preview.chats", defaultValue: "Claude Code chats found in them: %@", + table: "ImportSuggestions", bundle: .module), count) + } + public static func recoverable(_ count: Int) -> String { + format(String(localized: "conductorImport.preview.recoverable", + defaultValue: "Archived workspaces left out (their commits are still in the repository): %@", + table: "ImportSuggestions", bundle: .module), count) + } + public static func cleanup(_ count: Int) -> String { + format(String(localized: "conductorImport.preview.cleanup", defaultValue: "Interrupted workspaces to replace (their terminals will end): %@", + table: "ImportSuggestions", bundle: .module), count) + } + /// `names`: the variables' names, never their values. + public static func environment(_ names: String) -> String { + String(format: String(localized: "conductorImport.preview.environment", + defaultValue: "Environment variables Conductor set, which you set yourself here: %@", + table: "ImportSuggestions", bundle: .module), names) + } + public static var archiveWarning: String { + String(localized: "conductorImport.preview.archiveWarning", + defaultValue: "Archiving one of these workspaces in Conductor deletes its folder, which cmux now uses. Archive it in cmux instead.", + table: "ImportSuggestions", bundle: .module) + } + public static var deadline: String { + String(localized: "conductorImport.preview.deadline", + defaultValue: "Conductor Cloud becomes read-only on November 13 and shuts down on November 20.", table: "ImportSuggestions", bundle: .module) + } + public static var opened: String { + String(localized: "conductorImport.toast.opened", defaultValue: "Your Conductor workspaces are open.", table: "ImportSuggestions", bundle: .module) + } + public static var allHere: String { + String(localized: "conductorImport.toast.allHere", defaultValue: "Every Conductor workspace is already open here.", table: "ImportSuggestions", bundle: .module) + } + public static var failed: String { + String(localized: "conductorImport.toast.failed", defaultValue: "Couldn’t import from Conductor.", table: "ImportSuggestions", bundle: .module) + } + /// The sidebar's one-time note (``ImportSuggestionState/showsConductorNote(findings:jobs:now:)``). + public static var noteTitle: String { + String(localized: "conductorImport.note.title", defaultValue: "Bring your Conductor workspaces", table: "ImportSuggestions", bundle: .module) + } + public static var noteDetail: String { + String(localized: "conductorImport.note.detail", + defaultValue: "Conductor Cloud shuts down on November 20. Settings › Import opens your workspaces here, in place.", + table: "ImportSuggestions", bundle: .module) + } + public static var noteOpen: String { + String(localized: "conductorImport.note.open", defaultValue: "Open Import", table: "ImportSuggestions", bundle: .module) + } + /// The note's x: it never shows again. + public static var noteClose: String { + String(localized: "conductorImport.note.close", defaultValue: "Don’t Show Again", table: "ImportSuggestions", bundle: .module) + } + /// The first-run history page's line (``ImportScanner/conductorOnboardingLine(now:)``). + public static var onboardingLine: String { + String(localized: "conductorImport.onboarding.line", + defaultValue: "Conductor workspaces: import them from Settings › Import before Conductor Cloud shuts down on November 20.", + table: "ImportSuggestions", bundle: .module) + } + + private static func format(_ template: String, _ count: Int) -> String { + String(format: template, count.formatted(.number)) + } +} diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextImportSuggestions/ConductorNote.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextImportSuggestions/ConductorNote.swift new file mode 100644 index 000000000000..a340abff4c4e --- /dev/null +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextImportSuggestions/ConductorNote.swift @@ -0,0 +1,33 @@ +import CmuxNextBrowserImport +public import Foundation + +/// Import from Conductor pointed out outside Settings › Import: the sidebar's one-time note and +/// the first-run history page's line, both only until Conductor Cloud shuts down. +extension ImportSuggestionState { + /// Conductor Cloud shuts down on November 20, 2026: neither shows from then on (midnight + /// Pacific time, 2026-11-20T08:00Z). + public static let conductorDeadline = Date(timeIntervalSince1970: 1_795_161_600) + + /// The sidebar note shows while this Mac has Conductor, before the deadline, until the person + /// closes it, starts the import (any job, running or done) or hides the Conductor card in + /// Settings › Import (its Done after an import does too). + public func showsConductorNote(findings: [ImportFinding], jobs: ImportJobLedger, now: Date) -> Bool { + guard now < Self.conductorDeadline, conductorNoteClosed != true, + let finding = findings.first(where: { $0.kind == .conductor }) else { return false } + return jobs.job(finding.id) == nil && !hides(finding) + } +} + +extension ImportScanner { + /// A scanner of `home` that finds no app (enough for Conductor, whose finding needs none). + public init(home: URL) { + self.init(home: home, environment: ImportEnvironment(homeDirectory: home, locateApp: { _ in nil })) + } + + /// The first-run history page's line when this home has Conductor's database or workspaces, + /// before the deadline; nil otherwise. Folder listings only; call it off the main thread. + public func conductorOnboardingLine(now: Date) -> String? { + guard now < ImportSuggestionState.conductorDeadline, conductor() != nil else { return nil } + return ConductorImportStrings.onboardingLine + } +} diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextImportSuggestions/ImportCardDeck.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextImportSuggestions/ImportCardDeck.swift index 9ab97a66f407..e8e99248c93e 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextImportSuggestions/ImportCardDeck.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextImportSuggestions/ImportCardDeck.swift @@ -62,6 +62,7 @@ public nonisolated struct ImportCardDeck: Sendable { case .codex: return ImportSuggestionStrings.threads(finding.count) case .cmuxClassic: return finding.offersNothingNew ? ImportSuggestionStrings.classicAllHere : ImportSuggestionStrings.workspaces(finding.count) + case .conductor: return ImportSuggestionStrings.workspaces(finding.count) case .iterm2: return ImportSuggestionStrings.profiles(finding.count) case .ghostty: return ImportSuggestionStrings.inUse } @@ -76,6 +77,7 @@ public nonisolated struct ImportCardDeck: Sendable { case .claudeCode: return ImportSuggestionStrings.claudeCode case .codex: return ImportSuggestionStrings.codex case .cmuxClassic: return finding.offersNothingNew ? ImportSuggestionStrings.classicAgain : ImportSuggestionStrings.classic + case .conductor: return ImportSuggestionStrings.conductor case .iterm2: return ImportSuggestionStrings.iterm2 case .ghostty: return ImportSuggestionStrings.ghostty } @@ -86,7 +88,7 @@ public nonisolated struct ImportCardDeck: Sendable { switch kind { case .browser: .init(.start, ImportSuggestionStrings.importButton) case .claudeCode, .codex: .init(.start, ImportSuggestionStrings.showChats) - case .cmuxClassic: .init(.start, ImportSuggestionStrings.importWorkspaces) + case .cmuxClassic, .conductor: .init(.start, ImportSuggestionStrings.importWorkspaces) case .iterm2: .init(.start, ImportSuggestionStrings.importColors) case .ghostty: .init(.open, ImportSuggestionStrings.openConfig) } @@ -155,6 +157,8 @@ public nonisolated struct ImportCardDeck: Sendable { case .iterm2: card.primary = .init(.open, ImportSuggestionStrings.openAppearance) // Settings › Import runs it again while Classic has a saved session; the ledger brings nothing twice. case .cmuxClassic: card.primary = finding != nil ? .init(.start, ImportSuggestionStrings.importAgain) : nil + // Again while Conductor's data is here; its ledger adopts nothing twice. + case .conductor: card.primary = finding != nil ? .init(.start, ImportSuggestionStrings.importAgain) : nil case .browser, .ghostty: card.primary = nil } card.secondary = [.init(.hide, ImportSuggestionStrings.done)] @@ -167,6 +171,7 @@ public nonisolated struct ImportCardDeck: Sendable { case .claudeCode, .codex: return ImportSuggestionStrings.doneChats case .iterm2: return ImportSuggestionStrings.doneTheme case .cmuxClassic: return job.opened.map(ImportSuggestionStrings.importedSessions) ?? ImportSuggestionStrings.doneClassic + case .conductor: return ImportSuggestionStrings.doneConductor case .ghostty: return ImportSuggestionStrings.doneNothing case .browser: var parts: [String] = [] @@ -188,6 +193,7 @@ extension ImportSourceKind { case .ghostty: "Ghostty" case .iterm2: "iTerm2" case .cmuxClassic: "cmux Classic" + case .conductor: "Conductor" } } } diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextImportSuggestions/ImportFinding.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextImportSuggestions/ImportFinding.swift index 1b7847c8da97..7d3d09779733 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextImportSuggestions/ImportFinding.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextImportSuggestions/ImportFinding.swift @@ -15,6 +15,8 @@ public nonisolated enum ImportSourceKind: String, Codable, Sendable, CaseIterabl case iterm2 /// cmux Classic's saved workspaces. case cmuxClassic + /// Conductor's workspaces (`~/conductor/workspaces`), adopted in place. + case conductor } /// What a scan found for one source, from folder listings and file @@ -43,7 +45,7 @@ public nonisolated struct ImportFinding: Codable, Sendable, Hashable, Identifiab } } - /// `browser.`, `agents.claudeCode`, `agents.codex`, `terminal.ghostty`, `terminal.iterm2`, `cmux.classic`. + /// `browser.`, `agents.claudeCode`, `agents.codex`, `terminal.ghostty`, `terminal.iterm2`, `cmux.classic`, `agents.conductor`. public var id: String public var kind: ImportSourceKind /// The browser, for `.browser`. @@ -52,7 +54,7 @@ public nonisolated struct ImportFinding: Codable, Sendable, Hashable, Identifiab public var name: String /// Browser profiles (only those not imported yet) or iTerm2 profiles. public var profiles: [Profile] - /// Sessions, threads or workspaces, for the agent and Classic sources. + /// Sessions, threads or workspaces, for the agent, Classic and Conductor sources. public var count: Int public var access: Access @@ -90,7 +92,7 @@ public nonisolated struct ImportFinding: Codable, Sendable, Hashable, Identifiab // keeps the card dismissed and only a new one brings it back (ImportSuggestionState.hides). let held = profiles.flatMap { profile in profile.kinds.map { "\(profile.directoryName)=\($0.rawValue)" } }.sorted() return ([id, access.rawValue] + held).joined(separator: "|") - case .claudeCode, .codex, .ghostty, .cmuxClassic: + case .claudeCode, .codex, .ghostty, .cmuxClassic, .conductor: return [id, access.rawValue].joined(separator: "|") } } diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextImportSuggestions/ImportJob.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextImportSuggestions/ImportJob.swift index 84c1bb8d239e..f4af3bccad52 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextImportSuggestions/ImportJob.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextImportSuggestions/ImportJob.swift @@ -59,7 +59,7 @@ public nonisolated struct ImportJob: Codable, Sendable, Equatable, Identifiable /// The current step's progress, 0...1. public var stepFraction: Double = 0 public var counts = ImportCounts() - /// The workspaces a cmux Classic import opened (nil for other sources, and until it is done). + /// The workspaces a cmux Classic or Conductor import opened (nil for other sources, and until it is done). public var opened: Int? public var startedAt: Date public var updatedAt: Date @@ -112,7 +112,7 @@ public nonisolated struct ImportJob: Codable, Sendable, Equatable, Identifiable if nextStep == nil { phase = .done; pause = nil } } - /// A cmux Classic import ended, having opened `workspaces` (0 when everything was here already). + /// A cmux Classic or Conductor import ended, having opened `workspaces` (0 when everything was here already). public mutating func importedClassic(_ workspaces: Int, now: Date) { opened = workspaces finishedStep(0, counts: ImportCounts(), now: now) diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextImportSuggestions/ImportScanner.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextImportSuggestions/ImportScanner.swift index 0a4313c573fc..0aea68ed5446 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextImportSuggestions/ImportScanner.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextImportSuggestions/ImportScanner.swift @@ -45,7 +45,7 @@ public nonisolated struct ImportScanner: Sendable { /// Every source found, browsers first in the order people most often come from. public func scan() -> [ImportFinding] { var findings = browsers() - let others: [() -> ImportFinding?] = [claudeCode, codex, classic, iterm2, ghostty] + let others: [() -> ImportFinding?] = [claudeCode, codex, classic, conductor, iterm2, ghostty] for source in others { if shouldStop() { break } if let found = source() { findings.append(found) } @@ -193,6 +193,19 @@ public nonisolated struct ImportScanner: Sendable { return ImportFinding(id: "cmux.classic", kind: .cmuxClassic, name: "cmux Classic", count: workspaces) } + /// Conductor's workspaces, one folder each under `~/conductor/workspaces/`, when its + /// database or that folder is here. Folder listings only: what each one becomes is the + /// import's dry run (`conductor_import.plan`), read when the person presses Import. + func conductor() -> ImportFinding? { + let database = home.appending(path: "Library/Application Support/com.conductor.app/conductor.db") + let root = home.appending(path: "conductor/workspaces", directoryHint: .isDirectory) + let workspaces = listing(root).reduce(0) { total, repo in + total + listing(root.appending(path: repo, directoryHint: .isDirectory)).count + } + guard workspaces > 0 || size(database) > 0 else { return nil } + return ImportFinding(id: "agents.conductor", kind: .conductor, name: "Conductor", count: workspaces) + } + /// Ghostty's config: cmux terminals read it as it is, so it is listed as already in use. func ghostty() -> ImportFinding? { let config = folder("XDG_CONFIG_HOME", default: ".config").appending(path: "ghostty", directoryHint: .isDirectory) diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextImportSuggestions/ImportSuggestionState.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextImportSuggestions/ImportSuggestionState.swift index ae6b65c67e70..0de2324cdd6e 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextImportSuggestions/ImportSuggestionState.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextImportSuggestions/ImportSuggestionState.swift @@ -84,6 +84,9 @@ public nonisolated struct ImportSuggestionState: Codable, Sendable, Equatable { /// Card id to the fingerprint it had when dismissed. public var dismissed: [String: String] = [:] public var requests: [ImportRequest] = [] + /// The sidebar's one-time Import from Conductor note was closed (``ConductorNote``): it never + /// shows again. Optional, so the state saved before it still decodes. + public var conductorNoteClosed: Bool? /// The most open requests kept; an older one goes when a new one comes. public static let maximumRequests = 6 diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextImportSuggestions/ImportSuggestionStrings.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextImportSuggestions/ImportSuggestionStrings.swift index 3490a6a700c9..340e17635157 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextImportSuggestions/ImportSuggestionStrings.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextImportSuggestions/ImportSuggestionStrings.swift @@ -68,6 +68,11 @@ public nonisolated struct ImportSuggestionStrings: Sendable { String(localized: "importSuggestions.benefit.classic", defaultValue: "Your Classic workspaces open here, with their tabs, folders and agent chats.", table: "ImportSuggestions", bundle: .module) } + static var conductor: String { + String(localized: "importSuggestions.benefit.conductor", + defaultValue: "Your Conductor workspaces open here in place, on their branches, with their chats. Conductor Cloud shuts down on November 20.", + table: "ImportSuggestions", bundle: .module) + } static var classicAgain: String { String(localized: "importSuggestions.benefit.classicAgain", defaultValue: "Import again to reopen Classic workspaces you closed here. Ones still open aren't duplicated.", @@ -131,6 +136,7 @@ public nonisolated struct ImportSuggestionStrings: Sendable { static var doneTheme: String { String(localized: "importSuggestions.status.doneTheme", defaultValue: "Your colors are a theme now.", table: "ImportSuggestions", bundle: .module) } static var doneClassic: String { String(localized: "importSuggestions.status.doneClassic", defaultValue: "Your Classic workspaces are open.", table: "ImportSuggestions", bundle: .module) } + static var doneConductor: String { String(localized: "importSuggestions.status.doneConductor", defaultValue: "Your Conductor workspaces are open.", table: "ImportSuggestions", bundle: .module) } /// A cmux Classic import's end: the workspaces this run opened. static func importedSessions(_ count: Int) -> String { String(localized: "importSuggestions.status.importedSessions", defaultValue: "Imported \(count) sessions", table: "ImportSuggestions", bundle: .module) diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextImportSuggestions/Resources/ImportSuggestions.xcstrings b/Packages/macOS/CmuxNext/Sources/CmuxNextImportSuggestions/Resources/ImportSuggestions.xcstrings index a0100a9e84f2..fda2a85c3681 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextImportSuggestions/Resources/ImportSuggestions.xcstrings +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextImportSuggestions/Resources/ImportSuggestions.xcstrings @@ -10858,6 +10858,2646 @@ } } } + }, + "conductorImport.preview.title" : { + "comment" : "The title of the dialog that previews Import from Conductor before anything opens.", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Import these Conductor workspaces?" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Diese Conductor-Arbeitsbereiche importieren?" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Importer ces espaces de travail Conductor ?" + } + }, + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "هل تريد استيراد مساحات عمل Conductor هذه؟" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "¿Importar estos espacios de trabajo de Conductor?" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "要匯入這些 Conductor 工作區嗎?" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "要导入这些 Conductor 工作区吗?" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "이 Conductor 작업 공간을 가져올까요?" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "これらの Conductor ワークスペースを読み込みますか?" + } + }, + "bs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Uvesti ove Conductor radne prostore?" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vil du importere disse Conductor-arbejdsområder?" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Importare questi spazi di lavoro di Conductor?" + } + }, + "km" : { + "stringUnit" : { + "state" : "translated", + "value" : "នាំចូលកន្លែងធ្វើការ Conductor ទាំងនេះទេ?" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vil du importere disse Conductor-arbeidsområdene?" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zaimportować te obszary robocze Conductor?" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Importar estes espaços de trabalho do Conductor?" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Импортировать эти рабочие пространства Conductor?" + } + }, + "th" : { + "stringUnit" : { + "state" : "translated", + "value" : "นำเข้าพื้นที่ทำงาน Conductor เหล่านี้ไหม" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bu Conductor çalışma alanları içe aktarılsın mı?" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Імпортувати ці робочі простори Conductor?" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nhập các không gian làm việc Conductor này?" + } + } + } + }, + "conductorImport.preview.projects" : { + "comment" : "A line of the Import from Conductor preview: how many of Conductor's repositories become cmux projects.", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Projects: %@" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Projekte: %@" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Projets : %@" + } + }, + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "المشاريع: %@" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Proyectos: %@" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "專案:%@" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "项目:%@" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "프로젝트: %@" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "プロジェクト: %@" + } + }, + "bs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Projekti: %@" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Projekter: %@" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Progetti: %@" + } + }, + "km" : { + "stringUnit" : { + "state" : "translated", + "value" : "គម្រោង៖ %@" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Prosjekter: %@" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Projekty: %@" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Projetos: %@" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Проекты: %@" + } + }, + "th" : { + "stringUnit" : { + "state" : "translated", + "value" : "โปรเจกต์: %@" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Projeler: %@" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Проєкти: %@" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dự án: %@" + } + } + } + }, + "conductorImport.preview.adopted" : { + "comment" : "A line of the Import from Conductor preview: how many live Conductor workspaces open as cmux workspaces at their existing folders (nothing is copied).", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Workspaces opened in place, in their own folders: %@" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Arbeitsbereiche, die an Ort und Stelle in ihren eigenen Ordnern geöffnet werden: %@" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Espaces de travail ouverts sur place, dans leurs propres dossiers : %@" + } + }, + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "مساحات العمل التي تُفتح في مكانها، في مجلداتها الخاصة: %@" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Espacios de trabajo abiertos en su lugar, en sus propias carpetas: %@" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "就地在其原有資料夾中開啟的工作區:%@" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "就地在其原有文件夹中打开的工作区:%@" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "원래 폴더에서 그대로 열리는 작업 공간: %@" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "元のフォルダでそのまま開くワークスペース: %@" + } + }, + "bs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Radni prostori otvoreni na mjestu, u vlastitim folderima: %@" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Arbejdsområder, der åbnes på stedet i deres egne mapper: %@" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Spazi di lavoro aperti sul posto, nelle loro cartelle: %@" + } + }, + "km" : { + "stringUnit" : { + "state" : "translated", + "value" : "កន្លែងធ្វើការដែលបើកនៅនឹងកន្លែង ក្នុងថតផ្ទាល់ខ្លួន៖ %@" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Arbeidsområder som åpnes der de er, i sine egne mapper: %@" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Obszary robocze otwierane na miejscu, we własnych folderach: %@" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Espaços de trabalho abertos no lugar, nas suas próprias pastas: %@" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Рабочие пространства, открываемые на месте, в своих папках: %@" + } + }, + "th" : { + "stringUnit" : { + "state" : "translated", + "value" : "พื้นที่ทำงานที่เปิดในที่เดิม ในโฟลเดอร์ของตัวเอง: %@" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kendi klasörlerinde yerinde açılan çalışma alanları: %@" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Робочі простори, що відкриваються на місці, у власних теках: %@" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Không gian làm việc mở tại chỗ, trong thư mục riêng: %@" + } + } + } + }, + "conductorImport.preview.chats" : { + "comment" : "A line of the Import from Conductor preview: how many Claude Code chats are already in those workspaces' folders (linked, not copied).", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Claude Code chats found in them: %@" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Darin gefundene Claude Code-Chats: %@" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Discussions Claude Code trouvées : %@" + } + }, + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "محادثات Claude Code الموجودة فيها: %@" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Chats de Claude Code encontrados en ellos: %@" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "其中找到的 Claude Code 對話:%@" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "其中找到的 Claude Code 对话:%@" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "그 안에서 찾은 Claude Code 채팅: %@" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "見つかった Claude Code のチャット: %@" + } + }, + "bs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pronađeni Claude Code razgovori: %@" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Fundne Claude Code-chats: %@" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Chat di Claude Code trovate: %@" + } + }, + "km" : { + "stringUnit" : { + "state" : "translated", + "value" : "ការជជែក Claude Code ដែលបានរកឃើញ៖ %@" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Claude Code-chatter funnet i dem: %@" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Znalezione czaty Claude Code: %@" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Conversas do Claude Code encontradas neles: %@" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Найдено чатов Claude Code: %@" + } + }, + "th" : { + "stringUnit" : { + "state" : "translated", + "value" : "แชต Claude Code ที่พบในนั้น: %@" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "İçlerinde bulunan Claude Code sohbetleri: %@" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Знайдено чатів Claude Code: %@" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cuộc trò chuyện Claude Code tìm thấy: %@" + } + } + } + }, + "conductorImport.preview.recoverable" : { + "comment" : "A line of the Import from Conductor preview: how many archived Conductor workspaces are not imported; their work can still be recovered from git.", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Archived workspaces left out (their commits are still in the repository): %@" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ausgelassene archivierte Arbeitsbereiche (ihre Commits sind noch im Repository): %@" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Espaces de travail archivés laissés de côté (leurs commits sont toujours dans le dépôt) : %@" + } + }, + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "مساحات العمل المؤرشفة المستبعدة (لا تزال إيداعاتها في المستودع): %@" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Espacios de trabajo archivados que se omiten (sus commits siguen en el repositorio): %@" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "略過的已封存工作區(其提交仍在儲存庫中):%@" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "略过的已归档工作区(其提交仍在仓库中):%@" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "제외된 보관된 작업 공간(커밋은 아직 저장소에 있음): %@" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "除外したアーカイブ済みワークスペース(コミットはリポジトリに残っています): %@" + } + }, + "bs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Izostavljeni arhivirani radni prostori (njihovi commitovi su još u repozitoriju): %@" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Arkiverede arbejdsområder, der udelades (deres commits er stadig i repositoriet): %@" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Spazi di lavoro archiviati esclusi (i loro commit sono ancora nel repository): %@" + } + }, + "km" : { + "stringUnit" : { + "state" : "translated", + "value" : "កន្លែងធ្វើការដែលបានទុកក្នុងប័ណ្ណសារ មិនត្រូវបាននាំចូល (commit របស់វានៅក្នុងឃ្លាំងនៅឡើយ)៖ %@" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Arkiverte arbeidsområder som utelates (commitene er fortsatt i repositoriet): %@" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pominięte zarchiwizowane obszary robocze (ich commity nadal są w repozytorium): %@" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Espaços de trabalho arquivados deixados de fora (os commits ainda estão no repositório): %@" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Пропущенные архивные рабочие пространства (их коммиты остаются в репозитории): %@" + } + }, + "th" : { + "stringUnit" : { + "state" : "translated", + "value" : "พื้นที่ทำงานที่เก็บถาวรซึ่งไม่ได้นำเข้า (คอมมิตยังอยู่ในรีโพสิทอรี): %@" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dışarıda bırakılan arşivlenmiş çalışma alanları (commit'leri hâlâ depoda): %@" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Пропущені архівні робочі простори (їхні коміти досі в репозиторії): %@" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Không gian làm việc đã lưu trữ bị bỏ qua (commit vẫn còn trong kho): %@" + } + } + } + }, + "conductorImport.preview.environment" : { + "comment" : "A line of the Import from Conductor preview: the names (never the values) of environment variables Conductor set, which the person sets in cmux themselves.", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Environment variables Conductor set, which you set yourself here: %@" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Von Conductor gesetzte Umgebungsvariablen, die du hier selbst setzt: %@" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Variables d’environnement définies par Conductor, à définir vous-même ici : %@" + } + }, + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "متغيرات البيئة التي ضبطها Conductor، والتي تضبطها بنفسك هنا: %@" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Variables de entorno que definía Conductor y que debes definir aquí tú mismo: %@" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "Conductor 設定的環境變數,需在此自行設定:%@" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "Conductor 设置的环境变量,需在此自行设置:%@" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "Conductor가 설정한 환경 변수(여기서는 직접 설정): %@" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "Conductor が設定していた環境変数(ここではご自身で設定してください): %@" + } + }, + "bs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Varijable okruženja koje je postavio Conductor, a ovdje ih postavljate sami: %@" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Miljøvariabler, som Conductor satte, og som du selv skal sætte her: %@" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Variabili d’ambiente impostate da Conductor, da impostare qui manualmente: %@" + } + }, + "km" : { + "stringUnit" : { + "state" : "translated", + "value" : "អថេរបរិស្ថានដែល Conductor បានកំណត់ ដែលអ្នកត្រូវកំណត់ដោយខ្លួនឯងនៅទីនេះ៖ %@" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Miljøvariabler Conductor satte, som du setter selv her: %@" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zmienne środowiskowe ustawiane przez Conductor, które ustawisz tu samodzielnie: %@" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Variáveis de ambiente que o Conductor definia e que você define aqui: %@" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Переменные окружения, которые задавал Conductor; здесь их нужно задать самостоятельно: %@" + } + }, + "th" : { + "stringUnit" : { + "state" : "translated", + "value" : "ตัวแปรสภาพแวดล้อมที่ Conductor ตั้งไว้ ซึ่งคุณต้องตั้งเองที่นี่: %@" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Conductor'ın ayarladığı, burada kendiniz ayarlayacağınız ortam değişkenleri: %@" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Змінні середовища, які задавав Conductor; тут їх потрібно задати самостійно: %@" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Biến môi trường mà Conductor đã đặt, bạn tự đặt ở đây: %@" + } + } + } + }, + "conductorImport.preview.archiveWarning" : { + "comment" : "A warning in the Import from Conductor preview: cmux opens the workspaces in their existing folders, and Conductor's Archive deletes the folder.", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Archiving one of these workspaces in Conductor deletes its folder, which cmux now uses. Archive it in cmux instead." + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wenn du einen dieser Arbeitsbereiche in Conductor archivierst, wird sein Ordner gelöscht, den cmux jetzt verwendet. Archiviere ihn stattdessen in cmux." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Archiver l’un de ces espaces de travail dans Conductor supprime son dossier, que cmux utilise désormais. Archivez-le plutôt dans cmux." + } + }, + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "أرشفة إحدى مساحات العمل هذه في Conductor تحذف مجلدها الذي يستخدمه cmux الآن. أرشفها في cmux بدلاً من ذلك." + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Archivar uno de estos espacios de trabajo en Conductor elimina su carpeta, que ahora usa cmux. Archívalo en cmux." + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "在 Conductor 中封存其中任一工作區會刪除其資料夾,而 cmux 現在正在使用該資料夾。請改在 cmux 中封存。" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "在 Conductor 中归档其中任一工作区会删除其文件夹,而 cmux 现在正在使用该文件夹。请改在 cmux 中归档。" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "Conductor에서 이 작업 공간을 보관하면 cmux가 지금 사용하는 폴더가 삭제됩니다. 대신 cmux에서 보관하세요." + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "Conductor でこれらのワークスペースをアーカイブすると、cmux が使用中のフォルダが削除されます。代わりに cmux でアーカイブしてください。" + } + }, + "bs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Arhiviranje nekog od ovih radnih prostora u Conductoru briše njegov folder, koji sada koristi cmux. Umjesto toga ga arhivirajte u cmuxu." + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hvis du arkiverer et af disse arbejdsområder i Conductor, slettes mappen, som cmux nu bruger. Arkivér det i cmux i stedet." + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Archiviare uno di questi spazi di lavoro in Conductor elimina la sua cartella, che ora usa cmux. Archivialo invece in cmux." + } + }, + "km" : { + "stringUnit" : { + "state" : "translated", + "value" : "ការដាក់កន្លែងធ្វើការណាមួយក្នុងប័ណ្ណសារនៅក្នុង Conductor នឹងលុបថតរបស់វា ដែល cmux កំពុងប្រើ។ សូមដាក់ក្នុងប័ណ្ណសារនៅក្នុង cmux ជំនួសវិញ។" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hvis du arkiverer et av disse arbeidsområdene i Conductor, slettes mappen som cmux nå bruker. Arkiver det i cmux i stedet." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zarchiwizowanie jednego z tych obszarów roboczych w Conductor usuwa jego folder, z którego teraz korzysta cmux. Zarchiwizuj go w cmux." + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Arquivar um destes espaços de trabalho no Conductor apaga a pasta dele, que o cmux agora usa. Arquive-o no cmux." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Архивация такого рабочего пространства в Conductor удаляет его папку, которую теперь использует cmux. Архивируйте его в cmux." + } + }, + "th" : { + "stringUnit" : { + "state" : "translated", + "value" : "การเก็บถาวรพื้นที่ทำงานเหล่านี้ใน Conductor จะลบโฟลเดอร์ที่ cmux ใช้อยู่ตอนนี้ ให้เก็บถาวรใน cmux แทน" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bu çalışma alanlarından birini Conductor'da arşivlemek, cmux'un artık kullandığı klasörünü siler. Bunun yerine cmux'ta arşivleyin." + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Архівування такого робочого простору в Conductor видаляє його теку, яку тепер використовує cmux. Архівуйте його в cmux." + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Lưu trữ một không gian làm việc này trong Conductor sẽ xóa thư mục mà cmux đang dùng. Hãy lưu trữ trong cmux." + } + } + } + }, + "conductorImport.preview.deadline" : { + "comment" : "The last line of the Import from Conductor preview: Conductor Cloud's announced dates.", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Conductor Cloud becomes read-only on November 13 and shuts down on November 20." + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Conductor Cloud ist ab dem 13. November schreibgeschützt und wird am 20. November eingestellt." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Conductor Cloud passe en lecture seule le 13 novembre et ferme le 20 novembre." + } + }, + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "يصبح Conductor Cloud للقراءة فقط في 13 نوفمبر ويتوقف في 20 نوفمبر." + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Conductor Cloud pasa a ser de solo lectura el 13 de noviembre y cierra el 20 de noviembre." + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "Conductor Cloud 將於 11 月 13 日變為唯讀,並於 11 月 20 日關閉。" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "Conductor Cloud 将于 11 月 13 日变为只读,并于 11 月 20 日关闭。" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "Conductor Cloud는 11월 13일에 읽기 전용이 되고 11월 20일에 종료됩니다." + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "Conductor Cloud は 11 月 13 日に読み取り専用になり、11 月 20 日に終了します。" + } + }, + "bs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Conductor Cloud postaje samo za čitanje 13. novembra i gasi se 20. novembra." + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Conductor Cloud bliver skrivebeskyttet den 13. november og lukker den 20. november." + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Conductor Cloud diventa di sola lettura il 13 novembre e chiude il 20 novembre." + } + }, + "km" : { + "stringUnit" : { + "state" : "translated", + "value" : "Conductor Cloud នឹងក្លាយជាអានតែប៉ុណ្ណោះនៅថ្ងៃទី 13 ខែវិច្ឆិកា ហើយបិទនៅថ្ងៃទី 20 ខែវិច្ឆិកា។" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Conductor Cloud blir skrivebeskyttet 13. november og stenges 20. november." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Conductor Cloud przejdzie w tryb tylko do odczytu 13 listopada i zostanie wyłączony 20 listopada." + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "O Conductor Cloud passa a ser somente leitura em 13 de novembro e será encerrado em 20 de novembro." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Conductor Cloud станет доступен только для чтения 13 ноября и закроется 20 ноября." + } + }, + "th" : { + "stringUnit" : { + "state" : "translated", + "value" : "Conductor Cloud จะเป็นแบบอ่านอย่างเดียวในวันที่ 13 พฤศจิกายน และปิดให้บริการในวันที่ 20 พฤศจิกายน" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Conductor Cloud 13 Kasım'da salt okunur olur ve 20 Kasım'da kapanır." + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Conductor Cloud стане доступним лише для читання 13 листопада й закриється 20 листопада." + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Conductor Cloud chuyển sang chỉ đọc vào ngày 13 tháng 11 và ngừng hoạt động vào ngày 20 tháng 11." + } + } + } + }, + "conductorImport.toast.opened" : { + "comment" : "A toast after Import from Conductor opened the workspaces.", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Your Conductor workspaces are open." + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Deine Conductor-Arbeitsbereiche sind geöffnet." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vos espaces de travail Conductor sont ouverts." + } + }, + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "مساحات عمل Conductor مفتوحة." + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tus espacios de trabajo de Conductor están abiertos." + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "你的 Conductor 工作區已開啟。" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "你的 Conductor 工作区已打开。" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "Conductor 작업 공간이 열렸습니다." + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "Conductor のワークスペースを開きました。" + } + }, + "bs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vaši Conductor radni prostori su otvoreni." + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dine Conductor-arbejdsområder er åbne." + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "I tuoi spazi di lavoro di Conductor sono aperti." + } + }, + "km" : { + "stringUnit" : { + "state" : "translated", + "value" : "កន្លែងធ្វើការ Conductor របស់អ្នកត្រូវបានបើក។" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Conductor-arbeidsområdene dine er åpne." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Twoje obszary robocze Conductor są otwarte." + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Seus espaços de trabalho do Conductor estão abertos." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ваши рабочие пространства Conductor открыты." + } + }, + "th" : { + "stringUnit" : { + "state" : "translated", + "value" : "พื้นที่ทำงาน Conductor ของคุณเปิดแล้ว" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Conductor çalışma alanlarınız açık." + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ваші робочі простори Conductor відкрито." + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Không gian làm việc Conductor của bạn đã mở." + } + } + } + }, + "conductorImport.toast.allHere" : { + "comment" : "A toast when Import from Conductor found nothing new to open.", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Every Conductor workspace is already open here." + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Alle Conductor-Arbeitsbereiche sind hier bereits geöffnet." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tous les espaces de travail Conductor sont déjà ouverts ici." + } + }, + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "كل مساحات عمل Conductor مفتوحة هنا بالفعل." + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Todos los espacios de trabajo de Conductor ya están abiertos aquí." + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "所有 Conductor 工作區都已在此開啟。" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "所有 Conductor 工作区都已在此打开。" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "모든 Conductor 작업 공간이 이미 여기에 열려 있습니다." + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "すべての Conductor ワークスペースはすでにここで開いています。" + } + }, + "bs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Svi Conductor radni prostori su već otvoreni ovdje." + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Alle Conductor-arbejdsområder er allerede åbne her." + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tutti gli spazi di lavoro di Conductor sono già aperti qui." + } + }, + "km" : { + "stringUnit" : { + "state" : "translated", + "value" : "កន្លែងធ្វើការ Conductor ទាំងអស់បានបើកនៅទីនេះរួចហើយ។" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Alle Conductor-arbeidsområder er allerede åpne her." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wszystkie obszary robocze Conductor są już tu otwarte." + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Todos os espaços de trabalho do Conductor já estão abertos aqui." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Все рабочие пространства Conductor уже открыты здесь." + } + }, + "th" : { + "stringUnit" : { + "state" : "translated", + "value" : "พื้นที่ทำงาน Conductor ทั้งหมดเปิดอยู่ที่นี่แล้ว" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tüm Conductor çalışma alanları zaten burada açık." + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Усі робочі простори Conductor уже відкриті тут." + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mọi không gian làm việc Conductor đều đã mở ở đây." + } + } + } + }, + "conductorImport.toast.failed" : { + "comment" : "A toast when Import from Conductor failed.", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Couldn’t import from Conductor." + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Import aus Conductor fehlgeschlagen." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Impossible d’importer depuis Conductor." + } + }, + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "تعذّر الاستيراد من Conductor." + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "No se pudo importar desde Conductor." + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "無法從 Conductor 匯入。" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "无法从 Conductor 导入。" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "Conductor에서 가져오지 못했습니다." + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "Conductor から読み込めませんでした。" + } + }, + "bs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Uvoz iz Conductora nije uspio." + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kunne ikke importere fra Conductor." + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Impossibile importare da Conductor." + } + }, + "km" : { + "stringUnit" : { + "state" : "translated", + "value" : "មិនអាចនាំចូលពី Conductor បានទេ។" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kunne ikke importere fra Conductor." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nie udało się zaimportować z Conductor." + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Não foi possível importar do Conductor." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Не удалось импортировать из Conductor." + } + }, + "th" : { + "stringUnit" : { + "state" : "translated", + "value" : "นำเข้าจาก Conductor ไม่สำเร็จ" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Conductor'dan içe aktarılamadı." + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Не вдалося імпортувати з Conductor." + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Không thể nhập từ Conductor." + } + } + } + }, + "importSuggestions.benefit.conductor" : { + "comment" : "What the Conductor card in Settings › Import brings over.", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Your Conductor workspaces open here in place, on their branches, with their chats. Conductor Cloud shuts down on November 20." + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Deine Conductor-Arbeitsbereiche öffnen sich hier an Ort und Stelle, auf ihren Branches, mit ihren Chats. Conductor Cloud wird am 20. November eingestellt." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vos espaces de travail Conductor s’ouvrent ici sur place, sur leurs branches, avec leurs discussions. Conductor Cloud ferme le 20 novembre." + } + }, + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "تُفتح مساحات عمل Conductor هنا في مكانها، على فروعها، مع محادثاتها. يتوقف Conductor Cloud في 20 نوفمبر." + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tus espacios de trabajo de Conductor se abren aquí en su lugar, en sus ramas, con sus chats. Conductor Cloud cierra el 20 de noviembre." + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "你的 Conductor 工作區會就地在此開啟,保留其分支與對話。Conductor Cloud 將於 11 月 20 日關閉。" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "你的 Conductor 工作区会就地在此打开,保留其分支和对话。Conductor Cloud 将于 11 月 20 日关闭。" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "Conductor 작업 공간이 브랜치와 채팅 그대로 여기에서 열립니다. Conductor Cloud는 11월 20일에 종료됩니다." + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "Conductor のワークスペースが、ブランチとチャットはそのままにここで開きます。Conductor Cloud は 11 月 20 日に終了します。" + } + }, + "bs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vaši Conductor radni prostori otvaraju se ovdje na mjestu, na svojim granama, sa svojim razgovorima. Conductor Cloud se gasi 20. novembra." + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dine Conductor-arbejdsområder åbnes her på stedet, på deres branches, med deres chats. Conductor Cloud lukker den 20. november." + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "I tuoi spazi di lavoro di Conductor si aprono qui sul posto, sui loro branch, con le loro chat. Conductor Cloud chiude il 20 novembre." + } + }, + "km" : { + "stringUnit" : { + "state" : "translated", + "value" : "កន្លែងធ្វើការ Conductor របស់អ្នកបើកនៅទីនេះនៅនឹងកន្លែង លើ branch របស់វា ជាមួយការជជែករបស់វា។ Conductor Cloud បិទនៅថ្ងៃទី 20 ខែវិច្ឆិកា។" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Conductor-arbeidsområdene dine åpnes her der de er, på sine grener, med chattene sine. Conductor Cloud stenges 20. november." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Twoje obszary robocze Conductor otwierają się tu na miejscu, na swoich gałęziach, z czatami. Conductor Cloud zostanie wyłączony 20 listopada." + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Seus espaços de trabalho do Conductor abrem aqui no lugar, nos branches deles, com as conversas. O Conductor Cloud será encerrado em 20 de novembro." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ваши рабочие пространства Conductor открываются здесь на месте, на своих ветках, со своими чатами. Conductor Cloud закроется 20 ноября." + } + }, + "th" : { + "stringUnit" : { + "state" : "translated", + "value" : "พื้นที่ทำงาน Conductor ของคุณจะเปิดที่นี่ในที่เดิม บนสาขาของมัน พร้อมแชต Conductor Cloud ปิดให้บริการวันที่ 20 พฤศจิกายน" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Conductor çalışma alanlarınız burada yerinde, dallarında ve sohbetleriyle açılır. Conductor Cloud 20 Kasım'da kapanıyor." + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ваші робочі простори Conductor відкриваються тут на місці, на своїх гілках, зі своїми чатами. Conductor Cloud закриється 20 листопада." + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Không gian làm việc Conductor của bạn mở tại chỗ ở đây, trên nhánh của chúng, cùng các cuộc trò chuyện. Conductor Cloud ngừng hoạt động vào ngày 20 tháng 11." + } + } + } + }, + "importSuggestions.status.doneConductor" : { + "comment" : "A Conductor card's status after its import ended.", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Your Conductor workspaces are open." + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Deine Conductor-Arbeitsbereiche sind geöffnet." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vos espaces de travail Conductor sont ouverts." + } + }, + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "مساحات عمل Conductor مفتوحة." + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tus espacios de trabajo de Conductor están abiertos." + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "你的 Conductor 工作區已開啟。" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "你的 Conductor 工作区已打开。" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "Conductor 작업 공간이 열렸습니다." + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "Conductor のワークスペースを開きました。" + } + }, + "bs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vaši Conductor radni prostori su otvoreni." + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dine Conductor-arbejdsområder er åbne." + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "I tuoi spazi di lavoro di Conductor sono aperti." + } + }, + "km" : { + "stringUnit" : { + "state" : "translated", + "value" : "កន្លែងធ្វើការ Conductor របស់អ្នកត្រូវបានបើក។" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Conductor-arbeidsområdene dine er åpne." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Twoje obszary robocze Conductor są otwarte." + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Seus espaços de trabalho do Conductor estão abertos." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ваши рабочие пространства Conductor открыты." + } + }, + "th" : { + "stringUnit" : { + "state" : "translated", + "value" : "พื้นที่ทำงาน Conductor ของคุณเปิดแล้ว" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Conductor çalışma alanlarınız açık." + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ваші робочі простори Conductor відкрито." + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Không gian làm việc Conductor của bạn đã mở." + } + } + } + }, + "conductorImport.preview.button" : { + "comment" : "The confirm button of the Import from Conductor preview.", + "extractionState" : "manual", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "استيراد" + } + }, + "bs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Uvezi" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Importér" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Importieren" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Import" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Importar" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Importer" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Importa" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "インポート" + } + }, + "km" : { + "stringUnit" : { + "state" : "translated", + "value" : "នាំចូល" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "가져오기" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Importer" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Importuj" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Importar" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Импортировать" + } + }, + "th" : { + "stringUnit" : { + "state" : "translated", + "value" : "นำเข้า" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "İçe Aktar" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Імпортувати" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nhập" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "导入" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "匯入" + } + } + } + }, + "conductorImport.preview.cleanup" : { + "comment" : "A line of the Import from Conductor preview: workspaces a cut-off earlier import half made, which are closed and made again.", + "extractionState" : "manual", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "مساحات العمل المتوقفة التي سيتم استبدالها (سيتم إنهاء محطاتها الطرفية): %1$@" + } + }, + "bs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Prekinuti radni prostori za zamjenu (njihovi terminali će se završiti): %1$@" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Afbrudte arbejdsområder, der erstattes (deres terminaler afsluttes): %1$@" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zu ersetzende unterbrochene Arbeitsbereiche (ihre Terminals werden beendet): %1$@" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Interrupted workspaces to replace (their terminals will end): %1$@" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Espacios de trabajo interrumpidos que se reemplazarán (sus terminales finalizarán): %1$@" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Espaces de travail interrompus à remplacer (leurs terminaux seront arrêtés) : %1$@" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Spazi di lavoro interrotti da sostituire (i relativi terminali verranno terminati): %1$@" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "置き換える中断済みワークスペース(そのターミナルは終了します):%1$@" + } + }, + "km" : { + "stringUnit" : { + "state" : "translated", + "value" : "កន្លែងធ្វើការដែលបានផ្អាក និងត្រូវជំនួស (ស្ថានីយរបស់វានឹងបញ្ចប់)៖ %1$@" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "교체할 중단된 작업 공간(해당 터미널이 종료됨): %1$@" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Avbrutte arbeidsområder som erstattes (terminalene deres avsluttes): %1$@" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Przerwane obszary robocze do zastąpienia (ich terminale zostaną zakończone): %1$@" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Espaços de trabalho interrompidos a substituir (os terminais serão encerrados): %1$@" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Прерванные рабочие пространства для замены (их терминалы будут завершены): %1$@" + } + }, + "th" : { + "stringUnit" : { + "state" : "translated", + "value" : "พื้นที่ทำงานที่หยุดชะงักซึ่งจะแทนที่ (เทอร์มินัลของพื้นที่เหล่านี้จะสิ้นสุด): %1$@" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Değiştirilecek kesintiye uğramış çalışma alanları (terminalleri sonlandırılacak): %1$@" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Перервані робочі простори для заміни (їхні термінали буде завершено): %1$@" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Không gian làm việc bị gián đoạn sẽ được thay thế (các terminal của chúng sẽ kết thúc): %1$@" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "要替换的中断工作区(其终端将结束):%1$@" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "要取代的中斷工作區(其終端將結束):%1$@" + } + } + } + }, + "conductorImport.note.title" : { + "comment" : "The title of the sidebar's one-time note that points to Import from Conductor.", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bring your Conductor workspaces" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bring deine Conductor-Arbeitsbereiche mit" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Apportez vos espaces de travail Conductor" + } + }, + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "أحضر مساحات عمل Conductor" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Trae tus espacios de trabajo de Conductor" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "帶來你的 Conductor 工作區" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "带来你的 Conductor 工作区" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "Conductor 작업 공간 가져오기" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "Conductor のワークスペースを移行" + } + }, + "bs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Donesite svoje Conductor radne prostore" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tag dine Conductor-arbejdsområder med" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Porta i tuoi spazi di lavoro di Conductor" + } + }, + "km" : { + "stringUnit" : { + "state" : "translated", + "value" : "នាំកន្លែងធ្វើការ Conductor របស់អ្នកមក" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ta med Conductor-arbeidsområdene dine" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Przenieś swoje obszary robocze Conductor" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Traga seus espaços de trabalho do Conductor" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Перенесите рабочие пространства Conductor" + } + }, + "th" : { + "stringUnit" : { + "state" : "translated", + "value" : "นำพื้นที่ทำงาน Conductor ของคุณมา" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Conductor çalışma alanlarınızı getirin" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Перенесіть робочі простори Conductor" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mang không gian làm việc Conductor sang đây" + } + } + } + }, + "conductorImport.note.detail" : { + "comment" : "The detail of the sidebar's one-time Import from Conductor note: the deadline, and where the import is.", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Conductor Cloud shuts down on November 20. Settings › Import opens your workspaces here, in place." + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Conductor Cloud wird am 20. November eingestellt. Einstellungen › Importieren öffnet deine Arbeitsbereiche hier, an Ort und Stelle." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Conductor Cloud ferme le 20 novembre. Réglages › Importer ouvre vos espaces de travail ici, sur place." + } + }, + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "يتوقف Conductor Cloud في 20 نوفمبر. الإعدادات › الاستيراد يفتح مساحات عملك هنا في مكانها." + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Conductor Cloud cierra el 20 de noviembre. Ajustes › Importar abre tus espacios de trabajo aquí, en su lugar." + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "Conductor Cloud 將於 11 月 20 日關閉。「設定 › 匯入」會就地在此開啟你的工作區。" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "Conductor Cloud 将于 11 月 20 日关闭。“设置 › 导入”会就地在此打开你的工作区。" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "Conductor Cloud는 11월 20일에 종료됩니다. 설정 › 가져오기에서 작업 공간을 그대로 여기에서 엽니다." + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "Conductor Cloud は 11 月 20 日に終了します。設定 › 読み込みで、ワークスペースをそのままここで開けます。" + } + }, + "bs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Conductor Cloud se gasi 20. novembra. Postavke › Uvoz otvara vaše radne prostore ovdje, na mjestu." + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Conductor Cloud lukker den 20. november. Indstillinger › Import åbner dine arbejdsområder her, på stedet." + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Conductor Cloud chiude il 20 novembre. Impostazioni › Importa apre qui i tuoi spazi di lavoro, sul posto." + } + }, + "km" : { + "stringUnit" : { + "state" : "translated", + "value" : "Conductor Cloud បិទនៅថ្ងៃទី 20 ខែវិច្ឆិកា។ ការកំណត់ › នាំចូល បើកកន្លែងធ្វើការរបស់អ្នកនៅទីនេះ នៅនឹងកន្លែង។" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Conductor Cloud stenges 20. november. Innstillinger › Import åpner arbeidsområdene dine her, der de er." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Conductor Cloud zostanie wyłączony 20 listopada. Ustawienia › Import otworzy tu Twoje obszary robocze, na miejscu." + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "O Conductor Cloud será encerrado em 20 de novembro. Ajustes › Importar abre seus espaços de trabalho aqui, no lugar." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Conductor Cloud закроется 20 ноября. Настройки › Импорт откроют ваши рабочие пространства здесь, на месте." + } + }, + "th" : { + "stringUnit" : { + "state" : "translated", + "value" : "Conductor Cloud ปิดให้บริการวันที่ 20 พฤศจิกายน การตั้งค่า › นำเข้า จะเปิดพื้นที่ทำงานของคุณที่นี่ในที่เดิม" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Conductor Cloud 20 Kasım'da kapanıyor. Ayarlar › İçe Aktar, çalışma alanlarınızı burada yerinde açar." + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Conductor Cloud закриється 20 листопада. Налаштування › Імпорт відкриють ваші робочі простори тут, на місці." + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Conductor Cloud ngừng hoạt động vào ngày 20 tháng 11. Cài đặt › Nhập sẽ mở không gian làm việc của bạn tại chỗ ở đây." + } + } + } + }, + "conductorImport.note.open" : { + "comment" : "The button of the sidebar's Import from Conductor note: opens Settings › Import.", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Open Import" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Import öffnen" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ouvrir l’importation" + } + }, + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "فتح الاستيراد" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Abrir Importar" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "開啟匯入" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "打开导入" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "가져오기 열기" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "読み込みを開く" + } + }, + "bs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Otvori uvoz" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Åbn import" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Apri Importa" + } + }, + "km" : { + "stringUnit" : { + "state" : "translated", + "value" : "បើកការនាំចូល" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Åpne import" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Otwórz import" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Abrir Importar" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Открыть импорт" + } + }, + "th" : { + "stringUnit" : { + "state" : "translated", + "value" : "เปิดการนำเข้า" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "İçe Aktarmayı Aç" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Відкрити імпорт" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mở Nhập" + } + } + } + }, + "conductorImport.note.close" : { + "comment" : "The x of the sidebar's Import from Conductor note (tooltip and VoiceOver label): it never shows again.", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Don’t Show Again" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nicht mehr anzeigen" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ne plus afficher" + } + }, + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "عدم الإظهار مرة أخرى" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "No volver a mostrar" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "不再顯示" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "不再显示" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "다시 보지 않기" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "今後表示しない" + } + }, + "bs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ne prikazuj ponovo" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vis ikke igen" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Non mostrare più" + } + }, + "km" : { + "stringUnit" : { + "state" : "translated", + "value" : "កុំបង្ហាញម្តងទៀត" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ikke vis igjen" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nie pokazuj ponownie" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Não mostrar novamente" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Больше не показывать" + } + }, + "th" : { + "stringUnit" : { + "state" : "translated", + "value" : "ไม่ต้องแสดงอีก" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bir Daha Gösterme" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Більше не показувати" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Không hiển thị lại" + } + } + } + }, + "conductorImport.onboarding.line" : { + "comment" : "A line on the first-run Bring your history page when this Mac has Conductor: where to import its workspaces, and the deadline.", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Conductor workspaces: import them from Settings › Import before Conductor Cloud shuts down on November 20." + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Conductor-Arbeitsbereiche: Importiere sie über Einstellungen › Importieren, bevor Conductor Cloud am 20. November eingestellt wird." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Espaces de travail Conductor : importez-les depuis Réglages › Importer avant la fermeture de Conductor Cloud le 20 novembre." + } + }, + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "مساحات عمل Conductor: استوردها من الإعدادات › الاستيراد قبل توقف Conductor Cloud في 20 نوفمبر." + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Espacios de trabajo de Conductor: impórtalos desde Ajustes › Importar antes de que Conductor Cloud cierre el 20 de noviembre." + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "Conductor 工作區:請在 Conductor Cloud 於 11 月 20 日關閉前,從「設定 › 匯入」匯入。" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "Conductor 工作区:请在 Conductor Cloud 于 11 月 20 日关闭前,从“设置 › 导入”导入。" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "Conductor 작업 공간: Conductor Cloud가 11월 20일에 종료되기 전에 설정 › 가져오기에서 가져오세요." + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "Conductor のワークスペース: Conductor Cloud が 11 月 20 日に終了する前に、設定 › 読み込みから読み込んでください。" + } + }, + "bs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Conductor radni prostori: uvezite ih iz Postavke › Uvoz prije nego što se Conductor Cloud ugasi 20. novembra." + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Conductor-arbejdsområder: Importér dem fra Indstillinger › Import, før Conductor Cloud lukker den 20. november." + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Spazi di lavoro di Conductor: importali da Impostazioni › Importa prima che Conductor Cloud chiuda il 20 novembre." + } + }, + "km" : { + "stringUnit" : { + "state" : "translated", + "value" : "កន្លែងធ្វើការ Conductor៖ នាំចូលពី ការកំណត់ › នាំចូល មុនពេល Conductor Cloud បិទនៅថ្ងៃទី 20 ខែវិច្ឆិកា។" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Conductor-arbeidsområder: Importer dem fra Innstillinger › Import før Conductor Cloud stenges 20. november." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Obszary robocze Conductor: zaimportuj je w Ustawienia › Import, zanim Conductor Cloud zostanie wyłączony 20 listopada." + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Espaços de trabalho do Conductor: importe-os em Ajustes › Importar antes que o Conductor Cloud seja encerrado em 20 de novembro." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Рабочие пространства Conductor: импортируйте их в Настройки › Импорт до закрытия Conductor Cloud 20 ноября." + } + }, + "th" : { + "stringUnit" : { + "state" : "translated", + "value" : "พื้นที่ทำงาน Conductor: นำเข้าจาก การตั้งค่า › นำเข้า ก่อนที่ Conductor Cloud จะปิดให้บริการวันที่ 20 พฤศจิกายน" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Conductor çalışma alanları: Conductor Cloud 20 Kasım'da kapanmadan önce Ayarlar › İçe Aktar'dan içe aktarın." + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Робочі простори Conductor: імпортуйте їх у Налаштування › Імпорт до закриття Conductor Cloud 20 листопада." + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Không gian làm việc Conductor: hãy nhập chúng từ Cài đặt › Nhập trước khi Conductor Cloud ngừng hoạt động vào ngày 20 tháng 11." + } + } + } } }, "version" : "1.0" diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Onboarding/ClassicImportPlan.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextOnboarding/System/ClassicImportPlan.swift similarity index 74% rename from Packages/macOS/CmuxNext/Sources/CmuxNextApp/Onboarding/ClassicImportPlan.swift rename to Packages/macOS/CmuxNext/Sources/CmuxNextOnboarding/System/ClassicImportPlan.swift index 9e76723c9b49..58324deaf3cb 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Onboarding/ClassicImportPlan.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextOnboarding/System/ClassicImportPlan.swift @@ -1,9 +1,8 @@ -import CmuxNextOnboarding import Foundation /// The exact classic snapshot shown for confirmation and restored after it. -struct ClassicImportPlan: Sendable { - enum Source: Sendable { +public nonisolated struct ClassicImportPlan: Sendable { + public nonisolated enum Source: Sendable { case stable case nightly case releaseCandidate @@ -11,7 +10,7 @@ struct ClassicImportPlan: Sendable { case debug /// A fixed technical identifier, never the untrusted source filename. - var fileName: String { + public var fileName: String { switch self { case .stable: "session-com.cmuxterm.app.json" case .nightly: "session-com.cmuxterm.app.nightly.json" @@ -22,24 +21,24 @@ struct ClassicImportPlan: Sendable { } } - let source: Source - let workspaces: [ClassicSessionWorkspace] - let skippedCloudVMs: Int - let cleanupWorkspaces: Int + public let source: Source + public let workspaces: [ClassicSessionWorkspace] + public let skippedCloudVMs: Int + public let cleanupWorkspaces: Int - init(source: ClassicMigrationSource, migration: ClassicMigrationPlan) { + public init(source: ClassicMigrationSource, migration: ClassicMigrationPlan) { self.source = Self.source(source.bundleID) workspaces = migration.windows.flatMap(\.workspaces) skippedCloudVMs = migration.skipped.values.count { $0 == .cloudVM } cleanupWorkspaces = migration.cleanup.count } - var browserTabs: Int { tabs.count { $0.url != nil } } - var localFilePages: Int { + public var browserTabs: Int { tabs.count { $0.url != nil } } + public var localFilePages: Int { tabs.count { $0.url.flatMap(URL.init(string:))?.isFileURL == true } } - var sshWorkspaces: Int { workspaces.count { $0.remote != nil } } - var agentChats: Int { + public var sshWorkspaces: Int { workspaces.count { $0.remote != nil } } + public var agentChats: Int { workspaces.filter { $0.remote == nil && $0.cloudVM == nil } .flatMap(\.layout.tabs).count { $0.agent != nil } } diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextOnboarding/System/ClassicSnapshotRead.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextOnboarding/System/ClassicSnapshotRead.swift new file mode 100644 index 000000000000..2816dd81f624 --- /dev/null +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextOnboarding/System/ClassicSnapshotRead.swift @@ -0,0 +1,14 @@ +public import Foundation + +/// Classic's session as read, the save it came from, and the files to back up. +public nonisolated struct ClassicSnapshotRead: Sendable { + public var snapshot: ClassicSessionSnapshot + public var source: ClassicMigrationSource + public var backups: [URL] + + public init(snapshot: ClassicSessionSnapshot, source: ClassicMigrationSource, backups: [URL]) { + self.snapshot = snapshot + self.source = source + self.backups = backups + } +} diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextPages/ImportHistoryPageProvider.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextPages/ImportHistoryPageProvider.swift index 95c33665e9cf..a7bb57bf3026 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextPages/ImportHistoryPageProvider.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextPages/ImportHistoryPageProvider.swift @@ -22,6 +22,9 @@ public final class ImportHistoryPageProvider: PageProvider { public var anchor: () -> NSWindow? = { nil } public var restart: () -> Void = {} public var finish: (Bool) -> Void = { _ in } + /// A line under the sources about another tool's import (Import from Conductor's, with its + /// deadline), nil for none. Called off the main thread each time the page detects its sources. + public var notice: (@Sendable () -> String?)? public init(muxHome: String, personOnlyMessage: String, run: @escaping Run) { command = MemoryImportCommand(target: .local(muxHome: muxHome)) @@ -56,11 +59,12 @@ public final class ImportHistoryPageProvider: PageProvider { admitted[name] = ["grok-bot", "muse", "dots", "instinct"].contains(kind) ? kind : "" } } + let line = await Self.line(notice) return ["sources": .array(sources.map { source in ["source": source["source"] ?? .null, "kind": source["kind"] ?? .null, "path": source["path"] ?? .null, "items": source["count"] ?? .null, "bytes": source["bytes"] ?? .null] - })] + }), "notice": line.map { JSONValue.string($0) } ?? .null] case "cmux.importHistory.begin": try requireGesture(context) guard !busy, let id = params["batch"]?.stringValue, !id.isEmpty else { @@ -178,6 +182,12 @@ public final class ImportHistoryPageProvider: PageProvider { spentGesture = event } + private nonisolated static func line(_ notice: (@Sendable () -> String?)?) async -> String? { + guard let notice else { return nil } + // task-owner: one off-main folder listing per detection, awaited here + return await Task.detached(priority: .utility) { notice() }.value + } + private nonisolated static func pasteFile(_ text: String) async throws -> (URL, URL) { // task-owner: one off-main file write; the provider removes its folder when the page closes try await Task.detached(priority: .utility) { diff --git a/Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/BrowserImportEnabledTests.swift b/Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/BrowserImportEnabledTests.swift index 15864d9aa78f..1943fe9df1eb 100644 --- a/Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/BrowserImportEnabledTests.swift +++ b/Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/BrowserImportEnabledTests.swift @@ -210,7 +210,7 @@ struct BrowserImportOffScanTests { let held = HeldScan() center.scanThisMac = { _ in await held.run() } center.host = ImportCardsHost(runImport: { _, _, _ in throw CancellationError() }, importedProfiles: { [] }, - openSettings: { _ in }, importClassic: { _ in nil }) + openSettings: { _ in }, importClassic: { _ in nil }, importConductor: { _ in nil }) var landed = false center.scan { landed = true } while held.started == 0 { await Task.yield() } diff --git a/Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/ImportSuggestionRemovedTests.swift b/Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/ImportSuggestionRemovedTests.swift index 1b611e7e8071..13a485d13e3c 100644 --- a/Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/ImportSuggestionRemovedTests.swift +++ b/Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/ImportSuggestionRemovedTests.swift @@ -51,7 +51,7 @@ import Testing center.bind(into: registry, host: ImportCardsHost(runImport: { _, _, _ in throw CancellationError() }, importedProfiles: { [] }, openSettings: { routes.append($0) }, - importClassic: { _ in nil })) + importClassic: { _ in nil }, importConductor: { _ in nil })) #expect(registry.isBound("import.open")) #expect(registry.perform("import.open", invocation: ActionInvocation(origin: .user))) #expect(routes == ["#/settings/import"]) diff --git a/Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/ImportSuggestionsCenterClassicTests.swift b/Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/ImportSuggestionsCenterClassicTests.swift index 4e5dfc1f0556..bd7f9d03ecbd 100644 --- a/Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/ImportSuggestionsCenterClassicTests.swift +++ b/Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/ImportSuggestionsCenterClassicTests.swift @@ -64,7 +64,8 @@ struct ImportSuggestionsCenterClassicTests { let center = ImportSuggestionsCenter(defaults: defaults, ledgerURL: ledger, now: { Self.now }) center.scanThisMac = { _ in [Self.classic] } center.host = ImportCardsHost(runImport: { _, _, _ in throw CancellationError() }, importedProfiles: { [] }, - openSettings: { _ in }, importClassic: { progress in try await runs.run(progress) }) + openSettings: { _ in }, importClassic: { progress in try await runs.run(progress) }, + importConductor: { _ in nil }) await withCheckedContinuation { done in center.scan { done.resume() } } return center } diff --git a/Packages/macOS/CmuxNext/Tests/CmuxNextDaemonTests/ConductorImportWireTests.swift b/Packages/macOS/CmuxNext/Tests/CmuxNextDaemonTests/ConductorImportWireTests.swift new file mode 100644 index 000000000000..1f15bd64cd25 --- /dev/null +++ b/Packages/macOS/CmuxNext/Tests/CmuxNextDaemonTests/ConductorImportWireTests.swift @@ -0,0 +1,58 @@ +@testable import CmuxNextDaemon +import Foundation +import Testing + +/// `conductor_import.plan`'s reply as the `cmux-conductor-import` crate serializes it: what +/// the app adopts, what it lists as recoverable, and what it leaves out. +struct ConductorImportWireTests { + static let reply = """ + {"found": {"database": "/Users/me/Library/Application Support/com.conductor.app/conductor.db", "workspaces": null, "app": null}, + "plan": {"source": "/Users/me/Library/Application Support/com.conductor.app/conductor.db", + "cloud_read_only": "2026-11-13", "cloud_shutdown": "2026-11-20", + "projects": [ + {"repo_id": "r1", "name": "app", "path": "/Users/me/src/app", "skipped": null, + "setup": "bun install", "teardown": null, "run": [{"id": "web", "command": "bun dev"}], + "aliases": [["CONDUCTOR_PORT", "CMUX_PORT"]], "worktreeinclude": [".env"], "env_names": ["API_KEY"], + "notes": [{"code": "not-imported", "detail": "spotlight_testing"}], + "workspaces": [ + {"id": "w-lima", "name": "lima", "branch": "leo/lima", "path": "/Users/me/conductor/workspaces/app/lima", + "disposition": {"kind": "adopt"}, "checked": true, + "chats": {"claude_folder": "/Users/me/.claude/projects/-Users-me-conductor-workspaces-app-lima", "transcripts": 2}}, + {"id": "w-done", "name": "kyiv", "branch": "leo/kyiv", "path": "/Users/me/conductor/workspaces/app/kyiv", + "disposition": {"kind": "adopt"}, "checked": false, "chats": null}, + {"id": "w-oslo", "name": "oslo", "branch": "leo/oslo", "path": "/Users/me/conductor/workspaces/app/oslo", + "disposition": {"kind": "recoverable", "from": "0123abcd"}, "checked": false, "chats": null}, + {"id": "w-plain", "name": "plain", "branch": null, "path": "/Users/me/conductor/workspaces/app/plain", + "disposition": {"kind": "skip", "reason": "not-a-worktree"}, "checked": false, "chats": null}]}, + {"repo_id": "r2", "name": "gone", "path": "/nowhere", "skipped": "/nowhere no longer exists", + "setup": null, "teardown": null, "run": [], "aliases": [], "worktreeinclude": [], "env_names": [], "notes": [], + "workspaces": [ + {"id": "w-x", "name": "x", "branch": "x", "path": "/nowhere/x", "disposition": {"kind": "skip", "reason": "project"}, + "checked": false, "chats": null}]}], + "cleanup": ["ws-half"], + "warnings": [{"code": "archive-deletes-folder", "detail": "Archiving ..."}]}} + """ + + @Test func decodesWhatTheDaemonSends() throws { + let reply = try JSONDecoder().decode(ConductorImportReply.self, from: Data(Self.reply.utf8)) + let plan = try #require(reply.plan) + #expect(reply.error == nil) + #expect(plan.cloudShutdown == "2026-11-20") + #expect(plan.cleanup == ["ws-half"]) + #expect(plan.projects.first?.envNames == ["API_KEY"]) + #expect(plan.warnings.map(\.code) == ["archive-deletes-folder"]) + // Only checked live workspaces of imported projects are made. + #expect(plan.adopting.map(\.workspace.id) == ["w-lima"]) + #expect(plan.adopting.first?.workspace.chats?.transcripts == 2) + #expect(plan.recoverable.map(\.id) == ["w-oslo"]) + #expect(plan.recoverable.first?.disposition.from == "0123abcd") + } + + @Test func nothingFoundIsNoPlan() throws { + let reply = try JSONDecoder().decode(ConductorImportReply.self, from: Data(#"{"found": {}, "plan": null}"#.utf8)) + #expect(reply.plan == nil) + let refused = try JSONDecoder().decode(ConductorImportReply.self, + from: Data(#"{"found": {}, "plan": null, "error": "ledger from a newer cmux"}"#.utf8)) + #expect(refused.error == "ledger from a newer cmux") + } +} diff --git a/Packages/macOS/CmuxNext/Tests/CmuxNextImportSuggestionsTests/ConductorNoteTests.swift b/Packages/macOS/CmuxNext/Tests/CmuxNextImportSuggestionsTests/ConductorNoteTests.swift new file mode 100644 index 000000000000..513cdc6c1500 --- /dev/null +++ b/Packages/macOS/CmuxNext/Tests/CmuxNextImportSuggestionsTests/ConductorNoteTests.swift @@ -0,0 +1,82 @@ +import CmuxNextBrowserImport +@testable import CmuxNextImportSuggestions +import Foundation +import Testing + +/// Import from Conductor pointed out outside Settings › Import: the sidebar's one-time note and +/// the first-run history page's line, each only while this Mac has Conductor and before Conductor +/// Cloud shuts down on November 20. +struct ConductorNoteTests { + /// A week before the deadline. + static let now = Date(timeIntervalSince1970: 1_794_556_800) + static let conductor = ImportFinding(id: "agents.conductor", kind: .conductor, name: "Conductor", count: 3) + + @Test func theNoteShowsWhileConductorIsHereAndNotImported() { + #expect(ImportSuggestionState().showsConductorNote(findings: [ImportCardDeckTests.claude, Self.conductor], jobs: ImportJobLedger(), now: Self.now)) + } + + @Test func theNoteNeedsConductor() { + #expect(!ImportSuggestionState().showsConductorNote(findings: [ImportCardDeckTests.claude], jobs: ImportJobLedger(), now: Self.now)) + #expect(!ImportSuggestionState().showsConductorNote(findings: [], jobs: ImportJobLedger(), now: Self.now)) + } + + @Test func theNoteEndsAtTheDeadline() { + let state = ImportSuggestionState() + let deadline = ImportSuggestionState.conductorDeadline + #expect(state.showsConductorNote(findings: [Self.conductor], jobs: ImportJobLedger(), now: deadline.addingTimeInterval(-1))) + #expect(!state.showsConductorNote(findings: [Self.conductor], jobs: ImportJobLedger(), now: deadline)) + #expect(!state.showsConductorNote(findings: [Self.conductor], jobs: ImportJobLedger(), now: deadline.addingTimeInterval(86_400))) + } + + /// Its x closes it for good, and the closed note survives a save and a load. + @Test func aClosedNoteNeverShowsAgain() throws { + var state = ImportSuggestionState() + state.conductorNoteClosed = true + #expect(!state.showsConductorNote(findings: [Self.conductor], jobs: ImportJobLedger(), now: Self.now)) + let suite = "conductor-note-\(UUID().uuidString)" + let defaults = try #require(UserDefaults(suiteName: suite)) + defer { defaults.removePersistentDomain(forName: suite) } + state.save(to: defaults) + #expect(ImportSuggestionState.load(from: defaults).conductorNoteClosed == true) + } + + /// The state saved before the note existed still loads, with its dismissals. + @Test func stateSavedBeforeTheNoteStillLoads() throws { + let suite = "conductor-note-\(UUID().uuidString)" + let defaults = try #require(UserDefaults(suiteName: suite)) + defer { defaults.removePersistentDomain(forName: suite) } + defaults.set(Data(#"{"dismissed":{"agents.codex":"agents.codex|ok"},"requests":[]}"#.utf8), forKey: ImportSuggestionState.defaultsKey) + let state = ImportSuggestionState.load(from: defaults) + #expect(state.dismissed == ["agents.codex": "agents.codex|ok"]) + #expect(state.conductorNoteClosed == nil) + #expect(state.showsConductorNote(findings: [Self.conductor], jobs: ImportJobLedger(), now: Self.now)) + } + + /// Once the import started (running, or done) the note goes; so does a Conductor card the + /// person hid in Settings › Import (Done after the import removes the job and hides the card). + @Test func theNoteGoesOnceTheImportRan() { + var running = ImportJobLedger() + running.put(ImportJob(finding: Self.conductor, now: Self.now)) + #expect(!ImportSuggestionState().showsConductorNote(findings: [Self.conductor], jobs: running, now: Self.now)) + + var job = ImportJob(finding: Self.conductor, now: Self.now) + job.importedClassic(3, now: Self.now) + var done = ImportJobLedger() + done.put(job) + #expect(!ImportSuggestionState().showsConductorNote(findings: [Self.conductor], jobs: done, now: Self.now)) + + var hidden = ImportSuggestionState() + hidden.dismiss(Self.conductor) + #expect(!hidden.showsConductorNote(findings: [Self.conductor], jobs: ImportJobLedger(), now: Self.now)) + } + + @Test func theOnboardingLineNeedsConductorAndTheDeadline() throws { + let home = try ImportScannerTests.Home(["conductor/workspaces/app/lima/README"]) + #expect(ImportScanner(home: home.url).conductorOnboardingLine(now: Self.now) == ConductorImportStrings.onboardingLine) + #expect(ImportScanner(home: home.url).conductorOnboardingLine(now: ImportSuggestionState.conductorDeadline) == nil) + let database = try ImportScannerTests.Home(["Library/Application Support/com.conductor.app/conductor.db"]) + #expect(ImportScanner(home: database.url).conductorOnboardingLine(now: Self.now) != nil) + let empty = try ImportScannerTests.Home() + #expect(ImportScanner(home: empty.url).conductorOnboardingLine(now: Self.now) == nil) + } +} diff --git a/Packages/macOS/CmuxNext/Tests/CmuxNextImportSuggestionsTests/ImportCardDeckTests.swift b/Packages/macOS/CmuxNext/Tests/CmuxNextImportSuggestionsTests/ImportCardDeckTests.swift index 85f60ae7e53e..a99e75cbc777 100644 --- a/Packages/macOS/CmuxNext/Tests/CmuxNextImportSuggestionsTests/ImportCardDeckTests.swift +++ b/Packages/macOS/CmuxNext/Tests/CmuxNextImportSuggestionsTests/ImportCardDeckTests.swift @@ -143,6 +143,24 @@ struct ImportCardDeckTests { } /// Resume and Try Again start over from 0, not at the fraction the cut-off run had reached. + /// The Conductor card imports its workspaces with the Cloud deadline in its text, and once + /// done stays offered again while Conductor's data is here. + @Test func conductorCardImportsWorkspacesAndOffersItAgain() throws { + let finding = ImportFinding(id: "agents.conductor", kind: .conductor, name: "Conductor", count: 3) + let card = try #require(Self.deck([finding]).cards().first) + #expect(card.id == "agents.conductor" && card.state == .ready) + #expect(card.primary?.action == .start && card.primary?.title == ImportSuggestionStrings.importWorkspaces) + #expect(card.facts == ImportSuggestionStrings.workspaces(3)) + #expect(card.benefit == ImportSuggestionStrings.conductor) + var job = ImportJob(finding: finding, now: Self.now) + job.importedClassic(3, now: Self.now) + var ledger = ImportJobLedger() + ledger.put(job) + let done = try #require(Self.deck([finding], jobs: ledger).cards().first) + #expect(done.state == .done && done.status == ImportSuggestionStrings.doneConductor) + #expect(done.primary?.action == .start && done.primary?.title == "Import Again") + } + @Test func aResumedClassicJobStartsAtZeroProgress() { var job = ImportJob(finding: Self.classicAllHere, now: Self.now) job.progressed(0.6, now: Self.now) diff --git a/Packages/macOS/CmuxNext/Tests/CmuxNextImportSuggestionsTests/ImportScannerTests.swift b/Packages/macOS/CmuxNext/Tests/CmuxNextImportSuggestionsTests/ImportScannerTests.swift index 06800827d432..61ed2f165845 100644 --- a/Packages/macOS/CmuxNext/Tests/CmuxNextImportSuggestionsTests/ImportScannerTests.swift +++ b/Packages/macOS/CmuxNext/Tests/CmuxNextImportSuggestionsTests/ImportScannerTests.swift @@ -140,6 +140,19 @@ struct ImportScannerTests { #expect(Self.scanner(home, classic: nil) { _ in .missing }.scan().allSatisfy { $0.kind != .cmuxClassic }) } + /// Conductor's workspaces are counted from its folders; its database alone is enough for a + /// card; nothing of Conductor's means no card. + @Test func conductorCountsItsWorkspaceFolders() throws { + let home = try Home(["conductor/workspaces/app/lima/README", "conductor/workspaces/app/oslo/README", "conductor/workspaces/web/kyiv/README"]) + let found = Self.scanner(home) { _ in .missing }.scan().first { $0.kind == .conductor } + #expect(found?.id == "agents.conductor") + #expect(found?.count == 3) + + let databaseOnly = try Home(["Library/Application Support/com.conductor.app/conductor.db"]) + #expect(Self.scanner(databaseOnly) { _ in .missing }.scan().first { $0.kind == .conductor }?.count == 0) + #expect(Self.scanner(try Home()) { _ in .missing }.scan().allSatisfy { $0.kind != .conductor }) + } + @Test func iTermProfilesAndTheirColors() throws { let color = { (r: Double, g: Double, b: Double) in ["Red Component": r, "Green Component": g, "Blue Component": b] } let plist: [String: Any] = [ diff --git a/cmux-next-ios/verify/parity-mac.md b/cmux-next-ios/verify/parity-mac.md index b7c361898486..d6a39a31be46 100644 --- a/cmux-next-ios/verify/parity-mac.md +++ b/cmux-next-ios/verify/parity-mac.md @@ -706,6 +706,7 @@ or the relay. States here are from the live run in this batch's proof report. Ri | Mac action | Mac sends | Phone | State | Notes | | --- | --- | --- | --- | --- | +| Import from Conductor (Settings › Import, the Conductor card) | resource `conductor_import.plan`, `conductor_import.record`; `project.add`; `windows.createWorkspace {cwd}` | — | not from a phone | It reads Conductor's database and folders on the Mac itself and opens workspaces in those folders; the phone has no Conductor data to read, and the import's workspaces show on the phone as any Mac workspace does. | | Automations page (list, Run now, Pause / Resume, Delete) | `cloud.automation`: `automation.list`, `automation.runs.list` (`/v1/read`), `automation.run` / `update {enabled}` / `delete` (`/v1/ops`) | Settings → Cloud → **Automations**: Needs you / Today / Later / Paused; a row pushes its page (Run Now, Pause or Resume, Delete, History); swipe to pause or delete | works (live) | Cloud automations only: this Mac's local runner (`cmux automation list`) is the Mac's own store, not on the phone's lane. Delete asks first; its run history stays. | | Show Artifacts (synced tree, preview, history) | platform `/v1/artifacts/tree`, `/file`, `/history` with the app's bearer | — | not from a phone (blocked) | The platform's artifact reads take only "a cmux access token of a team install" (`artifacts.unauthorized`, checked live on the development platform 2026-11-03); that token is minted from a Mac's install key (`cmux auth token`). The phone's own account token is refused, so a phone page needs the platform to accept a signed-in person's token for reads, or a phone install grant: a trust decision (#13742). | | Delete Conversation from List / Show Hidden Conversations | the Mac's Home source (local) | Home row menu **Delete Conversation from List** (asks first); **Show N Hidden Conversations** under the list | works (live) | Kept per account on this iPhone, like the Mac's; a newer message brings the row back. | diff --git a/release-notes/next/highlights/import-from-conductor.md b/release-notes/next/highlights/import-from-conductor.md new file mode 100644 index 000000000000..a2255aa9866e --- /dev/null +++ b/release-notes/next/highlights/import-from-conductor.md @@ -0,0 +1,5 @@ +title: Bring your Conductor workspaces to cmux +category: new +action: openSettings | Open Settings + +In Settings › Import, Import from Conductor opens each workspace in its own folder, on its branch, with its Claude Code chats. Conductor Cloud shuts down on November 20. diff --git a/webviews/src/pages/import-history/ImportHistory.tsx b/webviews/src/pages/import-history/ImportHistory.tsx index 57d029353e8e..339840de5072 100644 --- a/webviews/src/pages/import-history/ImportHistory.tsx +++ b/webviews/src/pages/import-history/ImportHistory.tsx @@ -67,6 +67,7 @@ export function ImportHistory({ store, strings, layout = "list" }: ImportHistory )} )} + {snapshot.notice &&

{snapshot.notice}

}

{t("history.safety")}