Skip to content
This repository was archived by the owner on Sep 4, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down
7 changes: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions docs/releases/UNRELEASED.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
22 changes: 22 additions & 0 deletions src-tauri/tauri.macos.conf.json
Original file line number Diff line number Diff line change
@@ -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
}
]
}
}
7 changes: 4 additions & 3 deletions src/components/ResizeHandles.tsx
Original file line number Diff line number Diff line change
@@ -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" },
Expand All @@ -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;
Expand Down
18 changes: 6 additions & 12 deletions src/components/WindowControls.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down Expand Up @@ -104,18 +105,11 @@ export function WindowControls() {
</button>
);

// 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 (
<div class="pf-winctl" role="group" aria-label="Window controls">
<Show when={isMac} fallback={<><Minimize /><Maximize /><Close /></>}>
<Close />
<Minimize />
<Maximize />
</Show>
<Minimize />
<Maximize />
<Close />
</div>
);
}
Expand Down
37 changes: 17 additions & 20 deletions src/screens/Settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2157,27 +2157,24 @@ const AppearanceContent = () => (
<button class="pf-text-btn" onClick={zoomReset}>Reset</button>
</div>
</div>
<div class="pf-settings-row">
<span class="pf-settings-label">
Window controls
<Show when={hostPlatform() === "macos"}>
<span class="pf-settings-hint-inline">macOS · always left</span>
</Show>
</span>
<div class="pf-seg" classList={{ "pf-seg--disabled": hostPlatform() === "macos" }}>
<For each={["auto", "left", "right"] as ControlsSide[]}>
{(s) => (
<button
classList={{ active: windowControlsSide() === s }}
disabled={hostPlatform() === "macos"}
onClick={() => setWindowControlsSide(s)}
>
{s === "auto" ? "Auto" : s === "left" ? "Left" : "Right"}
</button>
)}
</For>
{/* macOS has native traffic lights — there are no app-drawn controls to place. */}
<Show when={hostPlatform() !== "macos"}>
<div class="pf-settings-row">
<span class="pf-settings-label">Window controls</span>
<div class="pf-seg">
<For each={["auto", "left", "right"] as ControlsSide[]}>
{(s) => (
<button
classList={{ active: windowControlsSide() === s }}
onClick={() => setWindowControlsSide(s)}
>
{s === "auto" ? "Auto" : s === "left" ? "Left" : "Right"}
</button>
)}
</For>
</div>
</div>
</div>
</Show>
</>
);

Expand Down
32 changes: 32 additions & 0 deletions tests/unit/tauriMacosWindowConf.test.ts
Original file line number Diff line number Diff line change
@@ -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<string, unknown> {
const conf = JSON.parse(readFileSync(path, "utf8")) as {
app: { windows: Array<Record<string, unknown>> };
};
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<string, unknown>) =>
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);
});
});
Loading