Files
backnotprop__plannotator/packages/ui/components/KeyboardShortcuts.tsx
T
Michael Ramos 0fd9826c2c feat(review): all-files diff view with unified scrolling (#640)
* feat(review): all-files diff view with lazy mounting

Renders all diffs in a single scrollable pane using individual FileDiff
components with sticky file headers, collapse toggles, and viewport-based
lazy mounting via IntersectionObserver.

For provenance purposes, this commit was AI assisted.

* feat(review): convert all-files view to dock panel + fixes

- Register AllFilesDiffView as a proper dock panel so it coexists
  with single-file tabs, AI chat, PR panels
- Remove viewMode toggle in favor of dock panel lifecycle
- Fix annotation toolbar positioning (mouse tracking)
- Fix annotation PR/scope filtering for stacked PRs
- Fix stale patch on file switch (deferred toolbar open)
- Fix conventional comments boolean comparison
- Fix SuggestionModal props to match DiffViewer pattern
- Wire reviewBase for correct hunk expansion context
- Fix pre-existing CSS bug on .has-annotations background
- Add Shift+V/A keyboard shortcuts for viewed/stage toggle
- Add [/] keyboard shortcuts for file-to-file scrolling
- Add keyboard shortcuts to settings panel
- Restore scroll-based file tree highlight via dock panel state

For provenance purposes, this commit was AI assisted.

* feat(review): sidebar toggles, keyboard shortcuts, click-drag selection

- Move sidebar tab buttons (annotations/AI/agents) to header bar
- Generalize useSidebar hook for both plan and review editors
- Remove internal tab state from ReviewSidebar, receive from parent
- Add file tree toggle button at far-left of header
- Add click-and-drag line selection in diff content area
- Change keyboard shortcuts from Shift+V/A to bare v/a/c
  v = toggle viewed (+ collapse in all-files mode)
  a = toggle git add/stage
  c = collapse/expand file (all-files mode)
- Widen DiffOptionsPopover to prevent word-alt line break
- Fix Shift+V/A targeting wrong file in all-files mode
- Default to all-files panel on initial load
- Hide dock headers when all-files panel is the only panel

For provenance purposes, this commit was AI assisted.

* feat(review): click-and-drag line selection in diff content area

Extract shared selection helpers to utils/diffSelection.ts and add
mouseup handlers to both DiffViewer and AllFilesDiffView so users can
click and drag across code lines to open the annotation toolbar —
not just the gutter.

For provenance purposes, this commit was AI assisted.

* fix(review): address AI review findings

- Gate all-files keyboard shortcuts on isActive to prevent double-toggle
  when a single-file tab is focused
- Fix getDiffSelection to iterate all diffs-container elements so
  click-drag selection works on files beyond the first
- Clear needsInitialDiffPanel after opening all-files panel to prevent
  forced reactivation on diff switches
- Clear activeFilePath and collapsedFiles when files change to prevent
  stale state across PR/diff switches
- Add file.patch to LazyFileDiff fetch deps so file contents refetch
  when the diff context changes
- Add toggleCollapse to keyboard effect dependency array

For provenance purposes, this commit was AI assisted.

* feat(review): add z to undo collapse with scroll-back

Press z to reopen the last collapsed file and scroll back to it.
Maintains a history stack so repeated z walks back through collapses.
Skips already-expanded entries. History clears on diff/PR switch.

For provenance purposes, this commit was AI assisted.

* fix(review): review findings + sidebar toggles + folder icon

- Fix stale context memo: add isAllFilesActive + handleAddAnnotationForFile
  to useMemo deps
- Fix v handler: clear activeFilePath after collapse, only collapse when
  marking viewed ON (not when toggling OFF)
- Fix click-drag copy regression: only trigger annotation toolbar for
  multi-line selections, preserve single-line/word selection for Cmd+C
- Fix LazyFileDiff: clear stale file contents before refetch
- Clean up App.tsx v/a handler: early return when isAllFilesActive
- Add Cmd+B (file tree) and Cmd+. (sidebar) keyboard shortcuts
- Add toggle menu items with kbd badges to Options dropdown
- Replace file tree icon with simple folder
- Add z (undo collapse) to keyboard shortcuts panel

For provenance purposes, this commit was AI assisted.

* chore: remove dead code from iterations

- Remove unused FileTreeIcon import and component file
- Remove unused AnnotationsIcon component from ReviewHeaderMenu
- Remove unused onClose prop and close button from FileTree

For provenance purposes, this commit was AI assisted.

* fix(review): gate v/a on diff panel, keep annotation selection in all-files

- Track isDiffPanelActive so v/a shortcuts only fire when viewing a
  single-file diff, not PR Comments/Summary/Agent panels
- Annotation selection stays in all-files mode instead of switching to
  a single-file tab — lets the scroll-to-annotation effect handle it

For provenance purposes, this commit was AI assisted.
2026-04-30 23:39:43 -07:00

172 lines
5.8 KiB
TypeScript

import React from 'react';
import { isMac, modKey, altKey } from '../utils/platform';
/* ─── Key cap component ─── */
const Kbd: React.FC<{ children: React.ReactNode; wide?: boolean }> = ({ children, wide }) => (
<kbd
className={`inline-flex items-center justify-center h-[22px] ${
wide ? 'min-w-[22px] px-1.5' : 'min-w-[22px]'
} rounded bg-muted border border-border/60 border-b-[2px] text-[11px] font-mono leading-none text-foreground/80 shadow-sm`}
>
{children}
</kbd>
);
/* ─── Key combo renderer ─── */
const Keys: React.FC<{ keys: string[] }> = ({ keys }) => (
<span className="inline-flex items-center gap-0.5">
{keys.map((k, i) => (
<Kbd key={i} wide={k.length > 1}>{k}</Kbd>
))}
</span>
);
/* ─── Shortcut row ─── */
const ShortcutRow: React.FC<{ keys: string[]; desc: string; hint?: string }> = ({ keys, desc, hint }) => (
<div className="flex items-center justify-between py-1">
<span className="text-xs text-muted-foreground">
{desc}
{hint && (
<span className="relative group ml-1 inline-flex">
<span className="inline-flex items-center justify-center w-3.5 h-3.5 rounded-full text-[9px] font-medium bg-muted-foreground/15 text-muted-foreground/60 cursor-default">?</span>
<span className="absolute bottom-full left-0 mb-1.5 px-2.5 py-1.5 rounded bg-foreground text-background text-[11px] leading-snug w-[320px] opacity-0 pointer-events-none group-hover:opacity-100 transition-opacity shadow-lg z-50">
{hint}
</span>
</span>
)}
</span>
<Keys keys={keys} />
</div>
);
/* ─── Section ─── */
const Section: React.FC<{ title: string; children: React.ReactNode }> = ({ title, children }) => (
<div className="space-y-0.5">
<div className="text-[10px] font-semibold uppercase tracking-widest text-muted-foreground/60 mb-1.5">
{title}
</div>
{children}
</div>
);
/* ─── Platform-aware key names ─── */
const enter = isMac ? '⏎' : '↵';
/* ─── Shortcut data ─── */
interface Shortcut {
keys: string[];
desc: string;
hint?: string;
}
interface ShortcutSection {
title: string;
shortcuts: Shortcut[];
}
const planShortcuts: ShortcutSection[] = [
{
title: 'Actions',
shortcuts: [
{ keys: [modKey, enter], desc: 'Submit / Approve' },
{ keys: [modKey, 'S'], desc: 'Save to notes app' },
{ keys: [modKey, 'P'], desc: 'Print plan' },
{ keys: ['Esc'], desc: 'Close dialog' },
],
},
{
title: 'Input Method',
shortcuts: [
{ keys: [altKey, 'hold'], desc: 'Temporarily switch mode', hint: 'Hold to switch between Select and Pinpoint, release to revert' },
{ keys: [altKey, altKey], desc: 'Toggle mode', hint: 'Double-tap to permanently switch between Select and Pinpoint' },
],
},
{
title: 'Annotations',
shortcuts: [
{ keys: ['a-z'], desc: 'Start typing comment', hint: 'When the annotation toolbar is open, any letter key opens the comment editor with that character' },
{ keys: [altKey, '1-0'], desc: 'Apply quick label', hint: 'Instantly applies the Nth preset label (0 = 10th). When the label picker is open, bare digits also work.' },
{ keys: [modKey, enter], desc: 'Submit comment' },
{ keys: [modKey, 'C'], desc: 'Copy selected text' },
{ keys: ['Esc'], desc: 'Close toolbar / Cancel' },
],
},
{
title: 'Image Annotator',
shortcuts: [
{ keys: ['1'], desc: 'Pen tool' },
{ keys: ['2'], desc: 'Arrow tool' },
{ keys: ['3'], desc: 'Circle tool' },
{ keys: [modKey, 'Z'], desc: 'Undo' },
{ keys: [enter], desc: 'Finish' },
{ keys: ['Esc'], desc: 'Cancel' },
],
},
];
const reviewShortcuts: ShortcutSection[] = [
{
title: 'Actions',
shortcuts: [
{ keys: [modKey, enter], desc: 'Approve / Send feedback' },
{ keys: [altKey, altKey], desc: 'Toggle destination', hint: 'Double-tap to switch between GitHub and Agent in PR review mode' },
{ keys: [modKey, '⇧', 'C'], desc: 'Toggle comment mode' },
{ keys: [modKey, 'B'], desc: 'Toggle file tree' },
{ keys: [modKey, '.'], desc: 'Toggle sidebar' },
{ keys: ['Esc'], desc: 'Collapse sidebar' },
],
},
{
title: 'File Navigation',
shortcuts: [
{ keys: ['J'], desc: 'Next file' },
{ keys: ['K'], desc: 'Previous file' },
{ keys: ['['], desc: 'Scroll to previous file', hint: 'In all-files view, scrolls the viewport to the previous file' },
{ keys: [']'], desc: 'Scroll to next file', hint: 'In all-files view, scrolls the viewport to the next file' },
{ keys: ['Home'], desc: 'First file' },
{ keys: ['End'], desc: 'Last file' },
],
},
{
title: 'File Actions',
shortcuts: [
{ keys: ['V'], desc: 'Toggle viewed', hint: 'In all-files view, also collapses the file' },
{ keys: ['A'], desc: 'Toggle git add / stage' },
{ keys: ['C'], desc: 'Collapse / expand file', hint: 'In all-files view, toggles the focused file' },
{ keys: ['Z'], desc: 'Undo collapse', hint: 'Reopens the last collapsed file and scrolls to it' },
],
},
{
title: 'Annotations',
shortcuts: [
{ keys: [modKey, enter], desc: 'Submit comment' },
{ keys: ['Tab'], desc: 'Indent in editor' },
{ keys: ['Esc'], desc: 'Close toolbar / Cancel' },
],
},
];
/* ─── Exported panel ─── */
export const KeyboardShortcuts: React.FC<{ mode: 'plan' | 'review' }> = ({ mode }) => {
const sections = mode === 'review' ? reviewShortcuts : planShortcuts;
return (
<div className="space-y-4">
{sections.map((section) => (
<Section key={section.title} title={section.title}>
{section.shortcuts.map((s, i) => (
<ShortcutRow key={i} keys={s.keys} desc={s.desc} hint={s.hint} />
))}
</Section>
))}
</div>
);
};