From ce2767acd10a79672d10fe0eeffc62bf0aaf2a5e Mon Sep 17 00:00:00 2001 From: morgmart <98432065+morgmart@users.noreply.github.com> Date: Mon, 21 Sep 2026 15:05:26 -0700 Subject: [PATCH 1/3] Label dev worktrees and pause local notifications by default Signed-off-by: morgmart <98432065+morgmart@users.noreply.github.com> --- .env.example | 5 + dev/dev-desktop.test.mjs | 89 +++++++++++ dev/vite-config.test.mjs | 10 ++ docs/contributing.md | 8 + docs/notifications.md | 7 +- justfile | 2 +- scripts/dev-desktop.mjs | 73 +++++++++ scripts/generate-dev-icon.swift | 216 ++++++++++++++++++++++++++ src/app/NotificationSettings.test.tsx | 90 +++++++++++ src/app/NotificationSettings.tsx | 32 ++-- src/features/notifications/service.ts | 5 +- vite.config.ts | 3 + 12 files changed, 524 insertions(+), 16 deletions(-) create mode 100644 dev/dev-desktop.test.mjs create mode 100644 scripts/dev-desktop.mjs create mode 100755 scripts/generate-dev-icon.swift create mode 100644 src/app/NotificationSettings.test.tsx diff --git a/.env.example b/.env.example index e17457821..9cda97806 100644 --- a/.env.example +++ b/.env.example @@ -12,3 +12,8 @@ BUZZ_DEV_VIEWER= # Keep the same alias mapped to the same origin to retain existing memberships. # Values are public configuration included in the frontend bundle, never secrets. # BUZZ_COMMUNITY_ALIASES='{"primary":"wss://relay.example.com","secondary":"wss://other.example.com"}' + +# Local dev servers pause notifications by default, without changing saved choices. +# Opt in for notification testing; saved choices and OS permissions still apply. +# Restart the dev server after changing this. Production builds ignore it. +# BUZZ_DEV_NOTIFICATIONS=1 diff --git a/dev/dev-desktop.test.mjs b/dev/dev-desktop.test.mjs new file mode 100644 index 000000000..f5606c663 --- /dev/null +++ b/dev/dev-desktop.test.mjs @@ -0,0 +1,89 @@ +import { execFileSync } from "node:child_process"; +import { mkdtempSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { afterEach, expect, it, vi } from "vitest"; +import { desktopArgs, worktreeLabel } from "../scripts/dev-desktop.mjs"; + +const directories = []; +afterEach(() => { + for (const path of directories.splice(0)) + rmSync(path, { recursive: true, force: true }); +}); +function fixture() { + const base = mkdtempSync(join(tmpdir(), "buzz-dev-launch-")); + directories.push(base); + const main = join(base, "main"); + const linked = join(base, 'linked "checkout"'); + const git = (...args) => execFileSync("git", args, { stdio: "pipe" }); + git("init", "--initial-branch=main", main); + git( + "-C", + main, + "-c", + "user.name=Test", + "-c", + "user.email=test@example.com", + "-c", + "core.hooksPath=/dev/null", + "-c", + "commit.gpgsign=false", + "commit", + "--allow-empty", + "-m", + "fixture", + ); + git( + "-C", + main, + "-c", + "core.hooksPath=/dev/null", + "worktree", + "add", + "-b", + "person/onboarding-feedback", + linked, + ); + return { main, linked, git }; +} +it("labels linked worktrees by branch suffix and detached trees by directory", () => { + const { main, linked, git } = fixture(); + expect(worktreeLabel(main)).toBeNull(); + expect(worktreeLabel(linked)).toBe("onboarding-feedback"); + git("-C", linked, "-c", "core.hooksPath=/dev/null", "checkout", "--detach"); + expect(worktreeLabel(linked)).toBe('linked "checkout"'); +}); +it("overrides only the development icon, safely encoding checkout paths", () => { + const { linked } = fixture(); + const run = vi.fn(() => ({ status: 0 })); + const args = desktopArgs({ cwd: linked, platform: "darwin", run }); + expect(args.slice(0, 3)).toEqual(["tauri", "dev", "--config"]); + expect(JSON.parse(args[3])).toEqual({ + bundle: { icon: [join(linked, "src-tauri/target/dev-icons/icon.icns")] }, + }); + expect(run.mock.calls[0][1].at(-1)).toBe("onboarding-feedback"); +}); +it("leaves ordinary checkouts and non-macOS launches alone", () => { + const { main, linked } = fixture(); + const run = vi.fn(); + expect(desktopArgs({ cwd: main, platform: "darwin", run })).toEqual([ + "tauri", + "dev", + ]); + expect(desktopArgs({ cwd: linked, platform: "linux", run })).toEqual([ + "tauri", + "dev", + ]); + expect(run).not.toHaveBeenCalled(); +}); +it.each([{ status: 1 }, { status: null, error: new Error("Swift missing") }])( + "warns and falls back when icon generation fails: %j", + (result) => { + const { linked } = fixture(); + const warn = vi.fn(); + expect( + desktopArgs({ cwd: linked, platform: "darwin", run: () => result, warn }), + ).toEqual(["tauri", "dev"]); + expect(warn).toHaveBeenCalledOnce(); + }, +); diff --git a/dev/vite-config.test.mjs b/dev/vite-config.test.mjs index 898611403..f0ecb3cb1 100644 --- a/dev/vite-config.test.mjs +++ b/dev/vite-config.test.mjs @@ -19,6 +19,16 @@ it("loads the broker's Vite config without native-compatibility warnings", () => ); assert(loaded?.config.plugins.some(p => p?.name === 'buzz-relay-broker')); assert.equal(loaded.config.define['import.meta.env.VITE_BUZZ_LIVE'], '"1"'); + for (const command of ['serve', 'build']) { + for (const optIn of ['', '0', '1', 'true', 'invalid']) { + process.env.BUZZ_DEV_NOTIFICATIONS = optIn; + const result = await loadConfigFromFile({ command, mode: 'development' }); + assert.equal( + result.config.define['import.meta.env.VITE_BUZZ_NOTIFICATIONS_PAUSED'], + JSON.stringify(command === 'serve' && optIn !== '1' ? '1' : '0'), + ); + } + } `, ], { diff --git a/docs/contributing.md b/docs/contributing.md index 464929d33..77311cebb 100644 --- a/docs/contributing.md +++ b/docs/contributing.md @@ -47,6 +47,14 @@ need their own validation. - `just web`: install locked dependencies and start Vite on port 1430 or the next available port, allowing parallel browser development across worktrees. - `just desktop`: install locked dependencies and start Tauri, which starts Vite. + On macOS linked worktrees, the Dock icon displays the branch’s final name segment + (or the worktree directory name for detached HEAD). Relaunch after changing branches. + The generated icon stays in ignored build output; production icons, app identity, + and ports are unchanged. If generation fails, startup warns and uses the normal + icon. Direct `pnpm tauri dev` bypasses this label. +- Local dev servers pause notifications unless `BUZZ_DEV_NOTIFICATIONS=1` is set + in `.env.local`; restart the server after changing it. Saved preferences are + untouched and production builds ignore the variable. - `just fullstack`: reserved, exits unsuccessfully with an explanation. It will eventually start local Docker services including the Buzz relay backend. - `just iterate`: install locked dependencies, format Rust, apply Biome safe diff --git a/docs/notifications.md b/docs/notifications.md index 3b1e4971f..f09d3dfbb 100644 --- a/docs/notifications.md +++ b/docs/notifications.md @@ -2,7 +2,12 @@ The host provides one `notifications` service for built-in messages and trusted plugins. Settings → Notifications stores account-local choices: alerts are on -by default, subject to system permission; master off preserves category choices. +by default in production, subject to system permission; master off preserves category choices. +Local dev servers pause all alerts and permission requests unless +`BUZZ_DEV_NOTIFICATIONS=1` is set in `.env.local`. Restart the server after changing +it. This development gate never rewrites saved preferences; opting in still +honors account choices and system permission. Settings shows the pause and how to +enable notifications. Production builds ignore this variable. Browser sound uses the Notification API. Desktop sound is managed in OS settings; there is no separate audio player. diff --git a/justfile b/justfile index b054b841a..d95013a8e 100644 --- a/justfile +++ b/justfile @@ -14,7 +14,7 @@ web: install # Run the shared frontend in Tauri (requires native toolchain). desktop: install - pnpm tauri dev + node scripts/dev-desktop.mjs # Run buzzodz; forward arguments unchanged (Rust required). [positional-arguments] diff --git a/scripts/dev-desktop.mjs b/scripts/dev-desktop.mjs new file mode 100644 index 000000000..9cca633b9 --- /dev/null +++ b/scripts/dev-desktop.mjs @@ -0,0 +1,73 @@ +import { execFileSync, spawnSync } from "node:child_process"; +import { mkdirSync } from "node:fs"; +import { basename, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; + +const root = fileURLToPath(new URL("../", import.meta.url)); + +export function worktreeLabel(cwd) { + const git = (...args) => + execFileSync("git", args, { + cwd, + encoding: "utf8", + stdio: ["ignore", "pipe", "pipe"], + }).trim(); + try { + if ( + git("rev-parse", "--path-format=absolute", "--git-dir") === + git("rev-parse", "--path-format=absolute", "--git-common-dir") + ) + return null; + const branch = git("branch", "--show-current"); + return branch + ? branch.split("/").at(-1) + : basename(git("rev-parse", "--show-toplevel")); + } catch { + return null; + } +} + +export function desktopArgs({ + cwd = root, + platform = process.platform, + run = spawnSync, + warn = console.warn, +} = {}) { + const args = ["tauri", "dev"]; + if (platform !== "darwin") return args; + const label = worktreeLabel(cwd); + if (!label) return args; + try { + const icon = resolve(cwd, "src-tauri/target/dev-icons/icon.icns"); + mkdirSync(resolve(cwd, "src-tauri/target/dev-icons"), { recursive: true }); + const result = run( + "swift", + [ + resolve(cwd, "scripts/generate-dev-icon.swift"), + resolve(cwd, "src-tauri/icons/icon.icns"), + icon, + label, + ], + { cwd, stdio: "inherit" }, + ); + if (result.error || result.status !== 0) + throw result.error ?? new Error("Icon generator failed"); + args.push("--config", JSON.stringify({ bundle: { icon: [icon] } })); + } catch { + warn("Worktree icon could not be generated; using the ordinary Buzz icon."); + } + return args; +} + +if ( + process.argv[1] && + resolve(process.argv[1]) === fileURLToPath(import.meta.url) +) { + const result = spawnSync( + resolve(root, "bin/pnpm"), + [...desktopArgs(), ...process.argv.slice(2)], + { cwd: root, stdio: "inherit" }, + ); + if (result.error) console.error(result.error.message); + process.exitCode = result.status ?? 1; +} diff --git a/scripts/generate-dev-icon.swift b/scripts/generate-dev-icon.swift new file mode 100755 index 000000000..b324478cc --- /dev/null +++ b/scripts/generate-dev-icon.swift @@ -0,0 +1,216 @@ +#!/usr/bin/env swift + +import AppKit +import Foundation + +// Ported from block/buzz scripts/generate-dev-icon.swift. +// Generate a dev icon with worktree name badge +// Usage: generate-dev-icon.swift