diff --git a/AGENTS.md b/AGENTS.md index 870ce888..b9c7aa34 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -55,7 +55,10 @@ migrated away. See `plans/rust-migration/`.) That file grants Tauri core/plugin permissions; app-defined commands are reachable without ACL entries, and adding one would switch the app to default-deny and break every command not migrated at the same time. - `tauri.conf.json` is the app manifest. + `tauri.conf.json` is the app manifest. Platform overlays + (`tauri.macos.conf.json`) merge as RFC 7396 — arrays are replaced wholesale, + so an overlaid `windows` entry must restate every base field; a unit test + pins the macOS entry to the base one. - `src/` — SolidJS frontend. - `screens/` — `Onboarding`, `Settings`, `History`, `RunHistory`, and `workbench/` (Workbench, InspectorPanel, ProjectsChatsPanel, FileExplorer). diff --git a/README.md b/README.md index 6b94394c..b79c1b2d 100644 --- a/README.md +++ b/README.md @@ -42,8 +42,11 @@ Requires a [Rust toolchain](https://rustup.rs) and [Bun](https://bun.sh) 1.2+. P on [Tauri v2](https://tauri.app) — a Rust core (`crates/pickforge-core`) behind a Tauri shell (`src-tauri/`) with a SolidJS frontend (`src/`). -The window uses custom chrome (`decorations: false`) — one draggable title bar -with the brand, nav, status, and min/maximize/close controls. On Linux/Wayland, +The window uses custom chrome (`decorations: false`) on Windows/Linux — one +draggable title bar with the brand, nav, status, and min/maximize/close +controls. macOS is decorated instead (`src-tauri/tauri.macos.conf.json`: overlay +title bar, hidden title), so the system draws the rounded corners, shadow, and +traffic lights over the same title bar content. On Linux/Wayland, the window app_id is forced to the bundle identifier `dev.pickforge.app` at startup via `gtk::glib::set_prgname` in `src-tauri/src/lib.rs` (`enableGTKAppId` alone doesn't set the xdg_toplevel app_id under WebKitGTK — GTK derives it from diff --git a/docs/releases/UNRELEASED.md b/docs/releases/UNRELEASED.md index f94a2ad4..b56d7c77 100644 --- a/docs/releases/UNRELEASED.md +++ b/docs/releases/UNRELEASED.md @@ -6,6 +6,10 @@ reset this file. ## User-facing changes +- The macOS window now uses native chrome: system rounded corners, shadow, + and traffic lights via a titlebar overlay (the custom window-control + buttons and edge resize handles are macOS-native now). Windows/Linux keep + the existing frameless chrome. - The Codex agent-model picker now merges live-discovered models (`codex debug models --bundled`) with the curated static table, so new Codex releases can show up without a PickForge update. Curated entries keep diff --git a/src-tauri/tauri.macos.conf.json b/src-tauri/tauri.macos.conf.json new file mode 100644 index 00000000..86f50a98 --- /dev/null +++ b/src-tauri/tauri.macos.conf.json @@ -0,0 +1,22 @@ +{ + "app": { + "windows": [ + { + "label": "main", + "title": "PickForge", + "width": 1280, + "height": 820, + "minWidth": 880, + "minHeight": 600, + "decorations": true, + "titleBarStyle": "Overlay", + "hiddenTitle": true, + "trafficLightPosition": { "x": 14, "y": 13 }, + "transparent": false, + "backgroundColor": "#0a0a0b", + "dragDropEnabled": true, + "acceptFirstMouse": true + } + ] + } +} diff --git a/src/components/ResizeHandles.tsx b/src/components/ResizeHandles.tsx index 1d95b306..2585fb09 100644 --- a/src/components/ResizeHandles.tsx +++ b/src/components/ResizeHandles.tsx @@ -1,10 +1,11 @@ import { For } from "solid-js"; -import { isTauri } from "../lib/platform"; +import { hostPlatform, isTauri } from "../lib/platform"; // A frameless window (decorations: false) loses the OS resize border and its // resize cursors. These thin edge + corner zones restore the affordance: each // shows the matching resize cursor and starts a native resize-drag on press, -// so the window resizes exactly like a decorated one. Rendered only under Tauri. +// so the window resizes exactly like a decorated one. Rendered only under Tauri, +// and never on macOS — the window is decorated there and resizes natively. const HANDLES = [ { dir: "North", cls: "n" }, { dir: "South", cls: "s" }, @@ -17,7 +18,7 @@ const HANDLES = [ ] as const; export function ResizeHandles() { - if (!isTauri()) return null; + if (!isTauri() || hostPlatform() === "macos") return null; const start = (dir: string) => async (e: MouseEvent) => { if (e.button !== 0) return; diff --git a/src/components/WindowControls.tsx b/src/components/WindowControls.tsx index b2c4a58c..7734b3d7 100644 --- a/src/components/WindowControls.tsx +++ b/src/components/WindowControls.tsx @@ -13,8 +13,9 @@ async function appWindow() { export function WindowControls() { // The web build (and Playwright VRT) has no window chrome — keep it identical - // to today by rendering nothing. - if (hostPlatform() === "web") return null; + // to today by rendering nothing. macOS uses the native traffic lights (the + // window is decorated with an overlay titlebar there), so no custom controls. + if (hostPlatform() === "web" || hostPlatform() === "macos") return null; const [maximized, setMaximized] = createSignal(false); @@ -104,18 +105,11 @@ export function WindowControls() { ); - // macOS renders controls on the LEFT and follows the traffic-light action - // order close → minimize → maximize (left→right). Windows/Linux render on the - // right with minimize → maximize → close. - const isMac = hostPlatform() === "macos"; - return (
- }> - - - - + + +
); } diff --git a/src/screens/Settings.tsx b/src/screens/Settings.tsx index 200987e6..7e47e6f0 100644 --- a/src/screens/Settings.tsx +++ b/src/screens/Settings.tsx @@ -2157,27 +2157,24 @@ const AppearanceContent = () => ( -
- - Window controls - - macOS · always left - - -
- - {(s) => ( - - )} - + {/* macOS has native traffic lights — there are no app-drawn controls to place. */} + +
+ Window controls +
+ + {(s) => ( + + )} + +
-
+ ); diff --git a/tests/unit/tauriMacosWindowConf.test.ts b/tests/unit/tauriMacosWindowConf.test.ts new file mode 100644 index 00000000..39023a87 --- /dev/null +++ b/tests/unit/tauriMacosWindowConf.test.ts @@ -0,0 +1,32 @@ +// The Tauri v2 platform-config merge (RFC 7396) replaces the `windows` array +// wholesale, so tauri.macos.conf.json must restate the full main-window entry. +// This guards against drift: a field edited in the base config but forgotten in +// the macOS overlay would silently not apply on macOS. +import { readFileSync } from "node:fs"; +import { describe, expect, it } from "vitest"; + +const MACOS_ONLY_FIELDS = ["decorations", "titleBarStyle", "hiddenTitle", "trafficLightPosition"]; + +function mainWindow(path: string): Record { + const conf = JSON.parse(readFileSync(path, "utf8")) as { + app: { windows: Array> }; + }; + const win = conf.app.windows.find((w) => w.label === "main"); + if (!win) throw new Error(`no main window in ${path}`); + return win; +} + +describe("tauri.macos.conf.json window entry", () => { + it("matches the base window entry apart from the macOS chrome fields", () => { + const base = mainWindow("src-tauri/tauri.conf.json"); + const macos = mainWindow("src-tauri/tauri.macos.conf.json"); + + const strip = (win: Record) => + Object.fromEntries(Object.entries(win).filter(([k]) => !MACOS_ONLY_FIELDS.includes(k))); + + expect(strip(macos)).toEqual(strip(base)); + expect(macos.decorations).toBe(true); + expect(macos.titleBarStyle).toBe("Overlay"); + expect(macos.hiddenTitle).toBe(true); + }); +});