mirror of
https://github.com/davidkaya/aryx.git
synced 2026-08-08 04:38:45 +02:00
feat: add rich keyboard shortcuts with cheat sheet overlay
Comprehensive keybindings for power-user navigation: - Ctrl+N: new session, Ctrl+W: archive session - Ctrl+Tab / Ctrl+Shift+Tab: cycle between sessions - Ctrl+,: open settings, Ctrl+/: shortcuts cheat sheet - Ctrl+.: quick-approve pending tool call - Ctrl+L: focus composer, Escape: cancel turn / close overlay Centralized shortcut registry in lib/keyboardShortcuts.ts drives both the cheat sheet panel and command palette shortcut badges. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
+149
-3
@@ -1,10 +1,11 @@
|
|||||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
|
|
||||||
import { AppShell } from '@renderer/components/AppShell';
|
import { AppShell } from '@renderer/components/AppShell';
|
||||||
import { ActivityPanel } from '@renderer/components/ActivityPanel';
|
import { ActivityPanel } from '@renderer/components/ActivityPanel';
|
||||||
import { ChatPane } from '@renderer/components/ChatPane';
|
import { ChatPane } from '@renderer/components/ChatPane';
|
||||||
import { CommandPalette } from '@renderer/components/CommandPalette';
|
import { CommandPalette } from '@renderer/components/CommandPalette';
|
||||||
import { DiscoveredToolingModal } from '@renderer/components/DiscoveredToolingModal';
|
import { DiscoveredToolingModal } from '@renderer/components/DiscoveredToolingModal';
|
||||||
|
import { KeyboardShortcutsPanel } from '@renderer/components/KeyboardShortcutsPanel';
|
||||||
import { NewSessionModal } from '@renderer/components/NewSessionModal';
|
import { NewSessionModal } from '@renderer/components/NewSessionModal';
|
||||||
import { ProjectSettingsPanel } from '@renderer/components/ProjectSettingsPanel';
|
import { ProjectSettingsPanel } from '@renderer/components/ProjectSettingsPanel';
|
||||||
import { SettingsPanel } from '@renderer/components/SettingsPanel';
|
import { SettingsPanel } from '@renderer/components/SettingsPanel';
|
||||||
@@ -114,6 +115,7 @@ export default function App() {
|
|||||||
const [newSessionProjectId, setNewSessionProjectId] = useState<string>();
|
const [newSessionProjectId, setNewSessionProjectId] = useState<string>();
|
||||||
const [showDiscoveryModal, setShowDiscoveryModal] = useState(false);
|
const [showDiscoveryModal, setShowDiscoveryModal] = useState(false);
|
||||||
const [commandPaletteOpen, setCommandPaletteOpen] = useState(false);
|
const [commandPaletteOpen, setCommandPaletteOpen] = useState(false);
|
||||||
|
const [showShortcuts, setShowShortcuts] = useState(false);
|
||||||
|
|
||||||
// Terminal state
|
// Terminal state
|
||||||
const [terminalOpen, setTerminalOpen] = useState(false);
|
const [terminalOpen, setTerminalOpen] = useState(false);
|
||||||
@@ -266,18 +268,157 @@ export default function App() {
|
|||||||
if (hasPendingDiscoveries) setShowDiscoveryModal(true);
|
if (hasPendingDiscoveries) setShowDiscoveryModal(true);
|
||||||
}, [hasPendingDiscoveries]);
|
}, [hasPendingDiscoveries]);
|
||||||
|
|
||||||
// Terminal: Ctrl+` toggle + Command palette: Ctrl+K / Cmd+K
|
// Keep refs for values the keyboard handler reads — avoids re-registering on every render.
|
||||||
|
const workspaceRef = useRef(workspace);
|
||||||
|
workspaceRef.current = workspace;
|
||||||
|
const showSettingsRef = useRef(showSettings);
|
||||||
|
showSettingsRef.current = showSettings;
|
||||||
|
const showShortcutsRef = useRef(showShortcuts);
|
||||||
|
showShortcutsRef.current = showShortcuts;
|
||||||
|
const commandPaletteOpenRef = useRef(commandPaletteOpen);
|
||||||
|
commandPaletteOpenRef.current = commandPaletteOpen;
|
||||||
|
const projectSettingsIdRef = useRef(projectSettingsId);
|
||||||
|
projectSettingsIdRef.current = projectSettingsId;
|
||||||
|
const newSessionProjectIdRef = useRef(newSessionProjectId);
|
||||||
|
newSessionProjectIdRef.current = newSessionProjectId;
|
||||||
|
|
||||||
|
// ── Global keyboard shortcuts ──
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleKeyDown = (e: KeyboardEvent) => {
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
|
const mod = e.ctrlKey || e.metaKey;
|
||||||
|
const ws = workspaceRef.current;
|
||||||
|
|
||||||
|
// Ignore keyboard shortcuts while typing in inputs (except our global combos)
|
||||||
|
const target = e.target as HTMLElement;
|
||||||
|
const isInput = target.tagName === 'INPUT'
|
||||||
|
|| target.tagName === 'TEXTAREA'
|
||||||
|
|| target.isContentEditable;
|
||||||
|
|
||||||
|
// ── Ctrl+` — Toggle terminal ──
|
||||||
if (e.ctrlKey && e.key === '`') {
|
if (e.ctrlKey && e.key === '`') {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setTerminalOpen((prev) => !prev);
|
setTerminalOpen((prev) => !prev);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
if ((e.ctrlKey || e.metaKey) && e.key === 'k') {
|
|
||||||
|
// ── Ctrl/Cmd+K — Command palette ──
|
||||||
|
if (mod && e.key === 'k') {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setCommandPaletteOpen((prev) => !prev);
|
setCommandPaletteOpen((prev) => !prev);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Ctrl/Cmd+/ — Keyboard shortcuts cheat sheet ──
|
||||||
|
if (mod && e.key === '/') {
|
||||||
|
e.preventDefault();
|
||||||
|
setShowShortcuts((prev) => !prev);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Ctrl/Cmd+, — Open settings ──
|
||||||
|
if (mod && e.key === ',') {
|
||||||
|
e.preventDefault();
|
||||||
|
setShowSettings(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Escape — Close overlays or cancel running turn ──
|
||||||
|
if (e.key === 'Escape') {
|
||||||
|
// Close overlays in priority order (command palette and shortcuts use their own capture listeners)
|
||||||
|
if (projectSettingsIdRef.current) {
|
||||||
|
e.preventDefault();
|
||||||
|
setProjectSettingsId(undefined);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (showSettingsRef.current) {
|
||||||
|
e.preventDefault();
|
||||||
|
setShowSettings(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (newSessionProjectIdRef.current) {
|
||||||
|
e.preventDefault();
|
||||||
|
setNewSessionProjectId(undefined);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If nothing is open, cancel a running turn on the selected session
|
||||||
|
if (ws) {
|
||||||
|
const session = ws.sessions.find((s) => s.id === ws.selectedSessionId);
|
||||||
|
if (session?.status === 'running' && !isInput) {
|
||||||
|
e.preventDefault();
|
||||||
|
void api.cancelSessionTurn({ sessionId: session.id });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip remaining shortcuts when focus is in an input field
|
||||||
|
if (isInput) return;
|
||||||
|
|
||||||
|
// ── Ctrl/Cmd+N — New session ──
|
||||||
|
if (mod && e.key === 'n') {
|
||||||
|
e.preventDefault();
|
||||||
|
if (ws) {
|
||||||
|
const defaultProjectId =
|
||||||
|
ws.selectedProjectId ??
|
||||||
|
ws.projects.find((p) => !isScratchpadProject(p))?.id;
|
||||||
|
if (defaultProjectId) {
|
||||||
|
setNewSessionProjectId(defaultProjectId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Ctrl/Cmd+W — Archive / close current session ──
|
||||||
|
if (mod && e.key === 'w') {
|
||||||
|
e.preventDefault();
|
||||||
|
if (ws?.selectedSessionId) {
|
||||||
|
void api.setSessionArchived({ sessionId: ws.selectedSessionId, isArchived: true });
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Ctrl+Tab / Ctrl+Shift+Tab — Cycle sessions ──
|
||||||
|
if (e.ctrlKey && e.key === 'Tab') {
|
||||||
|
e.preventDefault();
|
||||||
|
if (ws) {
|
||||||
|
const activeSessions = ws.sessions.filter((s) => !s.isArchived);
|
||||||
|
if (activeSessions.length > 1) {
|
||||||
|
const currentIdx = activeSessions.findIndex((s) => s.id === ws.selectedSessionId);
|
||||||
|
const direction = e.shiftKey ? -1 : 1;
|
||||||
|
const nextIdx = (currentIdx + direction + activeSessions.length) % activeSessions.length;
|
||||||
|
void api.selectSession(activeSessions[nextIdx].id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Ctrl/Cmd+. — Quick approve pending tool call ──
|
||||||
|
if (mod && e.key === '.') {
|
||||||
|
e.preventDefault();
|
||||||
|
if (ws?.selectedSessionId) {
|
||||||
|
const session = ws.sessions.find((s) => s.id === ws.selectedSessionId);
|
||||||
|
if (session?.pendingApproval?.status === 'pending') {
|
||||||
|
void api.resolveSessionApproval({
|
||||||
|
sessionId: session.id,
|
||||||
|
approvalId: session.pendingApproval.id,
|
||||||
|
decision: 'approved',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Ctrl/Cmd+L — Focus the composer ──
|
||||||
|
if (mod && e.key === 'l') {
|
||||||
|
e.preventDefault();
|
||||||
|
const editor = document.querySelector<HTMLElement>('.markdown-composer-editable');
|
||||||
|
editor?.focus();
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
window.addEventListener('keydown', handleKeyDown);
|
window.addEventListener('keydown', handleKeyDown);
|
||||||
|
|
||||||
// Track terminal running state via exit events
|
// Track terminal running state via exit events
|
||||||
@@ -645,8 +786,13 @@ export default function App() {
|
|||||||
}}
|
}}
|
||||||
onAddProject={() => void api.addProject()}
|
onAddProject={() => void api.addProject()}
|
||||||
onOpenAppDataFolder={() => void api.openAppDataFolder()}
|
onOpenAppDataFolder={() => void api.openAppDataFolder()}
|
||||||
|
onShowShortcuts={() => setShowShortcuts(true)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{showShortcuts && (
|
||||||
|
<KeyboardShortcutsPanel onClose={() => setShowShortcuts(false)} />
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
Copy,
|
Copy,
|
||||||
FolderOpen,
|
FolderOpen,
|
||||||
FolderPlus,
|
FolderPlus,
|
||||||
|
Keyboard,
|
||||||
MessageSquare,
|
MessageSquare,
|
||||||
Monitor,
|
Monitor,
|
||||||
Moon,
|
Moon,
|
||||||
@@ -20,6 +21,7 @@ import {
|
|||||||
import type { AppearanceTheme } from '@shared/domain/tooling';
|
import type { AppearanceTheme } from '@shared/domain/tooling';
|
||||||
import { isScratchpadProject } from '@shared/domain/project';
|
import { isScratchpadProject } from '@shared/domain/project';
|
||||||
import type { WorkspaceState } from '@shared/domain/workspace';
|
import type { WorkspaceState } from '@shared/domain/workspace';
|
||||||
|
import { shortcutKeys } from '@renderer/lib/keyboardShortcuts';
|
||||||
|
|
||||||
interface PaletteCommand {
|
interface PaletteCommand {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -47,6 +49,7 @@ export interface CommandPaletteProps {
|
|||||||
onArchiveSession: (sessionId: string, isArchived: boolean) => void;
|
onArchiveSession: (sessionId: string, isArchived: boolean) => void;
|
||||||
onAddProject: () => void;
|
onAddProject: () => void;
|
||||||
onOpenAppDataFolder: () => void;
|
onOpenAppDataFolder: () => void;
|
||||||
|
onShowShortcuts: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Score how well `query` matches `text` (and optional `keywords`). 0 = no match. */
|
/** Score how well `query` matches `text` (and optional `keywords`). 0 = no match. */
|
||||||
@@ -86,6 +89,7 @@ export function CommandPalette({
|
|||||||
onArchiveSession,
|
onArchiveSession,
|
||||||
onAddProject,
|
onAddProject,
|
||||||
onOpenAppDataFolder,
|
onOpenAppDataFolder,
|
||||||
|
onShowShortcuts,
|
||||||
}: CommandPaletteProps) {
|
}: CommandPaletteProps) {
|
||||||
const [query, setQuery] = useState('');
|
const [query, setQuery] = useState('');
|
||||||
const [selectedIndex, setSelectedIndex] = useState(0);
|
const [selectedIndex, setSelectedIndex] = useState(0);
|
||||||
@@ -151,6 +155,7 @@ export function CommandPalette({
|
|||||||
label: 'New Session',
|
label: 'New Session',
|
||||||
category: 'Actions',
|
category: 'Actions',
|
||||||
keywords: 'create start',
|
keywords: 'create start',
|
||||||
|
shortcut: shortcutKeys('new-session'),
|
||||||
icon: <Plus className={ICON} />,
|
icon: <Plus className={ICON} />,
|
||||||
action: () => onNewSession(defaultProjectId),
|
action: () => onNewSession(defaultProjectId),
|
||||||
});
|
});
|
||||||
@@ -187,7 +192,8 @@ export function CommandPalette({
|
|||||||
id: 'archive-session',
|
id: 'archive-session',
|
||||||
label: 'Archive Session',
|
label: 'Archive Session',
|
||||||
category: 'Session',
|
category: 'Session',
|
||||||
keywords: 'archive hide remove',
|
keywords: 'archive hide remove close',
|
||||||
|
shortcut: shortcutKeys('close-session'),
|
||||||
icon: <Archive className={ICON} />,
|
icon: <Archive className={ICON} />,
|
||||||
action: () => onArchiveSession(selectedSession.id, true),
|
action: () => onArchiveSession(selectedSession.id, true),
|
||||||
});
|
});
|
||||||
@@ -221,6 +227,7 @@ export function CommandPalette({
|
|||||||
label: 'Open Settings',
|
label: 'Open Settings',
|
||||||
category: 'General',
|
category: 'General',
|
||||||
keywords: 'preferences config options',
|
keywords: 'preferences config options',
|
||||||
|
shortcut: shortcutKeys('settings'),
|
||||||
icon: <Settings className={ICON} />,
|
icon: <Settings className={ICON} />,
|
||||||
action: onOpenSettings,
|
action: onOpenSettings,
|
||||||
});
|
});
|
||||||
@@ -241,7 +248,7 @@ export function CommandPalette({
|
|||||||
label: 'Toggle Terminal',
|
label: 'Toggle Terminal',
|
||||||
category: 'General',
|
category: 'General',
|
||||||
keywords: 'terminal console shell command',
|
keywords: 'terminal console shell command',
|
||||||
shortcut: 'Ctrl+`',
|
shortcut: shortcutKeys('toggle-terminal'),
|
||||||
icon: <Terminal className={ICON} />,
|
icon: <Terminal className={ICON} />,
|
||||||
action: onToggleTerminal,
|
action: onToggleTerminal,
|
||||||
});
|
});
|
||||||
@@ -255,6 +262,16 @@ export function CommandPalette({
|
|||||||
action: onOpenAppDataFolder,
|
action: onOpenAppDataFolder,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
cmds.push({
|
||||||
|
id: 'keyboard-shortcuts',
|
||||||
|
label: 'Keyboard Shortcuts',
|
||||||
|
category: 'General',
|
||||||
|
keywords: 'keys keybindings hotkeys help cheatsheet',
|
||||||
|
shortcut: shortcutKeys('shortcut-help'),
|
||||||
|
icon: <Keyboard className={ICON} />,
|
||||||
|
action: onShowShortcuts,
|
||||||
|
});
|
||||||
|
|
||||||
// ── Theme ──
|
// ── Theme ──
|
||||||
cmds.push({
|
cmds.push({
|
||||||
id: 'theme-dark',
|
id: 'theme-dark',
|
||||||
@@ -287,7 +304,7 @@ export function CommandPalette({
|
|||||||
onSelectSession, onSelectProject, onNewSession, onCreateScratchpad,
|
onSelectSession, onSelectProject, onNewSession, onCreateScratchpad,
|
||||||
onOpenSettings, onOpenProjectSettings, onToggleTerminal, onSetTheme,
|
onOpenSettings, onOpenProjectSettings, onToggleTerminal, onSetTheme,
|
||||||
onDuplicateSession, onPinSession, onArchiveSession, onAddProject,
|
onDuplicateSession, onPinSession, onArchiveSession, onAddProject,
|
||||||
onOpenAppDataFolder,
|
onOpenAppDataFolder, onShowShortcuts,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const filteredCommands = useMemo(() => {
|
const filteredCommands = useMemo(() => {
|
||||||
|
|||||||
@@ -0,0 +1,120 @@
|
|||||||
|
import { useEffect } from 'react';
|
||||||
|
import { Keyboard } from 'lucide-react';
|
||||||
|
|
||||||
|
import { shortcuts, type ShortcutDefinition } from '@renderer/lib/keyboardShortcuts';
|
||||||
|
|
||||||
|
interface KeyboardShortcutsPanelProps {
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const categoryOrder = ['Navigation', 'Sessions', 'Workspace', 'General'] as const;
|
||||||
|
|
||||||
|
function groupByCategory(defs: ShortcutDefinition[]): Map<string, ShortcutDefinition[]> {
|
||||||
|
const groups = new Map<string, ShortcutDefinition[]>();
|
||||||
|
for (const def of defs) {
|
||||||
|
const list = groups.get(def.category) ?? [];
|
||||||
|
list.push(def);
|
||||||
|
groups.set(def.category, list);
|
||||||
|
}
|
||||||
|
return groups;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function KeyboardShortcutsPanel({ onClose }: KeyboardShortcutsPanelProps) {
|
||||||
|
// Escape to close — capture phase so it doesn't propagate
|
||||||
|
useEffect(() => {
|
||||||
|
const handleEscape = (e: KeyboardEvent) => {
|
||||||
|
if (e.key === 'Escape') {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopImmediatePropagation();
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
document.addEventListener('keydown', handleEscape, true);
|
||||||
|
return () => document.removeEventListener('keydown', handleEscape, true);
|
||||||
|
}, [onClose]);
|
||||||
|
|
||||||
|
const grouped = groupByCategory(shortcuts);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="palette-backdrop-enter fixed inset-0 z-[70] flex items-center justify-center bg-[#07080e]/80 backdrop-blur-sm"
|
||||||
|
onClick={onClose}
|
||||||
|
role="dialog"
|
||||||
|
aria-modal="true"
|
||||||
|
aria-labelledby="shortcuts-title"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="palette-enter w-full max-w-lg overflow-hidden rounded-xl border border-[var(--color-border)] bg-[var(--color-surface-1)] shadow-[0_24px_80px_rgba(0,0,0,0.55)]"
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
{/* Header */}
|
||||||
|
<div className="flex items-center gap-3 border-b border-[var(--color-border)] px-5 py-3.5">
|
||||||
|
<Keyboard className="size-4 text-[var(--color-text-accent)]" />
|
||||||
|
<h2
|
||||||
|
id="shortcuts-title"
|
||||||
|
className="font-display text-[14px] font-semibold text-[var(--color-text-primary)]"
|
||||||
|
>
|
||||||
|
Keyboard Shortcuts
|
||||||
|
</h2>
|
||||||
|
<span className="ml-auto text-[11px] text-[var(--color-text-muted)]">
|
||||||
|
Press <Kbd>Esc</Kbd> to close
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Body — two-column grid of categories */}
|
||||||
|
<div className="grid grid-cols-2 gap-x-6 gap-y-5 px-5 py-4">
|
||||||
|
{categoryOrder.map((cat) => {
|
||||||
|
const items = grouped.get(cat);
|
||||||
|
if (!items?.length) return null;
|
||||||
|
return (
|
||||||
|
<div key={cat}>
|
||||||
|
<h3 className="mb-2 text-[11px] font-medium uppercase tracking-wider text-[var(--color-text-muted)]">
|
||||||
|
{cat}
|
||||||
|
</h3>
|
||||||
|
<ul className="space-y-1.5">
|
||||||
|
{items.map((item) => (
|
||||||
|
<li
|
||||||
|
key={item.id}
|
||||||
|
className="flex items-center justify-between gap-3 text-[12.5px]"
|
||||||
|
>
|
||||||
|
<span className="truncate text-[var(--color-text-secondary)]">
|
||||||
|
{item.label}
|
||||||
|
</span>
|
||||||
|
<ShortcutBadge keys={item.keys} />
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Footer */}
|
||||||
|
<div className="border-t border-[var(--color-border)] px-5 py-2.5 text-[11px] text-[var(--color-text-muted)]">
|
||||||
|
Tip: Use the command palette for even more actions
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Inline keyboard key cap. */
|
||||||
|
function Kbd({ children }: { children: React.ReactNode }) {
|
||||||
|
return (
|
||||||
|
<kbd className="rounded border border-[var(--color-border)] bg-[var(--color-surface-0)] px-1.5 py-0.5 font-mono text-[10px] text-[var(--color-text-muted)]">
|
||||||
|
{children}
|
||||||
|
</kbd>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Renders a compound shortcut like "Ctrl+Shift+Tab" as joined key caps. */
|
||||||
|
function ShortcutBadge({ keys }: { keys: string }) {
|
||||||
|
const parts = keys.split('+');
|
||||||
|
return (
|
||||||
|
<span className="flex shrink-0 items-center gap-0.5">
|
||||||
|
{parts.map((part, i) => (
|
||||||
|
<Kbd key={i}>{part}</Kbd>
|
||||||
|
))}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
const isMac = navigator.platform.startsWith('Mac');
|
||||||
|
|
||||||
|
/** Platform-aware modifier key label. */
|
||||||
|
export const MOD = isMac ? '⌘' : 'Ctrl';
|
||||||
|
|
||||||
|
export interface ShortcutDefinition {
|
||||||
|
id: string;
|
||||||
|
/** Human-readable label for the shortcut cheat sheet. */
|
||||||
|
label: string;
|
||||||
|
/** Display string for the keybinding. */
|
||||||
|
keys: string;
|
||||||
|
/** Grouping category. */
|
||||||
|
category: 'Navigation' | 'Sessions' | 'Workspace' | 'General';
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Canonical shortcut registry used for the cheat sheet and command palette badges. */
|
||||||
|
export const shortcuts: ShortcutDefinition[] = [
|
||||||
|
// ── Navigation ──
|
||||||
|
{ id: 'command-palette', label: 'Command palette', keys: `${MOD}+K`, category: 'Navigation' },
|
||||||
|
{ id: 'settings', label: 'Open settings', keys: `${MOD}+,`, category: 'Navigation' },
|
||||||
|
{ id: 'toggle-terminal', label: 'Toggle terminal', keys: 'Ctrl+`', category: 'Navigation' },
|
||||||
|
{ id: 'shortcut-help', label: 'Keyboard shortcuts', keys: `${MOD}+/`, category: 'Navigation' },
|
||||||
|
|
||||||
|
// ── Sessions ──
|
||||||
|
{ id: 'new-session', label: 'New session', keys: `${MOD}+N`, category: 'Sessions' },
|
||||||
|
{ id: 'close-session', label: 'Close / archive', keys: `${MOD}+W`, category: 'Sessions' },
|
||||||
|
{ id: 'next-session', label: 'Next session', keys: 'Ctrl+Tab', category: 'Sessions' },
|
||||||
|
{ id: 'prev-session', label: 'Previous session', keys: 'Ctrl+Shift+Tab', category: 'Sessions' },
|
||||||
|
|
||||||
|
// ── Workspace ──
|
||||||
|
{ id: 'quick-approve', label: 'Quick approve', keys: `${MOD}+.`, category: 'Workspace' },
|
||||||
|
{ id: 'cancel-turn', label: 'Cancel running turn', keys: 'Escape', category: 'Workspace' },
|
||||||
|
{ id: 'focus-composer', label: 'Focus composer', keys: `${MOD}+L`, category: 'Workspace' },
|
||||||
|
|
||||||
|
// ── General ──
|
||||||
|
{ id: 'close-overlay', label: 'Close overlay', keys: 'Escape', category: 'General' },
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Look up the display-string for a shortcut by its id.
|
||||||
|
* Returns `undefined` if not found.
|
||||||
|
*/
|
||||||
|
export function shortcutKeys(id: string): string | undefined {
|
||||||
|
return shortcuts.find((s) => s.id === id)?.keys;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user