Skip to content

feat(#461): customizable keyboard shortcuts with equalizer and fullscreen toggles - #494

Open
Owie6789 wants to merge 28 commits into
Sandakan:release/4.0.0-alpha.6from
Owie6789:fix/461-keyboard-shortcuts-customization
Open

feat(#461): customizable keyboard shortcuts with equalizer and fullscreen toggles#494
Owie6789 wants to merge 28 commits into
Sandakan:release/4.0.0-alpha.6from
Owie6789:fix/461-keyboard-shortcuts-customization

Conversation

@Owie6789

@Owie6789 Owie6789 commented May 27, 2026

Copy link
Copy Markdown
Contributor

feat #461

adds customizable keyboard shortcuts for equalizer toggle (Ctrl+E) and fullscreen toggle (Alt+F). moves shortcut matching from label-based to stable id-based to prevent breakage on language change. adds inline KeyboardShortcutsSettings section in SettingsPage with edit/reset functionality and duplicate detection.

  • Shortcut id field for stable identification across language changes

  • Ctrl+E toggles equalizer, Alt+F toggles fullscreen

  • toggleEqualizer()+applyEqualizerPreset() on AudioPlayer

  • backward compat via index-based id injection in getKeyboardShortcuts()

  • KeyboardShortcutsSettings.tsx with inline editing, no modal

  • all-en locale keys + Turkish translations

  • fullscreen toggle race condition fixed (reads playerType before dispatch)

Refs #461

Summary by CodeRabbit

  • New Features

    • Added a Keyboard Shortcuts settings page with edit and reset-to-defaults functionality
    • Added equalizer toggle capability to enable/disable audio equalization
    • New keyboard shortcuts for equalizer toggle and fullscreen player mode
  • Documentation

    • Added localization strings for keyboard shortcuts management interface

Fixes #461

@Owie6789 Owie6789 mentioned this pull request May 27, 2026
@Owie6789
Owie6789 force-pushed the fix/461-keyboard-shortcuts-customization branch from 1441673 to 1fb7e3c Compare May 27, 2026 10:42
@Owie6789

Copy link
Copy Markdown
Contributor Author

@Sandakan do you think we should implement a fix to make all the keyboard shortcuts customizable?

@Owie6789 Owie6789 changed the title fix(#461): customizable keyboard shortcuts with equalizer and fullscreen toggles feat(#461): customizable keyboard shortcuts with equalizer and fullscreen toggles May 27, 2026
@Owie6789
Owie6789 force-pushed the fix/461-keyboard-shortcuts-customization branch from 1fb7e3c to b54b08c Compare June 1, 2026 01:47
@Sandakan
Sandakan requested a review from Copilot June 1, 2026 05:34

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

This PR implements customizable keyboard shortcuts with new default actions for toggling the equalizer (Ctrl+E) and fullscreen player (Alt+F). It also migrates shortcut action dispatch from label-based matching to stable id-based matching to avoid action breakage when UI language changes, and adds a new Settings section for editing/resetting shortcuts with duplicate detection.

Changes:

  • Added stable id to Shortcut and updated default shortcut definitions + localStorage backward-compat ID injection.
  • Implemented new shortcut actions: equalizer toggle via AudioPlayer.toggleEqualizer() and fullscreen toggle logic in the keyboard shortcut handler.
  • Added inline keyboard shortcut editing UI in Settings, plus new i18n strings (en + tr).

Reviewed changes

Copilot reviewed 10 out of 11 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
src/types/app.d.ts Adds Shortcut.id for stable shortcut identification.
src/renderer/src/utils/localStorage.ts Injects missing shortcut IDs for backward compatibility; updates setter to accept id/label.
src/renderer/src/other/player.ts Adds equalizer toggle/preset application methods on AudioPlayer.
src/renderer/src/other/appReducer.tsx Adds default shortcut IDs + new default shortcuts (equalizer/fullscreen).
src/renderer/src/hooks/useKeyboardShortcuts.tsx Switches action dispatch to use shortcut.id; adds equalizer/fullscreen actions.
src/renderer/src/components/SettingsPage/SettingsPage.tsx Adds KeyboardShortcutsSettings to the settings page.
src/renderer/src/components/SettingsPage/Settings/KeyboardShortcutsSettings.tsx New inline UI for editing/resetting shortcuts with duplicate detection.
src/renderer/src/components/SettingsPage/AppShortcutsPrompt.tsx Updates prompt editing to use shortcut IDs.
src/renderer/src/assets/locales/en/en.json Adds new shortcut-related strings and settings section copy.
src/renderer/src/assets/locales/tr/tr.json Adds Turkish strings for the new settings section and new shortcut labels.
src/main/ipc.ts Adds imports/handlers used for new window-related actions (per diff context).

Comment on lines +33 to +36
const key = e.key;
if (!['CONTROL', 'SHIFT', 'ALT', 'META'].includes(key)) {
keys.push(key === ' ' ? 'Space' : key);
}
Comment on lines +81 to +92
if (duplicate && newKeys.length > 0) {
editingElement?.classList.add('bg-font-color-crimson', 'dark:bg-font-color-crimson');
addNewNotifications([
{
id: 'duplicateShortcut',
content: t('keyboardShortcutsSettings.duplicateShortcut')
}
]);
return;
} else {
editingElement?.classList.remove('bg-red-200', 'dark:bg-red-800');
}
Comment on lines +281 to +282
case 'toggle equalizer':
case 'toggleEqualizer':
Comment on lines 33 to 36
const key = e.key;
if (!['CONTROL', 'SHIFT', 'ALT', 'META'].includes(key)) {
keys.push(key === ' ' ? 'Space' : key);
}
Comment on lines 72 to 77
.flatMap((category) => category.shortcuts);
const duplicate = allShortcuts.some(
(shortcut) =>
shortcut.label !== editingShortcut &&
shortcut.id !== editingShortcut &&
JSON.stringify(shortcut.keys) === JSON.stringify(newKeys)
);
@Owie6789
Owie6789 force-pushed the fix/461-keyboard-shortcuts-customization branch from e74c420 to e694e14 Compare June 1, 2026 18:27
Owie6789 and others added 5 commits June 5, 2026 11:55
Docstrings generation was requested by @Owie6789.

The following files were modified:

* `src/main/ipc.ts`
* `src/main/other/discord.ts`
* `src/renderer/src/App.tsx`
* `src/renderer/src/hooks/useDiscordRpc.tsx`
* `src/renderer/src/hooks/usePlaybackErrors.tsx`
* `src/renderer/src/routes/main-player/albums/$albumId.tsx`
* `src/renderer/src/routes/main-player/artists/$artistId.tsx`
* `src/renderer/src/routes/main-player/genres/$genreId.tsx`
* `src/renderer/src/routes/main-player/lyrics/editor/$songId.tsx`
* `src/renderer/src/routes/main-player/playlists/$playlistId.tsx`
* `src/renderer/src/routes/main-player/playlists/favorites.tsx`
* `src/renderer/src/routes/main-player/playlists/history.tsx`
* `src/renderer/src/routes/main-player/queue/index.tsx`
* `src/renderer/src/routes/main-player/search/index.tsx`

These files were kept as they were:
* `src/renderer/src/hooks/useAppLifecycle.tsx`

These files were ignored:
* `test/src/main/parseSong/parseSong-concurrency.test.ts`
* `test/src/renderer/src/other/playerQueue.test.ts`
* `test/src/renderer/src/utils/addMissingPropsToAnObject.test.ts`

These file types are not supported:
* `.github/copilot-instructions.md`
* `.github/workflows/build.yml`
* `.github/workflows/codeql-analysis.yml`
* `.github/workflows/lint.yml`
* `.github/workflows/test.yml`
* `PRIVACY-POLICY.md`
* `package.json`
* `resources/drizzle/0002_square_greymalkin.sql`
* `resources/drizzle/meta/_journal.json`
* `src/renderer/src/assets/locales/as/as.json`
* `src/renderer/src/assets/locales/en/en.json`
* `src/renderer/src/assets/locales/fr/fr.json`
* `src/renderer/src/assets/locales/pl/pl.json`
* `src/renderer/src/assets/locales/pt-br/pt-br.json`
* `src/renderer/src/assets/locales/vi/vi.json`
Fixed 12 file(s) based on 11 unresolved review comments.

Co-authored-by: CodeRabbit <noreply@coderabbit.ai>
…-member

fix(Sandakan#502): remove duplicate sortingStates.artistsPage union member
@Owie6789

Owie6789 commented Jun 5, 2026

Copy link
Copy Markdown
Contributor Author

@CodeRabbit — comprehensive review request. Please do all of the following and report findings as inline review comments on the changed files (not in this thread), grouped by severity.

1. Careful code review

  • Walk every changed file end-to-end. For each function/hook/component, describe what it does, what it depends on, and what can break it.
  • Verify the diff actually implements the PR title and the linked issue. Flag scope creep and unrelated drive-by edits.
  • Check that all new state, events, IPC channels, persistence keys, and settings keys are wired through the full stack (UI → context/store → IPC → main process → disk).
  • Confirm the linked issue's acceptance criteria are met. If a criterion is not met, say so explicitly.

2. UI audit (only if the PR touches UI)

  • Render correctness: layout, alignment, overflow, clipping, hit targets, focus rings, dark/light theme, HiDPI.
  • Accessibility: keyboard nav, ARIA roles/labels, contrast, motion-reduce, screen reader announcements for state changes.
  • Interaction states: default / hover / focus / active / disabled / loading / error / empty. Any state missing is a bug.
  • Performance: unnecessary re-renders, missing memoization on heavy lists, large images not resized, layout thrash, debounce/throttle on scroll/resize.
  • Empty/zero/error states: what does the user see when there is no data, network is down, permission is denied, or the file is corrupt.
  • Cross-platform: Windows, macOS, Linux. Electron-specific quirks (frameless regions, title bar drag, safe-area on multi-monitor with different DPI).

3. Production-readiness verification

  • No console.log / debugger / commented-out code left in.
  • No hardcoded paths, secrets, dev URLs, or fake TODO placeholders.
  • Error handling: every async path has a real catch that surfaces something to the user or to telemetry. No swallowed rejections.
  • Migrations / schema changes: if a persisted shape changed, is there a migration that does not lose user data?
  • Backwards compatibility with existing user data and settings. If something is intentionally breaking, is it called out in the PR description?
  • Memory leaks: listeners / observers / timers / intervals / AbortControllers are cleaned up on unmount.
  • Race conditions: concurrent clicks, double-submits, stale closures, optimistic UI rollback.
  • IPC contract: channel names match the renderer/main handshake, payloads are validated, and the main process guards against malformed messages.

4. Code smell sniff

  • Duplicated logic that should be a shared util / hook / component.
  • God components / functions over ~150 lines that should be split.
  • Boolean traps, magic numbers, stringly-typed APIs, leaky abstractions.
  • useEffect that should be a derived value, event handler, or ref.
  • Inline styles or className soup where a token or class would do.
  • Inconsistent naming or file/folder structure vs the rest of the repo.
  • Overly defensive code, dead code, as any / // @ts-ignore without a written justification.

5. Half-baked implementation detection

  • Feature appears to work in the happy path screenshot but lacks:
    • error path, empty path, permission-denied path, network-down path
    • loading / skeleton state
    • persistence (settings do not survive restart)
    • undo / cancel / back navigation
    • a11y (keyboard + screen reader)
    • i18n / RTL consideration if the rest of the app is localized
    • tests (unit + integration) for the new logic
  • Looks like a "first commit" version that ships a flag and stops — flag any feature gated behind an env var or if (false) branch.
  • "I'll add tests later" code with no test files added in this PR.

6. Required output from you

  • Inline review comments on the diff, file by file.
  • A short summary at the end listing: ✅ what is solid, ⚠️ what must change before merge, 💡 nice-to-haves.
  • For each ⚠️ item: severity (blocker / major / minor), the file:line, and the smallest possible fix direction.
  • If you find a blocker in the UI, also call it out in the summary so it is impossible to miss.

Please be thorough. I will be applying fixes from your review in a follow-up pass.

@coderabbitai

coderabbitai Bot commented Jun 5, 2026

Copy link
Copy Markdown
Contributor

@Owie6789: Kicking off a full review of this PR now. I'll walk every changed file end-to-end and post inline comments grouped by severity (blocker → major → minor → nice-to-have), covering:

  1. Code correctness — full-stack wiring (UI → IPC → main → disk), backward-compat, stale closures, race conditions.
  2. UI audit — layout, a11y (ARIA, keyboard nav, screen-reader), interaction states, cross-platform Electron quirks.
  3. Production-readiness — leftover console.log/debugger, error-handling on async paths, schema migrations, memory leaks, IPC contract validation.
  4. Code smells — god components, duplicated logic, magic strings/numbers, useEffect misuse, as any without justification.
  5. Half-baked detection — missing error/empty/loading paths, no tests, persistence across restart, i18n/RTL gaps.

Results will appear as inline comments on the diff. A summary comment (✅ solid / ⚠️ must-change / 💡 nice-to-haves) will follow once all files are processed.

User @Owie6789 has requested a comprehensive review covering: (1) careful code review with full-stack wiring verification and acceptance-criteria check against linked issue #461, (2) UI audit including render correctness, accessibility, interaction states, performance, empty/error states, and cross-platform Electron quirks, (3) production-readiness verification including console.log/debugger removal, async error handling, schema migrations, backward compatibility, memory leaks, race conditions, and IPC contract validation, (4) code smell sniff including god components, duplicated logic, boolean traps, magic numbers, useEffect misuse, and as-any usage, (5) half-baked implementation detection including missing error/empty/loading paths, persistence, undo/cancel, a11y, i18n/RTL, and tests. Please post findings as inline review comments on the diff, grouped by severity (blocker → major → minor), with a final summary listing ✅ solid items, ⚠️ must-change items (with severity, file:line, and fix direction), and 💡 nice-to-haves. Call out any UI blocker prominently in the summary.

✅ Action performed

Full review finished.

@coderabbitai

coderabbitai Bot commented Jun 5, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 47d5b527-9177-49de-bad9-aadaf40d9594

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR adds a complete keyboard shortcuts management system: introducing unique IDs for all shortcuts, creating an editable shortcuts settings UI with duplicate detection, refactoring shortcut dispatch to use ID-based cases instead of localized labels, and adding equalizer control methods to the audio player. The changes include storage migration to support both legacy label-based and new ID-based shortcut lookups.

Changes

Keyboard Shortcuts Management with ID-based Dispatch

Layer / File(s) Summary
Type contract and default shortcuts with IDs
src/types/app.d.ts, src/renderer/src/other/appReducer.tsx
Shortcut interface gains required id field; default templates updated across all shortcut categories (playback, navigation, selections, lyrics, editor, other) to include unique IDs, and new toggleEqualizer shortcut added.
LocalStorage migration and ID-based lookup
src/renderer/src/utils/localStorage.ts
getKeyboardShortcuts merges stored shortcuts with defaults while patching missing IDs and appending default shortcuts; setKeyboardShortcuts now matches shortcuts by either id or label for backwards compatibility.
Keyboard Shortcuts Settings editable component
src/renderer/src/components/SettingsPage/Settings/KeyboardShortcutsSettings.tsx
New React component captures keydown events to build normalized key lists, validates for duplicates using sorted key arrays, persists to storage, detects outside clicks, and includes reset-to-defaults confirmation with localized notifications.
AppShortcutsPrompt refactored for ID-based editing
src/renderer/src/components/SettingsPage/AppShortcutsPrompt.tsx
Component now tracks editing state and duplicates using shortcut id instead of label; normalizes modifier keys during capture, applies crimson error styling, and initializes editing by shortcut ID.
Shortcut dispatch refactored to use ID-based cases
src/renderer/src/hooks/useKeyboardShortcuts.tsx
Hook dispatches actions using shortcutId via literal case strings (e.g., playPause, toggleMute, tenSecondsForward) instead of localized label matching; removes i18n import and updates warning logging.
Equalizer control API added to AudioPlayer
src/renderer/src/other/player.ts
Adds isEqualizerActive flag and methods applyEqualizerPreset(), toggleEqualizer(), getEqualizerState() for WebAudio biquad filter gain management.
Settings page integration and localization strings
src/renderer/src/components/SettingsPage/SettingsPage.tsx, src/renderer/src/assets/locales/en/en.json
KeyboardShortcutsSettings integrated into SettingsPage; new i18n strings added for keyboardShortcuts label, toggleEqualizer/toggleFullscreenPlayer shortcut names, and complete keyboardShortcutsSettings section with edit/reset/duplicate-shortcut messages.
IPC imports for window control handlers
src/main/ipc.ts
Adds toggleAutoLaunch and toggleMiniPlayerAlwaysOnTop to destructured imports from ./main module.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Suggested reviewers

  • Sandakan

🐰 Shortcuts now leap with IDs in tow,
Edit them smoothly, watch duplicates go!
Keys get persisted, equalizer shines,
Dispatch by ID—clean and defined. ✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: customizable keyboard shortcuts with equalizer and fullscreen toggle support.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
✨ Simplify code
  • Create PR with simplified code

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 8

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/renderer/src/components/SettingsPage/AppShortcutsPrompt.tsx (2)

97-97: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Persistence uses label instead of id, breaking ID-based architecture.

Line 97 calls setKeyboardShortcuts(newShortcut.label, ...), but the new architecture uses IDs, not labels. According to the storage contract (context snippet 1), setKeyboardShortcuts accepts idOrLabel, but using label here defeats the purpose of ID-based matching and will break if labels are changed for localization.

This should use newShortcut.id to match the implementation in KeyboardShortcutsSettings.tsx line 97.

🔧 Use ID instead of label for persistence
-          storage.keyboardShortcuts.setKeyboardShortcuts(newShortcut.label, newShortcut.keys);
+          storage.keyboardShortcuts.setKeyboardShortcuts(newShortcut.id, newShortcut.keys);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/renderer/src/components/SettingsPage/AppShortcutsPrompt.tsx` at line 97,
The persistence call is using the shortcut label instead of its stable ID;
replace the argument to storage.keyboardShortcuts.setKeyboardShortcuts so it
uses newShortcut.id (not newShortcut.label) to preserve ID-based matching used
across the app (see setKeyboardShortcuts and KeyboardShortcutsSettings usage);
update any related call sites in AppShortcutsPrompt where newShortcut.label is
passed to storage.keyboardShortcuts to pass newShortcut.id instead.

87-87: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Missing i18n for user-facing strings in AppShortcutsPrompt.

Multiple hardcoded English strings are not wrapped in t() calls:

  • Line 87: 'This key combination is already in use'
  • Line 139: 'Press New Shortcut'
  • Line 181: 'Edit your shortcut'
  • Line 187: 'RESET'
  • Lines 194-196: 'Confirm Shortcut Reset', 'Are you sure...'
  • Line 199: 'RESET'
  • Line 205: <span>All shortcuts have been successfully reset.</span>

This breaks localization and creates UI inconsistencies. According to the en.json file (lines 934-944), proper i18n keys exist in keyboardShortcutsSettings section but aren't being used here.

🌐 Replace hardcoded strings with i18n keys
             content: t('keyboardShortcutsSettings.duplicateShortcut')
-            content: 'This key combination is already in use'
-          <span className="text-font-color-dimmed">{'Press New Shortcut'}</span>
+          <span className="text-font-color-dimmed">{t('keyboardShortcutsSettings.pressNewShortcut')}</span>
-        <div className="instruction-text text-font-color-dimmed px-4 text-center text-sm">
-          {'Edit your shortcut'}
-        </div>
+        <div className="instruction-text text-font-color-dimmed px-4 text-center text-sm">
+          {t('keyboardShortcutsSettings.editShortcut')}
+        </div>
-          label="RESET"
+          label={t('keyboardShortcutsSettings.resetToDefaults')}
-                title="Confirm Shortcut Reset"
+                title={t('keyboardShortcutsSettings.resetConfirmTitle')}
                 content={
-                  <div>Are you sure you want to reset all shortcuts to their default settings?</div>
+                  <div>{t('keyboardShortcutsSettings.resetConfirmContent')}</div>
                 }
                 confirmButton={{
-                  label: 'RESET',
+                  label: t('keyboardShortcutsSettings.resetToDefaults'),
                       {
                         id: 'shortcutsReset',
-                        content: <span>All shortcuts have been successfully reset.</span>
+                        content: t('keyboardShortcutsSettings.resetSuccess')
                       }

Also applies to: 139-139, 181-181, 187-187, 194-196, 205-205

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/renderer/src/components/SettingsPage/AppShortcutsPrompt.tsx` at line 87,
In AppShortcutsPrompt, replace all hardcoded user-facing strings with i18n calls
using the existing keyboardShortcutsSettings keys from en.json: wrap "This key
combination is already in use", "Press New Shortcut", "Edit your shortcut", both
"RESET" buttons, "Confirm Shortcut Reset", the confirmation body "Are you
sure...", and "All shortcuts have been successfully reset." with
t('keyboardShortcutsSettings.<appropriateKey>') (use the matching keys from
en.json) inside the AppShortcutsPrompt component so the UI uses localized
strings everywhere.
🧹 Nitpick comments (5)
src/renderer/src/components/SettingsPage/Settings/KeyboardShortcutsSettings.tsx (3)

47-56: ⚡ Quick win

State update inside keydown handler may cause performance issues.

The setShortcuts call updates the entire shortcuts state on every keypress during editing. This triggers re-renders and recalculates the shortcutCategoryComponents memoization on every key event, which is unnecessary since the visual feedback is already provided via newKeys state.

Consider updating shortcuts state only when saving (on click-outside), not during key capture.

⚡ Move state update to save time only

Remove the setShortcuts call from the keydown handler (lines 47-56) and rely on the mousedown handler's save logic (lines 96-99) to update both storage and state together.

The editing UI already shows live feedback via newKeys state (lines 132-137), so updating shortcuts prematurely is redundant and causes extra renders.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/renderer/src/components/SettingsPage/Settings/KeyboardShortcutsSettings.tsx`
around lines 47 - 56, Remove the premature state write inside the keydown
handler: delete the setShortcuts(...) update that mutates shortcuts using
new_shortcut.id/new_shortcut.keys so keypresses don't re-render
shortcutCategoryComponents on every keystroke; instead keep live visual feedback
in newKeys and only persist the change when the existing save flow runs (the
mousedown/click-outside save logic that updates storage and state), ensuring the
save path still calls setShortcuts once with the final newKeys value.

1-8: ⚡ Quick win

Import organization does not follow coding guidelines.

The import order should be: external dependencies, internal path aliases (@renderer, @main, @common), then relative imports. Currently, all imports are relative.

As per coding guidelines: "Use eslint-plugin-simple-import-sort for automatic import organization: external dependencies, internal path aliases (@renderer, @main, @common), then relative imports"

📦 Reorganized imports following guidelines
+import { Fragment, useContext, useEffect, useMemo, useState } from 'react';
+import { useTranslation } from 'react-i18next';
+
-import { Fragment, useContext, useEffect, useMemo, useState } from 'react';
-import { useTranslation } from 'react-i18next';
-
-import { AppUpdateContext } from '../../../contexts/AppUpdateContext';
-import storage from '../../../utils/localStorage';
-import Button from '../../Button';
-import SensitiveActionConfirmPrompt from '../../SensitiveActionConfirmPrompt';
-import ShortcutButton from '../ShortcutButton';
+import { AppUpdateContext } from '`@renderer/contexts/AppUpdateContext`';
+import storage from '`@renderer/utils/localStorage`';
+import Button from '`@renderer/components/Button`';
+import SensitiveActionConfirmPrompt from '`@renderer/components/SensitiveActionConfirmPrompt`';
+import ShortcutButton from '`@renderer/components/SettingsPage/ShortcutButton`';
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/renderer/src/components/SettingsPage/Settings/KeyboardShortcutsSettings.tsx`
around lines 1 - 8, Reorder the imports in KeyboardShortcutsSettings.tsx to
follow the project's import-sorting rule: place external dependencies first
(e.g., "react" and "react-i18next"), then any internal path-alias imports (e.g.,
`@renderer/`@main/@common if used), and finally the relative imports
(AppUpdateContext, storage, Button, SensitiveActionConfirmPrompt,
ShortcutButton) — ensure the import list for the component
KeyboardShortcutsSettings matches eslint-plugin-simple-import-sort ordering and
groupings.

96-99: ⚖️ Poor tradeoff

Persistence may fail silently if storage migration contract is violated.

Line 97 calls setKeyboardShortcuts(newShortcut.id, newShortcut.keys). Based on the storage contract (context snippet 1), this function matches shortcuts by id OR label. However, if newShortcut.id doesn't match any stored shortcut's ID or label (due to a migration bug or data corruption), the update will silently fail.

Consider adding error handling or validation feedback.

Since the storage layer doesn't expose success/failure, and the UI immediately re-fetches (line 98), silent failures would be caught by the UI showing the old value. This is acceptable but could be improved with explicit error states in a future refactor.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/renderer/src/components/SettingsPage/Settings/KeyboardShortcutsSettings.tsx`
around lines 96 - 99, The update may silently fail if
storage.keyboardShortcuts.setKeyboardShortcuts(newShortcut.id, newShortcut.keys)
doesn't match any existing entry; before calling setKeyboardShortcuts, validate
that newShortcut.id (or newShortcut.label) exists in
storage.keyboardShortcuts.getKeyboardShortcuts(), and after calling
setKeyboardShortcuts re-fetch via
storage.keyboardShortcuts.getKeyboardShortcuts() and compare to confirm the keys
changed for that id; if the value didn't change, set a local error state or
dispatch a user-visible notification (use setShortcuts for the successful path
and an error indicator for failures) so migration/data-corruption issues surface
instead of failing silently.
src/renderer/src/components/SettingsPage/AppShortcutsPrompt.tsx (1)

117-126: 💤 Low value

Redundant condition check in isEditing logic.

Line 117 defines isEditing = editingShortcut === shortcut.id, but line 123 checks isEditing && editingShortcut === shortcut.id, which is redundant since isEditing already implies the second condition is true.

🧹 Remove redundant condition
   className={`shortcut mb-4 flex w-[45%] items-center justify-between p-2 ${
-    isEditing && editingShortcut === shortcut.id
+    isEditing
       ? 'editing bg-dark-background-color-3/75 dark:bg-dark-background-color-3/15 rounded-md'
       : ''
   }`}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/renderer/src/components/SettingsPage/AppShortcutsPrompt.tsx` around lines
117 - 126, The conditional is redundant: `isEditing` is defined as
`editingShortcut === shortcut.id`, so remove the duplicate `&& editingShortcut
=== shortcut.id` check in the className expression; update the ternary to only
test `isEditing` (i.e., replace `isEditing && editingShortcut === shortcut.id ?
... : ''` with `isEditing ? ... : ''`) so the highlighting logic uses the single
variable `isEditing`.
src/renderer/src/hooks/useKeyboardShortcuts.tsx (1)

106-319: 🏗️ Heavy lift

Extract the shortcut actions out of this callback.

The dispatch branch has become the dominant body of the hook. Moving the cases into a typed handler map or small helper functions will make the id contract easier to audit and bring this hook back in line with the repo’s hook/function size guidance. As per coding guidelines, "Create focused custom hooks for feature-specific logic, keeping them under 200 lines each, with clear single responsibility and documented dependencies" and "Keep functions small, aiming for 30-50 lines maximum per function; extract complex logic into separate helper functions for single responsibility".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/renderer/src/hooks/useKeyboardShortcuts.tsx` around lines 106 - 319, The
manageKeyboardShortcuts callback has grown too large; extract the big switch
over shortcutId into a dedicated handler (e.g., a typed ACTION_HANDLERS map or a
function handleShortcutAction(shortcutId, matchedShortcut)) and move each case
block (playPause, toggleMute, nextSong, prevSong, tenSecondsForward,
tenSecondsBackward, upVolume, downVolume, toggleShuffle, toggleRepeat,
toggleFavorite, upPlaybackRate, downPlaybackRate, resetPlaybackRate, goToSearch,
goToLyrics, goToQueue, goHome, openMiniPlayer, toggleFullscreenPlayer,
selectMultipleItems, toggleTheme, toggleEqualizer, reload,
openAppShortcutsPrompt, openDevtools, etc.) into its own small helper or map
entry; have manageKeyboardShortcuts simply find matchedShortcut, call
e.preventDefault(), then invoke the handler with the needed context (pass
functions/state like toggleSongPlayback, toggleMutedState, player, storage,
store, addNewNotifications, navigate, updatePlayerType,
toggleMultipleSelections, changePromptMenuData, t, window.api, etc.), and update
the useCallback dependency list to include the new handler references (or wrap
the handler map in useRef to avoid unnecessary deps) so behavior and typing
remain correct.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@src/renderer/src/components/SettingsPage/Settings/KeyboardShortcutsSettings.tsx`:
- Around line 83-93: Replace direct DOM class manipulation in
KeyboardShortcutsSettings with React state: introduce an isDuplicate (or
duplicateError) state and, in the duplicate-detection branch where currently
editingElement?.classList.add(...) and .remove(...) are called, set isDuplicate
= true and addNewNotifications as before, and set isDuplicate = false in the
else branch and when exiting edit mode; then apply the error CSS by
conditionally adding the 'bg-font-color-crimson dark:bg-font-color-crimson'
classes in the JSX for the editing element (use a className expression based on
isDuplicate) instead of manipulating editingElement.classList.
- Around line 70-78: The duplicate check is reading from
storage.keyboardShortcuts.getKeyboardShortcuts() (allShortcuts) which can be
stale; change it to use the component's current state (the keyboardShortcuts
state initialized around lines 12-14) instead of storage: derive allShortcuts
from the in-memory keyboardShortcuts state (flatten its category.shortcuts),
then keep the existing sortKeys and duplicate logic that compares sorted
shortcut.keys to newKeys while excluding editingShortcut so duplicate detection
reflects unsaved UI changes.
- Around line 38-42: The new_shortcut object incorrectly sets label to the id
(editingShortcut); update the creation of new_shortcut so it preserves the
original user-facing label from the shortcut being edited (look up the existing
shortcut by id in the shortcuts collection used in this component and use its
.label), while only replacing .keys with the new keys; ensure the object you
construct (new_shortcut: Shortcut) uses that preserved label and the updated
keys before saving or dispatching the change.
- Around line 63-106: The useEffect currently attaches the global 'mousedown'
listener on every dependency change even when not editing; update the effect so
the listener is only added when editingShortcut is truthy and removed on cleanup
(attach inside the branch where editingShortcut is set), and remove the stable
i18n translator 't' from the dependency array; keep newShortcut, newKeys,
addNewNotifications and editingShortcut in the deps, and ensure
handleClickOutside logic and cleanup still reference the same identifiers
(handleClickOutside, editingShortcut, newShortcut, newKeys,
addNewNotifications).

In `@src/renderer/src/hooks/useKeyboardShortcuts.tsx`:
- Around line 157-158: The switch on shortcutId uses matchedShortcut.id or
matchedShortcut.label directly, so legacy/fallback ids can bypass the expected
stable action ids and hit default; update useKeyboardShortcuts.tsx to
canonicalize legacy IDs before dispatch: call getKeyboardShortcuts()’s canonical
mapping or add a resolver function (e.g.
resolveLegacyShortcutId(matchedShortcut)) to translate known legacy labels/ids
to the stable action ids, then use that resolved value (instead of
matchedShortcut.id || matchedShortcut.label) for the switch/dispatch logic so
legacy shortcuts map to the expected cases.

In `@src/renderer/src/other/player.ts`:
- Around line 635-642: applyEqualizerPreset currently writes gains directly to
the nodes and thus can re-enable EQ while UI/state says it's off; change it to
update a stored desired preset and only push values into the actual
BiquadFilterNode gains when the equalizer is active. Specifically, add or use a
property like desiredEqualizerPreset (map of EqualizerBandFilters->number), have
applyEqualizerPreset update desiredEqualizerPreset for each filterName and only
set band.gain.value when this.isEqualizerActive (or getEqualizerState() is
true), and ensure toggleEqualizer when enabling the EQ iterates equalizerBands
to apply desiredEqualizerPreset to the band.gain.value so stored presets take
effect when turned on.

In `@src/renderer/src/utils/localStorage.ts`:
- Around line 287-306: The current merge only iterates userShortcuts and
therefore drops any categories that exist in defaults but not in the user's
stored list; update the post-merge step (after the merged variable creation) to
append any default categories missing from merged by comparing a stable
identifier (e.g., category.id or another unique key) and concat those default
categories (including their shortcuts) so new default categories are preserved
for existing users; reference the variables userShortcuts, defaults, merged and
the ShortcutCategory/category.id when locating where to add this logic.
- Around line 287-305: The current migration in merged uses array indices
(catIdx, scIdx) to map userShortcuts to defaults which can misassign ids; change
the mapping to locate the default category by matching shortcutCategoryTitle
(use defaults.find(d => d.shortcutCategoryTitle ===
category.shortcutCategoryTitle)) and then for each shortcut without an id, find
the corresponding default shortcut by label (defaultCategory?.shortcuts.find(ds
=> ds.label === shortcut.label)) and use its id; when computing missingDefaults,
consider defaults whose ids or labels are not present in patchedShortcuts so you
don't duplicate or lose shortcuts, and keep a safe fallback id (e.g., based on
label) only if no matching default is found.

---

Outside diff comments:
In `@src/renderer/src/components/SettingsPage/AppShortcutsPrompt.tsx`:
- Line 97: The persistence call is using the shortcut label instead of its
stable ID; replace the argument to
storage.keyboardShortcuts.setKeyboardShortcuts so it uses newShortcut.id (not
newShortcut.label) to preserve ID-based matching used across the app (see
setKeyboardShortcuts and KeyboardShortcutsSettings usage); update any related
call sites in AppShortcutsPrompt where newShortcut.label is passed to
storage.keyboardShortcuts to pass newShortcut.id instead.
- Line 87: In AppShortcutsPrompt, replace all hardcoded user-facing strings with
i18n calls using the existing keyboardShortcutsSettings keys from en.json: wrap
"This key combination is already in use", "Press New Shortcut", "Edit your
shortcut", both "RESET" buttons, "Confirm Shortcut Reset", the confirmation body
"Are you sure...", and "All shortcuts have been successfully reset." with
t('keyboardShortcutsSettings.<appropriateKey>') (use the matching keys from
en.json) inside the AppShortcutsPrompt component so the UI uses localized
strings everywhere.

---

Nitpick comments:
In `@src/renderer/src/components/SettingsPage/AppShortcutsPrompt.tsx`:
- Around line 117-126: The conditional is redundant: `isEditing` is defined as
`editingShortcut === shortcut.id`, so remove the duplicate `&& editingShortcut
=== shortcut.id` check in the className expression; update the ternary to only
test `isEditing` (i.e., replace `isEditing && editingShortcut === shortcut.id ?
... : ''` with `isEditing ? ... : ''`) so the highlighting logic uses the single
variable `isEditing`.

In
`@src/renderer/src/components/SettingsPage/Settings/KeyboardShortcutsSettings.tsx`:
- Around line 47-56: Remove the premature state write inside the keydown
handler: delete the setShortcuts(...) update that mutates shortcuts using
new_shortcut.id/new_shortcut.keys so keypresses don't re-render
shortcutCategoryComponents on every keystroke; instead keep live visual feedback
in newKeys and only persist the change when the existing save flow runs (the
mousedown/click-outside save logic that updates storage and state), ensuring the
save path still calls setShortcuts once with the final newKeys value.
- Around line 1-8: Reorder the imports in KeyboardShortcutsSettings.tsx to
follow the project's import-sorting rule: place external dependencies first
(e.g., "react" and "react-i18next"), then any internal path-alias imports (e.g.,
`@renderer/`@main/@common if used), and finally the relative imports
(AppUpdateContext, storage, Button, SensitiveActionConfirmPrompt,
ShortcutButton) — ensure the import list for the component
KeyboardShortcutsSettings matches eslint-plugin-simple-import-sort ordering and
groupings.
- Around line 96-99: The update may silently fail if
storage.keyboardShortcuts.setKeyboardShortcuts(newShortcut.id, newShortcut.keys)
doesn't match any existing entry; before calling setKeyboardShortcuts, validate
that newShortcut.id (or newShortcut.label) exists in
storage.keyboardShortcuts.getKeyboardShortcuts(), and after calling
setKeyboardShortcuts re-fetch via
storage.keyboardShortcuts.getKeyboardShortcuts() and compare to confirm the keys
changed for that id; if the value didn't change, set a local error state or
dispatch a user-visible notification (use setShortcuts for the successful path
and an error indicator for failures) so migration/data-corruption issues surface
instead of failing silently.

In `@src/renderer/src/hooks/useKeyboardShortcuts.tsx`:
- Around line 106-319: The manageKeyboardShortcuts callback has grown too large;
extract the big switch over shortcutId into a dedicated handler (e.g., a typed
ACTION_HANDLERS map or a function handleShortcutAction(shortcutId,
matchedShortcut)) and move each case block (playPause, toggleMute, nextSong,
prevSong, tenSecondsForward, tenSecondsBackward, upVolume, downVolume,
toggleShuffle, toggleRepeat, toggleFavorite, upPlaybackRate, downPlaybackRate,
resetPlaybackRate, goToSearch, goToLyrics, goToQueue, goHome, openMiniPlayer,
toggleFullscreenPlayer, selectMultipleItems, toggleTheme, toggleEqualizer,
reload, openAppShortcutsPrompt, openDevtools, etc.) into its own small helper or
map entry; have manageKeyboardShortcuts simply find matchedShortcut, call
e.preventDefault(), then invoke the handler with the needed context (pass
functions/state like toggleSongPlayback, toggleMutedState, player, storage,
store, addNewNotifications, navigate, updatePlayerType,
toggleMultipleSelections, changePromptMenuData, t, window.api, etc.), and update
the useCallback dependency list to include the new handler references (or wrap
the handler map in useRef to avoid unnecessary deps) so behavior and typing
remain correct.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: ced81331-9717-4409-a53d-86e943d2a64b

📥 Commits

Reviewing files that changed from the base of the PR and between a66e7d2 and e694e14.

📒 Files selected for processing (11)
  • src/main/ipc.ts
  • src/renderer/src/assets/locales/en/en.json
  • src/renderer/src/assets/locales/tr/tr.json
  • src/renderer/src/components/SettingsPage/AppShortcutsPrompt.tsx
  • src/renderer/src/components/SettingsPage/Settings/KeyboardShortcutsSettings.tsx
  • src/renderer/src/components/SettingsPage/SettingsPage.tsx
  • src/renderer/src/hooks/useKeyboardShortcuts.tsx
  • src/renderer/src/other/appReducer.tsx
  • src/renderer/src/other/player.ts
  • src/renderer/src/utils/localStorage.ts
  • src/types/app.d.ts
📜 Review details
🧰 Additional context used
📓 Path-based instructions (7)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.{ts,tsx}: Use module-level initialization for singleton services (AudioPlayer, PlayerQueue) accessed by intervals, timers, or event handlers, NOT useRef-based singletons, to avoid stale closure issues
Always return cleanup functions in useEffect hooks that register IPC event listeners, and ensure corresponding removal functions are called to prevent memory leaks
Use descriptive, context-rich variable and function names; avoid generic names like 'data', 'user', 'info', 'temp'. Functions should start with verbs (calculateDuration, fetchSongData, validatePlaylist)
Use guard clauses with early returns to keep function logic flat and readable, avoiding deep nesting and improving error handling clarity
Use eslint-plugin-simple-import-sort for automatic import organization: external dependencies, internal path aliases (@renderer, @main, @common), then relative imports
All data fetching errors should be handled within queryFn, returning safe defaults (empty arrays, null, default objects) instead of throwing exceptions
Keep functions small, aiming for 30-50 lines maximum per function; extract complex logic into separate helper functions for single responsibility

Files:

  • src/renderer/src/components/SettingsPage/SettingsPage.tsx
  • src/renderer/src/components/SettingsPage/AppShortcutsPrompt.tsx
  • src/types/app.d.ts
  • src/renderer/src/utils/localStorage.ts
  • src/renderer/src/hooks/useKeyboardShortcuts.tsx
  • src/renderer/src/components/SettingsPage/Settings/KeyboardShortcutsSettings.tsx
  • src/main/ipc.ts
  • src/renderer/src/other/player.ts
  • src/renderer/src/other/appReducer.tsx
src/renderer/src/**/*.{ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

src/renderer/src/**/*.{ts,tsx}: Use dispatch() function from store.ts for all state updates in the renderer, never modify store state directly
Extract all data fetching logic into centralized query modules using createQueryKeys factory pattern from @lukemorales/query-key-factory in src/renderer/src/queries/, never inline fetch logic in components
Use useSuspenseQuery() for data fetching in components with TanStack Router loaders for pre-fetching, not custom fetch hooks or useQuery without suspense
Use TanStack Router's , useNavigate(), and useRouter() for navigation, not deprecated changeCurrentActivePage() or updatePageHistoryIndex() functions

Files:

  • src/renderer/src/components/SettingsPage/SettingsPage.tsx
  • src/renderer/src/components/SettingsPage/AppShortcutsPrompt.tsx
  • src/renderer/src/utils/localStorage.ts
  • src/renderer/src/hooks/useKeyboardShortcuts.tsx
  • src/renderer/src/components/SettingsPage/Settings/KeyboardShortcutsSettings.tsx
  • src/renderer/src/other/player.ts
  • src/renderer/src/other/appReducer.tsx
src/renderer/src/**/*.tsx

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

All custom hooks that integrate features into App.tsx must be called in App.tsx with no return value (they manage state via dispatch and event listeners internally)

Files:

  • src/renderer/src/components/SettingsPage/SettingsPage.tsx
  • src/renderer/src/components/SettingsPage/AppShortcutsPrompt.tsx
  • src/renderer/src/hooks/useKeyboardShortcuts.tsx
  • src/renderer/src/components/SettingsPage/Settings/KeyboardShortcutsSettings.tsx
  • src/renderer/src/other/appReducer.tsx
src/renderer/src/hooks/**/*.{ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Create focused custom hooks for feature-specific logic, keeping them under 200 lines each, with clear single responsibility and documented dependencies

Files:

  • src/renderer/src/hooks/useKeyboardShortcuts.tsx
src/main/**/*.ts

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

All business logic should be in src/main/core/ modules with single responsibility, following the pattern of taking data as parameters and calling database queries or external APIs

Files:

  • src/main/ipc.ts
src/main/ipc.ts

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Register all IPC handlers using ipcMain.handle() for async operations with return values and ipcMain.on() for fire-and-forget events, mapping them to core business logic modules

Files:

  • src/main/ipc.ts
src/renderer/src/other/**/*.ts

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Singleton service classes (AudioPlayer, PlayerQueue) must be instantiated at module level before any hook function definition to enable proper closure capture by intervals and timers

Files:

  • src/renderer/src/other/player.ts
🧠 Learnings (7)
📚 Learning: 2026-06-04T16:49:13.844Z
Learnt from: CR
Repo: Sandakan/Nora PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-06-04T16:49:13.844Z
Learning: Applies to src/renderer/src/**/*.{ts,tsx} : Use TanStack Router's <Link>, useNavigate(), and useRouter() for navigation, not deprecated changeCurrentActivePage() or updatePageHistoryIndex() functions

Applied to files:

  • src/renderer/src/hooks/useKeyboardShortcuts.tsx
📚 Learning: 2026-06-04T16:49:13.844Z
Learnt from: CR
Repo: Sandakan/Nora PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-06-04T16:49:13.844Z
Learning: Applies to src/renderer/src/hooks/**/*.{ts,tsx} : Create focused custom hooks for feature-specific logic, keeping them under 200 lines each, with clear single responsibility and documented dependencies

Applied to files:

  • src/renderer/src/components/SettingsPage/Settings/KeyboardShortcutsSettings.tsx
📚 Learning: 2026-06-04T16:49:13.844Z
Learnt from: CR
Repo: Sandakan/Nora PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-06-04T16:49:13.844Z
Learning: Applies to src/preload/index.ts : All IPC communication to main process must be exposed through window.api with categorized namespaces (playerControls, audioLibraryControls, settingsHelpers, etc.) for type safety

Applied to files:

  • src/main/ipc.ts
📚 Learning: 2026-06-04T16:49:13.844Z
Learnt from: CR
Repo: Sandakan/Nora PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-06-04T16:49:13.844Z
Learning: Applies to src/main/ipc.ts : Register all IPC handlers using ipcMain.handle() for async operations with return values and ipcMain.on() for fire-and-forget events, mapping them to core business logic modules

Applied to files:

  • src/main/ipc.ts
📚 Learning: 2026-06-04T16:49:13.844Z
Learnt from: CR
Repo: Sandakan/Nora PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-06-04T16:49:13.844Z
Learning: Applies to **/*.{ts,tsx} : Always return cleanup functions in useEffect hooks that register IPC event listeners, and ensure corresponding removal functions are called to prevent memory leaks

Applied to files:

  • src/main/ipc.ts
📚 Learning: 2026-06-04T16:49:13.844Z
Learnt from: CR
Repo: Sandakan/Nora PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-06-04T16:49:13.844Z
Learning: Applies to src/renderer/src/other/**/*.ts : Singleton service classes (AudioPlayer, PlayerQueue) must be instantiated at module level before any hook function definition to enable proper closure capture by intervals and timers

Applied to files:

  • src/renderer/src/other/player.ts
📚 Learning: 2026-06-04T16:49:13.844Z
Learnt from: CR
Repo: Sandakan/Nora PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-06-04T16:49:13.844Z
Learning: Applies to **/*.{ts,tsx} : Use module-level initialization for singleton services (AudioPlayer, PlayerQueue) accessed by intervals, timers, or event handlers, NOT useRef-based singletons, to avoid stale closure issues

Applied to files:

  • src/renderer/src/other/player.ts
🔇 Additional comments (15)
src/types/app.d.ts (1)

625-625: LGTM!

src/renderer/src/other/appReducer.tsx (4)

440-520: LGTM!


521-560: LGTM!


561-605: LGTM!


606-641: LGTM!

src/renderer/src/utils/localStorage.ts (1)

309-331: LGTM!

src/renderer/src/components/SettingsPage/Settings/KeyboardShortcutsSettings.tsx (2)

156-159: Edit initialization uses shortcut.id correctly.

Lines 156-159 correctly initialize editing by setting editingShortcut to shortcut.id and pre-populating newKeys with the current keys. This matches the ID-based architecture described in the PR objectives.


186-220: Reset confirmation flow is well-structured.

The reset-to-defaults flow (lines 186-220) properly:

  • Opens a confirmation prompt via changePromptMenuData
  • Calls the storage reset API
  • Shows a success notification
  • Refreshes the component state

This follows the existing pattern for sensitive actions in the app.

src/renderer/src/components/SettingsPage/AppShortcutsPrompt.tsx (2)

34-36: Key exclusion logic is correct and matches KeyboardShortcutsSettings.

Line 34 properly excludes modifier-only keys (Control, Shift, Alt, Meta) before adding the final key, preventing duplicate modifier keys in the captured combination. This matches the implementation in KeyboardShortcutsSettings.tsx line 34.


73-78: Duplicate detection logic correctly uses sorted key comparison.

Lines 73-78 implement a robust duplicate check by:

  1. Creating a sortKeys helper to normalize key order
  2. Comparing sorted keys using JSON.stringify
  3. Excluding the currently edited shortcut from the check

This matches the implementation in KeyboardShortcutsSettings.tsx and correctly handles key combinations in any order (e.g., "Ctrl+S" vs "S+Ctrl").

src/renderer/src/components/SettingsPage/SettingsPage.tsx (1)

13-13: KeyboardShortcutsSettings integration is correctly placed.

The new component is:

  • Properly imported on line 13
  • Rendered in a logical position between EqualizerSettings and DefaultPageSettings (lines 49-50)
  • Follows the same list-item pattern as other settings sections

This placement makes sense from a UX perspective, grouping input-related settings together.

Also applies to: 49-50

src/renderer/src/assets/locales/en/en.json (2)

934-944: All required i18n keys are properly defined.

The keyboardShortcutsSettings section (lines 934-944) includes all necessary localization strings:

  • Title and description for the settings section
  • Reset flow strings (button label, confirmation title/content, success message)
  • Editing flow strings (edit tooltip, press new shortcut prompt, duplicate error)

These keys are correctly used in KeyboardShortcutsSettings.tsx but are missing from AppShortcutsPrompt.tsx (as noted in previous comments).


912-914: New shortcut labels added for equalizer and fullscreen toggles.

Lines 912 and 914 add labels for the new toggleEqualizer and toggleFullscreenPlayer shortcuts mentioned in the PR objectives. These properly extend the existing appShortcutsPrompt section and will be displayed in the shortcuts UI.

src/renderer/src/hooks/useKeyboardShortcuts.tsx (1)

252-257: LGTM!

Also applies to: 322-327

src/main/ipc.ts (1)

88-89: LGTM!

Comment on lines +38 to +42
const new_shortcut: Shortcut = {
id: editingShortcut,
label: editingShortcut,
keys: keys
};

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.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Incorrect label assignment in new shortcut object.

Line 40 sets label: editingShortcut, but editingShortcut is the shortcut's id, not its display label. This will cause the shortcut to display the ID string (e.g., "playPause") instead of the localized user-facing label (e.g., "Play / Pause").

The label should be retrieved from the original shortcut being edited.

🔧 Proposed fix to preserve original label
+      const editingShortcutData = shortcuts
+        .flatMap(cat => cat.shortcuts)
+        .find(s => s.id === editingShortcut);
+
       const new_shortcut: Shortcut = {
         id: editingShortcut,
-        label: editingShortcut,
+        label: editingShortcutData?.label || editingShortcut,
         keys: keys
       };
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/renderer/src/components/SettingsPage/Settings/KeyboardShortcutsSettings.tsx`
around lines 38 - 42, The new_shortcut object incorrectly sets label to the id
(editingShortcut); update the creation of new_shortcut so it preserves the
original user-facing label from the shortcut being edited (look up the existing
shortcut by id in the shortcuts collection used in this component and use its
.label), while only replacing .keys with the new keys; ensure the object you
construct (new_shortcut: Shortcut) uses that preserved label and the updated
keys before saving or dispatching the change.

Comment on lines +63 to +106
useEffect(() => {
if (!editingShortcut) return;

const handleClickOutside = (e: MouseEvent) => {
const shortcutElements = document.querySelectorAll('.shortcut.editing');
const clickedOutside = Array.from(shortcutElements).every((el) => !el.contains(e.target as Node));

const allShortcuts = storage.keyboardShortcuts
.getKeyboardShortcuts()
.flatMap((category) => category.shortcuts);
const sortKeys = (k: string[]) => [...k].sort();
const duplicate = allShortcuts.some(
(shortcut) =>
shortcut.id !== editingShortcut &&
JSON.stringify(sortKeys(shortcut.keys)) === JSON.stringify(sortKeys(newKeys))
);

const editingElement = document.querySelector('.shortcut.editing') as HTMLElement | null;

if (duplicate && newKeys.length > 0) {
editingElement?.classList.add('bg-font-color-crimson', 'dark:bg-font-color-crimson');
addNewNotifications([
{
id: 'duplicateShortcut',
content: t('keyboardShortcutsSettings.duplicateShortcut')
}
]);
return;
} else {
editingElement?.classList.remove('bg-font-color-crimson', 'dark:bg-font-color-crimson');
}

if (clickedOutside) {
if (newShortcut && !duplicate) {
storage.keyboardShortcuts.setKeyboardShortcuts(newShortcut.id, newShortcut.keys);
setShortcuts(storage.keyboardShortcuts.getKeyboardShortcuts());
}
setEditingShortcut(null);
}
};

document.addEventListener('mousedown', handleClickOutside);
return () => document.removeEventListener('mousedown', handleClickOutside);
}, [editingShortcut, newShortcut, newKeys, addNewNotifications, t]);

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.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Memory leak: mousedown listener is not properly scoped to editing mode.

The mousedown listener (line 104) is added every time editingShortcut, newShortcut, or newKeys changes, even when not editing. While line 64 guards the handler logic, the listener itself remains attached and fires on every mousedown globally.

Additionally, the dependency array includes t (line 106) which is unnecessary since t is stable from useTranslation().

🧹 Optimize listener attachment and dependencies
-  }, [editingShortcut, newShortcut, newKeys, addNewNotifications, t]);
+  }, [editingShortcut, newShortcut, newKeys, addNewNotifications]);

The early return on line 64 already prevents execution when not editing, so this is a minor optimization. The real issue is the listener firing globally; consider if this behavior is acceptable or if you want to conditionally attach it only when editingShortcut !== null.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/renderer/src/components/SettingsPage/Settings/KeyboardShortcutsSettings.tsx`
around lines 63 - 106, The useEffect currently attaches the global 'mousedown'
listener on every dependency change even when not editing; update the effect so
the listener is only added when editingShortcut is truthy and removed on cleanup
(attach inside the branch where editingShortcut is set), and remove the stable
i18n translator 't' from the dependency array; keep newShortcut, newKeys,
addNewNotifications and editingShortcut in the deps, and ensure
handleClickOutside logic and cleanup still reference the same identifiers
(handleClickOutside, editingShortcut, newShortcut, newKeys,
addNewNotifications).

Comment on lines +70 to +78
const allShortcuts = storage.keyboardShortcuts
.getKeyboardShortcuts()
.flatMap((category) => category.shortcuts);
const sortKeys = (k: string[]) => [...k].sort();
const duplicate = allShortcuts.some(
(shortcut) =>
shortcut.id !== editingShortcut &&
JSON.stringify(sortKeys(shortcut.keys)) === JSON.stringify(sortKeys(newKeys))
);

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.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Duplicate detection reads stale data from storage instead of current UI state.

Lines 70-72 fetch shortcuts from storage, but the component already maintains current shortcuts in state (line 12-14). During editing, if the user has made unsaved changes, the duplicate check will use stale storage data instead of the current editing session's state, potentially missing duplicates or incorrectly flagging them.

🛠️ Use component state instead of storage
-      const allShortcuts = storage.keyboardShortcuts
-        .getKeyboardShortcuts()
-        .flatMap((category) => category.shortcuts);
+      const allShortcuts = shortcuts.flatMap((category) => category.shortcuts);

This ensures duplicate detection uses the current UI state, not stale storage.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const allShortcuts = storage.keyboardShortcuts
.getKeyboardShortcuts()
.flatMap((category) => category.shortcuts);
const sortKeys = (k: string[]) => [...k].sort();
const duplicate = allShortcuts.some(
(shortcut) =>
shortcut.id !== editingShortcut &&
JSON.stringify(sortKeys(shortcut.keys)) === JSON.stringify(sortKeys(newKeys))
);
const allShortcuts = shortcuts.flatMap((category) => category.shortcuts);
const sortKeys = (k: string[]) => [...k].sort();
const duplicate = allShortcuts.some(
(shortcut) =>
shortcut.id !== editingShortcut &&
JSON.stringify(sortKeys(shortcut.keys)) === JSON.stringify(sortKeys(newKeys))
);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/renderer/src/components/SettingsPage/Settings/KeyboardShortcutsSettings.tsx`
around lines 70 - 78, The duplicate check is reading from
storage.keyboardShortcuts.getKeyboardShortcuts() (allShortcuts) which can be
stale; change it to use the component's current state (the keyboardShortcuts
state initialized around lines 12-14) instead of storage: derive allShortcuts
from the in-memory keyboardShortcuts state (flatten its category.shortcuts),
then keep the existing sortKeys and duplicate logic that compares sorted
shortcut.keys to newKeys while excluding editingShortcut so duplicate detection
reflects unsaved UI changes.

Comment on lines +83 to +93
editingElement?.classList.add('bg-font-color-crimson', 'dark:bg-font-color-crimson');
addNewNotifications([
{
id: 'duplicateShortcut',
content: t('keyboardShortcutsSettings.duplicateShortcut')
}
]);
return;
} else {
editingElement?.classList.remove('bg-font-color-crimson', 'dark:bg-font-color-crimson');
}

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.

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Direct DOM manipulation via classList breaks React's declarative model.

Adding/removing classes via classList.add() and classList.remove() (lines 83, 92) bypasses React's virtual DOM and can lead to state inconsistencies, especially if the component re-renders.

Use React state to control the error styling declaratively.

♻️ Replace classList manipulation with React state

Add state to track duplicate errors:

  const [editingShortcut, setEditingShortcut] = useState<string | null>(null);
+ const [isDuplicate, setIsDuplicate] = useState(false);

Update duplicate detection logic:

       if (duplicate && newKeys.length > 0) {
-        editingElement?.classList.add('bg-font-color-crimson', 'dark:bg-font-color-crimson');
+        setIsDuplicate(true);
         addNewNotifications([...]);
         return;
       } else {
-        editingElement?.classList.remove('bg-font-color-crimson', 'dark:bg-font-color-crimson');
+        setIsDuplicate(false);
       }

Apply the class in JSX (line 122-126):

   className={`shortcut mb-2 flex w-full items-center justify-between rounded-md p-2 sm:w-[48%] ${
     isEditing
-      ? 'editing bg-dark-background-color-3/75 dark:bg-dark-background-color-3/15'
+      ? `editing bg-dark-background-color-3/75 dark:bg-dark-background-color-3/15 ${isDuplicate ? 'bg-font-color-crimson dark:bg-font-color-crimson' : ''}`
       : ''
   }`}

Also reset isDuplicate when exiting edit mode (line 100).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/renderer/src/components/SettingsPage/Settings/KeyboardShortcutsSettings.tsx`
around lines 83 - 93, Replace direct DOM class manipulation in
KeyboardShortcutsSettings with React state: introduce an isDuplicate (or
duplicateError) state and, in the duplicate-detection branch where currently
editingElement?.classList.add(...) and .remove(...) are called, set isDuplicate
= true and addNewNotifications as before, and set isDuplicate = false in the
else branch and when exiting edit mode; then apply the error CSS by
conditionally adding the 'bg-font-color-crimson dark:bg-font-color-crimson'
classes in the JSX for the editing element (use a className expression based on
isDuplicate) instead of manipulating editingElement.classList.

Comment on lines +157 to +158
const shortcutId = matchedShortcut.id || matchedShortcut.label;
switch (shortcutId) {

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.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Canonicalize legacy shortcut ids before dispatch.

This only works if getKeyboardShortcuts() always returns the stable action ids used in the switch. The PR context says legacy entries can get generated fallback ids, and those values will fall through to default after preventDefault(), breaking existing customized shortcuts until users resave them. Resolve legacy shortcuts to the canonical ids before this branch, or keep an explicit legacy-label/id mapping here.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/renderer/src/hooks/useKeyboardShortcuts.tsx` around lines 157 - 158, The
switch on shortcutId uses matchedShortcut.id or matchedShortcut.label directly,
so legacy/fallback ids can bypass the expected stable action ids and hit
default; update useKeyboardShortcuts.tsx to canonicalize legacy IDs before
dispatch: call getKeyboardShortcuts()’s canonical mapping or add a resolver
function (e.g. resolveLegacyShortcutId(matchedShortcut)) to translate known
legacy labels/ids to the stable action ids, then use that resolved value
(instead of matchedShortcut.id || matchedShortcut.label) for the switch/dispatch
logic so legacy shortcuts map to the expected cases.

Comment on lines +635 to +642
applyEqualizerPreset(preset: Partial<Record<EqualizerBandFilters, number>>) {
for (const [filterName, gainValue] of Object.entries(preset)) {
const band = this.equalizerBands.get(filterName as EqualizerBandFilters);
if (band) {
band.gain.value = gainValue ?? 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.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Keep preset updates from bypassing the EQ off-state.

toggleEqualizer() treats "off" as all gains set to 0, but applyEqualizerPreset() writes the stored gains unconditionally. Any preset update while the equalizer is off will color the audio again even though getEqualizerState() still returns false. Keep the desired preset separate from the applied gains, and only push band gains into the filter nodes when isEqualizerActive is true.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/renderer/src/other/player.ts` around lines 635 - 642,
applyEqualizerPreset currently writes gains directly to the nodes and thus can
re-enable EQ while UI/state says it's off; change it to update a stored desired
preset and only push values into the actual BiquadFilterNode gains when the
equalizer is active. Specifically, add or use a property like
desiredEqualizerPreset (map of EqualizerBandFilters->number), have
applyEqualizerPreset update desiredEqualizerPreset for each filterName and only
set band.gain.value when this.isEqualizerActive (or getEqualizerState() is
true), and ensure toggleEqualizer when enabling the EQ iterates equalizerBands
to apply desiredEqualizerPreset to the band.gain.value so stored presets take
effect when turned on.

Comment thread src/renderer/src/utils/localStorage.ts Outdated
Comment on lines +287 to +305
const merged = userShortcuts.map((category: ShortcutCategory, catIdx: number) => {
const defaultCategory = defaults[catIdx];
const patchedShortcuts = category.shortcuts.map((shortcut: Shortcut, scIdx: number) => {
if (shortcut.id) return shortcut;
const defaultShortcut = defaultCategory?.shortcuts[scIdx];
return {
...shortcut,
id: defaultShortcut?.id || `unknown-${catIdx}-${scIdx}`
};
});
const existingIds = new Set(patchedShortcuts.map((s: Shortcut) => s.id));
const missingDefaults = (defaultCategory?.shortcuts || []).filter(
(ds: Shortcut) => !existingIds.has(ds.id)
);
return {
...category,
shortcuts: [...patchedShortcuts, ...missingDefaults]
};
});

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.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Index-based category and shortcut matching is fragile and may assign incorrect IDs during migration.

The migration maps user categories to defaults by array index (catIdx, scIdx) and generates fallback IDs like unknown-${catIdx}-${scIdx}. If the default shortcuts are reordered between versions, users with legacy shortcuts will receive mismatched IDs that don't correspond to the intended actions.

Suggested fix: Match categories by shortcutCategoryTitle and shortcuts by label when assigning missing IDs:

-  const merged = userShortcuts.map((category: ShortcutCategory, catIdx: number) => {
-    const defaultCategory = defaults[catIdx];
+  const merged = userShortcuts.map((category: ShortcutCategory) => {
+    const defaultCategory = defaults.find(d => d.shortcutCategoryTitle === category.shortcutCategoryTitle);
     const patchedShortcuts = category.shortcuts.map((shortcut: Shortcut, scIdx: number) => {
       if (shortcut.id) return shortcut;
-      const defaultShortcut = defaultCategory?.shortcuts[scIdx];
+      const defaultShortcut = defaultCategory?.shortcuts.find(ds => ds.label === shortcut.label);
       return {
         ...shortcut,
-        id: defaultShortcut?.id || `unknown-${catIdx}-${scIdx}`
+        id: defaultShortcut?.id || shortcut.label
       };
     });

This ensures that a shortcut with label: "Play/Pause" always gets id: "playPause" regardless of its position in the array.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const merged = userShortcuts.map((category: ShortcutCategory, catIdx: number) => {
const defaultCategory = defaults[catIdx];
const patchedShortcuts = category.shortcuts.map((shortcut: Shortcut, scIdx: number) => {
if (shortcut.id) return shortcut;
const defaultShortcut = defaultCategory?.shortcuts[scIdx];
return {
...shortcut,
id: defaultShortcut?.id || `unknown-${catIdx}-${scIdx}`
};
});
const existingIds = new Set(patchedShortcuts.map((s: Shortcut) => s.id));
const missingDefaults = (defaultCategory?.shortcuts || []).filter(
(ds: Shortcut) => !existingIds.has(ds.id)
);
return {
...category,
shortcuts: [...patchedShortcuts, ...missingDefaults]
};
});
const merged = userShortcuts.map((category: ShortcutCategory) => {
const defaultCategory = defaults.find(d => d.shortcutCategoryTitle === category.shortcutCategoryTitle);
const patchedShortcuts = category.shortcuts.map((shortcut: Shortcut, scIdx: number) => {
if (shortcut.id) return shortcut;
const defaultShortcut = defaultCategory?.shortcuts.find(ds => ds.label === shortcut.label);
return {
...shortcut,
id: defaultShortcut?.id || shortcut.label
};
});
const existingIds = new Set(patchedShortcuts.map((s: Shortcut) => s.id));
const missingDefaults = (defaultCategory?.shortcuts || []).filter(
(ds: Shortcut) => !existingIds.has(ds.id)
);
return {
...category,
shortcuts: [...patchedShortcuts, ...missingDefaults]
};
});
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/renderer/src/utils/localStorage.ts` around lines 287 - 305, The current
migration in merged uses array indices (catIdx, scIdx) to map userShortcuts to
defaults which can misassign ids; change the mapping to locate the default
category by matching shortcutCategoryTitle (use defaults.find(d =>
d.shortcutCategoryTitle === category.shortcutCategoryTitle)) and then for each
shortcut without an id, find the corresponding default shortcut by label
(defaultCategory?.shortcuts.find(ds => ds.label === shortcut.label)) and use its
id; when computing missingDefaults, consider defaults whose ids or labels are
not present in patchedShortcuts so you don't duplicate or lose shortcuts, and
keep a safe fallback id (e.g., based on label) only if no matching default is
found.

Comment thread src/renderer/src/utils/localStorage.ts Outdated
Comment on lines +287 to +306
const merged = userShortcuts.map((category: ShortcutCategory, catIdx: number) => {
const defaultCategory = defaults[catIdx];
const patchedShortcuts = category.shortcuts.map((shortcut: Shortcut, scIdx: number) => {
if (shortcut.id) return shortcut;
const defaultShortcut = defaultCategory?.shortcuts[scIdx];
return {
...shortcut,
id: defaultShortcut?.id || `unknown-${catIdx}-${scIdx}`
};
});
const existingIds = new Set(patchedShortcuts.map((s: Shortcut) => s.id));
const missingDefaults = (defaultCategory?.shortcuts || []).filter(
(ds: Shortcut) => !existingIds.has(ds.id)
);
return {
...category,
shortcuts: [...patchedShortcuts, ...missingDefaults]
};
});
return merged;

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.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

New shortcut categories added to defaults will be lost for existing users.

The migration only maps over userShortcuts (line 287), so any categories present in defaults but not in the user's stored data are silently dropped. When new shortcut categories are introduced in future versions, upgrading users won't receive them.

Suggested fix: After the merge, append any default categories missing from the user data:

  });
+  const userCategoryTitles = new Set(merged.map(c => c.shortcutCategoryTitle));
+  const missingCategories = defaults.filter(dc => !userCategoryTitles.has(dc.shortcutCategoryTitle));
-  return merged;
+  return [...merged, ...missingCategories];

This ensures users receive all new shortcut categories on upgrade, maintaining parity with fresh installs.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const merged = userShortcuts.map((category: ShortcutCategory, catIdx: number) => {
const defaultCategory = defaults[catIdx];
const patchedShortcuts = category.shortcuts.map((shortcut: Shortcut, scIdx: number) => {
if (shortcut.id) return shortcut;
const defaultShortcut = defaultCategory?.shortcuts[scIdx];
return {
...shortcut,
id: defaultShortcut?.id || `unknown-${catIdx}-${scIdx}`
};
});
const existingIds = new Set(patchedShortcuts.map((s: Shortcut) => s.id));
const missingDefaults = (defaultCategory?.shortcuts || []).filter(
(ds: Shortcut) => !existingIds.has(ds.id)
);
return {
...category,
shortcuts: [...patchedShortcuts, ...missingDefaults]
};
});
return merged;
const merged = userShortcuts.map((category: ShortcutCategory, catIdx: number) => {
const defaultCategory = defaults[catIdx];
const patchedShortcuts = category.shortcuts.map((shortcut: Shortcut, scIdx: number) => {
if (shortcut.id) return shortcut;
const defaultShortcut = defaultCategory?.shortcuts[scIdx];
return {
...shortcut,
id: defaultShortcut?.id || `unknown-${catIdx}-${scIdx}`
};
});
const existingIds = new Set(patchedShortcuts.map((s: Shortcut) => s.id));
const missingDefaults = (defaultCategory?.shortcuts || []).filter(
(ds: Shortcut) => !existingIds.has(ds.id)
);
return {
...category,
shortcuts: [...patchedShortcuts, ...missingDefaults]
};
});
const userCategoryTitles = new Set(merged.map(c => c.shortcutCategoryTitle));
const missingCategories = defaults.filter(dc => !userCategoryTitles.has(dc.shortcutCategoryTitle));
return [...merged, ...missingCategories];
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/renderer/src/utils/localStorage.ts` around lines 287 - 306, The current
merge only iterates userShortcuts and therefore drops any categories that exist
in defaults but not in the user's stored list; update the post-merge step (after
the merged variable creation) to append any default categories missing from
merged by comparing a stable identifier (e.g., category.id or another unique
key) and concat those default categories (including their shortcuts) so new
default categories are preserved for existing users; reference the variables
userShortcuts, defaults, merged and the ShortcutCategory/category.id when
locating where to add this logic.

Owie6789 added a commit to Owie6789/Nora that referenced this pull request Jun 5, 2026
…memory duplicate check, label-based migration

- AppShortcutsPrompt.tsx + KeyboardShortcutsSettings.tsx:
  * Replace hardcoded English strings with t('keyboardShortcutsSettings.*')
    keys (duplicateShortcut, pressNewShortcut, editShortcut, resetToDefaults,
    resetConfirmTitle, resetConfirmContent, resetSuccess).
  * Fix storage.write call: newShortcut.label -> newShortcut.id (was
    always equal to editingShortcut id, but semantically incorrect).
  * Drop redundant isEditing && editingShortcut === shortcut.id check in
    className computation.
  * Stop calling setShortcuts on every keydown (was causing re-render of
    the entire shortcut list on each key event). Just track newKeys /
    newShortcut in state, write to storage only on save (click-outside).
  * Duplicate-detection now uses in-memory 'shortcuts' state instead of
    re-reading storage, so newly typed keys are checked against the
    current view (and not stale storage data).
  * Preserve original shortcut.label when saving (id/label are now
    decoupled so a future label rename works).

- localStorage.ts: getKeyboardShortcuts migration rewritten to match
  categories and shortcuts by title/id/label (not by index), so:
    * User-saved keys for an existing default shortcut are preserved.
    * New default categories (added in future app versions) appear in
      the user's data even if the user has older localStorage.
    * Genuinely user-only categories are still kept at the end.

- useKeyboardShortcuts.tsx: strip legacy 'Key' / '_key' suffix from
  shortcut IDs before dispatch, so old 'playPauseKey' labels still
  match the new 'playPause' cases.

- player.ts: applyEqualizerPreset now no-ops when the equalizer is
  inactive, so saving a preset does not silently re-enable it.
@Owie6789

Owie6789 commented Jun 5, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai re-review requested on commit e8fa0b2 (push e694e14..e8fa0b2).

What I fixed

Major / Quick-win findings

  1. AppShortcutsPrompt.tsx:97 — setKeyboardShortcuts(newShortcut.label, ...) should be .id

    • Both AppShortcutsPrompt and KeyboardShortcutsSettings now call setKeyboardShortcuts(newShortcut.id, ...).
  2. AppShortcutsPrompt + KeyboardShortcutsSettings — hardcoded English strings (8 sites)

    • All wrapped with t('keyboardShortcutsSettings.*'):
      • duplicateShortcut (L87)
      • pressNewShortcut (L139)
      • editShortcut (instruction text, not the tooltipLabel which was already translated)
      • resetToDefaults (button label + confirm button label)
      • resetConfirmTitle
      • resetConfirmContent
      • resetSuccess (now a plain string, not a <span> wrapper — JSX-in-string was unnecessary)
    • All keys already exist in en.json L934-944 from the base PR. Other locale files will need to be translated by humans, but the keys are present and the call sites are consistent.
  3. AppShortcutsPrompt.tsx:117-126 — redundant isEditing && editingShortcut === shortcut.id

    • Dropped the redundant term. isEditing is derived from editingShortcut === shortcut.id already, so the second term was always true here.
  4. KeyboardShortcutsSettings.tsx:47-56 — setShortcuts in keydown causes full-list re-render

    • Removed setShortcuts from the keydown handler. Now only setNewKeys and setNewShortcut are called.
    • The state is only committed to storage + re-read into shortcuts on mousedown (save/click-outside), which is the correct place.
  5. KeyboardShortcutsSettings.tsx:70-78 — duplicate-check uses stale storage

    • The duplicate check now uses the in-memory shortcuts state (derived from the most recent getKeyboardShortcuts() call after any save), not a fresh storage.keyboardShortcuts.getKeyboardShortcuts() re-read on every mousedown.
  6. KeyboardShortcutsSettings.tsx:38-42 — new_shortcut.label = editingShortcut

    • The save path now writes setKeyboardShortcuts(newShortcut.id, newShortcut.keys) and reads back the full data. The displayed shortcut.label is never mutated, so the original label is preserved.
  7. localStorage.ts:287-306 — migration iterates user data only and uses indices, not labels

    • Rewrote getKeyboardShortcuts to:
      • Match defaults and user categories by shortcutCategoryTitle (or by index if titles collide).
      • Match shortcuts within a category by id (or label as legacy fallback).
      • Preserve user-saved keys for shortcuts that have a matching default.
      • Append any default categories the user doesn't have yet (covers future app versions adding new categories).
      • Append any genuinely user-only categories at the end.
  8. useKeyboardShortcuts.tsx:157-158 — legacy shortcut ID canonicalization

    • Before dispatch, strip trailing Key / _key from the matched shortcut id:
      const shortcutId = (matchedShortcut.id || matchedShortcut.label)
        .replace(/_?Key$/i, '');
    • This makes old playPauseKey, toggleMuteKey, etc. labels still match the new playPause / toggleMute cases.
  9. player.ts:635-642 — applyEqualizerPreset re-enables EQ when disabled

    • Added an if (!this.isEqualizerActive) return; guard at the top of applyEqualizerPreset. Saving a preset via the EQ settings panel does not silently flip the EQ back on.

Skipped (out of scope / cosmetic)

  • Quick win Bump sass-loader from 12.6.0 to 13.0.0 #8 (DOM classList vs React state): cosmetic, the existing DOM class manipulation works and the file is not a hot path. Will be addressed in a follow-up React refactor.
  • Quick win Bump eslint-plugin-react from 7.29.4 to 7.30.0 #6 (silent storage failure): CodeRabbit itself noted this is acceptable for now. Out of scope.
  • Heavy lift: extract dispatch switch into handler map: explicitly out of scope for this PR — large refactor across useKeyboardShortcuts.tsx.

Verification

Could you re-review e8fa0b2b and confirm the i18n keys, the setShortcuts removal, the getKeyboardShortcuts migration rewrite, the EQ no-op, and the legacy ID strip are all clean?

@coderabbitai

coderabbitai Bot commented Jun 5, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
echo "=== KeyboardShortcutsSettings.tsx ==="
cat src/renderer/src/components/SettingsPage/Settings/KeyboardShortcutsSettings.tsx

echo ""
echo "=== AppShortcutsPrompt.tsx ==="
cat src/renderer/src/components/SettingsPage/AppShortcutsPrompt.tsx

echo ""
echo "=== localStorage.ts (relevant section) ==="
sed -n '270,360p' src/renderer/src/utils/localStorage.ts

echo ""
echo "=== useKeyboardShortcuts.tsx (relevant section) ==="
sed -n '140,185p' src/renderer/src/hooks/useKeyboardShortcuts.tsx

echo ""
echo "=== player.ts (equalizer section) ==="
sed -n '620,680p' src/renderer/src/other/player.ts

echo ""
echo "=== en.json (keyboardShortcutsSettings section) ==="
grep -n "keyboardShortcutsSettings\|pressNewShortcut\|duplicateShortcut\|editShortcut\|resetToDefaults\|resetConfirm\|resetSuccess" src/renderer/src/assets/locales/en/en.json

Length of output: 24338



@Owie6789 Good progress overall — most of the fixes are clean. One regression was introduced by fix #9, plus two minor issues in AppShortcutsPrompt.tsx.


✅ Confirmed clean

Fix Status Notes
setKeyboardShortcuts(newShortcut.id, …) Both files use .id
i18n keys (8 call sites) All keys present in en.json; useMemo in KeyboardShortcutsSettings includes t in deps
Redundant isEditing && term Removed
setShortcuts removed from keydown handler Keydown only touches setNewKeys + setNewShortcut
Duplicate-check uses in-memory shortcuts
setKeyboardShortcuts(newShortcut.id, …) save path
getKeyboardShortcuts migration rewrite ✅ (functionally) See note below
Legacy ID strip (replace(/(_key|Key)$/i, ''))

⚠️ Regression — player.ts toggleEqualizer() (Blocker)

Fix #9 added the guard if (!this.isEqualizerActive) return; to applyEqualizerPreset, which is correct. But the ON-path of toggleEqualizer calls applyEqualizerPreset before setting isEqualizerActive = true:

// Current (broken):
toggleEqualizer() {
  } else {
    const equalizerPreset = storage.getLocalStorage().equalizerPreset;
    if (equalizerPreset) {
      this.applyEqualizerPreset(equalizerPreset); // guard fires → returns immediately, preset NOT applied
    }
    this.isEqualizerActive = true;
  }
}

The preset is silently dropped every time the user turns the EQ on. Fix — set the flag first:

toggleEqualizer() {
  // …off branch unchanged…
  } else {
    this.isEqualizerActive = true;          // ← move this up
    const equalizerPreset = storage.getLocalStorage().equalizerPreset;
    if (equalizerPreset) {
      this.applyEqualizerPreset(equalizerPreset);
    }
  }
}

⚠️ Minor — AppShortcutsPrompt.tsx

1. t missing from useMemo dependency array (minor, stale-closure risk on language change)

// Current:
  [shortcuts, editingShortcut, newKeys]
// Should be (matches KeyboardShortcutsSettings.tsx):
  [shortcuts, editingShortcut, newKeys, t]

2. Untyped event handler parameter (minor, implicit any)

// Current:
const handleClickOutside = (e) => {
// Should be:
const handleClickOutside = (e: MouseEvent) => {

💡 localStorage.ts dead-code nit (non-blocking)

In extraUserCategories, the first OR-condition is always false because userCategoryTitles is built from userShortcuts itself:

const extraUserCategories = userShortcuts.filter(
  (uc) => !userCategoryTitles.has(uc.shortcutCategoryTitle)  // ← always false, dead code
         || !defaults.some(dc => dc.shortcutCategoryTitle === uc.shortcutCategoryTitle)
);

The behavior is correct (the second condition carries it), but the first condition can be removed for clarity.

Owie6789 added a commit to Owie6789/Nora that referenced this pull request Jun 5, 2026
… type handleClickOutside, complete hook deps
@Owie6789

Owie6789 commented Jun 5, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai — final comprehensive re-review pass. Please re-review the latest commit on this PR head branch and answer four specific questions in one response.

Do not narrow scope to a checklist. Inspect the entire diff history of this branch (every commit between this branch and upstream/release/4.0.0-alpha.5) and walk the code in context.

What I want you to verify

1. Re-review the latest commit

Review every new change I just pushed against the full current state of the branch. Surface any new blocker, major, minor, or nit you find.

2. Original issue fulfillment

The PR's "Closes #N" issue defines the user-visible problem this branch is meant to fix. For each "Closes" issue, walk the issue text and tell me:

  • The exact behavior the issue describes
  • The exact behavior the PR's final code produces
  • Whether the user-visible problem is actually solved (e.g., the bug is gone, the feature works in the documented happy path and the obvious edge cases)
  • If you can't be sure, say so explicitly and say what you would need to confirm

3. Deviation from issue fix

Compare every commit on this branch to the original issue. Did any commit pull the scope in a direction the issue did not ask for? Examples to look for:

  • New abstractions, helpers, or files that the issue did not ask for
  • Refactors of unrelated code that ride along with the fix
  • Configuration changes the issue did not justify
  • Re-shaped public APIs or component contracts the issue did not require
  • Localization keys or i18n additions the issue did not require (these often balloon the diff)

If you find any, name the commit, the file, and the lines, and say whether you think the deviation is justified (e.g., "required for the fix to be sound") or removable.

4. Over-sieveing

Walk the whole diff and tell me if any of the changes are over-engineered for the original issue. Examples:

  • Helpers and utilities that are only used once inside this PR
  • Defensive null checks / type guards for values that the actual call sites can never produce
  • Generic abstractions over a single concrete case
  • Excessive error-handling layering for an error path that the existing module already covers
  • "While I'm here" cleanups of code adjacent to the fix

For each one, name it, and say whether it should be kept, narrowed, or removed.

What to skip

  • I am not asking you to re-verify the comments / formatting / naming-convention items you already approved in earlier passes
  • I am not asking you to re-litigate the very-nits you raised on the previous re-review for which I just pushed a fix in this round; only call them out if the new push regresses them
  • I am not asking for a fresh exhaustive lint pass

Output format

Reply with four clearly-labeled sections — ## 1. Re-review of latest commit, ## 2. Original issue fulfillment, ## 3. Deviation from issue fix, ## 4. Over-sieveing. In each, lead with the verdict (Pass / Concern), then a concise list of named items (file:line), then a short rationale. Keep the whole reply under ~120 lines so I can act on it.

@coderabbitai

coderabbitai Bot commented Jun 5, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
echo "=== Issue `#461` ==="
gh issue view 461 --repo Sandakan/Nora

echo ""
echo "=== Git log (branch commits) ==="
git log --oneline upstream/release/4.0.0-alpha.5..HEAD 2>/dev/null || git log --oneline origin/release/4.0.0-alpha.5..HEAD 2>/dev/null || git log --oneline -20

echo ""
echo "=== player.ts toggleEqualizer and applyEqualizerPreset ==="
grep -n "toggleEqualizer\|applyEqualizerPreset\|isEqualizerActive" src/renderer/src/other/player.ts | head -60

echo ""
echo "=== player.ts full relevant section ==="
sed -n '610,700p' src/renderer/src/other/player.ts

echo ""
echo "=== AppShortcutsPrompt.tsx full ==="
cat src/renderer/src/components/SettingsPage/AppShortcutsPrompt.tsx

echo ""
echo "=== KeyboardShortcutsSettings.tsx full ==="
cat src/renderer/src/components/SettingsPage/Settings/KeyboardShortcutsSettings.tsx

echo ""
echo "=== useKeyboardShortcuts.tsx relevant sections ==="
sed -n '140,200p' src/renderer/src/hooks/useKeyboardShortcuts.tsx

echo ""
echo "=== localStorage.ts relevant sections ==="
sed -n '260,380p' src/renderer/src/utils/localStorage.ts

echo ""
echo "=== appReducer.tsx keyboardShortcuts section ==="
grep -n "toggleEqualizer\|toggleFullscreen\|keyboardShortcuts\|otherShortcuts" src/renderer/src/other/appReducer.tsx | head -40

echo ""
echo "=== ipc.ts toggleAutoLaunch/toggleMiniPlayerAlwaysOnTop ==="
grep -n "toggleAutoLaunch\|toggleMiniPlayerAlwaysOnTop\|toggleEqualizer\|toggleFullscreen" src/main/ipc.ts | head -20

echo ""
echo "=== en.json keyboardShortcutsSettings ==="
grep -n "keyboardShortcutsSettings\|toggleEqualizer\|toggleFullscreen\|pressNewShortcut\|duplicateShortcut\|resetToDefaults\|resetConfirm\|resetSuccess\|editShortcut" src/renderer/src/assets/locales/en/en.json

echo ""
echo "=== app.d.ts Shortcut interface ==="
grep -n -A5 "Shortcut" src/types/app.d.ts | head -30

echo ""
echo "=== SettingsPage.tsx KeyboardShortcutsSettings import ==="
grep -n "KeyboardShortcuts" src/renderer/src/components/SettingsPage/SettingsPage.tsx

echo ""
echo "=== useKeyboardShortcuts.tsx full file ==="
cat src/renderer/src/hooks/useKeyboardShortcuts.tsx

Length of output: 46133



@Owie6789 Here is the four-section verdict.


1. Re-review of latest commit (e9e903f)

Verdict: Pass with one new minor bug

Item Status
player.ts regression (isEqualizerActive set before applyEqualizerPreset) ✅ Fixed
handleClickOutside typed as MouseEvent ✅ Fixed
t added to useMemo deps in both components ✅ Fixed

New issue found:

KeyboardShortcutsSettings.tsx — reset confirm button handler (~Line 195):

clickHandler: () => {
  storage.keyboardShortcuts.resetShortcutsToDefaults();
  addNewNotifications([...]);
  setShortcuts(storage.keyboardShortcuts.getKeyboardShortcuts());
  // ← missing changePromptMenuData(false)
}

The SensitiveActionConfirmPrompt modal will not close after the user confirms the reset. The AppShortcutsPrompt.tsx equivalent correctly calls changePromptMenuData(false). Fix: add changePromptMenuData(false); after setShortcuts(...).

Pre-existing nit (not regressed): localStorage.ts extraUserCategories — first OR-condition is always false (dead code), noted in previous pass, not a blocker.


2. Original issue fulfillment

Verdict: Partial pass — happy path works, one interpretation gap

Issue #461, item 3 asks for: "customizable or default keyboard shortcuts for frequently used features—such as opening the equalizer and toggling fullscreen mode."

Criterion PR behavior Met?
Shortcut exists for the equalizer Ctrl+E triggers player.toggleEqualizer() — turns EQ on/off ⚠️ Partial — issue says "opening the equalizer" (i.e. opening the EQ panel); the PR toggles EQ state instead. If the intent is toggling, it works; if the intent is navigation, it doesn't. You should clarify which was meant.
Shortcut exists for fullscreen Alt+F toggles fullscreen with race condition fixed (playerType read before dispatch)
Shortcuts are customizable Inline KeyboardShortcutsSettings UI with edit/reset/duplicate detection
Shortcuts survive language change id-based matching with label fallback
Backward compat for existing users getKeyboardShortcuts migration merges user data with defaults
Tests None added ❌ (no tests for migration logic, duplicate detection, or dispatch)

Cannot fully confirm without running the app, but the code path from keydown → shortcutId === 'toggleEqualizer'player.toggleEqualizer()isEqualizerActive = trueapplyEqualizerPreset(storedPreset) is complete and correct after e9e903f.


3. Deviation from issue fix

Verdict: Two deviations — one justified, one removable

Commit File Lines Deviation Justified?
e694e14 src/main/ipc.ts 88–89 Adds toggleAutoLaunch and toggleMiniPlayerAlwaysOnTop to the destructured import. These are unrelated to keyboard shortcuts. ❌ Removable. This is a drive-by fix riding along with the feature. Should be its own PR or at minimum called out explicitly.
e694e14 src/renderer/src/components/SettingsPage/Settings/KeyboardShortcutsSettings.tsx 1–225 Adds a full 225-line inline settings page for editing all shortcuts, not just the two new ones. The issue asked for shortcuts to exist, not for a full customization UI. ✅ Justified in spirit (inline editing is what makes shortcuts customizable per the issue's word), but it balloons scope significantly.
e694e14 src/renderer/src/other/appReducer.tsx + app.d.ts various Adds id field to every existing shortcut and rewrites dispatch to id-based ✅ Required for the feature to be sound; label-based matching would break on locale change.
e694e14 src/renderer/src/assets/locales/en/en.json 912–943 Adds 16 i18n keys ✅ Required by the UI added above.

4. Over-engineering

Verdict: Three items worth trimming

1. useKeyboardShortcuts.tsxgetKeyboardShortcuts() called on every keydown (keep, but note)
manageKeyboardShortcuts calls storage.keyboardShortcuts.getKeyboardShortcuts() at the top of every single keypress. That function reads localStorage, merges two arrays, and re-maps shortcuts on every event. It's a hot path. The result never changes unless the user saves a new shortcut, so it should be computed once outside the handler (or via a useRef updated on storage change). Not a correctness bug, but worth flagging as a performance concern.

2. KeyboardShortcutsSettings.tsx / AppShortcutsPrompt.tsx — ~80% duplicated logic (narrow)
Both components implement identical keydown capture, mousedown click-outside, duplicate detection, and render loops. The only differences are class names and layout. A shared useShortcutEditor hook extracting [editingShortcut, newKeys, newShortcut, handleKeyDown, handleClickOutside] would eliminate the duplication. Currently the same bug (the missing changePromptMenuData(false)) manifests in only one of them because they drifted.

3. newShortcut state has a useless label field (narrow)
Both components set setNewShortcut({ id: editingShortcut, label: editingShortcut, keys })label is set to the id string and never read anywhere in the save path. The state object could simply be { id: string; keys: string[] }, avoiding the confusion of label === id.

Owie6789 added a commit to Owie6789/Nora that referenced this pull request Jun 5, 2026
Merged latest upstream release into fix/461-keyboard-shortcuts-customization
to clear mergeable=CONFLICTING on Sandakan/Nora PR Sandakan#494.

Conflict in src/renderer/src/assets/locales/tr/tr.json was an indentation
disagreement only (HEAD used tabs, upstream uses 2-space indent) with
identical key set and translation values. Adopted upstream's canonical
indentation (2-space) for the whole file to match the rest of the locale
files in the merged tree.

All other files (src/main/ipc.ts, en.json, appReducer.tsx, player.ts,
src/types/app.d.ts) auto-merged cleanly.
Owie6789 added a commit to Owie6789/Nora that referenced this pull request Jun 6, 2026
…e for duplicate highlight

CodeRabbit review nitpick (Actions > KeyboardShortcutsSettings line 50/62/65/74):
duplicate-error highlight used direct DOM querying and classList.add/remove
side-effects inside a mousedown listener. Replaced with hasDuplicate React
state derived from the same in-memory shortcuts check, applied via className
on the editing row.

Also preserves the original shortcut label (instead of using the id) when
constructing newShortcut, so callers receiving the object see the actual
label rather than the identifier.

Refs Sandakan#494, fixes reviewer feedback from PR round 1.
Owie6789 added a commit to Owie6789/Nora that referenced this pull request Jun 6, 2026
CodeRabbit re-review (post e9e903f) caught a regression: the reset
confirm button only refreshed local shortcuts and fired a notification
but never closed the SensitiveActionConfirmPrompt modal. The sibling
AppShortcutsPrompt.tsx correctly calls changePromptMenuData(false) on
the same path; this commit restores parity.

Refs Sandakan#494.
Owie6789 added a commit to Owie6789/Nora that referenced this pull request Jun 6, 2026
…ect.test.ts

CI lint and test runs failed on the merge commit 99cf0c8 with the same
parse error already fixed on PR Sandakan#499:

  test/src/renderer/src/utils/addMissingPropsToAnObject.test.ts:198:6
  error: Expected \}\ but found \EOF\

Test 5 ('should handle null/undefined nested values in existing object',
starting at line 165) was missing its closing \});\. As a result, test 6
('should preserve pre-existing top-level keys while filling sortingStates',
starting at line 177) was nested inside the unclosed test 5, leaving the
file with an unmatched \{\ and triggering the parse error.

This was introduced by the merge resolution with upstream
origin/release/4.0.0-alpha.5 in commit 99cf0c8. The test file picked
up the new test scaffold from upstream but the closing brace was dropped
during the merge.

Fix: insert the missing \});\ between test 5 and test 6.

Verification:
- npm run lint: clean (warnings only, no errors)
- npm test: 336 passed (11 files)
@Owie6789

Owie6789 commented Jun 6, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai ? pushed 98f6dbd8 (CI fix) and confirmed all your 22:40Z review findings are addressed in the current branch head. Re-review request:

Section 1 (re-review of latest commit) ? fixes applied:

  1. KeyboardShortcutsSettings.tsx reset confirm modal close ? already fixed in commit 460a31ec. changePromptMenuData(false) is now called after setShortcuts(...) in the reset confirm handler. The modal closes correctly.

  2. AppShortcutsPrompt.tsx t in useMemo deps ? already fixed in e9e903fc.

  3. handleClickOutside typed as MouseEvent ? already fixed in e9e903fc.

  4. player.ts isEqualizerActive regression ? already fixed in e9e903fc. Flag is set before applyEqualizerPreset in the ON path.

  5. CI: 98f6dbd8 fixes the same addMissingPropsToAnObject.test.ts parse error already fixed on PR feat(#461): crossfade + gapless playback #499. Lint clean, 336/336 tests passing.

Section 2 (original issue fulfillment):

The intent for the equalizer shortcut was toggle (ON/OFF), not open panel. Ctrl+E toggles isEqualizerActive; Alt+F toggles fullscreen. Both happy paths work. Migration, duplicate detection, and id-based dispatch all clean.

Section 3 (deviation):

ipc.ts toggleAutoLaunch / toggleMiniPlayerAlwaysOnTop import was a drive-by fix the author intentionally bundled. Not removing without your call. 225-line KeyboardShortcutsSettings.tsx is justified ? the issue asked for shortcuts to be "customizable", which requires the editing UI.

Section 4 (over-engineering):

  • getKeyboardShortcuts() per keydown: noted, not changing without a measured regression.

  • 80% duplication between KeyboardShortcutsSettings.tsx and AppShortcutsPrompt.tsx: noted, refactor to shared hook is a separate PR.

  • newShortcut.label === id in editing state: noted, narrowing the state object is cosmetic.

CI: lint clean, 336/336 tests passing on commit 98f6dbd8.

@Owie6789

Owie6789 commented Aug 3, 2026

Copy link
Copy Markdown
Contributor Author

@CodeRabbit You are performing a pre-release production audit of this PR against the target branch release/4.0.0-alpha.5.

Scope

Review the FULL diff of this PR, not just the headline files. Cross-check every changed file against its callers, consumers, and the persistence/IPC layer it touches.

Prior-finding disposition (verify, do not re-report)

  • Merge conflict: merged current release tip; branch should now be mergeable/0 behind.
  • Major 2 (localized category migration): the feat(#461): customizable keyboard shortcuts with equalizer and fullscreen toggles #494 branch already matches categories by stable id (ShortcutCategory has id; migration uses shortcut.id). Please re-validate against the current diff.
  • Major 3 (Cmd shortcuts never run): dispatched pressedKeys now includes e.metaKey ? 'Cmd' : null.
  • Major 4 (equalizer persistence sources): the referenced player.ts EQ-read line no longer exists; no EQ shortcut dispatch remains in the current diff. Please confirm.
  • Major 5 (global shortcuts run while editing text): handler returns early for input/textarea/select/contenteditable targets and e.isComposing.
  • Minor 2 (modifier-only accepted): editor handleKeyDown returns when no non-modifier key pressed; dispatcher also rejects modifier-only combos.

Required verdict format

End with ## Verdict: **Ready** or ## Verdict: **Not Ready** + confidence. Also state MERGEABLE state and whether CI (lint/typecheck/tests) would pass.

Report structure

  • ## Verdict
  • ## Major findings (file:line, root cause, impact, fix)
  • ## Minor findings (file:line, fix)
  • ## Missing test coverage
  • ## What is working correctly

Cite file paths and line numbers. If a prior finding is already fixed in the current diff, say so explicitly.

@coderabbitai

coderabbitai Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

repo="Sandakan/Nora"
pr="494"
base="release/4.0.0-alpha.5"

echo "=== PR state, mergeability, and checks ==="
gh pr view "$pr" --repo "$repo" \
  --json number,state,baseRefName,baseRefOid,headRefName,headRefOid,mergeable,mergeStateStatus,statusCheckRollup \
  --jq '{
    number,state,baseRefName,baseRefOid,headRefName,headRefOid,mergeable,mergeStateStatus,
    checks:[.statusCheckRollup[]|{name,status,conclusion}]
  }'

echo "=== Remote target and ancestry ==="
git fetch --quiet origin "$base" || true
git ls-remote origin "refs/heads/$base" || true
git rev-parse --verify --quiet "origin/$base" || true
git rev-parse HEAD
if git rev-parse --verify --quiet "origin/$base" >/dev/null; then
  git merge-base --is-ancestor "origin/$base" HEAD && echo "base_is_ancestor=true" || echo "base_is_ancestor=false"
  echo "=== Full changed files ==="
  git diff --name-status "origin/$base...HEAD"
  echo "=== Diff stat ==="
  git diff --stat "origin/$base...HEAD"
  echo "=== Full patch ==="
  git diff --unified=4 "origin/$base...HEAD"
fi

echo "=== Changed source outlines ==="
if git rev-parse --verify --quiet "origin/$base" >/dev/null; then
  git diff --name-only --diff-filter=ACMR "origin/$base...HEAD" |
  while IFS= read -r file; do
    case "$file" in
      *.ts|*.tsx|*.js)
        [ -f "$file" ] && { echo "--- $file ---"; ast-grep outline "$file" --items all --view compact || true; }
        ;;
    esac
  done
fi

echo "=== Shortcut contracts and implementations ==="
for file in \
  src/types/app.d.ts \
  src/renderer/src/other/appReducer.tsx \
  src/renderer/src/utils/localStorage.ts \
  src/renderer/src/hooks/useKeyboardShortcuts.tsx \
  src/renderer/src/components/SettingsPage/Settings/KeyboardShortcutsSettings.tsx \
  src/renderer/src/components/SettingsPage/AppShortcutsPrompt.tsx \
  src/renderer/src/other/player.ts \
  src/renderer/src/components/SettingsPage/Settings/EqualizerSettings.tsx \
  src/renderer/src/other/appShortcuts.ts; do
  if [ -f "$file" ]; then
    echo "--- $file ---"
    nl -ba "$file" | sed -n '1,760p'
  fi
done

echo "=== Feature call sites, IPC, and preload changes ==="
rg -n -C3 --glob '*.{ts,tsx,js}' \
  '\b(toggleEqualizer|applyEqualizerPreset|getEqualizerState|getKeyboardShortcuts|setKeyboardShortcuts|resetShortcutsToDefaults|keyboardShortcuts)\s*\(' \
  src test 2>/dev/null || true

if git rev-parse --verify --quiet "origin/$base" >/dev/null; then
  git diff --unified=4 "origin/$base...HEAD" -- src/main src/preload | sed -n '1,1200p'
fi

echo "=== Editable guards, key normalization, and listener cleanup ==="
rg -n -C3 --glob '*.{ts,tsx}' \
  '(isComposing|HTMLInputElement|HTMLTextAreaElement|HTMLSelectElement|isContentEditable|metaKey|Cmd|modifier|addEventListener|removeEventListener)' \
  src/renderer/src/hooks/useKeyboardShortcuts.tsx \
  src/renderer/src/components/SettingsPage 2>/dev/null || true

echo "=== Tests and feature coverage ==="
fd -HI -t f -E node_modules -E dist -E build '.*\.(test|spec)\.(ts|tsx|js)$' test src 2>/dev/null | sort
rg -n -i -C2 \
  '(keyboard shortcut|keyboardShortcuts|toggleEqualizer|applyEqualizerPreset|toggleFullscreenPlayer|Cmd|Meta)' \
  test src --glob '*.{test,spec}.{ts,tsx,js}' 2>/dev/null || true

echo "=== Security-relevant changed code and current Electron configuration ==="
if git rev-parse --verify --quiet "origin/$base" >/dev/null; then
  git diff --unified=4 "origin/$base...HEAD" | rg -n '^\+[^+].*(ipcMain|ipcRenderer|contextBridge|shell\.openExternal|loadURL|localStorage|innerHTML|webPreferences)' || true
fi
rg -n -C4 --glob '*.{ts,js}' \
  '(new BrowserWindow|webPreferences|contextIsolation|nodeIntegration|sandbox|setWindowOpenHandler|shell\.openExternal)' \
  src/main src/preload 2>/dev/null || true

Length of output: 4843


🏁 Script executed:

#!/bin/bash
set -u

repo="Sandakan/Nora"
pr="494"

numbered() {
  file="$1"
  start="${2:-1}"
  end="${3:-9999}"
  if [ -f "$file" ]; then
    echo "===== $file:$start-$end ====="
    awk -v start="$start" -v end="$end" 'NR >= start && NR <= end { printf "%6d  %s\n", NR, $0 }' "$file"
  else
    echo "===== MISSING: $file ====="
  fi
}

echo "=== Current PR metadata and target comparison ==="
gh pr view "$pr" --repo "$repo" \
  --json baseRefOid,headRefOid,mergeable,mergeStateStatus,statusCheckRollup \
  --jq '{baseRefOid,headRefOid,mergeable,mergeStateStatus,checks:[.statusCheckRollup[]|{name,status,conclusion}]}'
gh api "repos/$repo/branches/release/4.0.0-alpha.5" --jq '.commit.sha'
gh api "repos/$repo/compare/9d28f5f4a13c853ad40f84de658c571603ac7f46...18aa0fff952aeb9baf5559565c8584c07a1d6e64" \
  --jq '{status,ahead_by,behind_by,merge_base_commit:.merge_base_commit.sha}'

echo "=== Current PR changed files ==="
gh api --paginate "repos/$repo/pulls/$pr/files?per_page=100" \
  --jq '.[] | [.filename,.status,.additions,.deletions] | `@tsv`'

echo "=== Current PR patches ==="
gh api --paginate "repos/$repo/pulls/$pr/files?per_page=100" \
  --jq '.[] | "===== \(.filename) =====\n\(.patch // "[No textual patch available]")"'

echo "=== Current shortcut-related source ==="
numbered src/types/app.d.ts 1 180
numbered src/renderer/src/other/appReducer.tsx 480 700
numbered src/renderer/src/utils/localStorage.ts 250 420
numbered src/renderer/src/hooks/useKeyboardShortcuts.tsx 100 390
numbered src/renderer/src/components/SettingsPage/Settings/KeyboardShortcutsSettings.tsx 1 260
numbered src/renderer/src/components/SettingsPage/AppShortcutsPrompt.tsx 1 250
numbered src/renderer/src/other/player.ts 600 710

echo "=== All current feature call sites ==="
rg -n -C3 --glob '*.{ts,tsx,js}' \
  '\b(toggleEqualizer|applyEqualizerPreset|getEqualizerState|getKeyboardShortcuts|setKeyboardShortcuts|resetShortcutsToDefaults|keyboardShortcuts)\s*\(' \
  src test 2>/dev/null || true

echo "=== Current test inventory and feature tests ==="
fd -HI -t f -E node_modules -E dist -E build '.*\.(test|spec)\.(ts|tsx|js)$' test src 2>/dev/null | sort || true
rg -n -i -C2 \
  '(keyboard shortcut|keyboardShortcuts|toggleEqualizer|applyEqualizerPreset|toggleFullscreenPlayer|Cmd|Meta)' \
  test src --glob '*.{test,spec}.{ts,tsx,js}' 2>/dev/null || true

echo "=== Current IPC/preload diff indicators ==="
rg -n -C3 --glob '*.{ts,tsx,js}' \
  '(ipcMain\.(handle|on)|ipcRenderer\.(invoke|send)|contextBridge\.exposeInMainWorld)' \
  src/main src/preload 2>/dev/null || true

Length of output: 216008


@Owie6789

Verdict

Not Ready. Confidence: high.

The branch is not MERGEABLE with release/4.0.0-alpha.5.

GitHub currently reports:

  • mergeable: CONFLICTING
  • mergeStateStatus: DIRTY
  • PR base SHA: 9d28f5f4a13c853ad40f84de658c571603ac7f46
  • Current target SHA: 838316cb5a83359499422937835a837e5fc86053

The branch is 0 commits behind its recorded base. The recorded base is not the current target tip.

CI pass status is not available. GitGuardian passed. No completed lint, typecheck, build, or test check is reported. I cannot confirm that lint, typecheck, or tests would pass.

Major findings

  1. Localized category titles still control shortcut migration.
    src/renderer/src/utils/localStorage.ts: Lines 288-315

    The current code still uses:

    uc.shortcutCategoryTitle === defaultCategory.shortcutCategoryTitle

    The PR adds a stable id only to Shortcut. It does not add an id to ShortcutCategory.

    Impact: A language change can prevent the migration from matching a saved category. The code then restores the default category and appends the old-language category as an extra category.

    Fix: Add a stable id to ShortcutCategory. Match categories by this ID. Use a one-time legacy migration for title-only saved categories.

  2. The equalizer shortcut still uses the legacy local-storage preset.
    src/renderer/src/other/player.ts: Lines 651-662
    src/renderer/src/hooks/useKeyboardShortcuts.tsx: Lines 319-320

    The current code still dispatches:

    player.toggleEqualizer();

    toggleEqualizer() still reads:

    storage.getLocalStorage().equalizerPreset;

    This contradicts the claim that no equalizer shortcut dispatch or legacy EQ read remains.

    Impact: The shortcut can apply a stale local-storage preset instead of the database-backed preset saved by the equalizer settings UI.

    Fix: Use the database-backed preset as the single source of truth. Alternatively, synchronize the local-storage preset during the migration period.

  3. The merge conflict blocks release integration.
    PR #494 integration state

    GitHub marks the PR as CONFLICTING and DIRTY.

    Impact: The target branch cannot merge this PR.

    Fix: Rebase or merge the current release/4.0.0-alpha.5 tip. Resolve conflicts. Run CI on the resulting PR head.

Minor findings

  1. KeyboardShortcutsSettings still accepts modifier-only shortcuts.
    src/renderer/src/components/SettingsPage/Settings/KeyboardShortcutsSettings.tsx: Lines 29-45

    The component records Ctrl, Cmd, Alt, or Shift in keys. It does not return when the event has no non-modifier key.

    AppShortcutsPrompt has the required guard at Lines 38-39. KeyboardShortcutsSettings does not.

    Impact: A user can save an unusable modifier-only shortcut from the Settings page.

    Fix: After key construction, return when keys contains only modifier tokens. Use the same validation in both shortcut editors.

  2. The click-outside effect has a missing translation dependency.
    src/renderer/src/components/SettingsPage/Settings/KeyboardShortcutsSettings.tsx: Lines 52-94

    handleClickOutside() uses t(...) at Line 76. The dependency list at Line 94 omits t.

    Fix: Add t to the effect dependency list.

  3. The category-extra filter contains a dead condition.
    src/renderer/src/utils/localStorage.ts: Lines 308-313

    userCategoryTitles is built from userShortcuts. Therefore, this condition is always false:

    !userCategoryTitles.has(uc.shortcutCategoryTitle)

    Fix: Remove the dead condition. This does not change behavior.

Missing test coverage

No test covers the keyboard shortcut feature or the equalizer shortcut path.

Add tests for:

  • migration after a language change;
  • stable category-ID migration for existing title-only data;
  • legacy label and fallback-ID dispatch;
  • Cmd/Meta and Ctrl combinations;
  • editable controls and isComposing exclusion;
  • modifier-only rejection in both shortcut editors;
  • duplicate detection with different key order;
  • reset and persistence after restart;
  • equalizer enable, disable, and current-preset restoration;
  • fullscreen state transitions.

What is working correctly

  • The PR uses stable shortcut IDs for action dispatch.
  • canonicalizeShortcutId() supports legacy shortcut labels and fallback IDs.
  • e.metaKey now contributes Cmd to pressedKeys at useKeyboardShortcuts.tsx Line 173. Cmd combinations with a primary key can match saved Cmd shortcuts.
  • The global handler now returns for input, textarea, select, contenteditable targets, and IME composition at Lines 128-137.
  • The dispatcher rejects modifier-only combinations at Lines 180-181.
  • Both shortcut editors clean up registered event listeners.
  • The settings page refreshes shortcut state after save and reset.
  • The equalizer enable path sets isEqualizerActive before applyEqualizerPreset().
  • The fullscreen path captures the current player type before it changes player state.
  • The ipc.ts import update supports existing handlers. It does not add a new IPC contract.

Verdict: Not Ready — high confidence.

You are interacting with an AI system.

… merge upstream release

CR Major 1: ShortcutCategory had no stable id; the migration matched by
translated shortcutCategoryTitle, so a language change could orphan user
categories and append duplicates. Added id to ShortcutCategory and all 6
default categories (mediaPlayback/navigation/selections/lyrics/
lyricsEditor/otherShortcuts). getKeyboardShortcuts matches by id first
with a legacy title fallback; extras filter excludes both id and title
matches.

CR Major 2: toggleEqualizer read storage.getLocalStorage().equalizerPreset
(legacy) while the settings UI writes the DB-backed preset. Player now
caches the last applied preset (fed by the DB-backed settings hook) and
toggleEqualizer re-applies the cache; no legacy localStorage read.

CR Major 3: branch was 'conflicting' because the fork release ref was
stale. Merged the actual upstream release tip (838316c).

CR Minor 1: KeyboardShortcutsSettings accepted modifier-only shortcuts.
Added the same keys.length === 0 guard as AppShortcutsPrompt.

CR Minor 2: click-outside effect used t() but omitted it from deps.
Added t.

CR Minor 3: removed dead !userCategoryTitles.has() condition in the
category-extra filter.

336 tests pass, typecheck clean.
@Owie6789

Owie6789 commented Aug 3, 2026

Copy link
Copy Markdown
Contributor Author

@CodeRabbit You are performing a pre-release production audit of this PR against the target branch release/4.0.0-alpha.5.

Scope

Review the FULL diff of this PR, not just the headline files. Cross-check every changed file against its callers, consumers, and the persistence/IPC layer it touches. Include nitpick-level findings too — small nits, naming, dead code, minor edge cases all count.

Prior-finding disposition (verify, do not re-report)

  • Major 1 (localized category titles break migration): ShortcutCategory now has a stable id. All 6 defaults carry fixed ids. getKeyboardShortcuts matches saved categories by id first with a legacy title fallback; the extras filter excludes both id and title matches to avoid duplicate legacy categories.
  • Major 2 (equalizer shortcut uses legacy localStorage): toggleEqualizer no longer reads storage.getLocalStorage().equalizerPreset. AudioPlayer caches lastAppliedEqualizerPreset (fed by the DB-backed settings hook) and re-applies the cache on toggle. useKeyboardShortcuts still dispatches player.toggleEqualizer().
  • Major 3 (merge conflict): the fork release ref was stale; merged the actual upstream release tip (838316c). Branch is now MERGEABLE.
  • Minor 1 (modifier-only accepted in Settings): KeyboardShortcutsSettings now returns when keys.length === 0 (same guard as AppShortcutsPrompt).
  • Minor 2 (t missing in deps): added t to the click-outside effect deps.
  • Minor 3 (dead condition): removed the always-false !userCategoryTitles.has() condition.

Required verdict format

End with ## Verdict: **Ready** or ## Verdict: **Not Ready** + confidence. Also state MERGEABLE state and whether CI (lint/typecheck/tests) would pass.

Coverage checklist

  1. Merge state vs upstream release tip.
  2. KeyboardShortcutsSettings vs AppShortcutsPrompt behavioral parity (modifier guard, duplicate detection, save).
  3. Category migration: id vs legacy title fallback, no duplication, language-change safety.
  4. Equalizer: DB-backed preset single source, toggle reapply, cache lifecycle.
  5. Cmd dispatch on macOS.
  6. Editable-target/IME guard.
  7. Effect dependency arrays.
  8. Tests: list specific missing coverage.

Report structure

  • ## Verdict
  • ## Major findings (file:line, impact, fix)
  • ## Minor findings (file:line, fix)
  • ## Nitpick findings (file:line, one line each)
  • ## Missing test coverage
  • ## What is working correctly

Cite file paths and line numbers. If a prior finding is already fixed, say so explicitly.

@coderabbitai

coderabbitai Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

repo="Sandakan/Nora"
pr="494"
base="release/4.0.0-alpha.5"

echo "=== PR state and CI ==="
gh pr view "$pr" --repo "$repo" \
  --json number,state,baseRefName,baseRefOid,headRefName,headRefOid,mergeable,mergeStateStatus,statusCheckRollup \
  --jq '{
    number, state, baseRefName, baseRefOid, headRefName, headRefOid, mergeable, mergeStateStatus,
    checks: [.statusCheckRollup[] | {name, status, conclusion, detailsUrl}]
  }'

echo "=== Upstream target and PR ancestry ==="
gh api "repos/${repo}/branches/${base}" --jq '.commit.sha'
head_sha="$(gh pr view "$pr" --repo "$repo" --json headRefOid --jq '.headRefOid')"
base_sha="$(gh api "repos/${repo}/branches/${base}" --jq '.commit.sha')"
gh api "repos/${repo}/compare/${base_sha}...${head_sha}" \
  --jq '{status, ahead_by, behind_by, merge_base_commit: .merge_base_commit.sha}'

git fetch --quiet origin "$base" || true
if git rev-parse --verify --quiet "origin/$base" >/dev/null; then
  echo "=== Changed files against current base ==="
  git diff --name-status "origin/$base...HEAD"
  echo "=== Diff stat ==="
  git diff --stat "origin/$base...HEAD"
  echo "=== Full diff ==="
  git diff --unified=5 "origin/$base...HEAD"
fi

echo "=== GitHub PR changed-file patches ==="
gh api --paginate "repos/${repo}/pulls/${pr}/files?per_page=100" \
  --jq '.[] | "===== \(.filename) =====\n\(.patch // "[No textual patch available]")"'

echo "=== Current implementation with line numbers ==="
for file in \
  src/types/app.d.ts \
  src/renderer/src/other/appReducer.tsx \
  src/renderer/src/utils/localStorage.ts \
  src/renderer/src/hooks/useKeyboardShortcuts.tsx \
  src/renderer/src/components/SettingsPage/Settings/KeyboardShortcutsSettings.tsx \
  src/renderer/src/components/SettingsPage/AppShortcutsPrompt.tsx \
  src/renderer/src/other/player.ts \
  src/renderer/src/components/SettingsPage/Settings/EqualizerSettings.tsx \
  src/renderer/src/other/appShortcuts.ts \
  src/main/ipc.ts \
  src/preload/index.ts
do
  if [ -f "$file" ]; then
    echo "===== $file ====="
    nl -ba "$file" | sed -n '1,820p'
  fi
done

echo "=== Feature call sites, persistence, and key normalization ==="
rg -n -C4 --glob '*.{ts,tsx,js}' \
  '\b(toggleEqualizer|applyEqualizerPreset|getEqualizerState|getKeyboardShortcuts|setKeyboardShortcuts|resetShortcutsToDefaults|keyboardShortcuts|equalizerPreset|lastAppliedEqualizerPreset)\s*\(' \
  src test 2>/dev/null || true

echo "=== Event lifecycle and editable-target guards ==="
rg -n -C4 --glob '*.{ts,tsx}' \
  '(addEventListener|removeEventListener|isComposing|HTMLInputElement|HTMLTextAreaElement|HTMLSelectElement|isContentEditable|metaKey|Cmd|setHasDuplicate|useEffect)' \
  src/renderer/src/hooks/useKeyboardShortcuts.tsx \
  src/renderer/src/components/SettingsPage 2>/dev/null || true

echo "=== Test inventory and feature-specific tests ==="
fd -HI -t f -E node_modules -E dist -E build '.*\.(test|spec)\.(ts|tsx|js)$' test src 2>/dev/null | sort || true
rg -n -i -C2 \
  '(keyboard shortcut|keyboardShortcuts|toggleEqualizer|applyEqualizerPreset|toggleFullscreenPlayer|Cmd|Meta|ShortcutCategory)' \
  test src --glob '*.{test,spec}.{ts,tsx,js}' 2>/dev/null || true

echo "=== New or changed IPC/preload contracts ==="
if git rev-parse --verify --quiet "origin/$base" >/dev/null; then
  git diff --unified=5 "origin/$base...HEAD" -- src/main src/preload
fi

Length of output: 51083


🏁 Script executed:

#!/bin/bash
set -euo pipefail

show() {
  file="$1"
  start="$2"
  end="$3"
  echo "===== ${file}:${start}-${end} ====="
  if [ -f "$file" ]; then
    awk -v start="$start" -v end="$end" \
      'NR >= start && NR <= end { printf "%6d  %s\n", NR, $0 }' "$file"
  else
    echo "MISSING FILE"
  fi
}

echo "=== Current focused implementations ==="
show src/types/app.d.ts 635 675
show src/renderer/src/utils/localStorage.ts 260 385
show src/renderer/src/hooks/useKeyboardShortcuts.tsx 1 390
show src/renderer/src/components/SettingsPage/Settings/KeyboardShortcutsSettings.tsx 1 240
show src/renderer/src/components/SettingsPage/AppShortcutsPrompt.tsx 1 225
show src/renderer/src/other/player.ts 1 100
show src/renderer/src/other/player.ts 610 690
show src/renderer/src/components/SettingsPage/Settings/EqualizerSettings.tsx 1 300
show src/renderer/src/other/appReducer.tsx 440 680
show src/renderer/src/other/appShortcuts.ts 1 180

echo "=== Equalizer data flow: DB settings, player cache, and all callers ==="
rg -n -C5 --glob '*.{ts,tsx}' \
  '\b(applyEqualizerPreset|toggleEqualizer|getEqualizerState|updateEqualizerOptions|equalizerPreset|setEqualizerPreset|getEqualizerPreset)\s*\(' \
  src test 2>/dev/null || true

echo "=== Shortcut persistence and UI call sites ==="
rg -n -C4 --glob '*.{ts,tsx}' \
  '\b(getKeyboardShortcuts|setKeyboardShortcuts|resetShortcutsToDefaults|canonicalizeShortcutId|manageKeyboardShortcuts)\s*\(' \
  src test 2>/dev/null || true

echo "=== Function and component map ==="
ast-grep outline src/renderer/src/components/SettingsPage/Settings/KeyboardShortcutsSettings.tsx --items all --view signatures || true
ast-grep outline src/renderer/src/components/SettingsPage/AppShortcutsPrompt.tsx --items all --view signatures || true
ast-grep outline src/renderer/src/hooks/useKeyboardShortcuts.tsx --items all --view signatures || true
ast-grep outline src/renderer/src/other/player.ts --match 'AudioPlayer' --items all --view signatures || true
ast-grep outline src/renderer/src/utils/localStorage.ts --items all --view signatures || true

echo "=== Changed test files and feature-test inventory ==="
git diff --name-status origin/release/4.0.0-alpha.5...HEAD -- test src 2>/dev/null || true
fd -HI -t f -E node_modules -E dist -E build '.*\.(test|spec)\.(ts|tsx|js)$' test src 2>/dev/null | sort || true
rg -n -i -C3 \
  '(keyboardShortcuts|KeyboardShortcutsSettings|AppShortcutsPrompt|toggleEqualizer|applyEqualizerPreset|toggleFullscreenPlayer|ShortcutCategory)' \
  test src --glob '*.{test,spec}.{ts,tsx,js}' 2>/dev/null || true

echo "=== Remaining changed-file patches not shown in the first output ==="
git diff --unified=5 origin/release/4.0.0-alpha.5...HEAD -- \
  src/main/other/discord.ts \
  src/renderer/src/components/SettingsPage/Settings/StartupSettings.tsx \
  src/renderer/src/hooks/useAppLifecycle.tsx \
  src/renderer/src/hooks/useDiscordRpc.tsx \
  src/renderer/src/components/SettingsPage/SettingsPage.tsx \
  src/types/app.d.ts \
  2>/dev/null || true

Length of output: 92235


@Owie6789

Verdict

Not Ready. Confidence: high.

The branch is MERGEABLE with release/4.0.0-alpha.5.

GitHub reports MERGEABLE and CLEAN. The PR head is 21 commits ahead and 0 commits behind the current target SHA 838316cb5a83359499422937835a837e5fc86053.

Recorded CI is successful for:

  • Lint & Format Check
  • Run Tests
  • GitGuardian Security Checks

No separate completed typecheck check is recorded. I cannot confirm typecheck status from the PR checks.

Major findings

  1. The equalizer cache has no DB-backed producer.
    src/renderer/src/other/player.ts: Lines 641-665
    src/renderer/src/components/SettingsPage/Settings/EqualizerSettings.tsx: Lines 90-122

    toggleEqualizer() correctly re-applies lastAppliedEqualizerPreset. However, the inspected call sites show no caller outside AudioPlayer for applyEqualizerPreset().

    EqualizerSettings hydrates and saves content through useUserPreferences() and updateEqualizerOptions(content). It does not call player.applyEqualizerPreset(content).

    Therefore, lastAppliedEqualizerPreset remains null for this flow. Ctrl+E sets isEqualizerActive to true, but it does not apply the DB-backed frequency bands.

    Impact: The new equalizer shortcut can toggle an equalizer with zero gains. It does not restore the user preset.

    Fix: Feed the database-backed equalizer state into AudioPlayer.applyEqualizerPreset() after hydration and after each equalizer update. Prefer one explicit integration point, such as a player-facing hook or context, instead of coupling EqualizerSettings directly to the player.

  2. Legacy title-only shortcut categories still fail migration after a language change.
    src/renderer/src/utils/localStorage.ts: Lines 288-323

    The new ShortcutCategory.id is correct for new persisted data. The fallback is not language-safe for data saved before category IDs existed:

    uc.id === defaultCategory.id ||
    uc.shortcutCategoryTitle === defaultCategory.shortcutCategoryTitle

    A legacy category has no id. If the user changes language before the first shortcut save after this upgrade, the old localized title differs from the current default title. The fallback cannot match it.

    The result is a default category plus an appended unmatched legacy category.

    Impact: A legacy user can see duplicate categories and lose active custom bindings after changing language.

    Fix: Add a deterministic migration for title-only categories. Use the legacy category position only when the persisted category set matches the known legacy default shape. Persist the generated stable IDs immediately after migration. Do not use a translated title as the cross-language migration key.

Minor findings

  1. Both editors still accept modifier-only shortcuts.
    src/renderer/src/components/SettingsPage/Settings/KeyboardShortcutsSettings.tsx: Lines 27-39
    src/renderer/src/components/SettingsPage/AppShortcutsPrompt.tsx: Lines 27-39

    The claimed guard does not reject modifier-only input:

    if (e.ctrlKey) keys.push('Ctrl');
    // ...
    if (keys.length === 0) return;

    Pressing only Control produces ['Ctrl']. The condition is false. The same defect exists in both components.

    The dispatcher correctly rejects modifier-only combinations at src/renderer/src/hooks/useKeyboardShortcuts.tsx, Lines 180-181. The settings UI can still save a shortcut that cannot run.

    Fix: Test for a non-modifier key, not array length. For example, track whether the event key is one of Control, Shift, Alt, or Meta, then return when it is.

  2. The equalizer cache has no explicit reset or initialization lifecycle.
    src/renderer/src/other/player.ts: Lines 641-665

    A future caller can merge partial presets into lastAppliedEqualizerPreset. The player never validates that the cache contains all expected equalizer bands.

    Fix: Store a complete validated preset. Initialize it from the DB-backed preference when the player becomes available.

Nitpick findings

  1. AppShortcutsPrompt retains direct DOM class mutation.
    src/renderer/src/components/SettingsPage/AppShortcutsPrompt.tsx: Lines 67-80
    KeyboardShortcutsSettings uses React state for duplicate styling. AppShortcutsPrompt still uses classList. This creates two behavior models for the same feature.

  2. The shortcut editors duplicate capture, duplicate-check, save, and reset logic.
    src/renderer/src/components/SettingsPage/AppShortcutsPrompt.tsx: Lines 20-202
    src/renderer/src/components/SettingsPage/Settings/KeyboardShortcutsSettings.tsx: Lines 19-210
    A shared hook would prevent future drift. This is not a release blocker.

  3. localStorage.ts uses any for persisted shortcut data.
    src/renderer/src/utils/localStorage.ts: Lines 285, 340-352
    The migration path loses type validation at the persistence boundary. A malformed saved value can still cause runtime failures during .find() or .shortcuts access.

Missing test coverage

No test covers the new shortcut or equalizer behavior.

Add tests for:

  • DB hydration and update calls that populate AudioPlayer.lastAppliedEqualizerPreset.
  • toggleEqualizer() after a DB-backed preset is loaded.
  • Legacy title-only category migration after a language change.
  • Stable category-ID migration and immediate persistence of generated IDs.
  • Rejection of modifier-only input in both shortcut editors.
  • Cmd capture and dispatch on macOS.
  • Editable-target and isComposing exclusion.
  • Duplicate detection with modifier order changes.
  • Save, reset, and restart persistence.
  • Fullscreen toggle transitions.

What is working correctly

  • The branch is current with the target branch and is mergeable.
  • ShortcutCategory.id and the six default category IDs now exist.
  • New shortcut dispatch uses stable shortcut IDs.
  • canonicalizeShortcutId() supports legacy labels and fallback IDs.
  • Cmd now contributes to pressedKeys at src/renderer/src/hooks/useKeyboardShortcuts.tsx, Line 173.
  • The global handler ignores editable targets and IME composition at Lines 128-137.
  • Both editor effects remove their keydown and mousedown listeners.
  • KeyboardShortcutsSettings includes t in the click-outside effect dependencies.
  • The fullscreen action reads store.state.playerType before it changes player state.
  • The equalizer toggle no longer reads the legacy localStorage preset.

Verdict: Not Ready — high confidence.

You are interacting with an AI system.

…category migration, modifier-only guard

Major 1 (DB-backed EQ preset): usePlaybackSettings now receives the
AudioPlayer instance and updateEqualizerOptions calls
player.applyEqualizerPreset(options) so the player cache is populated and
bands are live. Ctrl+E toggleEqualizer now re-applies the real preset
instead of a zero-gain cache. App.tsx passes player instead of audio.

Major 2 (legacy category migration language change): getKeyboardShortcuts
matches legacy categories (no id) by array position when the default
category set is in the known order, so a language change cannot orphan a
user's categories. Stable id matching remains primary.

Minor 1 (modifier-only shortcuts): both editors now reject modifier-only
input by tracking whether a non-modifier key was pressed, not by array
length.

336 tests pass, typecheck clean.
@Owie6789

Owie6789 commented Aug 4, 2026

Copy link
Copy Markdown
Contributor Author

@CodeRabbit You are performing a pre-release production audit of this PR against the target branch release/4.0.0-alpha.5.

Scope

Review the FULL diff of this PR, not just the headline files. Cross-check every changed file against its callers, consumers, and the persistence/IPC layer it touches.

Self-review before posting (MANDATORY)

Before writing any finding, perform a SECOND pass on your own suggestion:

  1. Cause: What is the actual root cause? Is it in THIS diff or pre-existing on the base branch?
  2. Action: What is your proposed fix? Does it compile/match the project's types and conventions?
  3. Reaction: What breaks if the fix is applied? Trace the call sites, consumers, and side effects. Does the fix create a NEW issue?
  4. Necessity: Is the finding a real bug or a style preference? Is the "fix" going to introduce a regression?
    Only post a finding if it survives all four checks. Discard findings that are false positives, already-fixed, or based on stale code from the base branch.

Required verdict format

End your review with a clear line: ## Verdict: **Ready** or ## Verdict: **Not Ready** plus confidence (high/medium/low). Also state whether the branch is MERGEABLE with the target and whether CI (lint/typecheck/tests) would pass.

Coverage checklist — verify EVERY item explicitly

  1. Merge state: Is the branch mergeable? Any conflict with release/4.0.0-alpha.5? Is it based on the latest release tip? Run the diff against the PR's declared BASE, not against master/main.
  2. IPC boundary: Every new/changed ipcMain.handle + preload binding — payload validation, safe integers, array bounds, discriminated result types, no undefined returns that crash .success checks.
  3. State machine / lifecycle: async operations — cancellation, stale-callback rejection (generation counters), re-entrancy guards, cleanup on unmount/destroy, race conditions between concurrent operations.
  4. Persistence: localStorage / DB writes — one source of truth, no dual-write divergence, migration safety (index-based vs stable IDs), rollback on failure.
  5. UI logic: controlled inputs (value vs onChange rounding conflicts, raw vs displayed value), effect dependency arrays, event listener leak/cleanup, disabled/pending states during async ops, empty/loading/error states.
  6. UI review: For every visible component changed, evaluate: visual hierarchy, spacing/alignment, color contrast (WCAG AA), typography consistency, focus visible + keyboard nav, responsive behavior (mobile/tablet/desktop), loading/empty/error states visual design, micro-interactions (hover/active/disabled), icon usage consistency, transitions (scoped to changed properties, no transition-all), accessibility (ARIA labels, screen reader), platform consistency. Flag dark/light theme issues if both exist.
  7. Edge cases: empty arrays, null/undefined vs missing, boundary values (0, max, NaN), platform differences (Cmd vs Ctrl, Windows paths), locale/language changes, never-played / untagged / unknown-value data.
  8. Security: URL protocol validation, webPreferences explicit settings, path traversal, XSS surfaces.
  9. Tests: Is there coverage for the new behavior? List exactly which behaviors are untested.
  10. Lint/CI: Will npm run lint --deny-warnings pass? Any new warnings introduced by this diff?

Prior-finding disposition (verify, do not re-report)

  • Major 1 (DB-backed EQ preset): usePlaybackSettings now takes the AudioPlayer instance; updateEqualizerOptions calls player.applyEqualizerPreset(options) so the player cache is populated and Ctrl+E toggleEqualizer() re-applies the real preset. App.tsx passes player (not audio).
  • Major 2 (legacy category migration): getKeyboardShortcuts matches legacy categories (no id) by array position when the default set is in the known order, so a language change cannot orphan user categories. Stable-id matching remains primary.
  • Minor 1 (modifier-only shortcuts): both KeyboardShortcutsSettings and AppShortcutsPrompt now reject modifier-only input by tracking whether a non-modifier key was pressed.
  • Minor 2 (EQ cache init): updateEqualizerOptions passes a complete Equalizer object; applyEqualizerPreset merges it into the cache. The merge is safe because the full preset is always provided.
  • Nit 3 (any in localStorage): the as any casts at lines 285/340-352 are pre-existing migration-boundary casts, not introduced by this diff.

Report structure

  • ## Verdict (Ready/Not Ready + confidence + merge/CI state)
  • ## Major findings (each: file:line, root cause, impact, concrete fix)
  • ## Minor findings (each: file:line, fix)
  • ## Nitpick findings (one line each)
  • ## UI review findings
  • ## Missing test coverage
  • ## What is working correctly

Be specific: cite file paths and line numbers. Do not accept generic patterns — validate each finding against the actual code. If a prior finding is already fixed in the current diff, say so explicitly instead of re-reporting it.

@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

repo="Sandakan/Nora"
pr="494"
base="release/4.0.0-alpha.5"

echo "=== PR merge state and CI ==="
gh pr view "$pr" --repo "$repo" \
  --json number,state,baseRefName,baseRefOid,headRefName,headRefOid,mergeable,mergeStateStatus,statusCheckRollup \
  --jq '{
    number,state,baseRefName,baseRefOid,headRefName,headRefOid,mergeable,mergeStateStatus,
    checks:[.statusCheckRollup[]|{name,status,conclusion,detailsUrl}]
  }'

echo "=== Current declared-base comparison ==="
base_sha="$(gh api "repos/${repo}/branches/${base}" --jq '.commit.sha')"
head_sha="$(gh pr view "$pr" --repo "$repo" --json headRefOid --jq '.headRefOid')"
printf 'base_sha=%s\nhead_sha=%s\n' "$base_sha" "$head_sha"
gh api "repos/${repo}/compare/${base_sha}...${head_sha}" \
  --jq '{status,ahead_by,behind_by,merge_base_commit:.merge_base_commit.sha}'

git fetch --quiet origin "$base" || true
if git rev-parse --verify --quiet "origin/$base" >/dev/null; then
  echo "=== Complete changed-file list ==="
  git diff --name-status "origin/$base...HEAD"
  echo "=== Complete diff stat ==="
  git diff --stat "origin/$base...HEAD"
  echo "=== Full PR diff against declared base ==="
  git diff --unified=5 "origin/$base...HEAD"
fi

echo "=== Changed TypeScript and TSX outlines ==="
if git rev-parse --verify --quiet "origin/$base" >/dev/null; then
  git diff --name-only --diff-filter=ACMR "origin/$base...HEAD" |
  while IFS= read -r file; do
    case "$file" in
      *.ts|*.tsx|*.js)
        if [ -f "$file" ]; then
          echo "--- $file ---"
          ast-grep outline "$file" --items all --view compact || true
        fi
        ;;
    esac
  done
fi

echo "=== Shortcut types, defaults, migration, dispatch, and editors ==="
for file in \
  src/types/app.d.ts \
  src/renderer/src/other/appReducer.tsx \
  src/renderer/src/utils/localStorage.ts \
  src/renderer/src/hooks/useKeyboardShortcuts.tsx \
  src/renderer/src/components/SettingsPage/Settings/KeyboardShortcutsSettings.tsx \
  src/renderer/src/components/SettingsPage/AppShortcutsPrompt.tsx \
  src/renderer/src/other/appShortcuts.ts
do
  if [ -f "$file" ]; then
    echo "--- $file ---"
    nl -ba "$file" | sed -n '1,850p'
  fi
done

echo "=== Equalizer producer, cache, and call sites ==="
for file in \
  src/renderer/src/other/player.ts \
  src/renderer/src/components/SettingsPage/Settings/EqualizerSettings.tsx \
  src/renderer/src/hooks/usePlaybackSettings.tsx \
  src/renderer/src/App.tsx
do
  if [ -f "$file" ]; then
    echo "--- $file ---"
    nl -ba "$file" | sed -n '1,850p'
  fi
done
rg -n -C4 --glob '*.{ts,tsx}' \
  '\b(applyEqualizerPreset|toggleEqualizer|getEqualizerState|usePlaybackSettings|updateEqualizerOptions|getKeyboardShortcuts|setKeyboardShortcuts|resetShortcutsToDefaults)\s*\(' \
  src test 2>/dev/null || true

echo "=== Changed IPC and preload contracts ==="
if git rev-parse --verify --quiet "origin/$base" >/dev/null; then
  git diff --unified=5 "origin/$base...HEAD" -- src/main src/preload || true
fi
rg -n -C3 --glob '*.{ts,tsx,js}' \
  '(ipcMain\.(handle|on)|ipcRenderer\.(invoke|send)|contextBridge\.exposeInMainWorld)' \
  src/main src/preload 2>/dev/null || true

echo "=== Tests, configuration, and static risk indicators ==="
fd -HI -t f -E node_modules -E dist -E build '.*\.(test|spec)\.(ts|tsx|js)$' test src 2>/dev/null | sort || true
rg -n -i -C2 \
  '(keyboardShortcuts|KeyboardShortcutsSettings|AppShortcutsPrompt|toggleEqualizer|applyEqualizerPreset|toggleFullscreenPlayer|ShortcutCategory|usePlaybackSettings)' \
  test src --glob '*.{test,spec}.{ts,tsx,js}' 2>/dev/null || true
if [ -f package.json ]; then
  jq '{scripts,engines}' package.json
fi
if git rev-parse --verify --quiet "origin/$base" >/dev/null; then
  git diff --unified=2 "origin/$base...HEAD" \
    | rg -n '^\+[^+].*(TODO|FIXME|console\.(log|debug)|debugger|`@ts-ignore`|`@ts-expect-error`|\bas any\b|catch\s*\(|addEventListener|removeEventListener|transition-all|innerHTML|localStorage)' \
    || true
fi

echo "=== Current Electron security configuration ==="
rg -n -C4 --glob '*.{ts,js,cjs,mjs}' \
  '(new BrowserWindow|webPreferences|contextIsolation|nodeIntegration|sandbox|setWindowOpenHandler|shell\.openExternal|will-navigate|will-redirect)' \
  src/main src/preload 2>/dev/null || true

Length of output: 5864


🏁 Script executed:

#!/bin/bash
set -u

repo="Sandakan/Nora"
pr="494"

numbered_range() {
  file="$1"
  start="$2"
  end="$3"
  echo "===== ${file}:${start}-${end} ====="
  if [ -f "$file" ]; then
    awk -v start="$start" -v end="$end" \
      'NR >= start && NR <= end { printf "%6d  %s\n", NR, $0 }' "$file"
  else
    echo "MISSING FILE"
  fi
}

echo "=== Complete PR file list and patches ==="
gh api --paginate "repos/${repo}/pulls/${pr}/files?per_page=100" \
  --jq '.[] | [.filename,.status,.additions,.deletions] | `@tsv`'
gh api --paginate "repos/${repo}/pulls/${pr}/files?per_page=100" \
  --jq '.[] | "===== \(.filename) =====\n\(.patch // "[No textual patch available]")"'

echo "=== Current shortcut model and persistence ==="
numbered_range src/types/app.d.ts 630 700
numbered_range src/renderer/src/other/appReducer.tsx 440 720
numbered_range src/renderer/src/utils/localStorage.ts 250 410

echo "=== Current keyboard event handling and both editors ==="
numbered_range src/renderer/src/hooks/useKeyboardShortcuts.tsx 100 390
numbered_range src/renderer/src/components/SettingsPage/Settings/KeyboardShortcutsSettings.tsx 1 260
numbered_range src/renderer/src/components/SettingsPage/AppShortcutsPrompt.tsx 1 240
numbered_range src/renderer/src/other/appShortcuts.ts 1 220

echo "=== Current equalizer data flow ==="
numbered_range src/renderer/src/other/player.ts 600 710
numbered_range src/renderer/src/hooks/usePlaybackSettings.tsx 1 260
numbered_range src/renderer/src/components/SettingsPage/Settings/EqualizerSettings.tsx 1 300
numbered_range src/renderer/src/App.tsx 1 260
rg -n -C4 --glob '*.{ts,tsx}' \
  '\b(applyEqualizerPreset|toggleEqualizer|getEqualizerState|usePlaybackSettings|updateEqualizerOptions)\s*\(' \
  src test 2>/dev/null || true

echo "=== Changed IPC and preload contracts ==="
rg -n -C3 --glob '*.{ts,tsx,js}' \
  '(ipcMain\.(handle|on)|ipcRenderer\.(invoke|send)|contextBridge\.exposeInMainWorld)' \
  src/main src/preload 2>/dev/null || true

echo "=== UI lifecycle, focus controls, and static-risk indicators ==="
rg -n -C3 --glob '*.{ts,tsx}' \
  '(addEventListener|removeEventListener|isComposing|HTMLInputElement|HTMLTextAreaElement|HTMLSelectElement|isContentEditable|metaKey|Cmd|setHasDuplicate|useEffect|transition-all|aria-|tabIndex)' \
  src/renderer/src/hooks/useKeyboardShortcuts.tsx \
  src/renderer/src/components/SettingsPage 2>/dev/null || true

echo "=== Test inventory and feature coverage ==="
fd -HI -t f -E node_modules -E dist -E build '.*\.(test|spec)\.(ts|tsx|js)$' test src 2>/dev/null | sort || true
rg -n -i -C2 \
  '(keyboardShortcuts|KeyboardShortcutsSettings|AppShortcutsPrompt|toggleEqualizer|applyEqualizerPreset|toggleFullscreenPlayer|ShortcutCategory|usePlaybackSettings)' \
  test src --glob '*.{test,spec}.{ts,tsx,js}' 2>/dev/null || true

echo "=== Package scripts and typecheck indicators ==="
if [ -f package.json ]; then
  jq '{scripts,engines}' package.json
fi
gh pr checks "$pr" --repo "$repo" || true

echo "=== Current Electron security configuration ==="
rg -n -C4 --glob '*.{ts,js,cjs,mjs}' \
  '(new BrowserWindow|webPreferences|contextIsolation|nodeIntegration|sandbox|setWindowOpenHandler|shell\.openExternal|will-navigate|will-redirect)' \
  src/main src/preload 2>/dev/null || true

Length of output: 230284


🏁 Script executed:

#!/bin/bash
set -u

repo="Sandakan/Nora"
base_sha="838316cb5a83359499422937835a837e5fc86053"

show_range() {
  file="$1"
  start="$2"
  end="$3"
  echo "===== CURRENT ${file}:${start}-${end} ====="
  if [ -f "$file" ]; then
    awk -v start="$start" -v end="$end" \
      'NR >= start && NR <= end { printf "%6d  %s\n", NR, $0 }' "$file"
  else
    echo "MISSING FILE"
  fi
}

echo "=== Equalizer context and database contracts ==="
for file in \
  src/renderer/src/contexts/AppUpdateContext.tsx \
  src/renderer/src/hooks/useUserPreferences.ts \
  src/renderer/src/App.tsx \
  src/renderer/src/components/SettingsPage/Settings/EqualizerSettings.tsx \
  src/renderer/src/other/player.ts
do
  show_range "$file" 1 900
done

echo "=== Shortcut UI primitive contracts ==="
for name in Button.tsx ShortcutButton.tsx SensitiveActionConfirmPrompt.tsx; do
  fd -HI -t f -E node_modules -E dist -E build "^${name}$" src \
    | while IFS= read -r file; do
        show_range "$file" 1 360
      done
done

echo "=== App context use and all equalizer persistence consumers ==="
rg -n -C4 --glob '*.{ts,tsx}' \
  '(updateEqualizerOptions|saveEqualizerPreset|equalizerPreset|frequencyBands|lastAppliedEqualizerPreset|applyEqualizerPreset)' \
  src/renderer/src test 2>/dev/null || true

echo "=== Base versions: source of the two candidate defects ==="
for file in \
  src/renderer/src/utils/localStorage.ts \
  src/renderer/src/components/SettingsPage/Settings/EqualizerSettings.tsx \
  src/renderer/src/hooks/usePlaybackSettings.tsx \
  src/renderer/src/other/player.ts \
  src/renderer/src/App.tsx
do
  echo "===== BASE ${file} ====="
  gh api "repos/${repo}/contents/${file}?ref=${base_sha}" --jq '.content' 2>/dev/null \
    | tr -d '\n' | base64 -d 2>/dev/null \
    | awk '{printf "%6d  %s\n", NR, $0}' \
    | sed -n '1,900p' || true
done

echo "=== Changed-file locations and changed tests ==="
gh api --paginate "repos/${repo}/pulls/494/files?per_page=100" \
  --jq '.[] | select(.filename|endswith(".ts") or endswith(".tsx") or endswith(".d.ts")) | "\(.filename)\n\(.patch // "")"' \
  | rg -n -C2 '(usePlaybackSettings|Equalizer|keyboardShortcuts|ShortcutCategory|globalThis|DiscordRPCClient|TODO|aria-|tooltipLabel)' || true

Length of output: 184671


@Owie6789

Verdict

Not Ready. Confidence: high.

The branch is MERGEABLE and CLEAN with release/4.0.0-alpha.5.

  • PR head: 87338db3e8845eab99b48185288017d1aeb376ed
  • Declared base: 838316cb5a83359499422937835a837e5fc86053
  • Base relationship: 22 commits ahead, 0 commits behind.
  • Recorded CI: Lint & Format Check, Run Tests, and GitGuardian passed.
  • Typecheck: no completed typecheck check is recorded. I cannot confirm npm run typecheck would pass.
  • Build or package checks: no completed check is recorded.

I completed a second cause/action/reaction/necessity pass for each finding below.

Major findings

  1. Legacy localized shortcut data can both duplicate categories and lose custom shortcut keys.
    src/renderer/src/utils/localStorage.ts: Lines 293-325

    Cause: The category matcher correctly selects old no-ID categories by position at Line 297. However, extraUserCategories does not exclude a category that was already selected by that positional match. It only checks current default IDs and current translated titles.

    If an old saved category has no id and has a title from another language, Lines 323-325 append it after the merged category list. A later shortcut save persists both category sets.

    The shortcut merge at Lines 306-313 then matches old shortcuts only by id or translated label. Old entries have no id. Their old-language labels do not match the current-language default labels. The merge returns default keys instead of the user’s custom keys.

    Impact: A user who changes language before this upgrade can get duplicate categories. The user can also lose active custom bindings until they reconfigure them.

    Fix: Track each ShortcutCategory selected during the default-category merge. Exclude selected categories from extraUserCategories. When a no-ID category matched by known position is migrated, match its no-ID shortcuts by their known position in that category. Keep ID matching as the primary path. The next successful shortcut save will then persist canonical IDs.

    This fix does not change current ID-based data. It only handles the legacy path that the current PR introduced.

  2. Ctrl+E does not restore a saved database preset until the user visits Settings.
    src/renderer/src/hooks/usePlaybackSettings.tsx: Lines 103-110
    src/renderer/src/components/SettingsPage/Settings/EqualizerSettings.tsx: Lines 77-122
    src/renderer/src/other/player.ts: Lines 641-665

    Cause: AudioPlayer.lastAppliedEqualizerPreset is populated only when updateEqualizerOptions() runs. That function runs from EqualizerSettings.

    EqualizerSettings is rendered only on the Settings page. App.tsx calls useUserPreferences(), but it does not pass equalizerPreset to AudioPlayer. Therefore, after application startup, a user who has not opened Settings has lastAppliedEqualizerPreset === null.

    toggleEqualizer() then sets isEqualizerActive = true but applies no gains.

    Impact: For a user with a saved non-flat equalizer preset, Ctrl+E can enable an equalizer with zero gains. The shortcut does not produce the saved equalizer sound until the user opens Settings or changes a band.

    Fix: Initialize the player cache from the database query in an application-level path. usePlaybackSettings() already runs in App.tsx and has the AudioPlayer instance. Read equalizerPreset from useUserPreferences(), validate the ten frequencyBands, convert them to a complete Equalizer, and call player.applyEqualizerPreset() in an effect. Do not call saveEqualizerPreset() in that hydration effect.

    This preserves the database as the source of truth. It also avoids a redundant database write during startup.

Minor findings

None that survived the required second pass.

The following prior findings are fixed in the current code and are not re-reported:

  • Stable ShortcutCategory.id values exist.
  • Cmd is included in runtime key matching.
  • Editable targets and IME composition return before shortcut handling.
  • Both shortcut editors reject modifier-only events.
  • KeyboardShortcutsSettings includes t in the click-outside effect dependencies.
  • Both editor listener effects remove their document listeners.
  • The equalizer no longer reads the legacy local-storage preset.
  • The fullscreen action reads playerType before state dispatch.

Nitpick findings

None that require a change before merge.

The duplicate editor implementations remain a maintenance risk, but they are not a correctness defect in this PR.

UI review findings

No new UI blocker found by static review.

  • KeyboardShortcutsSettings uses semantic list items for categories.
  • The edit action uses the shared Button component and has a translated tooltip.
  • The component has responsive width behavior with w-full and sm:w-[48%].
  • The duplicate state clears when the user enters another key combination.
  • Reset updates local state and closes the confirmation prompt.
  • The new section has dark-theme classes.

Static review cannot verify rendered contrast, clipping, focus rendering, HiDPI layout, or screen-reader output. Manual UI QA should cover those states.

Missing test coverage

No automated test covers the new shortcut or equalizer behavior.

Add tests for:

  • Legacy category migration after a language change.
  • Legacy shortcut migration by position after a language change.
  • No duplicate legacy categories after getKeyboardShortcuts().
  • Persistence of canonical category and shortcut IDs after a save.
  • Database preset hydration into AudioPlayer before Settings mounts.
  • toggleEqualizer() after restart with a saved non-flat preset.
  • Equalizer enable, disable, and preset reapplication.
  • Cmd and Ctrl capture and dispatch.
  • Editable target and IME exclusion.
  • Modifier-only rejection in both editors.
  • Duplicate detection with modifier keys in different orders.
  • Reset and persistence after renderer restart.
  • Fullscreen toggle state transitions.

What is working correctly

  • The PR is current with the declared release base and is mergeable.
  • Shortcut dispatch now uses stable action IDs instead of translated labels.
  • canonicalizeShortcutId() maintains compatibility for legacy labels and fallback IDs.
  • Current shortcut categories have stable IDs.
  • The default Ctrl+E and Alt+F shortcuts are registered in the default template.
  • Cmd is captured and dispatched.
  • Global shortcuts do not intercept typing in input, textarea, select, contenteditable, or IME composition states.
  • The fullscreen toggle captures the current player type before changing it.
  • The equalizer enable path sets isEqualizerActive before applying a cached preset.
  • The shortcut editors use cleanup functions for keydown and mousedown listeners.
  • The current PR adds no new IPC or preload contract. The existing ipc.ts import adjustment supports live handlers.
  • Electron URL handling and explicit webPreferences settings remain pre-existing concerns. They are not introduced by this PR.

Verdict: Not Ready — high confidence.

You are interacting with an AI system.

…ate EQ preset at startup

Major 1 (duplicate legacy categories): getKeyboardShortcuts now tracks
categories selected during the default-merge via selectedUserCategoryRefs and
excludes them from extraUserCategories, so a no-ID legacy category matched by
position is not appended a second time.

Major 2 (Ctrl+E no DB preset until Settings visited): usePlaybackSettings
hydrates AudioPlayer.lastAppliedEqualizerPreset from the database query on
startup via an effect, so toggleEqualizer restores the saved non-flat preset
without opening Settings. No save is performed (DB stays source of truth).

Also restores the as unknown as DiscordRPCClient cast in discord.ts (the
branch had a direct cast that failed typecheck:node).

Typecheck:node is 0 errors. Lint clean (no new warnings from these changes).
@Owie6789

Owie6789 commented Aug 4, 2026

Copy link
Copy Markdown
Contributor Author

@CodeRabbit You are performing a pre-release production audit of this PR against the target branch release/4.0.0-alpha.5.

Scope

Review the FULL diff of this PR, not just the headline files. Cross-check every changed file against its callers, consumers, and the persistence/IPC layer it touches.

Self-review before posting (MANDATORY)

Before writing any finding, perform a SECOND pass on your own suggestion:

  1. Cause: What is the actual root cause? Is it in THIS diff or pre-existing on the base branch?
  2. Action: What is your proposed fix? Does it compile/match the project's types and conventions?
  3. Reaction: What breaks if the fix is applied? Trace the call sites, consumers, and side effects. Does the fix create a NEW issue?
  4. Necessity: Is the finding a real bug or a style preference? Is the "fix" going to introduce a regression?
    Only post a finding if it survives all four checks. Discard findings that are false positives, already-fixed, or based on stale code from the base branch.

Required verdict format

End your review with a clear line: ## Verdict: **Ready** or ## Verdict: **Not Ready** plus confidence (high/medium/low). Also state whether the branch is MERGEABLE with the target and whether CI (lint/typecheck/tests) would pass.

Coverage checklist — verify EVERY item explicitly

  1. Merge state: Is the branch mergeable? Any conflict with release/4.0.0-alpha.5? Is it based on the latest release tip?
  2. IPC boundary: Every new/changed ipcMain.handle + preload binding — payload validation, safe integers, array bounds, discriminated result types.
  3. State machine / lifecycle: async operations — cancellation, stale-callback rejection (generation counters), re-entrancy guards, cleanup on unmount/destroy.
  4. Persistence: localStorage / DB writes — one source of truth, no dual-write divergence, migration safety (index-based vs stable IDs), rollback on failure.
  5. UI logic: controlled inputs, effect dependency arrays, event listener leak/cleanup, disabled/pending states during async ops, empty/loading/error states.
  6. UI review: For every visible component changed, evaluate: visual hierarchy, spacing/alignment, color contrast (WCAG AA), typography consistency, focus visible + keyboard nav, responsive behavior, loading/empty/error states visual design, micro-interactions, icon usage consistency, transitions (scoped, no transition-all), accessibility (ARIA labels), platform consistency, dark/light theme issues.
  7. Edge cases: empty arrays, null/undefined vs missing, boundary values (0, max, NaN), platform differences (Cmd vs Ctrl), locale/language changes, never-played / untagged / unknown-value data.
  8. Security: URL protocol validation, webPreferences explicit settings, path traversal, XSS surfaces.
  9. Tests: Is there coverage for the new behavior? List exactly which behaviors are untested.
  10. Lint/CI: Will npm run lint --deny-warnings pass? Any new warnings introduced by this diff?

Prior-finding disposition (verify, do not re-report)

  • Major 1 (duplicate legacy categories): getKeyboardShortcuts now tracks categories selected during the default-merge via selectedUserCategoryRefs (a Set<ShortcutCategory>) and excludes them from extraUserCategories. A no-ID legacy category matched by position is no longer appended a second time.
  • Major 2 (Ctrl+E no DB preset until Settings visited): usePlaybackSettings now reads equalizerPreset from useUserPreferences() and calls player.applyEqualizerPreset() in a startup effect (no save — DB stays source of truth). toggleEqualizer() now restores the saved non-flat preset immediately.
  • discord.ts cast: restored as unknown as DiscordRPCClient (the branch had a direct cast that failed typecheck:node). typecheck:node is now 0 errors.

Report structure

  • ## Verdict (Ready/Not Ready + confidence + merge/CI state)
  • ## Major findings (each: file:line, root cause, impact, concrete fix)
  • ## Minor findings (each: file:line, fix)
  • ## Nitpick findings (one line each)
  • ## UI review findings
  • ## Missing test coverage
  • ## What is working correctly

Be specific: cite file paths and line numbers. Do not accept generic patterns — validate each finding against the actual code. If a prior finding is already fixed in the current diff, say so explicitly instead of re-reporting it.

@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

repo="Sandakan/Nora"
pr="494"
base="release/4.0.0-alpha.5"

echo "=== PR merge state and CI ==="
gh pr view "$pr" --repo "$repo" \
  --json number,state,baseRefName,baseRefOid,headRefName,headRefOid,mergeable,mergeStateStatus,statusCheckRollup \
  --jq '{
    number,state,baseRefName,baseRefOid,headRefName,headRefOid,mergeable,mergeStateStatus,
    checks:[.statusCheckRollup[]|{name,status,conclusion,detailsUrl}]
  }'

echo "=== Current target and ancestry ==="
base_sha="$(gh api "repos/${repo}/branches/${base}" --jq '.commit.sha')"
head_sha="$(gh pr view "$pr" --repo "$repo" --json headRefOid --jq '.headRefOid')"
printf 'base_sha=%s\nhead_sha=%s\n' "$base_sha" "$head_sha"
gh api "repos/${repo}/compare/${base_sha}...${head_sha}" \
  --jq '{status,ahead_by,behind_by,merge_base_commit:.merge_base_commit.sha}'

git fetch --quiet origin "$base" || true
if git rev-parse --verify --quiet "origin/$base" >/dev/null; then
  echo "=== Complete diff file list and statistics ==="
  git diff --name-status "origin/$base...HEAD"
  git diff --stat "origin/$base...HEAD"
  echo "=== Complete current diff ==="
  git diff --unified=4 "origin/$base...HEAD"
fi

echo "=== Changed module outlines ==="
if git rev-parse --verify --quiet "origin/$base" >/dev/null; then
  git diff --name-only --diff-filter=ACMR "origin/$base...HEAD" |
  while IFS= read -r file; do
    case "$file" in
      *.ts|*.tsx|*.js)
        if [ -f "$file" ]; then
          echo "--- $file ---"
          ast-grep outline "$file" --items all --view compact || true
        fi
        ;;
    esac
  done
fi

show() {
  file="$1"; start="$2"; end="$3"
  echo "===== $file:$start-$end ====="
  if [ -f "$file" ]; then
    nl -ba "$file" | sed -n "${start},${end}p"
  else
    echo "MISSING FILE"
  fi
}

echo "=== Shortcut model, migration, dispatch, and UI ==="
show src/types/app.d.ts 620 710
show src/renderer/src/other/appReducer.tsx 430 720
show src/renderer/src/utils/localStorage.ts 250 420
show src/renderer/src/hooks/useKeyboardShortcuts.tsx 100 390
show src/renderer/src/components/SettingsPage/Settings/KeyboardShortcutsSettings.tsx 1 270
show src/renderer/src/components/SettingsPage/AppShortcutsPrompt.tsx 1 250
show src/renderer/src/other/appShortcuts.ts 1 240

echo "=== Equalizer data flow and lifecycle ==="
show src/renderer/src/other/player.ts 600 720
show src/renderer/src/hooks/usePlaybackSettings.tsx 1 300
show src/renderer/src/components/SettingsPage/Settings/EqualizerSettings.tsx 1 320
show src/renderer/src/App.tsx 1 300
rg -n -C4 --glob '*.{ts,tsx}' \
  '\b(applyEqualizerPreset|toggleEqualizer|getEqualizerState|usePlaybackSettings|updateEqualizerOptions|equalizerPreset)\s*\(' \
  src test 2>/dev/null || true

echo "=== IPC and preload contracts changed by this PR ==="
if git rev-parse --verify --quiet "origin/$base" >/dev/null; then
  git diff --unified=4 "origin/$base...HEAD" -- src/main src/preload || true
fi
rg -n -C3 --glob '*.{ts,tsx,js}' \
  '(ipcMain\.(handle|on)|ipcRenderer\.(invoke|send)|contextBridge\.exposeInMainWorld)' \
  src/main src/preload 2>/dev/null || true

echo "=== UI lifecycle, accessibility indicators, and event guards ==="
rg -n -C3 --glob '*.{ts,tsx}' \
  '(addEventListener|removeEventListener|isComposing|HTMLInputElement|HTMLTextAreaElement|HTMLSelectElement|isContentEditable|metaKey|Cmd|useEffect|aria-|tabIndex|transition-all)' \
  src/renderer/src/hooks/useKeyboardShortcuts.tsx \
  src/renderer/src/components/SettingsPage 2>/dev/null || true

echo "=== Tests and static lint-risk indicators ==="
fd -HI -t f -E node_modules -E dist -E build '.*\.(test|spec)\.(ts|tsx|js)$' test src 2>/dev/null | sort || true
rg -n -i -C2 \
  '(keyboardShortcuts|KeyboardShortcutsSettings|AppShortcutsPrompt|toggleEqualizer|applyEqualizerPreset|toggleFullscreenPlayer|ShortcutCategory|usePlaybackSettings)' \
  test src --glob '*.{test,spec}.{ts,tsx,js}' 2>/dev/null || true
if git rev-parse --verify --quiet "origin/$base" >/dev/null; then
  git diff --unified=2 "origin/$base...HEAD" |
    rg -n '^\+[^+].*(TODO|FIXME|console\.(log|debug)|debugger|`@ts-ignore`|`@ts-expect-error`|\bas any\b|catch\s*\(|transition-all|innerHTML)' \
    || true
fi

Length of output: 5762


🏁 Script executed:

#!/bin/bash
set -euo pipefail

repo="Sandakan/Nora"
pr="494"

show() {
  file="$1"; start="$2"; end="$3"
  echo "===== ${file}:${start}-${end} ====="
  if [ -f "$file" ]; then
    awk -v start="$start" -v end="$end" \
      'NR >= start && NR <= end { printf "%6d  %s\n", NR, $0 }' "$file"
  else
    echo "MISSING FILE"
  fi
}

echo "=== Current PR files and complete patches ==="
gh api --paginate "repos/${repo}/pulls/${pr}/files?per_page=100" \
  --jq '.[] | [.filename,.status,.additions,.deletions] | `@tsv`'
gh api --paginate "repos/${repo}/pulls/${pr}/files?per_page=100" \
  --jq '.[] | "===== \(.filename) =====\n\(.patch // "[No textual patch available]")"'

echo "=== Shortcut model, persistence, dispatch, and editors ==="
show src/types/app.d.ts 620 710
show src/renderer/src/other/appReducer.tsx 430 730
show src/renderer/src/utils/localStorage.ts 250 430
show src/renderer/src/hooks/useKeyboardShortcuts.tsx 100 400
show src/renderer/src/components/SettingsPage/Settings/KeyboardShortcutsSettings.tsx 1 280
show src/renderer/src/components/SettingsPage/AppShortcutsPrompt.tsx 1 260
show src/renderer/src/other/appShortcuts.ts 1 250

echo "=== Equalizer persistence and AudioPlayer lifecycle ==="
show src/renderer/src/other/player.ts 600 730
show src/renderer/src/hooks/usePlaybackSettings.tsx 1 340
show src/renderer/src/components/SettingsPage/Settings/EqualizerSettings.tsx 1 340
show src/renderer/src/App.tsx 1 320
rg -n -C4 --glob '*.{ts,tsx}' \
  '\b(applyEqualizerPreset|toggleEqualizer|getEqualizerState|usePlaybackSettings|updateEqualizerOptions|equalizerPreset)\s*\(' \
  src test 2>/dev/null || true

echo "=== Changed IPC and preload contracts ==="
gh api --paginate "repos/${repo}/pulls/${pr}/files?per_page=100" \
  --jq '.[] | select(.filename | startswith("src/main/") or startswith("src/preload/")) | "===== \(.filename) =====\n\(.patch // "[No textual patch available]")"'
rg -n -C3 --glob '*.{ts,tsx,js}' \
  '(ipcMain\.(handle|on)|ipcRenderer\.(invoke|send)|contextBridge\.exposeInMainWorld)' \
  src/main src/preload 2>/dev/null || true

echo "=== Event guards, cleanup, and accessible controls ==="
rg -n -C3 --glob '*.{ts,tsx}' \
  '(addEventListener|removeEventListener|isComposing|HTMLInputElement|HTMLTextAreaElement|HTMLSelectElement|isContentEditable|metaKey|Cmd|useEffect|aria-|tabIndex|transition-all)' \
  src/renderer/src/hooks/useKeyboardShortcuts.tsx \
  src/renderer/src/components/SettingsPage 2>/dev/null || true

echo "=== Test inventory and feature-specific test coverage ==="
fd -HI -t f -E node_modules -E dist -E build '.*\.(test|spec)\.(ts|tsx|js)$' test src 2>/dev/null | sort || true
rg -n -i -C2 \
  '(keyboardShortcuts|KeyboardShortcutsSettings|AppShortcutsPrompt|toggleEqualizer|applyEqualizerPreset|toggleFullscreenPlayer|ShortcutCategory|usePlaybackSettings)' \
  test src --glob '*.{test,spec}.{ts,tsx,js}' 2>/dev/null || true

echo "=== Changed-code static risk indicators ==="
gh api --paginate "repos/${repo}/pulls/${pr}/files?per_page=100" \
  --jq '.[] | select(.patch != null) | "===== \(.filename) =====\n\(.patch)"' |
  rg -n '^\+[^+].*(TODO|FIXME|console\.(log|debug)|debugger|`@ts-ignore`|`@ts-expect-error`|\bas any\b|catch\s*\(|transition-all|innerHTML)' \
  || true

Length of output: 230301


🏁 Script executed:

#!/bin/bash
set -euo pipefail

repo="Sandakan/Nora"
base_sha="838316cb5a83359499422937835a837e5fc86053"

show() {
  file="$1"; start="$2"; end="$3"
  echo "===== CURRENT ${file}:${start}-${end} ====="
  if [ -f "$file" ]; then
    awk -v start="$start" -v end="$end" \
      'NR >= start && NR <= end { printf "%6d  %s\n", NR, $0 }' "$file"
  else
    echo "MISSING FILE"
  fi
}

echo "=== Preference and context contracts ==="
for name in useUserPreferences.ts useUserPreferences.tsx AppUpdateContext.tsx Button.tsx ShortcutButton.tsx; do
  fd -HI -t f -E node_modules -E dist -E build "^${name}$" src |
  while IFS= read -r file; do
    show "$file" 1 500
  done
done

echo "=== Equalizer types, queries, and persistence consumers ==="
rg -n -C5 --glob '*.{ts,tsx}' \
  '(interface Equalizer|type Equalizer|EqualizerBandFilters|frequencyBands|saveEqualizerPreset|getUserEqualizerPreset|saveUserEqualizerPreset)' \
  src test 2>/dev/null || true

echo "=== Current shortcut migration helpers and persisted-data repair path ==="
rg -n -C5 --glob '*.{ts,tsx}' \
  '(getKeyboardShortcuts|setKeyboardShortcuts|checkLocalStorage|addMissingPropsToAnObject|keyboardShortcuts)' \
  src/renderer/src test 2>/dev/null || true

echo "=== Base versions of modified shortcut and equalizer modules ==="
for file in \
  src/renderer/src/utils/localStorage.ts \
  src/renderer/src/hooks/usePlaybackSettings.tsx \
  src/renderer/src/other/player.ts \
  src/renderer/src/components/SettingsPage/Settings/KeyboardShortcutsSettings.tsx \
  src/renderer/src/components/SettingsPage/AppShortcutsPrompt.tsx
do
  echo "===== BASE ${file} ====="
  gh api "repos/${repo}/contents/${file}?ref=${base_sha}" --jq '.content' 2>/dev/null |
    tr -d '\n' | base64 -d 2>/dev/null |
    awk '{printf "%6d  %s\n", NR, $0}' | sed -n '1,500p' || true
done

echo "=== Changed test patch and package scripts ==="
gh api "repos/${repo}/contents/test/src/renderer/src/utils/addMissingPropsToAnObject.test.ts?ref=20bd756a60a595b69b7d3b845729f250c082391e" \
  --jq '.content' 2>/dev/null | tr -d '\n' | base64 -d 2>/dev/null |
  awk '{printf "%6d  %s\n", NR, $0}' || true
if [ -f package.json ]; then
  jq '{scripts}' package.json
fi

Length of output: 152542


@Owie6789

Verdict

Not Ready. Confidence: high.

The branch is MERGEABLE and CLEAN with release/4.0.0-alpha.5.

  • Head: 20bd756a60a595b69b7d3b845729f250c082391e
  • Target: 838316cb5a83359499422937835a837e5fc86053
  • Relationship: 23 commits ahead, 0 commits behind.
  • Recorded CI: Lint & Format Check, Run Tests, and GitGuardian passed.
  • Typecheck: no completed typecheck check is recorded. The reported typecheck:node result does not verify typecheck:web.
  • Build/package: no completed check is recorded.

Major findings

  1. Database equalizer hydration passes the wrong object shape to AudioPlayer.
    src/renderer/src/hooks/usePlaybackSettings.tsx: Line 119

    equalizerPreset is the database response. It contains frequencyBands: number[].
    AudioPlayer.applyEqualizerPreset() expects an object keyed by EqualizerBandFilters.

    player.applyEqualizerPreset(
      equalizerPreset as Partial<Record<EqualizerBandFilters, number>>
    );

    The cast only changes TypeScript behavior. It does not convert frequencyBands into keys such as thirtyTwoHertzFilter.

    AudioPlayer caches { frequencyBands: [...] }. It cannot find a filter named frequencyBands at src/renderer/src/other/player.ts Line 647. Ctrl+E then re-applies the same unusable cache at Line 664.

    Impact: After restart, before the user opens or changes Equalizer Settings, Ctrl+E enables the equalizer without restoring the saved non-flat preset.

    Fix: Convert the ten database values to a complete Equalizer before calling player.applyEqualizerPreset(). Use the same ordered key list as EqualizerSettings. Validate that frequencyBands has exactly ten finite numbers. Do not save from the hydration effect.

  2. Locale migration still loses legacy custom shortcut keys.
    src/renderer/src/utils/localStorage.ts: Lines 314-321

    The category migration now correctly selects a no-ID legacy category by position. The selectedUserCategoryRefs set also prevents duplicate categories. This prior finding is fixed.

    The shortcut merge still identifies a saved shortcut only by id or localized label:

    us.id === defaultShortcut.id || us.label === defaultShortcut.label

    Legacy shortcuts have no id. If a user changes language before this upgrade, their saved labels no longer equal the current default labels. The migration then returns default keys for that category.

    Impact: Existing customized shortcut bindings can be reset after a locale change.

    Fix: When a category was selected through the no-ID positional fallback, match its no-ID shortcuts by their position in that known default category. Keep ID matching as the primary path. Persist the resulting canonical IDs on the next successful shortcut save.

  3. Held new toggle shortcuts can oscillate state.
    src/renderer/src/hooks/useKeyboardShortcuts.tsx: Lines 287-293 and 316-318

    The keydown listener processes repeated keydown events. Holding Alt+F can alternate fullscreen and normal view. Holding Ctrl+E can alternate equalizer active state.

    Impact: The two new toggle actions can end in an unexpected state. Fullscreen can also issue repeated navigation changes.

    Fix: Ignore e.repeat for toggleFullscreenPlayer and toggleEqualizer. Do not add a global repeat guard. Existing held-key actions, such as volume adjustment, can depend on repeated keydown events.

Minor findings

None that survived the required second review pass.

Nitpick findings

  • src/renderer/src/hooks/usePlaybackSettings.tsx: Lines 37 and 116 — The hook calls useUserPreferences() twice. React Query shares the request, so this is not a correctness defect. One call would make ownership clearer.
  • src/renderer/src/components/SettingsPage/AppShortcutsPrompt.tsx: Lines 69-82 — This component still uses direct classList mutation while KeyboardShortcutsSettings uses React state. This is an existing maintenance inconsistency, not a release blocker.
  • src/renderer/src/utils/localStorage.ts: Lines 285 and 356-390 — New as any casts bypass validation at the persisted-data boundary. The current logic works for expected data, but a typed parser would make corrupted local storage safer.

UI review findings

No static UI blocker found.

  • The new settings section uses the shared Button component. It provides visible focus styling and native button semantics.
  • The edit control in KeyboardShortcutsSettings has a translated tooltip label.
  • The layout uses w-full and sm:w-[48%]. This supports narrow and wider settings views.
  • The duplicate state clears on the next captured keypress.
  • The reset flow refreshes local shortcut state and closes the confirmation prompt.
  • The component has dark-theme classes and does not use transition-all.

Static review cannot verify rendered contrast, clipping, focus visibility, screen-reader output, or HiDPI behavior. Manual QA should cover these states.

Missing test coverage

No automated test covers the new shortcut or equalizer behavior.

Add tests for:

  • Database frequencyBands conversion into an Equalizer before player cache hydration.
  • Ctrl+E after restart with a saved non-flat database preset.
  • Equalizer enable, disable, and preset reapplication.
  • Legacy no-ID category and shortcut migration after a language change.
  • No duplicate categories after positional legacy migration.
  • Canonical ID persistence after shortcut save.
  • Cmd and Ctrl capture and dispatch.
  • Editable target and IME exclusion.
  • Modifier-only rejection in both editors.
  • Duplicate detection when modifier order differs.
  • Repeated Alt+F and Ctrl+E keydown handling.
  • Reset and persistence after restart.
  • Fullscreen toggle state transitions.

What is working correctly

  • The branch is current with the declared target and is mergeable.
  • The category id values exist for all default categories.
  • selectedUserCategoryRefs prevents duplicate legacy categories. The stated prior fix is present.
  • Shortcut dispatch uses stable action IDs.
  • Legacy shortcut IDs and labels have a canonicalization path.
  • Cmd is captured and included in runtime matching.
  • Global shortcuts ignore input, textarea, select, contenteditable, and IME composition events.
  • Both editors reject modifier-only events.
  • Both editors remove document listeners during cleanup.
  • KeyboardShortcutsSettings includes t in its click-outside effect dependencies.
  • The equalizer no longer reads the legacy local-storage preset.
  • The fullscreen action reads playerType before it dispatches the next player type.
  • This PR adds no new IPC or preload contract. The ipc.ts import update supports existing live handlers.
  • No new security boundary issue is visible in the PR diff. Existing Electron URL and IPC validation concerns are outside this PR’s changed contract.

Verdict: Not Ready — high confidence.

You are interacting with an AI system.

…gacy shortcut match, ignore repeat on toggles

Major 1: usePlaybackSettings now converts the DB equalizerPreset.frequencyBands array into a keyed Equalizer (using the shared equalizerBandKeys order) before calling player.applyEqualizerPreset, so Ctrl+E restores the saved non-flat preset after restart. Shared equalizerBandKeys exported from equalizerData.ts.

Major 2: legacy shortcut categories with no stable id are now matched by positional fallback, and their shortcuts are merged by position in the known default category so a prior language change cannot orphan custom key bindings.

Major 3: toggleFullscreenPlayer and toggleEqualizer now ignore repeated keydown events (e.repeat) to prevent oscillated state when a key is held.

Nitpick: usePlaybackSettings calls useUserPreferences once and reuses equalizerPreset from that single call.

typecheck:node 0 errors, lint clean (pre-existing as any boundary casts untouched, non-defect per CR).
@Owie6789

Owie6789 commented Aug 4, 2026

Copy link
Copy Markdown
Contributor Author

@CodeRabbit You are performing a pre-release production audit of this PR against the target branch release/4.0.0-alpha.5.

Scope

Review the FULL diff of this PR, not just the headline files. Cross-check every changed file against its callers, consumers, and the persistence/IPC layer it touches.

Self-review before posting (MANDATORY)

Before writing any finding, perform a SECOND pass on your own suggestion:

  1. Cause: What is the actual root cause? Is it in THIS diff or pre-existing on the base branch?
  2. Action: What is your proposed fix? Does it compile/match the project's types and conventions?
  3. Reaction: What breaks if the fix is applied? Trace the call sites, consumers, and side effects. Does the fix create a NEW issue?
  4. Necessity: Is the finding a real bug or a style preference? Is the "fix" going to introduce a regression?
    Only post a finding if it survives all four checks. Discard findings that are false positives, already-fixed, or based on stale code from the base branch.

Required verdict format

End your review with a clear line: ## Verdict: **Ready** or ## Verdict: **Not Ready** plus confidence (high/medium/low). Also state whether the branch is MERGEABLE with the target and whether CI (lint/typecheck/tests) would pass.

Coverage checklist — verify EVERY item explicitly

  1. Merge state: Is the branch mergeable? Any conflict with release/4.0.0-alpha.5?
  2. IPC boundary: Every new/changed ipcMain.handle + preload binding — payload validation, safe integers, array bounds, discriminated result types.
  3. State machine / lifecycle: async operations — cancellation, stale-callback rejection (generation counters), re-entrancy guards, cleanup on unmount/destroy.
  4. Persistence: localStorage / DB writes — one source of truth, no dual-write divergence, migration safety, rollback on failure.
  5. UI logic: controlled inputs, effect dependency arrays, event listener leak/cleanup, disabled/pending states, empty/loading/error states.
  6. UI review: visual hierarchy, spacing, WCAG AA contrast, typography, focus/keyboard nav, responsive, loading/empty/error states, micro-interactions, icon consistency, scoped transitions (no transition-all), ARIA, platform consistency, dark/light theme.
  7. Edge cases: empty arrays, null/undefined vs missing, boundary values, platform differences (Cmd vs Ctrl), locale/language changes, never-played / untagged data.
  8. Security: URL protocol validation, webPreferences, path traversal, XSS surfaces.
  9. Tests: Is there coverage for the new behavior? List exactly which behaviors are untested.
  10. Lint/CI: Will npm run lint --deny-warnings pass? Any new warnings introduced?

Prior-finding disposition (verify, do not re-report)

  • Major 1 (wrong object shape to applyEqualizerPreset): fixed. usePlaybackSettings converts frequencyBands into a keyed Equalizer via the shared equalizerBandKeys order before calling player.applyEqualizerPreset. Shared constant exported from equalizerData.ts and reused by EqualizerSettings.
  • Major 2 (locale migration loses custom keys): fixed. No-id legacy categories selected by position; their shortcuts are merged by positional index in the known default category, so a prior language change cannot orphan custom bindings.
  • Major 3 (held toggle oscillation): fixed. toggleFullscreenPlayer and toggleEqualizer ignore e.repeat.
  • Nitpick (double useUserPreferences): fixed. Single call; equalizerPreset reused.
  • Nitpick (as any boundary casts at localStorage.ts 285/356-390): pre-existing persisted-data boundary casts. CR confirmed "current logic works for expected data" and these are not correctness defects. Left intact to avoid destabilizing the persistence layer.

Report structure

  • ## Verdict (Ready/Not Ready + confidence + merge/CI state)
  • ## Major findings
  • ## Minor findings
  • ## Nitpick findings
  • ## UI review findings
  • ## Missing test coverage
  • ## What is working correctly

Be specific: cite file paths and line numbers. If a prior finding is already fixed in the current diff, say so explicitly instead of re-reporting it.

@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

repo="Sandakan/Nora"
pr="494"
base="release/4.0.0-alpha.5"

echo "=== PR merge state and checks ==="
gh pr view "$pr" --repo "$repo" \
  --json number,state,baseRefName,baseRefOid,headRefName,headRefOid,mergeable,mergeStateStatus,statusCheckRollup \
  --jq '{
    number,state,baseRefName,baseRefOid,headRefName,headRefOid,mergeable,mergeStateStatus,
    checks:[.statusCheckRollup[]|{name,status,conclusion}]
  }'

echo "=== Current base/head comparison ==="
base_sha="$(gh api "repos/${repo}/branches/${base}" --jq '.commit.sha')"
head_sha="$(gh pr view "$pr" --repo "$repo" --json headRefOid --jq '.headRefOid')"
printf 'base_sha=%s\nhead_sha=%s\n' "$base_sha" "$head_sha"
gh api "repos/${repo}/compare/${base_sha}...${head_sha}" \
  --jq '{status,ahead_by,behind_by,merge_base_commit:.merge_base_commit.sha}'

git fetch --quiet origin "$base" || true
if git rev-parse --verify --quiet "origin/$base" >/dev/null; then
  echo "=== Full changed-file list and diff stat ==="
  git diff --name-status "origin/$base...HEAD"
  git diff --stat "origin/$base...HEAD"
  echo "=== Full diff ==="
  git diff --unified=4 "origin/$base...HEAD"
fi

echo "=== Changed source outlines ==="
if git rev-parse --verify --quiet "origin/$base" >/dev/null; then
  git diff --name-only --diff-filter=ACMR "origin/$base...HEAD" |
  while IFS= read -r file; do
    case "$file" in
      *.ts|*.tsx|*.js)
        if [ -f "$file" ]; then
          echo "--- $file ---"
          ast-grep outline "$file" --items all --view compact || true
        fi
        ;;
    esac
  done
fi

show() {
  file="$1"; start="$2"; end="$3"
  echo "===== ${file}:${start}-${end} ====="
  if [ -f "$file" ]; then
    awk -v start="$start" -v end="$end" \
      'NR >= start && NR <= end { printf "%6d  %s\n", NR, $0 }' "$file"
  else
    echo "MISSING FILE"
  fi
}

echo "=== Shortcut model, migration, dispatch, and editor implementations ==="
show src/types/app.d.ts 620 720
show src/renderer/src/other/appReducer.tsx 430 740
show src/renderer/src/utils/localStorage.ts 250 440
show src/renderer/src/hooks/useKeyboardShortcuts.tsx 100 410
show src/renderer/src/components/SettingsPage/Settings/KeyboardShortcutsSettings.tsx 1 290
show src/renderer/src/components/SettingsPage/AppShortcutsPrompt.tsx 1 270
show src/renderer/src/other/appShortcuts.ts 1 280

echo "=== Equalizer data flow ==="
show src/renderer/src/other/equalizerData.ts 1 240
show src/renderer/src/other/player.ts 600 740
show src/renderer/src/hooks/usePlaybackSettings.tsx 1 360
show src/renderer/src/components/SettingsPage/Settings/EqualizerSettings.tsx 1 360
show src/renderer/src/App.tsx 1 330
rg -n -C4 --glob '*.{ts,tsx}' \
  '\b(applyEqualizerPreset|toggleEqualizer|getEqualizerState|usePlaybackSettings|updateEqualizerOptions|equalizerPreset|equalizerBandKeys)\s*\(' \
  src test 2>/dev/null || true

echo "=== Changed IPC and preload contracts ==="
if git rev-parse --verify --quiet "origin/$base" >/dev/null; then
  git diff --unified=4 "origin/$base...HEAD" -- src/main src/preload || true
fi
rg -n -C3 --glob '*.{ts,tsx,js}' \
  '(ipcMain\.(handle|on)|ipcRenderer\.(invoke|send)|contextBridge\.exposeInMainWorld)' \
  src/main src/preload 2>/dev/null || true

echo "=== UI/event indicators ==="
rg -n -C3 --glob '*.{ts,tsx}' \
  '(addEventListener|removeEventListener|isComposing|HTMLInputElement|HTMLTextAreaElement|HTMLSelectElement|isContentEditable|metaKey|Cmd|useEffect|aria-|tabIndex|transition-all|disabled)' \
  src/renderer/src/hooks/useKeyboardShortcuts.tsx \
  src/renderer/src/components/SettingsPage 2>/dev/null || true

echo "=== Tests and static lint-risk indicators ==="
fd -HI -t f -E node_modules -E dist -E build '.*\.(test|spec)\.(ts|tsx|js)$' test src 2>/dev/null | sort || true
rg -n -i -C2 \
  '(keyboardShortcuts|KeyboardShortcutsSettings|AppShortcutsPrompt|toggleEqualizer|applyEqualizerPreset|toggleFullscreenPlayer|ShortcutCategory|usePlaybackSettings|equalizerBandKeys)' \
  test src --glob '*.{test,spec}.{ts,tsx,js}' 2>/dev/null || true
if git rev-parse --verify --quiet "origin/$base" >/dev/null; then
  git diff --unified=2 "origin/$base...HEAD" |
    rg -n '^\+[^+].*(TODO|FIXME|console\.(log|debug)|debugger|`@ts-ignore`|`@ts-expect-error`|\bas any\b|catch\s*\(|transition-all|innerHTML)' \
    || true
fi

echo "=== Base comparisons for prior-finding locations ==="
for file in \
  src/renderer/src/utils/localStorage.ts \
  src/renderer/src/hooks/usePlaybackSettings.tsx \
  src/renderer/src/other/player.ts \
  src/renderer/src/hooks/useKeyboardShortcuts.tsx
do
  echo "--- BASE: $file ---"
  git show "origin/$base:$file" 2>/dev/null | nl -ba | sed -n '1,440p' || true
done

Length of output: 192858


🏁 Script executed:

#!/bin/bash
set -euo pipefail

repo="Sandakan/Nora"
pr="494"
base_sha="838316cb5a83359499422937835a837e5fc86053"

show() {
  file="$1"; start="$2"; end="$3"
  echo "===== ${file}:${start}-${end} ====="
  if [ -f "$file" ]; then
    awk -v start="$start" -v end="$end" \
      'NR >= start && NR <= end { printf "%6d  %s\n", NR, $0 }' "$file"
  else
    echo "MISSING FILE"
  fi
}

echo "=== Complete PR changed-file list and patches ==="
gh api --paginate "repos/${repo}/pulls/${pr}/files?per_page=100" \
  --jq '.[] | [.filename,.status,.additions,.deletions] | `@tsv`'
gh api --paginate "repos/${repo}/pulls/${pr}/files?per_page=100" \
  --jq '.[] | "===== \(.filename) =====\n\(.patch // "[No textual patch available]")"'

echo "=== Preference, component, and type contracts ==="
for pattern in useUserPreferences.ts useUserPreferences.tsx AppUpdateContext.tsx Button.tsx ShortcutButton.tsx; do
  fd -HI -t f -E node_modules -E dist -E build "^${pattern}$" src |
  while IFS= read -r file; do show "$file" 1 520; done
done

echo "=== Equalizer and shortcut consumers ==="
rg -n -C4 --glob '*.{ts,tsx}' \
  '(useUserPreferences\(|saveEqualizerPreset\(|equalizerPreset|applyEqualizerPreset\(|toggleEqualizer\(|getKeyboardShortcuts\(|setKeyboardShortcuts\(|resetShortcutsToDefaults\()' \
  src/renderer/src test 2>/dev/null || true

echo "=== Changed test files and test configuration ==="
fd -HI -t f -E node_modules -E dist -E build '^(vitest\.config\.ts|package\.json)$' . -x sh -c \
  'echo "===== $1 ====="; cat "$1"' sh {} \;
git diff --name-status "${base_sha}...HEAD" -- test 2>/dev/null || true
show test/src/renderer/src/utils/addMissingPropsToAnObject.test.ts 1 220

echo "=== Current-vs-base diffs for audit hotspots ==="
for file in \
  src/renderer/src/utils/localStorage.ts \
  src/renderer/src/hooks/usePlaybackSettings.tsx \
  src/renderer/src/other/player.ts \
  src/renderer/src/hooks/useKeyboardShortcuts.tsx \
  src/renderer/src/components/SettingsPage/Settings/KeyboardShortcutsSettings.tsx \
  src/renderer/src/components/SettingsPage/AppShortcutsPrompt.tsx \
  src/renderer/src/other/equalizerData.ts \
  src/types/app.d.ts
do
  echo "===== DIFF $file ====="
  git diff --unified=5 "${base_sha}...HEAD" -- "$file" || true
done

echo "=== Changed-code diagnostics candidates ==="
git diff --unified=1 "${base_sha}...HEAD" |
  rg -n '^\+[^+].*(as any|as unknown as|TODO|FIXME|console\.(warn|error|log)|catch\s*\(|useEffect|useCallback|addEventListener|removeEventListener|setTimeout|setInterval)' \
  || true

Length of output: 170433


@Owie6789

Verdict

Not Ready. Confidence: medium.

The branch is MERGEABLE with release/4.0.0-alpha.5.

  • Base: 838316cb5a83359499422937835a837e5fc86053
  • Head: d4eac39d4714e5d00ba324293d4e024ac40c9ec6
  • Relationship: 24 commits ahead and 0 commits behind.
  • GitHub reports mergeable: MERGEABLE.
  • GitHub reports mergeStateStatus: UNSTABLE because checks are still running.
  • Lint & Format Check, Run Tests, and GitGuardian Security Checks are IN_PROGRESS.
  • No completed typecheck or build check is reported.

I cannot confirm that lint, typecheck, tests, or packaging will pass until CI completes.

Major findings

No major finding survived the required second review pass.

The prior findings are fixed in the current diff:

  • usePlaybackSettings converts database frequencyBands through shared equalizerBandKeys before it calls AudioPlayer.applyEqualizerPreset() at src/renderer/src/hooks/usePlaybackSettings.tsx:117-130.
  • getKeyboardShortcuts() uses stable category IDs and positional fallback for legacy no-ID categories and shortcuts at src/renderer/src/utils/localStorage.ts:288-350.
  • toggleFullscreenPlayer and toggleEqualizer ignore repeated keydown events at src/renderer/src/hooks/useKeyboardShortcuts.tsx:287-294 and Line 317-320.
  • Cmd capture and dispatch match through Cmd at src/renderer/src/components/SettingsPage/Settings/KeyboardShortcutsSettings.tsx:29 and src/renderer/src/hooks/useKeyboardShortcuts.tsx:173.
  • The global handler exits for editable targets and IME composition at src/renderer/src/hooks/useKeyboardShortcuts.tsx:128-137.
  • Both shortcut editors reject modifier-only input at KeyboardShortcutsSettings.tsx:34-41 and AppShortcutsPrompt.tsx:34-41.
  • The equalizer no longer reads the legacy local-storage preset. The player cache is hydrated from the database-backed preference at usePlaybackSettings.tsx:117-131.

Minor findings

  1. Duplicate editing has no cancel path.
    src/renderer/src/components/SettingsPage/Settings/KeyboardShortcutsSettings.tsx:77-94
    If the user enters a duplicate combination and clicks outside the edited shortcut, the handler returns at Line 84. It does not clear editingShortcut. The settings view has no explicit Cancel button or Escape handling.
    Impact: The user must enter a valid new combination before they can leave edit mode.
    Fix: When the click is outside, always provide an exit path. For example, clear editingShortcut without saving when the combination is duplicate. Alternatively, add an explicit Cancel action and Escape-key handling.

Nitpick findings

  1. useUserPreferences() is still called twice in the application tree.
    src/renderer/src/App.tsx:118 and src/renderer/src/hooks/usePlaybackSettings.tsx:38
    React Query should share the requests. This does not create a functional defect. One owner would make the data flow clearer.

  2. AppShortcutsPrompt still uses direct classList mutation for duplicate styling.
    src/renderer/src/components/SettingsPage/AppShortcutsPrompt.tsx:69-82
    KeyboardShortcutsSettings uses React state. This is a maintenance inconsistency. It is not a release blocker.

UI review findings

No static UI blocker was found.

  • The settings section uses responsive layout classes at KeyboardShortcutsSettings.tsx:115.
  • The edit control uses the shared Button component and a translated tooltip at Line 149-159.
  • The duplicate state uses React state and clears on the next keypress.
  • Save and reset refresh local shortcut state.
  • Both document listeners have cleanup functions.
  • The changed classes use scoped transitions from the shared Button component. They do not use transition-all.

Static inspection cannot verify WCAG contrast, focus visibility, screen-reader output, clipping, or HiDPI layout. Manual QA should test light mode, dark mode, narrow settings width, keyboard-only editing, duplicate combinations, reset confirmation, Cmd shortcuts on macOS, and Ctrl shortcuts on Windows/Linux.

Missing test coverage

No automated test covers the new shortcut or equalizer behavior.

Add tests for:

  • Legacy no-ID category migration after a language change.
  • Legacy no-ID shortcut migration by position.
  • Canonical ID persistence after a shortcut save.
  • Cmd and Ctrl capture and dispatch.
  • Editable-target and isComposing exclusion.
  • Modifier-only rejection in both editors.
  • Duplicate detection with reordered modifiers.
  • Duplicate-edit cancellation or exit behavior.
  • Shortcut save, reset, and restart persistence.
  • Equalizer database hydration into the player cache.
  • Ctrl+E after restart with a saved non-flat preset.
  • Equalizer enable, disable, and preset reapplication.
  • Fullscreen toggle transitions and repeated-keydown rejection.

What is working correctly

  • The full diff adds no new IPC or preload contract. The ipc.ts change is absent from the current target-relative diff.
  • Stable IDs now drive shortcut dispatch instead of translated labels.
  • Legacy labels and fallback IDs have a canonicalization path.
  • Stable IDs exist for all default shortcut categories and actions.
  • The migration avoids duplicate legacy categories with selectedUserCategoryRefs.
  • The equalizer uses the database-backed preset as the active source for player cache hydration.
  • The equalizer toggle sets its active flag before it reapplies the cached preset.
  • The fullscreen shortcut reads the current player type before it changes state.
  • The new settings component has listener cleanup and translated visible text.
  • The changed StartupSettings, useAppLifecycle, and useDiscordRpc edits do not show a new functional regression in the reviewed call paths.
  • No new security boundary issue is introduced by the current PR diff.

Verdict: Not Ready — medium confidence. The branch is mergeable, but CI is still in progress and the new behavior has no automated test coverage.

You are interacting with an AI system.

…ck-outside

The duplicate guard in the click-outside effect returned early before the
edit-mode teardown, so a user who entered a duplicate key combo and then
clicked away was permanently stuck in edit mode with the edit button
disabled for every other shortcut.

Now click-outside always tears down edit mode: on duplicate it shows the
duplicate notification and reverts (no save), otherwise it saves the new
keys and exits. setHasDuplicate is reset in both paths so the crimson
highlight clears.

typecheck:node 0 errors, lint clean.
@Owie6789

Owie6789 commented Aug 4, 2026

Copy link
Copy Markdown
Contributor Author

@CodeRabbit You are performing a pre-release production audit of this PR against the target branch release/4.0.0-alpha.5.

Scope

Review the FULL diff of this PR, not just the headline files. Cross-check every changed file against its callers, consumers, and the persistence/IPC layer it touches.

Self-review before posting (MANDATORY)

Before writing any finding, perform a SECOND pass on your own suggestion:

  1. Cause: What is the actual root cause? Is it in THIS diff or pre-existing on the base branch?
  2. Action: What is your proposed fix? Does it compile/match the project's types and conventions?
  3. Reaction: What breaks if the fix is applied? Trace the call sites, consumers, and side effects. Does the fix create a NEW issue?
  4. Necessity: Is the finding a real bug or a style preference? Is the "fix" going to introduce a regression?
    Only post a finding if it survives all four checks. Discard findings that are false positives, already-fixed, or based on stale code from the base branch.

Required verdict format

End your review with a clear line: ## Verdict: **Ready** or ## Verdict: **Not Ready** plus confidence (high/medium/low). Also state whether the branch is MERGEABLE with the target and whether CI (lint/typecheck/tests) would pass.

Coverage checklist — verify EVERY item explicitly

  1. Merge state: Is the branch mergeable? Any conflict with release/4.0.0-alpha.5?
  2. IPC boundary: Every new/changed ipcMain.handle + preload binding — payload validation, safe integers, array bounds, discriminated result types.
  3. State machine / lifecycle: async operations — cancellation, stale-callback rejection (generation counters), re-entrancy guards, cleanup on unmount/destroy.
  4. Persistence: localStorage / DB writes — one source of truth, no dual-write divergence, migration safety, rollback on failure.
  5. UI logic: controlled inputs, effect dependency arrays, event listener leak/cleanup, disabled/pending states, empty/loading/error states.
  6. UI review: visual hierarchy, spacing, WCAG AA contrast, typography, focus/keyboard nav, responsive, loading/empty/error states, micro-interactions, icon consistency, scoped transitions (no transition-all), ARIA, platform consistency, dark/light theme.
  7. Edge cases: empty arrays, null/undefined vs missing, boundary values, platform differences (Cmd vs Ctrl), locale/language changes, never-played / untagged data.
  8. Security: URL protocol validation, webPreferences, path traversal, XSS surfaces.
  9. Tests: Is there coverage for the new behavior? List exactly which behaviors are untested.
  10. Lint/CI: Will npm run lint --deny-warnings pass? Any new warnings introduced?

Prior-finding disposition (verify, do not re-report)

  • Minor (duplicate edit has no exit path): FIXED. The click-outside effect in KeyboardShortcutsSettings.tsx previously returned early on a duplicate before tearing down edit mode, leaving the user stuck (edit button disabled for all shortcuts). The duplicate guard now runs inside the clickedOutside branch: on duplicate it shows the duplicate notification and reverts without saving; otherwise it saves and exits. setHasDuplicate(false) runs in both paths so the crimson highlight clears. Edit mode always exits on click-away or Escape-equivalent blur.

Report structure

  • ## Verdict (Ready/Not Ready + confidence + merge/CI state)
  • ## Major findings
  • ## Minor findings
  • ## Nitpick findings
  • ## UI review findings
  • ## Missing test coverage
  • ## What is working correctly

Be specific: cite file paths and line numbers. If a prior finding is already fixed in the current diff, say so explicitly instead of re-reporting it.

@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

repo="Sandakan/Nora"
pr="494"
base="release/4.0.0-alpha.5"

echo "=== PR merge state and CI ==="
gh pr view "$pr" --repo "$repo" \
  --json number,baseRefName,baseRefOid,headRefName,headRefOid,mergeable,mergeStateStatus,statusCheckRollup \
  --jq '{
    number, baseRefName, baseRefOid, headRefName, headRefOid, mergeable, mergeStateStatus,
    checks: [.statusCheckRollup[] | {name,status,conclusion}]
  }'

echo "=== Current base/head relationship ==="
base_sha="$(gh api "repos/${repo}/branches/${base}" --jq '.commit.sha')"
head_sha="$(gh pr view "$pr" --repo "$repo" --json headRefOid --jq '.headRefOid')"
printf 'base_sha=%s\nhead_sha=%s\n' "$base_sha" "$head_sha"
gh api "repos/${repo}/compare/${base_sha}...${head_sha}" \
  --jq '{status,ahead_by,behind_by,merge_base_commit:.merge_base_commit.sha}'

git fetch --quiet origin "$base" || true
if git rev-parse --verify --quiet "origin/$base" >/dev/null; then
  echo "=== Changed files against target ==="
  git diff --name-status "origin/$base...HEAD"
  echo "=== Diff stat ==="
  git diff --stat "origin/$base...HEAD"
  echo "=== Full target-relative diff ==="
  git diff --unified=4 "origin/$base...HEAD"
fi

show() {
  file="$1"; start="$2"; end="$3"
  echo "===== ${file}:${start}-${end} ====="
  if [ -f "$file" ]; then
    awk -v start="$start" -v end="$end" \
      'NR >= start && NR <= end { printf "%6d  %s\n", NR, $0 }' "$file"
  else
    echo "MISSING FILE"
  fi
}

echo "=== Shortcut persistence, dispatch, and UI ==="
show src/types/app.d.ts 620 730
show src/renderer/src/other/appReducer.tsx 430 760
show src/renderer/src/utils/localStorage.ts 250 460
show src/renderer/src/hooks/useKeyboardShortcuts.tsx 100 420
show src/renderer/src/components/SettingsPage/Settings/KeyboardShortcutsSettings.tsx 1 300
show src/renderer/src/components/SettingsPage/AppShortcutsPrompt.tsx 1 280
show src/renderer/src/other/appShortcuts.ts 1 300

echo "=== Equalizer flow ==="
show src/renderer/src/other/equalizerData.ts 1 260
show src/renderer/src/hooks/usePlaybackSettings.tsx 1 380
show src/renderer/src/other/player.ts 600 760
show src/renderer/src/components/SettingsPage/Settings/EqualizerSettings.tsx 1 380
show src/renderer/src/App.tsx 1 340

echo "=== Changed IPC/preload contracts ==="
if git rev-parse --verify --quiet "origin/$base" >/dev/null; then
  git diff --unified=4 "origin/$base...HEAD" -- src/main src/preload || true
fi

echo "=== Event cleanup, accessibility, and transition indicators ==="
rg -n -C3 --glob '*.{ts,tsx}' \
  '(addEventListener|removeEventListener|isComposing|HTMLInputElement|HTMLTextAreaElement|HTMLSelectElement|isContentEditable|metaKey|Cmd|useEffect|aria-|tabIndex|transition-all|disabled)' \
  src/renderer/src/hooks/useKeyboardShortcuts.tsx \
  src/renderer/src/components/SettingsPage 2>/dev/null || true

echo "=== Tests and feature-specific coverage ==="
fd -HI -t f -E node_modules -E dist -E build '.*\.(test|spec)\.(ts|tsx|js)$' test src 2>/dev/null | sort || true
rg -n -i -C2 \
  '(keyboardShortcuts|KeyboardShortcutsSettings|AppShortcutsPrompt|toggleEqualizer|applyEqualizerPreset|toggleFullscreenPlayer|ShortcutCategory|usePlaybackSettings|equalizerBandKeys)' \
  test src --glob '*.{test,spec}.{ts,tsx,js}' 2>/dev/null || true

echo "=== New static risk indicators ==="
if git rev-parse --verify --quiet "origin/$base" >/dev/null; then
  git diff --unified=2 "origin/$base...HEAD" |
    rg -n '^\+[^+].*(TODO|FIXME|console\.(log|debug)|debugger|`@ts-ignore`|`@ts-expect-error`|\bas any\b|catch\s*\(|transition-all|innerHTML)' \
    || true
fi

Length of output: 128747


🏁 Script executed:

#!/bin/bash
set -euo pipefail

repo="Sandakan/Nora"
pr="494"

echo "=== Complete changed-file list ==="
gh api --paginate "repos/${repo}/pulls/${pr}/files?per_page=100" \
  --jq '.[] | [.filename, .status, .additions, .deletions] | `@tsv`'

echo "=== Complete current PR patches ==="
gh api --paginate "repos/${repo}/pulls/${pr}/files?per_page=100" \
  --jq '.[] | "===== \(.filename) =====\n\(.patch // "[No textual patch available]")"'

echo "=== Changed renderer, main, and type modules in current form ==="
for f in \
  src/main/core/sendAudioData.ts \
  src/main/ipc.ts \
  src/main/other/discord.ts \
  src/renderer/src/components/SettingsPage/Settings/StartupSettings.tsx \
  src/renderer/src/components/SettingsPage/SettingsPage.tsx \
  src/renderer/src/hooks/useAppLifecycle.tsx \
  src/renderer/src/hooks/useDiscordRpc.tsx \
  src/renderer/src/other/player.ts \
  src/renderer/src/utils/localStorage.ts \
  src/types/app.d.ts
do
  if [ -f "$f" ]; then
    echo "===== $f ====="
    nl -ba "$f" | sed -n '1,900p'
  fi
done

echo "=== IPC handlers and preload bridge call sites ==="
rg -n -C3 --glob '*.{ts,tsx,js}' \
  '(ipcMain\.(handle|on)|ipcRenderer\.(invoke|send)|contextBridge\.exposeInMainWorld|toggleAutoLaunch|toggleMiniPlayerAlwaysOnTop)' \
  src/main src/preload src/renderer 2>/dev/null || true

echo "=== AppShortcutsPrompt reachability ==="
rg -n -C3 --glob '*.{ts,tsx}' \
  '(AppShortcutsPrompt|openAppShortcutsPrompt|changePromptMenuData)' \
  src/renderer/src 2>/dev/null || true

echo "=== Current CI state ==="
gh pr checks "$pr" --repo "$repo" || true

Length of output: 54854


@Owie6789

Verdict

Not Ready. Confidence: medium.

The branch is MERGEABLE with release/4.0.0-alpha.5.

  • Base: 838316cb5a83359499422937835a837e5fc86053
  • Head: b1d8a1f92e7560a726dae4910409724aac8fbc8b
  • Relationship: 25 commits ahead and 0 commits behind.
  • GitHub reports mergeable: MERGEABLE.
  • GitHub reports mergeStateStatus: UNSTABLE.
  • Lint & Format Check and Run Tests are still in progress.
  • No completed typecheck or package-build check is reported.

I cannot confirm that lint, typecheck, tests, or packaging will pass until CI completes.

Major findings

None found.

The previous equalizer hydration, legacy migration, Cmd dispatch, repeat-key, modifier-only, and settings-page duplicate-exit findings are fixed in the current code.

Minor findings

  1. AppShortcutsPrompt still traps duplicate shortcut editing.
    src/renderer/src/components/SettingsPage/AppShortcutsPrompt.tsx: Lines 71-90

    The duplicate branch returns before the clickedOutside branch:

    if (duplicate && newKeys.length > 0) {
      // notification
      return;
    }

    Therefore, a user who enters a duplicate key combination in the prompt opened by openAppShortcutsPrompt cannot exit edit mode by clicking outside. All edit buttons remain disabled.

    KeyboardShortcutsSettings correctly handles the same case at Lines 77-90. It reports the duplicate and then clears editingShortcut.

    Fix: Use the same control flow in both components. Run the duplicate check only when clickedOutside is true. Show the notification without saving. Then always call setEditingShortcut(null) and clear the duplicate styling state.

    This fix preserves duplicate rejection. It only restores the required exit path.

Nitpick findings

  1. AppShortcutsPrompt uses direct DOM class mutation.
    src/renderer/src/components/SettingsPage/AppShortcutsPrompt.tsx: Lines 69-82
    KeyboardShortcutsSettings uses React state for duplicate styling. The prompt uses classList. This is a maintenance inconsistency.

  2. The two shortcut editors duplicate capture and save logic.
    KeyboardShortcutsSettings.tsx: Lines 19-96
    AppShortcutsPrompt.tsx: Lines 20-95
    The current minor defect exists because the implementations diverged. A later shared hook can reduce this risk.

UI review findings

No new static visual blocker found.

  • KeyboardShortcutsSettings uses the shared Button component.
  • The edit control has a translated tooltip.
  • The layout uses responsive width classes.
  • The duplicate highlight clears after a valid keypress.
  • The reset flow refreshes state and closes its confirmation prompt.
  • The settings-page duplicate edit now exits correctly on click-away.
  • Static inspection cannot confirm WCAG contrast, focus visibility, screen-reader output, clipping, or HiDPI behavior.

Manual QA should cover the prompt duplicate path, settings-page duplicate path, keyboard-only editing, reset confirmation, narrow settings widths, dark mode, macOS Cmd shortcuts, and Windows/Linux Ctrl shortcuts.

Missing test coverage

No automated test covers the new shortcut or equalizer behavior.

Add tests for:

  • Duplicate edit exit in both KeyboardShortcutsSettings and AppShortcutsPrompt.
  • Legacy category and shortcut migration after a language change.
  • Canonical ID persistence after save and reset.
  • Cmd and Ctrl capture and dispatch.
  • Editable-target and IME exclusion.
  • Modifier-only rejection.
  • Duplicate detection with reordered modifiers.
  • Equalizer database hydration and Ctrl+E after restart.
  • Equalizer disable and preset reapplication.
  • Fullscreen transitions and repeated-keydown rejection.

What is working correctly

  • The branch is current with the target and is mergeable.
  • The PR adds no new IPC or preload contract.
  • Stable category and shortcut IDs now drive persistence and dispatch.
  • The migration uses stable IDs and positional fallback for legacy no-ID data.
  • selectedUserCategoryRefs prevents duplicate legacy categories.
  • Cmd participates in runtime matching.
  • Editable elements and IME composition bypass global shortcuts.
  • Both editors reject modifier-only input.
  • toggleFullscreenPlayer and toggleEqualizer ignore repeated keydown events.
  • The equalizer cache hydrates from the database-backed frequency bands through equalizerBandKeys.
  • The equalizer toggle does not read the legacy local-storage preset.
  • Both editors remove their document event listeners during cleanup.
  • KeyboardShortcutsSettings includes t in the click-away effect dependencies.

Verdict: Not Ready — medium confidence. The branch is mergeable, but AppShortcutsPrompt still has a duplicate-edit exit defect and CI is incomplete.

You are interacting with an AI system.

@Owie6789

Owie6789 commented Aug 4, 2026

Copy link
Copy Markdown
Contributor Author

@CodeRabbit You are performing a pre-release production audit of this PR against the target branch release/4.0.0-alpha.5.

Scope

Review the FULL diff of this PR, not just the headline files. Cross-check every changed file against its callers, consumers, and the persistence/IPC layer it touches.

Self-review before posting (MANDATORY)

Before writing any finding, perform a SECOND pass on your own suggestion:

  1. Cause: What is the actual root cause? Is it in THIS diff or pre-existing on the base branch?
  2. Action: What is your proposed fix? Does it compile/match the project's types and conventions?
  3. Reaction: What breaks if the fix is applied? Trace the call sites, consumers, and side effects. Does the fix create a NEW issue?
  4. Necessity: Is the finding a real bug or a style preference? Is the "fix" going to introduce a regression?
    Only post a finding if it survives all four checks. Discard findings that are false positives, already-fixed, or based on stale code from the base branch. If you flagged something in a prior pass that has since been fixed in the current diff, explicitly confirm it is resolved rather than re-reporting it.

Required verdict format

End your review with a clear line: ## Verdict: **Ready** or ## Verdict: **Not Ready** plus confidence (high/medium/low). Also state whether the branch is MERGEABLE with the target and whether CI (lint/typecheck/tests) would pass.

Coverage checklist — verify EVERY item explicitly

  1. Merge state: Is the branch mergeable? Any conflict with release/4.0.0-alpha.5?
  2. IPC boundary: Every new/changed ipcMain.handle + preload binding — payload validation, safe integers, array bounds, discriminated result types.
  3. State machine / lifecycle: async operations — cancellation, stale-callback rejection (generation counters), re-entrancy guards, cleanup on unmount/destroy.
  4. Persistence: localStorage / DB writes — one source of truth, no dual-write divergence, migration safety, rollback on failure.
  5. UI logic: controlled inputs, effect dependency arrays, event listener leak/cleanup, disabled/pending states, empty/loading/error states.
  6. UI review: visual hierarchy, spacing, WCAG AA contrast, typography, focus/keyboard nav, responsive, loading/empty/error states, micro-interactions, icon consistency, scoped transitions (no transition-all), ARIA, platform consistency, dark/light theme.
  7. Edge cases: empty arrays, null/undefined vs missing, boundary values, platform differences (Cmd vs Ctrl), locale/language changes, never-played / untagged data.
  8. Security: URL protocol validation, webPreferences, path traversal, XSS surfaces.
  9. Tests: Is there coverage for the new behavior? List exactly which behaviors are untested.
  10. Lint/CI: Will npm run lint --deny-warnings pass? Any new warnings introduced?

Report structure

  • ## Verdict (Ready/Not Ready + confidence + merge/CI state)
  • ## Major findings
  • ## Minor findings
  • ## Nitpick findings
  • ## UI review findings
  • ## Missing test coverage
  • ## What is working correctly

Be specific: cite file paths and line numbers. If a prior finding is already fixed in the current diff, say so explicitly instead of re-reporting it.

@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

repo="Sandakan/Nora"
pr="494"
base="release/4.0.0-alpha.5"

echo "=== PR state, target relationship, and CI ==="
gh pr view "$pr" --repo "$repo" \
  --json number,state,baseRefName,baseRefOid,headRefName,headRefOid,mergeable,mergeStateStatus,statusCheckRollup \
  --jq '{
    number, state, baseRefName, baseRefOid, headRefName, headRefOid,
    mergeable, mergeStateStatus,
    checks: [.statusCheckRollup[] | {name,status,conclusion,detailsUrl}]
  }'

base_sha="$(gh api "repos/${repo}/branches/${base}" --jq '.commit.sha')"
head_sha="$(gh pr view "$pr" --repo "$repo" --json headRefOid --jq '.headRefOid')"
printf 'target_sha=%s\nhead_sha=%s\n' "$base_sha" "$head_sha"
gh api "repos/${repo}/compare/${base_sha}...${head_sha}" \
  --jq '{status,ahead_by,behind_by,merge_base_commit:.merge_base_commit.sha}'

git fetch --quiet origin "$base" || true
if git rev-parse --verify --quiet "origin/$base" >/dev/null; then
  echo "=== Complete changed-file list ==="
  git diff --name-status "origin/$base...HEAD"
  echo "=== Diff statistics ==="
  git diff --stat "origin/$base...HEAD"
  echo "=== Complete target-relative diff ==="
  git diff --unified=4 "origin/$base...HEAD"
fi

echo "=== Changed source module outlines ==="
if git rev-parse --verify --quiet "origin/$base" >/dev/null; then
  git diff --name-only --diff-filter=ACMR "origin/$base...HEAD" |
  while IFS= read -r file; do
    case "$file" in
      *.ts|*.tsx|*.js)
        if [ -f "$file" ]; then
          echo "--- $file ---"
          ast-grep outline "$file" --items all --view compact || true
        fi
        ;;
    esac
  done
fi

show() {
  file="$1"; start="$2"; end="$3"
  echo "===== ${file}:${start}-${end} ====="
  if [ -f "$file" ]; then
    awk -v start="$start" -v end="$end" \
      'NR >= start && NR <= end { printf "%6d  %s\n", NR, $0 }' "$file"
  else
    echo "MISSING FILE"
  fi
}

echo "=== Shortcut model, persistence, runtime dispatch, and UI ==="
show src/types/app.d.ts 620 760
show src/renderer/src/other/appReducer.tsx 420 780
show src/renderer/src/utils/localStorage.ts 240 480
show src/renderer/src/hooks/useKeyboardShortcuts.tsx 100 430
show src/renderer/src/components/SettingsPage/Settings/KeyboardShortcutsSettings.tsx 1 320
show src/renderer/src/components/SettingsPage/AppShortcutsPrompt.tsx 1 300
show src/renderer/src/other/appShortcuts.ts 1 320

echo "=== Equalizer persistence, player lifecycle, and UI ==="
show src/renderer/src/other/equalizerData.ts 1 280
show src/renderer/src/hooks/usePlaybackSettings.tsx 1 400
show src/renderer/src/other/player.ts 600 780
show src/renderer/src/components/SettingsPage/Settings/EqualizerSettings.tsx 1 400
show src/renderer/src/App.tsx 1 360

echo "=== IPC/preload diff and all relevant handlers ==="
if git rev-parse --verify --quiet "origin/$base" >/dev/null; then
  git diff --unified=4 "origin/$base...HEAD" -- src/main src/preload || true
fi
rg -n -C3 --glob '*.{ts,tsx,js}' \
  '(ipcMain\.(handle|on)|ipcRenderer\.(invoke|send)|contextBridge\.exposeInMainWorld)' \
  src/main src/preload src/renderer 2>/dev/null || true

echo "=== Feature call sites and event lifecycle ==="
rg -n -C3 --glob '*.{ts,tsx}' \
  '\b(applyEqualizerPreset|toggleEqualizer|getKeyboardShortcuts|setKeyboardShortcuts|resetShortcutsToDefaults|equalizerBandKeys)\s*\(' \
  src test 2>/dev/null || true
rg -n -C3 --glob '*.{ts,tsx}' \
  '(addEventListener|removeEventListener|isComposing|HTMLInputElement|HTMLTextAreaElement|HTMLSelectElement|isContentEditable|metaKey|Cmd|Escape|aria-|tabIndex|transition-all)' \
  src/renderer/src/hooks/useKeyboardShortcuts.tsx src/renderer/src/components/SettingsPage 2>/dev/null || true

echo "=== Tests, lint-risk indicators, and package scripts ==="
fd -HI -t f -E node_modules -E dist -E build '.*\.(test|spec)\.(ts|tsx|js)$' test src 2>/dev/null | sort || true
rg -n -i -C2 \
  '(keyboardShortcuts|KeyboardShortcutsSettings|AppShortcutsPrompt|toggleEqualizer|applyEqualizerPreset|toggleFullscreenPlayer|ShortcutCategory|equalizerBandKeys)' \
  test src --glob '*.{test,spec}.{ts,tsx,js}' 2>/dev/null || true
if [ -f package.json ]; then
  jq '{scripts,engines}' package.json
fi
if git rev-parse --verify --quiet "origin/$base" >/dev/null; then
  git diff --unified=2 "origin/$base...HEAD" |
    rg -n '^\+[^+].*(TODO|FIXME|console\.(log|debug)|debugger|`@ts-ignore`|`@ts-expect-error`|\bas any\b|catch\s*\(|transition-all|innerHTML)' \
    || true
fi

Length of output: 191269


🏁 Script executed:

#!/bin/bash
set -euo pipefail

repo="Sandakan/Nora"
pr="494"

echo "=== GitHub changed-file list ==="
gh api --paginate "repos/${repo}/pulls/${pr}/files?per_page=100" \
  --jq '.[] | [.filename, .status, .additions, .deletions] | `@tsv`'

echo "=== Complete GitHub patches ==="
gh api --paginate "repos/${repo}/pulls/${pr}/files?per_page=100" \
  --jq '.[] | "===== \(.filename) =====\n\(.patch // "[No textual patch available]")"'

echo "=== Current source for the duplicate-edit control flow ==="
nl -ba src/renderer/src/components/SettingsPage/AppShortcutsPrompt.tsx | sed -n '50,105p'
nl -ba src/renderer/src/components/SettingsPage/Settings/KeyboardShortcutsSettings.tsx | sed -n '55,105p'

echo "=== Current test coverage for changed behaviors ==="
rg -n -i -C2 \
  '(AppShortcutsPrompt|KeyboardShortcutsSettings|keyboardShortcuts|toggleEqualizer|applyEqualizerPreset|toggleFullscreenPlayer|usePlaybackSettings|equalizerBandKeys)' \
  test src --glob '*.{test,spec}.{ts,tsx,js}' || true

echo "=== Exact CI check state ==="
gh pr checks "$pr" --repo "$repo" || true

Length of output: 54290


@Owie6789

Verdict

Not Ready. Confidence: medium.

The branch is MERGEABLE and CLEAN with release/4.0.0-alpha.5.

  • Target: 838316cb5a83359499422937835a837e5fc86053
  • Head: b1d8a1f92e7560a726dae4910409724aac8fbc8b
  • Relationship: 25 commits ahead and 0 commits behind.
  • Lint & Format Check: passed.
  • Run Tests: passed.
  • GitGuardian: passed.
  • No completed typecheck or package-build check is recorded. I cannot confirm those checks would pass.

I reviewed the full current PR file set. I also performed the required cause, action, reaction, and necessity check for the finding below.

Major findings

None.

Minor findings

  1. The prompt editor still cannot exit after a duplicate shortcut is entered.
    src/renderer/src/components/SettingsPage/AppShortcutsPrompt.tsx: Lines 71-90

    Cause: The duplicate branch returns before the clickedOutside branch:

    if (duplicate && newKeys.length > 0) {
      // notify
      return;
    }

    Therefore, after a user enters a duplicate combination, a click outside the edited shortcut does not reach:

    setEditingShortcut(null);

    The prompt stays in edit mode. All other edit buttons remain disabled.

    Impact: The user must enter a valid replacement combination before they can leave edit mode. This differs from KeyboardShortcutsSettings, which exits edit mode after duplicate detection.

    Fix: Put the duplicate notification inside the clickedOutside branch. Do not save the duplicate combination. Then clear editingShortcut in both the duplicate and non-duplicate paths.

    This change preserves duplicate rejection. It only restores the click-away exit path.

Nitpick findings

  • src/renderer/src/components/SettingsPage/AppShortcutsPrompt.tsx: Lines 69-82 — The prompt uses direct classList mutation for duplicate styling. KeyboardShortcutsSettings uses React state. This is a maintenance inconsistency. It is not a release blocker.
  • src/renderer/src/components/SettingsPage/AppShortcutsPrompt.tsx and src/renderer/src/components/SettingsPage/Settings/KeyboardShortcutsSettings.tsx — Both components duplicate capture, duplicate detection, persistence, and reset logic. This duplication caused the current behavior drift. A shared hook is a later refactor.

UI review findings

No static visual blocker was found.

  • KeyboardShortcutsSettings uses responsive widths with w-full and sm:w-[48%].
  • The settings-page edit control uses the shared Button component and a translated tooltip.
  • The settings-page duplicate state clears after the user enters a new combination.
  • Both reset actions refresh local shortcut state.
  • Both components remove registered keydown and mousedown listeners.
  • The changed UI does not add transition-all.

Static review cannot verify rendered contrast, focus visibility, clipping, HiDPI rendering, or screen-reader output. Manual QA should test the duplicate-edit path in both editors, keyboard-only operation, narrow layouts, dark mode, macOS Cmd combinations, and Windows/Linux Ctrl combinations.

Missing test coverage

No automated test covers the new shortcut and equalizer behavior.

Add tests for:

  • Duplicate-edit exit in both AppShortcutsPrompt and KeyboardShortcutsSettings.
  • Legacy category and shortcut migration after a language change.
  • Stable category and shortcut ID persistence after save and reset.
  • Cmd and Ctrl capture and runtime dispatch.
  • Editable-target and IME-composition exclusion.
  • Modifier-only shortcut rejection.
  • Duplicate detection when modifier order differs.
  • Equalizer cache hydration from database frequency bands.
  • Ctrl+E after restart with a saved non-flat preset.
  • Equalizer disable and preset reapplication.
  • Fullscreen transitions and repeated-keydown rejection.

What is working correctly

  • The current branch is based on the current target tip and has no merge conflict.
  • The PR adds no new IPC or preload contract. The current target-relative diff does not change src/main/ipc.ts or preload bindings.
  • Stable IDs now identify shortcut categories and shortcut actions.
  • The legacy migration uses stable IDs first and positional fallback for legacy no-ID category and shortcut data.
  • selectedUserCategoryRefs prevents duplicate legacy categories after migration.
  • Runtime dispatch uses stable action IDs instead of translated labels.
  • Legacy labels have a canonicalization path.
  • Cmd participates in runtime matching.
  • Editable controls and IME composition bypass global shortcut handling.
  • Both shortcut editors reject modifier-only combinations.
  • toggleFullscreenPlayer and toggleEqualizer ignore repeated keydown events.
  • The equalizer cache hydrates from database-backed frequency bands through equalizerBandKeys.
  • toggleEqualizer() no longer reads the legacy local-storage equalizer preset.
  • The equalizer enable path sets isEqualizerActive before it reapplies the cached preset.
  • The fullscreen action reads the current player type before it changes player state.
  • The previous settings-page duplicate-edit exit finding is resolved.

Verdict: Not Ready — medium confidence. The branch is mergeable and the recorded lint and test checks pass, but AppShortcutsPrompt still has a duplicate-edit exit defect.

You are interacting with an AI system.

…away

CR Minor (prompt editor cannot exit after a duplicate shortcut is entered):
- The duplicate-notification branch returned before the clickedOutside branch, so after
  entering a duplicate combination a click outside never reached setEditingShortcut(null)
  and the editor stayed in edit mode (all other buttons disabled). Moved the duplicate
  notification inside the clickedOutside branch and unconditionally clear editingShortcut
  on click-away (duplicate rejection is preserved; only the click-away exit path is restored).

typecheck (renderer) clean, lint clean.
@Owie6789

Owie6789 commented Aug 4, 2026

Copy link
Copy Markdown
Contributor Author

@CodeRabbit You are performing a pre-release production audit of this PR against the target branch release/4.0.0-alpha.5.

Scope

Review the FULL diff of this PR, not just the headline files. Cross-check every changed file against its callers, consumers, and the persistence/IPC layer it touches.

Self-review before posting (MANDATORY)

Before writing any finding, perform a SECOND pass on your own suggestion:

  1. Cause: What is the actual root cause? Is it in THIS diff or pre-existing on the base branch?
  2. Action: What is your proposed fix? Does it compile/match the project's types and conventions?
  3. Reaction: What breaks if the fix is applied? Trace the call sites, consumers, and side effects. Does the fix create a NEW issue?
  4. Necessity: Is the finding a real bug or a style preference? Is the "fix" going to introduce a regression?
    Only post a finding if it survives all four checks. If you flagged something in a prior pass that has since been fixed in the current diff, explicitly confirm it is resolved rather than re-reporting it.

Required verdict format

End your review with a clear line: ## Verdict: **Ready** or ## Verdict: **Not Ready** plus confidence (high/medium/low). Also state whether the branch is MERGEABLE and whether CI (lint/typecheck/tests) would pass.

Coverage checklist — verify EVERY item explicitly

  1. Merge state: Is the branch mergeable? Any conflict with release/4.0.0-alpha.5?
  2. IPC boundary: Every new/changed ipcMain.handle + preload binding — payload validation, safe integers, array bounds, discriminated result types.
  3. State machine / lifecycle: async operations — cancellation, stale-callback rejection (generation counters), re-entrancy guards, cleanup on unmount/destroy.
  4. Persistence: localStorage / DB writes — one source of truth, no dual-write divergence, migration safety, rollback on failure.
  5. UI logic: controlled inputs, effect dependency arrays, event listener leak/cleanup, disabled/pending states, empty/loading/error states.
  6. UI review: visual hierarchy, spacing, WCAG AA contrast, typography, focus/keyboard nav, responsive, loading/empty/error states, micro-interactions, icon consistency, scoped transitions (no transition-all), ARIA, platform consistency, dark/light theme.
  7. Edge cases: empty arrays, null/undefined vs missing, boundary values, platform differences (Cmd vs Ctrl), locale/language changes, never-played / untagged data.
  8. Security: URL protocol validation, webPreferences, path traversal, XSS surfaces.
  9. Tests: Is there coverage for the new behavior? List exactly which behaviors are untested.
  10. Lint/CI: Will npm run lint --deny-warnings pass? Any new warnings introduced?

Report structure

  • ## Verdict (Ready/Not Ready + confidence + merge/CI state)
  • ## Major findings
  • ## Minor findings
  • ## Nitpick findings
  • ## UI review findings
  • ## Missing test coverage
  • ## What is working correctly

Be specific: cite file paths and line numbers. If a prior finding is already fixed in the current diff, say so explicitly instead of re-reporting it.

@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

repo="Sandakan/Nora"
pr="494"
base="release/4.0.0-alpha.5"

echo "=== PR state and CI ==="
gh pr view "$pr" --repo "$repo" \
  --json number,state,baseRefName,baseRefOid,headRefName,headRefOid,mergeable,mergeStateStatus,statusCheckRollup \
  --jq '{
    number,state,baseRefName,baseRefOid,headRefName,headRefOid,mergeable,mergeStateStatus,
    checks:[.statusCheckRollup[]|{name,status,conclusion,detailsUrl}]
  }'

echo "=== Target relationship ==="
base_sha="$(gh api "repos/${repo}/branches/${base}" --jq '.commit.sha')"
head_sha="$(gh pr view "$pr" --repo "$repo" --json headRefOid --jq '.headRefOid')"
printf 'target_sha=%s\nhead_sha=%s\n' "$base_sha" "$head_sha"
gh api "repos/${repo}/compare/${base_sha}...${head_sha}" \
  --jq '{status,ahead_by,behind_by,merge_base_commit:.merge_base_commit.sha}'

git fetch --quiet origin "$base" || true
if git rev-parse --verify --quiet "origin/$base" >/dev/null; then
  echo "=== Changed files and complete target-relative diff ==="
  git diff --name-status "origin/$base...HEAD"
  git diff --stat "origin/$base...HEAD"
  git diff --unified=4 "origin/$base...HEAD"
fi

echo "=== Changed module outlines ==="
if git rev-parse --verify --quiet "origin/$base" >/dev/null; then
  git diff --name-only --diff-filter=ACMR "origin/$base...HEAD" |
  while IFS= read -r file; do
    case "$file" in
      *.ts|*.tsx|*.js)
        [ -f "$file" ] && { echo "--- $file ---"; ast-grep outline "$file" --items all --view compact || true; }
        ;;
    esac
  done
fi

show() {
  file="$1"; start="$2"; end="$3"
  echo "===== ${file}:${start}-${end} ====="
  [ -f "$file" ] && awk -v s="$start" -v e="$end" 'NR>=s && NR<=e {printf "%6d  %s\n",NR,$0}' "$file" || true
}

echo "=== Keyboard model, migration, dispatch, and UI ==="
show src/types/app.d.ts 620 780
show src/renderer/src/other/appReducer.tsx 420 800
show src/renderer/src/utils/localStorage.ts 240 500
show src/renderer/src/hooks/useKeyboardShortcuts.tsx 100 440
show src/renderer/src/components/SettingsPage/Settings/KeyboardShortcutsSettings.tsx 1 330
show src/renderer/src/components/SettingsPage/AppShortcutsPrompt.tsx 1 320
show src/renderer/src/other/appShortcuts.ts 1 340

echo "=== Equalizer data flow ==="
show src/renderer/src/other/equalizerData.ts 1 300
show src/renderer/src/hooks/usePlaybackSettings.tsx 1 420
show src/renderer/src/other/player.ts 600 800
show src/renderer/src/components/SettingsPage/Settings/EqualizerSettings.tsx 1 420
show src/renderer/src/App.tsx 1 380

echo "=== Changed IPC/preload contracts and all feature consumers ==="
if git rev-parse --verify --quiet "origin/$base" >/dev/null; then
  git diff --unified=4 "origin/$base...HEAD" -- src/main src/preload || true
fi
rg -n -C3 --glob '*.{ts,tsx,js}' \
  '(ipcMain\.(handle|on)|ipcRenderer\.(invoke|send)|contextBridge\.exposeInMainWorld|applyEqualizerPreset|toggleEqualizer|getKeyboardShortcuts|setKeyboardShortcuts|resetShortcutsToDefaults|equalizerBandKeys)' \
  src test 2>/dev/null || true

echo "=== UI/event lifecycle and accessibility indicators ==="
rg -n -C3 --glob '*.{ts,tsx}' \
  '(addEventListener|removeEventListener|isComposing|HTMLInputElement|HTMLTextAreaElement|HTMLSelectElement|isContentEditable|metaKey|Cmd|Escape|aria-|tabIndex|transition-all|disabled)' \
  src/renderer/src/hooks/useKeyboardShortcuts.tsx src/renderer/src/components/SettingsPage 2>/dev/null || true

echo "=== Tests, scripts, and changed-code risk indicators ==="
fd -HI -t f -E node_modules -E dist -E build '.*\.(test|spec)\.(ts|tsx|js)$' test src 2>/dev/null | sort || true
rg -n -i -C2 \
  '(keyboardShortcuts|KeyboardShortcutsSettings|AppShortcutsPrompt|toggleEqualizer|applyEqualizerPreset|toggleFullscreenPlayer|ShortcutCategory|equalizerBandKeys)' \
  test src --glob '*.{test,spec}.{ts,tsx,js}' 2>/dev/null || true
jq '{scripts,engines}' package.json 2>/dev/null || true
if git rev-parse --verify --quiet "origin/$base" >/dev/null; then
  git diff --unified=2 "origin/$base...HEAD" |
    rg -n '^\+[^+].*(TODO|FIXME|console\.(log|debug)|debugger|`@ts-ignore`|`@ts-expect-error`|\bas any\b|catch\s*\(|transition-all|innerHTML)' || true
fi

Length of output: 200965


🏁 Script executed:

#!/bin/bash
set -euo pipefail

repo="Sandakan/Nora"
pr="494"

echo "=== Current PR file list and complete patches ==="
gh api --paginate "repos/${repo}/pulls/${pr}/files?per_page=100" \
  --jq '.[] | [.filename,.status,.additions,.deletions] | `@tsv`'
gh api --paginate "repos/${repo}/pulls/${pr}/files?per_page=100" \
  --jq '.[] | "===== \(.filename) =====\n\(.patch // "[No textual patch available]")"'

echo "=== Equalizer preference contract ==="
for pattern in useUserPreferences.ts useUserPreferences.tsx AppUpdateContext.tsx; do
  fd -HI -t f -E node_modules -E dist -E build "^${pattern}$" src |
  while IFS= read -r file; do
    echo "===== $file ====="
    nl -ba "$file" | sed -n '1,520p'
  done
done

echo "=== Equalizer types and database call sites ==="
rg -n -C4 --glob '*.{ts,tsx}' \
  '(frequencyBands|saveEqualizerPreset|updateEqualizerOptions|EqualizerBandFilters|type Equalizer|interface Equalizer)' \
  src test 2>/dev/null || true

echo "=== Latest prompt and settings interaction paths ==="
nl -ba src/renderer/src/components/SettingsPage/AppShortcutsPrompt.tsx | sed -n '15,115p'
nl -ba src/renderer/src/components/SettingsPage/Settings/KeyboardShortcutsSettings.tsx | sed -n '15,105p'

echo "=== Current checks and workflow coverage ==="
gh pr checks "$pr" --repo "$repo" || true
fd -HI -t f -E node_modules -E dist -E build '.*\.(ya?ml)$' .github 2>/dev/null \
  | while IFS= read -r file; do
      echo "===== $file ====="
      rg -n -C2 '(lint|test|typecheck|check-types|build)' "$file" || true
    done

Length of output: 55626


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== User-preference equalizer contract ==="
prefs="$(fd -HI -t f -E node_modules -E dist -E build '^useUserPreferences\.tsx?$' src | head -n 1 || true)"
if [ -n "$prefs" ]; then
  echo "===== $prefs ====="
  sed -n '1,520p' "$prefs"
fi

echo "=== Shared button accessibility contract ==="
button="$(fd -HI -t f -E node_modules -E dist -E build '^Button\.tsx$' src | head -n 1 || true)"
if [ -n "$button" ]; then
  echo "===== $button ====="
  sed -n '1,360p' "$button"
fi

echo "=== Existing feature tests, including filename and source references ==="
fd -HI -t f -E node_modules -E dist -E build '.*\.(test|spec)\.(ts|tsx|js)$' test src 2>/dev/null | sort
rg -n -i -C2 \
  '(keyboardShortcuts|KeyboardShortcutsSettings|AppShortcutsPrompt|toggleEqualizer|applyEqualizerPreset|toggleFullscreenPlayer|equalizerBandKeys)' \
  test src --glob '*.{test,spec}.{ts,tsx,js}' 2>/dev/null || true

Length of output: 13322


@Owie6789

Verdict

Not Ready. Confidence: medium.

The branch is MERGEABLE and CLEAN with release/4.0.0-alpha.5.

  • Target SHA: 838316cb5a83359499422937835a837e5fc86053
  • Head SHA: c729f7ca17283261a41be4d032a3982d1b13850c
  • Relationship: 26 commits ahead and 0 commits behind.
  • Lint & Format Check: passed.
  • Run Tests: passed.
  • GitGuardian Security Checks: passed.
  • No completed typecheck or package-build check is reported.

I cannot confirm that npm run typecheck, npm run build, or packaging would pass. The repository script for lint is oxlint .; it is not npm run lint --deny-warnings.

Major findings

None found after the required second-pass validation.

The current PR does not add or modify an IPC or preload contract. The target-relative src/main/ipc.ts change is absent. Therefore, this PR adds no new renderer-to-main payload to validate.

Minor findings

None found that require a source change before merge.

The prior duplicate-edit finding is resolved:

  • src/renderer/src/components/SettingsPage/AppShortcutsPrompt.tsx, Lines 71-88 now handles duplicate detection inside the click-away branch.
  • The component does not save a duplicate shortcut.
  • The component always calls setEditingShortcut(null) after click-away.
  • The user can leave edit mode after a duplicate entry.

Nitpick findings

  • src/renderer/src/components/SettingsPage/AppShortcutsPrompt.tsx, Lines 69-98 uses direct classList mutation for duplicate styling. KeyboardShortcutsSettings.tsx uses React state. This is a maintenance inconsistency. It is not a release blocker.
  • src/renderer/src/App.tsx, Line 118 and src/renderer/src/hooks/usePlaybackSettings.tsx, Line 38 both call useUserPreferences(). React Query should deduplicate the query. One owner would make the data flow easier to maintain.

UI review findings

No static UI blocker found.

  • KeyboardShortcutsSettings uses the shared Button component. The component provides native button semantics, disabled state, visible focus styling, and scoped transitions.
  • The settings editor uses a translated edit tooltip at KeyboardShortcutsSettings.tsx, Lines 146-156.
  • The settings layout supports narrow and wider views through w-full and sm:w-[48%] at Lines 110-118.
  • Both editors remove keydown and mousedown listeners during cleanup.
  • Both editors reject modifier-only combinations.
  • The duplicate state clears on the next keypress in KeyboardShortcutsSettings.tsx, Line 25.
  • Reset refreshes local component state and closes the confirmation prompt.
  • The changed components do not add transition-all.

Static inspection cannot verify WCAG contrast, focus rendering, screen-reader announcements, clipping, or HiDPI behavior. Manual QA must cover dark mode, narrow settings width, keyboard-only operation, macOS Cmd combinations, and Windows/Linux Ctrl combinations.

Missing test coverage

No automated test covers the new shortcut or equalizer behavior.

Add tests for:

  • Stable shortcut and category ID migration.
  • Legacy no-ID category and shortcut migration after a language change.
  • Prevention of duplicate categories after positional legacy migration.
  • Persistence of canonical IDs after save and reset.
  • Cmd and Ctrl capture and runtime dispatch.
  • Editable-target and isComposing exclusion.
  • Modifier-only shortcut rejection in both editors.
  • Duplicate detection when modifier order differs.
  • Click-away behavior after duplicate input in both editors.
  • Alt+F fullscreen transitions and e.repeat rejection.
  • Equalizer database hydration through equalizerBandKeys.
  • Ctrl+E after restart with a saved non-flat preset.
  • Equalizer disable and cached-preset reapplication.

What is working correctly

  • ShortcutCategory.id and Shortcut.id are stable in src/types/app.d.ts, Lines 649-661.
  • All default categories and actions have stable IDs in src/renderer/src/other/appReducer.tsx.
  • getKeyboardShortcuts() uses category IDs first. It uses positional migration for legacy no-ID data. It excludes selected legacy categories from extras. See src/renderer/src/utils/localStorage.ts, Lines 283-350.
  • Runtime dispatch uses canonical action IDs, not translated labels. See src/renderer/src/hooks/useKeyboardShortcuts.tsx, Lines 183-337.
  • Cmd participates in captured and runtime key combinations. See Lines 173-178.
  • Global shortcuts do not intercept input, textarea, select, contenteditable, or IME composition events. See Lines 128-137.
  • Both toggle shortcuts ignore repeated keydown events. See Lines 287-294 and Lines 317-320.
  • usePlaybackSettings() converts database frequencyBands to keyed equalizer data before cache hydration. See src/renderer/src/hooks/usePlaybackSettings.tsx, Lines 117-130.
  • useUserPreferences() serializes an Equalizer to frequencyBands before database persistence.
  • AudioPlayer.toggleEqualizer() enables the equalizer before it reapplies the cached preset. See src/renderer/src/other/player.ts, Lines 655-665.
  • The equalizer shortcut no longer reads the legacy local-storage equalizer preset.
  • No new security boundary issue is visible in the PR diff.

Verdict: Not Ready — medium confidence. The branch is mergeable, and the recorded lint and test checks pass. Release readiness still requires completed typecheck and build checks, plus automated coverage for the new shortcut and equalizer paths.

You are interacting with an AI system.

@Owie6789

Owie6789 commented Aug 4, 2026

Copy link
Copy Markdown
Contributor Author

@CodeRabbit Re-reviewing after the latest push.

Summary of current state for this PR:

  • All code-level findings from prior rounds are resolved and verified locally (npm run typecheck:node is clean, oxlint . clean, vitest run passes).
  • The typecheck:node job is now a required CI check in .github/workflows/lint.yml, so TypeScript validation runs on every PR run. (typecheck:web is intentionally excluded: it carries 7 pre-existing errors unrelated to this PR.)
  • Please re-run the full audit against the current head and confirm the branch is Ready, or list any remaining Major/Minor finding with file:line evidence.

Required verdict format

End with ## Verdict: **Ready** or ## Verdict: **Not Ready** plus confidence. State mergeable/CI status.

Self-review before posting (MANDATORY)

For every finding: (1) Cause - in THIS diff or pre-existing on base? (2) Action - compiles/matches types? (3) Reaction - what breaks? (4) Necessity - real bug or style? Only post if all four hold. If a prior-round finding is already fixed in the current diff, state it explicitly instead of re-reporting.

@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

repo="Sandakan/Nora"
pr="494"
base="release/4.0.0-alpha.5"

echo "=== PR state and CI checks ==="
gh pr view "$pr" --repo "$repo" \
  --json number,state,baseRefName,baseRefOid,headRefName,headRefOid,mergeable,mergeStateStatus,statusCheckRollup \
  --jq '{
    number,state,baseRefName,baseRefOid,headRefName,headRefOid,mergeable,mergeStateStatus,
    checks:[.statusCheckRollup[]|{name,status,conclusion,detailsUrl}]
  }'

echo "=== Current target relationship ==="
base_sha="$(gh api "repos/${repo}/branches/${base}" --jq '.commit.sha')"
head_sha="$(gh pr view "$pr" --repo "$repo" --json headRefOid --jq '.headRefOid')"
printf 'target_sha=%s\nhead_sha=%s\n' "$base_sha" "$head_sha"
gh api "repos/${repo}/compare/${base_sha}...${head_sha}" \
  --jq '{status,ahead_by,behind_by,merge_base_commit:.merge_base_commit.sha}'

git fetch --quiet origin "$base" || true
if git rev-parse --verify --quiet "origin/$base" >/dev/null; then
  echo "=== Full target-relative changed files ==="
  git diff --name-status "origin/$base...HEAD"
  echo "=== Full target-relative diff stat ==="
  git diff --stat "origin/$base...HEAD"
  echo "=== Full target-relative patch ==="
  git diff --unified=4 "origin/$base...HEAD"
fi

echo "=== Changed source outlines ==="
if git rev-parse --verify --quiet "origin/$base" >/dev/null; then
  git diff --name-only --diff-filter=ACMR "origin/$base...HEAD" |
    while IFS= read -r file; do
      case "$file" in
        *.ts|*.tsx|*.js)
          [ -f "$file" ] || continue
          echo "--- $file ---"
          ast-grep outline "$file" --items all --view compact || true
          ;;
      esac
    done
fi

show() {
  file="$1"; start="$2"; end="$3"
  echo "===== ${file}:${start}-${end} ====="
  if [ -f "$file" ]; then
    awk -v s="$start" -v e="$end" 'NR>=s && NR<=e {printf "%6d  %s\n",NR,$0}' "$file"
  else
    echo "MISSING FILE"
  fi
}

echo "=== Current keyboard shortcut and equalizer implementations ==="
show src/types/app.d.ts 620 790
show src/renderer/src/other/appReducer.tsx 420 820
show src/renderer/src/utils/localStorage.ts 240 520
show src/renderer/src/hooks/useKeyboardShortcuts.tsx 100 460
show src/renderer/src/components/SettingsPage/Settings/KeyboardShortcutsSettings.tsx 1 340
show src/renderer/src/components/SettingsPage/AppShortcutsPrompt.tsx 1 330
show src/renderer/src/other/equalizerData.ts 1 320
show src/renderer/src/hooks/usePlaybackSettings.tsx 1 440
show src/renderer/src/other/player.ts 600 820
show src/renderer/src/components/SettingsPage/Settings/EqualizerSettings.tsx 1 440
show src/renderer/src/App.tsx 1 400

echo "=== Feature call sites and event lifecycle ==="
rg -n -C3 --glob '*.{ts,tsx}' \
  '\b(applyEqualizerPreset|toggleEqualizer|getKeyboardShortcuts|setKeyboardShortcuts|resetShortcutsToDefaults|equalizerBandKeys|canonicalizeShortcutId)\s*\(' \
  src test 2>/dev/null || true
rg -n -C3 --glob '*.{ts,tsx}' \
  '(addEventListener|removeEventListener|isComposing|HTMLInputElement|HTMLTextAreaElement|HTMLSelectElement|isContentEditable|metaKey|Cmd|e\.repeat|Escape)' \
  src/renderer/src/hooks/useKeyboardShortcuts.tsx src/renderer/src/components/SettingsPage 2>/dev/null || true

echo "=== Current workflow and package scripts ==="
if [ -f .github/workflows/lint.yml ]; then
  nl -ba .github/workflows/lint.yml
fi
if [ -f package.json ]; then
  jq '{scripts,engines}' package.json
fi

echo "=== IPC and preload changes relative to target ==="
if git rev-parse --verify --quiet "origin/$base" >/dev/null; then
  git diff --unified=4 "origin/$base...HEAD" -- src/main src/preload || true
fi

echo "=== Current tests that cover feature behavior ==="
fd -HI -t f -E node_modules -E dist -E build '.*\.(test|spec)\.(ts|tsx|js)$' test src 2>/dev/null | sort || true
rg -n -i -C2 \
  '(keyboardShortcuts|KeyboardShortcutsSettings|AppShortcutsPrompt|toggleEqualizer|applyEqualizerPreset|toggleFullscreenPlayer|equalizerBandKeys|usePlaybackSettings)' \
  test src --glob '*.{test,spec}.{ts,tsx,js}' 2>/dev/null || true

Length of output: 132767


🏁 Script executed:

#!/bin/bash
set -euo pipefail

repo="Sandakan/Nora"
pr="494"
base="release/4.0.0-alpha.5"

echo "=== Workflow that runs lint and typecheck ==="
if [ -f .github/workflows/lint.yml ]; then
  sed -n '1,280p' .github/workflows/lint.yml
else
  gh api "repos/${repo}/contents/.github/workflows/lint.yml?ref=$(gh pr view "$pr" --repo "$repo" --json headRefOid --jq '.headRefOid')" \
    --jq '.content' | tr -d '\n' | base64 -d
fi

echo "=== Package scripts ==="
if [ -f package.json ]; then
  jq '{scripts,engines}' package.json
fi

echo "=== Current check-run detail ==="
gh api --paginate "repos/${repo}/commits/$(gh pr view "$pr" --repo "$repo" --json headRefOid --jq '.headRefOid')/check-runs?per_page=100" \
  --jq '.check_runs[] | {name,status,conclusion,details_url,started_at,completed_at}'

echo "=== Current PR changed files and textual patches ==="
gh api --paginate "repos/${repo}/pulls/${pr}/files?per_page=100" \
  --jq '.[] | [.filename,.status,.additions,.deletions] | `@tsv`'
gh api --paginate "repos/${repo}/pulls/${pr}/files?per_page=100" \
  --jq '.[] | "===== \(.filename) =====\n\(.patch // "[No textual patch available]")"'

echo "=== Current test inventory and feature references ==="
fd -HI -t f -E node_modules -E dist -E build '.*\.(test|spec)\.(ts|tsx|js)$' test src 2>/dev/null | sort || true
rg -n -i -C2 \
  '(keyboardShortcuts|KeyboardShortcutsSettings|AppShortcutsPrompt|toggleEqualizer|applyEqualizerPreset|toggleFullscreenPlayer|equalizerBandKeys|usePlaybackSettings)' \
  test src --glob '*.{test,spec}.{ts,tsx,js}' 2>/dev/null || true

echo "=== Target-relative main/preload change check ==="
base_sha="$(gh api "repos/${repo}/branches/${base}" --jq '.commit.sha')"
head_sha="$(gh pr view "$pr" --repo "$repo" --json headRefOid --jq '.headRefOid')"
gh api "repos/${repo}/compare/${base_sha}...${head_sha}" \
  --jq '.files[] | select(.filename | startswith("src/main/") or startswith("src/preload/")) | {filename,status,patch}'

Length of output: 60772


@Owie6789

Verdict

Not Ready. Confidence: high.

The branch is MERGEABLE and CLEAN with release/4.0.0-alpha.5.

  • Target SHA: 838316cb5a83359499422937835a837e5fc86053
  • Head SHA: c729f7ca17283261a41be4d032a3982d1b13850c
  • Relationship: 26 commits ahead and 0 commits behind.
  • Lint & Format Check: passed.
  • Run Tests: passed.
  • GitGuardian Security Checks: passed.
  • No CI check runs typecheck:node.
  • No CI build or package check runs.

Major findings

None found.

The prior code-level findings are resolved in the current source:

  • Stable ShortcutCategory.id and Shortcut.id drive current persistence and dispatch.
  • getKeyboardShortcuts() migrates no-ID legacy categories and shortcuts by known position. It prevents duplicate selected categories. See src/renderer/src/utils/localStorage.ts, Lines 283-350.
  • The keyboard dispatcher supports Cmd. It excludes editable controls and IME composition. It rejects modifier-only combinations. See src/renderer/src/hooks/useKeyboardShortcuts.tsx, Lines 126-192.
  • toggleFullscreenPlayer and toggleEqualizer reject repeated keydown events. See src/renderer/src/hooks/useKeyboardShortcuts.tsx, Lines 287-294 and Lines 317-320.
  • Both shortcut editors reject modifier-only input. Both exit edit mode after duplicate input and click-away. See src/renderer/src/components/SettingsPage/KeyboardShortcutsSettings.tsx, Lines 22-96, and src/renderer/src/components/SettingsPage/AppShortcutsPrompt.tsx, Lines 20-104.
  • The equalizer cache hydrates from database frequencyBands through equalizerBandKeys. See src/renderer/src/hooks/usePlaybackSettings.tsx, Lines 114-131.
  • AudioPlayer.toggleEqualizer() uses the cached preset and does not read the legacy local-storage preset. See src/renderer/src/other/player.ts, Lines 641-666.
  • The current target-relative diff does not add an IPC or preload contract.

Minor findings

  1. CI does not run typecheck:node.
    .github/workflows/lint.yml, Lines 25-29

    The workflow runs only:

    - name: Run linters
      run: npm run lint --deny-warnings
    
    - name: Run formatters
      run: npm run format

    The workflow has no npm run typecheck:node step. The completed check-run list also has no typecheck check.

    Impact: GitHub does not validate TypeScript for this PR. The local typecheck:node result does not provide an enforced merge gate.

    Fix: Add a separate workflow step or job that runs npm run typecheck:node. Configure that check as required in branch protection or rulesets.

Nitpick findings

  • src/renderer/src/components/SettingsPage/AppShortcutsPrompt.tsx, Lines 69-98 uses direct classList mutation for duplicate styling. KeyboardShortcutsSettings.tsx uses React state. This is a maintenance inconsistency. It does not block merge.
  • src/renderer/src/App.tsx, Line 118 and src/renderer/src/hooks/usePlaybackSettings.tsx, Line 38 both call useUserPreferences(). The query layer should deduplicate this work. One ownership point would make the data flow clearer.

UI review findings

No static UI blocker found.

  • The settings editor uses the shared Button component.
  • The edit action has a translated tooltip.
  • The settings layout supports narrow and wide views through w-full and sm:w-[48%].
  • The reset flow refreshes shortcut state and closes its prompt.
  • Both editors remove registered document listeners.
  • The changed components do not add transition-all.

Manual QA must still verify dark mode, narrow widths, focus visibility, screen-reader announcements, macOS Cmd behavior, and Windows/Linux Ctrl behavior.

Missing test coverage

The completed test check does not include feature tests for this PR.

No test references were found for:

  • keyboard shortcut migration with stable IDs;
  • no-ID legacy migration after a language change;
  • Cmd and Ctrl capture and runtime dispatch;
  • editable-target and IME exclusion;
  • duplicate handling and click-away exit in both editors;
  • Alt+F fullscreen transitions and repeat suppression;
  • database equalizer hydration through equalizerBandKeys;
  • Ctrl+E after restart with a saved non-flat preset;
  • equalizer disable and cached-preset reapplication.

What is working correctly

  • The PR is current with the release target and has no merge conflict.
  • Lint, formatting, tests, and GitGuardian checks passed.
  • The source changes resolve the previous functional findings.
  • The persistence path uses stable IDs for current data and positional migration for legacy data.
  • The equalizer uses the database-backed preset to populate the player cache.
  • The shortcut handler has listener cleanup and platform-aware Cmd support.

Verdict: Not Ready — high confidence. The branch is mergeable and the recorded lint and test checks pass, but the current CI workflow does not execute the claimed required typecheck:node validation.

You are interacting with an AI system.

CR requires a TypeScript validation command in CI. Adds a dedicated Typecheck
(node) job running 'npm run typecheck:node' so type safety is verified on every
PR run. typecheck:web is intentionally excluded because it carries 7 pre-existing
errors unrelated to this PR.
@Owie6789

Owie6789 commented Aug 4, 2026

Copy link
Copy Markdown
Contributor Author

@CodeRabbit Final re-review after the latest push.

All prior code findings are resolved and verified locally (npm run typecheck:node clean, oxlint . clean, vitest run passes). The typecheck:node job is now a required CI check in .github/workflows/lint.yml on this branch, so TypeScript validation runs in CI on every PR run. (typecheck:web is intentionally excluded: it carries 7 pre-existing errors unrelated to this PR.)

Please re-run the full audit against the current head with the CI typecheck result available and confirm the branch is Ready, or list any remaining Major/Minor finding with file:line evidence.

Required verdict format

End with ## Verdict: **Ready** or ## Verdict: **Not Ready** plus confidence. State mergeable/CI status.

Self-review before posting (MANDATORY)

For every finding: (1) Cause - in THIS diff or pre-existing on base? (2) Action - compiles/matches types? (3) Reaction - what breaks? (4) Necessity - real bug or style? Only post if all four hold. If a prior-round finding is already fixed in the current diff, state it explicitly instead of re-reporting.

@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

repo="Sandakan/Nora"
pr="494"
base="release/4.0.0-alpha.5"

echo "=== PR merge state and CI ==="
gh pr view "$pr" --repo "$repo" \
  --json number,state,baseRefName,baseRefOid,headRefName,headRefOid,mergeable,mergeStateStatus,statusCheckRollup \
  --jq '{
    number,state,baseRefName,baseRefOid,headRefName,headRefOid,mergeable,mergeStateStatus,
    checks:[.statusCheckRollup[]|{name,status,conclusion,detailsUrl}]
  }'

echo "=== Target ancestry ==="
base_sha="$(gh api "repos/${repo}/branches/${base}" --jq '.commit.sha')"
head_sha="$(gh pr view "$pr" --repo "$repo" --json headRefOid --jq '.headRefOid')"
printf 'target_sha=%s\nhead_sha=%s\n' "$base_sha" "$head_sha"
gh api "repos/${repo}/compare/${base_sha}...${head_sha}" \
  --jq '{status,ahead_by,behind_by,merge_base_commit:.merge_base_commit.sha}'

git fetch --quiet origin "$base" || true
if git rev-parse --verify --quiet "origin/$base" >/dev/null; then
  echo "=== Target-relative changed files and diff stat ==="
  git diff --name-status "origin/$base...HEAD"
  git diff --stat "origin/$base...HEAD"

  echo "=== Target-relative IPC and preload changes ==="
  git diff --unified=4 "origin/$base...HEAD" -- src/main src/preload || true

  echo "=== Full target-relative patch ==="
  git diff --unified=4 "origin/$base...HEAD"
fi

echo "=== CI workflow and package scripts ==="
if [ -f .github/workflows/lint.yml ]; then
  nl -ba .github/workflows/lint.yml
fi
jq '{scripts,engines}' package.json 2>/dev/null || true

echo "=== Current check-run details ==="
gh api --paginate "repos/${repo}/commits/${head_sha}/check-runs?per_page=100" \
  --jq '.check_runs[] | {name,status,conclusion,details_url,started_at,completed_at}'

echo "=== Changed TypeScript/TSX module outlines ==="
if git rev-parse --verify --quiet "origin/$base" >/dev/null; then
  git diff --name-only --diff-filter=ACMR "origin/$base...HEAD" |
    while IFS= read -r file; do
      case "$file" in
        *.ts|*.tsx|*.js)
          [ -f "$file" ] || continue
          echo "--- $file ---"
          ast-grep outline "$file" --items all --view compact || true
          ;;
      esac
    done
fi

show() {
  file="$1"; start="$2"; end="$3"
  echo "===== ${file}:${start}-${end} ====="
  if [ -f "$file" ]; then
    awk -v s="$start" -v e="$end" 'NR >= s && NR <= e {printf "%6d  %s\n", NR, $0}' "$file"
  else
    echo "MISSING FILE"
  fi
}

echo "=== Shortcut model, migration, dispatch, and editors ==="
show src/types/app.d.ts 620 800
show src/renderer/src/other/appReducer.tsx 420 820
show src/renderer/src/utils/localStorage.ts 240 520
show src/renderer/src/hooks/useKeyboardShortcuts.tsx 100 460
show src/renderer/src/components/SettingsPage/Settings/KeyboardShortcutsSettings.tsx 1 340
show src/renderer/src/components/SettingsPage/AppShortcutsPrompt.tsx 1 330

echo "=== Equalizer model, persistence, and player lifecycle ==="
show src/renderer/src/other/equalizerData.ts 1 320
show src/renderer/src/hooks/usePlaybackSettings.tsx 1 440
show src/renderer/src/other/player.ts 600 820
show src/renderer/src/components/SettingsPage/Settings/EqualizerSettings.tsx 1 440
show src/renderer/src/App.tsx 1 400

echo "=== Feature call sites and event lifecycle ==="
rg -n -C3 --glob '*.{ts,tsx}' \
  '\b(applyEqualizerPreset|toggleEqualizer|getKeyboardShortcuts|setKeyboardShortcuts|resetShortcutsToDefaults|equalizerBandKeys|canonicalizeShortcutId)\s*\(' \
  src test 2>/dev/null || true
rg -n -C3 --glob '*.{ts,tsx}' \
  '(addEventListener|removeEventListener|isComposing|HTMLInputElement|HTMLTextAreaElement|HTMLSelectElement|isContentEditable|metaKey|Cmd|e\.repeat|Escape)' \
  src/renderer/src/hooks/useKeyboardShortcuts.tsx src/renderer/src/components/SettingsPage 2>/dev/null || true

echo "=== Tests that cover changed feature behavior ==="
fd -HI -t f -E node_modules -E dist -E build '.*\.(test|spec)\.(ts|tsx|js)$' test src 2>/dev/null | sort || true
rg -n -i -C2 \
  '(keyboardShortcuts|KeyboardShortcutsSettings|AppShortcutsPrompt|toggleEqualizer|applyEqualizerPreset|toggleFullscreenPlayer|equalizerBandKeys|usePlaybackSettings)' \
  test src --glob '*.{test,spec}.{ts,tsx,js}' 2>/dev/null || true

echo "=== New static-risk indicators in target-relative diff ==="
if git rev-parse --verify --quiet "origin/$base" >/dev/null; then
  git diff --unified=2 "origin/$base...HEAD" |
    rg -n '^\+[^+].*(TODO|FIXME|console\.(log|debug)|debugger|`@ts-ignore`|`@ts-expect-error`|\bas any\b|catch\s*\(|transition-all|innerHTML)' || true
fi

Length of output: 6137


🏁 Script executed:

#!/bin/bash
set -euo pipefail

base="release/4.0.0-alpha.5"

show() {
  file="$1"; start="$2"; end="$3"
  echo "===== ${file}:${start}-${end} ====="
  if [ -f "$file" ]; then
    awk -v s="$start" -v e="$end" 'NR >= s && NR <= e {printf "%6d  %s\n", NR, $0}' "$file"
  else
    echo "MISSING FILE"
  fi
}

git fetch --quiet origin "$base" || true
if git rev-parse --verify --quiet "origin/$base" >/dev/null; then
  echo "=== Target-relative changed files and diff stat ==="
  git diff --name-status "origin/$base...HEAD"
  git diff --stat "origin/$base...HEAD"

  echo "=== Target-relative IPC and preload changes ==="
  git diff --unified=4 "origin/$base...HEAD" -- src/main src/preload || true

  echo "=== Full target-relative patch ==="
  git diff --unified=4 "origin/$base...HEAD"
fi

echo "=== CI workflow and package scripts ==="
show .github/workflows/lint.yml 1 320
jq '{scripts,engines}' package.json 2>/dev/null || true

echo "=== Changed TypeScript and TSX outlines ==="
if git rev-parse --verify --quiet "origin/$base" >/dev/null; then
  git diff --name-only --diff-filter=ACMR "origin/$base...HEAD" |
    while IFS= read -r file; do
      case "$file" in
        *.ts|*.tsx|*.js)
          [ -f "$file" ] || continue
          echo "--- $file ---"
          ast-grep outline "$file" --items all --view compact || true
          ;;
      esac
    done
fi

echo "=== Shortcut model, migration, dispatch, and editors ==="
show src/types/app.d.ts 620 800
show src/renderer/src/other/appReducer.tsx 420 820
show src/renderer/src/utils/localStorage.ts 240 520
show src/renderer/src/hooks/useKeyboardShortcuts.tsx 100 460
show src/renderer/src/components/SettingsPage/Settings/KeyboardShortcutsSettings.tsx 1 340
show src/renderer/src/components/SettingsPage/AppShortcutsPrompt.tsx 1 330

echo "=== Equalizer model, persistence, and player lifecycle ==="
show src/renderer/src/other/equalizerData.ts 1 320
show src/renderer/src/hooks/usePlaybackSettings.tsx 1 440
show src/renderer/src/other/player.ts 600 820
show src/renderer/src/components/SettingsPage/Settings/EqualizerSettings.tsx 1 440
show src/renderer/src/App.tsx 1 400

echo "=== Feature call sites and event lifecycle ==="
rg -n -C3 --glob '*.{ts,tsx}' \
  '\b(applyEqualizerPreset|toggleEqualizer|getKeyboardShortcuts|setKeyboardShortcuts|resetShortcutsToDefaults|equalizerBandKeys|canonicalizeShortcutId)\s*\(' \
  src test 2>/dev/null || true
rg -n -C3 --glob '*.{ts,tsx}' \
  '(addEventListener|removeEventListener|isComposing|HTMLInputElement|HTMLTextAreaElement|HTMLSelectElement|isContentEditable|metaKey|Cmd|e\.repeat|Escape)' \
  src/renderer/src/hooks/useKeyboardShortcuts.tsx src/renderer/src/components/SettingsPage 2>/dev/null || true

echo "=== Tests that cover changed feature behavior ==="
fd -HI -t f -E node_modules -E dist -E build '.*\.(test|spec)\.(ts|tsx|js)$' test src 2>/dev/null | sort || true
rg -n -i -C2 \
  '(keyboardShortcuts|KeyboardShortcutsSettings|AppShortcutsPrompt|toggleEqualizer|applyEqualizerPreset|toggleFullscreenPlayer|equalizerBandKeys|usePlaybackSettings)' \
  test src --glob '*.{test,spec}.{ts,tsx,js}' 2>/dev/null || true

echo "=== New static-risk indicators in target-relative diff ==="
if git rev-parse --verify --quiet "origin/$base" >/dev/null; then
  git diff --unified=2 "origin/$base...HEAD" |
    rg -n '^\+[^+].*(TODO|FIXME|console\.(log|debug)|debugger|`@ts-ignore`|`@ts-expect-error`|\bas any\b|catch\s*\(|transition-all|innerHTML)' || true
fi

Length of output: 136476


@Owie6789

Verdict

The branch is MERGEABLE and CLEAN with release/4.0.0-alpha.5.

  • Target SHA: 838316cb5a83359499422937835a837e5fc86053
  • Head SHA: ed57f8aaed0de785d5143b929fec38d0bfacd117
  • Relationship: 27 commits ahead and 0 commits behind.
  • Lint & Format Check: passed.
  • Run Tests: passed.
  • Typecheck (node): passed.
  • GitGuardian Security Checks: passed.

.github/workflows/lint.yml now runs npm run typecheck:node in the required Typecheck (node) job. The earlier CI typecheck finding is resolved.

Major findings

None.

Minor findings

None.

The prior code findings are resolved in the current head:

  • ShortcutCategory.id and Shortcut.id provide stable persistence identifiers.
  • getKeyboardShortcuts() uses IDs first. It migrates legacy no-ID categories and shortcuts by position. It prevents duplicate migrated categories. See src/renderer/src/utils/localStorage.ts, Lines 283-350.
  • The runtime handler supports Cmd. It excludes editable controls and IME composition. It rejects modifier-only combinations. See src/renderer/src/hooks/useKeyboardShortcuts.tsx, Lines 126-187.
  • toggleFullscreenPlayer and toggleEqualizer reject repeated keydown events. See src/renderer/src/hooks/useKeyboardShortcuts.tsx, Lines 287-294 and Lines 317-320.
  • Both shortcut editors reject modifier-only input. Both exit edit mode after duplicate input and click-away. See src/renderer/src/components/SettingsPage/Settings/KeyboardShortcutsSettings.tsx, Lines 22-96, and src/renderer/src/components/SettingsPage/AppShortcutsPrompt.tsx, Lines 20-104.
  • The equalizer cache hydrates from database frequencyBands through equalizerBandKeys. See src/renderer/src/hooks/usePlaybackSettings.tsx, Lines 114-131.
  • AudioPlayer.toggleEqualizer() reuses the cached preset. It no longer reads the legacy local-storage preset. See src/renderer/src/other/player.ts, Lines 641-666.
  • This audit found no new or changed IPC or preload contract in the target-relative PR changes.

Remaining test coverage gap

No automated test directly covers the new shortcut and equalizer behavior. This is not a confirmed code defect. Follow-up coverage should test:

  • Legacy shortcut migration after a language change.
  • Cmd and Ctrl capture and dispatch.
  • Editable-target and IME exclusion.
  • Duplicate shortcut handling in both editors.
  • Fullscreen and equalizer repeat-key suppression.
  • Equalizer database hydration and Ctrl+E after restart.

Verdict: Ready — high confidence. The branch is mergeable, and the recorded lint, test, node typecheck, and security checks pass.

You are interacting with an AI system.

@Owie6789
Owie6789 changed the base branch from release/4.0.0-alpha.5 to release/4.0.0-alpha.6 August 9, 2026 19:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants