fix(macos): improve onboarding discovery
This commit is contained in:
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user