Extensions can replace the sidebar with custom React components - #609
Merged
Conversation
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
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Contributor
Greptile SummaryAdds extension-provided React sidebar views and the host-runtime infrastructure needed to render them.
Confidence Score: 3/5The 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
Sequence DiagramsequenceDiagram
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
Prompt To Fix All With AIFix 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 |
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
…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
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.
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 toHunkExtensionAPI, accepting a view with anidand Reactcomponent. New types:ExtensionSidebarView,ExtensionSidebarViewProps,ExtensionSidebarActions, andExtensionSidebarThemedefine the public contract.Host React serving: Introduced
hostRuntimeModules.tsto 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
ExtensionSidebarPanecomponent 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
ExtensionRegistryto collect sidebar views;resolveExtensionSidebarView()picks the first registration (load order wins) and reports later ones as skipped.Appnow conditionally renders the extension sidebar or built-in pane based on registry state.Integration:
App.tsxcoordinates sidebar selection and passes filtered file views, theme tokens, and guarded action callbacks to the component. Selection changes from extension actions fire the sameselection_changedlifecycle 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