From 843f252a420a187d4f5f396da63dadbf40f0f154 Mon Sep 17 00:00:00 2001 From: Leo Li Date: Thu, 22 Oct 2026 00:40:11 -0700 Subject: [PATCH 1/4] Test: no import suggestion on the Chief front page; the palette's import still opens Settings Red on base: the empty Chief front page holds ImportSuggestionRowView (cmux.home.importSuggestion). Co-Authored-By: Claude Opus 5.5 Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a16057-0fea-7933-80f2-cec81447d873&host=cosmic-gray --- .../ImportSuggestionRemovedTests.swift | 64 +++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/ImportSuggestionRemovedTests.swift diff --git a/Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/ImportSuggestionRemovedTests.swift b/Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/ImportSuggestionRemovedTests.swift new file mode 100644 index 000000000000..9106c8ef4116 --- /dev/null +++ b/Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/ImportSuggestionRemovedTests.swift @@ -0,0 +1,64 @@ +import AppKit +import CmuxNextActions +@testable import CmuxNextApp +import CmuxHomeCore +@testable import CmuxNextHome +@testable import CmuxNextImportCards +import Foundation +import Testing + +/// Leo 2026-10-21: "yeah the bring your stuff to cmux is extrmeely questionable idk why that's +/// there + why they went to the chief thingy", and "yes to both" to removing it from the New Tab +/// and the Chief completely. The import suggestion is gone from the Chief's front page; importing +/// lives in Settings › Import and the palette's Import from Other Apps…. +@MainActor @Suite(.serialized) struct ImportSuggestionRemovedTests { + /// The empty Chief conversation's front page holds no import suggestion row and no import card, + /// shown or hidden. + @Test func theChiefFrontPageHasNoImportSuggestion() { + _ = NSApplication.shared + let services = AppServices(environment: AppEnvironment.current([:])) + let view = HomeHostView(services: services, conversation: ConversationID("conv_no_import_suggestion").rawValue) + view.frame = NSRect(x: 0, y: 0, width: 900, height: 700) + view.layoutSubtreeIfNeeded() + let page = view.transcript.firstRun.everyDescendant + #expect(!page.contains { $0.accessibilityIdentifier() == "cmux.home.importSuggestion" }, "the front page holds the import row") + #expect(!page.contains { String(describing: type(of: $0)).contains("Import") }, "the front page holds an import view") + #expect(!page.contains { $0.texts.contains { $0.localizedCaseInsensitiveContains("Bring your stuff") } }) + } + + /// Import stays in the command palette: Import from Other Apps… is a palette action, and it + /// opens Settings › Import. + @Test func thePalettesImportCommandStillOpensSettingsImport() throws { + let registry = ActionRegistry(catalog: ActionCatalog.all, aliases: ActionCatalog.legacyAliases) + let descriptor = try #require(registry.descriptor(for: "import.open")) + #expect(descriptor.surfaces.contains(.palette)) + let suite = "ImportSuggestionRemovedTests.\(UUID().uuidString)" + let defaults = try #require(UserDefaults(suiteName: suite)) + defer { defaults.removePersistentDomain(forName: suite) } + let ledger = FileManager.default.temporaryDirectory.appending(path: "import-jobs-\(UUID().uuidString).json") + defer { try? FileManager.default.removeItem(at: ledger) } + var routes: [String] = [] + let center = ImportSuggestionsCenter(defaults: defaults, ledgerURL: ledger) + center.bind(into: registry, host: ImportCardsHost(runImport: { _, _, _ in throw CancellationError() }, + importedProfiles: { [] }, + openSettings: { routes.append($0) }, + importClassic: { _ in nil })) + #expect(registry.isBound("import.open")) + #expect(registry.perform("import.open", invocation: ActionInvocation(origin: .user))) + #expect(routes == ["#/settings/import"]) + } +} + +extension NSView { + /// This view and every view under it, hidden ones too. + fileprivate var everyDescendant: [NSView] { + [self] + subviews.flatMap(\.everyDescendant) + } + + /// The view's own label or button text. + fileprivate var texts: [String] { + if let field = self as? NSTextField { return [field.stringValue] } + if let button = self as? NSButton { return [button.title] } + return [] + } +} From 90be08ed084a03285c7e27be81cb8e089814e1d2 Mon Sep 17 00:00:00 2001 From: Leo Li Date: Thu, 22 Oct 2026 00:40:41 -0700 Subject: [PATCH 2/4] cmux NEXT Home: no import suggestion on the Chief front page Leo 2026-10-21: "yeah the bring your stuff to cmux is extrmeely questionable idk why that's there + why they went to the chief thingy", then "yes to both" to removing it from the New Tab and the Chief completely, with import only in Settings and the command palette. Removes the Chief front page's import row (ImportSuggestionRowView, from #23940), its wiring in HomeHostView, HomeFirstRunView's accessory slot and setFirstRunAccessory / firstRunAccessoryChanged, and the row's "Show All" string. The New Tab part is a separate PR. Co-Authored-By: Claude Opus 5.5 Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a16057-0fea-7933-80f2-cec81447d873&host=cosmic-gray --- Packages/macOS/CmuxNext/Package.swift | 2 +- .../CmuxNextApp/Home/HomeHostView.swift | 7 - .../NativeTranscript/HomeFirstRunView.swift | 29 +-- .../HomeNativeTranscriptView+FirstRun.swift | 12 - .../ImportSuggestionRowView.swift | 234 ------------------ .../ImportSuggestionStrings.swift | 1 - .../Resources/ImportSuggestions.xcstrings | 131 ---------- .../HomeImportSuggestionRowTests.swift | 136 ---------- 8 files changed, 3 insertions(+), 549 deletions(-) delete mode 100644 Packages/macOS/CmuxNext/Sources/CmuxNextImportCards/ImportSuggestionRowView.swift delete mode 100644 Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/HomeImportSuggestionRowTests.swift diff --git a/Packages/macOS/CmuxNext/Package.swift b/Packages/macOS/CmuxNext/Package.swift index 05e4270e87eb..0e2d2dcec48f 100644 --- a/Packages/macOS/CmuxNext/Package.swift +++ b/Packages/macOS/CmuxNext/Package.swift @@ -453,7 +453,7 @@ let package = Package( swiftSettings: daemonSwiftSettings ), // The cards' center and surfaces: the scan schedule, the job runner, the `import.*` - // actions, the agent pages' push and the Chief front page's row. The App supplies `ImportCardsHost`. + // actions and the agent pages' push. The App supplies `ImportCardsHost`. .target( name: "CmuxNextImportCards", dependencies: ["CmuxNextActions", "CmuxNextAgentPane", "CmuxNextBrowserImport", "CmuxNextCompat", "CmuxNextDesign", diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Home/HomeHostView.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Home/HomeHostView.swift index 54bf8bf30422..c2aef289ac9d 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Home/HomeHostView.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Home/HomeHostView.swift @@ -9,7 +9,6 @@ import CmuxNextDesign import CmuxNextHome import CmuxNextHomeEngine import CmuxNextHostedChief -import CmuxNextImportCards import CmuxNextPages import CmuxNextWakeups @@ -230,12 +229,6 @@ final class HomeHostView: NSView { } // The first-run panel's agent rows open the agent's tab like a jump (Search Tabs). transcript.onFirstRunAgent = { [weak services] tab in _ = services?.revealTab(tab) } - // What this Mac can bring over (Claude Code's sessions, a browser's history) is one row on - // the Chief's front page, under its action rows: never a strip over the conversation - // (Leo 2026-10-21, nightly 331e2e9). Not Now dismisses it; Show All opens Settings › Import. - let suggestion = ImportSuggestionRowView() - suggestion.onChange = { [weak transcript] in transcript?.firstRunAccessoryChanged() } - transcript.setFirstRunAccessory(suggestion) // task-owner: lives as long as this view; event-driven (Observation): // the user's agents for the first-run glance, only while it can show // (the Chief with no messages yet): otherwise no tab is read, so tab diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextHome/NativeTranscript/HomeFirstRunView.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextHome/NativeTranscript/HomeFirstRunView.swift index 2ba688b5d9d2..1f876cf58767 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextHome/NativeTranscript/HomeFirstRunView.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextHome/NativeTranscript/HomeFirstRunView.swift @@ -15,8 +15,7 @@ public enum HomeFirstRunAction: Sendable, Equatable { /// The empty Chief conversation's first-run panel, the Chief's front page: the Chief's face /// and what it does (`HomeFirstRunHero`), then things to do now, each a row with its shortcut /// (start an agent, open a terminal) and the Chief pet's Show / Hide, then the user's agents -/// and the host's row under them (Home's import suggestion), then the user's agents at a -/// glance, most urgent first, each opening its tab, then the "Switching" card with the +/// at a glance, most urgent first, each opening its tab, then the "Switching" card with the /// core switching keys (`HomeFirstRunSwitchCard`, hidden for good by its Hide button). Hidden /// once the conversation has a message (`HomeNativeTranscriptView.updateFirstRun`). A short /// pane drops agent rows first, then the hero. @@ -44,26 +43,6 @@ final class HomeFirstRunView: NSView { private var scale: CGFloat = 1 private var colors: Colors? private lazy var headerWidth = agentsHeader.widthAnchor.constraint(equalToConstant: Self.maxWidth) - /// The host's row under the action rows (Home's import suggestion, `ImportSuggestionRowView`), - /// at the column's width; out of the layout while hidden. It sizes its own height and tells - /// the host when it shows or hides (`HomeNativeTranscriptView.firstRunAccessoryChanged`). - var accessory: NSView? { - didSet { - guard accessory !== oldValue else { return } - accessoryWidth?.isActive = false - accessoryWidth = nil - oldValue?.removeFromSuperview() - if let accessory { - accessory.translatesAutoresizingMaskIntoConstraints = false - let width = accessory.widthAnchor.constraint(equalToConstant: Self.maxWidth) - width.isActive = true - accessoryWidth = width - } - arrange() - needsLayout = true - } - } - private var accessoryWidth: NSLayoutConstraint? /// Called when the user picks open a terminal or start an agent. var onAction: (HomeFirstRunAction) -> Void = { _ in } /// Called with an agent's tab id when the user picks its row. @@ -142,8 +121,7 @@ final class HomeFirstRunView: NSView { private func arrange() { for view in stack.arrangedSubviews { stack.removeArrangedSubview(view); view.removeFromSuperview() } let listed: [NSView] = agentRows.isEmpty ? [] : [agentsHeader] + agentRows - let hosted: [NSView] = accessory.map { [$0] } ?? [] - for view in [hero] + actionRows + hosted + listed + [switchCard] as [NSView] { stack.addArrangedSubview(view) } + for view in [hero] + actionRows + listed + [switchCard] as [NSView] { stack.addArrangedSubview(view) } applySpacing() } @@ -156,7 +134,6 @@ final class HomeFirstRunView: NSView { let row = HomeFirstRunRow.height * scale, gap = 6 * scale, group = 14 * scale let card = switchCard.isHidden ? 0 : group + cardHeight.constant let actions = CGFloat(actionRows.count) * row + CGFloat(actionRows.count - 1) * gap - + (accessory.map { $0.isHidden ? 0 : group + $0.fittingSize.height } ?? 0) // The hero goes before any action row does: it shows only when it fits with them and the card. let heroPart = heroHeight.constant + Self.heroGap * scale let heroFits = showsHero && actions + card + heroPart <= bounds.height @@ -177,7 +154,6 @@ final class HomeFirstRunView: NSView { private func applySpacing() { stack.spacing = 6 * scale stack.setCustomSpacing(14 * scale, after: pet) - if let accessory { stack.setCustomSpacing(14 * scale, after: accessory) } stack.setCustomSpacing(Self.heroGap * scale, after: hero) } @@ -250,7 +226,6 @@ final class HomeFirstRunView: NSView { let width = max(0, min(Self.maxWidth * scale, bounds.width - 48 * scale)) for row in rows { row.width = width } if headerWidth.constant != width { headerWidth.constant = width } - if let accessoryWidth, accessoryWidth.constant != width { accessoryWidth.constant = width } if cardWidth.constant != width { cardWidth.constant = width } if heroWidth.constant != width { heroWidth.constant = width } let heroTall = hero.height(for: width) diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextHome/NativeTranscript/HomeNativeTranscriptView+FirstRun.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextHome/NativeTranscript/HomeNativeTranscriptView+FirstRun.swift index 6d1c740e2db3..1327f0da99ea 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextHome/NativeTranscript/HomeNativeTranscriptView+FirstRun.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextHome/NativeTranscript/HomeNativeTranscriptView+FirstRun.swift @@ -39,18 +39,6 @@ extension HomeNativeTranscriptView { } } - /// A row the host puts on the Chief's front page under its action rows (Home's import - /// suggestion, `ImportSuggestionRowView`), in the panel's own layout so it never covers the - /// conversation; nil removes it. - public func setFirstRunAccessory(_ view: NSView?) { - firstRun.accessory = view - } - - /// The front page's host row showed, hid or changed size: the panel fits its rows again. - public func firstRunAccessoryChanged() { - firstRun.needsLayout = true - } - /// The user's agents for the first-run panel's glance (the host maps /// them from the tab status the strip and the sidebar draw); empty lists /// none. diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextImportCards/ImportSuggestionRowView.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextImportCards/ImportSuggestionRowView.swift deleted file mode 100644 index 922a4b9b9035..000000000000 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextImportCards/ImportSuggestionRowView.swift +++ /dev/null @@ -1,234 +0,0 @@ -public import AppKit -import CmuxNextDesign -import CmuxNextImportSuggestions -import Observation - -/// Home's import suggestion: one compact row on the Chief's front page (the empty Chief -/// conversation's first-run panel), never over a conversation. It shows the first card of the -/// Home list: the source app's icon, its name and what was found ("Claude Code", "1,002 -/// sessions"), its button and Not Now; Show All opens Settings › Import when there are more. A -/// running import shows its status and a thin progress line. Hidden when there is nothing to -/// suggest. (Leo 2026-10-21 on nightly 331e2e9: the floating strip of cards it replaces sat over -/// the chat, which showed through and around it.) -@MainActor -public final class ImportSuggestionRowView: NSView { - /// The row's height: one line of controls on the front page. - static let height: CGFloat = 40 - /// The source app's icon side. - static let iconSide: CGFloat = 20 - - /// The sidebar cards' glass veil, never an opaque card. - let surface = CardSurfaceView(cornerRadius: 8) - let icon = NSImageView() - let titleLabel = NSTextField(labelWithString: "") - let factsLabel = NSTextField(labelWithString: "") - /// The shown card's buttons: its primary, then Not Now (or Done). More Options… stays in - /// Settings › Import, behind Show All. - private(set) var actions: [NSButton] = [] - let showAll = NSButton(title: "", target: nil, action: nil) - private let buttons = NSStackView() - private var progress: ImportProgressBar? - private var drawn: ImportCardList? - private var card: ImportCard? - private let press: (String, ImportCardAction) -> Void - private let openAll: () -> Void - /// Called after the row shows, hides or changes: the panel holding it lays out again. - public var onChange: () -> Void = {} - - /// A row that follows `center`'s Home list and runs its buttons there. - public convenience init(center: ImportSuggestionsCenter = .shared) { - self.init(press: { [weak center] id, action in try? center?.perform(id, action) }, - showAll: { [weak center] in center?.openSettings() }) - follow(center) - } - - init(press: @escaping (String, ImportCardAction) -> Void, showAll: @escaping () -> Void) { - self.press = press - openAll = showAll - super.init(frame: .zero) - setAccessibilityIdentifier("cmux.home.importSuggestion") - setAccessibilityElement(true) - setAccessibilityRole(.group) - build() - isHidden = true - } - - @available(*, unavailable) - required init?(coder: NSCoder) { nil } - - public override var intrinsicContentSize: NSSize { NSSize(width: NSView.noIntrinsicMetric, height: Self.height) } - - public override func viewDidChangeEffectiveAppearance() { - super.viewDidChangeEffectiveAppearance() - applyColors() - } - - private func build() { - surface.translatesAutoresizingMaskIntoConstraints = false - addSubview(surface) - icon.imageScaling = .scaleProportionallyUpOrDown - icon.translatesAutoresizingMaskIntoConstraints = false - for (label, font) in [(titleLabel, Typography.bodyEmphasized), (factsLabel, Typography.caption)] { - label.font = font - label.lineBreakMode = .byTruncatingTail - label.maximumNumberOfLines = 1 - label.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) - label.setContentHuggingPriority(.defaultHigh, for: .horizontal) - } - // The name keeps its room before what was found does. - titleLabel.setContentCompressionResistancePriority(NSLayoutConstraint.Priority(300), for: .horizontal) - showAll.isBordered = false - showAll.font = Typography.body - showAll.target = self - showAll.action = #selector(openSettings) - showAll.title = ImportSuggestionStrings.showAll - buttons.orientation = .horizontal - buttons.spacing = Metrics.space3 - buttons.setHuggingPriority(.required, for: .horizontal) - buttons.setContentCompressionResistancePriority(.required, for: .horizontal) - let spacer = NSView() - spacer.setContentHuggingPriority(.fittingSizeCompression, for: .horizontal) - let line = NSStackView(views: [icon, titleLabel, factsLabel, spacer, buttons]) - line.orientation = .horizontal - line.alignment = .centerY - line.spacing = Metrics.space2 - line.translatesAutoresizingMaskIntoConstraints = false - addSubview(line) - let inset = Metrics.space3 - NSLayoutConstraint.activate([ - heightAnchor.constraint(equalToConstant: Self.height), - surface.leadingAnchor.constraint(equalTo: leadingAnchor), surface.trailingAnchor.constraint(equalTo: trailingAnchor), - surface.topAnchor.constraint(equalTo: topAnchor), surface.bottomAnchor.constraint(equalTo: bottomAnchor), - icon.widthAnchor.constraint(equalToConstant: Self.iconSide), icon.heightAnchor.constraint(equalToConstant: Self.iconSide), - line.leadingAnchor.constraint(equalTo: leadingAnchor, constant: inset), - line.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -inset), - line.centerYAnchor.constraint(equalTo: centerYAnchor), - ]) - } - - private func follow(_ center: ImportSuggestionsCenter) { - let list = withObservationTracking { - center.list(for: .home) - } onChange: { [weak self, weak center] in - Task { @MainActor in - guard let center else { return } - self?.follow(center) - } - } - show(list) - } - - /// Shows the first card of `list`, or hides the row when it has none. - func show(_ list: ImportCardList) { - guard list != drawn else { return } - drawn = list - defer { onChange() } - guard let first = list.cards.first else { - card = nil - isHidden = true - return - } - card = first - isHidden = false - setAccessibilityLabel(first.title) - icon.image = first.app.flatMap(ImportAppIcons.image) ?? NSImage(systemSymbolName: "square.and.arrow.down", accessibilityDescription: nil) - titleLabel.stringValue = first.title - // A running, paused, finished or failed import says where it is instead of what was found. - factsLabel.stringValue = first.status ?? first.facts - factsLabel.toolTip = first.benefit - for button in buttons.arrangedSubviews { buttons.removeArrangedSubview(button); button.removeFromSuperview() } - let shown = (first.primary.map { [$0] } ?? []) + first.secondary.filter { $0.action != .more && $0.action != .restore } - actions = shown.enumerated().map { index, item in - let button = NSButton(title: item.title, target: self, action: #selector(pressed(_:))) - button.identifier = NSUserInterfaceItemIdentifier(item.action.rawValue) - button.isBordered = false - button.font = index == 0 && first.primary != nil ? Typography.bodyEmphasized : Typography.body - button.setContentCompressionResistancePriority(.required, for: .horizontal) - buttons.addArrangedSubview(button) - return button - } - showAll.isHidden = list.cards.count < 2 - buttons.addArrangedSubview(showAll) - showProgress(first) - applyColors() - needsLayout = true - } - - private func showProgress(_ card: ImportCard) { - progress?.removeFromSuperview() - progress = nil - guard let value = card.progress, card.state == .running || card.state == .paused else { return } - let bar = ImportProgressBar(progress: value) - bar.translatesAutoresizingMaskIntoConstraints = false - addSubview(bar) - let inset = Metrics.space3 - NSLayoutConstraint.activate([ - bar.leadingAnchor.constraint(equalTo: leadingAnchor, constant: inset), - bar.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -inset), - bar.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -3), - ]) - progress = bar - } - - /// The theme's text tiers; the card's own button in the theme's accent. - private func applyColors() { - let (primary, secondary, tertiary, accent) = performWithTheme { - (Palette.textPrimary, Palette.textSecondary, Palette.textTertiary, Palette.accent) - } - icon.contentTintColor = secondary - titleLabel.textColor = primary - factsLabel.textColor = tertiary - for (index, button) in actions.enumerated() { - button.contentTintColor = index == 0 && card?.primary != nil ? accent : secondary - } - showAll.contentTintColor = secondary - } - - @objc private func pressed(_ sender: NSButton) { - guard let id = card?.id, let raw = sender.identifier?.rawValue, let action = ImportCardAction(rawValue: raw) else { return } - press(id, action) - } - - @objc private func openSettings() { openAll() } -} - -/// A running or paused import's share done: a 2 pt track with the accent fill, drawn in one -/// frame (no animation), as the web cards draw it. Not NSProgressIndicator: its bar style is -/// the system's, not the theme's, and the lint keeps it for spinners. -@MainActor -final class ImportProgressBar: NSView { - private let progress: Double - private let fill = CALayer() - - init(progress: Double) { - self.progress = min(1, max(0, progress)) - super.init(frame: .zero) - wantsLayer = true - layer?.cornerRadius = 1 - layer?.masksToBounds = true - layer?.addSublayer(fill) - setAccessibilityElement(true) - setAccessibilityRole(.progressIndicator) - setAccessibilityValue(NSNumber(value: self.progress)) - heightAnchor.constraint(equalToConstant: 2).isActive = true - } - - @available(*, unavailable) - required init?(coder: NSCoder) { nil } - - override var wantsUpdateLayer: Bool { true } - - override func updateLayer() { - let (track, accent) = performWithTheme { (Palette.separator.cgColor, Palette.accent.cgColor) } - layer?.backgroundColor = track - fill.backgroundColor = accent - } - - override func layout() { - super.layout() - CATransaction.begin() - CATransaction.setDisableActions(true) - fill.frame = CGRect(x: 0, y: 0, width: bounds.width * progress, height: bounds.height) - CATransaction.commit() - } -} diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextImportSuggestions/ImportSuggestionStrings.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextImportSuggestions/ImportSuggestionStrings.swift index 178a52c7e7ec..544cef8169cf 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextImportSuggestions/ImportSuggestionStrings.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextImportSuggestions/ImportSuggestionStrings.swift @@ -165,7 +165,6 @@ public nonisolated struct ImportSuggestionStrings: Sendable { String(localized: "importSuggestions.settings.empty", defaultValue: "Nothing to bring over right now. cmux looks again when you come back to it.", table: "ImportSuggestions", bundle: .module) } - public static var showAll: String { String(localized: "importSuggestions.button.showAll", defaultValue: "Show All", table: "ImportSuggestions", bundle: .module) } /// `import.card` start, resume or try again from an agent, a script or another client. public static var personOnly: String { String(localized: "importSuggestions.refusal.personOnly", defaultValue: "Only you can start an import: press its button in cmux.", diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextImportSuggestions/Resources/ImportSuggestions.xcstrings b/Packages/macOS/CmuxNext/Sources/CmuxNextImportSuggestions/Resources/ImportSuggestions.xcstrings index a3d6f4ba2f9b..41af8ef42abf 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextImportSuggestions/Resources/ImportSuggestions.xcstrings +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextImportSuggestions/Resources/ImportSuggestions.xcstrings @@ -3014,137 +3014,6 @@ } } }, - "importSuggestions.button.showAll" : { - "extractionState" : "manual", - "localizations" : { - "ar" : { - "stringUnit" : { - "state" : "translated", - "value" : "عرض الكل" - } - }, - "bs" : { - "stringUnit" : { - "state" : "translated", - "value" : "Prikaži sve" - } - }, - "da" : { - "stringUnit" : { - "state" : "translated", - "value" : "Vis alle" - } - }, - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Alle anzeigen" - } - }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Show All" - } - }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Mostrar todo" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Tout afficher" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Mostra tutto" - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "すべて表示" - } - }, - "km" : { - "stringUnit" : { - "state" : "translated", - "value" : "បង្ហាញទាំងអស់" - } - }, - "ko" : { - "stringUnit" : { - "state" : "translated", - "value" : "모두 보기" - } - }, - "nb" : { - "stringUnit" : { - "state" : "translated", - "value" : "Vis alle" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Pokaż wszystko" - } - }, - "pt-BR" : { - "stringUnit" : { - "state" : "translated", - "value" : "Mostrar tudo" - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Показать все" - } - }, - "th" : { - "stringUnit" : { - "state" : "translated", - "value" : "แสดงทั้งหมด" - } - }, - "tr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Tümünü Göster" - } - }, - "uk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Показати все" - } - }, - "vi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Hiện tất cả" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "全部显示" - } - }, - "zh-Hant" : { - "stringUnit" : { - "state" : "translated", - "value" : "全部顯示" - } - } - } - }, "importSuggestions.button.showChats" : { "extractionState" : "manual", "localizations" : { diff --git a/Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/HomeImportSuggestionRowTests.swift b/Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/HomeImportSuggestionRowTests.swift deleted file mode 100644 index 67f59a46f48d..000000000000 --- a/Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/HomeImportSuggestionRowTests.swift +++ /dev/null @@ -1,136 +0,0 @@ -import AppKit -import CmuxNextDesign -@testable import CmuxNextHome -@testable import CmuxNextImportCards -import CmuxNextImportSuggestions -import Testing - -/// Leo 2026-10-21 on nightly 331e2e9 ("WHAT IS THIS"): Home's import cards ("Bring your stuff to -/// cmux", a Claude Code card and a Codex card) floated over the Chief's transcript at a fixed -/// 80 pt from the column's top, so the chat showed through and around them. The suggestion is -/// now one compact glass row on the Chief's front page (the empty Chief conversation's first-run -/// panel), in the panel's own layout, never over a conversation. -@MainActor @Suite(.serialized) struct HomeImportSuggestionRowTests { - static func card(_ id: String, kind: ImportSourceKind, title: String, facts: String) -> ImportCard { - ImportCard(id: id, kind: kind, app: nil, title: title, facts: facts, benefit: "Your chats show in cmux.", - state: .ready, primary: .init(.start, "Show Chats"), - secondary: [.init(.more, "More Options…"), .init(.dismiss, "Not Now")]) - } - - static let two = ImportCardList(title: "Bring your stuff to cmux", cards: [ - card("claudeCode", kind: .claudeCode, title: "Claude Code", facts: "1,002 sessions"), - card("codex", kind: .codex, title: "Codex", facts: "1,118 threads"), - ]) - - static func row(pressed: @escaping (String, ImportCardAction) -> Void = { _, _ in }, - showAll: @escaping () -> Void = {}) -> ImportSuggestionRowView { - let row = ImportSuggestionRowView(press: pressed, showAll: showAll) - row.frame = NSRect(x: 0, y: 0, width: HomeFirstRunView.maxWidth, height: ImportSuggestionRowView.height) - return row - } - - /// Two suggestions are one row: the first one's name and facts, its button and Not Now, and - /// Show All for the rest. No heading, no second card, no benefit paragraph. - @Test func twoSuggestionsAreOneCompactRow() { - let row = Self.row() - row.show(Self.two) - row.layoutSubtreeIfNeeded() - #expect(!row.isHidden) - #expect(row.fittingSize.height <= ImportSuggestionRowView.height) - #expect(row.titleLabel.stringValue == "Claude Code") - #expect(row.factsLabel.stringValue == "1,002 sessions") - #expect(row.actions.map(\.title) == ["Show Chats", "Not Now"]) - #expect(!row.showAll.isHidden) - let texts = row.descendantTexts - #expect(!texts.contains { $0.contains("Codex") || $0.contains("Bring your stuff") || $0.contains("Your chats") }) - } - - /// One suggestion has nothing more to show: no Show All. - @Test func oneSuggestionHasNoShowAll() { - let row = Self.row() - row.show(ImportCardList(title: Self.two.title, cards: [Self.two.cards[0]])) - #expect(row.showAll.isHidden) - } - - /// Not Now on the row dismisses the suggestion it shows; its button starts it; Show All opens - /// Settings › Import. - @Test func theRowsButtonsRunTheShownSuggestionsActions() { - var pressed: [String] = [] - var settings = 0 - let row = Self.row(pressed: { pressed.append("\($0):\($1.rawValue)") }, showAll: { settings += 1 }) - row.show(Self.two) - for button in row.actions { button.performClick(nil) } - row.showAll.performClick(nil) - #expect(pressed == ["claudeCode:start", "claudeCode:dismiss"]) - #expect(settings == 1) - } - - /// Nothing to suggest: the row is gone and says it changed, so its panel lays out without it. - @Test func nothingToSuggestHidesTheRow() { - let row = Self.row() - var changes = 0 - row.onChange = { changes += 1 } - row.show(Self.two) - row.show(.empty) - #expect(row.isHidden) - #expect(changes == 2) - } - - /// The row is the sidebar cards' glass veil, never an opaque card. - @Test func theRowIsGlass() { - let row = Self.row() - #expect(row.surface is CardSurfaceView) - } - - /// The Chief's front page holds the row in its own column, under the action rows and inside - /// the panel: nothing floats over the conversation's top. - @Test func theFrontPageHoldsTheRowUnderItsActions() { - let panel = HomeFirstRunView(frame: NSRect(x: 0, y: 0, width: 600, height: 700)) - let row = Self.row() - row.show(Self.two) - panel.accessory = row - panel.layoutSubtreeIfNeeded() - #expect(row.superview != nil) - #expect(row.isDescendant(of: panel)) - let place = panel.convert(row.bounds, from: row) - let pet = panel.convert(panel.pet.bounds, from: panel.pet) - #expect(place.minY >= pet.maxY) - #expect(panel.bounds.contains(place)) - #expect(abs(place.width - pet.width) < 1) - #expect(abs(place.minX - pet.minX) < 1) - } - - /// A hidden row leaves no gap: the panel is laid out as with no row at all. - @Test func aHiddenRowTakesNoSpace() { - let plain = HomeFirstRunView(frame: NSRect(x: 0, y: 0, width: 600, height: 700)) - plain.layoutSubtreeIfNeeded() - let panel = HomeFirstRunView(frame: NSRect(x: 0, y: 0, width: 600, height: 700)) - let row = Self.row() - row.show(.empty) - panel.accessory = row - panel.layoutSubtreeIfNeeded() - let top = { (view: HomeFirstRunView) in view.convert(view.agent.bounds, from: view.agent).minY } - #expect(abs(top(plain) - top(panel)) < 1) - } - - /// The row goes when the host takes it away. - @Test func clearingTheAccessoryRemovesTheRow() { - let panel = HomeFirstRunView(frame: NSRect(x: 0, y: 0, width: 600, height: 700)) - let row = Self.row() - row.show(Self.two) - panel.accessory = row - panel.accessory = nil - #expect(row.superview == nil) - } -} - -extension NSView { - /// Every label's and button's text under this view. - fileprivate var descendantTexts: [String] { - var texts: [String] = [] - if let field = self as? NSTextField { texts.append(field.stringValue) } - if let button = self as? NSButton, !button.isHidden { texts.append(button.title) } - for child in subviews where !child.isHidden { texts += child.descendantTexts } - return texts - } -} From 4bea78b8ca5ec9e5b9209ad7ea856a23d4849bf5 Mon Sep 17 00:00:00 2001 From: Leo Li Date: Thu, 22 Oct 2026 00:46:27 -0700 Subject: [PATCH 3/4] Test: the New Tab page gets no import cards Red on base: the agent page still has the importCards.act op and decodes it into .importCard. Co-Authored-By: Claude Opus 5.5 Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a16057-0fea-7933-80f2-cec81447d873&host=cosmic-gray --- .../ImportSuggestionRemovedTests.swift | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/ImportSuggestionRemovedTests.swift b/Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/ImportSuggestionRemovedTests.swift index 9106c8ef4116..1b611e7e8071 100644 --- a/Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/ImportSuggestionRemovedTests.swift +++ b/Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/ImportSuggestionRemovedTests.swift @@ -1,5 +1,6 @@ import AppKit import CmuxNextActions +@testable import CmuxNextAgentPane @testable import CmuxNextApp import CmuxHomeCore @testable import CmuxNextHome @@ -9,8 +10,8 @@ import Testing /// Leo 2026-10-21: "yeah the bring your stuff to cmux is extrmeely questionable idk why that's /// there + why they went to the chief thingy", and "yes to both" to removing it from the New Tab -/// and the Chief completely. The import suggestion is gone from the Chief's front page; importing -/// lives in Settings › Import and the palette's Import from Other Apps…. +/// and the Chief completely. The import suggestion is gone from the Chief's front page and the New +/// Tab page; importing lives in Settings › Import and the palette's Import from Other Apps…. @MainActor @Suite(.serialized) struct ImportSuggestionRemovedTests { /// The empty Chief conversation's front page holds no import suggestion row and no import card, /// shown or hidden. @@ -26,6 +27,14 @@ import Testing #expect(!page.contains { $0.texts.contains { $0.localizedCaseInsensitiveContains("Bring your stuff") } }) } + /// The New Tab page gets no import cards: the agent page has no `importCards.act` op, and a + /// page that still posts one is refused like any unknown request. + @Test func theNewTabHasNoImportCards() { + #expect(!AgentPageOps.all.contains(AgentPageOps.namespace + "importCards.act")) + #expect(AgentPaneRequest(body: ["method": "importCards.act", "params": ["id": "agents.codex", "action": "start"]]) + == .unsupported("importCards.act")) + } + /// Import stays in the command palette: Import from Other Apps… is a palette action, and it /// opens Settings › Import. @Test func thePalettesImportCommandStillOpensSettingsImport() throws { From ca9f86ffacd70dd6bdc74e360e07ac1c44755be5 Mon Sep 17 00:00:00 2001 From: Leo Li Date: Thu, 22 Oct 2026 00:46:44 -0700 Subject: [PATCH 4/4] cmux NEXT: no import suggestion cards on the New Tab page MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Leo 2026-10-21: "yeah the bring your stuff to cmux is extrmeely questionable idk why that's there + why they went to the chief thingy", then "yes to both" to removing it from the New Tab and the Chief completely, with import only in Settings and the command palette. Removes the New Tab's import cards: AgentPaneImportCards, the importCards page push and the importCards.act op, NewTabImportCards and its store, and ImportSuggestionsCenter's push to the pages. With Home's row gone (the previous PR) the deck has one list, Settings › Import's: ImportCardDeck's home/newTab surfaces, their caps and the 24 h linger go, and so do the "Bring your stuff to cmux" title, the ImportCards row layout and the gallery's New Tab variants. CmuxNextImportCards no longer depends on CmuxNextAgentPane. Kept: the scan, the jobs, Settings › Import and the import.* actions, including the palette's Import from Other Apps… (import.open). Co-Authored-By: Claude Opus 5.5 Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a16057-0fea-7933-80f2-cec81447d873&host=cosmic-gray --- Packages/macOS/CmuxNext/Package.swift | 6 +- .../Catalog/ImportActionCatalog.swift | 2 +- .../CmuxNextAgentPane/AgentPageProvider.swift | 2 +- .../CmuxNextAgentPane/AgentPaneBridge.swift | 1 - .../AgentPaneImportCards.swift | 71 ---------- .../CmuxNextAgentPane/AgentPaneModel.swift | 1 - .../CmuxNextAgentPane/AgentPanePageHost.swift | 1 - .../AgentPaneRequest+Decoding.swift | 2 - .../AgentPaneRequest+SafeDescription.swift | 1 - .../CmuxNextAgentPane/AgentPaneRequest.swift | 2 - .../Handlers/OnboardingHandlers.swift | 2 +- .../CmuxNextImportCards/ImportCardJSON.swift | 6 +- .../ImportCardsActions.swift | 2 +- .../ImportSuggestionsCenter.swift | 33 ++--- .../ImportCard.swift | 3 +- .../ImportCardDeck.swift | 52 ++----- .../ImportSuggestionStrings.swift | 1 - .../Resources/ImportSuggestions.xcstrings | 131 ------------------ .../Model/BrowserImportTunables.swift | 6 +- .../AgentPaneImportCardsTests.swift | 37 ----- .../ClassicMigrationTests.swift | 2 +- .../ImportSuggestionsCenterClassicTests.swift | 2 +- .../ImportCardDeckTests.swift | 78 +++++------ webviews/src/agent-session/acpmux/App.tsx | 16 +-- .../acpmux/NewTabImportCards.tsx | 14 -- .../src/agent-session/acpmux/importCards.ts | 29 ---- webviews/src/agent-session/acpmux/pageHost.ts | 4 - webviews/src/dev-host/host.ts | 1 - .../components/ImportSuggestionsCard.tsx | 6 +- webviews/src/ui/ImportCards.gallery.tsx | 50 +++---- webviews/src/ui/ImportCards.tsx | 10 +- webviews/src/ui/importCards.css | 10 +- webviews/test/gallery-coverage.allowlist.json | 1 - 33 files changed, 101 insertions(+), 484 deletions(-) delete mode 100644 Packages/macOS/CmuxNext/Sources/CmuxNextAgentPane/AgentPaneImportCards.swift delete mode 100644 Packages/macOS/CmuxNext/Tests/CmuxNextAgentPaneTests/AgentPaneImportCardsTests.swift delete mode 100644 webviews/src/agent-session/acpmux/NewTabImportCards.tsx delete mode 100644 webviews/src/agent-session/acpmux/importCards.ts diff --git a/Packages/macOS/CmuxNext/Package.swift b/Packages/macOS/CmuxNext/Package.swift index 0e2d2dcec48f..c21cef88a231 100644 --- a/Packages/macOS/CmuxNext/Package.swift +++ b/Packages/macOS/CmuxNext/Package.swift @@ -452,11 +452,11 @@ let package = Package( dependencies: ["CmuxNextImportSuggestions", "CmuxNextBrowserImport"], swiftSettings: daemonSwiftSettings ), - // The cards' center and surfaces: the scan schedule, the job runner, the `import.*` - // actions and the agent pages' push. The App supplies `ImportCardsHost`. + // 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`. .target( name: "CmuxNextImportCards", - dependencies: ["CmuxNextActions", "CmuxNextAgentPane", "CmuxNextBrowserImport", "CmuxNextCompat", "CmuxNextDesign", + dependencies: ["CmuxNextActions", "CmuxNextBrowserImport", "CmuxNextCompat", "CmuxNextDesign", "CmuxNextImportSuggestions", "CmuxNextOnboarding", "CmuxNextSettings"], swiftSettings: uiSwiftSettings ), diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextActions/Catalog/ImportActionCatalog.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextActions/Catalog/ImportActionCatalog.swift index 4d1415eeae97..62c7d44db83a 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextActions/Catalog/ImportActionCatalog.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextActions/Catalog/ImportActionCatalog.swift @@ -1,5 +1,5 @@ // Import suggestion cards (CmuxNextImportCards): what this Mac has that cmux can bring over, -// on Home, New Tab and Settings › Import. Titles live in ImportActions.xcstrings. The CLI and +// listed in Settings › Import (never on Home or New Tab). Titles live in ImportActions.xcstrings. The CLI and // agents list the cards, press a card's button, and raise a card for a site another browser is // signed into (`cmux import suggest --browser chrome --site github.com --reason …`): a raised card // reads nothing until the person clicks Import on it. diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextAgentPane/AgentPageProvider.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextAgentPane/AgentPageProvider.swift index 09cc7962f8d7..8134f5f50362 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextAgentPane/AgentPageProvider.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextAgentPane/AgentPageProvider.swift @@ -33,7 +33,7 @@ public nonisolated struct AgentPageOps { var methods = Dictionary(uniqueKeysWithValues: [ "pane.checkpointAvailability", "pane.framePacing", "pane.painted", "pane.renderRate", "pane.saveLog", "pane.saveFile", "pane.showContextUsage", "pane.showDictation", "pane.setDisplay", "pane.edit", "pane.setQuickActions", "tab.open", "run.attach", "tab.typeAhead", "tab.jump", - "newTab.remember", "newTab.setTemplate", "newTab.editWidgets", "nowPlaying.command", "importCards.act", "newTab.inputReady", "newTab.touched", "newTab.context", "newTab.warm", "shortcut.edit", "action.run", "file.open", "browser.open", + "newTab.remember", "newTab.setTemplate", "newTab.editWidgets", "nowPlaying.command", "newTab.inputReady", "newTab.touched", "newTab.context", "newTab.warm", "shortcut.edit", "action.run", "file.open", "browser.open", "project.list", "project.icon", "project.browse", "workspace.chooseFolder", "workspace.useFolder", "chat.folder.choose", "chat.attach.choose", "chat.attach.dropped", "app.action", "chats.open", "chats.openInTerminal", "chats.watch", "chats.page", "quick.dismiss", "quick.openInWindow", "quick.startInBackground", "quick.setOnTop", "quick.setCollapsed", "quick.floatingState", "quick.context", "pane.action", "pane.tabState", "pane.tabRowTools", "chat.toolCard.respond", "chat.upload", "chat.sideChat", "chat.subagentPane", "chat.processTerminal", "chat.openTab", "chat.offer.open", "chat.asks.open", "chat.newHere", "chat.unsentDraft", "chat.scrollAnchor", "chat.scrollAnchor.get", "chat.tail", "chat.tail.get", "chat.draftAttachments", "chat.draftAttachments.get", "chat.asks.pending", "shell.run", "shell.read", "shell.stop", "harness.update", "harness.restart", diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextAgentPane/AgentPaneBridge.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextAgentPane/AgentPaneBridge.swift index f0fe9a396140..c20e397250ad 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextAgentPane/AgentPaneBridge.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextAgentPane/AgentPaneBridge.swift @@ -95,7 +95,6 @@ final class AgentPaneBridge: NSObject, WKScriptMessageHandlerWithReply { AgentPaneDeviceChat.push(view.deviceChats, to: view) AgentPaneClosedItem.push(view.recentlyClosed, to: view) AgentPaneNowPlaying.shared.push(to: view) - AgentPaneImportCards.shared.push(to: view) } return view.model } diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextAgentPane/AgentPaneImportCards.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextAgentPane/AgentPaneImportCards.swift deleted file mode 100644 index 530ed5bdcfde..000000000000 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextAgentPane/AgentPaneImportCards.swift +++ /dev/null @@ -1,71 +0,0 @@ -public import CmuxNextSettings -public import Foundation - -/// The import suggestion cards on the New Tab page: what this Mac has that cmux can bring -/// over (CmuxNextImportCards' `ImportSuggestionsCenter` sets ``cards``) and a card's button -/// back to it. App-wide like ``AgentPaneNowPlaying``: each page gets the cards at load and -/// handshake and again on every change. Not an AgentPaneView extension: that type is at its size limit. -@MainActor -public final class AgentPaneImportCards { - public static let shared = AgentPaneImportCards() - - /// One deck; the App uses `shared`, a test its own. - init() {} - - /// The New Tab list as the page draws it (`{title, cards: [...]}`, every text already - /// localized); null when there is nothing to show or the cards are off. - public var cards: JSONValue = .null { - didSet { - guard cards != oldValue else { return } - for view in views.allObjects { send(to: view) } - } - } - /// Runs a card's button (`importCards.act {id, action}`). - public var onAction: ((_ id: String, _ action: String) -> Void)? - private let views = NSHashTable.weakObjects() - - /// A page's button: a card id and one of the card actions, a short lowercase ASCII word. - public nonisolated struct Act: Hashable, Sendable { - public var id: String - public var action: String - - init?(_ params: [String: Any]?) { - guard let id = params?["id"] as? String, let action = params?["action"] as? String, - (1...200).contains(id.count), (1...16).contains(action.count), - action.unicodeScalars.allSatisfy({ (97...122).contains($0.value) }) else { return nil } - self.id = id - self.action = action - } - } - - /// The cards, read now, as a page host event; `view` then also gets every change. - func event(for view: AgentPaneView) -> AgentPageEvent { - views.add(view) - return .importCards(cards) - } - - /// Pushes the cards, read now, to `view`'s page; `view` then also gets every change. - func push(to view: AgentPaneView) { - views.add(view) - send(to: view) - } - - private func send(to view: AgentPaneView) { - let event = AgentPageEvent.importCards(cards) - view.deliver([event], scripts: ["window.cmuxAcpmuxBridge?.applyImportCards?.(\(event.value.compactText));"]) - } - - /// Runs a page's button; false when the App has not wired the cards. - func perform(_ act: Act) -> Bool { - guard let onAction else { return false } - onAction(act.id, act.action) - return true - } -} - -extension AgentPageEvent { - /// The New Tab page's import cards (`importCards`); null while there are none. - static func importCards(_ cards: JSONValue) -> AgentPageEvent { - AgentPageEvent(kind: "importCards", value: cards) - } -} diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextAgentPane/AgentPaneModel.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextAgentPane/AgentPaneModel.swift index dd6b9dbbc264..776d427a1b7f 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextAgentPane/AgentPaneModel.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextAgentPane/AgentPaneModel.swift @@ -306,7 +306,6 @@ public final class AgentPaneModel { case .rememberNewTab(let pick): guard let onRememberNewTab else { return Self.unsupported("newTab.remember") }; onRememberNewTab(pick); return AgentPaneReply.success() case .editNewTab(let edit): guard let onEditNewTab else { return Self.unsupported(edit.method) }; onEditNewTab(edit); return AgentPaneReply.success() case .nowPlaying(let command): return AgentPaneNowPlaying.shared.perform(command) ? AgentPaneReply.success() : Self.unsupported("nowPlaying.command") - case .importCard(let act): return AgentPaneImportCards.shared.perform(act) ? AgentPaneReply.success() : Self.unsupported("importCards.act") case .runAction(let id): return runAction(id) case .jump(let target, let id): guard newTab != nil, let onJump else { return Self.unsupported("tab.jump") }; onJump(target, id); return AgentPaneReply.success() case .chooseFolder where remoteChat: return Self.unsupported("workspace.chooseFolder") diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextAgentPane/AgentPanePageHost.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextAgentPane/AgentPanePageHost.swift index efb46fa10816..b8f9f6c0dfc2 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextAgentPane/AgentPanePageHost.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextAgentPane/AgentPanePageHost.swift @@ -33,7 +33,6 @@ enum AgentPanePageHost { events.append(.deviceChats(AgentPaneDeviceChat.shown(view.deviceChats, in: view))) events.append(view.recentlyClosed.event) events.append(AgentPaneNowPlaying.shared.event(for: view)) - events.append(AgentPaneImportCards.shared.event(for: view)) events.append(.editedFiles(view.editedFiles)) events.append(.composer(view.model.composer)) events.append(.sidebarPreviewVariant(view.model.sidebarPreviewVariant)) diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextAgentPane/AgentPaneRequest+Decoding.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextAgentPane/AgentPaneRequest+Decoding.swift index 8c05656cfffd..d2baddee9c13 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextAgentPane/AgentPaneRequest+Decoding.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextAgentPane/AgentPaneRequest+Decoding.swift @@ -182,8 +182,6 @@ extension AgentPaneRequest { self = AgentPaneNewTabEdit(widgetParams: params).map(AgentPaneRequest.editNewTab) ?? .unsupported(method) case "nowPlaying.command": if let command = AgentPaneNowPlaying.Command(params) { self = .nowPlaying(command) } else { self = .unsupported(method) } - case "importCards.act": - if let act = AgentPaneImportCards.Act(params) { self = .importCard(act) } else { self = .unsupported(method) } case "newTab.remember": // One input (R86): the agent pick, and the model picked on it in `folder`. if let agent = params?["agent"] as? String, !agent.isEmpty, agent.count <= 128 { diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextAgentPane/AgentPaneRequest+SafeDescription.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextAgentPane/AgentPaneRequest+SafeDescription.swift index a4e2815451ed..5ec50e99f9de 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextAgentPane/AgentPaneRequest+SafeDescription.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextAgentPane/AgentPaneRequest+SafeDescription.swift @@ -46,7 +46,6 @@ extension AgentPaneRequest: CustomStringConvertible, CustomDebugStringConvertibl case .listProjects: "listProjects" case .projectIcon: "projectIcon" case .nowPlaying: "nowPlaying" - case .importCard: "importCard" case .openChat: "openChat" case .openChatInTerminal: "openChatInTerminal" case .watchChat: "watchChat" diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextAgentPane/AgentPaneRequest.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextAgentPane/AgentPaneRequest.swift index 5fd74cc6f75d..5cb4fe1b8402 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextAgentPane/AgentPaneRequest.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextAgentPane/AgentPaneRequest.swift @@ -62,8 +62,6 @@ public nonisolated enum AgentPaneRequest: Equatable, Sendable { case editNewTab(AgentPaneNewTabEdit) /// The New Tab page player's control (`nowPlaying.command {command, seconds}`). case nowPlaying(AgentPaneNowPlaying.Command) - /// An import card's button on the New Tab page (`importCards.act {id, action}`). - case importCard(AgentPaneImportCards.Act) /// The new tab page got its first user input (`newTab.touched`); a /// touched page is never recycled into the prewarm pool. case touched diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Handlers/OnboardingHandlers.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Handlers/OnboardingHandlers.swift index e0b7af6c3836..2507d49735f5 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Handlers/OnboardingHandlers.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Handlers/OnboardingHandlers.swift @@ -54,7 +54,7 @@ enum OnboardingHandlers { track: { registry.track($0) }, destination: ChromiumCookieDestination { writes, profile in try await cef.importCookies(writes, into: profile) })) registry.bind("browser.importCookies", run: { invocation in try cookieImports.run(invocation) }) - // Import suggestion cards (Home, New Tab, Settings › Import): the same import run as the window's. + // Import suggestion cards (Settings › Import only): the same import run as the window's. ImportSuggestionsCenter.shared.bind(into: registry, host: importCardsHost(services)) // Every entrypoint previews the exact snapshot and waits for the // person's answer before any browser, SSH host or agent is restored. diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextImportCards/ImportCardJSON.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextImportCards/ImportCardJSON.swift index dc6717c1afe6..8afac1f0457d 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextImportCards/ImportCardJSON.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextImportCards/ImportCardJSON.swift @@ -2,9 +2,9 @@ import AppKit import CmuxNextImportSuggestions import CmuxNextSettings -/// A card list as the web surfaces read it (New Tab's `importCards` push, Settings' host lists -/// `import_suggestions`, `import.list`): the Codable cards, each with its source app's icon as -/// a small `data:image/png` URL (a cmux page's CSP allows `img-src data:` only). +/// A card list as the web surfaces read it (Settings' host lists `import_suggestions`, +/// `import.list`): the Codable cards, each with its source app's icon as a small +/// `data:image/png` URL (a cmux page's CSP allows `img-src data:` only). @MainActor struct ImportCardJSON { /// Null for an empty list: the surface draws nothing and keeps no room. diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextImportCards/ImportCardsActions.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextImportCards/ImportCardsActions.swift index 26a555254a15..ac6ddf7e42cb 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextImportCards/ImportCardsActions.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextImportCards/ImportCardsActions.swift @@ -13,7 +13,7 @@ extension ImportSuggestionsCenter { // Every card Settings › Import lists, as JSON text (`cmux import list`). registry.bind("import.list", unavailable: refusedWhileOff, invoke: { [weak self, weak registry] _ in guard let self, let registry else { return } - let text = ImportCardJSON.text(list(for: .settings)) + let text = ImportCardJSON.text(list()) registry.track(result: Task { .success(["cards": .string(text)]) }) }) registry.bind("import.card", unavailable: refusedWhileOff, invoke: { [weak self, weak registry] invocation in diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextImportCards/ImportSuggestionsCenter.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextImportCards/ImportSuggestionsCenter.swift index 5aa45335b369..28ce7b6e07e6 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextImportCards/ImportSuggestionsCenter.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextImportCards/ImportSuggestionsCenter.swift @@ -1,6 +1,5 @@ public import AppKit public import CmuxNextActions -import CmuxNextAgentPane public import CmuxNextBrowserImport import CmuxNextCompat import CmuxNextDesign @@ -14,8 +13,8 @@ import Observation /// ``rescanInterval``), keeps the person's dismissals and the Chief's /// requests, and runs each card's import as a job saved after every step, so /// a quit or crash leaves it paused for Resume (or resumes it at launch when -/// no prompt is left). Home, New Tab (through `AgentPaneImportCards`) and -/// Settings › Import draw what ``list(for:)`` returns. +/// no prompt is left). Settings › Import draws what ``list()`` returns; Home and +/// New Tab show no suggestion (Leo 2026-10-21: "yes to both", removing it from both). @MainActor @Observable public final class ImportSuggestionsCenter { @@ -71,13 +70,6 @@ public final class ImportSuggestionsCenter { started = true restoreJobs() state.expire(now: now()) - AgentPaneImportCards.shared.onAction = { [weak self] id, action in - guard let action = ImportCardAction(rawValue: action) else { return } - // A New Tab page's button: a first-party control, never the person's own gesture, so a - // start asks natively first and a cookie request still gets its consent panel. - try? self?.perform(id, action, caller: ActionInvocation(arguments: [:], origin: .page)) - } - follow() activation = NotificationCenter.default.addObserver(forName: NSApplication.didBecomeActiveNotification, object: nil, queue: .main) { [weak self] _ in Task { @MainActor in self?.rescanIfStale() } @@ -149,16 +141,16 @@ public final class ImportSuggestionsCenter { "terminal.iterm2": "com.googlecode.iterm2", "terminal.ghostty": "com.mitchellh.ghostty", "cmux.classic": "com.cmuxterm.app", ] - /// The cards `surface` draws now; empty while the cards are off. - public func list(for surface: ImportCardDeck.Surface) -> ImportCardList { + /// The cards Settings › Import lists now; empty while the cards are off. + public func list() -> ImportCardList { guard isEnabled else { return .empty } - return deck.list(for: surface, now: now()) + return deck.list() } /// Settings › Import's list for the page's host lists (`import_suggestions`), with its empty text; null while the cards are off. public var settingsJSON: JSONValue { guard isEnabled else { return .null } - var value = ImportCardJSON.value(list(for: .settings), keepEmpty: true) + var value = ImportCardJSON.value(list(), keepEmpty: true) if case .object(var members) = value { members["empty"] = .string(ImportSuggestionStrings.settingsEmpty) members["scanning"] = .bool(isScanning) @@ -167,18 +159,9 @@ public final class ImportSuggestionsCenter { return value } - /// Pushes the New Tab cards to every agent page on each change of what they show. - private func follow() { - withObservationTracking { - AgentPaneImportCards.shared.cards = ImportCardJSON.value(list(for: .newTab)) - } onChange: { [weak self] in - Task { @MainActor in self?.follow() } - } - } - // MARK: Buttons - /// Runs a card's button (Home's row, a page, `import.card`). `caller` is the run that + /// Runs a card's button (Settings › Import's page, `import.card`). `caller` is the run that /// pressed it, the person's own click when nil. Only that click starts an import at once: any /// other caller's Import, Resume or Try Again asks the person in a native dialog first (page /// script cannot answer it), and an agent's cookie request always gets @@ -252,7 +235,7 @@ public final class ImportSuggestionsCenter { } else { guard let job = ledger.job(id), job.phase != .running else { throw ImportCardsFailure.noSuchCard(id) } } - guard let card = deck.cards(for: .settings, now: now()).first(where: { $0.id == id }), card.kind.startReadsAnotherApp else { return nil } + guard let card = deck.cards().first(where: { $0.id == id }), card.kind.startReadsAnotherApp else { return nil } let confirm = CmuxDialogButton(id: "confirm", title: ImportSuggestionStrings.confirmButton(action), role: .default) return CmuxDialogSpec(title: ImportSuggestionStrings.confirmTitle(card.title), lines: [card.facts, card.benefit].filter { !$0.isEmpty }, buttons: [.cancel(), confirm], identifier: "cmux.dialog.importCard") diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextImportSuggestions/ImportCard.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextImportSuggestions/ImportCard.swift index ad8fc2ccb99e..145d8b845c42 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextImportSuggestions/ImportCard.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextImportSuggestions/ImportCard.swift @@ -33,8 +33,7 @@ extension ImportSourceKind { nonisolated public var startReadsAnotherApp: Bool { self == .browser || self == .iterm2 } } -/// One card as every surface draws it (Home, New Tab, Settings › Import): -/// every text is already in the person's language. +/// One card as Settings › Import draws it: every text is already in the person's language. public nonisolated struct ImportCard: Codable, Sendable, Equatable, Identifiable { public nonisolated enum State: String, Codable, Sendable { case ready, running, paused, done, failed diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextImportSuggestions/ImportCardDeck.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextImportSuggestions/ImportCardDeck.swift index 6981045e3cfa..9ab97a66f407 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextImportSuggestions/ImportCardDeck.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextImportSuggestions/ImportCardDeck.swift @@ -1,29 +1,10 @@ public import CmuxNextBrowserImport public import Foundation -/// Which cards a surface shows, from what the scan found, what the person -/// dismissed, the Chief's requests and the jobs. Pure, so every rule here is -/// a unit test. +/// The cards Settings › Import lists, from what the scan found, what the person +/// dismissed, the Chief's requests and the jobs: everything found, dismissed ones +/// too. Pure, so every rule here is a unit test. Home and New Tab show no cards. public nonisolated struct ImportCardDeck: Sendable { - public nonisolated enum Surface: String, Sendable, CaseIterable { - /// Home and New Tab: only what one click can import now, a few at a time. - case home, newTab - /// Settings › Import: everything found, dismissed ones too. - case settings - - /// The most cards the surface draws. - var limit: Int { - switch self { - case .home: 4 - case .newTab: 3 - case .settings: .max - } - } - } - - /// A finished card stays on Home and New Tab this long, then only Settings lists it. - public static let doneLingers: TimeInterval = 24 * 3600 - public var findings: [ImportFinding] public var state: ImportSuggestionState public var jobs: ImportJobLedger @@ -37,12 +18,11 @@ public nonisolated struct ImportCardDeck: Sendable { self.apps = apps } - public func list(for surface: Surface, now: Date) -> ImportCardList { - ImportCardList(title: surface == .settings ? ImportSuggestionStrings.settingsTitle : ImportSuggestionStrings.listTitle, - cards: cards(for: surface, now: now)) + public func list() -> ImportCardList { + ImportCardList(title: ImportSuggestionStrings.settingsTitle, cards: cards()) } - public func cards(for surface: Surface, now: Date) -> [ImportCard] { + public func cards() -> [ImportCard] { var cards: [ImportCard] = [] var placed = Set() // The Chief's requests lead: they answer something the person just ran into. @@ -54,19 +34,13 @@ public nonisolated struct ImportCardDeck: Sendable { let order: [ImportJob.Phase: Int] = [.running: 0, .paused: 1, .failed: 2, .done: 3] for job in jobs.jobs.sorted(by: { (order[$0.phase] ?? 9, $1.updatedAt) < (order[$1.phase] ?? 9, $0.updatedAt) }) { guard !placed.contains(job.id) else { continue } - // A finished job past its linger leaves Home and New Tab, and so does its source's card. placed.insert(job.id) - if surface != .settings, job.phase == .done, now.timeIntervalSince(job.updatedAt) > Self.doneLingers { continue } - // A classic run that brought nothing has nothing to confirm there; Settings keeps it. - if surface != .settings, job.phase == .done, job.opened == 0 { continue } - cards.append(card(for: job, finding: findings.first { $0.id == job.id }, surface: surface)) + cards.append(card(for: job, finding: findings.first { $0.id == job.id })) } for finding in findings where !placed.contains(finding.id) { - let dismissed = state.hides(finding) - if surface != .settings, dismissed || finding.access != .ready || finding.offersNothingNew { continue } - cards.append(card(for: finding, dismissed: dismissed, surface: surface)) + cards.append(card(for: finding, dismissed: state.hides(finding))) } - return Array(cards.prefix(surface.limit)) + return cards } // MARK: Cards @@ -118,7 +92,7 @@ public nonisolated struct ImportCardDeck: Sendable { } } - func card(for finding: ImportFinding, dismissed: Bool, surface: Surface) -> ImportCard { + func card(for finding: ImportFinding, dismissed: Bool) -> ImportCard { var primary: ImportCard.Button? = finding.offersNothingNew ? .init(.start, ImportSuggestionStrings.importAgain) : Self.startButton(finding.kind) var secondary: [ImportCard.Button] = [] var state = ImportCard.State.ready @@ -131,7 +105,7 @@ public nonisolated struct ImportCardDeck: Sendable { if finding.kind == .browser, finding.access == .ready { secondary.append(.init(.more, ImportSuggestionStrings.more)) } if dismissed { secondary.append(.init(.restore, ImportSuggestionStrings.showAgain)) - } else if finding.access != .inUse || surface != .settings { + } else if finding.access != .inUse { secondary.append(.init(.dismiss, ImportSuggestionStrings.notNow)) } return ImportCard(id: finding.id, kind: finding.kind, app: app(finding), title: finding.name, facts: facts(finding), @@ -147,7 +121,7 @@ public nonisolated struct ImportCardDeck: Sendable { primary: .init(.start, ImportSuggestionStrings.importButton), secondary: [.init(.dismiss, ImportSuggestionStrings.notNow)]) } - func card(for job: ImportJob, finding: ImportFinding?, surface: Surface) -> ImportCard { + func card(for job: ImportJob, finding: ImportFinding?) -> ImportCard { let name = finding?.name ?? job.browser?.displayName ?? job.kind.displayName let title = job.site.map { ImportSuggestionStrings.requestTitle(browser: name, site: $0) } ?? name let factsText = finding.map { self.facts($0) } ?? "" @@ -180,7 +154,7 @@ public nonisolated struct ImportCardDeck: Sendable { case .claudeCode, .codex: card.primary = .init(.open, ImportSuggestionStrings.showChats) 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 = surface == .settings && finding != nil ? .init(.start, ImportSuggestionStrings.importAgain) : nil + case .cmuxClassic: card.primary = finding != nil ? .init(.start, ImportSuggestionStrings.importAgain) : nil case .browser, .ghostty: card.primary = nil } card.secondary = [.init(.hide, ImportSuggestionStrings.done)] diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextImportSuggestions/ImportSuggestionStrings.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextImportSuggestions/ImportSuggestionStrings.swift index 544cef8169cf..3490a6a700c9 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextImportSuggestions/ImportSuggestionStrings.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextImportSuggestions/ImportSuggestionStrings.swift @@ -5,7 +5,6 @@ import Foundation public nonisolated struct ImportSuggestionStrings: Sendable { public init() {} - public static var listTitle: String { String(localized: "importSuggestions.list.title", defaultValue: "Bring your stuff to cmux", table: "ImportSuggestions", bundle: .module) } public static var settingsTitle: String { String(localized: "importSuggestions.settings.title", defaultValue: "Found on This Mac", table: "ImportSuggestions", bundle: .module) } // MARK: Facts diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextImportSuggestions/Resources/ImportSuggestions.xcstrings b/Packages/macOS/CmuxNext/Sources/CmuxNextImportSuggestions/Resources/ImportSuggestions.xcstrings index 41af8ef42abf..a0100a9e84f2 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextImportSuggestions/Resources/ImportSuggestions.xcstrings +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextImportSuggestions/Resources/ImportSuggestions.xcstrings @@ -7504,137 +7504,6 @@ } } }, - "importSuggestions.list.title" : { - "extractionState" : "manual", - "localizations" : { - "ar" : { - "stringUnit" : { - "state" : "translated", - "value" : "انقل أغراضك إلى cmux" - } - }, - "bs" : { - "stringUnit" : { - "state" : "translated", - "value" : "Prenesite svoje stvari u cmux" - } - }, - "da" : { - "stringUnit" : { - "state" : "translated", - "value" : "Tag dine ting med til cmux" - } - }, - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Bring deine Sachen zu cmux" - } - }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Bring your stuff to cmux" - } - }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Trae tus cosas a cmux" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Apportez vos affaires dans cmux" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Porta le tue cose in cmux" - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "cmux に持ってくる" - } - }, - "km" : { - "stringUnit" : { - "state" : "translated", - "value" : "នាំយករបស់របររបស់អ្នកមក cmux" - } - }, - "ko" : { - "stringUnit" : { - "state" : "translated", - "value" : "cmux로 가져오기" - } - }, - "nb" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ta med deg tingene dine til cmux" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Przenieś swoje rzeczy do cmux" - } - }, - "pt-BR" : { - "stringUnit" : { - "state" : "translated", - "value" : "Traga suas coisas para o cmux" - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Перенесите свои данные в cmux" - } - }, - "th" : { - "stringUnit" : { - "state" : "translated", - "value" : "นำข้อมูลของคุณมาที่ cmux" - } - }, - "tr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Verilerinizi cmux'a getirin" - } - }, - "uk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Перенесіть свої дані в cmux" - } - }, - "vi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Mang dữ liệu của bạn sang cmux" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "把你的东西带到 cmux" - } - }, - "zh-Hant" : { - "stringUnit" : { - "state" : "translated", - "value" : "把你的東西帶到 cmux" - } - } - } - }, "importSuggestions.refusal.invalidSite" : { "extractionState" : "manual", "localizations" : { diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextOnboarding/Model/BrowserImportTunables.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextOnboarding/Model/BrowserImportTunables.swift index 655bb10b32f9..3848f400e151 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextOnboarding/Model/BrowserImportTunables.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextOnboarding/Model/BrowserImportTunables.swift @@ -15,11 +15,11 @@ public nonisolated struct BrowserImportTunables { help: "Import from Browser reads a browser's profiles only after you pick its folder in the open panel. Off reads every browser's folder on Find Browsers.", default: true, code: "BrowserImportTunables.folderGrant") - /// On (the default): Home, New Tab and Settings › Import show cards for what this Mac has - /// that cmux can bring over (CmuxNextImportCards). Off: no cards and no scan. + /// On (the default): Settings › Import shows cards for what this Mac has that cmux can bring + /// over (CmuxNextImportCards). Off: no cards and no scan. Home and New Tab never show them. public static let suggestionCards = Tunable.toggle( "browserImport.suggestionCards", section, "Show import suggestion cards", - help: "Home, New Tab and Settings › Import suggest what to bring over from other browsers, agents and terminals on this Mac. Off hides the cards and stops the scan.", + help: "Settings › Import suggests what to bring over from other browsers, agents and terminals on this Mac. Off hides the cards and stops the scan.", default: true, code: "BrowserImportTunables.suggestionCards") public static var all: [TunableDescriptor] { [folderGrant.descriptor, suggestionCards.descriptor] } diff --git a/Packages/macOS/CmuxNext/Tests/CmuxNextAgentPaneTests/AgentPaneImportCardsTests.swift b/Packages/macOS/CmuxNext/Tests/CmuxNextAgentPaneTests/AgentPaneImportCardsTests.swift deleted file mode 100644 index 08f936ccd152..000000000000 --- a/Packages/macOS/CmuxNext/Tests/CmuxNextAgentPaneTests/AgentPaneImportCardsTests.swift +++ /dev/null @@ -1,37 +0,0 @@ -@testable import CmuxNextAgentPane -import Foundation -import Testing - -/// The New Tab import cards' button: the page posts `importCards.act {id, action}`; only a short -/// card id and a lowercase action word reach the App. -@MainActor -struct AgentPaneImportCardsTests { - @Test func aCardButtonDecodesAndAMalformedOneIsRefused() { - #expect(AgentPaneRequest(body: ["method": "importCards.act", "params": ["id": "browser.chrome", "action": "start"]]) - == .importCard(AgentPaneImportCards.Act(["id": "browser.chrome", "action": "start"])!)) - for params: [String: Any] in [ - [:], - ["id": "browser.chrome"], - ["id": "", "action": "start"], - ["id": String(repeating: "x", count: 201), "action": "start"], - ["id": "browser.chrome", "action": "Start"], - ["id": "browser.chrome", "action": "start now"], - ["id": "browser.chrome", "action": "émettre"], - ["id": "browser.chrome", "action": String(repeating: "a", count: 17)], - ["id": 3, "action": "start"], - ] { - #expect(AgentPaneRequest(body: ["method": "importCards.act", "params": params]) == .unsupported("importCards.act")) - } - #expect(AgentPageOps.all.contains(AgentPageOps.namespace + "importCards.act")) - } - - @Test func aButtonRunsOnlyOnceTheAppWiredTheCards() { - let cards = AgentPaneImportCards() - let act = AgentPaneImportCards.Act(["id": "agents.codex", "action": "dismiss"])! - #expect(!cards.perform(act)) - var pressed: [String] = [] - cards.onAction = { id, action in pressed.append("\(id) \(action)") } - #expect(cards.perform(act)) - #expect(pressed == ["agents.codex dismiss"]) - } -} diff --git a/Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/ClassicMigrationTests.swift b/Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/ClassicMigrationTests.swift index 6c7eb7c3f46e..d803dca2838b 100644 --- a/Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/ClassicMigrationTests.swift +++ b/Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/ClassicMigrationTests.swift @@ -228,7 +228,7 @@ struct ClassicMigrationTests { for _ in 0..<2 { try center.perform("cmux.classic", .start) await center.settled("cmux.classic") - runs.statuses.append(center.list(for: .settings).cards.first { $0.id == "cmux.classic" }?.status) + runs.statuses.append(center.list().cards.first { $0.id == "cmux.classic" }?.status) } } let first = try #require(runs.opened.first ?? nil) diff --git a/Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/ImportSuggestionsCenterClassicTests.swift b/Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/ImportSuggestionsCenterClassicTests.swift index cb0f02a7ae72..4e5dfc1f0556 100644 --- a/Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/ImportSuggestionsCenterClassicTests.swift +++ b/Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/ImportSuggestionsCenterClassicTests.swift @@ -70,7 +70,7 @@ struct ImportSuggestionsCenterClassicTests { } private static func card(_ center: ImportSuggestionsCenter) -> ImportCard? { - center.list(for: .settings).cards.first { $0.id == id } + center.list().cards.first { $0.id == id } } @Test func importRunsAsAJobWhoseProgressMovesThenSaysHowManyCameOver() async throws { diff --git a/Packages/macOS/CmuxNext/Tests/CmuxNextImportSuggestionsTests/ImportCardDeckTests.swift b/Packages/macOS/CmuxNext/Tests/CmuxNextImportSuggestionsTests/ImportCardDeckTests.swift index a5bb15a7457d..85f60ae7e53e 100644 --- a/Packages/macOS/CmuxNext/Tests/CmuxNextImportSuggestionsTests/ImportCardDeckTests.swift +++ b/Packages/macOS/CmuxNext/Tests/CmuxNextImportSuggestionsTests/ImportCardDeckTests.swift @@ -3,7 +3,7 @@ import CmuxNextBrowserImport import Foundation import Testing -/// Which cards each surface shows, dismissals that stick until something new +/// The cards Settings › Import lists, dismissals that stick until something new /// appears, the Chief's requests, and jobs that survive a relaunch. struct ImportCardDeckTests { static let now = Date(timeIntervalSince1970: 1_800_000_000) @@ -23,17 +23,12 @@ struct ImportCardDeckTests { ImportCardDeck(findings: findings, state: state, jobs: jobs) } - @Test func homeShowsOnlyWhatOneClickImports() { - let cards = Self.deck([Self.chrome, Self.safari, Self.ghostty, Self.claude]).cards(for: .home, now: Self.now) - #expect(cards.map(\.id) == ["browser.chrome", "agents.claudeCode"]) + @Test func settingsListsEverythingFound() { + let cards = Self.deck([Self.chrome, Self.safari, Self.ghostty]).cards() + #expect(cards.map(\.id) == ["browser.chrome", "browser.safari", "terminal.ghostty"]) #expect(cards[0].primary?.action == .start) #expect(cards[0].secondary.map(\.action) == [.more, .dismiss]) #expect(!cards[0].facts.isEmpty && !cards[0].benefit.isEmpty) - } - - @Test func settingsListsEverythingFound() { - let cards = Self.deck([Self.chrome, Self.safari, Self.ghostty]).cards(for: .settings, now: Self.now) - #expect(cards.map(\.id) == ["browser.chrome", "browser.safari", "terminal.ghostty"]) #expect(cards[1].state == .info && cards[1].primary?.action == .open) #expect(cards[2].state == .info && cards[2].secondary.isEmpty) } @@ -42,22 +37,22 @@ struct ImportCardDeckTests { var state = ImportSuggestionState() state.dismiss(Self.chrome) state.dismiss(Self.claude) - #expect(Self.deck([Self.chrome, Self.claude], state: state).cards(for: .newTab, now: Self.now).isEmpty) - // More sessions is ordinary use: still gone. + let dismissed = { (findings: [ImportFinding]) in Self.deck(findings, state: state).cards().filter(\.dismissed).map(\.id) } + #expect(dismissed([Self.chrome, Self.claude]) == ["browser.chrome", "agents.claudeCode"]) + // More sessions is ordinary use: still dismissed. var grown = Self.claude grown.count = 900 // A new Chrome profile is something new: back. var newProfile = Self.chrome newProfile.profiles.append(.init(directoryName: "Profile 2", kinds: [.history])) - let cards = Self.deck([newProfile, grown], state: state).cards(for: .newTab, now: Self.now) - #expect(cards.map(\.id) == ["browser.chrome"]) - // A profile imported elsewhere (Chrome holds less now) is not new: still gone. + #expect(dismissed([newProfile, grown]) == ["agents.claudeCode"]) + // A profile imported elsewhere (Chrome holds less now) is not new: still dismissed. var fewer = Self.chrome fewer.profiles = Array(fewer.profiles.prefix(1)) fewer.profiles[0].kinds = Array(fewer.profiles[0].kinds.prefix(1)) - #expect(Self.deck([fewer], state: state).cards(for: .newTab, now: Self.now).isEmpty) - // Settings keeps the dismissed one, with Show Again. - let settings = Self.deck([Self.claude], state: state).cards(for: .settings, now: Self.now) + #expect(dismissed([fewer]) == ["browser.chrome"]) + // A dismissed card stays listed, with Show Again. + let settings = Self.deck([Self.claude], state: state).cards() #expect(settings.first?.dismissed == true) #expect(settings.first?.secondary.map(\.action) == [.restore]) } @@ -66,7 +61,7 @@ struct ImportCardDeckTests { var state = ImportSuggestionState() let site = try #require(ImportRequest.site("https://GitHub.com/login")) state.raise(ImportRequest(browser: .chrome, site: site, reason: "The page asks to sign in.", requestedAt: Self.now)) - let cards = Self.deck([Self.chrome], state: state).cards(for: .home, now: Self.now) + let cards = Self.deck([Self.chrome], state: state).cards() #expect(cards.map(\.id) == ["request.cookies.chrome.github.com", "browser.chrome"]) #expect(cards[0].reason == "The page asks to sign in.") #expect(cards[0].title.contains("github.com")) @@ -108,47 +103,43 @@ struct ImportCardDeckTests { #expect(legacy.raisedBy == nil && legacy.agentSession == nil && legacy.terminal == nil) } - @Test func surfacesCapTheirCards() { + @Test func settingsListsEveryCard() { let many = (0..<8).map { ImportFinding(id: "agents.\($0)", kind: .codex, name: "Codex", count: 1) } - #expect(Self.deck(many).cards(for: .newTab, now: Self.now).count == 3) - #expect(Self.deck(many).cards(for: .home, now: Self.now).count == 4) - #expect(Self.deck(many).cards(for: .settings, now: Self.now).count == 8) + #expect(Self.deck(many).cards().count == 8) } - /// Everything classic saved is already here: Home and New Tab have nothing one click brings, - /// while Settings › Import keeps the card so the person can run the import again. + /// Everything classic saved is already here: Settings › Import keeps the card so the person + /// can run the import again. @Test func settingsKeepsClassicOnceEverythingCameOver() throws { let deck = Self.deck([Self.classicAllHere]) - #expect(deck.cards(for: .home, now: Self.now).isEmpty) - #expect(deck.cards(for: .newTab, now: Self.now).isEmpty) - let card = try #require(deck.cards(for: .settings, now: Self.now).first) + let card = try #require(deck.cards().first) #expect(card.id == "cmux.classic" && card.state == .ready) #expect(card.primary?.action == .start && card.primary?.title == "Import Again") #expect(card.facts != ImportSuggestionStrings.workspaces(0) && !card.facts.isEmpty && !card.benefit.isEmpty) - // Workspaces still to bring: the one-click card, on Home too. + // Workspaces still to bring: the one-click card. var more = Self.classicAllHere more.count = 2 - let home = try #require(Self.deck([more]).cards(for: .home, now: Self.now).first) - #expect(home.primary?.title == ImportSuggestionStrings.importWorkspaces) + let first = try #require(Self.deck([more]).cards().first) + #expect(first.primary?.title == ImportSuggestionStrings.importWorkspaces) } - /// A finished classic import stays offered again in Settings › Import. Home and New Tab - /// confirm a run that brought sessions, and show nothing for one that brought none. + /// A finished classic import stays offered again in Settings › Import, with what it brought. @Test func aFinishedClassicImportOffersItAgainInSettings() throws { var job = ImportJob(finding: Self.classicAllHere, now: Self.now) job.importedClassic(0, now: Self.now) var ledger = ImportJobLedger() ledger.put(job) let deck = Self.deck([Self.classicAllHere], jobs: ledger) - let settings = deck.cards(for: .settings, now: Self.now).first + let settings = deck.cards().first #expect(settings?.state == .done && settings?.primary?.action == .start && settings?.primary?.title == "Import Again") #expect(settings?.status == ImportSuggestionStrings.importedSessions(0)) - #expect(deck.cards(for: .home, now: Self.now).isEmpty) - #expect(deck.cards(for: .newTab, now: Self.now).isEmpty) job.importedClassic(2, now: Self.now) ledger.put(job) - let home = try #require(Self.deck([Self.classicAllHere], jobs: ledger).cards(for: .home, now: Self.now).first) - #expect(home.state == .done && home.primary == nil && home.status == ImportSuggestionStrings.importedSessions(2)) + let again = try #require(Self.deck([Self.classicAllHere], jobs: ledger).cards().first) + #expect(again.state == .done && again.primary?.title == "Import Again" && again.status == ImportSuggestionStrings.importedSessions(2)) + // Classic's saved session is gone: nothing to run again. + let gone = try #require(Self.deck([], jobs: ledger).cards().first) + #expect(gone.state == .done && gone.primary == nil) } /// Resume and Try Again start over from 0, not at the fraction the cut-off run had reached. @@ -183,14 +174,11 @@ struct ImportCardDeckTests { #expect(job.phase == .done && job.counts.history == 12) var ledger = ImportJobLedger() ledger.put(job) - let card = Self.deck([], jobs: ledger).cards(for: .home, now: Self.now) + let card = Self.deck([], jobs: ledger).cards() #expect(card.first?.state == .done && card.first?.secondary.map(\.action) == [.hide]) - // A day later Home lets it go; Settings still lists it. - let later = Self.now.addingTimeInterval(ImportCardDeck.doneLingers + 1) - #expect(Self.deck([], jobs: ledger).cards(for: .home, now: later).isEmpty) - #expect(Self.deck([], jobs: ledger).cards(for: .settings, now: later).count == 1) - // The browser's own Import card does not come back on Home in its place. - #expect(Self.deck([Self.chrome], jobs: ledger).cards(for: .home, now: later).isEmpty) + // The browser's own Import card does not come back beside its finished job. + let listed = Self.deck([Self.chrome], jobs: ledger).cards() + #expect(listed.map(\.id) == ["browser.chrome"] && listed.first?.state == .done) } @Test func onlyStartResumeAndTryAgainReadAnotherAppsData() { @@ -216,7 +204,7 @@ struct ImportCardDeckTests { // Chrome's cookies need a Keychain prompt, so it waits for Resume; Firefox's need none. #expect(!chromeAfter.resumesQuietly) #expect(relaunched.job("browser.firefox")?.resumesQuietly == true) - let card = try #require(Self.deck([], jobs: relaunched).cards(for: .home, now: Self.now).first { $0.id == "browser.chrome" }) + let card = try #require(Self.deck([], jobs: relaunched).cards().first { $0.id == "browser.chrome" }) #expect(card.state == .paused && card.primary?.action == .resume) } diff --git a/webviews/src/agent-session/acpmux/App.tsx b/webviews/src/agent-session/acpmux/App.tsx index b9b0f3a5db60..f0e6acc51c10 100644 --- a/webviews/src/agent-session/acpmux/App.tsx +++ b/webviews/src/agent-session/acpmux/App.tsx @@ -48,8 +48,6 @@ import { FOCUS_LOCATION_EVENT, NewTabPage, newTabHost, type NewTabHost } from ". import { setDeviceChats } from "./newtab/deviceChats"; import { setRecentlyClosed } from "./newtab/recentlyClosed"; import { setNowPlaying } from "./newtab/nowPlaying"; -import { setImportCards } from "./importCards"; -import { NewTabImportCards } from "./NewTabImportCards"; import { NewTabScreen, type NewTabScreenActions } from "./newtab/NewTabScreen"; import { HistoryScreen } from "./newtab/HistoryScreen"; import type { AllChatsPage } from "./newtab/AllChatsList"; @@ -475,8 +473,6 @@ declare global { applyRecentlyClosed?(items: unknown): void; /// What plays, for the New Tab page's player (newtab/nowPlaying.ts); null while nothing does. applyNowPlaying?(track: unknown): void; - /// The New Tab page's import suggestion cards (importCards.ts); null while there are none. - applyImportCards?(cards: unknown): void; /// Scrolls to a turn a `cmux://session/#turn-` link names (links.ts), once its row /// renders; gives up quietly after a few seconds. revealTurn?(turnId: string): void; @@ -5535,9 +5531,6 @@ function AcpmuxPane() { applyNowPlaying(track) { setNowPlaying(track); }, - applyImportCards(cards) { - setImportCards(cards); - }, revealTurn(turnId) { revealRef.current(turnId); }, @@ -7868,14 +7861,7 @@ function AcpmuxPane() { accessory={} dictating={isWaitingForWords(dictation.state)} dictationField={newTabDictationField} - notice={ - <> - - void callNative("importCards.act", { id, action }).catch(() => undefined)} - /> - - } + notice={} // The host answers only for a page on this Mac; another machine's keeps the refusal. {...(!quick ? { localPaths } : {})} template={screenTemplate(shownTemplate(newTab))} diff --git a/webviews/src/agent-session/acpmux/NewTabImportCards.tsx b/webviews/src/agent-session/acpmux/NewTabImportCards.tsx deleted file mode 100644 index 415dcd76c49a..000000000000 --- a/webviews/src/agent-session/acpmux/NewTabImportCards.tsx +++ /dev/null @@ -1,14 +0,0 @@ -// The New Tab page's import cards, in the page's notice slot (App.tsx): the host's few cards for -// what this Mac has that cmux can bring over. A button goes back as `importCards.act {id, action}`. -import { ImportCards } from "../../ui/ImportCards"; -import { useImportCards } from "./importCards"; - -export function NewTabImportCards({ onAction }: { onAction: (id: string, action: string) => void }) { - const list = useImportCards(); - if (!list || list.cards.length === 0) return null; - return ( -
- -
- ); -} diff --git a/webviews/src/agent-session/acpmux/importCards.ts b/webviews/src/agent-session/acpmux/importCards.ts deleted file mode 100644 index 6bd738765760..000000000000 --- a/webviews/src/agent-session/acpmux/importCards.ts +++ /dev/null @@ -1,29 +0,0 @@ -// The import suggestion cards for the New Tab page, as the host pushes them (`importCards`, -// CmuxNextAgentPane AgentPaneImportCards): a few cards for what this Mac has that cmux can bring -// over, or null while there are none or the cards are off (Debug Settings -// `browserImport.suggestionCards`). A module store like newtab/nowPlaying.ts, so a push that -// arrives before the screen mounts is kept. -import { useSyncExternalStore } from "react"; -import { parseImportCards, type ImportCardList } from "../../ui/ImportCards"; - -let list: ImportCardList | null = null; -const listeners = new Set<() => void>(); - -/// Replaces the cards from a host push; anything malformed clears them. -export function setImportCards(value: unknown): void { - list = parseImportCards(value); - for (const listener of listeners) listener(); -} - -export function importCards(): ImportCardList | null { - return list; -} - -function subscribe(listener: () => void): () => void { - listeners.add(listener); - return () => listeners.delete(listener); -} - -export function useImportCards(): ImportCardList | null { - return useSyncExternalStore(subscribe, importCards, importCards); -} diff --git a/webviews/src/agent-session/acpmux/pageHost.ts b/webviews/src/agent-session/acpmux/pageHost.ts index 3e12b1e752fa..37f795e622cc 100644 --- a/webviews/src/agent-session/acpmux/pageHost.ts +++ b/webviews/src/agent-session/acpmux/pageHost.ts @@ -13,7 +13,6 @@ import { setDeviceChats } from "./newtab/deviceChats"; import { setToolCards } from "./toolCards/toolCardStore"; import { setRecentlyClosed } from "./newtab/recentlyClosed"; import { setNowPlaying } from "./newtab/nowPlaying"; -import { setImportCards } from "./importCards"; import { setHeaderState } from "./header/headerState"; import { setTabRow } from "./header/tabRow"; import { setFloatingState } from "./floatingState"; @@ -110,9 +109,6 @@ export function applyHostEvent(event: HostEvent): void { case "nowPlaying": // Direct, like recentlyClosed: a push before the New Tab screen mounts is kept. return setNowPlaying(event.value); - case "importCards": - // Direct, like nowPlaying: a push before the New Tab screen mounts is kept. - return setImportCards(event.value); case "toolCards": // Direct, like deviceChats: a push before the chat mounts is kept. return setToolCards(event.value); diff --git a/webviews/src/dev-host/host.ts b/webviews/src/dev-host/host.ts index 1cc080bd1ad8..07c45b8cde6e 100644 --- a/webviews/src/dev-host/host.ts +++ b/webviews/src/dev-host/host.ts @@ -192,7 +192,6 @@ export function devHostReply(host: DevHostParams, request: Request): DevHostRepl case "newTab.setTemplate": case "newTab.editWidgets": case "nowPlaying.command": - case "importCards.act": case "newTab.touched": case "newTab.warm": case "shortcut.edit": diff --git a/webviews/src/pages/settings/components/ImportSuggestionsCard.tsx b/webviews/src/pages/settings/components/ImportSuggestionsCard.tsx index 6d681f9922c2..3d11051f1794 100644 --- a/webviews/src/pages/settings/components/ImportSuggestionsCard.tsx +++ b/webviews/src/pages/settings/components/ImportSuggestionsCard.tsx @@ -12,11 +12,7 @@ export function ImportSuggestionsCard() { if (!list) return null; return (
- void store.runAction("import.card", { id, action })} - /> + void store.runAction("import.card", { id, action })} />
); } diff --git a/webviews/src/ui/ImportCards.gallery.tsx b/webviews/src/ui/ImportCards.gallery.tsx index 14acf72a90f2..f985b57b234e 100644 --- a/webviews/src/ui/ImportCards.gallery.tsx +++ b/webviews/src/ui/ImportCards.gallery.tsx @@ -4,7 +4,7 @@ import { componentEntry } from "../gallery/format"; import type { Play } from "../gallery/play"; import { ImportCards, type ImportCardData, type ImportCardList } from "./ImportCards"; -type Props = { list: ImportCardList; layout: "row" | "list" }; +type Props = { list: ImportCardList }; const notNow = { action: "dismiss", title: "Not Now" }; const more = { action: "more", title: "More Options…" }; @@ -137,7 +137,6 @@ const failed: ImportCardData = { secondary: [{ action: "hide", title: "Not Now" }], }; -const newTab = (cards: ImportCardData[]): ImportCardList => ({ title: "Bring your stuff to cmux", cards }); const settings = (cards: ImportCardData[]): ImportCardList => ({ title: "Found on This Mac", cards, @@ -145,18 +144,13 @@ const settings = (cards: ImportCardData[]): ImportCardList => ({ }); /// The cards with the host stood in: Not Now takes a card away, as the host's next push would. -function GalleryImportCards({ list, layout }: Props) { +function GalleryImportCards({ list }: Props) { const [cards, setCards] = useState(list.cards); const [pressed, setPressed] = useState(""); return ( -
+
{ setPressed(`${id} ${action}`); if (action === "dismiss" || action === "hide") setCards((current) => current.filter((c) => c.id !== id)); @@ -185,35 +179,35 @@ export default componentEntry({ load: async () => GalleryImportCards, styles: () => import("./ui.css"), variants: { - "new-tab": { - note: "New Tab: the three cards one click imports, each with what was found and what you get.", - props: { list: newTab([chrome, claude, codex]), layout: "row" }, - height: 260, + settings: { + note: "Settings › Import lists everything found: done, failed, info cards and a dismissed one with Show Again.", + props: { list: settings([done, failed, safari, ghostty, iterm]) }, + height: 820, + }, + "settings-ready": { + note: "Settings › Import: what one click imports, each with what was found and what you get.", + props: { list: settings([chrome, claude, codex]) }, + height: 560, }, - "new-tab-request": { + "settings-request": { note: "Chief raised a card for a sign-in wall: it leads, with Chief's reason in its words.", - props: { list: newTab([request, chrome, claude]), layout: "row" }, - height: 300, + props: { list: settings([request, chrome, claude]) }, + height: 600, }, - "new-tab-running": { + "settings-running": { note: "An import runs in the background with its progress; a paused one offers Resume.", - props: { list: newTab([running, paused, claude]), layout: "row" }, - height: 260, + props: { list: settings([running, paused, claude]) }, + height: 560, }, - "new-tab-dismiss": { + "settings-dismiss": { note: "Play: Not Now takes the Codex card away; it comes back only when something new appears.", - props: { list: newTab([chrome, claude, codex]), layout: "row" }, - height: 260, + props: { list: settings([chrome, claude, codex]) }, + height: 560, play: dismissOne, }, - settings: { - note: "Settings › Import lists everything found: done, failed, info cards and a dismissed one with Show Again.", - props: { list: settings([done, failed, safari, ghostty, iterm]), layout: "list" }, - height: 820, - }, "settings-empty": { note: "Settings › Import with nothing found.", - props: { list: settings([]), layout: "list" }, + props: { list: settings([]) }, height: 120, }, }, diff --git a/webviews/src/ui/ImportCards.tsx b/webviews/src/ui/ImportCards.tsx index 18f9dfdfc097..5206e945fa3d 100644 --- a/webviews/src/ui/ImportCards.tsx +++ b/webviews/src/ui/ImportCards.tsx @@ -1,7 +1,7 @@ // Import suggestion cards (Packages/macOS/CmuxNext CmuxNextImportCards): what this Mac has that cmux // can bring over. The host builds every card with its text already localized (ImportCardDeck) and -// pushes the list; this draws it the same way on New Tab (a row of a few) and in Settings › Import -// (every card, dismissed ones dimmed). A button press goes back as the card id and its action. +// sends the list; this draws it in Settings › Import (every card, dismissed ones dimmed). Home and +// New Tab show no import suggestion. A button press goes back as the card id and its action. import "./importCards.css"; export type ImportCardButton = { action: string; title: string }; @@ -157,20 +157,18 @@ function Card({ card, onAction }: { card: ImportCardData; onAction: (id: string, ); } -/// The cards: `row` lays a few side by side (New Tab), `list` stacks every card (Settings › Import). +/// The cards, stacked (Settings › Import). export function ImportCards({ list, - layout, onAction, showTitle = true, }: { list: ImportCardList; - layout: "row" | "list"; onAction: (id: string, action: string) => void; showTitle?: boolean; }) { return ( -
+
{showTitle && list.title &&

{list.title}

} {list.cards.length === 0 && list.empty ? (

{list.empty}

diff --git a/webviews/src/ui/importCards.css b/webviews/src/ui/importCards.css index 77c0ac7156b7..f3bfa1ac2698 100644 --- a/webviews/src/ui/importCards.css +++ b/webviews/src/ui/importCards.css @@ -1,6 +1,6 @@ -/* Import suggestion cards (ImportCards.tsx). Colors come from the page's theme: the agent page's - --agent-* tokens on New Tab, the --cmux-* tokens in Settings, so the cards follow light, dark - and custom themes. No animation: a card changes in one frame. */ +/* Import suggestion cards (ImportCards.tsx), in Settings › Import. Colors come from the page's + theme (--cmux-* tokens), so the cards follow light, dark and custom themes. No animation: a card + changes in one frame. */ .import-cards { --import-text: var(--agent-text, var(--cmux-text, CanvasText)); --import-dim: color-mix(in srgb, var(--import-text) 64%, transparent); @@ -39,10 +39,6 @@ min-width: 0; } -.import-cards[data-layout="row"] .import-cards-grid { - grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); -} - .import-card { display: grid; align-content: start; diff --git a/webviews/test/gallery-coverage.allowlist.json b/webviews/test/gallery-coverage.allowlist.json index 7c2a8dc0900e..e1d8c3dcdfb2 100644 --- a/webviews/test/gallery-coverage.allowlist.json +++ b/webviews/test/gallery-coverage.allowlist.json @@ -22,7 +22,6 @@ "agent-session/acpmux/ComposerPickers.tsx#SlashIcon", "agent-session/acpmux/ComposerPickers.tsx#StopIcon", "agent-session/acpmux/Keycap.tsx#Keycap", - "agent-session/acpmux/NewTabImportCards.tsx#NewTabImportCards", "agent-session/acpmux/ProjectBadge.tsx#ProjectBadge", "agent-session/acpmux/SwitchNotice.tsx#SwitchNotice", "agent-session/acpmux/changeIcons.tsx#AlertCircle",