Skip to content

feat(extensions): give command handlers the review selection - #616

Merged
benvinegar merged 4 commits into
mainfrom
claude/hunk-extension-sidebar-override-jzf60c
Jul 28, 2026
Merged

feat(extensions): give command handlers the review selection#616
benvinegar merged 4 commits into
mainfrom
claude/hunk-extension-sidebar-override-jzf60c

Conversation

@benvinegar

Copy link
Copy Markdown
Member

Item 2 from the extension-system roadmap: extension command handlers previously received no review selection, so a "copy current file path" or "open this hunk in my tool" command had to shadow-track the selection_changed lifecycle event into module state.

What changed

Contract (src/extension-api/types.ts, additive within apiVersion 1, still import-free):

  • ExtensionReviewSelection { file: ExtensionDiffFile | null; hunkIndex: number | null }
  • ExtensionCommandContext.selection — a snapshot built when the command fires: a handler that awaits still sees the selection it was run from. file is the same frozen read-only view sidebar components receive; since Hunk keeps the selection inside the visible files, it is null only when nothing is visible at all. hunkIndex is clamped against the file's real hunk count (guarding the one-frame window after a reload shrinks a file) and null for files with no hunks.

One pipeline, lazily: the toReadOnlyFileViews conversion moved out of ExtensionSidebarPane and now runs on demand behind a per-source cache in App — sidebar panes and command snapshots consume literally the same frozen objects, hidden sidebars and extension-free sessions never pay for the conversion, and the command callback stays identity-stable so the dispatch table, keymap, and Extensions menu don't rebuild on navigation keypresses. readMetadataHunkCount joins readMetadataChangeType in src/extensions/events.ts so knowledge of the opaque metadata shape stays in one module.

Works identically whether the command fires from its key or from the Extensions menu (both dispatch through the same run path).

Docs & release

  • docs/extensions.md: ctx.selection semantics plus a typechecked example command (gated by check:pack alongside the other 16 examples).
  • docs/extension-architecture.md: command-system note on the shared frozen-view pipeline.
  • Changeset: minor.

Testing

  • src/ui/lib/extensionSelection.test.ts: resolve/no-selection/not-visible cases, clamp high/low/NaN, zero-hunk file → null, frozen-ness and exact-object identity with the sidebar list.
  • AppHost.extension-sidebar.test.tsx: a fixture command logs ctx.selection.file?.path + hunkIndex through real key dispatch; asserts the startup snapshot, then ]-navigation updates it.
  • Verified: typecheck, bun test src/ui src/extensions src/lib (797 pass), check:pack, lint/format. PTY integration: green except two failures confirmed pre-existing on a main-equivalent tree in this container ("a command key opens an extension sidebar", "filter focus narrows the visible review stream").

An internal adversarial review pass confirmed ref-timing, callback-identity stability, and contract purity, and caught the eager-conversion perf regression fixed in the second commit.

🤖 Generated with Claude Code

https://claude.ai/code/session_01JML2m7oy2sUbFftN7QoHhi


Generated by Claude Code

claude added 2 commits July 28, 2026 04:51
A command that acts on what the user is looking at — copy this file's
path, open this hunk elsewhere — had no way to ask where the review is:
handlers received cwd, notify, and sidebar controls only, so the only
route to the selection was shadow-tracking `selection_changed` into
module state and hoping it stayed in sync.

`ctx.selection` now carries the selected file and hunk index, captured
when the command fires. It resolves through the same frozen file views
the sidebar panes render from — App converts once and both surfaces read
that list — so a command and a sidebar can never describe the review
differently, and a snapshot an extension holds cannot reach the review
model.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JML2m7oy2sUbFftN7QoHhi
Review follow-ups: the frozen-view conversion was hoisted into App as an
eager memo keyed on the visible-files list, whose identity changes on every
selection change — so every navigation keypress reconverted every visible
file, in every session, extensions or not. The conversion now runs on demand
behind a per-source cache: hidden sidebars and extension-free sessions never
pay for it, panes and command snapshots still share the exact same frozen
view objects, and the command callback stays identity-stable so the dispatch
table and menus do not rebuild per keypress. Also rights the contract docs:
Hunk keeps the selection inside the visible files, so a null selection.file
means nothing is visible at all, not a hidden selection.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JML2m7oy2sUbFftN7QoHhi
@vercel

vercel Bot commented Jul 28, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
hunk-web Ready Ready Preview Jul 28, 2026 12:56pm

Request Review

@greptile-apps

greptile-apps Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

Adds review-selection snapshots to extension command contexts.

  • Introduces the public ExtensionReviewSelection contract and documents snapshot semantics.
  • Shares lazily cached extension file views between sidebars and command dispatch.
  • Resolves and clamps selected hunk indices from file metadata.
  • Adds unit, component, integration, documentation, and release-note coverage.

Confidence Score: 3/5

The PR should not merge until command selection files are isolated from the mutable review metadata they promise not to expose.

The new command API hands extension handlers a top-level frozen object whose nested metadata and hunk array still alias the live review model, allowing the documented read-only boundary to be bypassed.

Files Needing Attention: src/ui/App.tsx, src/extensions/events.ts, and src/extension-api/types.ts

Important Files Changed

Filename Overview
src/ui/App.tsx Adds the shared lazy view cache and invocation-time selection snapshots, but exposes nested mutable model metadata through the new command context.
src/ui/lib/extensionSelection.ts Resolves visible selected files and safely normalizes hunk indices against parsed hunk counts.
src/extensions/events.ts Adds centralized hunk-count inspection; the existing shallow file-view conversion does not isolate nested metadata for the newly exposed command API.
src/extension-api/types.ts Adds the command selection contract, whose mutation-isolation guarantee is stronger than the shallow-frozen runtime object provides.
src/ui/components/panes/ExtensionSidebarPane.tsx Accepts host-produced file views so sidebars and command snapshots share exact object identities.
src/ui/lib/extensionSelection.test.ts Covers selection resolution, clamping, null states, top-level freezing, and identity, but not nested mutation isolation.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart LR
  R[Review controller state] --> C[Lazy frozen-view cache]
  C --> S[Extension sidebar props]
  C --> B[Selection snapshot builder]
  B --> K[Extension command context]
  R --> B
Loading
Prompt To Fix All With AI
### Issue 1
src/ui/App.tsx:515
**Nested metadata remains mutable**

When an extension command mutates nested data such as `ctx.selection.file.metadata.hunks`, the shallow-frozen file view writes through to the live review metadata, causing subsequent rendering, selection, and navigation to operate on extension-modified state despite the documented read-only contract.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Reviews (1): Last reviewed commit: "fix(ui): build extension selection snaps..." | Re-trigger Greptile

Comment thread src/ui/App.tsx
… surface

Review follow-up on #616: the read-only file views froze their own
properties but shared metadata, stats, and agent with the live review model,
so an extension writing through selection.file.metadata.hunks (or a sidebar
prop's agent annotations) mutated what the renderer draws from despite the
documented read-only contract. The shared state now sits behind a lazy deep
read-only proxy: reads pass through — including the host's own metadata
readers — while writes, deletes, and redefinitions throw inside the
extension, and the cost the original shallow design avoided (walking the
whole diff model to deep-freeze it per conversion) stays avoided because
wrapping happens only on access. Proxies cache per source object so repeated
conversions hand out identical views; exotic objects (a Map's internal-slot
methods would break through a proxy) read through unwrapped.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JML2m7oy2sUbFftN7QoHhi
@benvinegar
benvinegar merged commit fe21ef9 into main Jul 28, 2026
12 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.

2 participants