diff --git a/apps/code/src/renderer/components/GlobalEventHandlers.tsx b/apps/code/src/renderer/components/GlobalEventHandlers.tsx index de2aa27c1..67a51367f 100644 --- a/apps/code/src/renderer/components/GlobalEventHandlers.tsx +++ b/apps/code/src/renderer/components/GlobalEventHandlers.tsx @@ -35,6 +35,7 @@ export function GlobalEventHandlers({ (state) => state.navigateToTaskInput, ); const navigateToTask = useNavigationStore((state) => state.navigateToTask); + const navigateToInbox = useNavigationStore((state) => state.navigateToInbox); const navigateToFolderSettings = useNavigationStore( (state) => state.navigateToFolderSettings, ); @@ -165,6 +166,7 @@ export function GlobalEventHandlers({ useHotkeys(SHORTCUTS.TOGGLE_LEFT_SIDEBAR, toggleLeftSidebar, globalOptions); useHotkeys(SHORTCUTS.TOGGLE_RIGHT_SIDEBAR, toggleRightSidebar, globalOptions); useHotkeys(SHORTCUTS.SHORTCUTS_SHEET, onToggleShortcutsSheet, globalOptions); + useHotkeys(SHORTCUTS.INBOX, navigateToInbox, globalOptions); useHotkeys(SHORTCUTS.PREV_TASK, handlePrevTask, globalOptions, [ handlePrevTask, ]); diff --git a/apps/code/src/renderer/constants/keyboard-shortcuts.ts b/apps/code/src/renderer/constants/keyboard-shortcuts.ts index dba95a10b..3ae4df768 100644 --- a/apps/code/src/renderer/constants/keyboard-shortcuts.ts +++ b/apps/code/src/renderer/constants/keyboard-shortcuts.ts @@ -18,6 +18,7 @@ export const SHORTCUTS = { COPY_PATH: "mod+shift+c", TOGGLE_FOCUS: "mod+r", PASTE_AS_FILE: "mod+shift+v", + INBOX: "mod+i", BLUR: "escape", SUBMIT_BLUR: "mod+enter", } as const; @@ -66,6 +67,12 @@ export const KEYBOARD_SHORTCUTS: KeyboardShortcut[] = [ description: "Show keyboard shortcuts", category: "general", }, + { + id: "inbox", + keys: SHORTCUTS.INBOX, + description: "Open inbox", + category: "navigation", + }, { id: "switch-task", keys: "mod+0-9", diff --git a/apps/code/src/renderer/features/inbox/components/InboxSignalsTab.tsx b/apps/code/src/renderer/features/inbox/components/InboxSignalsTab.tsx index 6da67984a..cf46e65c3 100644 --- a/apps/code/src/renderer/features/inbox/components/InboxSignalsTab.tsx +++ b/apps/code/src/renderer/features/inbox/components/InboxSignalsTab.tsx @@ -1,4 +1,5 @@ import { useAuthStateValue } from "@features/auth/hooks/authQueries"; +import { InboxLiveRail } from "@features/inbox/components/InboxLiveRail"; import { useInboxReportArtefacts, useInboxReportSignals, @@ -32,8 +33,10 @@ import { CircleNotchIcon, ClockIcon, Cloud as CloudIcon, + CommandIcon, GithubLogoIcon, KanbanIcon, + KeyReturnIcon, TicketIcon, VideoIcon, WarningIcon, @@ -556,6 +559,68 @@ export function InboxSignalsTab() { [reports, selectedReportId], ); + // ── Arrow-key navigation between reports ────────────────────────────── + const reportsRef = useRef(reports); + reportsRef.current = reports; + const selectedReportIdRef = useRef(selectedReportId); + selectedReportIdRef.current = selectedReportId; + + const leftPaneRef = useRef(null); + + // Auto-focus the list pane when the inbox mounts so arrow keys work immediately + useEffect(() => { + leftPaneRef.current?.focus(); + }, []); + + const navigateReport = useCallback((direction: 1 | -1) => { + const list = reportsRef.current; + if (list.length === 0) return; + const currentId = selectedReportIdRef.current; + const currentIndex = currentId + ? list.findIndex((r) => r.id === currentId) + : -1; + const nextIndex = + currentIndex === -1 + ? 0 + : Math.max(0, Math.min(list.length - 1, currentIndex + direction)); + const nextId = list[nextIndex].id; + setSelectedReportId(nextId); + // Move focus back to the list container so the previously clicked card + // loses its focus outline + leftPaneRef.current?.focus(); + leftPaneRef.current + ?.querySelector(`[data-report-id="${nextId}"]`) + ?.scrollIntoView({ block: "nearest" }); + }, []); + + const handleCreateTaskRef = useRef<() => void>(() => {}); + + const handleListKeyDown = useCallback( + (e: React.KeyboardEvent) => { + // Don't capture arrow keys when focus is inside interactive child UI + // like filter popovers, dropdowns, or search inputs + const target = e.target as HTMLElement; + if ( + target.closest( + "[role='menu'], [role='listbox'], [role='dialog'], [data-radix-popper-content-wrapper], input, select, textarea", + ) + ) + return; + + if (e.key === "ArrowDown") { + e.preventDefault(); + navigateReport(1); + } else if (e.key === "ArrowUp") { + e.preventDefault(); + navigateReport(-1); + } else if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) { + e.preventDefault(); + handleCreateTaskRef.current(); + } + }, + [navigateReport], + ); + const artefactsQuery = useInboxReportArtefacts(selectedReport?.id ?? "", { enabled: !!selectedReport, }); @@ -635,6 +700,7 @@ export function InboxSignalsTab() { }); navigateToTaskInput(); }; + handleCreateTaskRef.current = handleCreateTask; const handleOpenCloudConfirm = useCallback(() => { openCloudConfirm(repositories[0] ?? null); @@ -759,7 +825,12 @@ export function InboxSignalsTab() { disabled={!canActOnReport} className="text-[12px]" > - Pick up task + + {isRunningCloudTask ? "Running..." : "Run task"} + + + + {cloudModeEnabled && (