diff --git a/apps/.i18n/native-source.json b/apps/.i18n/native-source.json index 258286653739..7dd5a783e135 100644 --- a/apps/.i18n/native-source.json +++ b/apps/.i18n/native-source.json @@ -25587,7 +25587,7 @@ }, { "kind": "ui-localized-call", - "line": 139, + "line": 162, "path": "apps/macos/Sources/OpenClaw/BrowserProfileImportModel.swift", "source": "Browser import requires Local mode", "surface": "apple", @@ -25595,7 +25595,7 @@ }, { "kind": "ui-localized-call", - "line": 141, + "line": 164, "path": "apps/macos/Sources/OpenClaw/BrowserProfileImportModel.swift", "source": "Switch this Mac app to a local Gateway before importing browser cookies.", "surface": "apple", @@ -25603,7 +25603,7 @@ }, { "kind": "ui-localized-call", - "line": 161, + "line": 190, "path": "apps/macos/Sources/OpenClaw/BrowserProfileImportModel.swift", "source": "No Chrome, Brave, Edge, or Chromium profile with cookies was found on this Mac.", "surface": "apple", @@ -25611,7 +25611,7 @@ }, { "kind": "ui-localized-call", - "line": 163, + "line": 192, "path": "apps/macos/Sources/OpenClaw/BrowserProfileImportModel.swift", "source": "System browser profile import is disabled in the local Gateway configuration.", "surface": "apple", @@ -25619,7 +25619,7 @@ }, { "kind": "ui-localized-call", - "line": 164, + "line": 194, "path": "apps/macos/Sources/OpenClaw/BrowserProfileImportModel.swift", "source": "No browser login available", "surface": "apple", @@ -25627,7 +25627,7 @@ }, { "kind": "ui-localized-call", - "line": 174, + "line": 206, "path": "apps/macos/Sources/OpenClaw/BrowserProfileImportModel.swift", "source": "Browser import unavailable", "surface": "apple", @@ -27147,7 +27147,7 @@ }, { "kind": "conditional-branch", - "line": 665, + "line": 718, "path": "apps/macos/Sources/OpenClaw/DashboardWindowController.swift", "source": "[\\(host)]", "surface": "apple", @@ -29027,7 +29027,7 @@ }, { "kind": "ui-call", - "line": 108, + "line": 103, "path": "apps/macos/Sources/OpenClaw/MenuBar.swift", "source": "Settings...", "surface": "apple", @@ -29035,7 +29035,7 @@ }, { "kind": "ui-call", - "line": 115, + "line": 110, "path": "apps/macos/Sources/OpenClaw/MenuBar.swift", "source": "Back", "surface": "apple", @@ -29043,7 +29043,7 @@ }, { "kind": "ui-call", - "line": 120, + "line": 115, "path": "apps/macos/Sources/OpenClaw/MenuBar.swift", "source": "Forward", "surface": "apple", @@ -29051,7 +29051,7 @@ }, { "kind": "conditional-branch", - "line": 138, + "line": 133, "path": "apps/macos/Sources/OpenClaw/MenuBar.swift", "source": "OpenClaw - Voice Wake live meter active", "surface": "apple", @@ -29059,7 +29059,7 @@ }, { "kind": "conditional-branch", - "line": 139, + "line": 134, "path": "apps/macos/Sources/OpenClaw/MenuBar.swift", "source": "OpenClaw", "surface": "apple", @@ -29067,7 +29067,7 @@ }, { "kind": "conditional-branch", - "line": 339, + "line": 334, "path": "apps/macos/Sources/OpenClaw/MenuBar.swift", "source": "Close Canvas", "surface": "apple", @@ -29075,7 +29075,7 @@ }, { "kind": "conditional-branch", - "line": 339, + "line": 334, "path": "apps/macos/Sources/OpenClaw/MenuBar.swift", "source": "Open Canvas", "surface": "apple", @@ -29651,7 +29651,7 @@ }, { "kind": "conditional-branch", - "line": 687, + "line": 688, "path": "apps/macos/Sources/OpenClaw/Onboarding.swift", "source": "Finish", "surface": "apple", @@ -29659,7 +29659,7 @@ }, { "kind": "conditional-branch", - "line": 687, + "line": 688, "path": "apps/macos/Sources/OpenClaw/Onboarding.swift", "source": "Next", "surface": "apple", diff --git a/apps/macos/Sources/OpenClaw/BrowserProfileImportModel.swift b/apps/macos/Sources/OpenClaw/BrowserProfileImportModel.swift index b5b033510de5..336a19dfd333 100644 --- a/apps/macos/Sources/OpenClaw/BrowserProfileImportModel.swift +++ b/apps/macos/Sources/OpenClaw/BrowserProfileImportModel.swift @@ -119,26 +119,50 @@ final class BrowserProfileImportModel { self.phaseGeneration += 1 } - /// Launch/connect/window triggers. Only fills an empty banner slot so a - /// visible offer, in-flight import, or result is never clobbered by a - /// background status poll. + /// First inline-browser open trigger. Only fills an empty banner slot so a + /// visible offer, in-flight import, or result is never clobbered by the + /// status poll. @discardableResult - func refreshIfIdle() async -> Bool { + func refreshIfIdle( + while shouldApply: @escaping @MainActor () -> Bool = { true }) async -> Bool + { guard !self.dismissedThisSession, case .hidden = self.phase else { return false } - await self.refresh(force: false) - return true + return await self.performRefresh(force: false, shouldApply: shouldApply).didApply + } + + /// Defers the one-shot automatic offer until the app can actually query + /// the host-local import endpoint. A remote or pre-onboarding sidebar open + /// must not consume the first eligible local attempt. + @discardableResult + func requestAutomaticOfferIfEligible( + while shouldApply: @escaping @MainActor () -> Bool = { true }) async -> Bool + { + guard self.isOnboarded(), self.isLocalMode(), shouldApply() else { return false } + return await self.refreshIfIdle(while: shouldApply) } /// Force (Settings → Import…) re-offers even after a persisted dismissal /// and reports why nothing can be offered so the caller can tell the user. @discardableResult func refresh(force: Bool) async -> ForceRefreshOutcome { + await self.performRefresh(force: force).outcome + } + + /// Automatic offers are consumed only after a status response is applied. + /// Failed or stale startup/reconnect requests stay retryable. + private func performRefresh( + force: Bool, + shouldApply: @escaping @MainActor () -> Bool = { true }) async + -> (outcome: ForceRefreshOutcome, didApply: Bool) + { guard self.isOnboarded(), self.isLocalMode() else { self.setPhase(.hidden) - return .unavailable( - title: String(localized: "Browser import requires Local mode"), - message: String( - localized: "Switch this Mac app to a local Gateway before importing browser cookies.")) + return ( + .unavailable( + title: String(localized: "Browser import requires Local mode"), + message: String( + localized: "Switch this Mac app to a local Gateway before importing browser cookies.")), + false) } let generation = self.phaseGeneration do { @@ -150,9 +174,14 @@ final class BrowserProfileImportModel { // must stay dismissed); a forced refresh wins over everything // except an import that is already running. if force { - if case .importing = self.phase { return .offering } + if case .importing = self.phase { return (.offering, true) } } else { - guard self.phaseGeneration == generation else { return .offering } + // The sidebar may close while the host-local status request is + // in flight. Never surface its automatic offer after that UI + // owner has gone away. + guard self.phaseGeneration == generation, shouldApply() else { + return (.offering, false) + } } guard Self.shouldOffer(status: status, force: force) else { self.setPhase(.hidden) @@ -161,18 +190,22 @@ final class BrowserProfileImportModel { localized: "No Chrome, Brave, Edge, or Chromium profile with cookies was found on this Mac.") : String( localized: "System browser profile import is disabled in the local Gateway configuration.") - return .unavailable(title: String(localized: "No browser login available"), message: message) + return ( + .unavailable(title: String(localized: "No browser login available"), message: message), + true) } self.setPhase(.offering(status)) - return .offering + return (.offering, true) } catch { // Same interleaving rule: a failed poll only clears state it owns. if force, self.phaseGeneration == generation { if case .importing = self.phase {} else { self.setPhase(.hidden) } } - return .unavailable( - title: String(localized: "Browser import unavailable"), - message: error.localizedDescription) + return ( + .unavailable( + title: String(localized: "Browser import unavailable"), + message: error.localizedDescription), + false) } } diff --git a/apps/macos/Sources/OpenClaw/DashboardManager.swift b/apps/macos/Sources/OpenClaw/DashboardManager.swift index 8e8d9243b616..ff0291320127 100644 --- a/apps/macos/Sources/OpenClaw/DashboardManager.swift +++ b/apps/macos/Sources/OpenClaw/DashboardManager.swift @@ -235,6 +235,10 @@ final class DashboardManager { self.controller?.closeDashboard() } + func handleOnboardingCompletion() { + self.controller?.handleOnboardingCompletion() + } + func navigateBack() { guard self.controller?.window?.isKeyWindow == true else { return } self.controller?.navigateBack() diff --git a/apps/macos/Sources/OpenClaw/DashboardWindowController.swift b/apps/macos/Sources/OpenClaw/DashboardWindowController.swift index fe5bf21935d4..7327e5a9b59f 100644 --- a/apps/macos/Sources/OpenClaw/DashboardWindowController.swift +++ b/apps/macos/Sources/OpenClaw/DashboardWindowController.swift @@ -74,20 +74,31 @@ final class DashboardWindowController: NSWindowController, WKNavigationDelegate, private var auth: DashboardWindowAuth private let updater: UpdaterProviding? private var updateBridgeEnabled: Bool + private let requestBrowserProfileImportOffer: + @MainActor (@escaping @MainActor () -> Bool) async -> Bool private var canGoBackObservation: NSKeyValueObservation? private var canGoForwardObservation: NSKeyValueObservation? + private var didRequestBrowserProfileImportOffer = false + private var browserProfileImportOfferIsArmed = false + private var browserProfileImportOfferRequestIsInFlight = false + private var browserProfileImportOfferRetryPending = false init( url: URL, auth: DashboardWindowAuth, updater: UpdaterProviding? = nil, - updateBridgeEnabled: Bool = true) + updateBridgeEnabled: Bool = true, + requestBrowserProfileImportOffer: + @escaping @MainActor (@escaping @MainActor () -> Bool) async -> Bool = { shouldApply in + await BrowserProfileImportModel.shared.requestAutomaticOfferIfEligible(while: shouldApply) + }) { let shouldEnableUpdateBridge = updater?.isAvailable == true && updateBridgeEnabled self.currentURL = url self.auth = auth self.updater = updater self.updateBridgeEnabled = shouldEnableUpdateBridge + self.requestBrowserProfileImportOffer = requestBrowserProfileImportOffer let dataStore = WKWebsiteDataStore.default() let config = WKWebViewConfiguration() @@ -261,6 +272,7 @@ final class DashboardWindowController: NSWindowController, WKNavigationDelegate, self.setUpdateBridgeEnabled(updateBridgeEnabled) } self.load(url) + self.requestBrowserProfileImportOfferIfNeeded() } /// Miniaturized windows report `isVisible == false` but must still follow @@ -307,10 +319,51 @@ final class DashboardWindowController: NSWindowController, WKNavigationDelegate, self.webView.load(URLRequest(url: url)) } - private func openLinkBrowser(_ url: URL) { + private func openLinkBrowser(_ url: URL, requestBrowserProfileImportOffer: Bool = true) { self.linkBrowserItem.isCollapsed = false self.linkBrowser.open(url) window?.makeFirstResponder(self.linkBrowser.activeWebView) + if requestBrowserProfileImportOffer { + self.browserProfileImportOfferIsArmed = true + self.requestBrowserProfileImportOfferIfNeeded() + } + } + + private func requestBrowserProfileImportOfferIfNeeded() { + guard self.browserProfileImportOfferIsArmed, + !self.linkBrowserItem.isCollapsed, + !self.didRequestBrowserProfileImportOffer + else { return } + if self.browserProfileImportOfferRequestIsInFlight { + // Gateway readiness can arrive while the status poll awaits transport. + // Latch one retry so in-flight dedupe does not discard that reconnect signal. + self.browserProfileImportOfferRetryPending = true + return + } + self.browserProfileImportOfferRequestIsInFlight = true + Task { [weak self] in + guard let self else { return } + let didApply = await self.requestBrowserProfileImportOffer { [weak self] in + guard let self else { return false } + return self.browserProfileImportOfferIsArmed && + !self.linkBrowserItem.isCollapsed && + !self.didRequestBrowserProfileImportOffer + } + self.browserProfileImportOfferRequestIsInFlight = false + let shouldRetry = self.browserProfileImportOfferRetryPending && !didApply + self.browserProfileImportOfferRetryPending = false + if didApply { + self.didRequestBrowserProfileImportOffer = true + } else if shouldRetry { + self.requestBrowserProfileImportOfferIfNeeded() + } + } + } + + func handleOnboardingCompletion() { + // A pre-onboarding inline browser leaves the one-shot armed. Retry at + // the eligibility transition so it does not depend on later navigation. + self.requestBrowserProfileImportOfferIfNeeded() } private func closeLinkBrowser(focusDashboard: Bool = true) { @@ -984,8 +1037,8 @@ extension DashboardWindowController { self.splitViewController.splitView.autosaveName } - func _testOpenLinkBrowser(_ url: URL) { - self.openLinkBrowser(url) + func _testOpenLinkBrowser(_ url: URL, requestBrowserProfileImportOffer: Bool = false) { + self.openLinkBrowser(url, requestBrowserProfileImportOffer: requestBrowserProfileImportOffer) } func _testCloseLinkBrowser() { diff --git a/apps/macos/Sources/OpenClaw/MenuBar.swift b/apps/macos/Sources/OpenClaw/MenuBar.swift index 49800952f6d6..ae5025aab529 100644 --- a/apps/macos/Sources/OpenClaw/MenuBar.swift +++ b/apps/macos/Sources/OpenClaw/MenuBar.swift @@ -78,11 +78,6 @@ struct OpenClawApp: App { } .onChange(of: self.controlChannel.state) { _, _ in self.applyStatusItemAppearance(paused: self.state.isPaused, sleeping: self.isGatewaySleeping) - if self.controlChannel.state == .connected { - DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) { - Task { await BrowserProfileImportModel.shared.refreshIfIdle() } - } - } } .onChange(of: self.gatewayManager.status) { _, _ in self.applyStatusItemAppearance(paused: self.state.isPaused, sleeping: self.isGatewaySleeping) @@ -454,9 +449,6 @@ final class AppDelegate: NSObject, NSApplicationDelegate { DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { CLIInstallPrompter.shared.checkAndPromptIfNeeded(reason: "launch") } - DispatchQueue.main.asyncAfter(deadline: .now() + 4.0) { - Task { await BrowserProfileImportModel.shared.refreshIfIdle() } - } #if DEBUG // Screenshot/demo helper: show the pairing panel with sample requests. diff --git a/apps/macos/Sources/OpenClaw/Onboarding.swift b/apps/macos/Sources/OpenClaw/Onboarding.swift index cc8638625e75..7078a5a763a2 100644 --- a/apps/macos/Sources/OpenClaw/Onboarding.swift +++ b/apps/macos/Sources/OpenClaw/Onboarding.swift @@ -496,6 +496,7 @@ final class OnboardingController: NSObject, NSWindowDelegate { UserDefaults.standard.set(true, forKey: onboardingSeenKey) UserDefaults.standard.set(currentOnboardingVersion, forKey: onboardingVersionKey) AppStateStore.shared.onboardingSeen = true + DashboardManager.shared.handleOnboardingCompletion() } func show() { diff --git a/apps/macos/Tests/OpenClawIPCTests/BrowserProfileImportModelTests.swift b/apps/macos/Tests/OpenClawIPCTests/BrowserProfileImportModelTests.swift index ac83d87c0a16..7d9c57febac8 100644 --- a/apps/macos/Tests/OpenClawIPCTests/BrowserProfileImportModelTests.swift +++ b/apps/macos/Tests/OpenClawIPCTests/BrowserProfileImportModelTests.swift @@ -29,7 +29,10 @@ private final class BrowserImportTransportStub { } """ - func makeModel(isLocalMode: @escaping @MainActor () -> Bool = { true }) -> BrowserProfileImportModel { + func makeModel( + isOnboarded: @escaping @MainActor () -> Bool = { true }, + isLocalMode: @escaping @MainActor () -> Bool = { true }) -> BrowserProfileImportModel + { BrowserProfileImportModel( transport: { [weak self] request in guard let self else { throw StubError() } @@ -50,7 +53,7 @@ private final class BrowserImportTransportStub { return Data(#"{"ok":true}"#.utf8) } }, - isOnboarded: { true }, + isOnboarded: isOnboarded, isLocalMode: isLocalMode) } @@ -125,6 +128,55 @@ struct BrowserProfileImportModelTests { #expect(stub.requests.isEmpty) } + @Test func `automatic offer request waits for onboarding and local mode`() async { + let stub = BrowserImportTransportStub() + var isOnboarded = false + var isLocalMode = false + let model = stub.makeModel( + isOnboarded: { isOnboarded }, + isLocalMode: { isLocalMode }) + + #expect(await !model.requestAutomaticOfferIfEligible()) + isOnboarded = true + #expect(await !model.requestAutomaticOfferIfEligible()) + isLocalMode = true + #expect(await model.requestAutomaticOfferIfEligible()) + #expect(stub.requests(for: "/system-profile-import/status").count == 1) + } + + @Test func `failed automatic status request stays retryable`() async { + let stub = BrowserImportTransportStub() + stub.failingPaths = ["/system-profile-import/status"] + let model = stub.makeModel() + + #expect(await !model.requestAutomaticOfferIfEligible()) + stub.failingPaths = [] + #expect(await model.requestAutomaticOfferIfEligible()) + #expect(stub.requests(for: "/system-profile-import/status").count == 2) + } + + @Test func `automatic offer does not apply after its inline browser closes`() async { + let stub = BrowserImportTransportStub() + let model = stub.makeModel() + let gate = ContinuationBox() + var shouldApply = true + stub.beforeStatusResponse = { + await withCheckedContinuation { gate.continuation = $0 } + } + + let request = Task { + await model.requestAutomaticOfferIfEligible(while: { shouldApply }) + } + while gate.continuation == nil { + await Task.yield() + } + + shouldApply = false + gate.continuation?.resume() + #expect(await !request.value) + #expect(model.phase == .hidden) + } + @Test func `import success records counts and target`() async throws { let stub = BrowserImportTransportStub() let model = stub.makeModel() @@ -212,7 +264,7 @@ struct BrowserProfileImportModelTests { model._testSetPhase(.offering(forced)) gate.continuation?.resume() - _ = await idle.value + #expect(await !idle.value) #expect(model.phase == .offering(forced)) } diff --git a/apps/macos/Tests/OpenClawIPCTests/DashboardWindowSmokeTests.swift b/apps/macos/Tests/OpenClawIPCTests/DashboardWindowSmokeTests.swift index cbceca79eb25..787af4cf36af 100644 --- a/apps/macos/Tests/OpenClawIPCTests/DashboardWindowSmokeTests.swift +++ b/apps/macos/Tests/OpenClawIPCTests/DashboardWindowSmokeTests.swift @@ -25,7 +25,20 @@ private actor DashboardRouteAuthGate { self.token = token } - func probes() -> Int { self.probeCount } + func probes() -> Int { + self.probeCount + } +} + +@MainActor +private final class DashboardBrowserImportGate { + var isOnboarded = false + private(set) var requestCount = 0 + + func request() -> Bool { + self.requestCount += 1 + return self.isOnboarded + } } @Suite(.serialized) @@ -83,12 +96,138 @@ struct DashboardWindowSmokeTests { auth: DashboardWindowAuth(gatewayUrl: nil, token: nil, password: nil)) #expect(controller._testNavigationWebViewIdentity == controller._testDashboardWebViewIdentity) - controller._testOpenLinkBrowser(try #require(URL(string: "https://docs.openclaw.ai/"))) + try controller._testOpenLinkBrowser(#require(URL(string: "https://docs.openclaw.ai/"))) let linkWebView = try #require(controller._testLinkBrowserWebViewIdentity) #expect(controller._testFocusLinkBrowser()) #expect(controller._testNavigationWebViewIdentity == linkWebView) } + @Test func `browser import offer retries until the first completed inline browser request`() async throws { + let dashboard = try #require(URL(string: "http://127.0.0.1:18789/control/")) + var requestCount = 0 + var firstRequestContinuation: CheckedContinuation? + let controller = DashboardWindowController( + url: dashboard, + auth: DashboardWindowAuth(gatewayUrl: nil, token: nil, password: nil), + requestBrowserProfileImportOffer: { _ in + requestCount += 1 + if requestCount == 1 { + return await withCheckedContinuation { continuation in + firstRequestContinuation = continuation + } + } + return true + }) + defer { controller.closeDashboard() } + + controller.show() + #expect(requestCount == 0) + + let link = try #require(URL(string: "https://docs.openclaw.ai/")) + controller._testOpenLinkBrowser(link) + controller.update( + url: dashboard, + auth: DashboardWindowAuth(gatewayUrl: nil, token: nil, password: nil)) + #expect(requestCount == 0) + + controller._testOpenLinkBrowser(link, requestBrowserProfileImportOffer: true) + for _ in 0..<200 where firstRequestContinuation == nil { + await Task.yield() + } + #expect(requestCount == 1) + + controller.update( + url: dashboard, + auth: DashboardWindowAuth(gatewayUrl: nil, token: nil, password: nil)) + firstRequestContinuation?.resume(returning: false) + firstRequestContinuation = nil + for _ in 0..<200 where requestCount == 1 { + await Task.yield() + } + #expect(requestCount == 2) + + controller._testCloseLinkBrowser() + controller._testOpenLinkBrowser(link, requestBrowserProfileImportOffer: true) + for _ in 0..<10 { + await Task.yield() + } + #expect(requestCount == 2) + } + + @Test func `browser import offer retries when onboarding completes with browser open`() async throws { + let dashboard = try #require(URL(string: "http://127.0.0.1:18789/control/")) + let gate = DashboardBrowserImportGate() + let controller = DashboardWindowController( + url: dashboard, + auth: DashboardWindowAuth(gatewayUrl: nil, token: nil, password: nil), + requestBrowserProfileImportOffer: { _ in gate.request() }) + defer { controller.closeDashboard() } + let manager = DashboardManager._testMake() + manager._testSetController(controller) + + let link = try #require(URL(string: "https://docs.openclaw.ai/")) + controller._testOpenLinkBrowser(link, requestBrowserProfileImportOffer: true) + for _ in 0..<200 where gate.requestCount == 0 { + await Task.yield() + } + #expect(gate.requestCount == 1) + + gate.isOnboarded = true + manager.handleOnboardingCompletion() + for _ in 0..<200 where gate.requestCount == 1 { + await Task.yield() + } + #expect(gate.requestCount == 2) + + manager.handleOnboardingCompletion() + for _ in 0..<10 { + await Task.yield() + } + #expect(gate.requestCount == 2) + } + + @Test func `closing inline browser invalidates an in-flight import offer`() async throws { + let dashboard = try #require(URL(string: "http://127.0.0.1:18789/control/")) + var requestCount = 0 + var firstRequestContinuation: CheckedContinuation? + var firstRequestApplied: Bool? + let controller = DashboardWindowController( + url: dashboard, + auth: DashboardWindowAuth(gatewayUrl: nil, token: nil, password: nil), + requestBrowserProfileImportOffer: { shouldApply in + requestCount += 1 + if requestCount == 1 { + await withCheckedContinuation { continuation in + firstRequestContinuation = continuation + } + firstRequestApplied = shouldApply() + return firstRequestApplied == true + } + return shouldApply() + }) + defer { controller.closeDashboard() } + + let link = try #require(URL(string: "https://docs.openclaw.ai/")) + controller._testOpenLinkBrowser(link, requestBrowserProfileImportOffer: true) + for _ in 0..<200 where firstRequestContinuation == nil { + await Task.yield() + } + #expect(requestCount == 1) + + controller._testCloseLinkBrowser() + firstRequestContinuation?.resume() + for _ in 0..<200 where firstRequestApplied == nil { + await Task.yield() + } + #expect(firstRequestApplied == false) + + controller._testOpenLinkBrowser(link, requestBrowserProfileImportOffer: true) + for _ in 0..<200 where requestCount == 1 { + await Task.yield() + } + #expect(requestCount == 2) + } + @Test func `dashboard parses only bounded native link requests`() throws { let request = DashboardWindowController.linkRequest(from: [ "type": "open-link", @@ -651,9 +790,9 @@ struct DashboardWindowSmokeTests { manager._testSetController(controller) defer { manager._testController()?.closeDashboard() } - await manager.handleEndpointState(.ready( + try await manager.handleEndpointState(.ready( mode: .remote, - url: try #require(URL(string: "ws://127.0.0.1:60001")), + url: #require(URL(string: "ws://127.0.0.1:60001")), token: nil, password: nil, routeRevision: 2)) diff --git a/docs/platforms/macos.md b/docs/platforms/macos.md index 5f9c565f4ff4..e1a3ecaeeb02 100644 --- a/docs/platforms/macos.md +++ b/docs/platforms/macos.md @@ -67,7 +67,7 @@ Right-click an external link to choose **Open in Sidebar**, **Open in Default Br ## Import browser logins -When the app runs against a local Gateway and a Chrome-family profile with cookies exists on the Mac, the dashboard window shows a dismissible banner offering to copy those cookies into an isolated managed profile that agents use for browsing. Choose a profile from the banner's **Import** control (Touch ID may be required); progress and the imported-cookie count appear inline, and only cookies are copied — passwords never leave the source browser. Dismissing the banner records the choice; **Settings → General → Browser login → Import…** re-offers it at any time. See [Browser](/cli/browser) for the underlying import flow and the `browser.allowSystemProfileImport` gate. +The first time the browser sidebar opens while the app runs against a local Gateway, the dashboard shows a dismissible banner when a Chrome-family profile with cookies exists on the Mac. The banner offers to copy those cookies into an isolated managed profile that agents use for browsing. Choose a profile from its **Import** control (Touch ID may be required); progress and the imported-cookie count appear inline, and only cookies are copied — passwords never leave the source browser. Dismissing the banner records the choice; **Settings → General → Browser login → Import…** re-offers it at any time. See [Browser](/cli/browser) for the underlying import flow and the `browser.allowSystemProfileImport` gate. ## Choose a Gateway mode