Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
4 changes: 2 additions & 2 deletions src/renderer/app/app-menu-events.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { useEffect } from 'react';
import { menuOpenSettingsChannel } from '@shared/events/appEvents';
import { events } from '@renderer/lib/ipc';
import { useNavigate, useWorkspaceSlots } from '@renderer/lib/layout/navigation-provider';
import { toggleSettingsView } from '@renderer/lib/layout/settings-toggle';

export function AppMenuEvents({ onOpenSettings }: { onOpenSettings?: () => boolean | void }) {
const { navigate } = useNavigate();
Expand All @@ -11,9 +12,8 @@ export function AppMenuEvents({ onOpenSettings }: { onOpenSettings?: () => boole
return events.on(menuOpenSettingsChannel, () => {
const shouldOpen = onOpenSettings?.() ?? true;
if (shouldOpen === false) return;
if (currentView === 'settings') return;

navigate('settings');
toggleSettingsView(navigate, currentView);
});
Comment on lines 14 to 21
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 onOpenSettings guard runs on the close path too

onOpenSettings?.() is now called even when currentView === 'settings' (i.e., the user is toggling settings off). In App.tsx, the concrete implementation calls setView('workspace') as a side effect every time — including on close — and returns false (blocking navigation) when the app is in onboarding. While those specific side effects are harmless today, the callback is semantically an "open" guard and was never designed to control the close path. The fix is to call onOpenSettings only when the intent is to open: check currentView !== 'settings' before invoking the callback.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/renderer/app/app-menu-events.tsx
Line: 12-17

Comment:
**`onOpenSettings` guard runs on the close path too**

`onOpenSettings?.()` is now called even when `currentView === 'settings'` (i.e., the user is toggling settings *off*). In `App.tsx`, the concrete implementation calls `setView('workspace')` as a side effect every time — including on close — and returns `false` (blocking navigation) when the app is in onboarding. While those specific side effects are harmless today, the callback is semantically an "open" guard and was never designed to control the close path. The fix is to call `onOpenSettings` only when the intent is to open: check `currentView !== 'settings'` before invoking the callback.

How can I resolve this? If you propose a fix, please make it concise.

}, [navigate, onOpenSettings, currentView]);

Expand Down
5 changes: 2 additions & 3 deletions src/renderer/lib/components/app-keyboard-shortcuts.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
useParams,
useWorkspaceSlots,
} from '@renderer/lib/layout/navigation-provider';
import { toggleSettingsView } from '@renderer/lib/layout/settings-toggle';
import { useShowModal } from '@renderer/lib/modal/modal-provider';

/**
Expand Down Expand Up @@ -54,9 +55,7 @@ export function AppKeyboardShortcuts() {

useHotkey(
getHotkeyRegistration('settings', keyboard),
() => {
if (currentView !== 'settings') navigate('settings');
},
() => toggleSettingsView(navigate, currentView),
{ enabled: settingsHotkey !== null }
);

Expand Down
26 changes: 26 additions & 0 deletions src/renderer/lib/layout/settings-toggle.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import type { ViewId } from '@renderer/app/view-registry';
import type { NavigateFnTyped } from './navigation-provider';

type NonSettingsViewId = Exclude<ViewId, 'settings'>;

let previousView: NonSettingsViewId = 'home';
let lastToggleAt = 0;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Module-level previousView shared across all toggle callers

previousView and lastToggleAt are module-level singletons. Both AppMenuEvents and AppKeyboardShortcuts call toggleSettingsView, so a menu-triggered toggle and a keyboard-triggered toggle that happen to arrive outside the 150 ms dedup window (e.g., under load) will each read and overwrite the same previousView. More broadly, if a future caller opens settings through a different code path that doesn't go through this helper, previousView will be stale ('home') on the next close, silently dropping the user's actual context. The guard is correct for the current callers; the risk is for future additions.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/renderer/lib/layout/settings-toggle.ts
Line: 5-7

Comment:
**Module-level `previousView` shared across all toggle callers**

`previousView` and `lastToggleAt` are module-level singletons. Both `AppMenuEvents` and `AppKeyboardShortcuts` call `toggleSettingsView`, so a menu-triggered toggle and a keyboard-triggered toggle that happen to arrive outside the 150 ms dedup window (e.g., under load) will each read and overwrite the same `previousView`. More broadly, if a future caller opens settings through a different code path that doesn't go through this helper, `previousView` will be stale (`'home'`) on the next close, silently dropping the user's actual context. The guard is correct for the current callers; the risk is for future additions.

How can I resolve this? If you propose a fix, please make it concise.


// macOS menu accelerator and renderer hotkey both fire for one Cmd+, press;
// without this guard they'd toggle then untoggle on the same keystroke.
const DEDUP_WINDOW_MS = 150;

export function toggleSettingsView(navigate: NavigateFnTyped, currentView: string): void {
const now = Date.now();
if (now - lastToggleAt < DEDUP_WINDOW_MS) return;
lastToggleAt = now;

if (currentView === 'settings') {
// Bare navigate(viewId) preserves stored params for views that statically require them.
(navigate as (viewId: ViewId) => void)(previousView);
return;
}

previousView = currentView as NonSettingsViewId;
navigate('settings');
}
Loading