Skip to content
Open
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
1 change: 1 addition & 0 deletions desktop/src-tauri/capabilities/default.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"core:window:allow-request-user-attention",
"core:window:allow-set-focus",
"core:window:allow-start-dragging",
"core:window:allow-set-decorations",
"core:window:allow-toggle-maximize",
"core:window:allow-unminimize",
"core:window:allow-show",
Expand Down
2 changes: 2 additions & 0 deletions desktop/src/app/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
import { deriveShellRoute } from "@/app/AppShell.helpers";
import { ThemeGrainientBackground } from "@/app/ThemeGrainientBackground";
import { useReloadShortcut } from "@/app/useReloadShortcut";
import { useWindowDecorationsPreference } from "@/app/useWindowDecorationsPreference";
import { KnownAgentPubkeysProvider } from "@/features/agents/useKnownAgentPubkeys";
import { useAppOnboardingState } from "@/features/onboarding/hooks";
import { useMachineOnboardingState } from "@/features/onboarding/machineOnboarding";
Expand Down Expand Up @@ -673,6 +674,7 @@ function MachineBootstrap({ sharedIdentity }: { sharedIdentity: boolean }) {

export function App() {
useReloadShortcut();
useWindowDecorationsPreference();
useInitialRenderReady();
const [sharedIdentity, setSharedIdentity] = useState<boolean | null>(null);
const [queryClient] = useState(createBuzzQueryClient);
Expand Down
22 changes: 22 additions & 0 deletions desktop/src/app/useWindowDecorationsPreference.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { isTauri } from "@tauri-apps/api/core";
import { getCurrentWindow } from "@tauri-apps/api/window";
import * as React from "react";

import { useWindowDecorationsVisible } from "@/shared/lib/windowDecorationsPreference";

/** Applies the persisted native window-frame preference to the Tauri window. */
export function useWindowDecorationsPreference() {
const decorationsVisible = useWindowDecorationsVisible();

React.useEffect(() => {
if (!isTauri()) {
return;
}

void getCurrentWindow()
.setDecorations(decorationsVisible)
.catch((error) => {
console.warn("Unable to update native window decorations:", error);
});
}, [decorationsVisible]);
}
36 changes: 35 additions & 1 deletion desktop/src/features/settings/ui/SettingsPanels.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ import {
type ThreadViewMode,
} from "@/features/channels/lib/threadViewModePreference";
import { cn } from "@/shared/lib/cn";
import {
setWindowDecorationsVisible,
useWindowDecorationsVisible,
} from "@/shared/lib/windowDecorationsPreference";
import { Button } from "@/shared/ui/button";
import {
DropdownMenu,
Expand Down Expand Up @@ -84,6 +88,7 @@ import { SettingsOptionGroup, SettingsOptionRow } from "./SettingsOptionGroup";
import { ProfileSettingsCard } from "./ProfileSettingsCard";
import { UpdateChecker } from "../UpdateChecker";
import { SettingsSectionHeader } from "./SettingsSectionHeader";
import { Switch } from "@/shared/ui/switch";

export type SettingsSection =
| "profile"
Expand Down Expand Up @@ -647,11 +652,40 @@ function ThemeSettingsCard() {
</AnimatePresence>
)}

<WindowDecorationsSetting />
<ThreadLayoutSetting />
</section>
);
}

function WindowDecorationsSetting() {
const decorationsVisible = useWindowDecorationsVisible();

return (
<SettingsOptionGroup className="mt-8">
<SettingsOptionRow>
<div className="min-w-0">
<label
className="text-sm font-medium"
htmlFor="native-window-controls-switch"
>
Native window controls
</label>
<p className="text-sm font-normal text-muted-foreground">
Show the title bar with minimize, maximize, and close controls.
</p>
</div>
<Switch
checked={decorationsVisible}
data-testid="native-window-controls-toggle"
id="native-window-controls-switch"
onCheckedChange={setWindowDecorationsVisible}
/>
</SettingsOptionRow>
</SettingsOptionGroup>
);
}

const THREAD_VIEW_MODE_OPTIONS: {
value: ThreadViewMode;
label: string;
Expand Down Expand Up @@ -682,7 +716,7 @@ function ThreadLayoutSetting() {
) ?? THREAD_VIEW_MODE_OPTIONS[0];

return (
<SettingsOptionGroup className="mt-8">
<SettingsOptionGroup className="mt-3">
<SettingsOptionRow>
<div className="min-w-0">
<p className="text-sm font-medium">Thread layout</p>
Expand Down
80 changes: 80 additions & 0 deletions desktop/src/shared/lib/windowDecorationsPreference.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import assert from "node:assert/strict";
import test from "node:test";

const KEY = "buzz.window.nativeDecorations";
let importSequence = 0;

async function withStorage(storage, run) {
const descriptor = Object.getOwnPropertyDescriptor(
globalThis,
"localStorage",
);
Object.defineProperty(globalThis, "localStorage", {
configurable: true,
value: storage,
});
try {
const module = await import(
`./windowDecorationsPreference.ts?test=${importSequence++}`
);
await run(module);
} finally {
if (descriptor)
Object.defineProperty(globalThis, "localStorage", descriptor);
else delete globalThis.localStorage;
}
}

test("missing, malformed, and unreadable preferences default to visible", async () => {
for (const stored of [null, "hidden", "{bad-json"]) {
await withStorage(
{ getItem: (key) => (key === KEY ? stored : null), setItem() {} },
({ getWindowDecorationsVisible }) => {
assert.equal(getWindowDecorationsVisible(), true);
},
);
}

await withStorage(
{
getItem() {
throw new Error("storage unavailable");
},
setItem() {},
},
({ getWindowDecorationsVisible }) => {
assert.equal(getWindowDecorationsVisible(), true);
},
);
});

test("loads and writes the native decoration preference", async () => {
const writes = [];
await withStorage(
{
getItem: (key) => (key === KEY ? "false" : null),
setItem: (key, value) => writes.push([key, value]),
},
({ getWindowDecorationsVisible, setWindowDecorationsVisible }) => {
assert.equal(getWindowDecorationsVisible(), false);
setWindowDecorationsVisible(true);
assert.equal(getWindowDecorationsVisible(), true);
assert.deepEqual(writes, [[KEY, "true"]]);
},
);
});

test("keeps the in-memory choice when persistence fails", async () => {
await withStorage(
{
getItem: () => "true",
setItem() {
throw new Error("quota exceeded");
},
},
({ getWindowDecorationsVisible, setWindowDecorationsVisible }) => {
assert.doesNotThrow(() => setWindowDecorationsVisible(false));
assert.equal(getWindowDecorationsVisible(), false);
},
);
});
66 changes: 66 additions & 0 deletions desktop/src/shared/lib/windowDecorationsPreference.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import * as React from "react";

const STORAGE_KEY = "buzz.window.nativeDecorations";
const DEFAULT_WINDOW_DECORATIONS_VISIBLE = true;

const listeners = new Set<() => void>();

let windowDecorationsVisible = readStoredWindowDecorationsVisible();

function parseWindowDecorationsVisible(
value: string | null | undefined,
): boolean {
if (value === "true") return true;
if (value === "false") return false;
return DEFAULT_WINDOW_DECORATIONS_VISIBLE;
}

function readStoredWindowDecorationsVisible(): boolean {
try {
return parseWindowDecorationsVisible(
globalThis.localStorage?.getItem(STORAGE_KEY),
);
} catch {
return DEFAULT_WINDOW_DECORATIONS_VISIBLE;
}
}

function subscribe(listener: () => void): () => void {
listeners.add(listener);
return () => {
listeners.delete(listener);
};
}

function getSnapshot(): boolean {
return windowDecorationsVisible;
}

function getServerSnapshot(): boolean {
return DEFAULT_WINDOW_DECORATIONS_VISIBLE;
}

/** Read the native window-frame preference outside React. */
export function getWindowDecorationsVisible(): boolean {
return windowDecorationsVisible;
}

/** Update whether the operating system draws Buzz's native window frame. */
export function setWindowDecorationsVisible(visible: boolean): void {
windowDecorationsVisible = visible;

try {
globalThis.localStorage?.setItem(STORAGE_KEY, String(visible));
} catch {
// Persistence is best-effort; the in-memory preference still applies.
}

for (const listener of listeners) {
listener();
}
}

/** Whether the native title bar and its window controls should be visible. */
export function useWindowDecorationsVisible(): boolean {
return React.useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
}