mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-28 17:21:19 +00:00
fix(macos): prevent clipped and stalled onboarding (#107598)
* fix(macos): harden onboarding on short screens * chore: keep release notes in PR body * chore(i18n): refresh macOS onboarding inventory
This commit is contained in:
committed by
GitHub
parent
9daef57c85
commit
2bbf5e6ca0
@@ -31195,7 +31195,7 @@
|
||||
},
|
||||
{
|
||||
"kind": "conditional-branch",
|
||||
"line": 688,
|
||||
"line": 704,
|
||||
"path": "apps/macos/Sources/OpenClaw/Onboarding.swift",
|
||||
"source": "Finish",
|
||||
"surface": "apple",
|
||||
@@ -31203,7 +31203,7 @@
|
||||
},
|
||||
{
|
||||
"kind": "conditional-branch",
|
||||
"line": 688,
|
||||
"line": 704,
|
||||
"path": "apps/macos/Sources/OpenClaw/Onboarding.swift",
|
||||
"source": "Next",
|
||||
"surface": "apple",
|
||||
|
||||
@@ -514,22 +514,38 @@ final class OnboardingController: NSObject, NSWindowDelegate {
|
||||
let hosting = NSHostingController(rootView: OnboardingView())
|
||||
let window = NSWindow(contentViewController: hosting)
|
||||
window.title = UIStrings.welcomeTitle
|
||||
window.setContentSize(NSSize(width: OnboardingView.windowWidth, height: OnboardingView.windowHeight))
|
||||
window.styleMask = Self.windowStyleMask
|
||||
window.setContentSize(NSSize(width: OnboardingView.windowWidth, height: OnboardingView.windowHeight))
|
||||
if let visibleFrame = (NSScreen.main ?? NSScreen.screens.first)?.visibleFrame {
|
||||
// Constrain the full window frame, not only its content. Otherwise the
|
||||
// navigation bar can land below the Dock on shorter displays.
|
||||
window.setFrame(Self.initialWindowFrame(visibleFrame: visibleFrame), display: false)
|
||||
} else {
|
||||
window.center()
|
||||
}
|
||||
// Keep the focused dialog width while letting taller displays give setup more breathing room.
|
||||
window.contentMinSize = NSSize(width: OnboardingView.windowWidth, height: OnboardingView.windowHeight)
|
||||
window.contentMinSize = NSSize(
|
||||
width: OnboardingView.windowWidth,
|
||||
height: OnboardingView.minimumWindowHeight)
|
||||
window.contentMaxSize = NSSize(width: OnboardingView.windowWidth, height: .greatestFiniteMagnitude)
|
||||
window.titlebarAppearsTransparent = true
|
||||
window.titleVisibility = .hidden
|
||||
window.isMovableByWindowBackground = true
|
||||
window.delegate = self
|
||||
window.center()
|
||||
DockIconManager.shared.temporarilyShowDock()
|
||||
window.makeKeyAndOrderFront(nil)
|
||||
NSApp.activate(ignoringOtherApps: true)
|
||||
self.window = window
|
||||
}
|
||||
|
||||
static func initialWindowFrame(visibleFrame: NSRect) -> NSRect {
|
||||
let contentRect = NSRect(
|
||||
origin: .zero,
|
||||
size: NSSize(width: OnboardingView.windowWidth, height: OnboardingView.windowHeight))
|
||||
let preferredFrame = NSWindow.frameRect(forContentRect: contentRect, styleMask: self.windowStyleMask)
|
||||
return WindowPlacement.centeredFrame(size: preferredFrame.size, in: visibleFrame)
|
||||
}
|
||||
|
||||
func close() {
|
||||
self.busyReason = nil
|
||||
self.window?.close()
|
||||
@@ -607,6 +623,7 @@ struct OnboardingView: View {
|
||||
|
||||
static let windowWidth: CGFloat = 630
|
||||
static let windowHeight: CGFloat = 752 // ~+10% to fit full onboarding content
|
||||
static let minimumWindowHeight: CGFloat = 520
|
||||
|
||||
let pageWidth: CGFloat = Self.windowWidth
|
||||
let connectionPageIndex = 1
|
||||
@@ -625,16 +642,15 @@ struct OnboardingView: View {
|
||||
130
|
||||
}
|
||||
|
||||
/// The baseline fits every setup control. Taller windows donate all extra room
|
||||
/// to the active page instead of leaving the content pinned to a fixed canvas.
|
||||
/// The active page is scrollable on short screens. Taller windows donate all
|
||||
/// extra room instead of leaving the content pinned to a fixed canvas.
|
||||
func contentHeight(for windowHeight: CGFloat) -> CGFloat {
|
||||
Self.contentHeight(for: windowHeight, usesCompactHero: self.usesCompactHero)
|
||||
}
|
||||
|
||||
static func contentHeight(for windowHeight: CGFloat, usesCompactHero: Bool) -> CGFloat {
|
||||
let availableHeight = max(Self.windowHeight, windowHeight)
|
||||
let heroHeight: CGFloat = usesCompactHero ? 78 : 145
|
||||
return availableHeight - heroHeight - 72
|
||||
return max(0, windowHeight - heroHeight - 72)
|
||||
}
|
||||
|
||||
static func pageOrder(
|
||||
|
||||
@@ -42,7 +42,7 @@ extension OnboardingView {
|
||||
.frame(
|
||||
minWidth: pageWidth,
|
||||
maxWidth: pageWidth,
|
||||
minHeight: Self.windowHeight,
|
||||
minHeight: Self.minimumWindowHeight,
|
||||
maxHeight: .infinity)
|
||||
.background(Color(NSColor.windowBackgroundColor))
|
||||
.onAppear {
|
||||
|
||||
@@ -54,6 +54,25 @@ struct OnboardingViewSmokeTests {
|
||||
#expect(taller - baseline == 200)
|
||||
}
|
||||
|
||||
@Test func `onboarding window fits within a short visible screen`() {
|
||||
let visibleFrame = NSRect(x: 0, y: 78, width: 1600, height: 626)
|
||||
let frame = OnboardingController.initialWindowFrame(visibleFrame: visibleFrame)
|
||||
|
||||
#expect(frame.height == visibleFrame.height)
|
||||
#expect(frame.minY == visibleFrame.minY)
|
||||
#expect(frame.maxY == visibleFrame.maxY)
|
||||
}
|
||||
|
||||
@Test func `short onboarding window keeps a usable scrollable page`() {
|
||||
let short = OnboardingView.contentHeight(for: 626, usesCompactHero: false)
|
||||
let preferred = OnboardingView.contentHeight(
|
||||
for: OnboardingView.windowHeight,
|
||||
usesCompactHero: false)
|
||||
|
||||
#expect(short == 409)
|
||||
#expect(short < preferred)
|
||||
}
|
||||
|
||||
@Test func `page order delegates setup after inference to Crestodian`() {
|
||||
let order = OnboardingView.pageOrder(
|
||||
for: .local,
|
||||
|
||||
@@ -211,8 +211,8 @@ by default, plus git-checkout installs under the same prefix flow.
|
||||
</Step>
|
||||
<Step title="Refresh loaded gateway service">
|
||||
If a gateway service is already loaded from that same prefix, the script runs
|
||||
`openclaw gateway install --force`, then `openclaw gateway restart`, and
|
||||
probes gateway health best-effort.
|
||||
`openclaw gateway install --force`, which activates the replacement service,
|
||||
and then probes gateway health best-effort.
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
|
||||
@@ -1274,12 +1274,8 @@ refresh_gateway_service_if_loaded() {
|
||||
return 0
|
||||
fi
|
||||
|
||||
if ! "$claw" gateway restart >/dev/null 2>&1; then
|
||||
emit_json '{"event":"step","name":"gateway-service","status":"warn","reason":"restart-failed"}'
|
||||
log "Warning: gateway service restart failed; continuing. Run: openclaw gateway restart"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# `gateway install --force` activates the replacement service. A second
|
||||
# restart can kill startup migrations and strand their lock until expiry.
|
||||
"$claw" gateway status --probe --json >/dev/null 2>&1 || true
|
||||
emit_json '{"event":"step","name":"gateway-service","status":"ok"}'
|
||||
}
|
||||
|
||||
@@ -164,6 +164,39 @@ describe("install-cli.sh", () => {
|
||||
expect(result.stdout + result.stderr).not.toContain("unbound variable");
|
||||
});
|
||||
|
||||
it("does not restart a gateway again after force-install activates it", () => {
|
||||
const tmp = mkdtempSync(join(tmpdir(), "openclaw-install-cli-gateway-refresh-"));
|
||||
const prefix = join(tmp, "prefix");
|
||||
const bin = join(prefix, "bin");
|
||||
const commandLog = join(tmp, "commands.log");
|
||||
const openclaw = join(bin, "openclaw");
|
||||
mkdirSync(bin, { recursive: true });
|
||||
writeFileSync(openclaw, '#!/bin/bash\nprintf "%s\\n" "$*" >> "$COMMAND_LOG"\n');
|
||||
chmodSync(openclaw, 0o755);
|
||||
|
||||
try {
|
||||
const result = runInstallCliShell(
|
||||
[
|
||||
"set -euo pipefail",
|
||||
`cd ${JSON.stringify(process.cwd())}`,
|
||||
`source ${JSON.stringify(SCRIPT_PATH)}`,
|
||||
`PREFIX=${JSON.stringify(prefix)}`,
|
||||
"is_gateway_daemon_loaded() { return 0; }",
|
||||
"refresh_gateway_service_if_loaded",
|
||||
].join("\n"),
|
||||
{ COMMAND_LOG: commandLog },
|
||||
);
|
||||
|
||||
expect(result.status).toBe(0);
|
||||
expect(readFileSync(commandLog, "utf8").trim().split("\n")).toEqual([
|
||||
"gateway install --force",
|
||||
"gateway status --probe --json",
|
||||
]);
|
||||
} finally {
|
||||
rmSync(tmp, { force: true, recursive: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("keeps HOME for default prefix while OPENCLAW_HOME controls git checkout paths", () => {
|
||||
const tmp = mkdtempSync(join(tmpdir(), "openclaw-install-cli-home-"));
|
||||
const osHome = join(tmp, "os-home");
|
||||
|
||||
Reference in New Issue
Block a user