Skip to content

Add diff panel keyboard toggle and shortcut hint#93

Merged
juliusmarminge merged 4 commits intomainfrom
codething/62a0cc86
Feb 27, 2026
Merged

Add diff panel keyboard toggle and shortcut hint#93
juliusmarminge merged 4 commits intomainfrom
codething/62a0cc86

Conversation

@juliusmarminge
Copy link
Copy Markdown
Member

@juliusmarminge juliusmarminge commented Feb 27, 2026

Summary

  • Add a new diff.toggle keybinding command and include it in shared keybinding contracts.
  • Bind mod+d to toggle the diff panel when terminal is not focused (while keeping terminal split on mod+d when terminal is focused).
  • Handle diff.toggle in ChatView keyboard dispatch and surface its shortcut as a tooltip on the diff toggle button.
  • Export isDiffToggleShortcut in web keybinding helpers and add coverage for command matching/label formatting.
  • Restrict the sidebar keyboard shortcut handler to mobile so desktop mod+d is not intercepted.

Testing

  • Added unit tests in apps/web/src/keybindings.test.ts for:
  • shortcutLabelForCommand(..., "diff.toggle") label resolution (Ctrl+D on Linux).
  • isDiffToggleShortcut matching outside terminal focus and not matching inside terminal focus.
  • Added contract test in packages/contracts/src/keybindings.test.ts to verify diff.toggle parses as a valid keybinding command.
  • Not run: full project lint/test suites in this PR context.

Note

Medium Risk
Updates shared keybinding contracts and default bindings, plus global keyboard handling in ChatView, which could introduce shortcut conflicts or regressions in key dispatch/navigation behavior.

Overview
Adds a new diff.toggle keybinding command to the shared contracts and server defaults, binding mod+d to toggle the diff panel when !terminalFocus (while keeping mod+d for terminal.split when focused).

Updates ChatView to handle the diff.toggle command in the global keydown dispatcher and to show a tooltip on the diff toggle button that includes the resolved shortcut label.

Removes the sidebar’s global keyboard shortcut handler, and adds unit tests covering diff.toggle parsing, command matching, and shortcut label formatting.

Written by Cursor Bugbot for commit 9a2fc6f. This will update automatically on new commits. Configure here.

Note

Add mod+d keybinding to emit diff.toggle and show a tooltip with the shortcut in ChatHeader for toggling the diff panel

Introduce diff.toggle as a recognized command, bind mod+d when !terminalFocus, handle the shortcut in ChatView, and display a tooltip with the shortcut label in ChatHeader; remove global mod+b sidebar toggle.

📍Where to Start

Start with the global keybinding handling and shortcut label in ChatView in ChatView.tsx.

Macroscope summarized 9a2fc6f.

Summary by CodeRabbit

  • New Features

    • Added a global keyboard shortcut (Ctrl/Cmd+D) to toggle the diff panel when not focused on the terminal.
    • Diff toggle shortcut label now appears in the chat header UI and is usable via keyboard command.
  • Improvements

    • Sidebar keyboard shortcut now only activates on mobile and reattaches when breakpoint changes.
  • Tests

    • Added tests covering diff toggle shortcut labeling and matching behavior.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Feb 27, 2026

Walkthrough

Adds a new diff.toggle command with a default mod+d binding active when not in terminal focus, exposes isDiffToggleShortcut, updates contracts and server defaults, wires shortcut label and toggle handler into ChatView/ChatHeader, adds tests, and guards a sidebar shortcut to run only on mobile.

Changes

Cohort / File(s) Summary
Keybinding Contract & Tests
packages/contracts/src/keybindings.ts, packages/contracts/src/keybindings.test.ts
Added "diff.toggle" to STATIC_KEYBINDING_COMMANDS and added a parser test asserting a mod+ddiff.toggle binding.
Server Default Bindings
apps/server/src/keybindings.ts
Added a default keybinding mapping mod+d to diff.toggle with condition terminalFocus: false (global/non-terminal context).
Web Keybinding API & Tests
apps/web/src/keybindings.ts, apps/web/src/keybindings.test.ts
Exported isDiffToggleShortcut(...) that delegates to matchesCommandShortcut, and added tests for label resolution and context-sensitive matching (ensuring it matches only when terminalFocus is false).
Chat UI Integration
apps/web/src/components/ChatView.tsx
Computed diffPanelShortcutLabel from keybindings, added shared onToggleDiff handler that updates route search to toggle the diff panel, wired keyboard command handling to call it, and passed diffToggleShortcutLabel into ChatHeader (prop added to ChatHeaderProps).
Sidebar Shortcut Guard
apps/web/src/components/ui/sidebar.tsx
Added isMobile guard to the keyboard shortcut handler and included isMobile in the effect dependency array to reattach handler on breakpoint changes.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% 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
Title check ✅ Passed The title clearly and concisely describes the main change: adding keyboard toggle and shortcut hint for the diff panel.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

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

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch codething/62a0cc86

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

Copy link
Copy Markdown
Contributor

@cursor cursor bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Autofix Details

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Desktop sidebar toggle shortcut unnecessarily broken
    • Removed the erroneous if (!isMobile) return guard that disabled the Cmd/Ctrl+B sidebar toggle shortcut on desktop, since the shortcut key 'b' never conflicts with the 'mod+d' diff toggle.

Create PR

Or push these changes by commenting:

@cursor push 919f3b34a0
Preview (919f3b34a0)
diff --git a/apps/web/src/components/ui/sidebar.tsx b/apps/web/src/components/ui/sidebar.tsx
--- a/apps/web/src/components/ui/sidebar.tsx
+++ b/apps/web/src/components/ui/sidebar.tsx
@@ -132,7 +132,6 @@
   // Adds a keyboard shortcut to toggle the sidebar.
   React.useEffect(() => {
     const handleKeyDown = (event: KeyboardEvent) => {
-      if (!isMobile) return;
       if (event.key === SIDEBAR_KEYBOARD_SHORTCUT && (event.metaKey || event.ctrlKey)) {
         event.preventDefault();
         toggleSidebar();
@@ -141,7 +140,7 @@
 
     window.addEventListener("keydown", handleKeyDown);
     return () => window.removeEventListener("keydown", handleKeyDown);
-  }, [isMobile, toggleSidebar]);
+  }, [toggleSidebar]);
 
   // We add a state so that we can do data-state="expanded" or "collapsed".
   // This makes it easier to style the sidebar with Tailwind classes.

Comment thread apps/web/src/components/ui/sidebar.tsx Outdated
// Adds a keyboard shortcut to toggle the sidebar.
React.useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if (!isMobile) return;
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.

Desktop sidebar toggle shortcut unnecessarily broken

High Severity

The early return if (!isMobile) return; disables the sidebar keyboard shortcut (Cmd/Ctrl+B) on desktop. The PR description states this prevents desktop mod+d from being intercepted, but SIDEBAR_KEYBOARD_SHORTCUT is "b", not "d" — the sidebar handler would never intercept mod+d. This is a regression that removes a working desktop feature for no benefit.

Fix in Cursor Fix in Web

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

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

Inline comments:
In `@apps/web/src/components/ChatView.tsx`:
- Around line 622-631: The onToggleDiff handler uses the captured diffOpen
variable which can become stale on rapid toggles; inside the navigate call use
the search callback's previous value to decide current diff state instead of the
outer diffOpen. In the search: (previous) => { const rest =
stripDiffSearchParams(previous); const currentlyOpen =
Boolean(getDiffParamFrom(previous) /*or check previous.diff*/); return
currentlyOpen ? rest : { ...rest, diff: "1" }; } so replace references to
diffOpen with a computation based on previous (use existing
stripDiffSearchParams / parse previous.search for the diff param) in the
onToggleDiff function to ensure reliable toggling.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5748a99 and 68e4bf5.

📒 Files selected for processing (7)
  • apps/server/src/keybindings.ts
  • apps/web/src/components/ChatView.tsx
  • apps/web/src/components/ui/sidebar.tsx
  • apps/web/src/keybindings.test.ts
  • apps/web/src/keybindings.ts
  • packages/contracts/src/keybindings.test.ts
  • packages/contracts/src/keybindings.ts

Comment on lines +622 to +631
const onToggleDiff = useCallback(() => {
void navigate({
to: "/$threadId",
params: { threadId },
search: (previous) => {
const rest = stripDiffSearchParams(previous);
return diffOpen ? rest : { ...rest, diff: "1" };
},
});
}, [diffOpen, navigate, threadId]);
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 | 🟡 Minor

Use previous search state for diff toggle decisions.

Line [628] relies on captured diffOpen, which can produce missed toggles on rapid repeated shortcut/button invocations.

Proposed fix
   const onToggleDiff = useCallback(() => {
     void navigate({
       to: "/$threadId",
       params: { threadId },
       search: (previous) => {
         const rest = stripDiffSearchParams(previous);
-        return diffOpen ? rest : { ...rest, diff: "1" };
+        const previousDiffOpen =
+          parseDiffRouteSearch(previous as Record<string, unknown>).diff === "1";
+        return previousDiffOpen ? rest : { ...rest, diff: "1" };
       },
     });
-  }, [diffOpen, navigate, threadId]);
+  }, [navigate, threadId]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/components/ChatView.tsx` around lines 622 - 631, The
onToggleDiff handler uses the captured diffOpen variable which can become stale
on rapid toggles; inside the navigate call use the search callback's previous
value to decide current diff state instead of the outer diffOpen. In the search:
(previous) => { const rest = stripDiffSearchParams(previous); const
currentlyOpen = Boolean(getDiffParamFrom(previous) /*or check previous.diff*/);
return currentlyOpen ? rest : { ...rest, diff: "1" }; } so replace references to
diffOpen with a computation based on previous (use existing
stripDiffSearchParams / parse previous.search for the diff param) in the
onToggleDiff function to ensure reliable toggling.

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.

♻️ Duplicate comments (1)
apps/web/src/components/ChatView.tsx (1)

622-632: ⚠️ Potential issue | 🟡 Minor

Avoid stale diffOpen capture in onToggleDiff.

Line [629] bases toggle behavior on the outer diffOpen, which can be stale under rapid repeated toggles. Compute from previous inside the search updater instead.

Proposed fix
   const onToggleDiff = useCallback(() => {
     void navigate({
       to: "/$threadId",
       params: { threadId },
       replace: true,
       search: (previous) => {
         const rest = stripDiffSearchParams(previous);
-        return diffOpen ? rest : { ...rest, diff: "1" };
+        const previousDiffOpen =
+          parseDiffRouteSearch(previous as Record<string, unknown>).diff === "1";
+        return previousDiffOpen ? rest : { ...rest, diff: "1" };
       },
     });
-  }, [diffOpen, navigate, threadId]);
+  }, [navigate, threadId]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/components/ChatView.tsx` around lines 622 - 632, The
onToggleDiff callback captures outer diffOpen which can be stale; instead derive
whether diff is open from the previous search params inside the search updater
passed to navigate. Modify onToggleDiff (the useCallback that calls navigate
with search: (previous) => { ... }) to inspect previous (use
stripDiffSearchParams or parse previous.search/diff param) to decide whether to
include diff: "1" or not, rather than referencing the outer diffOpen variable;
keep threadId and replace behavior unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@apps/web/src/components/ChatView.tsx`:
- Around line 622-632: The onToggleDiff callback captures outer diffOpen which
can be stale; instead derive whether diff is open from the previous search
params inside the search updater passed to navigate. Modify onToggleDiff (the
useCallback that calls navigate with search: (previous) => { ... }) to inspect
previous (use stripDiffSearchParams or parse previous.search/diff param) to
decide whether to include diff: "1" or not, rather than referencing the outer
diffOpen variable; keep threadId and replace behavior unchanged.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 68e4bf5 and f935173.

📒 Files selected for processing (1)
  • apps/web/src/components/ChatView.tsx

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.

🧹 Nitpick comments (1)
apps/web/src/components/ChatView.tsx (1)

2116-2136: Consider using Popover for interactive element tooltip.

Per the coding guidelines, Tooltip should not be used when the trigger is an interactive element like a button. The Toggle here is interactive, and the guideline recommends using Popover with tooltipStyle instead.

That said, since the Toggle already has aria-label="Toggle diff panel" for screen-reader accessibility and the tooltip is purely supplementary (showing the shortcut hint), this is not a blocker. Based on learnings: "For Tooltip vs Popover in Base UI: NEVER use Tooltip when the trigger is an interactive element... Use Popover with tooltipStyle instead."

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

In `@apps/web/src/components/ChatView.tsx` around lines 2116 - 2136, The Tooltip
is being used with an interactive trigger (the Toggle component); replace the
Tooltip wrapper with a Popover configured for tooltip behavior: use Popover
(instead of Tooltip) with a PopoverTrigger that renders the existing Toggle
(preserving props: className="shrink-0", pressed={diffOpen},
onPressedChange={onToggleDiff}, aria-label="Toggle diff panel",
variant="outline", size="xs" and the DiffIcon child) and a PopoverContent (or
Popup) styled via tooltipStyle to show the same text computed from
diffToggleShortcutLabel (`Toggle diff panel (${diffToggleShortcutLabel})` or
fallback). Keep the accessibility label on Toggle and ensure the popover is
positioned side="bottom" and behaves as a non-focus-trapping tooltip.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@apps/web/src/components/ChatView.tsx`:
- Around line 2116-2136: The Tooltip is being used with an interactive trigger
(the Toggle component); replace the Tooltip wrapper with a Popover configured
for tooltip behavior: use Popover (instead of Tooltip) with a PopoverTrigger
that renders the existing Toggle (preserving props: className="shrink-0",
pressed={diffOpen}, onPressedChange={onToggleDiff}, aria-label="Toggle diff
panel", variant="outline", size="xs" and the DiffIcon child) and a
PopoverContent (or Popup) styled via tooltipStyle to show the same text computed
from diffToggleShortcutLabel (`Toggle diff panel (${diffToggleShortcutLabel})`
or fallback). Keep the accessibility label on Toggle and ensure the popover is
positioned side="bottom" and behaves as a non-focus-trapping tooltip.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f935173 and 6d9ad65.

📒 Files selected for processing (1)
  • apps/web/src/components/ChatView.tsx

Copy link
Copy Markdown
Contributor

@cursor cursor bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Added replace: true silently changes existing toggle button behavior
    • Added replace: true to closeDiff, openDiff, and onOpenTurnDiff navigate calls to make all diff-related navigation consistent with onToggleDiff, eliminating the inconsistent browser history behavior.

Create PR

Or push these changes by commenting:

@cursor push 1c78f72156
Preview (1c78f72156)
diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx
--- a/apps/web/src/components/ChatView.tsx
+++ b/apps/web/src/components/ChatView.tsx
@@ -1641,6 +1641,7 @@
       void navigate({
         to: "/$threadId",
         params: { threadId },
+        replace: true,
         search: (previous) => {
           const rest = stripDiffSearchParams(previous);
           return filePath

diff --git a/apps/web/src/routes/_chat.$threadId.tsx b/apps/web/src/routes/_chat.$threadId.tsx
--- a/apps/web/src/routes/_chat.$threadId.tsx
+++ b/apps/web/src/routes/_chat.$threadId.tsx
@@ -145,6 +145,7 @@
     void navigate({
       to: "/$threadId",
       params: { threadId },
+      replace: true,
       search: (previous) => {
         return stripDiffSearchParams(previous);
       },
@@ -154,6 +155,7 @@
     void navigate({
       to: "/$threadId",
       params: { threadId },
+      replace: true,
       search: (previous) => {
         const rest = stripDiffSearchParams(previous);
         return { ...rest, diff: "1" };

return diffOpen ? rest : { ...rest, diff: "1" };
},
});
}, [diffOpen, navigate, threadId]);
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.

Added replace: true silently changes existing toggle button behavior

Low Severity

The refactored onToggleDiff adds replace: true to the navigate call, which was absent from the original. This changes the existing diff toggle button's behavior — it now replaces the current history entry rather than pushing a new one. This means users can no longer use browser back to undo a toggle. It also creates an inconsistency with closeDiff and onOpenTurnDiff, which don't use replace: true, leading to inconsistent history behavior depending on how the diff panel is opened or closed.

Fix in Cursor Fix in Web

- Bind `mod+d` to `diff.toggle` when terminal is not focused
- Wire ChatView to handle `diff.toggle` and show its shortcut in the diff button tooltip
- Limit sidebar `mod+b` listener to mobile to avoid desktop shortcut conflicts
- Extend web/contracts keybinding tests and command list for `diff.toggle`
- Add `replace: true` to thread navigation in `ChatView`
- Prevents extra browser history entries when opening/closing diff mode
- Wrap diff panel toggle in shared tooltip components
- Display shortcut hint in tooltip popup instead of title attribute
- Remove global keydown listener from `SidebarProvider`
- Keep sidebar toggling to explicit UI interactions only
@juliusmarminge juliusmarminge merged commit 1245ce9 into main Feb 27, 2026
3 of 4 checks passed
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.

1 participant