Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 27 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,13 @@ plannotator/
│ │ │ ├── icons/ # Shared SVG icon components (themeIcons, etc.)
│ │ │ ├── plan-diff/ # PlanDiffBadge, PlanDiffViewer, clean/raw diff views
│ │ │ └── sidebar/ # SidebarContainer, SidebarTabs, VersionBrowser, ArchiveBrowser
│ │ ├── shortcuts/ # Keyboard shortcut registry (see Keyboard Shortcuts section below)
│ │ │ ├── core.ts # Engine: parser, formatter, dispatcher, validator
│ │ │ ├── runtime.ts # Engine: useShortcutScope, useDoubleTapShortcuts hooks
│ │ │ ├── index.ts # Barrel — re-exports engine + scopes from both subfolders
│ │ │ ├── plan-review/ # Scopes for plan-editor surfaces (annotationToolbar, annotationPanel, commentPopover, imageAnnotator, inputMethod, viewer)
│ │ │ └── code-review/ # Scopes for review-editor surfaces (ai, allFilesDiff, annotationToolbar, fileTree, prComments, suggestionModal, tourDialog)
│ │ ├── shortcuts.test.ts # Registry unit tests (parser, dispatcher, validator)
│ │ ├── utils/ # parser.ts, sharing.ts, storage.ts, planSave.ts, agentSwitch.ts, planDiffEngine.ts, planAgentInstructions.ts
│ │ ├── hooks/ # useAnnotationHighlighter.ts, useSharing.ts, usePlanDiff.ts, useSidebar.ts, useLinkedDoc.ts, useAnnotationDraft.ts, useCodeAnnotationDraft.ts, useArchive.ts
│ │ └── types.ts
Expand All @@ -60,9 +67,12 @@ plannotator/
│ │ ├── storage.ts # Plan saving, version history, archive listing (node:fs only)
│ │ ├── draft.ts # Annotation draft persistence (node:fs only)
│ │ └── project.ts # Pure string helpers (sanitizeTag, extractRepoName, extractDirName)
│ ├── editor/ # Plan review App.tsx
│ ├── editor/ # Plan review app
│ │ ├── App.tsx # Main plan review app
│ │ └── shortcuts.ts # planReviewSurface + annotateSurface — composes plan-review scopes into per-surface registries
│ └── review-editor/ # Code review UI
│ ├── App.tsx # Main review app
│ ├── shortcuts.ts # codeReviewSurface — composes code-review scopes into the review registry
│ ├── components/ # DiffViewer, FileTree, ReviewSidebar
│ ├── dock/ # Dockview center panel infrastructure
│ ├── demoData.ts # Demo diff for standalone mode
Expand Down Expand Up @@ -380,6 +390,20 @@ interface Block {

Text highlighting uses `web-highlighter` library. Code blocks use manual `<mark>` wrapping (web-highlighter can't select inside `<pre>`).

## Keyboard Shortcuts

**Location:** `packages/ui/shortcuts/` (engine + scope data), `packages/editor/shortcuts.ts` and `packages/review-editor/shortcuts.ts` (per-app surfaces).

The shortcut system has three layers:

1. **Engine** (`packages/ui/shortcuts/{core,runtime}.ts`) — parser for declarative bindings (`Mod+Enter`, `Alt Alt` double-tap, `Alt hold`), dispatcher, platform-aware formatter (mac glyphs vs. `Ctrl`), validator, and the `useShortcutScope` / `useDoubleTapShortcuts` React hooks. Truly shared — both apps use it as-is.
2. **Scopes** — `defineShortcutScope({ id, title, shortcuts: { actionId: { bindings, description, section, ... } } })`. One scope per UI surface (annotation toolbar, comment popover, file tree, etc.). Lives in `packages/ui/shortcuts/{plan-review,code-review}/` — **the subfolder names which app's UI the scope serves**. Components/Apps wire handlers to a scope via `useShortcutScope({ scope, handlers: { actionId: () => ... } })`.
3. **Surfaces** (`packages/editor/shortcuts.ts`, `packages/review-editor/shortcuts.ts`) — each app composes its scopes into a `ShortcutSurface` (`planReviewSurface`, `annotateSurface`, `codeReviewSurface`). Surfaces feed both the in-app help modal and the marketing site's auto-generated docs page.

**Convention for adding new shortcuts:** define the action in the relevant scope file under the right subfolder (`plan-review/` or `code-review/`), declare the binding(s) and description, then wire a handler at the call site with `useShortcutScope`. The marketing docs page picks it up automatically at next build. Unit tests in `packages/ui/shortcuts.test.ts` enforce normalized binding tokens (`Mod`, `Shift`, `Alt`, `A-Z`, `1-0`, named keys, `F1`–`F12`) and unique scope ids.

**Marketing docs auto-generation:** `apps/marketing/src/lib/shortcutReference.ts` reads the three surfaces and `apps/marketing/src/components/ShortcutReference.astro` renders them as tables. The `/docs/reference/keyboard-shortcuts` page is special-cased in `apps/marketing/src/pages/docs/[...slug].astro` to render the component instead of the markdown body.

## URL Sharing

**Location:** `packages/ui/utils/sharing.ts`, `packages/ui/hooks/useSharing.ts`
Expand Down Expand Up @@ -482,6 +506,8 @@ Running only `build:opencode` will copy stale HTML files.

`apps/marketing/` is the plannotator.ai website — landing page, documentation, and blog. Built with Astro 5 (static output, zero client JS except a theme toggle island). Docs are markdown files in `src/content/docs/`, blog posts in `src/content/blog/`, both using Astro content collections. Tailwind CSS v4 via `@tailwindcss/vite`. Deploys to S3/CloudFront via GitHub Actions on push to main.

The `/docs/reference/keyboard-shortcuts` page is auto-generated from the shortcut registry at build time — see the Keyboard Shortcuts section above. Editing the markdown body has no effect; update the scope files instead.

## Test plugin locally

```
Expand Down
37 changes: 37 additions & 0 deletions apps/marketing/src/components/ShortcutReference.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
---
import { shortcutReferenceSurfaces } from '../lib/shortcutReference';
import { formatShortcutBindingsText } from '../../../../packages/ui/shortcuts';
---

<p>Keyboard shortcuts available across the Plannotator review and annotation UIs.</p>

{shortcutReferenceSurfaces.map((surface) => (
<Fragment>
<h2 id={surface.slug}>{surface.title}</h2>
<p>{surface.description}</p>

{surface.sections.map((section) => (
<Fragment>
<h3 id={section.slug}>{section.title}</h3>
<table>
<thead>
<tr>
<th>Shortcut</th>
<th>Action</th>
<th>Notes</th>
</tr>
</thead>
<tbody>
{section.shortcuts.map((shortcut) => (
<tr>
<td><code>{formatShortcutBindingsText(shortcut.bindings)}</code></td>
<td>{shortcut.description}</td>
<td>{shortcut.hint ?? '—'}</td>
</tr>
))}
</tbody>
</table>
</Fragment>
))}
</Fragment>
))}
26 changes: 9 additions & 17 deletions apps/marketing/src/content/docs/reference/keyboard-shortcuts.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,14 @@ sidebar:
section: "Reference"
---

Keyboard shortcuts available in the Plannotator plan review, code review, and annotation UIs.
<!--
This page is auto-generated from the shortcut registry at build time.
Editing the body below has no effect — the slug page renders
`apps/marketing/src/components/ShortcutReference.astro` instead.

## Global shortcuts
To change a shortcut, edit the relevant scope file under
`packages/ui/shortcuts/plan-review/` or `packages/ui/shortcuts/code-review/`,
or a per-app surface in `packages/{editor,review-editor}/shortcuts.ts`.
-->

| Shortcut | Context | Action |
|----------|---------|--------|
| `Cmd/Ctrl+Enter` | Plan review (no annotations) | Approve plan |
| `Cmd/Ctrl+Enter` | Plan review (with annotations) | Send feedback |
| `Cmd/Ctrl+Enter` | Code review | Send feedback / Approve |
| `Cmd/Ctrl+Enter` | Annotate mode | Send annotations |
| `Cmd/Ctrl+S` | Any mode (with API) | Quick save to default notes app |
| `Escape` | Annotation toolbar | Close toolbar |

## Notes

- `Cmd/Ctrl+Enter` is blocked when a modal or dialog is open (export, import, confirm dialogs, image annotator)
- `Cmd/Ctrl+Enter` is blocked when typing in an input or textarea
- `Cmd/Ctrl+S` opens the Export modal if no default notes app is configured
- `Escape` in the annotation toolbar closes it without creating an annotation
This page is generated from the shared shortcut registry at build time.
21 changes: 21 additions & 0 deletions apps/marketing/src/lib/shortcutReference.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { planReviewSurface, annotateSurface } from '../../../../packages/editor/shortcuts';
import { codeReviewSurface } from '../../../../packages/review-editor/shortcuts';
import { listRegistryShortcutSections } from '../../../../packages/ui/shortcuts';
import type { ShortcutSurface } from '../../../../packages/ui/shortcuts';

const slugify = (value: string) => value.toLowerCase().replace(/\s+/g, '-');

const allSurfaces: ShortcutSurface[] = [planReviewSurface, annotateSurface, codeReviewSurface];

export const shortcutReferenceSurfaces = allSurfaces.map((surface) => ({
...surface,
sections: listRegistryShortcutSections(surface.registry).map((section) => ({
...section,
slug: `${surface.slug}-${slugify(section.title)}`,
})),
}));

export const shortcutReferenceHeadings = shortcutReferenceSurfaces.flatMap((surface) => [
{ depth: 2 as const, slug: surface.slug, text: surface.title },
...surface.sections.map((section) => ({ depth: 3 as const, slug: section.slug, text: section.title })),
]);
11 changes: 9 additions & 2 deletions apps/marketing/src/pages/docs/[...slug].astro
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
---
import { getCollection, render } from 'astro:content';
import Docs from '../../layouts/Docs.astro';
import ShortcutReference from '../../components/ShortcutReference.astro';
import { shortcutReferenceHeadings } from '../../lib/shortcutReference';

export async function getStaticPaths() {
const docs = await getCollection('docs');
Expand All @@ -11,13 +13,18 @@ export async function getStaticPaths() {
}

const { doc } = Astro.props;
const { Content, headings } = await render(doc);
const isShortcutReference = doc.id === 'reference/keyboard-shortcuts';
const rendered = isShortcutReference ? null : await render(doc);
const Content = rendered?.Content;
const headings = isShortcutReference
? shortcutReferenceHeadings
: rendered?.headings ?? [];
---
<Docs
title={doc.data.title}
description={doc.data.description}
headings={headings}
currentId={doc.id}
>
<Content />
{isShortcutReference ? <ShortcutReference /> : Content && <Content />}
</Docs>
6 changes: 3 additions & 3 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion packages/editor/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
"type": "module",
"exports": {
".": "./App.tsx",
"./styles": "./index.css"
"./styles": "./index.css",
"./shortcuts": "./shortcuts.ts"
},
"dependencies": {
"@plannotator/shared": "workspace:*",
Expand Down
109 changes: 109 additions & 0 deletions packages/editor/shortcuts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import {
annotationPanelShortcuts,
annotationToolbarShortcuts,
commentPopoverShortcuts,
createShortcutRegistry,
createShortcutScopeHook,
defineShortcutScope,
imageAnnotatorShortcuts,
inputMethodShortcuts,
viewerShortcuts,
type ShortcutSurface,
} from '@plannotator/ui/shortcuts';

export const planEditorShortcuts = defineShortcutScope({
id: 'plan-editor',
title: 'Plan Editor',
shortcuts: {
submitPlan: {
description: 'Approve / Send feedback',
bindings: ['Mod+Enter'],
section: 'Actions',
hint: 'Approves when there are no annotations and sends feedback when there are.',
displayOrder: 10,
},
submitAnnotations: {
description: 'Send annotations',
bindings: ['Mod+Enter'],
section: 'Actions',
displayOrder: 10,
},
quickSave: {
description: 'Save to notes app',
bindings: ['Mod+S'],
section: 'Actions',
hint: 'Opens Export if no default notes app is configured.',
displayOrder: 20,
},
exitPlanDiff: {
description: 'Close diff view',
bindings: ['Escape'],
section: 'Actions',
hint: 'Available while plan diff is open.',
displayOrder: 30,
},
printPlan: {
description: 'Print',
bindings: ['Mod+P'],
section: 'Actions',
hint: 'Opens the browser print dialog for the current document.',
displayOrder: 40,
},
},
});

export const usePlanEditorShortcuts = createShortcutScopeHook(planEditorShortcuts);

const planReviewEditorSettingsShortcuts = defineShortcutScope({
id: 'plan-review-editor-settings',
title: 'Plan Editor',
shortcuts: {
submitPlan: planEditorShortcuts.shortcuts.submitPlan,
quickSave: planEditorShortcuts.shortcuts.quickSave,
exitPlanDiff: planEditorShortcuts.shortcuts.exitPlanDiff,
printPlan: planEditorShortcuts.shortcuts.printPlan,
},
});

const annotateEditorSettingsShortcuts = defineShortcutScope({
id: 'annotate-editor-settings',
title: 'Annotate Editor',
shortcuts: {
submitAnnotations: planEditorShortcuts.shortcuts.submitAnnotations,
quickSave: planEditorShortcuts.shortcuts.quickSave,
printPlan: planEditorShortcuts.shortcuts.printPlan,
},
});

const sharedPlanSurfaceShortcuts = [
inputMethodShortcuts,
annotationToolbarShortcuts,
viewerShortcuts,
commentPopoverShortcuts,
annotationPanelShortcuts,
imageAnnotatorShortcuts,
] as const;

export const planReviewSettingsShortcutRegistry = createShortcutRegistry([
planReviewEditorSettingsShortcuts,
...sharedPlanSurfaceShortcuts,
] as const);

export const annotateSettingsShortcutRegistry = createShortcutRegistry([
annotateEditorSettingsShortcuts,
...sharedPlanSurfaceShortcuts,
] as const);

export const planReviewSurface: ShortcutSurface = {
slug: 'plan-review',
title: 'Plan review',
description: 'Shortcuts surfaced by the plan review UI.',
registry: planReviewSettingsShortcutRegistry,
};

export const annotateSurface: ShortcutSurface = {
slug: 'annotate-mode',
title: 'Annotate mode',
description: 'Shortcuts surfaced by the standalone annotation UI.',
registry: annotateSettingsShortcutRegistry,
};
5 changes: 0 additions & 5 deletions packages/review-editor/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -691,11 +691,6 @@ const ReviewApp: React.FC = () => {
clearSearch();
}
}
// Cmd/Ctrl+Shift+C to copy diff
if ((e.metaKey || e.ctrlKey) && e.shiftKey && e.key === 'c') {
e.preventDefault();
handleCopyDiff();
}
// Cmd/Ctrl+B to toggle file tree
if ((e.metaKey || e.ctrlKey) && !e.shiftKey && !e.altKey && e.key.toLowerCase() === 'b' && !isTypingTarget(e.target)) {
e.preventDefault();
Expand Down
3 changes: 2 additions & 1 deletion packages/review-editor/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
"type": "module",
"exports": {
".": "./App.tsx",
"./styles": "./index.css"
"./styles": "./index.css",
"./shortcuts": "./shortcuts.ts"
},
"dependencies": {
"@pierre/diffs": "^1.1.12",
Expand Down
Loading