fix(macos): keep permissions reachable on short screens (#112507)

* fix(macos): keep permissions reachable on short screens

* fix(macos): preserve onboarding i18n inventory
This commit is contained in:
Peter Steinberger
2026-07-21 21:53:07 -07:00
committed by GitHub
parent 735a6acd56
commit 9231bcb38f
3 changed files with 58 additions and 31 deletions

View File

@@ -19,7 +19,7 @@ extension OnboardingView {
case 4:
self.memoryImportPage(contentHeight: contentHeight)
case 5:
self.permissionsPage(contentHeight: contentHeight)
self.permissionsPage()
case 9:
self.readyPage()
default:
@@ -712,41 +712,41 @@ extension OnboardingView {
.buttonStyle(.plain)
}
func permissionsPage(contentHeight: CGFloat) -> some View {
// Fixed layout (no ScrollView): sorted by importance and sized so all
// permissions stay visible at once no scrollbars during onboarding.
VStack(spacing: 12) {
HStack(spacing: 8) {
Text("Grant permissions")
.font(.largeTitle.weight(.semibold))
if self.isRequesting {
ProgressView()
.controlSize(.small)
func permissionsPage() -> some View {
onboardingPage {
VStack(spacing: 12) {
// Keep intro and rows in one document so short windows can reveal every permission.
HStack(spacing: 8) {
Text("Grant permissions")
.font(.largeTitle.weight(.semibold))
if self.isRequesting {
ProgressView()
.controlSize(.small)
}
}
}
Text(
"These macOS permissions let OpenClaw automate apps and capture context on this Mac. " +
"Status updates automatically.")
.font(.body)
.foregroundStyle(.secondary)
.multilineTextAlignment(.center)
.frame(maxWidth: 520)
.fixedSize(horizontal: false, vertical: true)
Text(
"These macOS permissions let OpenClaw automate apps and capture context on this Mac. " +
"Status updates automatically.")
.font(.body)
.foregroundStyle(.secondary)
.multilineTextAlignment(.center)
.frame(maxWidth: 520)
.fixedSize(horizontal: false, vertical: true)
self.onboardingCard(spacing: 4, padding: 12) {
ForEach(Capability.importanceOrdered, id: \.self) { cap in
PermissionRow(
capability: cap,
status: self.permissionMonitor.status[cap] ?? .notGranted,
compact: true)
{
Task { await self.request(cap) }
self.onboardingCard(spacing: 4, padding: 12) {
ForEach(Capability.importanceOrdered, id: \.self) { cap in
PermissionRow(
capability: cap,
status: self.permissionMonitor.status[cap] ?? .notGranted,
compact: true)
{
Task { await self.request(cap) }
}
}
}
}
}
.padding(.horizontal, 28)
.frame(width: self.pageWidth, height: contentHeight, alignment: .top)
// The root onboarding layout keeps navigation outside this scrollable document.
}
func cliPage() -> some View {

View File

@@ -40,7 +40,7 @@ extension OnboardingView {
_ = view.connectionPage()
_ = view.aiSetupPage(contentHeight: contentHeight)
_ = view.memoryImportPage(contentHeight: contentHeight)
_ = view.permissionsPage(contentHeight: contentHeight)
_ = view.permissionsPage()
_ = view.cliPage()
_ = view.readyPage()

View File

@@ -73,6 +73,25 @@ struct OnboardingViewSmokeTests {
#expect(short < preferred)
}
@Test func `permissions page scrolls when the onboarding window is short`() throws {
let state = AppState(preview: true)
let view = OnboardingView(state: state)
let hosting = NSHostingView(rootView: view.permissionsPage())
let contentHeight = OnboardingView.contentHeight(
for: OnboardingView.minimumWindowHeight,
usesCompactHero: false)
hosting.frame = NSRect(
x: 0,
y: 0,
width: OnboardingView.windowWidth,
height: contentHeight)
hosting.layoutSubtreeIfNeeded()
let scrollView = try #require(Self.firstDescendant(of: NSScrollView.self, in: hosting))
#expect(contentHeight == 303)
#expect(scrollView.documentView != nil)
}
@Test func `local page order includes memory import only while eligible`() {
let configuredOrder = OnboardingView.pageOrder(
for: .local,
@@ -589,4 +608,12 @@ struct OnboardingViewSmokeTests {
== [.appleScript, .accessibility, .screenRecording])
#expect(Capability.importanceOrdered.last == Capability.location)
}
private static func firstDescendant<T: NSView>(of type: T.Type, in view: NSView) -> T? {
if let match = view as? T { return match }
for child in view.subviews {
if let match = self.firstDescendant(of: type, in: child) { return match }
}
return nil
}
}