diff --git a/.opencode/skills/textile-design/components.md b/.opencode/skills/textile-design/components.md
index 0278bf1..c6dd3d5 100644
--- a/.opencode/skills/textile-design/components.md
+++ b/.opencode/skills/textile-design/components.md
@@ -31,6 +31,12 @@ Reference implementations live under `src/components/` unless noted.
- `aria-label` describing action; `aria-pressed` if toggle
- SVG decorative: `aria-hidden`
+## Icons
+
+**Reference:** `src/icons/index.tsx`
+
+All shared SVG icons live in `src/icons/index.tsx`. They accept `className` for Tailwind sizing/coloring and use `currentColor` for stroke so they inherit the parent's text color. Import them from `../icons` (or `@/icons` if aliased).
+
## Primary / secondary buttons
**Primary CTA:** `bg-primary text-primary-foreground`, adequate padding (`px-3 py-2`), optional `hover:opacity-90`, `disabled:opacity-50`.
diff --git a/src/App.tsx b/src/App.tsx
index 09279aa..3603006 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -83,7 +83,7 @@ function AppShell() {
const isMac = getRendererPlatform() === 'darwin';
// Core action-system services — created once at app start and passed down.
- const { registry, hotkeyManager } = useMemo(
+ const { registry, hotkeyManager, hotkeyStore } = useMemo(
() => createActionServices(isMac),
[isMac],
);
@@ -171,6 +171,7 @@ function AppShell() {
actionSets={actionSets}
registry={registry}
hotkeyManager={hotkeyManager}
+ hotkeyStore={hotkeyStore}
>
(null);
/**
- * Hook to access the action registry.
+ * Hook to access the action registry and hotkey store.
* Must be used within an ActionProvider.
*/
export function useActions(): ActionProviderValue {
@@ -31,3 +34,11 @@ export function useActions(): ActionProviderValue {
export function useActionRegistry(): ActionRegistry {
return useActions().registry;
}
+
+/**
+ * Hook to access the hotkey store.
+ * Must be used within an ActionProvider.
+ */
+export function useHotkeyStore(): HotkeyStore {
+ return useActions().hotkeyStore;
+}
diff --git a/src/actions/ActionProvider.tsx b/src/actions/ActionProvider.tsx
index 5576f2f..04a6cdb 100644
--- a/src/actions/ActionProvider.tsx
+++ b/src/actions/ActionProvider.tsx
@@ -8,6 +8,7 @@ import {
import type { ActionSet, RegisteredAction } from './types';
import type { ActionRegistry } from './registry';
import type { HotkeyManager } from './hotkey-manager';
+import type { HotkeyStore } from './hotkeyStore';
import { ActionContext } from './ActionContext';
import { CommandPalette } from './CommandPalette';
import { getRendererPlatform } from '../lib/platform';
@@ -20,6 +21,8 @@ interface ActionProviderProps {
registry: ActionRegistry;
/** The hotkey manager (created and owned by AppShell). */
hotkeyManager: HotkeyManager;
+ /** The hotkey store (created and owned by AppShell). */
+ hotkeyStore: HotkeyStore;
}
/**
@@ -34,6 +37,7 @@ export function ActionProvider({
actionSets,
registry,
hotkeyManager,
+ hotkeyStore,
}: ActionProviderProps) {
const isMac = getRendererPlatform() === 'darwin';
@@ -60,27 +64,46 @@ export function ActionProvider({
[actionSets],
);
- // Register/unregister all actions from all sets
- useEffect(() => {
+ /**
+ * Register all actions from all sets and wire their effective hotkeys.
+ */
+ const registerAll = useCallback(() => {
for (const set of allActionSets) {
for (const def of set.getActions()) {
const registered = registry.register(set.id, def);
- if (registered.hotkeys) {
- for (const hotkey of registered.hotkeys) {
- hotkeyManager.registerHotkey(registered.id, hotkey);
- }
+ for (const hotkey of hotkeyStore.getEffectiveHotkeys(registered)) {
+ hotkeyManager.registerHotkey(registered.id, hotkey);
}
}
}
+ }, [allActionSets, registry, hotkeyManager, hotkeyStore]);
- return () => {
- for (const set of allActionSets) {
- registry.unregisterAll(set.id);
- hotkeyManager.removeSourceHotkeys(set.id);
- }
- };
+ /**
+ * Unregister all actions and clear their hotkeys.
+ */
+ const unregisterAll = useCallback(() => {
+ for (const set of allActionSets) {
+ registry.unregisterAll(set.id);
+ hotkeyManager.removeSourceHotkeys(set.id);
+ }
}, [allActionSets, registry, hotkeyManager]);
+ // Register on mount / when action sets change.
+ // Re-register hotkeys when the user edits an override.
+ useEffect(() => {
+ registerAll();
+ return unregisterAll;
+ }, [registerAll, unregisterAll]);
+
+ // Re-register hotkeys immediately when the user edits an override,
+ // without waiting for a React state cycle.
+ useEffect(() => {
+ return hotkeyStore.onChange(() => {
+ unregisterAll();
+ registerAll();
+ });
+ }, [hotkeyStore, registerAll, unregisterAll]);
+
const handlePaletteExecute = useCallback(
(action: RegisteredAction) => {
void registry.execute(action.id);
@@ -93,7 +116,10 @@ export function ActionProvider({
[registry, paletteOpen],
);
- const value = useMemo(() => ({ registry }), [registry]);
+ const value = useMemo(
+ () => ({ registry, hotkeyStore }),
+ [registry, hotkeyStore],
+ );
return (
@@ -103,6 +129,7 @@ export function ActionProvider({
onClose={() => setPaletteOpen(false)}
onExecute={handlePaletteExecute}
actions={availableActions}
+ hotkeyStore={hotkeyStore}
isMac={isMac}
/>
diff --git a/src/actions/CommandPalette.tsx b/src/actions/CommandPalette.tsx
index 861ca48..74e345a 100644
--- a/src/actions/CommandPalette.tsx
+++ b/src/actions/CommandPalette.tsx
@@ -7,6 +7,7 @@ import {
type KeyboardEvent,
} from 'react';
import type { RegisteredAction } from './types';
+import type { HotkeyStore } from './hotkeyStore';
import { formatHotkeyForDisplay } from './hotkey-utils';
interface CommandPaletteProps {
@@ -18,6 +19,8 @@ interface CommandPaletteProps {
onExecute: (action: RegisteredAction) => void;
/** All available actions */
actions: RegisteredAction[];
+ /** Hotkey store for resolving effective hotkeys */
+ hotkeyStore: HotkeyStore;
/** Whether the platform is macOS (affects hotkey display) */
isMac: boolean;
/** Optional placeholder text */
@@ -38,6 +41,7 @@ export function CommandPalette({
onClose,
onExecute,
actions,
+ hotkeyStore,
isMac,
placeholder = 'Type a command...',
}: CommandPaletteProps) {
@@ -177,8 +181,9 @@ export function CommandPalette({
{filteredActions.map((action, index) => {
const isSelected = index === selectedIndex;
- const hotkeyHint = action.hotkeys?.[0]
- ? formatHotkeyForDisplay(action.hotkeys[0], isMac)
+ const effectiveHotkeys = hotkeyStore.getEffectiveHotkeys(action);
+ const hotkeyHint = effectiveHotkeys[0]
+ ? formatHotkeyForDisplay(effectiveHotkeys[0], isMac)
: undefined;
return (
@@ -198,7 +203,7 @@ export function CommandPalette({
>
{action.name}
{hotkeyHint ? (
-
+
{hotkeyHint}
) : null}
diff --git a/src/actions/hotkeyStore.ts b/src/actions/hotkeyStore.ts
new file mode 100644
index 0000000..8ec6df8
--- /dev/null
+++ b/src/actions/hotkeyStore.ts
@@ -0,0 +1,122 @@
+import type { Hotkey, RegisteredAction } from './types';
+import type { ActionRegistry } from './registry';
+import { normalizeHotkey, hotkeyToString } from './hotkey-utils';
+
+const STORAGE_KEY = 'textile.hotkeys.v2';
+
+/**
+ * Persists user hotkey overrides and resolves effective hotkeys.
+ *
+ * Each action has default hotkeys from its ActionSet (an array). Users can
+ * replace the entire array, clear it (unbind), or revert to defaults.
+ * Overrides are stored in localStorage and survive app restarts.
+ */
+export class HotkeyStore {
+ /** Map of action full ID -> override array ([] = explicitly unbound) */
+ private overrides = new Map();
+ private listeners = new Set<() => void>();
+
+ constructor() {
+ this.load();
+ }
+
+ private load(): void {
+ if (typeof localStorage === 'undefined') return;
+ try {
+ const raw = localStorage.getItem(STORAGE_KEY);
+ if (!raw) return;
+ const data = JSON.parse(raw) as Record;
+ for (const [id, hotkeys] of Object.entries(data)) {
+ if (Array.isArray(hotkeys)) {
+ this.overrides.set(id, hotkeys);
+ }
+ }
+ } catch {
+ // Ignore corrupt storage
+ }
+ }
+
+ private save(): void {
+ if (typeof localStorage === 'undefined') return;
+ const data: Record = {};
+ for (const [id, hotkeys] of this.overrides) {
+ data[id] = hotkeys;
+ }
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(data));
+ }
+
+ private notify(): void {
+ for (const listener of this.listeners) {
+ listener();
+ }
+ }
+
+ /**
+ * Get the user override for an action, if any.
+ * Returns `[]` if the user explicitly unbound it.
+ * Returns `undefined` if there is no override.
+ */
+ getOverride(actionId: string): Hotkey[] | undefined {
+ return this.overrides.get(actionId);
+ }
+
+ /**
+ * Set the full override array for an action.
+ * Pass `[]` to explicitly unbind.
+ */
+ setOverride(actionId: string, hotkeys: Hotkey[]): void {
+ this.overrides.set(actionId, hotkeys);
+ this.save();
+ this.notify();
+ }
+
+ /** Remove any override for an action (revert to defaults). */
+ removeOverride(actionId: string): void {
+ this.overrides.delete(actionId);
+ this.save();
+ this.notify();
+ }
+
+ /**
+ * Get all effective hotkeys for an action.
+ * Checks user override first, then falls back to the action's defaults.
+ */
+ getEffectiveHotkeys(action: RegisteredAction): Hotkey[] {
+ const override = this.overrides.get(action.id);
+ if (override !== undefined) return override;
+ return action.hotkeys ?? [];
+ }
+
+ /** Subscribe to hotkey override changes. Returns an unsubscribe function. */
+ onChange(callback: () => void): () => void {
+ this.listeners.add(callback);
+ return () => {
+ this.listeners.delete(callback);
+ };
+ }
+
+ /**
+ * Find hotkey conflicts among all registered actions using effective hotkeys.
+ * Returns a map of normalized hotkey string -> array of conflicting action IDs.
+ */
+ getConflicts(registry: ActionRegistry, isMac: boolean): Map {
+ const bindings = new Map();
+
+ for (const action of registry.getAll()) {
+ for (const hotkey of this.getEffectiveHotkeys(action)) {
+ const key = hotkeyToString(normalizeHotkey(hotkey, isMac));
+ const existing = bindings.get(key) ?? [];
+ existing.push(action.id);
+ bindings.set(key, existing);
+ }
+ }
+
+ const conflicts = new Map();
+ for (const [key, actionIds] of bindings) {
+ if (actionIds.length > 1) {
+ conflicts.set(key, actionIds);
+ }
+ }
+ return conflicts;
+ }
+}
diff --git a/src/actions/index.ts b/src/actions/index.ts
index b192977..32ff345 100644
--- a/src/actions/index.ts
+++ b/src/actions/index.ts
@@ -8,6 +8,7 @@ export type {
export { ActionRegistry } from './registry';
export { HotkeyManager, type HotkeyConflict } from './hotkey-manager';
+export { HotkeyStore } from './hotkeyStore';
export {
normalizeHotkey,
hotkeyToString,
@@ -18,7 +19,12 @@ export {
} from './hotkey-utils';
export { CommandPalette } from './CommandPalette';
export { ActionProvider } from './ActionProvider';
-export { ActionContext, useActions, useActionRegistry } from './ActionContext';
+export {
+ ActionContext,
+ useActions,
+ useActionRegistry,
+ useHotkeyStore,
+} from './ActionContext';
export { createActionServices } from './services';
export { SidebarActionSet } from './sets/sidebarActionSet';
export { SettingsActionSet } from './sets/settingsActionSet';
diff --git a/src/actions/services.ts b/src/actions/services.ts
index d728189..0ea20a6 100644
--- a/src/actions/services.ts
+++ b/src/actions/services.ts
@@ -1,5 +1,6 @@
import { ActionRegistry } from './registry';
import { HotkeyManager } from './hotkey-manager';
+import { HotkeyStore } from './hotkeyStore';
/**
* Create the core action system services.
@@ -11,6 +12,7 @@ import { HotkeyManager } from './hotkey-manager';
*/
export function createActionServices(isMac: boolean) {
const registry = new ActionRegistry();
+ const hotkeyStore = new HotkeyStore();
const hotkeyManager = new HotkeyManager(
registry,
@@ -20,5 +22,5 @@ export function createActionServices(isMac: boolean) {
},
);
- return { registry, hotkeyManager };
+ return { registry, hotkeyManager, hotkeyStore };
}
diff --git a/src/components/HotkeysPane.tsx b/src/components/HotkeysPane.tsx
new file mode 100644
index 0000000..20f72d7
--- /dev/null
+++ b/src/components/HotkeysPane.tsx
@@ -0,0 +1,323 @@
+import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
+import {
+ useActionRegistry,
+ useHotkeyStore,
+} from '../actions/ActionContext';
+import {
+ formatHotkeyForDisplay,
+ eventToHotkey,
+ normalizeHotkey,
+ hotkeyToString,
+} from '../actions/hotkey-utils';
+import { getRendererPlatform } from '../lib/platform';
+import { PlusIcon, XIcon, SearchIcon } from '../icons';
+import type { RegisteredAction, Hotkey } from '../actions/types';
+
+/** Parse editingKey into action ID and slot info. */
+function parseEditingKey(
+ key: string | null,
+): { actionId: string | null; slot: number | 'new' | null } {
+ if (!key) return { actionId: null, slot: null };
+ const colonIdx = key.lastIndexOf(':');
+ if (colonIdx < 0) return { actionId: null, slot: null };
+ const actionId = key.slice(0, colonIdx);
+ const slotRaw = key.slice(colonIdx + 1);
+ const slot = slotRaw === 'new' ? 'new' : parseInt(slotRaw, 10);
+ return { actionId, slot };
+}
+
+export function HotkeysPane() {
+ const registry = useActionRegistry();
+ const hotkeyStore = useHotkeyStore();
+ const isMac = getRendererPlatform() === 'darwin';
+
+ const [editingKey, setEditingKey] = useState(null);
+ const [captured, setCaptured] = useState(null);
+ const [filter, setFilter] = useState('');
+
+ // Refs so the persistent keydown listener always sees latest state
+ const editingKeyRef = useRef(editingKey);
+ editingKeyRef.current = editingKey;
+ const capturedRef = useRef(captured);
+ capturedRef.current = captured;
+
+ // Force re-render when overrides change so badges and conflicts update
+ const [version, setVersion] = useState(0);
+ useEffect(() => {
+ return hotkeyStore.onChange(() => setVersion((v) => v + 1));
+ }, [hotkeyStore]);
+
+ // Reset captured combo whenever the user switches to a different editing target
+ useEffect(() => {
+ setCaptured(null);
+ }, [editingKey]);
+
+ const allActions = useMemo(() => {
+ const actions = registry.getAll();
+ // Sort by source then name for stable ordering
+ actions.sort((a, b) => {
+ if (a.source !== b.source) return a.source.localeCompare(b.source);
+ return a.name.localeCompare(b.name);
+ });
+ return actions;
+ }, [registry]);
+
+ const filteredActions = useMemo(() => {
+ if (!filter.trim()) return allActions;
+ const q = filter.toLowerCase();
+ return allActions.filter(
+ (a) =>
+ a.name.toLowerCase().includes(q) ||
+ (a.description ?? '').toLowerCase().includes(q),
+ );
+ }, [allActions, filter]);
+
+ /** Whether the currently captured hotkey conflicts with another action's binding. */
+ const isCapturedConflicting = useMemo(() => {
+ if (!captured || !editingKey) return false;
+ const { actionId: editingActionId } = parseEditingKey(editingKey);
+ const capturedStr = hotkeyToString(normalizeHotkey(captured, isMac));
+ for (const action of allActions) {
+ if (action.id === editingActionId) continue; // same-action duplicates are handled separately
+ for (const hk of hotkeyStore.getEffectiveHotkeys(action)) {
+ if (hotkeyToString(normalizeHotkey(hk, isMac)) === capturedStr) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }, [captured, editingKey, allActions, hotkeyStore, isMac, version]);
+
+ /** Live conflicts including the currently captured (but not yet confirmed) hotkey. */
+ const liveConflicts = useMemo(() => {
+ const base = hotkeyStore.getConflicts(registry, isMac);
+ if (!captured || !editingKey) return base;
+
+ const { actionId: editingActionId } = parseEditingKey(editingKey);
+ const capturedStr = hotkeyToString(normalizeHotkey(captured, isMac));
+ const result = new Map(base);
+
+ for (const action of allActions) {
+ if (action.id === editingActionId) continue;
+ for (const hk of hotkeyStore.getEffectiveHotkeys(action)) {
+ const hkStr = hotkeyToString(normalizeHotkey(hk, isMac));
+ if (hkStr === capturedStr) {
+ const existing = result.get(capturedStr) ?? [];
+ if (!existing.includes(action.id)) {
+ result.set(capturedStr, [...existing, action.id]);
+ }
+ }
+ }
+ }
+ return result;
+ }, [hotkeyStore, registry, isMac, captured, editingKey, allActions, version]);
+
+ const finishEdit = useCallback(
+ (actionId: string, nextHotkeys: Hotkey[]) => {
+ hotkeyStore.setOverride(actionId, nextHotkeys);
+ setEditingKey(null);
+ setCaptured(null);
+ },
+ [hotkeyStore],
+ );
+
+ // One persistent document-level keydown listener.
+ // Uses refs to avoid effect re-runs on every state change.
+ useEffect(() => {
+ const handleKeyDown = (e: KeyboardEvent) => {
+ const currentEditing = editingKeyRef.current;
+ if (!currentEditing) return;
+
+ e.preventDefault();
+ e.stopPropagation();
+
+ const { actionId, slot } = parseEditingKey(currentEditing);
+ if (!actionId || slot === null) return;
+
+ const currentCaptured = capturedRef.current;
+ const action = registry.get(actionId);
+ if (!action) return;
+
+ const base = hotkeyStore.getEffectiveHotkeys(action);
+
+ if (e.key === 'Escape') {
+ setEditingKey(null);
+ setCaptured(null);
+ return;
+ }
+
+ if (e.key === 'Enter') {
+ const next = [...base];
+ if (currentCaptured) {
+ if (slot === 'new') {
+ next.push(currentCaptured);
+ } else if (typeof slot === 'number') {
+ next[slot] = currentCaptured;
+ }
+ }
+ // Deduplicate within this action
+ const seen = new Set();
+ const deduped = next.filter((hk) => {
+ const key = hotkeyToString(normalizeHotkey(hk, isMac));
+ if (seen.has(key)) return false;
+ seen.add(key);
+ return true;
+ });
+ finishEdit(actionId, deduped);
+ return;
+ }
+
+ if (e.key === 'Delete' || e.key === 'Backspace') {
+ const next = [...base];
+ if (typeof slot === 'number') {
+ next.splice(slot, 1);
+ }
+ finishEdit(actionId, next);
+ return;
+ }
+
+ // Skip bare modifier keys
+ if (['Control', 'Shift', 'Alt', 'Meta'].includes(e.key)) return;
+
+ setCaptured(eventToHotkey(e));
+ };
+
+ document.addEventListener('keydown', handleKeyDown, true);
+ return () => document.removeEventListener('keydown', handleKeyDown, true);
+ }, [registry, hotkeyStore, finishEdit]);
+
+ const removeAt = (action: RegisteredAction, index: number) => {
+ const next = [...hotkeyStore.getEffectiveHotkeys(action)];
+ next.splice(index, 1);
+ hotkeyStore.setOverride(action.id, next);
+ };
+
+ return (
+
+ {/* Conflict banner */}
+ {liveConflicts.size > 0 ? (
+
+
Conflicting hotkeys
+
+ {Array.from(liveConflicts.entries()).map(([key, actionIds]) => (
+ -
+ {key}: {actionIds.join(', ')}
+
+ ))}
+
+
+ ) : null}
+
+ {/* Filter bar */}
+
+
+ setFilter(e.target.value)}
+ placeholder="Filter by name or description…"
+ className="border-border bg-foreground/5 text-foreground placeholder:text-foreground/30 w-full rounded-md border py-1.5 pl-8 pr-3 text-sm outline-none focus:border-accent focus:ring-1 focus:ring-accent"
+ />
+
+
+ {/* Table — scrollable body */}
+
+ {filteredActions.map((action: RegisteredAction) => {
+ const effective = hotkeyStore.getEffectiveHotkeys(action);
+ const { actionId: editingActionId, slot } = parseEditingKey(
+ editingKey,
+ );
+ const isEditingAction = editingActionId === action.id;
+
+ return (
+
+
+
+ {action.name}
+
+ {action.description ? (
+
+ {action.description}
+
+ ) : null}
+
+
+
+ {effective.map((hk, index) => {
+ const isEditing = isEditingAction && slot === index;
+ return (
+
+ {isEditing ? (
+
+ {captured
+ ? formatHotkeyForDisplay(captured, isMac)
+ : 'Press keys…'}
+
+ ) : (
+ <>
+
+
+ >
+ )}
+
+ );
+ })}
+
+
+ {isEditingAction && slot === 'new' ? (
+
+ {captured
+ ? formatHotkeyForDisplay(captured, isMac)
+ : 'Press keys…'}
+
+ ) : null}
+
+
+
+
+ );
+ })}
+ {filteredActions.length === 0 ? (
+
+ No commands match your filter
+
+ ) : null}
+
+
+ );
+}
diff --git a/src/components/SettingsModal.tsx b/src/components/SettingsModal.tsx
index fe991c3..2109057 100644
--- a/src/components/SettingsModal.tsx
+++ b/src/components/SettingsModal.tsx
@@ -7,6 +7,7 @@ import {
} from '../workspaces/workspace';
import { Modal } from './Modal';
import { SettingsPaneLayout, SettingsPaneSection } from './SettingsPane';
+import { HotkeysPane } from './HotkeysPane';
type SettingsModalProps = {
open: boolean;
@@ -17,7 +18,7 @@ type SettingsModalProps = {
onWorkspaceKindChange: (kind: WorkspaceKind) => void;
};
-type SettingsCategory = 'atmosphere' | 'appearance' | 'subtext';
+type SettingsCategory = 'atmosphere' | 'appearance' | 'hotkeys' | 'subtext';
function AtmosphereAccountPane() {
const { authInfo, login, logout } = useHabitatAuth();
@@ -171,6 +172,24 @@ function MoonIcon() {
+function KeyboardIcon() {
+ return (
+
+ );
+}
+
function SubtextSettingsPane() {
const [enabled, setEnabled] = useState(() => {
if (typeof localStorage === 'undefined') return false;
@@ -244,6 +263,11 @@ export function SettingsModal({
label: 'Appearance',
icon: ,
},
+ {
+ id: 'hotkeys',
+ label: 'Hotkeys',
+ icon: ,
+ },
{
id: 'subtext',
label: 'Subtext',
@@ -282,6 +306,8 @@ export function SettingsModal({
>
{selectedCategory === 'atmosphere' ? (
+ ) : selectedCategory === 'hotkeys' ? (
+
) : selectedCategory === 'subtext' ? (
) : (
diff --git a/src/components/SettingsPane.tsx b/src/components/SettingsPane.tsx
index da1d219..f7aedfe 100644
--- a/src/components/SettingsPane.tsx
+++ b/src/components/SettingsPane.tsx
@@ -19,7 +19,7 @@ export function SettingsPaneLayout({
-
+
);
}
diff --git a/src/icons/index.tsx b/src/icons/index.tsx
index c1f54b1..56a609b 100644
--- a/src/icons/index.tsx
+++ b/src/icons/index.tsx
@@ -67,3 +67,58 @@ export function SubtextIcon({ className }: IconProps) {
);
}
+
+/** Plus icon for "add" actions. */
+export function PlusIcon({ className }: IconProps) {
+ return (
+
+ );
+}
+
+/** X icon for "remove" / "close" actions. */
+export function XIcon({ className }: IconProps) {
+ return (
+
+ );
+}
+
+/** Search / magnifying-glass icon. */
+export function SearchIcon({ className }: IconProps) {
+ return (
+
+ );
+}
diff --git a/src/main.ts b/src/main.ts
index c1deb2d..b52e6e5 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -93,7 +93,8 @@ function installApplicationMenu() {
submenu: [
{
label: 'Settings…',
- accelerator: 'CommandOrControl+,',
+ // No accelerator here — hotkeys are managed by the renderer's
+ // ActionRegistry + HotkeyManager so users can customize bindings.
click: () => {
const target =
BrowserWindow.getFocusedWindow() ??