diff --git a/CLAUDE.md b/CLAUDE.md
index f2f7e83..ffb9ff6 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -62,6 +62,7 @@ Data flow at runtime: timers poll `/api/now-playing`, `/api/state`, `/api/themes
- **A `/` in a scene window title crashes GTK at app_start on Linux.** Keep the branded "SUB/WAVE" slash out of `app.zon` / `shell_windows` titles; it's fine in `runWithOptions.window_title`.
- **The SDK cannot resize a live window.** Per-mode window shapes apply at next launch only.
+- **A `hidden_inset_tall` masthead must pad `insets.left` AND `insets.right`.** The window-control cluster sits on a different edge per platform — macOS traffic lights lead, Windows min/max/close trail — so `onChrome` maps both into `chrome_leading` / `chrome_trailing` and `player-top.native` spacers both ends. Dropping the trailing one put the gear button under the DWM caption buttons on Windows, and fed the host's per-present caption-colour sampler (which reads the pixel 8px leading of the cluster and pushes it through `DWMWA_CAPTION_COLOR` + `USE_IMMERSIVE_DARK_MODE`) an antialiased glyph edge instead of flat header — the caption buttons flickered.
- **The FFT spectrum feed only emits while a window is visibly on screen** (occlusion gate in the SDK). A flat visualizer from an app launched in the background is not a bug — activate the app first.
- SDK 0.8.4 still has **no OS media-controls surface** (no MPNowPlayingInfoCenter / MPRemoteCommandCenter / MPRIS / hardware media keys — re-checked at the 0.8.4 upgrade). The substitutes are the tray extra, the in-window keyboard transport, and the background track toast: 0.8.4 added `fx.showNotification`, so a track change while the app is backgrounded posts a desktop notification. It is opt-in (back panel → NOTIFICATIONS) and the whole decision lives in `Model.shouldNotifyTrack` — pure, because the effect is inert and unrecorded under fake execution, so no test can observe the call itself.
- SDK 0.8.4 also has **no audio output-device API** — the platform seam is load/play/pause/stop/seek/volume, with no enumeration and no device property, so there is no in-app "play through these speakers" picker to build. Route it at the OS (`pavucontrol`/PipeWire, Windows volume mixer; macOS has nothing native). The upstream request lives in `docs/sdk-audio-device-request.md`.
diff --git a/src/main.zig b/src/main.zig
index 548053d..32c1308 100644
--- a/src/main.zig
+++ b/src/main.zig
@@ -163,12 +163,18 @@ fn onLifecycle(event: native_sdk.LifecycleEvent) ?Msg {
};
}
-// Hidden-titlebar chrome geometry → model (masthead pads around the traffic
-// lights). All-zero on standard chrome / fullscreen / non-macOS.
+// Hidden-titlebar chrome geometry → model (the masthead pads around the window
+// controls). BOTH horizontal edges are mapped because the cluster sits on a
+// different one per platform: macOS puts the traffic lights left, Windows puts
+// min/max/close right. Dropping `right` left the gear button underneath the DWM
+// caption buttons on Windows, which also fed the host's caption-colour sampler
+// a glyph edge instead of flat header — see CLAUDE.md's gotchas. All-zero on
+// standard chrome and in fullscreen.
fn onChrome(chrome: native_sdk.platform.WindowChrome) ?Msg {
return Msg{ .chrome_changed = .{
.top = chrome.insets.top,
.leading = chrome.insets.left,
+ .trailing = chrome.insets.right,
} };
}
diff --git a/src/model.zig b/src/model.zig
index cf31567..c9bdf53 100644
--- a/src/model.zig
+++ b/src/model.zig
@@ -165,10 +165,14 @@ pub const DayTab = struct {
label: []const u8,
};
-/// Payload for chrome_changed (hidden-titlebar insets, macOS).
+/// Payload for chrome_changed (hidden-titlebar insets). The window-control
+/// cluster lands on the edge its platform puts it on — macOS reports the
+/// traffic lights as `leading`, Windows reports min/max/close as `trailing` —
+/// so the masthead pads BOTH and the unused edge is honestly zero.
pub const ChromeInsets = struct {
top: f32 = 0,
leading: f32 = 0,
+ trailing: f32 = 0,
};
// ------------------------------------------------------------------ model
@@ -317,9 +321,11 @@ pub const Model = struct {
sheet: Sheet = .none,
mini_open: bool = false,
- // hidden-titlebar chrome insets (macOS traffic lights)
+ // hidden-titlebar chrome insets (macOS traffic lights lead, Windows
+ // min/max/close trails)
chrome_top: f32 = 0,
chrome_leading: f32 = 0,
+ chrome_trailing: f32 = 0,
// sleep timer (wall-clock deadline; 0 = off)
sleep_deadline_ms: i64 = 0,
@@ -2731,6 +2737,7 @@ pub fn update(model: *Model, msg: Msg, fx: *Effects) void {
.chrome_changed => |c| {
model.chrome_top = c.top;
model.chrome_leading = c.leading;
+ model.chrome_trailing = c.trailing;
},
// ------------------------------------------------------ request slip
@@ -4357,6 +4364,24 @@ test "update check: newer tag arms the notice, older clears it, failures keep st
try testing.expect(m.update_available());
}
+test "chrome insets land on both edges, so the masthead clears controls on any platform" {
+ var fx = Effects.init(testing.allocator);
+ defer fx.deinit();
+ fx.executor = .fake;
+ var m: Model = .{};
+ // macOS shape: traffic lights lead, nothing trails.
+ update(&m, .{ .chrome_changed = .{ .top = 52, .leading = 78, .trailing = 0 } }, &fx);
+ try testing.expectEqual(@as(f32, 52), m.chrome_top);
+ try testing.expectEqual(@as(f32, 78), m.chrome_leading);
+ try testing.expectEqual(@as(f32, 0), m.chrome_trailing);
+ // Windows shape: the min/max/close cluster trails instead. Dropping it is
+ // what put the gear button under the DWM caption buttons, whose colour the
+ // host samples from the pixel just leading of the cluster every present.
+ update(&m, .{ .chrome_changed = .{ .top = 32, .leading = 0, .trailing = 138 } }, &fx);
+ try testing.expectEqual(@as(f32, 0), m.chrome_leading);
+ try testing.expectEqual(@as(f32, 138), m.chrome_trailing);
+}
+
test "log_dir_value reports the diagnostic log folder" {
const m: Model = .{};
// Unopened in the suite, so it reads empty rather than crashing.
diff --git a/src/views/player-top.native b/src/views/player-top.native
index 0cb849e..73fb430 100644
--- a/src/views/player-top.native
+++ b/src/views/player-top.native
@@ -25,6 +25,12 @@
Back panel
+
+