fix(macos): improve onboarding discovery

This commit is contained in:
Peter Steinberger
2026-01-11 03:50:59 +01:00
parent 9d802abd9a
commit 89291c384b
7 changed files with 90 additions and 28 deletions

View File

@@ -33,9 +33,16 @@ struct GeneralSettings: View {
Button { Button {
DebugActions.restartOnboarding() DebugActions.restartOnboarding()
} label: { } label: {
Text("Complete onboarding to finish setup") HStack(spacing: 8) {
.font(.callout.weight(.semibold)) Label("Complete onboarding to finish setup", systemImage: "arrow.counterclockwise")
.foregroundColor(.accentColor) .font(.callout.weight(.semibold))
.foregroundStyle(Color.accentColor)
Spacer(minLength: 0)
Image(systemName: "chevron.right")
.font(.caption.weight(.semibold))
.foregroundStyle(.tertiary)
}
.contentShape(Rectangle())
} }
.buttonStyle(.plain) .buttonStyle(.plain)
.padding(.bottom, 2) .padding(.bottom, 2)

View File

@@ -456,17 +456,28 @@ final class MenuSessionsInjector: NSObject, NSMenuDelegate {
private func makeMessageItem(text: String, symbolName: String, width: CGFloat, maxLines: Int? = 2) -> NSMenuItem { private func makeMessageItem(text: String, symbolName: String, width: CGFloat, maxLines: Int? = 2) -> NSMenuItem {
let view = AnyView( let view = AnyView(
Label(text, systemImage: symbolName) HStack(alignment: .top, spacing: 8) {
.font(.caption) Image(systemName: symbolName)
.foregroundStyle(.secondary) .font(.caption)
.multilineTextAlignment(.leading) .foregroundStyle(.secondary)
.lineLimit(maxLines) .frame(width: 14, alignment: .leading)
.truncationMode(.tail) .padding(.top, 1)
.fixedSize(horizontal: false, vertical: true)
.padding(.leading, 18) Text(text)
.padding(.trailing, 12) .font(.caption)
.padding(.vertical, 6) .foregroundStyle(.secondary)
.frame(width: max(1, width), alignment: .leading)) .multilineTextAlignment(.leading)
.lineLimit(maxLines)
.truncationMode(.tail)
.fixedSize(horizontal: false, vertical: true)
.layoutPriority(1)
Spacer(minLength: 0)
}
.padding(.leading, 18)
.padding(.trailing, 12)
.padding(.vertical, 6)
.frame(width: max(1, width), alignment: .leading))
let item = NSMenuItem() let item = NSMenuItem()
item.tag = self.tag item.tag = self.tag

View File

@@ -166,7 +166,8 @@ struct OnboardingView: View {
state: AppState = AppStateStore.shared, state: AppState = AppStateStore.shared,
permissionMonitor: PermissionMonitor = .shared, permissionMonitor: PermissionMonitor = .shared,
discoveryModel: GatewayDiscoveryModel = GatewayDiscoveryModel( discoveryModel: GatewayDiscoveryModel = GatewayDiscoveryModel(
localDisplayName: InstanceIdentity.displayName)) localDisplayName: InstanceIdentity.displayName,
filterLocalGateways: false))
{ {
self.state = state self.state = state
self.permissionMonitor = permissionMonitor self.permissionMonitor = permissionMonitor

View File

@@ -178,17 +178,15 @@ extension OnboardingView {
padding: CGFloat = 16, padding: CGFloat = 16,
@ViewBuilder _ content: () -> some View) -> some View @ViewBuilder _ content: () -> some View) -> some View
{ {
VStack(alignment: .leading, spacing: spacing) { let shape = RoundedRectangle(cornerRadius: 16, style: .continuous)
return VStack(alignment: .leading, spacing: spacing) {
content() content()
} }
.padding(padding) .padding(padding)
.frame(maxWidth: .infinity, alignment: .leading) .frame(maxWidth: .infinity, alignment: .leading)
.background( .background(Color.clear)
RoundedRectangle(cornerRadius: 16, style: .continuous) .clipShape(shape)
.fill(.ultraThinMaterial) .overlay(shape.strokeBorder(Color.white.opacity(0.10), lineWidth: 1))
.overlay(
RoundedRectangle(cornerRadius: 16, style: .continuous)
.strokeBorder(Color.white.opacity(0.10), lineWidth: 1)))
} }
func featureRow(title: String, subtitle: String, systemImage: String) -> some View { func featureRow(title: String, subtitle: String, systemImage: String) -> some View {

View File

@@ -33,7 +33,7 @@ extension OnboardingView {
if shouldMonitor, !self.monitoringDiscovery { if shouldMonitor, !self.monitoringDiscovery {
self.monitoringDiscovery = true self.monitoringDiscovery = true
Task { @MainActor in Task { @MainActor in
try? await Task.sleep(nanoseconds: 550_000_000) try? await Task.sleep(nanoseconds: 150_000_000)
guard self.monitoringDiscovery else { return } guard self.monitoringDiscovery else { return }
self.gatewayDiscovery.start() self.gatewayDiscovery.start()
await self.refreshLocalGatewayProbe() await self.refreshLocalGatewayProbe()

View File

@@ -292,6 +292,7 @@ final class LocationPermissionRequester: NSObject, CLLocationManagerDelegate {
static let shared = LocationPermissionRequester() static let shared = LocationPermissionRequester()
private let manager = CLLocationManager() private let manager = CLLocationManager()
private var continuation: CheckedContinuation<CLAuthorizationStatus, Never>? private var continuation: CheckedContinuation<CLAuthorizationStatus, Never>?
private var timeoutTask: Task<Void, Never>?
override init() { override init() {
super.init() super.init()
@@ -306,6 +307,16 @@ final class LocationPermissionRequester: NSObject, CLLocationManagerDelegate {
return await withCheckedContinuation { cont in return await withCheckedContinuation { cont in
self.continuation = cont self.continuation = cont
self.timeoutTask?.cancel()
self.timeoutTask = Task { [weak self] in
try? await Task.sleep(nanoseconds: 3_000_000_000)
await MainActor.run { [weak self] in
guard let self else { return }
guard self.continuation != nil else { return }
LocationPermissionHelper.openSettings()
self.finish(status: self.manager.authorizationStatus)
}
}
if always { if always {
self.manager.requestAlwaysAuthorization() self.manager.requestAlwaysAuthorization()
} else { } else {
@@ -317,13 +328,43 @@ final class LocationPermissionRequester: NSObject, CLLocationManagerDelegate {
} }
} }
private func finish(status: CLAuthorizationStatus) {
self.timeoutTask?.cancel()
self.timeoutTask = nil
guard let cont = self.continuation else { return }
self.continuation = nil
cont.resume(returning: status)
}
// nonisolated for Swift 6 strict concurrency compatibility // nonisolated for Swift 6 strict concurrency compatibility
nonisolated func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) { nonisolated func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
let status = manager.authorizationStatus let status = manager.authorizationStatus
Task { @MainActor in Task { @MainActor in
guard let cont = self.continuation else { return } self.finish(status: status)
self.continuation = nil }
cont.resume(returning: status) }
// Legacy callback (still used on some macOS versions / configurations).
nonisolated func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
Task { @MainActor in
self.finish(status: status)
}
}
nonisolated func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
let status = manager.authorizationStatus
Task { @MainActor in
if status == .denied || status == .restricted {
LocationPermissionHelper.openSettings()
}
self.finish(status: status)
}
}
nonisolated func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
let status = manager.authorizationStatus
Task { @MainActor in
self.finish(status: status)
} }
} }
} }

View File

@@ -20,7 +20,7 @@ App bundle layout:
- `Clawdbot.app/Contents/Resources/Relay/dist/` - `Clawdbot.app/Contents/Resources/Relay/dist/`
- Compiled CLI/gateway payload from `pnpm exec tsc` - Compiled CLI/gateway payload from `pnpm exec tsc`
- `Clawdbot.app/Contents/Resources/Relay/node_modules/` - `Clawdbot.app/Contents/Resources/Relay/node_modules/`
- Production dependencies staged via `pnpm deploy --prod --no-optional --legacy` - Production dependencies staged via `pnpm deploy --prod --legacy` (includes optional native addons)
- `Clawdbot.app/Contents/Resources/Relay/clawdbot` - `Clawdbot.app/Contents/Resources/Relay/clawdbot`
- Wrapper script that execs the bundled Node + dist entrypoint - Wrapper script that execs the bundled Node + dist entrypoint
- `Clawdbot.app/Contents/Resources/Relay/package.json` - `Clawdbot.app/Contents/Resources/Relay/package.json`
@@ -47,7 +47,7 @@ Packaging script:
It builds: It builds:
- TS: `pnpm exec tsc` - TS: `pnpm exec tsc`
- Swift app + helper: `swift build …` - Swift app + helper: `swift build …`
- Relay payload: `pnpm deploy --prod --no-optional --legacy` + copy `dist/` - Relay payload: `pnpm deploy --prod --legacy` + copy `dist/`
- Node runtime: downloads the latest Node release (override via `NODE_VERSION`) - Node runtime: downloads the latest Node release (override via `NODE_VERSION`)
Important knobs: Important knobs:
@@ -90,6 +90,10 @@ Node uses JIT. The bundled runtime is signed with:
This is applied by `scripts/codesign-mac-app.sh`. This is applied by `scripts/codesign-mac-app.sh`.
Note: because the relay runs under hardened runtime, any bundled `*.node` native
addons must be signed with the same Team ID as the relay `node` binary.
`scripts/codesign-mac-app.sh` re-signs `Contents/Resources/Relay/**/*.node` for this.
## Image processing ## Image processing
To avoid shipping native `sharp` addons inside the bundle, the gateway defaults To avoid shipping native `sharp` addons inside the bundle, the gateway defaults