Skip to content

feat: add task navigation with Ctrl+# shortcuts#1395

Open
drochag wants to merge 4 commits intogeneralaction:mainfrom
drochag:feat/task-navigation-shortcuts
Open

feat: add task navigation with Ctrl+# shortcuts#1395
drochag wants to merge 4 commits intogeneralaction:mainfrom
drochag:feat/task-navigation-shortcuts

Conversation

@drochag
Copy link
Copy Markdown

@drochag drochag commented Mar 10, 2026

Summary

Add direct task navigation using Ctrl+1-9 shortcuts, solving #1389.

  • Tasks display number badges (⌃1, ⌃2, etc.) for the first 9 tasks globally
  • Pressing Ctrl+N selects the corresponding task
  • Cmd+1-9 remains for agent tab switching (no regression)
  • Configurable: users can swap modifiers in Settings → Interface → "Number shortcuts (1-9)"

Changes

  • src/main/settings.ts: Add numberShortcutBehavior setting type and defaults
  • src/renderer/types/shortcuts.ts: Add types for the new setting and onSelectTask handler
  • src/renderer/hooks/useKeyboardShortcuts.ts: Add getTaskSelectionIndex function and update handler
  • src/renderer/hooks/useTaskManagement.ts: Add handleSelectTaskByIndex function
  • src/renderer/components/AppKeyboardShortcuts.tsx: Wire up the task selection handler
  • src/renderer/components/TaskItem.tsx: Add shortcut badge display (⌃1, ⌘2, etc.)
  • src/renderer/components/sidebar/LeftSidebar.tsx: Compute global task indices and pass to TaskItem
  • src/renderer/components/KeyboardSettingsCard.tsx: Add dropdown to configure modifier behavior
  • src/test/renderer/useKeyboardShortcuts.test.ts: Add tests for new functions

Test plan

  • Open app with multiple tasks across multiple projects
  • Verify tasks show ⌃1, ⌃2, etc. badges (first 9 globally)
  • Press Ctrl+1 → should select first task
  • Press Ctrl+5 → should select fifth task (if exists)
  • With 3 tasks, press Ctrl+5 → nothing should happen
  • Cmd+1-9 still switches agent tabs (no regression)
  • Change setting to "⌘N for tasks, ⌃N for agents" in Settings
  • Verify badges now show ⌘1, ⌘2, etc.
  • Cmd+1 should now select tasks, Ctrl+1 should switch agents
Screenshot 2026-03-10 at 12 14 43 p m Screenshot 2026-03-10 at 12 14 30 p m

Summary by CodeRabbit

  • New Features
    • Configurable number shortcuts (1–9) to select tasks using Ctrl or Cmd modifier.
    • Task items show keyboard shortcut badges with modifier and number.
    • New keyboard setting to choose number-shortcut modifier behavior (Ctrl vs Cmd).

Add direct task navigation using Ctrl+1-9 shortcuts. Tasks display
number badges (⌃1, ⌃2, etc.) and pressing Ctrl+N selects the
corresponding task. Cmd+1-9 remains for agent tab switching.

- Add getTaskSelectionIndex function for Ctrl+1-9 task selection
- Add shortcut badges to TaskItem component (first 9 tasks globally)
- Add handleSelectTaskByIndex to useTaskManagement hook
- Add numberShortcutBehavior setting to swap Ctrl/Cmd behavior
- Add settings UI dropdown to configure number shortcut behavior
- Update tests for new keyboard shortcut functions

The behavior is configurable: users can swap modifiers in Settings
so that Cmd+# selects tasks and Ctrl+# switches agent tabs.

Closes generalaction#1389
@vercel
Copy link
Copy Markdown

vercel bot commented Mar 10, 2026

@drochag is attempting to deploy a commit to the General Action Team on Vercel.

A member of the Team first needs to authorize it.

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Mar 10, 2026

Greptile Summary

This PR adds direct task navigation via Ctrl+1-9 shortcuts (configurable to Cmd+1-9), with matching ⌃N/⌘N badges rendered on the first 9 tasks in the sidebar. The overall architecture is sound — settings plumbing, badge rendering, and the new getTaskSelectionIndex helper are all cleanly implemented — but two logic bugs affect correctness.

  • Wrong task selected when projects are reordered: LeftSidebar computes badge indices using sortedProjects (the user's custom drag-and-drop order from localStorage), while handleSelectTaskByIndex in useTaskManagement builds its flat task list from projects (raw DB order). Any user who has reordered their projects will see ⌃1 on one task but have Ctrl+1 activate a different task.
  • Shortcuts fire inside editable fields: The isEditableTarget guard that prevents other shortcuts from firing inside <input> / <textarea> / contenteditable elements is not applied to the number-key block. Pressing Ctrl+1 while typing in any text field will unexpectedly switch the active task.
  • The KeyboardSettingsCard type-guard fix (checking 'key' in saved && 'modifier' in saved before using a keyboard setting as a ShortcutBinding) is a nice defensive improvement.
  • Test coverage for getTaskSelectionIndex and the updated getAgentTabSelectionIndex is thorough.

Confidence Score: 2/5

  • Not safe to merge — the shortcut selects the wrong task for any user who has reordered their projects.
  • Two logic bugs are present: (1) the project-ordering mismatch between badge display and task resolution will silently select the wrong task for users with custom project order, and (2) number shortcuts are not blocked inside editable fields, causing unintended task switches while typing.
  • src/renderer/hooks/useTaskManagement.ts (ordering mismatch) and src/renderer/hooks/useKeyboardShortcuts.ts (missing editable-field guard) need fixes before merge.

Important Files Changed

Filename Overview
src/renderer/hooks/useTaskManagement.ts Adds handleSelectTaskByIndex but iterates projects (raw DB order) instead of sortedProjects (user's custom order), causing the wrong task to be selected when projects have been reordered.
src/renderer/hooks/useKeyboardShortcuts.ts Adds getTaskSelectionIndex and integrates both task/agent number-key handlers, but the number-key section is not guarded by the isEditableTarget check, so shortcuts fire unexpectedly while typing in inputs.
src/renderer/components/sidebar/LeftSidebar.tsx Correctly computes global shortcut indices using sortedProjects and passes them to TaskItem; however the ordering used here diverges from useTaskManagement.ts's resolution logic.
src/renderer/components/AppKeyboardShortcuts.tsx Cleanly wires handleSelectTaskByIndex from context into useKeyboardShortcuts via the onSelectTask handler; no issues.
src/renderer/components/KeyboardSettingsCard.tsx Adds a Select dropdown for the new setting and includes a defensive type-guard fix to prevent non-ShortcutBinding values (like numberShortcutBehavior) from being treated as bindings.
src/renderer/components/TaskItem.tsx Adds optional shortcutIndex / shortcutModifier props and renders a small <kbd> badge; straightforward and well-isolated.
src/main/settings.ts Adds NumberShortcutBehavior type, default value, and normalization logic; normalization correctly whitelists only the non-default value.
src/renderer/types/shortcuts.ts Adds NumberShortcutBehavior type, extends KeyboardSettings and GlobalShortcutHandlers; clean, no issues.
src/test/renderer/useKeyboardShortcuts.test.ts Good test coverage for both getTaskSelectionIndex and the updated getAgentTabSelectionIndex, including modifier-swap scenarios; no issues.

Sequence Diagram

sequenceDiagram
    participant User
    participant KeyboardShortcuts as useKeyboardShortcuts
    participant TaskMgmt as useTaskManagement
    participant Sidebar as LeftSidebar
    participant TaskItem

    Note over Sidebar: On render: sortedProjects (custom order)
    Sidebar->>Sidebar: compute taskShortcutIndices (1-9) from sortedProjects
    Sidebar->>TaskItem: shortcutIndex={1..9}, shortcutModifier={'ctrl'|'cmd'}
    TaskItem-->>User: renders ⌃1 / ⌘1 badge

    User->>KeyboardShortcuts: keydown Ctrl+1
    KeyboardShortcuts->>KeyboardShortcuts: getTaskSelectionIndex(event, useCmdForTasks)
    KeyboardShortcuts->>TaskMgmt: onSelectTask(index=0)
    Note over TaskMgmt: Iterates raw `projects` (DB order) ⚠️
    TaskMgmt->>TaskMgmt: build allTasksInOrder from projects (not sortedProjects)
    TaskMgmt-->>User: activates allTasksInOrder[0] (may differ from badge ⌃1)
Loading

Comments Outside Diff (1)

  1. src/renderer/hooks/useKeyboardShortcuts.ts, line 604-638 (link)

    Number shortcuts fire inside editable fields

    The isEditableTarget guard (line 540-541) is applied only inside the for (const shortcut of shortcuts) loop. The new task-selection and existing agent-tab-selection blocks that follow the loop have no such guard. As a result, pressing Ctrl+1 (or Cmd+1) while the cursor is in a text <input>, <textarea>, or contentEditable element will unexpectedly switch the active task (or agent tab).

    Consider applying the same guard here:

    // After the shortcut loop, before number-key handling:
    if (isEditableTarget) return;
    
    const useCmdForTasks = ...;

    This matches the pattern used for every other non-palette shortcut and prevents surprising task switches while the user is actively typing.

Last reviewed commit: 2e51aef

Comment on lines +392 to +407
const handleSelectTaskByIndex = useCallback(
(index: number) => {
// Build a flat list of all tasks in sidebar order (by project, then pinned first within each)
const allTasksInOrder: Task[] = [];
for (const project of projects) {
const projectTasks = (tasksByProjectId[project.id] ?? [])
.slice()
.sort((a, b) => (b.metadata?.isPinned ? 1 : 0) - (a.metadata?.isPinned ? 1 : 0));
allTasksInOrder.push(...projectTasks);
}

if (index >= 0 && index < allTasksInOrder.length) {
handleSelectTask(allTasksInOrder[index]);
}
},
[projects, tasksByProjectId, handleSelectTask]
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale project order causes wrong task selection

handleSelectTaskByIndex iterates projects (raw DB order from context), but the sidebar badge numbers are computed using sortedProjects (which respects the user-configurable projectOrder stored in localStorage — see LeftSidebar.tsx lines 110-120).

If the user has reordered their projects via drag-and-drop, the two orderings diverge. Pressing Ctrl+1 will select the task ranked first in DB order, but the ⌃1 badge will be shown on a different task (the first in the user's custom order). This means the shortcut silently selects the wrong task.

useTaskManagement doesn't have access to sortedProjects directly, so the fix should be to either:

  • Accept the sorted project list as a parameter to the callback, or
  • Move the index-to-task resolution into LeftSidebar where sortedProjects is available and pass the resolved Task up instead of a numeric index.
// e.g. accept a pre-computed ordered list
const handleSelectTaskByIndex = useCallback(
  (index: number, orderedTasks: Task[]) => {
    if (index >= 0 && index < orderedTasks.length) {
      handleSelectTask(orderedTasks[index]);
    }
  },
  [handleSelectTask]
);

- Add isEditableTarget guard before number-key shortcuts to prevent
  task switching while typing in input fields
- Use same project ordering (from localStorage) in handleSelectTaskByIndex
  as LeftSidebar uses for badge display, fixing mismatch when projects
  are reordered
@drochag
Copy link
Copy Markdown
Author

drochag commented Mar 11, 2026

Addressed the review feedback in e20b80c:

  1. Fixed shortcuts firing in editable fields - Added isEditableTarget guard before the number-key handling block, so Ctrl+1-9 won't trigger while typing in input fields.

  2. Fixed project ordering mismatch - handleSelectTaskByIndex now reads from the same localStorage key (sidebarProjectOrder) that LeftSidebar uses to sort projects. This ensures the task selected by Ctrl+N matches the badge displayed.

Copy link
Copy Markdown
Contributor

@yashdev9274 yashdev9274 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hey @drochag

this task change shortcut feature is not smooth.

it's needing to click on project root to change the task every time - the sidebar/main content doesn't update properly on keyboard-triggered task selection.

> possible root cause

The keyboard shortcut flow (Ctrl+1handleSelectTaskByIndex(index)handleSelectTask(task)) should call the same handleSelectTask as clicking on a task. However, there may be:

  1. Focus handling - keyboard events may not trigger proper focus/context updates
  2. State sync - React state batching differs between keyboard vs click flows
  3. Sidebar refresh - sidebar may not re-render to show active task highlight

> i would like you to address couple of things:

Investigate and fix the keyboard-triggered task selection to match click behavior:

  1. Add debug logging to verify:
    • handleSelectTaskByIndex is called correctly
    • Same task is resolved as clicking would produce
  2. Ensure proper focus/context updates after keyboard task selection
  3. Verify sidebar properly reflects activeTask changes when triggered via keyboard

and push the new changes

Resolved conflicts by integrating upstream changes:
- Added KeyboardShortcutBinding type that supports null (disabled shortcuts)
- Added openInEditor shortcut
- Added changelog notification feature
- Added ProjectItem component in LeftSidebar

While preserving feature changes:
- numberShortcutBehavior setting for task navigation shortcuts
- Task shortcut indices display (Ctrl+1-9)
- handleSelectTaskByIndex function

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 21, 2026

📝 Walkthrough

Walkthrough

Adds configurable number-key task selection (1–9) with a new numberShortcutBehavior setting, keyboard handling to map modifier+number to task or agent selection, sidebar-indexed task lookup, UI for setting and badges, and tests for the new selection logic.

Changes

Cohort / File(s) Summary
Settings & Types
src/main/settings.ts, src/renderer/types/shortcuts.ts
Added `NumberShortcutBehavior = 'ctrl-tasks'
Keyboard Shortcut Logic
src/renderer/hooks/useKeyboardShortcuts.ts
Added getTaskSelectionIndex(event, useCmdForTasks, isMac?); updated getAgentTabSelectionIndex signature to accept modifier param; reworked number-key flow to prefer task selection, gate on editable targets, and handle command-palette close + delayed selection.
Task Management
src/renderer/hooks/useTaskManagement.ts
Added handleSelectTaskByIndex(index) that builds sidebar-ordered, pinned-first flattened task list (using localStorage.sidebarProjectOrder when available) and selects the task at the given index.
UI Integration
src/renderer/components/KeyboardSettingsCard.tsx, src/renderer/components/AppKeyboardShortcuts.tsx
Added Select control to configure numberShortcutBehavior; wired onSelectTask from task management into keyboard shortcuts hook.
Task Display
src/renderer/components/TaskItem.tsx, src/renderer/components/sidebar/LeftSidebar.tsx
TaskItem accepts shortcutIndex and shortcutModifier and renders a kbd badge; LeftSidebar computes global 1–9 shortcut indices (pinned-first) and passes index/modifier into TaskItem.
Tests
src/test/renderer/useKeyboardShortcuts.test.ts
Added tests for getTaskSelectionIndex; updated getAgentTabSelectionIndex tests to cover the new modifier parameter and expanded edge-case coverage.

Sequence Diagram

sequenceDiagram
    participant User
    participant Hook as useKeyboardShortcuts
    participant Palette as CommandPalette
    participant TaskMgmt as useTaskManagement
    participant SidebarUI as LeftSidebar/TaskItem

    User->>Hook: press modifier+N (1-9)
    Hook->>Hook: getTaskSelectionIndex(event, useCmdForTasks)
    alt matches task index
        Hook->>Palette: request close (if open)
        Palette-->>Hook: closed
        Hook->>TaskMgmt: handlers.onSelectTask(index)
        TaskMgmt->>TaskMgmt: resolve task from sidebar-ordered list
        TaskMgmt->>SidebarUI: update selected task
        SidebarUI->>User: task highlighted
    else no task match
        Hook->>Hook: getAgentTabSelectionIndex(event, useCtrlForAgents)
        alt matches agent index
            Hook->>Palette: request close (if open)
            Palette-->>Hook: closed
            Hook->>Hook: select agent tab
        end
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Poem

🐇⌨️ I hopped through keys, one to nine,
Ctrl or Cmd — the choice is mine.
Badges gleam beside each task,
Quick selects done — no need to ask.
A rabbit's cheer for shortcuts fine! 🎉

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 66.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: adding task navigation using Ctrl+# shortcuts, which is the primary feature across all modified files.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (3)
src/renderer/hooks/useTaskManagement.ts (1)

460-497: Extract the sidebar ordering into a shared helper.

handleSelectTaskByIndex now re-implements the same sidebarProjectOrder + pinned-first flattening that src/renderer/components/sidebar/LeftSidebar.tsx uses for badge numbering. That contract already drifted once in this PR; keeping two copies means the displayed badge order and the selected task can silently diverge again the next time one side changes.

Also applies to: 1151-1151

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/hooks/useTaskManagement.ts` around lines 460 - 497,
handleSelectTaskByIndex duplicates the sidebar ordering + pinned-first
flattening logic that LeftSidebar.tsx uses, causing potential drift; extract
that logic into a shared helper (e.g., getSidebarOrderedTasks or
getSidebarProjectOrder + flattenTasksByPinned) and replace the local
implementation in useTaskManagement's handleSelectTaskByIndex as well as the
logic in LeftSidebar.tsx to call the common helper; the helper should read
'sidebarProjectOrder' from localStorage, return projects in the stored order
(falling back safely on errors), and provide the flattened task list that sorts
pinned tasks first within each project so both badge numbering and task
selection use identical ordering.
src/renderer/components/KeyboardSettingsCard.tsx (1)

148-151: Consider keeping binding settings homogeneous.

This cast is only needed because KeyboardSettings now mixes numberShortcutBehavior in with the ShortcutBinding | null entries. The same mixed shape now leaks into generic consumers like assertNoKeyboardShortcutConflicts in src/main/settings.ts. Splitting non-binding prefs out of the binding map would keep this code fully typed.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/components/KeyboardSettingsCard.tsx` around lines 148 - 151, The
keyboard settings map (KeyboardSettings) currently mixes non-binding prefs like
numberShortcutBehavior into entries that should be only ShortcutBinding, forcing
casts in KeyboardSettingsCard (the keyboard lookup using
keyboard?.[shortcut.settingsKey as ShortcutSettingsKey]) and leaking the mixed
shape into services such as assertNoKeyboardShortcutConflicts; split non-binding
prefs out of the binding map so the binding map contains only ShortcutBinding |
null, update the type of keyboard and ShortcutSettingsKey consumers accordingly,
remove the ad-hoc cast in KeyboardSettingsCard, and update callers like
assertNoKeyboardShortcutConflicts to read non-binding prefs (e.g.
numberShortcutBehavior) from the separate config area rather than from the
binding map.
src/renderer/hooks/useKeyboardShortcuts.ts (1)

643-644: Consider adding a comment to clarify the parameter mapping.

The call passes useCmdForTasks to a parameter named useCtrlForAgents. While logically equivalent (when tasks use Cmd, agents use Ctrl), this reads confusingly at the call site. A brief comment would improve maintainability.

💡 Suggested clarification
-      // Check for agent tab selection (Cmd+1-9 by default, or Ctrl+1-9 if tasks use Cmd)
-      const agentTabIndex = getAgentTabSelectionIndex(event, useCmdForTasks);
+      // Check for agent tab selection (Cmd+1-9 by default, or Ctrl+1-9 if tasks use Cmd)
+      // Note: useCmdForTasks === useCtrlForAgents (when tasks use Cmd, agents use Ctrl)
+      const agentTabIndex = getAgentTabSelectionIndex(event, useCmdForTasks);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/hooks/useKeyboardShortcuts.ts` around lines 643 - 644, The call
to getAgentTabSelectionIndex(event, useCmdForTasks) is confusing because the
second argument is named useCtrlForAgents while you're passing useCmdForTasks;
add a concise inline comment at this callsite in useKeyboardShortcuts.ts
explaining the mapping (e.g., "pass useCmdForTasks here because when tasks use
Cmd, agents use Ctrl") so readers understand why the flipped flag is provided to
getAgentTabSelectionIndex; reference getAgentTabSelectionIndex and the
useCmdForTasks variable in the comment.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/test/renderer/useKeyboardShortcuts.test.ts`:
- Around line 166-195: The test suite is missing the non-macOS case for
useCmdForTasks; update the helper and tests so Ctrl acts as the
Command-equivalent on non-Mac. In getTaskSelectionIndex, when useCmdForTasks is
true, change the modifier check to use (isMac ? event.metaKey : event.ctrlKey)
instead of always event.metaKey so Windows/Linux treat Ctrl as "Cmd"; then add
assertions in useKeyboardShortcuts.test.ts mirroring the mac case but with
isMac=false: expect a KeyboardEvent with ctrlKey true to return 0 and expect
metaKey true / ctrlKey false to return null.

---

Nitpick comments:
In `@src/renderer/components/KeyboardSettingsCard.tsx`:
- Around line 148-151: The keyboard settings map (KeyboardSettings) currently
mixes non-binding prefs like numberShortcutBehavior into entries that should be
only ShortcutBinding, forcing casts in KeyboardSettingsCard (the keyboard lookup
using keyboard?.[shortcut.settingsKey as ShortcutSettingsKey]) and leaking the
mixed shape into services such as assertNoKeyboardShortcutConflicts; split
non-binding prefs out of the binding map so the binding map contains only
ShortcutBinding | null, update the type of keyboard and ShortcutSettingsKey
consumers accordingly, remove the ad-hoc cast in KeyboardSettingsCard, and
update callers like assertNoKeyboardShortcutConflicts to read non-binding prefs
(e.g. numberShortcutBehavior) from the separate config area rather than from the
binding map.

In `@src/renderer/hooks/useKeyboardShortcuts.ts`:
- Around line 643-644: The call to getAgentTabSelectionIndex(event,
useCmdForTasks) is confusing because the second argument is named
useCtrlForAgents while you're passing useCmdForTasks; add a concise inline
comment at this callsite in useKeyboardShortcuts.ts explaining the mapping
(e.g., "pass useCmdForTasks here because when tasks use Cmd, agents use Ctrl")
so readers understand why the flipped flag is provided to
getAgentTabSelectionIndex; reference getAgentTabSelectionIndex and the
useCmdForTasks variable in the comment.

In `@src/renderer/hooks/useTaskManagement.ts`:
- Around line 460-497: handleSelectTaskByIndex duplicates the sidebar ordering +
pinned-first flattening logic that LeftSidebar.tsx uses, causing potential
drift; extract that logic into a shared helper (e.g., getSidebarOrderedTasks or
getSidebarProjectOrder + flattenTasksByPinned) and replace the local
implementation in useTaskManagement's handleSelectTaskByIndex as well as the
logic in LeftSidebar.tsx to call the common helper; the helper should read
'sidebarProjectOrder' from localStorage, return projects in the stored order
(falling back safely on errors), and provide the flattened task list that sorts
pinned tasks first within each project so both badge numbering and task
selection use identical ordering.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 18b4a176-40b8-420c-8f4a-9a0306c2ef15

📥 Commits

Reviewing files that changed from the base of the PR and between 75c4458 and ef44ca2.

📒 Files selected for processing (9)
  • src/main/settings.ts
  • src/renderer/components/AppKeyboardShortcuts.tsx
  • src/renderer/components/KeyboardSettingsCard.tsx
  • src/renderer/components/TaskItem.tsx
  • src/renderer/components/sidebar/LeftSidebar.tsx
  • src/renderer/hooks/useKeyboardShortcuts.ts
  • src/renderer/hooks/useTaskManagement.ts
  • src/renderer/types/shortcuts.ts
  • src/test/renderer/useKeyboardShortcuts.test.ts

Comment on lines +166 to +195
it('uses Cmd for tasks when useCmdForTasks is true', () => {
// When tasks use Cmd
expect(
getTaskSelectionIndex(
{
key: '1',
metaKey: true,
ctrlKey: false,
altKey: false,
shiftKey: false,
} as KeyboardEvent,
true, // useCmdForTasks = true
true // isMac
)
).toBe(0);

// Ctrl should NOT work when tasks are set to use Cmd
expect(
getTaskSelectionIndex(
{
key: '1',
metaKey: false,
ctrlKey: true,
altKey: false,
shiftKey: false,
} as KeyboardEvent,
true // useCmdForTasks = true
)
).toBeNull();
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Add the swapped-modifier non-macOS case here.

This suite only covers useCmdForTasks = true on macOS. In src/renderer/hooks/useKeyboardShortcuts.ts, getTaskSelectionIndex(..., true, false) currently checks event.metaKey on non-Mac, while getAgentTabSelectionIndex treats Ctrl as the Command equivalent there. That leaves the "⌘N for tasks, ⌃N for agents" setting effectively unreachable on Windows/Linux. Please add the non-Mac assertion here and align the helper with the existing Ctrl-as-Command behavior.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/test/renderer/useKeyboardShortcuts.test.ts` around lines 166 - 195, The
test suite is missing the non-macOS case for useCmdForTasks; update the helper
and tests so Ctrl acts as the Command-equivalent on non-Mac. In
getTaskSelectionIndex, when useCmdForTasks is true, change the modifier check to
use (isMac ? event.metaKey : event.ctrlKey) instead of always event.metaKey so
Windows/Linux treat Ctrl as "Cmd"; then add assertions in
useKeyboardShortcuts.test.ts mirroring the mac case but with isMac=false: expect
a KeyboardEvent with ctrlKey true to return 0 and expect metaKey true / ctrlKey
false to return null.

When useCmdForTasks is true on non-Mac platforms, Ctrl should act as
the Command equivalent since the Meta/Windows key isn't commonly used
for keyboard shortcuts. This aligns with how getAgentTabSelectionIndex
already handles the inverse case.

Added test coverage for the non-Mac useCmdForTasks=true scenario.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/renderer/hooks/useKeyboardShortcuts.ts`:
- Around line 294-296: The cmd-tasks branch makes Ctrl+1-9 always go to task
selection off macOS; update the logic in useKeyboardShortcuts where
useCmdForTasks/hasTaskModifier and numberShortcutBehavior are evaluated so
agent-number shortcuts remain reachable on non-Mac. Specifically, change the
non-Mac fallback for hasTaskModifier (and the checks around lines referencing
numberShortcutBehavior === 'cmd-tasks') to use a distinct modifier (e.g., Alt or
Shift) or gate swapping to isMac before remapping, and ensure the handler order
still allows onSelectAgentTab to run when the agent modifier is pressed; adjust
the conditions that call onSelectTask vs onSelectAgentTab so they check the
corrected modifier flags and numberShortcutBehavior consistently.
- Around line 302-311: The digit-row keyboard shortcut code in
getTaskTabSelectionIndex and getAgentTabSelectionIndex is layout-dependent
because it uses event.key and rejects event.shiftKey; replace that logic to use
event.code for layout-independent matching (check event.code matches
/^Digit[1-9]$/), remove or relax the event.shiftKey rejection so AZERTY/shifted
digit keys work, and convert the matched code (e.g., "Digit1") to a zero-based
index (DigitN -> N-1) to return the correct tab index.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 19cc6be5-f9af-4f7e-a4b7-dfcbc4eaa562

📥 Commits

Reviewing files that changed from the base of the PR and between ef44ca2 and 025e26d.

📒 Files selected for processing (2)
  • src/renderer/hooks/useKeyboardShortcuts.ts
  • src/test/renderer/useKeyboardShortcuts.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/test/renderer/useKeyboardShortcuts.test.ts

Comment on lines +294 to +296
if (useCmdForTasks) {
// User wants Cmd/Meta for tasks (on non-Mac, Ctrl acts as Command equivalent)
hasTaskModifier = isMac ? event.metaKey && !event.ctrlKey : event.ctrlKey && !event.metaKey;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

cmd-tasks makes agent-number shortcuts unreachable off macOS.

On Windows/Linux, Line 296 still maps the task path back to Ctrl, and Lines 325-327 also put agents on Ctrl when numberShortcutBehavior === 'cmd-tasks'. Because Line 629 handles task selection first, Ctrl+1-9 always returns through onSelectTask, so onSelectAgentTab can never fire in that mode. This needs a distinct non-macOS fallback, or the swap option should be gated to macOS.

Also applies to: 325-327, 628-645

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/hooks/useKeyboardShortcuts.ts` around lines 294 - 296, The
cmd-tasks branch makes Ctrl+1-9 always go to task selection off macOS; update
the logic in useKeyboardShortcuts where useCmdForTasks/hasTaskModifier and
numberShortcutBehavior are evaluated so agent-number shortcuts remain reachable
on non-Mac. Specifically, change the non-Mac fallback for hasTaskModifier (and
the checks around lines referencing numberShortcutBehavior === 'cmd-tasks') to
use a distinct modifier (e.g., Alt or Shift) or gate swapping to isMac before
remapping, and ensure the handler order still allows onSelectAgentTab to run
when the agent modifier is pressed; adjust the conditions that call onSelectTask
vs onSelectAgentTab so they check the corrected modifier flags and
numberShortcutBehavior consistently.

Comment on lines +302 to +311
if (!hasTaskModifier || event.altKey || event.shiftKey) {
return null;
}

const key = normalizeShortcutKey(event.key);
if (!/^[1-9]$/.test(key)) {
return null;
}

return Number(key) - 1;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

On the web/Electron KeyboardEvent API, is KeyboardEvent.keylayout-dependent whileKeyboardEvent.codeforDigit1-Digit9 is layout-independent? How do shortcuts behave on AZERTY keyboards where digits require Shift?

💡 Result:

Yes.

  • KeyboardEvent.key is layout- (and modifier-) dependent: it reports the key value/character produced given the active layout and modifiers (e.g., Shift). (developer.mozilla.org)
  • KeyboardEvent.code is (mostly) layout-independent: it reports the physical key position (a stable identifier “unaffected by the current keyboard state”). (w3c.github.io)
  • For the top number row, codes like "Digit1""Digit9" refer to those physical keys (named after the US layout positions), even though the characters they produce vary by layout (the spec explicitly notes e.g. the "Digit2" key can produce "2" on US but "é" on French). (w3.org)

What happens on AZERTY (where digits typically require Shift)?

On (traditional) French AZERTY, pressing the physical "Digit1" key without Shift yields a symbol like &, and with Shift yields 1 (digits require Shift). (en.wikipedia.org)

So shortcuts differ depending on what you match:

  • If your shortcut logic is character-based (e.g. e.key === "1"), an AZERTY user generally must press Ctrl+Shift+Digit1 to make key be "1".
  • If your shortcut logic is physical-key-based (e.g. e.code === "Digit1"), an AZERTY user can press Ctrl+Digit1 (physically the same key as &/1), and it will match without requiring Shift, because code stays "Digit1" regardless of layout/modifiers. (w3c.github.io)

Citations:


🏁 Script executed:

cat -n src/renderer/hooks/useKeyboardShortcuts.ts | sed -n '300,345p'

Repository: generalaction/emdash

Length of output: 1683


🏁 Script executed:

# Search for the normalizeShortcutKey function
rg "normalizeShortcutKey" src/renderer/hooks/useKeyboardShortcuts.ts -A 5

Repository: generalaction/emdash

Length of output: 1531


Use KeyboardEvent.code instead of event.key for layout-independent digit key matching.

Both getTaskTabSelectionIndex (lines 302–311) and getAgentTabSelectionIndex (lines 333–342) reject event.shiftKey while matching on event.key. On keyboard layouts like AZERTY where digits require Shift (e.g., physical Digit1 produces & without Shift, 1 with Shift), these shortcuts become inaccessible from the number row. Use event.code === "Digit1" through event.code === "Digit9" instead to match the physical key position, which is layout-independent.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/hooks/useKeyboardShortcuts.ts` around lines 302 - 311, The
digit-row keyboard shortcut code in getTaskTabSelectionIndex and
getAgentTabSelectionIndex is layout-dependent because it uses event.key and
rejects event.shiftKey; replace that logic to use event.code for
layout-independent matching (check event.code matches /^Digit[1-9]$/), remove or
relax the event.shiftKey rejection so AZERTY/shifted digit keys work, and
convert the matched code (e.g., "Digit1") to a zero-based index (DigitN -> N-1)
to return the correct tab index.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants