mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-08-25 05:52:22 +02:00
[aidan] ui/ux: removed keyboard shortcuts
This commit is contained in:
@@ -78,11 +78,9 @@ if (typeof window !== 'undefined') {
|
||||
}
|
||||
import { report, getSessionTraceState, getRecentActions } from '@/shared/serviceClient';
|
||||
import { useRouteTracker } from '@/shared/hooks/useRouteTracker';
|
||||
import { useKeyboardShortcuts } from '@/shared/hooks/useKeyboardShortcuts';
|
||||
import { useDeepLink } from '@/shared/hooks/useDeepLink';
|
||||
import { useWindowFocus } from '@/shared/hooks/useWindowFocus';
|
||||
import { useInteractionHeartbeat } from '@/shared/hooks/useInteractionHeartbeat';
|
||||
import KeyboardShortcutsHelp from './components/overlays/KeyboardShortcutsHelp';
|
||||
import { ThemeProvider, useThemeMode, useClaudeTokens } from '@/shared/styles/ThemeContext';
|
||||
import { ClaudeTokens } from '@/shared/styles/claudeTokens';
|
||||
|
||||
@@ -207,11 +205,6 @@ function buildMuiTheme(c: ClaudeTokens, mode: 'light' | 'dark') {
|
||||
});
|
||||
}
|
||||
|
||||
const ShortcutsProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||
useKeyboardShortcuts();
|
||||
return <>{children}<KeyboardShortcutsHelp /></>;
|
||||
};
|
||||
|
||||
const DeepLinkListener: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||
useDeepLink();
|
||||
useWindowFocus();
|
||||
@@ -469,8 +462,7 @@ const ThemedApp: React.FC = () => {
|
||||
<CssBaseline />
|
||||
<HashRouter>
|
||||
<RouteTrackerMount />
|
||||
<ShortcutsProvider>
|
||||
<SettingsLoader>
|
||||
<SettingsLoader>
|
||||
<DefaultModelGuard>
|
||||
<UpdateListener>
|
||||
<CrashRecoveryChip />
|
||||
@@ -502,7 +494,6 @@ const ThemedApp: React.FC = () => {
|
||||
</UpdateListener>
|
||||
</DefaultModelGuard>
|
||||
</SettingsLoader>
|
||||
</ShortcutsProvider>
|
||||
</HashRouter>
|
||||
</MuiThemeProvider>
|
||||
);
|
||||
|
||||
@@ -1,95 +0,0 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import Dialog from '@mui/material/Dialog';
|
||||
import DialogTitle from '@mui/material/DialogTitle';
|
||||
import DialogContent from '@mui/material/DialogContent';
|
||||
import Box from '@mui/material/Box';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import { useClaudeTokens } from '@/shared/styles/ThemeContext';
|
||||
|
||||
const shortcuts = [
|
||||
{ key: 'd', description: 'Go to Dashboard' },
|
||||
{ key: '1-9', description: 'Open agent by position' },
|
||||
{ key: '⌘M', description: 'Add App' },
|
||||
{ key: '⌘N', description: 'New Browser' },
|
||||
{ key: '⌘O', description: 'History' },
|
||||
{ key: 'Shift+A', description: 'Approve all pending' },
|
||||
{ key: 'Shift+D', description: 'Deny all pending' },
|
||||
{ key: '?', description: 'Show this help' },
|
||||
];
|
||||
|
||||
const KeyboardShortcutsHelp: React.FC = () => {
|
||||
const c = useClaudeTokens();
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const handler = (e: KeyboardEvent) => {
|
||||
const target = e.target as HTMLElement;
|
||||
if (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable) return;
|
||||
if (e.key === '?') {
|
||||
setOpen((prev) => !prev);
|
||||
}
|
||||
};
|
||||
window.addEventListener('keydown', handler);
|
||||
return () => window.removeEventListener('keydown', handler);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={open}
|
||||
onClose={() => setOpen(false)}
|
||||
PaperProps={{
|
||||
sx: {
|
||||
bgcolor: c.bg.surface,
|
||||
color: c.text.primary,
|
||||
borderRadius: 4,
|
||||
border: `1px solid ${c.border.subtle}`,
|
||||
minWidth: 360,
|
||||
boxShadow: c.shadow.lg,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<DialogTitle sx={{ color: c.text.primary, fontWeight: 600, fontSize: '1rem' }}>
|
||||
Keyboard Shortcuts
|
||||
</DialogTitle>
|
||||
<DialogContent>
|
||||
{shortcuts.map((s) => (
|
||||
<Box
|
||||
key={s.key}
|
||||
sx={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
py: 0.75,
|
||||
borderBottom: `0.5px solid ${c.border.medium}`,
|
||||
'&:last-child': { borderBottom: 'none' },
|
||||
}}
|
||||
>
|
||||
<Typography sx={{ color: c.text.muted, fontSize: '0.85rem' }}>{s.description}</Typography>
|
||||
<Box
|
||||
sx={{
|
||||
bgcolor: c.bg.secondary,
|
||||
border: `1px solid ${c.border.medium}`,
|
||||
borderRadius: 1,
|
||||
px: 1,
|
||||
py: 0.25,
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
sx={{
|
||||
color: c.accent.primary,
|
||||
fontSize: '0.75rem',
|
||||
fontFamily: c.font.mono,
|
||||
fontWeight: 600,
|
||||
}}
|
||||
>
|
||||
{s.key}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
))}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default KeyboardShortcutsHelp;
|
||||
@@ -5,7 +5,6 @@ import Chip from '@mui/material/Chip';
|
||||
import PsychologyIcon from '@mui/icons-material/Psychology';
|
||||
import SmartToyOutlinedIcon from '@mui/icons-material/SmartToyOutlined';
|
||||
import AlternateEmailIcon from '@mui/icons-material/AlternateEmail';
|
||||
import KeyboardIcon from '@mui/icons-material/Keyboard';
|
||||
import TerminalIcon from '@mui/icons-material/Terminal';
|
||||
import InsertDriveFileOutlinedIcon from '@mui/icons-material/InsertDriveFileOutlined';
|
||||
import LanguageIcon from '@mui/icons-material/Language';
|
||||
@@ -36,46 +35,6 @@ interface AtCommand {
|
||||
isChild?: boolean;
|
||||
}
|
||||
|
||||
interface Shortcut {
|
||||
key: string;
|
||||
description: string;
|
||||
category: 'navigation' | 'action';
|
||||
}
|
||||
|
||||
const SHORTCUTS: Shortcut[] = [
|
||||
{ key: 'd', description: 'Go to Dashboard', category: 'navigation' },
|
||||
{ key: '1-9', description: 'Open agent by position', category: 'navigation' },
|
||||
{ key: 'Shift+A', description: 'Approve all pending', category: 'action' },
|
||||
{ key: 'Shift+D', description: 'Deny all pending', category: 'action' },
|
||||
{ key: '?', description: 'Show shortcuts dialog', category: 'navigation' },
|
||||
];
|
||||
|
||||
const KeyBadge: React.FC<{ keys: string; c: any }> = ({ keys, c }) => (
|
||||
<Box
|
||||
sx={{
|
||||
bgcolor: c.bg.secondary,
|
||||
border: `1px solid ${c.border.medium}`,
|
||||
borderRadius: 1.5,
|
||||
px: 1.25,
|
||||
py: 0.4,
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
sx={{
|
||||
color: c.accent.primary,
|
||||
fontSize: '0.75rem',
|
||||
fontFamily: c.font.mono,
|
||||
fontWeight: 600,
|
||||
lineHeight: 1,
|
||||
}}
|
||||
>
|
||||
{keys}
|
||||
</Typography>
|
||||
</Box>
|
||||
);
|
||||
|
||||
const SectionHeader: React.FC<{
|
||||
icon: React.ReactNode;
|
||||
title: string;
|
||||
@@ -248,9 +207,6 @@ export const CommandsContent: React.FC = () => {
|
||||
return items;
|
||||
}, [builtinTools, customTools, outputItems]);
|
||||
|
||||
const navShortcuts = SHORTCUTS.filter((s) => s.category === 'navigation');
|
||||
const actionShortcuts = SHORTCUTS.filter((s) => s.category === 'action');
|
||||
|
||||
return (
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column' }}>
|
||||
<Box>
|
||||
@@ -436,95 +392,6 @@ export const CommandsContent: React.FC = () => {
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<Box sx={{ my: 2, borderTop: `1px solid ${c.border.subtle}` }} />
|
||||
|
||||
<Box>
|
||||
<SectionHeader
|
||||
icon={<KeyboardIcon sx={{ fontSize: 22 }} />}
|
||||
title="Keyboard Shortcuts"
|
||||
subtitle="Press ? anywhere to see the quick-reference dialog"
|
||||
count={SHORTCUTS.length}
|
||||
c={c}
|
||||
/>
|
||||
|
||||
<Box sx={{ display: 'flex', gap: 4 }}>
|
||||
<Box sx={{ flex: 1 }}>
|
||||
<Typography
|
||||
sx={{
|
||||
color: c.text.tertiary,
|
||||
fontSize: '0.7rem',
|
||||
fontWeight: 600,
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: 1,
|
||||
mb: 1.5,
|
||||
px: 1,
|
||||
}}
|
||||
>
|
||||
Navigation
|
||||
</Typography>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.75 }}>
|
||||
{navShortcuts.map((s) => (
|
||||
<Box
|
||||
key={s.key}
|
||||
sx={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
px: 1.5,
|
||||
py: 1,
|
||||
borderRadius: 2,
|
||||
'&:hover': { bgcolor: `${c.accent.primary}06` },
|
||||
transition: 'background-color 0.15s',
|
||||
}}
|
||||
>
|
||||
<Typography sx={{ color: c.text.muted, fontSize: '0.84rem' }}>
|
||||
{s.description}
|
||||
</Typography>
|
||||
<KeyBadge keys={s.key} c={c} />
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ flex: 1 }}>
|
||||
<Typography
|
||||
sx={{
|
||||
color: c.text.tertiary,
|
||||
fontSize: '0.7rem',
|
||||
fontWeight: 600,
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: 1,
|
||||
mb: 1.5,
|
||||
px: 1,
|
||||
}}
|
||||
>
|
||||
Actions
|
||||
</Typography>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.75 }}>
|
||||
{actionShortcuts.map((s) => (
|
||||
<Box
|
||||
key={s.key}
|
||||
sx={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
px: 1.5,
|
||||
py: 1,
|
||||
borderRadius: 2,
|
||||
'&:hover': { bgcolor: `${c.accent.primary}06` },
|
||||
transition: 'background-color 0.15s',
|
||||
}}
|
||||
>
|
||||
<Typography sx={{ color: c.text.muted, fontSize: '0.84rem' }}>
|
||||
{s.description}
|
||||
</Typography>
|
||||
<KeyBadge keys={s.key} c={c} />
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,76 +0,0 @@
|
||||
import { useEffect, useCallback } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useAppDispatch, useAppSelector } from '@/shared/hooks';
|
||||
import { handleApproval, setActiveSession } from '@/shared/state/agentsSlice';
|
||||
|
||||
export function useKeyboardShortcuts() {
|
||||
const navigate = useNavigate();
|
||||
const dispatch = useAppDispatch();
|
||||
const sessions = useAppSelector((state) => state.agents.sessions);
|
||||
|
||||
const handler = useCallback(
|
||||
(e: KeyboardEvent) => {
|
||||
const target = e.target as HTMLElement | null;
|
||||
const active = document.activeElement as HTMLElement | null;
|
||||
// Double-guard: e.target AND document.activeElement. A bare-letter
|
||||
// shortcut would otherwise fire if focus is on a wrapper Box and the
|
||||
// child input never received it, kicking the user out mid-type.
|
||||
const isInputLike = (el: HTMLElement | null) =>
|
||||
!!el && (
|
||||
el.tagName === 'INPUT' ||
|
||||
el.tagName === 'TEXTAREA' ||
|
||||
el.isContentEditable ||
|
||||
!!el.closest('input, textarea, [contenteditable="true"]')
|
||||
);
|
||||
if (isInputLike(target) || isInputLike(active)) return;
|
||||
|
||||
// Mod-gated shortcuts only. Bare letters were footguns: typing the
|
||||
// letter "d" anywhere outside a tagged input field used to navigate
|
||||
// home, which surprised users typing workflow titles/descriptions.
|
||||
if (e.key.toLowerCase() === 'd' && (e.metaKey || e.ctrlKey) && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
navigate('/');
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.key === 'A' && e.shiftKey && (e.metaKey || e.ctrlKey)) {
|
||||
e.preventDefault();
|
||||
for (const session of Object.values(sessions)) {
|
||||
for (const req of session.pending_approvals) {
|
||||
dispatch(handleApproval({ requestId: req.id, behavior: 'allow' }));
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.key === 'D' && e.shiftKey && (e.metaKey || e.ctrlKey)) {
|
||||
e.preventDefault();
|
||||
for (const session of Object.values(sessions)) {
|
||||
for (const req of session.pending_approvals) {
|
||||
dispatch(handleApproval({ requestId: req.id, behavior: 'deny' }));
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.key >= '1' && e.key <= '9' && (e.metaKey || e.ctrlKey) && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
const idx = parseInt(e.key) - 1;
|
||||
const sessionList = Object.values(sessions).sort(
|
||||
(a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime()
|
||||
);
|
||||
if (sessionList[idx]) {
|
||||
navigate('/');
|
||||
dispatch(setActiveSession(sessionList[idx].id));
|
||||
}
|
||||
return;
|
||||
}
|
||||
},
|
||||
[navigate, dispatch, sessions]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener('keydown', handler);
|
||||
return () => window.removeEventListener('keydown', handler);
|
||||
}, [handler]);
|
||||
}
|
||||
Reference in New Issue
Block a user