Skip to content

Commit 87fbd02

Browse files
author
SJ
committed
Fix SaneBar wake recovery release proof
1 parent fa93de8 commit 87fbd02

18 files changed

Lines changed: 1453 additions & 416 deletions

.outreach.yml

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ launch_calendar:
9999
next_eligible_launch:
100100
status: "not_scheduled"
101101
trigger: "Major release only, such as a visible v2.2/v3 workflow change, new Touch ID/security story, or substantial user proof."
102-
earliest_recommendation: "After the current v2.1.53 customer UI reliability patch is published and support noise is stable."
102+
earliest_recommendation: "After the current customer UI reliability patch is published and support noise is stable."
103103
scheduled:
104104
- cadence: "weekly"
105105
channel: "Opportunity monitoring"
@@ -108,15 +108,16 @@ launch_calendar:
108108
gate: "No Product Hunt relaunch, no Show HN repost, and no generic standalone launch posts."
109109
success_metric: "0-2 high-fit replies per week or a recorded no-go."
110110
last_launch_readiness:
111-
date: "2026-05-20"
111+
date: "2026-05-24"
112112
status: "no_go"
113113
launch_readiness_exit: 1
114114
blocker_summary:
115-
- "Fresh Mini launch_readiness on 2026-05-20 stayed red because SaneBar already had the real Product Hunt/Hacker News launch and a repost without a major new story would be weak."
115+
- "Canonical launch_readiness rerun at 2026-05-24 09:32 EDT exited nonzero and the wrapper reported 'Mac mini is unreachable. Falling back to local execution.'"
116+
- "SaneBar remains launch no-go because it already had the real Product Hunt/Hacker News launch and a repost without a major new story would be weak."
116117
- "Open support issues remain around layout stability and icon movement; do not amplify broadly until the current patch is stable."
117-
- "Mini launch_readiness reported the latest release_preflight as failed with 1 issue and 4 warnings on 2026-05-20."
118-
- "Global validation at 2026-05-20 09:02 EDT is still red for SaneBar because Lemon Squeezy hosted file `2.1.56` lags the live appcast/website `2.1.57`, and customer UI proof is stale."
119-
next_date: "2026-05-25"
118+
- "Local fallback launch_readiness still reported the latest release_preflight as passed but carrying 5 warnings, and the shared validation report at 2026-05-24 09:31 EDT still says BROKEN RELEASE PIPELINE because SaneBar customer UI proof is stale."
119+
- "No weekly opportunity reply was executed on 2026-05-24 because the app gate remained red and the schedule only allows targeted disclosed replies, not a relaunch."
120+
next_date: "2026-05-29"
120121

121122
x_search_keywords:
122123
- "\"Bartender\" (alternative OR replacement OR acquired OR privacy) lang:en -is:retweet"

.sane/customer_ui_action_receipt.json

Lines changed: 504 additions & 379 deletions
Large diffs are not rendered by default.

Core/AppIntents/SaneBarAppIntents.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
#if !DEBUG
12
import AppIntents
23
import Foundation
34

@@ -145,3 +146,5 @@ struct SaneBarAppShortcuts: AppShortcutsProvider {
145146
)
146147
}
147148
}
149+
150+
#endif

Core/Controllers/StatusBarController.swift

Lines changed: 65 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -783,6 +783,36 @@ final class StatusBarController: StatusBarControllerProtocol {
783783
return (main: safeMainLimit, separator: safeMainLimit + preservedGap)
784784
}
785785

786+
nonisolated static func launchSafePreferredSeparatorGap(for screenWidth: Double) -> Double {
787+
guard screenWidth > 0 else { return 120.0 }
788+
// Recovery is allowed to move SaneBar back beside Control Center, but it
789+
// must not collapse the user's visible lane so far that leftmost shown
790+
// items wake up hidden. Keep a moderate lane on external displays while
791+
// bounding it on smaller screens.
792+
return min(240.0, max(180.0, screenWidth * 0.09))
793+
}
794+
795+
private nonisolated static func migratedLegacyNarrowRecoveryPair(
796+
mainPosition: Double,
797+
separatorPosition: Double,
798+
screenWidth: Double,
799+
screenHasTopSafeAreaInset: Bool
800+
) -> (main: Double, separator: Double)? {
801+
let safeMain = launchSafePreferredMainPositionLimit(
802+
for: screenWidth,
803+
screenHasTopSafeAreaInset: screenHasTopSafeAreaInset
804+
)
805+
let targetGap = launchSafePreferredSeparatorGap(for: screenWidth)
806+
guard targetGap > 0 else { return nil }
807+
guard abs(mainPosition - safeMain) < 0.5 else { return nil }
808+
let currentGap = separatorPosition - mainPosition
809+
guard currentGap < targetGap - 0.5 else { return nil }
810+
811+
let widenedSeparator = min(screenWidth - 24.0, safeMain + targetGap)
812+
guard widenedSeparator > safeMain else { return nil }
813+
return (main: safeMain, separator: widenedSeparator)
814+
}
815+
786816
nonisolated static func launchSafeCurrentDisplayRecoveryPair(
787817
screenWidth: Double,
788818
screenHasTopSafeAreaInset: Bool
@@ -793,7 +823,10 @@ final class StatusBarController: StatusBarControllerProtocol {
793823
for: screenWidth,
794824
screenHasTopSafeAreaInset: screenHasTopSafeAreaInset
795825
)
796-
let safeSeparator = min(screenWidth - 24.0, safeMain + 120.0)
826+
let safeSeparator = min(
827+
screenWidth - 24.0,
828+
safeMain + launchSafePreferredSeparatorGap(for: screenWidth)
829+
)
797830
guard safeSeparator > safeMain else { return nil }
798831
return (main: safeMain, separator: safeSeparator)
799832
}
@@ -929,16 +962,22 @@ final class StatusBarController: StatusBarControllerProtocol {
929962
screenWidth: width,
930963
screenHasTopSafeAreaInset: currentScreenHasTopSafeAreaInset
931964
) {
932-
setPreferredPosition(reanchored.main, forAutosaveName: mainAutosaveName)
933-
setPreferredPosition(reanchored.separator, forAutosaveName: separatorAutosaveName)
934-
saveDisplayPositionBackupIfNeeded(
935-
for: width,
965+
let restoredPair = migratedLegacyNarrowRecoveryPair(
936966
mainPosition: reanchored.main,
937967
separatorPosition: reanchored.separator,
968+
screenWidth: width,
969+
screenHasTopSafeAreaInset: currentScreenHasTopSafeAreaInset
970+
) ?? reanchored
971+
setPreferredPosition(restoredPair.main, forAutosaveName: mainAutosaveName)
972+
setPreferredPosition(restoredPair.separator, forAutosaveName: separatorAutosaveName)
973+
saveDisplayPositionBackupIfNeeded(
974+
for: width,
975+
mainPosition: restoredPair.main,
976+
separatorPosition: restoredPair.separator,
938977
referenceScreen: referenceScreen
939978
)
940979
logger.warning(
941-
"Display validation: reanchored unsafe backup for width \(width, privacy: .public) (main=\(mainBackup, privacy: .public) -> \(reanchored.main, privacy: .public), separator=\(separatorBackup, privacy: .public) -> \(reanchored.separator, privacy: .public))"
980+
"Display validation: reanchored unsafe backup for width \(width, privacy: .public) (main=\(mainBackup, privacy: .public) -> \(restoredPair.main, privacy: .public), separator=\(separatorBackup, privacy: .public) -> \(restoredPair.separator, privacy: .public))"
942981
)
943982
return true
944983
}
@@ -950,6 +989,26 @@ final class StatusBarController: StatusBarControllerProtocol {
950989
return false
951990
}
952991

992+
if let migrated = migratedLegacyNarrowRecoveryPair(
993+
mainPosition: mainBackup,
994+
separatorPosition: separatorBackup,
995+
screenWidth: width,
996+
screenHasTopSafeAreaInset: currentScreenHasTopSafeAreaInset
997+
) {
998+
setPreferredPosition(migrated.main, forAutosaveName: mainAutosaveName)
999+
setPreferredPosition(migrated.separator, forAutosaveName: separatorAutosaveName)
1000+
setDisplayPositionBackup(
1001+
for: width,
1002+
mainPosition: migrated.main,
1003+
separatorPosition: migrated.separator,
1004+
referenceScreen: resolvedReferenceScreen
1005+
)
1006+
logger.warning(
1007+
"Display validation: widened legacy narrow recovery backup for width \(width, privacy: .public) (separator=\(separatorBackup, privacy: .public) -> \(migrated.separator, privacy: .public))"
1008+
)
1009+
return true
1010+
}
1011+
9531012
setPreferredPosition(mainBackup, forAutosaveName: mainAutosaveName)
9541013
setPreferredPosition(separatorBackup, forAutosaveName: separatorAutosaveName)
9551014
logger.info("Display validation: restored backup positions for width \(width)")

Core/Services/AppleScriptCommands.swift

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -661,6 +661,26 @@ private func scriptListingZonesForCommand() -> [ScriptZonedIcon] {
661661
)
662662
}
663663

664+
@MainActor
665+
private func authoritativeScriptListingZonesForCommand() -> [ScriptZonedIcon] {
666+
AccessibilityService.shared.invalidateMenuBarItemPositionsCache()
667+
let result = ScriptResultBox<ScriptClassifiedApps?>(nil)
668+
Task { @MainActor in
669+
result.value = await SearchService.shared.refreshClassifiedApps()
670+
}
671+
672+
let deadline = Date().addingTimeInterval(5.0)
673+
while result.value == nil, Date() < deadline {
674+
_ = RunLoop.current.run(mode: .default, before: Date().addingTimeInterval(0.05))
675+
}
676+
677+
if let classified = result.value {
678+
return sortedScriptZones(zones(from: classified))
679+
}
680+
681+
return []
682+
}
683+
664684
@MainActor
665685
private func runScriptMove(timeoutSeconds: TimeInterval = 9.0, operation: @escaping @MainActor () async -> Bool) -> Bool? {
666686
let box = ScriptResultBox<Bool?>(nil)
@@ -914,6 +934,32 @@ final class ListIconZonesCommand: SaneBarScriptCommand {
914934
}
915935
}
916936

937+
@objc(ListAuthoritativeIconZonesCommand)
938+
final class ListAuthoritativeIconZonesCommand: SaneBarScriptCommand {
939+
override func performDefaultImplementation() -> Any? {
940+
guard checkAccessibilityTrusted() else {
941+
setAccessibilityError()
942+
return nil
943+
}
944+
945+
let zones: [ScriptZonedIcon] = if Thread.isMainThread {
946+
MainActor.assumeIsolated {
947+
return authoritativeScriptListingZonesForCommand()
948+
}
949+
} else {
950+
DispatchQueue.main.sync {
951+
return authoritativeScriptListingZonesForCommand()
952+
}
953+
}
954+
955+
let lines = zones.map { item in
956+
let movable = item.app.isUnmovableSystemItem ? "false" : "true"
957+
return "\(item.zone.rawValue)\t\(movable)\t\(item.app.bundleId)\t\(item.app.uniqueId)\t\(item.app.name)"
958+
}
959+
return lines.joined(separator: "\n")
960+
}
961+
}
962+
917963
// MARK: - Layout Snapshot Command
918964

919965
@objc(LayoutSnapshotCommand)

Resources/SaneBar.sdef

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,11 @@
9292
<result type="text" description="Newline-separated lines: zone, movable, bundleId, uniqueId, name"/>
9393
</command>
9494

95+
<command name="list authoritative icon zones" code="SBarlaiz" description="List detected menu bar icons with fresh authoritative visibility zones">
96+
<cocoa class="ListAuthoritativeIconZonesCommand"/>
97+
<result type="text" description="Newline-separated lines: zone, movable, bundleId, uniqueId, name"/>
98+
</command>
99+
95100
<!-- Layout Snapshot Command -->
96101
<command name="layout snapshot" code="SBarlyot" description="Return a JSON snapshot of menu bar launch/layout invariants">
97102
<cocoa class="LayoutSnapshotCommand"/>

SaneBarApp.swift

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import AppKit
2-
import AppIntents
32
import KeyboardShortcuts
43
import os.log
54
import SaneUI
@@ -36,14 +35,25 @@ class SaneBarAppDelegate: NSObject, NSApplicationDelegate {
3635

3736
// No @main - using main.swift instead
3837

38+
nonisolated static func shouldUpdateAppShortcutParameters(
39+
environment: [String: String] = ProcessInfo.processInfo.environment,
40+
isRunningTests: Bool = NSClassFromString("XCTestCase") != nil
41+
) -> Bool {
42+
guard environment["XCTestConfigurationFilePath"] == nil else { return false }
43+
guard !isRunningTests else { return false }
44+
return true
45+
}
46+
3947
func applicationDidFinishLaunching(_: Notification) {
4048
appLogger.info("🏁 applicationDidFinishLaunching START")
4149

4250
// Near-instant tooltips (default is ~1000ms)
4351
UserDefaults.standard.set(100, forKey: "NSInitialToolTipDelay")
44-
if ProcessInfo.processInfo.environment["XCTestConfigurationFilePath"] == nil {
52+
#if !DEBUG
53+
if Self.shouldUpdateAppShortcutParameters() {
4554
SaneBarAppShortcuts.updateAppShortcutParameters()
4655
}
56+
#endif
4757

4858
// Keep the menu bar process alive across idle periods.
4959
keepAliveActivity = ProcessInfo.processInfo.beginActivity(

Scripts/customer_ui_action_sweep.rb

Lines changed: 89 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ class CustomerUIActionSweep
3737
['layout snapshot', /separatorBeforeMain/],
3838
['list icons', /./],
3939
['list icon zones', /\t/],
40+
['list authoritative icon zones', /\t/],
4041
['open icon panel', /true|false/],
4142
['quick search "Sane"', /true|false/],
4243
['close browse panel', /true|false/],
@@ -58,6 +59,15 @@ class CustomerUIActionSweep
5859
'search?q=Sane'
5960
].freeze
6061

62+
HEALTH_WARNING_LABELS = [
63+
'Needs Action',
64+
'Needs Check',
65+
'Needs Repair',
66+
'Missing Items',
67+
'Hidden by macOS',
68+
'Detached'
69+
].freeze
70+
6171
STRICT_MINI_EVIDENCE_TYPES = %w[
6272
mini_click
6373
mini_automation
@@ -284,7 +294,11 @@ def verify_release_app_running!
284294
def exercise_settings_tabs
285295
app_script('open settings window')
286296
SETTINGS_TABS.each do |tab|
287-
text = press_settings_tab(tab[:index])
297+
text = if tab[:id] == 'health'
298+
wait_for_clean_health_tab(tab[:index])
299+
else
300+
press_settings_tab(tab[:index])
301+
end
288302
tab[:expected].each do |expected|
289303
raise "Settings #{tab[:id]} tab missing #{expected.inspect}: #{text}" unless text.include?(expected)
290304
end
@@ -325,6 +339,27 @@ def press_settings_tab(index)
325339
end
326340
end
327341

342+
def wait_for_clean_health_tab(index, timeout: 8.0)
343+
deadline = Time.now + timeout
344+
last_text = nil
345+
loop do
346+
last_text = press_settings_tab(index)
347+
warnings = health_tab_warnings(last_text)
348+
return last_text if warnings.empty?
349+
350+
break if Time.now >= deadline
351+
352+
sleep 0.5
353+
end
354+
355+
raise "Health tab is not release-clean: #{health_tab_warnings(last_text).join(', ')}"
356+
end
357+
358+
def health_tab_warnings(text)
359+
value = text.to_s
360+
HEALTH_WARNING_LABELS.select { |label| value.include?(label) }
361+
end
362+
328363
def exercise_url_routes
329364
{
330365
'toggle' => nil,
@@ -586,19 +621,71 @@ def runtime_state_results(report)
586621

587622
Array(item[:paths] || item['paths'] || item[:artifacts] || item['artifacts'] || item[:path] || item['path'])
588623
end.compact
589-
status = (required_types - evidence_types).empty? && evidence_paths.any? ? 'passed' : 'failed'
624+
runtime_artifact = runtime_state_artifact(id.to_s)
625+
if runtime_artifact
626+
evidence_types |= Array(runtime_artifact[:evidence_types])
627+
evidence_paths |= Array(runtime_artifact[:evidence_paths])
628+
end
629+
completed_scenarios = Array(runtime_artifact && runtime_artifact[:completed_scenarios]).map(&:to_s)
630+
required_scenarios = Array(row['required_scenarios']).map(&:to_s)
631+
status = (required_types - evidence_types).empty? &&
632+
(required_scenarios - completed_scenarios).empty? &&
633+
evidence_paths.any? ? 'passed' : 'failed'
590634
{
591635
id: id.to_s,
592636
status: status,
593637
action_ids: action_ids,
594638
required_evidence_types: required_types,
595639
evidence_types: evidence_types.uniq,
596640
evidence_paths: evidence_paths.uniq,
641+
completed_scenarios: completed_scenarios.uniq,
597642
manifest_sha256: report.fetch('manifest_sha256')
598643
}
599644
end
600645
end
601646

647+
def runtime_state_artifact(id)
648+
case id
649+
when 'fullscreen_maximize_transition'
650+
fullscreen_matrix_artifact
651+
when 'wake_visible_zone_persistence'
652+
wake_visible_zone_artifact
653+
end
654+
end
655+
656+
def fullscreen_matrix_artifact
657+
path = '/tmp/sanebar_runtime_fullscreen_matrix.json'
658+
return nil unless File.exist?(path) && File.mtime(path) >= @started_at - 30 * 60
659+
660+
payload = JSON.parse(File.read(path))
661+
return nil unless payload['status'] == 'pass'
662+
663+
{
664+
evidence_types: Array(payload['evidence_types']).map(&:to_s),
665+
evidence_paths: ([path] + Array(payload['evidence_paths'])).map(&:to_s),
666+
completed_scenarios: Array(payload['completed_scenarios']).map(&:to_s)
667+
}
668+
rescue JSON::ParserError
669+
nil
670+
end
671+
672+
def wake_visible_zone_artifact
673+
path = '/tmp/sanebar_runtime_wake_probe.json'
674+
return nil unless File.exist?(path) && File.mtime(path) >= @started_at - 30 * 60
675+
676+
payload = JSON.parse(File.read(path))
677+
proof = payload['visible_zone_persistence']
678+
return nil unless payload['status'] == 'pass' && proof.is_a?(Hash) && proof['status'] == 'pass'
679+
680+
{
681+
evidence_types: %w[mini_runtime screenshot log],
682+
evidence_paths: [path, '/tmp/sanebar_runtime_wake_probe.log'].select { |candidate| File.exist?(candidate) },
683+
completed_scenarios: Array(proof['completed_scenarios']).map(&:to_s)
684+
}
685+
rescue JSON::ParserError
686+
nil
687+
end
688+
602689
def write_failure_artifact(error)
603690
FileUtils.mkdir_p(OUTPUT_DIR)
604691
path = File.join(OUTPUT_DIR, "customer-ui-action-sweep-failed-#{@timestamp}.txt")

0 commit comments

Comments
 (0)