From a0e334763921586d44e9bb23cf56fe187e11ad94 Mon Sep 17 00:00:00 2001
From: debpalash <4178343+debpalash@users.noreply.github.com>
Date: Tue, 15 Sep 2026 12:13:12 +0530
Subject: [PATCH 1/3] fix(electron): refine macOS sidebar and titlebar controls
---
docs/electron-macos-shell.md | 13 ++
.../src/components/app-shell/app-shell.tsx | 10 +-
.../src/components/app-shell/status-bar.tsx | 158 +++++++++++-------
.../app-shell/system-notifications.test.tsx | 27 +++
.../app-shell/system-notifications.tsx | 39 +++--
.../app-shell/workspace-sidebar.tsx | 105 ++++++++----
electron/src/renderer/src/styles/globals.css | 3 +
electron/tests/sidebar-layout-smoke.mjs | 125 +++++++++++++-
8 files changed, 375 insertions(+), 105 deletions(-)
create mode 100644 docs/electron-macos-shell.md
diff --git a/docs/electron-macos-shell.md b/docs/electron-macos-shell.md
new file mode 100644
index 00000000..656f3e31
--- /dev/null
+++ b/docs/electron-macos-shell.md
@@ -0,0 +1,13 @@
+# macOS desktop shell
+
+The expanded sidebar reserves space for the native traffic lights and app name.
+The collapsed sidebar is 64 px wide, with its toggle below the traffic lights
+and its right divider beginning below the 72 px header region.
+
+Notifications appear at the top right, with space reserved before the bell.
+The notification menu opens downward and remains available while notification
+data loads. Settings uses an icon in the macOS sidebar footer; Local device
+sits beside it and opens the device and compute-target menu. The expanded
+sidebar retains the Local device label.
+
+Windows and Linux retain their existing notification and device placement.
diff --git a/electron/src/renderer/src/components/app-shell/app-shell.tsx b/electron/src/renderer/src/components/app-shell/app-shell.tsx
index 8af65cac..a93d0934 100644
--- a/electron/src/renderer/src/components/app-shell/app-shell.tsx
+++ b/electron/src/renderer/src/components/app-shell/app-shell.tsx
@@ -3,15 +3,23 @@ import { CommandPalette } from '@/components/command-palette';
import { Outlet, useRouterState } from '@tanstack/react-router';
import { BackendGate } from '../backend-gate';
import { RepairAgentDock } from './repair-agent-dock';
+import { isMac } from '../bridge';
+import { cn } from '@/lib/utils';
export function AppShell() {
const pathname = useRouterState({
select: (state) => state.location.pathname,
});
const settings = pathname.startsWith('/settings');
+ const macWorkspace = isMac() && !settings;
const SettingsWorkspace = pathname === '/settings/openapi' ? 'div' : 'main';
return (
-
+
{settings ? (
<>
diff --git a/electron/src/renderer/src/components/app-shell/status-bar.tsx b/electron/src/renderer/src/components/app-shell/status-bar.tsx
index ffee9b54..14db5513 100644
--- a/electron/src/renderer/src/components/app-shell/status-bar.tsx
+++ b/electron/src/renderer/src/components/app-shell/status-bar.tsx
@@ -1,4 +1,4 @@
-import { useRef, useState } from 'react';
+import { useRef, useState, type ReactNode } from 'react';
import { Tooltip, TooltipTrigger, TooltipContent } from '@/components/ui/tooltip';
import { useTranslationEngines } from '@/features/settings/translation-settings';
import { Link } from '@tanstack/react-router';
@@ -222,7 +222,15 @@ function EngineTip({
);
}
-export function StatusBar({ compact = false }: { compact?: boolean }) {
+export function StatusBar({
+ compact = false,
+ inline = false,
+ footerLeading,
+}: {
+ compact?: boolean;
+ inline?: boolean;
+ footerLeading?: ReactNode;
+}) {
const { t } = useTranslation();
const [expanded, setExpanded] = useState(false);
const [deviceOpen, setDeviceOpen] = useState(false);
@@ -661,72 +669,88 @@ export function StatusBar({ compact = false }: { compact?: boolean }) {
: 'modelSettings.unavailable',
},
];
+ const iconDevicePopover = (
+
+
+ }
+ >
+
+ {!compact && {deviceLabel}}
+
+
+ {deviceContent}
+
+ );
if (compact) {
return (
-
)}
+ {footerLeading && (
+
+ {footerLeading}
+
{iconDevicePopover}
+
+
+ )}
);
diff --git a/electron/src/renderer/src/components/app-shell/system-notifications.test.tsx b/electron/src/renderer/src/components/app-shell/system-notifications.test.tsx
index 33ba714b..378e7e8e 100644
--- a/electron/src/renderer/src/components/app-shell/system-notifications.test.tsx
+++ b/electron/src/renderer/src/components/app-shell/system-notifications.test.tsx
@@ -46,6 +46,13 @@ describe('SystemNotifications desktop updates', () => {
beforeEach(() => {
mocks.navigate.mockReset();
mocks.listener = undefined;
+ mocks.state = {
+ status: 'available',
+ currentVersion: '0.5.2',
+ availableVersion: '0.5.3',
+ channel: 'stable',
+ progress: 0,
+ } as UpdateState;
});
afterEach(cleanup);
@@ -62,4 +69,24 @@ describe('SystemNotifications desktop updates', () => {
fireEvent.click(await screen.findByText('common.open'));
await waitFor(() => expect(mocks.navigate).toHaveBeenCalledWith({ to: '/settings/updates' }));
});
+
+ test('opens while notifications are still loading', async () => {
+ mocks.state = {
+ status: 'idle',
+ currentVersion: '0.5.2',
+ channel: 'stable',
+ progress: 0,
+ } as UpdateState;
+ render(
+
+
+ ,
+ );
+
+ const trigger = await screen.findByRole('button', { name: 'preferences.loading' });
+ expect(trigger).toBeEnabled();
+ expect(trigger).toHaveClass('app-no-drag');
+ fireEvent.click(trigger);
+ expect(await screen.findByText('preferences.loading')).toBeVisible();
+ });
});
diff --git a/electron/src/renderer/src/components/app-shell/system-notifications.tsx b/electron/src/renderer/src/components/app-shell/system-notifications.tsx
index d186452b..72de9958 100644
--- a/electron/src/renderer/src/components/app-shell/system-notifications.tsx
+++ b/electron/src/renderer/src/components/app-shell/system-notifications.tsx
@@ -83,9 +83,11 @@ function LevelIcon({ level }: { level: SystemNotification['level'] }) {
export function SystemNotifications({
enabled,
compact = false,
+ titlebar = false,
}: {
enabled: boolean;
compact?: boolean;
+ titlebar?: boolean;
}) {
const { t } = useTranslation();
const navigate = useNavigate();
@@ -120,24 +122,20 @@ export function SystemNotifications({
return {
id: `desktop-update-${update.availableVersion}`,
level: 'info',
- title: t(
- update.status === 'downloaded' ? 'update.ready' : 'update.available',
- { version: update.availableVersion },
- ),
+ title: t(update.status === 'downloaded' ? 'update.ready' : 'update.available', {
+ version: update.availableVersion,
+ }),
message: t('update.safety'),
action: { type: 'navigate', target: '/settings/updates', label: t('common.open') },
persistent: true,
};
}, [t, update]);
- const visible = useMemo(
- () => {
- const backend = (query.data?.notifications ?? []).filter(
- (note) => note.level === 'error' || !dismissed.includes(note.id),
- );
- return updateNotification ? [updateNotification, ...backend] : backend;
- },
- [dismissed, query.data?.notifications, updateNotification],
- );
+ const visible = useMemo(() => {
+ const backend = (query.data?.notifications ?? []).filter(
+ (note) => note.level === 'error' || !dismissed.includes(note.id),
+ );
+ return updateNotification ? [updateNotification, ...backend] : backend;
+ }, [dismissed, query.data?.notifications, updateNotification]);
const dismiss = (id: string) => {
const next = [...dismissed.filter((item) => item !== id), id].slice(-50);
@@ -186,9 +184,11 @@ export function SystemNotifications({
}
>
@@ -206,10 +206,15 @@ export function SystemNotifications({
)}
+ {query.isPending && visible.length === 0 && (
+
+ {t('preferences.loading')}
+
+ )}
{!query.isPending && !query.isError && visible.length === 0 && (
{t('logs.all_clear')}
diff --git a/electron/src/renderer/src/components/app-shell/workspace-sidebar.tsx b/electron/src/renderer/src/components/app-shell/workspace-sidebar.tsx
index f6bde9e4..d2c33637 100644
--- a/electron/src/renderer/src/components/app-shell/workspace-sidebar.tsx
+++ b/electron/src/renderer/src/components/app-shell/workspace-sidebar.tsx
@@ -60,6 +60,7 @@ function useCompactViewport(): boolean {
export function WorkspaceSidebar() {
const backend = useBackendStatus();
const { t } = useTranslation();
+ const mac = isMac();
const { libraryOpen, libraryTab } = useWorkspace();
const pathname = useRouterState({ select: (state) => state.location.pathname });
const compactViewport = useCompactViewport();
@@ -76,48 +77,79 @@ export function WorkspaceSidebar() {
const sidebarResize = usePaneResize({
storageKey: 'voicestudio.library-width',
side: 'left',
- minimum: 220,
- initial: 256,
+ minimum: mac ? 288 : 220,
+ initial: mac ? 288 : 256,
maximum: 360,
reserve: compactViewport && secondaryWorkspace && forceExpanded ? 520 : 640,
enabled: libraryOpen,
});
return (
<>
+ {mac && (
+
+
+
+ )}
{compact && (
);
}
diff --git a/electron/src/renderer/src/components/app-shell/workspace-sidebar.tsx b/electron/src/renderer/src/components/app-shell/workspace-sidebar.tsx
index d2c33637..63f12bf4 100644
--- a/electron/src/renderer/src/components/app-shell/workspace-sidebar.tsx
+++ b/electron/src/renderer/src/components/app-shell/workspace-sidebar.tsx
@@ -85,14 +85,6 @@ export function WorkspaceSidebar() {
});
return (
<>
- {mac && (
-