feat(extensions): keyboard commands and additive multi-sidebar views - #611
Merged
Merged
Conversation
Extensions can now register named keyboard commands and any number of sidebar views, so a user can keep the built-in file navigation, replace it, or open extra panes — left or right of the review stream — from keys they registered. The command system is not a sidebar one-off: every app-level shortcut moved out of the keyboard hook's if-cascade into one named-command dispatch table (src/ui/lib/appCommands.ts), and hunk.registerCommand entries join that same table. Built-ins keep every key they ship with — conflicts are detected by probing matchers with a synthesized key event and refused with a warning that names both sides — and modal surfaces (dialogs, menus, focused inputs) still own their keys first, since those are widget structure, not shortcuts. Command handlers receive ctx.sidebars (open/close/toggle/isOpen), which is how a registered key opens the sidebar its extension contributed. registerSidebarView is now additive: views declare title, placement, and defaultOpen or replacesDefault, the pane model (src/ui/lib/sidebarPanes.ts) plans which open panes fit and at what width, per-pane dividers resize independently, and open/closed choices survive extension reloads. A view that fails rendering closes with a warning, reopening the built-in file navigation when nothing else is showing. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JML2m7oy2sUbFftN7QoHhi
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Contributor
Greptile SummaryAdds a unified named-command dispatch system and additive multi-pane sidebars for extensions.
Confidence Score: 5/5The PR appears safe to merge with no concrete blocking or independently actionable non-blocking issues identified. The command dispatch preserves modal and focused-input ownership, sidebar state is reconciled across extension reloads, pane planning protects the review stream minimum width, and extension failures are contained with fallback behavior. Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart LR
K[Keyboard event] --> P{Modal or focused input owns key?}
P -->|Yes| W[Widget handling]
P -->|No| D[Shared command dispatcher]
D --> B[Built-in commands]
D --> E[Extension commands]
E --> C[Extension command context]
C --> S[Sidebar controls]
S --> O[Open / close / toggle view]
O --> R[Sidebar open-state reconciliation]
R --> L[Multi-pane layout planner]
L --> LP[Left panes]
L --> V[Review stream]
L --> RP[Right panes]
Reviews (1): Last reviewed commit: "feat(extensions): keyboard commands and ..." | Re-trigger Greptile |
…chitecture map The extensions bullet had grown to a quarter of the file, mostly restating what module headers already document. It now carries only the trip-wire invariants an agent cannot infer from any single file — import-free contract module, renderer-free VCS load path, trust gating, bundled tier surviving --no-extensions — and points at docs/extension-architecture.md, a short map that names which module owns each part of the system so fresh-context agents discover depth on demand instead of paying for it up front. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JML2m7oy2sUbFftN7QoHhi
…ted symbol chords Two review fixes. ctx.sidebars.open/toggle only updated the logical open set, so opening a view while the sidebar area was hidden with s changed nothing on screen; opening now also reveals the area, mirroring toggleSidebar's reveal (including the forced-open path on responsive-hidden layouts with room), and an AppHost test covers hide-then-command-open. And parseKeyChord accepted shift on symbol and digit bases that matching then ignored, so shift+1 bound plain 1; shifted symbols have no layout-independent identity, so the parser now refuses the form and points at binding the shifted character itself. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JML2m7oy2sUbFftN7QoHhi
Shortcuts were matchers written in code: the keys a command answered to were spelled out in its predicate, which meant nobody outside the source could change them, nothing could report what a command was bound to, and an extension had exactly one chord to ask for. This turns bindings into an indirection — commands declare ids and default chords, and one resolution step decides what each id actually answers to. - Built-in commands now declare `defaultKeys` instead of hand-written predicates; matchers and key labels are both derived from the resolved chords, so a rebound command answers to its new key and advertises it. - The new `[keybindings]` table (user config only — keys belong to the machine a person types on, not to a repository under review) maps command ids to chords, or `false` to unbind. A user-bound chord is exclusive: whatever held it by default gives it up, keeping its other chords. Without that rule, moving a key would leave its old owner still swallowing it and the user hunting down every default that collides. - `ExtensionCommand.key` takes a list, and conflicts are refused one chord at a time, so a command bound to three keys that loses one keeps the other two. - The chord grammar moves to `src/extension-api/keys.ts` and ships as part of `hunkdiff/extension` (`matchesKey`, `parseKeyChord`, `matchesKeyChord`). Extension sidebar components with internal keys were the last place left hand-reading modifier flags; now they match the way the host does. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JML2m7oy2sUbFftN7QoHhi
…indings.md The full command-id table and remapping rules were outgrowing the README's config section; the README keeps a short summary and a link, and the reference — rules, chord grammar, built-in command table — lives in its own doc beside the other guides. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JML2m7oy2sUbFftN7QoHhi
…t load Extension ids are user-controlled file stems, and an id is the namespace it owns for commands, sidebar views, and config. Nothing kept those two id spaces apart: `app.ts` could mint `app.quit` and shadow a built-in, `my.ext.ts` made `<extensionId>.<commandId>` unsplittable, `sidebar.ts` could mint the bundled view key `sidebar:files`, and two `notes.ts` files from different sources loaded as one shared id. Close it structurally rather than case by case. Every built-in command id now lives under the reserved `hunk.` vendor namespace and the bundled sidebar registers as `hunk` (`hunk:files`), so any built-in grouping Hunk adds later is also safe from an id somebody already installed. `host.ts` is the single gate where candidates become extensions: it refuses reserved ids (`hunk` plus the bundled backends, via `isVcsId`), ids outside `[A-Za-z0-9][A-Za-z0-9_-]*`, and the later of two sources claiming one id. Discovery stays a pure filesystem walk with no issue channel, and every derivation path -- stem, folder name, manifest -- arrives there as `candidate.id`, so one gate covers all of them. Each refusal is a load issue that skips that extension only. Keymap classification follows the same structure: a `hunk.` id that is not a known built-in can only be a typo, while an unknown owner segment is an extension that is simply not loaded. `resolveCommandKeys` also keeps the first of two defaults entries sharing an id instead of silently merging their keys. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JML2m7oy2sUbFftN7QoHhi
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Follow-up to #609. Extensions can now register named keyboard commands and any number of sidebar views, so a user can keep the built-in file navigation, replace it, or open extra panes — left or right of the review stream — from keys their extensions registered.
Keyboard command system
Not a sidebar one-off: every app-level shortcut moved out of the keyboard hook's if-cascade into one named-command dispatch table (
src/ui/lib/appCommands.ts—review.nextHunk,view.toggleSidebar,app.quit, ...), and extensionregisterCommandentries join that same table.hunk.registerCommand({ id, title, key }, handler)— chord syntax"ctrl+m","F2","G","["; an unparsable chord fails registration loudly.ctx.sidebars(open/close/toggle/isOpen), which is how a registered key opens the sidebar its extension contributed.Additive multi-sidebar
registerSidebarViewnow contributes a view instead of replacing:{ id, title, placement: "left" | "right", defaultOpen, replacesDefault, component }.replacesDefaultstands a view in for the built-in file navigation, which stays available (just closed).src/ui/lib/sidebarPanes.ts) plans which open panes fit and at what width — narrow terminals drop latest-registered panes first, the review stream keeps its minimum — with per-pane divider resize (right-side dividers invert the drag) and open/closed choices that survive extension reloads.Coverage
src/lib/commandKeys.test.ts)scase and between-extension load order (extensionCommands.test.ts)sidebarPanes.test.ts)registerCommandvalidation and rollback (runExtension.test.ts)y→ command → sidebar opens beside files → its actions driveselection_changed→ycloses it;replacesDefault; crash-close-and-restorecheck:packverifies the new docs examples typecheck for consumers. Deliberately not in this pass (the table is built for them — ids, titles, key labels all exist): user-remappable keybindings and menu listings for extension commands.🤖 Generated with Claude Code
https://claude.ai/code/session_01JML2m7oy2sUbFftN7QoHhi
Generated by Claude Code