diff --git a/slab/menubar-swift/README.md b/slab/menubar-swift/README.md index 09446d8945..540b6c8022 100644 --- a/slab/menubar-swift/README.md +++ b/slab/menubar-swift/README.md @@ -30,6 +30,12 @@ The footer contains a native About Slab window and Quit Slab. Refresh cadence: 2 s. Mail unread count refreshes every 30 s (15 ticks). +The installer compares Mach-O build UUIDs before touching the app bundle. An +identical signed build is a true no-op: it is not copied, re-signed, or +restarted, so routine fleet deployment does not churn macOS privacy prompts. +Startup also avoids reading the current photo wallpaper; capture permissions +remain lazy until an explicit frame or reel request needs them. + ### Resource TVs The optional resource strip is five small, squared category TVs: CPU, RAM, diff --git a/slab/menubar-swift/Sources/SlabMenubar/AppDelegate.swift b/slab/menubar-swift/Sources/SlabMenubar/AppDelegate.swift index cd75e9695a..976a586eb1 100644 --- a/slab/menubar-swift/Sources/SlabMenubar/AppDelegate.swift +++ b/slab/menubar-swift/Sources/SlabMenubar/AppDelegate.swift @@ -65,9 +65,6 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate { /// or "" meaning "restored to the user's original"), so the per-tick /// refresh only re-sets the wallpaper when the aggregate status changed. private var lastDesktopTint: String? - /// True once we've saved the user's pre-slab desktop picture to - /// `Paths.desktopOriginalFile`; until then we never overwrite it. - private var desktopOriginalCaptured = false /// Base font size from the most recent `tileNow()` pass. `applyTerminalDecor` /// scales typography off this — `.awaiting` ("orange") tiles get bumped /// up so focus reads typographically while the cell geometry stays put. @@ -2905,16 +2902,12 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate { /// reapply. Uses the in-process `NSWorkspace` desktop-image API, NOT /// System Events osascript (`get/set picture of desktop` returns /// `missing value` on current macOS, and NSWorkspace needs no - /// Automation TCC). The user's pre-slab wallpaper is captured once - /// before the first overwrite and restored whenever theme-by-status is - /// off / no sessions are live. Decision runs on the main hop (NSScreen - /// is main-affine); the render + per-screen set are dispatched off-main - /// so the 2 s tick never stalls (see slab-menubar-perf). + /// Automation TCC). Slab deliberately stays on generated solid colors; + /// it never reads the user's current photo wallpaper during startup, which + /// avoids an unnecessary Photos permission request after installation. + /// Decision runs on the main hop (NSScreen is main-affine); the render + + /// per-screen set are dispatched off-main so the tick never stalls. private func applyDesktopTint() { - // Always runs, even when theming is off: this is how a wallpaper the - // user picks (while theme-by-status is disabled) gets remembered as - // the restore target. - captureOriginalIfNeeded() let sessions = state.claudeSessions guard state.themeByStatus, !sessions.isEmpty else { // No live sessions (or theme-by-status off): keep a SOLID color — @@ -2999,46 +2992,6 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate { } } - /// Save the current desktop picture path to `Paths.desktopOriginalFile` - /// so it can be restored when theming is off / no sessions are live. - /// Cheap in-process NSWorkspace read; skipped once captured, or if the - /// current picture is already one of slab's tint PNGs (so we never - /// record our own image as "the original"). Re-attempts every tick - /// while uncaptured, so a wallpaper the user sets later still sticks. - private func captureOriginalIfNeeded() { - guard !desktopOriginalCaptured else { return } - if FileManager.default.fileExists(atPath: Paths.desktopOriginalFile) { - desktopOriginalCaptured = true - return - } - guard let scr = NSScreen.main, - let cur = NSWorkspace.shared.desktopImageURL(for: scr), - !cur.path.hasPrefix(Paths.desktopWallpaperDir) - else { return } - let dir = (Paths.desktopOriginalFile as NSString).deletingLastPathComponent - try? FileManager.default.createDirectory( - atPath: dir, withIntermediateDirectories: true) - try? cur.path.write(toFile: Paths.desktopOriginalFile, - atomically: true, encoding: .utf8) - desktopOriginalCaptured = true - } - - private func restoreDesktopWallpaper() { - guard let raw = try? String( - contentsOfFile: Paths.desktopOriginalFile, encoding: .utf8) - else { return } - let orig = raw.trimmingCharacters(in: .whitespacesAndNewlines) - guard !orig.isEmpty, - FileManager.default.fileExists(atPath: orig) else { return } - let url = URL(fileURLWithPath: orig) - let screens = NSScreen.screens // main-affine; we're on the main hop - DispatchQueue.global(qos: .utility).async { - for s in screens { - try? NSWorkspace.shared.setDesktopImageURL(url, for: s, options: [:]) - } - } - } - /// Which terminal app to spawn restored sessions into. Honor whatever /// the user actually has open: if only one of iTerm2 / Terminal.app has /// windows, use it; if both, defer to the frontmost one; if neither, diff --git a/slab/menubar-swift/Sources/SlabMenubar/ResourceGraph.swift b/slab/menubar-swift/Sources/SlabMenubar/ResourceGraph.swift index a8567bdb87..f1eb6ee907 100644 --- a/slab/menubar-swift/Sources/SlabMenubar/ResourceGraph.swift +++ b/slab/menubar-swift/Sources/SlabMenubar/ResourceGraph.swift @@ -84,14 +84,17 @@ final class ResourceGraph: NSObject { let boardEdge = NSBezierPath(rect: bounds.insetBy(dx: 0.5, dy: 0.5)) boardEdge.lineWidth = 1 boardEdge.stroke() + let content = bounds.insetBy(dx: 4, dy: 4) let rowHeight: CGFloat = 20 let labelWidth: CGFloat = 64 let gap: CGFloat = 2 - let stripWidth = bounds.width - labelWidth + let stripWidth = content.width - labelWidth for (index, worker) in workers.prefix(5).enumerated() { - let y = bounds.maxY - CGFloat(index + 1) * rowHeight - CGFloat(index) * gap - drawLabel(worker, in: NSRect(x: 0, y: y, width: labelWidth - 4, height: rowHeight)) - drawStrip(worker, in: NSRect(x: labelWidth, y: y, width: stripWidth, height: rowHeight)) + let y = content.maxY - CGFloat(index + 1) * rowHeight - CGFloat(index) * gap + drawLabel(worker, in: NSRect(x: content.minX, y: y, + width: labelWidth - 4, height: rowHeight)) + drawStrip(worker, in: NSRect(x: content.minX + labelWidth, y: y, + width: stripWidth, height: rowHeight)) } } @@ -135,7 +138,7 @@ final class ResourceGraph: NSObject { drawTexture(value: raw, color: color, in: cell) } let tagRect = NSRect(x: cell.minX + 1, y: cell.maxY - 7, - width: min(16, cell.width - 2), height: 6) + width: cell.width - 2, height: 6) color.withAlphaComponent(worker.online ? 0.90 : 0.20).setFill() tagRect.fill() let tag = NSAttributedString(string: labels[index], attributes: [ @@ -471,7 +474,7 @@ final class ResourceGraph: NSObject { private func drawModuleTag(_ metric: Metric, in rect: NSRect) { let tagRect = NSRect(x: rect.minX + 1.5, y: rect.maxY - 7.8, - width: min(17, rect.width - 3), height: 6.2) + width: rect.width - 3, height: 6.2) metric.color.withAlphaComponent(0.94).setFill() tagRect.fill() let label = NSAttributedString(string: metric.name, attributes: [ @@ -793,12 +796,16 @@ final class ResourceGraph: NSObject { private func showHoverCard() { guard hoverPanel?.isVisible != true, let button = item?.button, let window = button.window else { return } - let rowCount = max(1, fleetWorkers.isEmpty ? fleetTargets().count : fleetWorkers.count) - let size = NSSize(width: 258, height: CGFloat(rowCount * 20 + max(0, rowCount - 1) * 2)) let windowRect = button.convert(button.bounds, to: nil) let statusRect = window.convertToScreen(windowRect) + let rowCount = max(1, fleetWorkers.isEmpty ? fleetTargets().count : fleetWorkers.count) + let labelWidth: CGFloat = 64 + let padding: CGFloat = 4 + let size = NSSize(width: statusRect.width + labelWidth + padding * 2, + height: CGFloat(rowCount * 20 + max(0, rowCount - 1) * 2 + 8)) let screen = window.screen ?? NSScreen.main - var origin = NSPoint(x: statusRect.minX - 58, y: statusRect.minY - size.height - 2) + var origin = NSPoint(x: statusRect.minX - labelWidth - padding, + y: statusRect.minY - size.height - 4) if let visible = screen?.visibleFrame { origin.x = min(max(origin.x, visible.minX + 2), visible.maxX - size.width - 2) } diff --git a/slab/menubar-swift/install.sh b/slab/menubar-swift/install.sh index 8fc0940b15..56241e48fc 100755 --- a/slab/menubar-swift/install.sh +++ b/slab/menubar-swift/install.sh @@ -310,6 +310,35 @@ else ok "built: ${BUILT}" fi +# Re-running the installer with the exact same build used to overwrite and +# re-sign the app, producing a fresh cdhash and restarting every TCC-sensitive +# subsystem for no functional change. macOS can then repeat Screen Recording, +# Photos, Accessibility, and background-item notices even though their sliders +# are already enabled. The Mach-O UUID survives signing and identifies the +# compiled payload, so an identical, valid bundle is a true no-op. +binary_uuid() { + /usr/bin/dwarfdump --uuid "$1" 2>/dev/null | awk '{print $2}' | paste -sd, - +} +BUILT_UUID="$(binary_uuid "${BUILT}")" +APP_UUID="$(binary_uuid "${APP_BIN}")" +if [[ -x "${APP_BIN}" && -f "${PLIST_PATH}" ]] \ + && [[ -n "${BUILT_UUID}" && "${BUILT_UUID}" == "${APP_UUID}" ]] \ + && cmp -s "${INFO_PLIST}" "${APP_DIR}/Contents/Info.plist" \ + && codesign --verify --deep --strict "${APP_DIR}" >/dev/null 2>&1; then + _sig="$(codesign -dvv "${APP_DIR}" 2>&1)" + if [[ "${_sig}" == *"Authority=${SIGN_CN}"* ]]; then + provision_tailscale_cli + if launchctl print "gui/$(id -u)/computer.slab.menubar" >/dev/null 2>&1 \ + && pgrep -f "SlabMenubar.app/Contents/MacOS/slab-menubar" >/dev/null 2>&1; then + ok "identical signed build is already running — no copy, re-sign, restart, or permission churn" + exit 0 + fi + ok "identical signed build is installed; starting the existing launch agent" + launchctl kickstart "gui/$(id -u)/computer.slab.menubar" 2>/dev/null || true + exit 0 + fi +fi + provision_tailscale_cli provision_iterm2_profiles provision_terminal_close_warning diff --git a/toolchain/macos/PERFORMANCE-GUARD.md b/toolchain/macos/PERFORMANCE-GUARD.md index 50393862ed..10841239ee 100644 --- a/toolchain/macos/PERFORMANCE-GUARD.md +++ b/toolchain/macos/PERFORMANCE-GUARD.md @@ -6,9 +6,8 @@ count, or duplicate AC Caddy servers exceed their budgets. Three consecutive pressure samples produce a rate-limited macOS notification. While a threshold is active, the guard exposes -`~/.local/share/slab/performance/pressure-active`. Slab uses this as an eco -governor: prompt rocks hold a static frame until pressure clears, preserving -their identity while removing continuous compositor animation. +`~/.local/share/slab/performance/pressure-active` as a stable eco-governor +signal for Slab renderers and other local tools. The fleet worker uses the same flag as an admission gate. An active render is allowed to finish, but that host accepts no additional missions until pressure