Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions apps/mac/Sources/XBotApp/QuitHandler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,16 @@ final class QuitHandler: NSObject, NSApplicationDelegate {
/// Set once, when the app builds its state. Nil in a build with no managed runtime.
static var state: AppState?

/// A bare executable — `swift run`, or the debug binary started from a script — has no
/// Info.plist, and macOS can hand it the prohibited policy: the window draws and its field even
/// shows focus, but the app is never frontmost, so every keystroke goes to whatever is. The
/// bundled app never takes this branch.
func applicationWillFinishLaunching(_ notification: Notification) {
guard Bundle.main.bundleIdentifier == nil else { return }
NSApp.setActivationPolicy(.regular)
NSApp.activate(ignoringOtherApps: true)
}

func applicationShouldTerminate(_ sender: NSApplication) -> NSApplication.TerminateReply {
guard let state = Self.state, state.hasManagedRuntime else { return .terminateNow }
Task { @MainActor in
Expand Down
17 changes: 17 additions & 0 deletions apps/mac/Sources/XBotUI/Components/WindowChromeConfigurator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ public struct WindowChromeConfigurator: NSViewRepresentable {
window.titlebarAppearsTransparent = true
window.titleVisibility = .hidden
window.styleMask.insert(.fullSizeContentView)
keepTitlebarAboveContent(in: window)

switch style {
case .main:
Expand All @@ -98,6 +99,22 @@ public struct WindowChromeConfigurator: NSViewRepresentable {
}
}

/// The title bar above the SwiftUI content, where AppKit is meant to keep it.
///
/// On macOS 27 the window opens with the hosting view stacked over `NSTitlebarContainerView`.
/// With a full-size content view the aurora then paints over the whole title bar: no traffic
/// lights, no toggles, no agent name — every view still laid out and not hidden, just covered.
/// Moving the container back to the top is all it takes, and it stays there once moved.
private func keepTitlebarAboveContent(in window: NSWindow) {
guard let frame = window.contentView?.superview,
let titlebar = frame.subviews.first(where: {
String(describing: type(of: $0)).contains("TitlebarContainer")
}),
frame.subviews.last !== titlebar
else { return }
frame.addSubview(titlebar, positioned: .above, relativeTo: nil)
}

private func stripToolbarItemBackgrounds(in view: NSView?) {
guard let view else { return }
let className = String(describing: type(of: view))
Expand Down
5 changes: 4 additions & 1 deletion apps/mac/Sources/XBotUI/DesignSystem/Materials.swift
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,10 @@ private struct FrostedGlassModifier: ViewModifier {
if reduceTransparency {
content.background(opaqueFallback)
} else {
content.background(FrostedGlassBackground(material: material))
// Up under the title bar, as the opaque fallback above already goes: a `Color`
// background ignores the safe area by default, a representable does not, so the rail
// and panel stopped 38pt short of the top only when transparency was on.
content.background(FrostedGlassBackground(material: material).ignoresSafeArea())
}
}
}
Expand Down
5 changes: 5 additions & 0 deletions apps/mac/Sources/XBotUI/MainWindow.swift
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,11 @@ public struct MainWindow: View {
ToolbarItem(placement: .principal) {
TitleBarAgentTitle()
}
// Pushes the panel toggle to the trailing corner, over the panel it opens. Without it
// `.primaryAction` sits flush against the centred title.
if #available(macOS 26.0, *) {
ToolbarSpacer(.flexible)
}
ToolbarItem(placement: .primaryAction) {
sidebarToggle(
isVisible: state.isPanelVisible,
Expand Down
12 changes: 12 additions & 0 deletions docs/13-launch-checklist.md
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,18 @@ logging proxy in the path (12 of 12). A leftover process, a database lock, ident
keep-alive and split request writes were each checked and ruled out. Worth one more look before
launch, starting from the app under ordinary use rather than nine tests at once.

**Two more, found starting the app on this Mac's own engine** (27 September), both leaving the
container unhealthy with nothing on its port and no way back short of deleting the data:

- **Any recreate of the container lost the database password.** The app mounts `xbot-data` at
`/var/lib/postgresql/data`; the password file lives one level up, so it went with the container
while the cluster kept the password. Every environment change recreates the container. Reproduced
on the old image with a clean stop and recreate; `postgres-init.sh` now sets a new password when a
cluster has none beside it, which also heals an install already in this state.
- **`migrate` raced Postgres after an unclean stop.** It started before crash recovery finished and
failed on `57P03`; 3 of 3 SIGKILL-and-start rounds broke the old image, 0 of 3 the fixed one.
`migrate.sh` now waits for `pg_isready`, up to 60s.

**Also close the last M2 item here:** point one agent at a second real vendor — Anthropic is already
proven, so use OpenAI or Google — and confirm the reply comes from the vendor you picked. A model
name the vendor does not have should come back as a named error, not a silent fall back to somebody
Expand Down
13 changes: 13 additions & 0 deletions engine/docker/s6/scripts/migrate.sh
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,19 @@
set -eu
[ "${EMBEDDED_POSTGRES:-off}" = "on" ] || exit 0
cd /app/server
# `postgres` is a longrun with no readiness notification, so s6 starts this the moment the process
# exists, not when it accepts connections. After an unclean stop it replays WAL first and answers
# 57P03 "not yet accepting connections"; migrating then exits 1, and `api`, which depends on this,
# never starts — the container sits unhealthy with nothing on :3001. Wait for it, but not forever.
i=0
until /usr/lib/postgresql/16/bin/pg_isready -q -h 127.0.0.1 -p 5432; do
i=$((i + 1))
if [ "$i" -ge 120 ]; then
echo "migrate: postgres did not accept connections within 60s" >&2
exit 1
fi
sleep 0.5
done
# `scripts/migrate.ts`, not `drizzle-kit`. The CLI is a development dependency and needs esbuild to
# read its TypeScript config, which `bun install --production` leaves out of this image: asked to
# migrate here it exits 1 without printing why, and the container comes up against an empty database.
Expand Down
15 changes: 15 additions & 0 deletions engine/docker/s6/scripts/postgres-init.sh
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,21 @@ if [ ! -s "$DATA/PG_VERSION" ]; then
s6-setuidgid postgres "$BIN/pg_ctl" -D "$DATA" -w stop >/dev/null
fi

# A cluster with no password file beside it. "Beside the data on the same volume" holds only when the
# volume is mounted at /var/lib/postgresql; mounted at $DATA — as the Mac app has always done, and
# moving it now would strand every existing cluster — the file lives in the container's own layer and
# is gone the first time the container is recreated, while the cluster keeps the password it no longer
# has. `migrate` then fails authentication, `api` never starts, and nothing fixes it short of deleting
# the data. So set a new one. Single-user mode needs no connection and so no password, and the
# statement arrives on stdin, never argv. The server is not running yet: `postgres` depends on this.
if [ -s "$DATA/PG_VERSION" ] && [ ! -s "$PW_FILE" ]; then
PW="$(od -An -N32 -tx1 /dev/urandom | tr -d ' \n')"
printf "ALTER ROLE openbot PASSWORD '%s';\n" "$PW" \
| s6-setuidgid postgres "$BIN/postgres" --single -D "$DATA" postgres >/dev/null
( umask 077; printf '%s' "$PW" > "$PW_FILE" )
chown postgres:postgres "$PW_FILE"
fi

# Every boot, not only the first: hand the password-bearing URL to the services that connect over TCP
# (`api` and `migrate`, both `with-contenv`). The password persists with the cluster; the container
# environment is fresh each boot, so this has to run outside the first-init guard above. The file is
Expand Down
6 changes: 6 additions & 0 deletions scripts/bundle-mac-app.sh
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,12 @@ cp "${MAC}/Resources/Info.plist" "${APP}/Contents/Info.plist"

chmod +x "${APP}/Contents/MacOS/XBot"

# SwiftPM's per-target resource bundles. `Bundle.module` traps when its bundle is missing, so an app
# without them dies at launch on the first view that loads a resource.
for bundle in "$(dirname "${BUILD}")"/*.bundle; do
[[ -d "${bundle}" ]] && rsync -a "${bundle}" "${APP}/Contents/Resources/"
done

# Sparkle ships as an embedded framework when linked through SwiftPM.
SPARKLE_FW="$(find "${MAC}/.build" -path '*/Sparkle.framework' -type d 2>/dev/null | head -1)"
if [[ -n "${SPARKLE_FW}" ]]; then
Expand Down
Loading