diff --git a/web/src/__tests__/useKeyboard.test.tsx b/web/src/__tests__/useKeyboard.test.tsx
index fabf059d..c542477a 100644
--- a/web/src/__tests__/useKeyboard.test.tsx
+++ b/web/src/__tests__/useKeyboard.test.tsx
@@ -113,6 +113,62 @@ describe('KeyboardProvider', () => {
);
});
+ it.each(['metaKey', 'ctrlKey', 'altKey', 'shiftKey'] as const)(
+ 'leaves %s combinations to the platform',
+ (modifier) => {
+ const create = vi.fn();
+ const move = vi.fn();
+ function RegisterCreate() {
+ const { setOpenCreateDialog } = useKeyboard();
+ React.useEffect(() => setOpenCreateDialog(create), [setOpenCreateDialog]);
+ return null;
+ }
+ render(
+
+
+
+
+ );
+ for (const key of ['c', 'j', 'k', '1', 'ArrowDown', 'ArrowUp', 'Enter']) {
+ const event = new KeyboardEvent('keydown', { key, [modifier]: true, cancelable: true });
+ fireEvent(window, event);
+ expect(event.defaultPrevented).toBe(false);
+ }
+ expect(create).not.toHaveBeenCalled();
+ expect(move).not.toHaveBeenCalled();
+ expect(screen.getByTestId('selected').textContent).toBe('none');
+ fireEvent.keyDown(window, { key: 'c' });
+ expect(create).toHaveBeenCalledOnce();
+ }
+ );
+
+ it('preserves the intentional chat chord and ignores IME composition', () => {
+ const chat = vi.fn();
+ const create = vi.fn();
+ function RegisterCommands() {
+ const { setOpenChatPanel, setOpenCreateDialog } = useKeyboard();
+ React.useEffect(() => {
+ setOpenChatPanel(chat);
+ setOpenCreateDialog(create);
+ }, [setOpenChatPanel, setOpenCreateDialog]);
+ return null;
+ }
+ render(
+
+
+
+ );
+ fireEvent.keyDown(window, { key: 'C', metaKey: true, shiftKey: true });
+ fireEvent.keyDown(window, { key: 'C', ctrlKey: true, shiftKey: true });
+ expect(chat).toHaveBeenCalledTimes(2);
+ fireEvent.keyDown(window, { key: 'C', ctrlKey: true, shiftKey: true, altKey: true });
+ fireEvent.keyDown(window, { key: 'C', metaKey: true, shiftKey: true, isComposing: true });
+ fireEvent.keyDown(window, { key: 'c', isComposing: true });
+ fireEvent.keyDown(window, { key: 'c', keyCode: 229 });
+ expect(chat).toHaveBeenCalledTimes(2);
+ expect(create).not.toHaveBeenCalled();
+ });
+
it('starts with no selected task and help closed', () => {
renderWithProvider();
expect(screen.getByTestId('selected').textContent).toBe('none');
diff --git a/web/src/components/board/KanbanBoard.tsx b/web/src/components/board/KanbanBoard.tsx
index 9ea2be52..bfd847b8 100644
--- a/web/src/components/board/KanbanBoard.tsx
+++ b/web/src/components/board/KanbanBoard.tsx
@@ -375,6 +375,7 @@ export function KanbanBoard() {
// Register filtered tasks with keyboard context
useEffect(() => {
setTasks(filteredTasks);
+ return () => setTasks([]);
}, [filteredTasks, setTasks]);
// Handler for opening a task
diff --git a/web/src/hooks/useKeyboard.tsx b/web/src/hooks/useKeyboard.tsx
index e1c57961..abd842ee 100644
--- a/web/src/hooks/useKeyboard.tsx
+++ b/web/src/hooks/useKeyboard.tsx
@@ -116,6 +116,8 @@ export function KeyboardProvider({ children }: { children: ReactNode }) {
const target = e.target instanceof HTMLElement ? e.target : null;
if (
e.defaultPrevented ||
+ e.isComposing ||
+ e.keyCode === 229 ||
target?.isContentEditable ||
target?.closest(
'input, textarea, select, button, a[href], summary, [role="button"], [role="link"], [role="switch"], [role="checkbox"], [role="radio"], [role="combobox"], [role="listbox"], [role="option"], [role="slider"], [role="spinbutton"], [role="tab"], [role="menuitem"]'
@@ -143,12 +145,16 @@ export function KeyboardProvider({ children }: { children: ReactNode }) {
const currentIndex = selectedTaskId ? taskList.findIndex((t) => t.id === selectedTaskId) : -1;
// Cmd+Shift+C (or Ctrl+Shift+C on Windows/Linux) - Toggle chat panel
- if ((e.metaKey || e.ctrlKey) && e.shiftKey && e.key === 'C') {
+ if (e.metaKey !== e.ctrlKey && !e.altKey && e.shiftKey && e.key.toLowerCase() === 'c') {
e.preventDefault();
openChatPanel();
return;
}
+ // Unregistered modifiers belong to the platform or focused application.
+ // Shift is permitted only for '?' so shortcut help remains reachable.
+ if (e.metaKey || e.ctrlKey || e.altKey || (e.shiftKey && e.key !== '?')) return;
+
switch (e.key) {
case 'c':
e.preventDefault();
@@ -215,7 +221,7 @@ export function KeyboardProvider({ children }: { children: ReactNode }) {
});
return;
}
- if (selectedTaskId && onMoveTaskRef.current) {
+ if (selectedTaskId && currentIndex >= 0 && onMoveTaskRef.current) {
onMoveTaskRef.current(selectedTaskId, newStatus);
} else {
// Show toast when no task is selected