Skip to content

Extensions can replace the sidebar with custom React components - #609

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

Extensions can replace the sidebar with custom React components#609
benvinegar merged 5 commits into
mainfrom
claude/hunk-extension-sidebar-override-jzf60c

Conversation

@benvinegar

Copy link
Copy Markdown
Member

Extensions can now contribute a custom sidebar view via hunk.registerSidebarView(), replacing the built-in file-navigation pane with their own React component. This enables extensions to provide alternative navigation UIs while maintaining full integration with Hunk's review model and theme system.

Key changes

  • Extension API: Added registerSidebarView() method to HunkExtensionAPI, accepting a view with an id and React component. New types: ExtensionSidebarView, ExtensionSidebarViewProps, ExtensionSidebarActions, and ExtensionSidebarTheme define the public contract.

  • Host React serving: Introduced hostRuntimeModules.ts to serve Hunk's own React instance to extension files at import time. Extensions outside the app bundle (user config or repo-local) now receive the host's React, hooks dispatcher, and JSX runtime through virtual modules (hunk-host:react, etc.), ensuring hooks work correctly and preventing dispatcher conflicts. The mechanism transpiles extension source and rewrites host-owned specifiers before Bun's loader would resolve them.

  • Sidebar rendering: New ExtensionSidebarPane component wraps extension-contributed views in an error boundary, projects the active theme onto a curated public token slice, and guards all navigation actions. Failures are reported as warnings naming the extension, and the built-in sidebar takes over as fallback.

  • Registry and resolution: Extended ExtensionRegistry to collect sidebar views; resolveExtensionSidebarView() picks the first registration (load order wins) and reports later ones as skipped. App now conditionally renders the extension sidebar or built-in pane based on registry state.

  • Integration: App.tsx coordinates sidebar selection and passes filtered file views, theme tokens, and guarded action callbacks to the component. Selection changes from extension actions fire the same selection_changed lifecycle event as the built-in sidebar.

Notable implementation details

  • Extension files are transpiled with JSX automatic runtime enabled and host specifiers rewritten before import, so JSX lowering emits imports that pass through the rewrite and resolve to virtual modules.

  • The sidebar component receives frozen, read-only file views and a curated theme object to prevent accidental review-model corruption.

  • Actions (selectFile, selectHunk, notify) are guarded: failures inside them emit warnings instead of propagating into the component, keeping the session stable.

  • Error boundary isolation is keyed by extension and view id, so a reloaded registry gets a fresh chance to render.

  • Comprehensive tests cover the host module serving (including JSX transpilation and helper module resolution), extension registration and rollback on factory failure, and live PTY integration with hooks and actions.

https://claude.ai/code/session_01JML2m7oy2sUbFftN7QoHhi

Extensions can now replace the file-navigation sidebar with their own React
component. The load path serves Hunk's own React, OpenTUI, and
hunkdiff/extension instances to extension files through per-directory Bun
runtime loader hooks (transpile + specifier rewrite to prefixed virtual
modules), so hooks and JSX run on the host reconciler without extensions
bundling anything — and without claiming bare specifiers process-wide, which
would break the host's own lazily imported modules when running from source.

The component gets frozen file views, selection, theme tokens, and guarded
actions that route through the same review controller as the built-in
sidebar, so navigation and the selection_changed event behave identically. A
component that throws while rendering is contained by an error boundary: the
user gets a warning naming the extension and the built-in sidebar takes over,
keeping the misbehaving-extension contract at a warning, not the session.

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

vercel Bot commented Jul 27, 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 27, 2026 5:38pm

Request Review

@greptile-apps

greptile-apps Bot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

Adds extension-provided React sidebar views and the host-runtime infrastructure needed to render them.

  • Extends the public extension API and registry with sidebar view registration, props, actions, and theme tokens.
  • Rewrites extension imports to host-served React, JSX, OpenTUI, and extension API virtual modules.
  • Conditionally renders extension sidebars with guarded navigation, error isolation, and a built-in fallback.
  • Adds unit and integration coverage plus extension authoring documentation.

Confidence Score: 3/5

The PR should not merge until invalid hunk selections are rejected and failed sidebar boundaries reliably reset after extension reloads.

The new action path can store a hunk index that has no corresponding hunk, and a sidebar that once threw remains permanently replaced by the fallback when corrected code reloads under the same identifiers.

Files Needing Attention: src/ui/App.tsx; src/ui/components/panes/ExtensionSidebarPane.tsx

Important Files Changed

Filename Overview
src/extensions/hostRuntimeModules.ts Adds scoped Bun plugins that transpile extension sources and redirect host-owned runtime imports.
src/ui/components/panes/ExtensionSidebarPane.tsx Adds extension rendering, error isolation, and action guards, but selectHunk permits invalid upper-bound and non-finite indices.
src/ui/App.tsx Integrates the extension sidebar, but its stable identifier-only key prevents a failed boundary from resetting after same-id reloads.
src/extension-api/types.ts Defines the import-free public sidebar component, action, file, and theme contracts.
src/extensions/runExtension.ts Validates, registers, and rolls back sidebar contributions with extension factory state.

Sequence Diagram

sequenceDiagram
  participant E as Extension
  participant R as Extension registry
  participant A as App
  participant P as ExtensionSidebarPane
  participant C as Review controller
  E->>R: registerSidebarView(view)
  A->>R: resolve first sidebar view
  A->>P: render files, theme, selection, actions
  P->>C: selectFile / selectHunk
  C-->>A: update review selection
  A-->>E: emit selection_changed
  alt Component render throws
    P-->>A: report warning
    A->>A: render built-in sidebar fallback
  end
Loading
Prompt To Fix All With AI
Fix the following 2 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 2
src/ui/App.tsx:1257
**Failed boundary survives reload**

When a sidebar throws and its extension is reloaded with the same extension and view IDs, this unchanged key preserves the error boundary's failed state, causing the corrected component to remain unmounted and the built-in fallback to stay active for the rest of the session.

### Issue 2 of 2
src/ui/components/panes/ExtensionSidebarPane.tsx:142-147
**Invalid hunk indices reach controller**

When an extension passes an index beyond the file's hunk count or a non-finite number, this guard forwards the invalid value to `review.selectHunk`, causing selection state to reference no hunk while reveal scrolling and `selection_changed` proceed with that invalid index.

Reviews (1): Last reviewed commit: "feat(extensions): custom sidebar views v..." | Re-trigger Greptile

Comment thread src/ui/App.tsx Outdated
Comment thread src/ui/components/panes/ExtensionSidebarPane.tsx
Like Git for VCS backends, the built-in file-navigation sidebar now registers
through the public registerSidebarView API and consumes exactly the published
sidebar props — files, selection, theme tokens, and actions — so the contract
stays sufficient for third-party sidebars because Hunk's own sidebar breaks
first if it is not. A user-registered sidebar view overrides the bundled
default, and the render-failure fallback is the same bundled component fed the
same public props.

Dogfooding immediately closed three contract gaps: read-only file views now
carry changeType (surfaced from the opaque diff metadata in the adapters'
existing vocabulary) and statsTruncated, and the sidebar theme tokens gained
noteBorder for the note-count badge. Selection scroll-follow moved from App
into the sidebar component, since every sidebar should own its follow policy;
the dead SidebarPane path and reviewState.sidebarEntries plumbing are removed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JML2m7oy2sUbFftN7QoHhi
claude added 2 commits July 27, 2026 14:48
…k indices

Addresses review feedback: a reloaded extension registers a fresh sidebar view
object, so the error boundary now clears its failed state on registration
identity instead of staying pinned to the fallback under unchanged ids; and
selectHunk refuses non-finite hunk indices and clamps the rest into the target
file's real hunk range so garbage never reaches selection state, reveal
scrolling, or selection_changed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JML2m7oy2sUbFftN7QoHhi
The compiled-in tier now lives under src/extensions/default/, split by what it
contributes: default/vcs/{git,jujutsu,sapling} for the VCS backends — each with
its full name and its own directory — and default/ui/sidebar for the built-in
sidebar. The directory layout now mirrors the tier split the loaders already
enforce: default/vcs is what VCS adapter resolution imports and must stay
renderer-free, while default/ui loads from the app's sidebar resolution.
Extension ids (git, jj, sl) and registration behavior are unchanged.

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

The host-module shim statically imported @opentui/core and @opentui/react,
and it is reachable from the extension loader that short-lived headless
commands (hunk session list, the daemon) also touch — so every headless
invocation of the compiled binary paid OpenTUI's native-library extraction,
failing the compiled headless portability suite on all three platforms.

The virtual-module factories now resolve their exports with a dynamic import
the first time an extension actually imports the specifier, which only happens
in sessions that render. Dynamic import() rather than require() because
@opentui/core publishes only an import exports condition, which a compile-time
require cannot resolve.

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