Skip to content

feat(extensions): add custom file previews - #632

Merged
benvinegar merged 17 commits into
mainfrom
feat/review-triage-extension
Jul 30, 2026
Merged

feat(extensions): add custom file previews#632
benvinegar merged 17 commits into
mainfrom
feat/review-triage-extension

Conversation

@benvinegar

@benvinegar benvinegar commented Jul 28, 2026

Copy link
Copy Markdown
Member

What this adds

This PR adds an experimental extension API for file previews: hunk.registerFileView().

An extension can now recognize files it knows how to present and offer an alternate view in Hunk's View menu. For example, the Markdown extension included here turns a changed .md file into rendered terminal Markdown instead of showing only the raw patch.

Raw diff remains the default. Previews are opt-in and live inside the normal multi-file review stream, so a review can freely mix raw diffs and custom previews.

What a file-view extension can do

A registered file view can:

  • match the file types it supports;
  • inspect the file, hunks, and added/removed ranges;
  • lazily read the exact old or new document when patch text is not enough;
  • return styled text rows rendered by Hunk;
  • optionally paint a row with a fixed-height React/OpenTUI component;
  • bind preview rows back to old/new source lines so Hunk can place inline review notes beside the relevant preview content;
  • use Hunk's semantic theme colors, updated live when the user changes themes;
  • add commands that select or toggle its preview for the current file.

Extensions describe rows and hunk locations, but Hunk still owns the review experience: measurement, scrolling, windowing, hunk navigation, selection, and inline-note rendering.

How it works for users

  • Raw diff is always the starting view. Installing an extension does not replace existing rendering automatically.
  • Matching previews appear under View → File presentation.
  • A preview can be selected independently for each file.
  • Apply “…” to all matching files enables the current preview across the whole changeset, including files hidden by the current filter.
  • Sidebar jumps, [/] hunk navigation, scrolling, filtering, and agent notes continue to work across mixed raw and previewed files.
  • If a preview cannot load, returns invalid data, times out, or cannot place a visible note safely, Hunk shows the complete raw diff instead of guessing or dropping information.

Included examples

Rendered Markdown

examples/extensions/rendered-markdown/ is an installable file-view extension built with the public API. It renders headings, lists, links, code, tables, quotes, and other Markdown constructs as terminal-friendly rows while preserving hunk navigation and inline-note placement.

It is an example, not a bundled default: users explicitly install it into their Hunk extension directory.

JSX file-view gallery

examples/extensions/jsx-file-view-gallery/ demonstrates the constrained JSX row API with real diffs:

  • a TypeScript change atlas;
  • a CSS color-palette preview;
  • a package dependency/version view;
  • a mixed review containing custom previews alongside ordinary Markdown and Python diffs.

The gallery also documents the important boundary: JSX can paint inside a fixed row, but it cannot take over the review stream or resize itself after mounting.

Review triage

The PR also keeps the review-triage example that motivated the broader extension API evaluation. It demonstrates commands, dialogs, notifications, lifecycle events, and a custom sidebar using only the public extension surface.

Reliability and performance

File-view layouts are validated, bounded, cancellable, and cached by file and terminal width. Source reads are lazy and deduplicated. Invalid or stale layouts fall back to raw diff, and one broken custom row cannot break the rest of the review.

All PR checks are green, including Linux tests and smoke coverage, Windows compatibility, macOS/Windows compiled portability, package validation, and website checks.

We also compared the current branch with the commit immediately before the latest file-view changes using seven samples each of the render-layout, large-stream, and interaction-latency benchmarks. The repository benchmark gate found no material regressions; windowed scrolling was 3.5% faster in the comparison.

This PR description was generated by Pi using OpenAI GPT-5.2

@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 30, 2026 1:03am

Request Review

Comment thread src/ui/fileViews/useFileViews.ts Outdated
Comment on lines +63 to +65
useEffect(() => {
const controller = new AbortController();
const next = new Map<string, ResolvedFileViewLayout>();

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.

P1 Stale layouts survive soft reloads

When a soft reload replaces a file under the same stable ID, the previous resolved layout remains active until the asynchronous preparation loop finishes, causing pre-reload content to render under the reloaded file header instead of falling back to the current raw diff.

Knowledge Base Used: UI App Shell: AppHost, App, and Controller Hooks

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/ui/fileViews/useFileViews.ts
Line: 63-65

Comment:
**Stale layouts survive soft reloads**

When a soft reload replaces a file under the same stable ID, the previous resolved layout remains active until the asynchronous preparation loop finishes, causing pre-reload content to render under the reloaded file header instead of falling back to the current raw diff.

**Knowledge Base Used:** [UI App Shell: AppHost, App, and Controller Hooks](https://app.greptile.com/modem/-/custom-context/knowledge-base/modem-dev/hunk/-/docs/ui-app-host.md)

---

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

Comment thread src/ui/fileViews/useFileViews.ts Outdated
};

const prepare = async () => {
for (const file of files) {

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.

P2 Layout preparation blocks serially

Selected layouts are awaited one file at a time and published only after the entire loop finishes, so an early slow or timed-out layout keeps completed presentations on raw rendering and adds up to 1.5 seconds of delay per slow file.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/ui/fileViews/useFileViews.ts
Line: 77

Comment:
**Layout preparation blocks serially**

Selected layouts are awaited one file at a time and published only after the entire loop finishes, so an early slow or timed-out layout keeps completed presentations on raw rendering and adds up to 1.5 seconds of delay per slow file.

---

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

@greptile-apps

greptile-apps Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds an experimental host-rendered file-presentation extension surface.

  • Adds public file-view registration, document reads, symbolic layouts, validation, resource limits, and command controls.
  • Adds a bundled rendered-Markdown presentation, View-menu selection, and Ctrl+G toggle.
  • Integrates alternate layouts with review-stream geometry, virtualization, hunk navigation, reload state, and raw-diff fallback.
  • Adds a documented review-triage extension example and extension API field notes.

Confidence Score: 3/5

This PR should not merge until alternate presentations are invalidated when a soft reload replaces the underlying file, preventing stale pre-reload content from being shown.

The layout state is keyed for rendering by file id but retains the layout produced for the previous file object until asynchronous replacement work completes, allowing stale code to remain visible after a same-id reload; serial preparation also unnecessarily delays independent layouts.

Files Needing Attention: src/ui/fileViews/useFileViews.ts, src/ui/App.tsx, src/ui/components/panes/DiffSection.tsx

Important Files Changed

Filename Overview
src/ui/fileViews/useFileViews.ts Adds asynchronous layout preparation, validation, caching, and fallback, but retains stale layouts across same-id file replacement and serializes all selected-file work.
src/ui/App.tsx Wires registration, per-file selection, menus, commands, reload reconciliation, and resolved layouts into the application shell.
src/ui/components/panes/DiffPane.tsx Integrates alternate layout geometry and rendering while retaining raw rendering for files with notes.
src/ui/components/panes/FileView.tsx Adds windowed rendering of validated symbolic rows using host theme colors.
src/ui/fileViews/layout.ts Validates extension layouts and enforces row, span, text, hunk-coordinate, and source-anchor limits.
src/ui/fileViews/host.ts Exposes frozen public file inputs, parsed change ranges, and cached abortable source-document reads.
src/extensions/default/ui/fileViews/markdown.ts Registers the bundled Markdown presentation and maps exact new-side source lines and hunks into symbolic rows.
src/extension-api/types.ts Advances the extension API and defines the experimental file-view contract and command controls.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart LR
  E[Extension registerFileView] --> R[Session file-view registry]
  R --> S[Per-file presentation selection]
  F[Current DiffFile] --> H[useFileViewLayouts]
  S --> H
  H --> D[Lazy document reads]
  D --> L[Extension layout]
  L --> V[Host validation and limits]
  V -->|valid| G[Host geometry]
  V -->|invalid, null, timeout| P[Pierre raw diff fallback]
  G --> W[Windowed FileView rendering]
  W --> N[Review stream and hunk navigation]
Loading
Prompt To Fix All With AI
### Issue 1
src/ui/fileViews/useFileViews.ts:63-65
**Stale layouts survive soft reloads**

When a soft reload replaces a file under the same stable ID, the previous resolved layout remains active until the asynchronous preparation loop finishes, causing pre-reload content to render under the reloaded file header instead of falling back to the current raw diff.

### Issue 2
src/ui/fileViews/useFileViews.ts:77
**Layout preparation blocks serially**

Selected layouts are awaited one file at a time and published only after the entire loop finishes, so an early slow or timed-out layout keeps completed presentations on raw rendering and adds up to 1.5 seconds of delay per slow file.

---

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

Reviews (1): Last reviewed commit: "feat(extensions): add review presentatio..." | Re-trigger Greptile

claude added 3 commits July 29, 2026 21:51
The source reads that verify row `sourceRanges` were awaited after the timeout
race had already settled, so a slow or hung read could hold one of the four
preparation slots indefinitely — even though the module promises to bound
third-party layout work. Source fetchers can come from extension-provided VCS
adapters, so that was a reachable contract hole, not a theoretical one.

Route every awaited phase through one `withinBudget()` helper so the extension
call and the reads its bindings require share a single deadline.

Also distinguish an unreadable document from a wrong binding: unavailable source
is an environment condition and should not be reported as a layout the extension
got wrong.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EKB5sYNLXDmyh6WCbN8HN9
…puts

`measureFileViewGeometry` carried a `_file` argument every caller had to pass but
nothing read, and defaulted `plannedRows` to a note-less plan alongside `width = 1`.
Those two defaults were only safe together: passing planned rows that contain
inline notes while omitting the width would measure every note at one column and
silently corrupt scroll geometry. Take one required options object instead.

`fileViewUnavailableReason` likewise kept `file` and `showAgentNotes` parameters
that stopped being read once committed notes moved to validated source bindings.
Narrowing it to `hasDraftNote` also drops `showAgentNotes` from App's dependency
list, so the reason map no longer recomputes when that toggle changes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EKB5sYNLXDmyh6WCbN8HN9
The `<extensionId>:<viewId>` address was templated in four places: sidebar
duplicate resolution, file-view duplicate resolution, file-view selection lookup,
and bare-id qualification for extension commands. Duplicate resolution and
selection lookup must agree exactly or a registration is deduped under one key
and looked up under another, so give them a single `qualifiedViewKey` source.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EKB5sYNLXDmyh6WCbN8HN9
Version 2 was the intermediate shape inside this stack and never shipped, so
publishing 3 would leave extensions branching on `hunk.apiVersion` with a gap
they can never have seen. main is still on 1; land the streamlined contract as 2.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EKB5sYNLXDmyh6WCbN8HN9
@benvinegar benvinegar changed the title feat(extensions): add review presentation tools feat(extensions): add custom file previews Jul 30, 2026
@benvinegar
benvinegar merged commit 86bf722 into main Jul 30, 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