Skip to content

refactor(web): register the Preview side panel - #16038

Open
saphid wants to merge 5 commits into
pingdotgg:mainfrom
saphid:stack/02-panel-host
Open

saphid wants to merge 5 commits into
pingdotgg:mainfrom
saphid:stack/02-panel-host

Conversation

@saphid

@saphid saphid commented Oct 5, 2026 •

Copy link
Copy Markdown
Contributor

Stacked on #15010. Review only the top 4 commits: 16877e3.

Rebased 2026-10-10 onto #15010's current head (1fe8efd69a, on main 57b3780). Main now gates Browser on the new preview:operate permission (#9789), so the registered Preview launcher reads canOperatePreview && browserAvailable, and PreviewSidePanel keeps main's "pair again with preview access" message. Main's PreviewView also cancels an in-flight element pick on a thread switch and drops its late result. The layer's test now asserts that a late pick reaches neither thread, and the host comment and commit message no longer claim it sends to the original thread. Main's new RightPanelTabs keyboard test (Mod+T menu, #15686) and the layer's browser-profile test were moved to the panels launcher props and given the new keybinding props. After review, the panel host is memoized so ChatView renders no longer re-render every panel, and a late element pick sends only to the thread it was picked in (matched by environment and thread), with tests for a late pick after a thread switch, the same thread id in another environment, and a same-thread rerender. The annotation sender ref is now updated in a layout effect instead of during render, so a render React discards cannot lend its onSend. The follow-up commits in this PR answer review-bot findings; GPT-6.1 Sol (high) reviewed them: SHIP. Main's Preview view now reads the primary environment id (usePrimaryEnvironmentId), so the registered Preview side panel test adds it to its ~/state/environments mock, as main's own Preview test does. Main's shared observeResize helper (#17656) now calls unobserve when a subscriber leaves, so the browser-profile launcher test's ResizeObserver stub gains an unobserve method, matching main's own right-panel keyboard test. GPT-6.1 Sol (high) reviewed this port: SHIP. Captures below were taken at the revisions they name. At this head (16877e3cf3) these pass: focused tests (8 files, 53 tests), typecheck (@t3tools/web), lint and fmt on the changed files, knip.

Problem

After #15010, Diff mounts through the side-panel registry, but Preview (the Browser panel) still has its own hand-written lazy import and prop wiring in ChatView. The tab strip, the empty launcher and the + menu also each keep their own hand-written list of surfaces, letters and "unavailable" copy, so Browser and Diff are described in four places. Adding or changing a panel means keeping all of them in step. This PR moves Preview onto the registry and lets the registered panels describe themselves once, with no visible change.

Why this qualifies

This is the second of the two PRs proposed in Ideas discussion #14938 (internal panel host, following #1377). No maintainer has agreed to that direction yet. It is stacked on #15010 and depends on it; review that first. If #14938 is declined, close both.

Fix

  • panels/panelRegistry.ts: a definition now also carries title, icon, launcherKey, optional isSupported, and the short and long unavailable copy. Each panel's props are inferred from its lazily imported body, so there is no hand-written props map. get(id) returns the same lazy() component for every lookup and is typed with that id's props. It throws for an unregistered id.
  • panels/bundledPanels.tsx registers Diff and Preview. <RegisteredSidePanel> takes a closed union, so another panel's props, an unknown id or a widened id do not compile. It owns the <Suspense fallback={null}> that ChatView used to write around each panel.
  • components/preview/PreviewPanel.tsx moves to panels/preview/PreviewSidePanel.tsx. ChatView only ever used mode="embedded", so the mode prop is removed. The desktop-only copy and the optional tabId handling are unchanged.
  • panels/panelHost.ts: ChatView lends the rendered panel { threadRef, visible, composerDraftTarget, workspaceMutationId, sendAnnotation } through context. That is exactly what Diff and Preview were given as props before, and each field has a consumer here (Diff reads the draft target and workspace mutation id; Preview reads the thread, visibility and annotation send). sendAnnotation is the same per-render closure over onSend as before, so an element pick that settles after navigating away still sends from the thread where it started.
  • RightPanelTabs.tsx: rightPanelSurfaceActions() builds one ordered list (B T F D P L M) for both the empty launcher and the + menu, replacing two inline lists and two copy maps. Browser and Diff take their label, icon, letter, availability and copy from their definitions. The Diff tab title and icon and the Browser fallback title and icon read the same source. Browser's availability is still isPreviewSupportedInRuntime(), now through isSupported, combined with the thread's own availability. The launcher calls onOpen() without arguments, so a click event can never be read as a browser profile id.
  • Keybinding ids and handlers (diff.toggle, preview.toggle), storage keys, launcher letters, order and all copy are unchanged. Stores, queries, server, contracts, desktop IPC/CSP and mobile are untouched.

Size: 14 files, +959 / −322. Production code is +351 / −313 (+38 net); the rest is tests.

Evidence

Environment: macOS 26.5.2 arm64. Before is #15010 rebased on main (5981e74cdf); after is this head (8b92a7f06b). Each revision ran on its own fresh, isolated T3 home with the same disposable fixtures: a Git repo on feature/panel with one commit and one uncommitted edit, a non-Git folder, and a local static page. Web: Vite dev client in headless Chromium at 1440×1000. Desktop: each revision's built Electron app (vp run build:desktop) at 1440×1000. The theme is set through Settings → Appearance. The providers were not signed in, so the annotation turn was stopped as soon as the message landed.

Reproduction (same steps on both):

  1. Open the empty right-panel launcher in a Git thread and in a non-Git thread. Hover the unavailable rows.
  2. Open Diff, then the + menu. Hover the unavailable rows.
  3. Desktop: open the Browser profile chooser from the launcher and from the + menu, then pick Incognito.
  4. Desktop: open Browser from the launcher and load the fixture page.
  5. Desktop: in Thread A, pick #hero, add a note and send it. Then start another pick in A, switch to Thread B, send a late click to A's page, and switch back to A.
  6. Open Diff from the header, close it, reopen it with ⌘D and switch to Uncommitted.
  7. Open the pull-requests page.

Observed before and after:

  • Web: all 28 still pairs (S1, S2, S6 and S7 in light and dark) are pixel-identical, both full frame and panel region.
  • Desktop: 31 of 40 still pairs are pixel-identical. The 9 others are explained below.
  • Launcher order and letters are B T F D P L M on both. On web, Browser shows "Only available in the desktop app." and the + menu shows "Browser previews are only available in the T3 Code desktop app." In a non-Git thread, Diff shows "Available for Git repositories." and "Diff is only available for server threads in Git repositories." All copy is identical on both revisions.
  • The Browser profile chooser lists Default and Incognito on both. Picking Incognito opens an Incognito tab. The loaded tab is titled "Panel Fixture Page".
  • Annotation: the picked #hero annotation lands as a user message in Thread A only, and Thread B stays empty. Switching to B cancels the pending pick, and a late click sent to A's retained page adds nothing to either thread. Back in A, there is still exactly one annotation. The behaviour is the same on both revisions.
  • Diff opens with branch changes (+4 −1, greet.ts and README), closes, reopens with ⌘D, and shows README only (+2 −0) in Uncommitted. All of these pairs are identical on web and desktop.
  • The 9 non-zero desktop pairs: eight are the annotation states. Seven of them differ by 82–90 pixels: the browser Back arrow is disabled on before and enabled on after, which reflects that tab's navigation history during the capture. The eighth (light, picked heading) differs by 1,150 pixels: the Back arrow plus raster pixels in the annotation note editor inside the page. The picked element, note and controls match. This PR does not change the Browser toolbar, PreviewView or the in-page annotation tool. The ninth pair (light, empty pull-requests page) differs by 19 pixels on the Sort icon and has no panel.

Before/after images are stacked below. Click a heading for the set.

Recording: Browser open via launcher → close → reopen, profile chooser → Incognito (desktop, dark, real time)

Before (#15010):
Before: Browser opened from the launcher, closed, reopened, then opened in Incognito from the profile chooser

After (this PR):
After: Browser opened from the launcher, closed, reopened, then opened in Incognito from the profile chooser

MP4: before · after

Recording: annotation sent from Thread A, then a pick started in A and abandoned by switching to Thread B (desktop, dark, real time)

Before (#15010):
Before: annotation lands in Thread A; after switching to Thread B the pending pick is cancelled and B stays empty

After (this PR):
After: annotation lands in Thread A; after switching to Thread B the pending pick is cancelled and B stays empty

MP4: before · after

Recording: Diff open → close → reopen with ⌘D (web, dark, real time)

Before (#15010):
Before: Diff opened from the launcher, closed, reopened with Cmd+D

After (this PR):
After: Diff opened from the launcher, closed, reopened with Cmd+D

MP4: before · after

Empty launcher, Git thread (desktop)

Before (#15010), light:
Before (#15010): Empty launcher, Git thread (desktop), light

After (this PR), light:
After (this PR): Empty launcher, Git thread (desktop), light

Before (#15010), dark:
Before (#15010): Empty launcher, Git thread (desktop), dark

After (this PR), dark:
After (this PR): Empty launcher, Git thread (desktop), dark

Empty launcher, Browser unavailable on web

Before (#15010), light:
Before (#15010): Empty launcher, Browser unavailable on web, light

After (this PR), light:
After (this PR): Empty launcher, Browser unavailable on web, light

Before (#15010), dark:
Before (#15010): Empty launcher, Browser unavailable on web, dark

After (this PR), dark:
After (this PR): Empty launcher, Browser unavailable on web, dark

Empty launcher, non-Git thread: Diff unavailable hint (desktop)

Before (#15010), light:
Before (#15010): Empty launcher, non-Git thread: Diff unavailable hint (desktop), light

After (this PR), light:
After (this PR): Empty launcher, non-Git thread: Diff unavailable hint (desktop), light

Before (#15010), dark:
Before (#15010): Empty launcher, non-Git thread: Diff unavailable hint (desktop), dark

After (this PR), dark:
After (this PR): Empty launcher, non-Git thread: Diff unavailable hint (desktop), dark

+ menu (desktop)

Before (#15010), light:
Before (#15010): + menu (desktop), light

After (this PR), light:
After (this PR): + menu (desktop), light

Before (#15010), dark:
Before (#15010): + menu (desktop), dark

After (this PR), dark:
After (this PR): + menu (desktop), dark

+ menu: Browser tooltip on web

Before (#15010), light:
Before (#15010): + menu: Browser tooltip on web, light

After (this PR), light:
After (this PR): + menu: Browser tooltip on web, light

Before (#15010), dark:
Before (#15010): + menu: Browser tooltip on web, dark

After (this PR), dark:
After (this PR): + menu: Browser tooltip on web, dark

+ menu, non-Git thread: Diff tooltip (desktop)

Before (#15010), light:
Before (#15010): + menu, non-Git thread: Diff tooltip (desktop), light

After (this PR), light:
After (this PR): + menu, non-Git thread: Diff tooltip (desktop), light

Before (#15010), dark:
Before (#15010): + menu, non-Git thread: Diff tooltip (desktop), dark

After (this PR), dark:
After (this PR): + menu, non-Git thread: Diff tooltip (desktop), dark

Launcher Browser profile chooser (desktop)

Before (#15010), light:
Before (#15010): Launcher Browser profile chooser (desktop), light

After (this PR), light:
After (this PR): Launcher Browser profile chooser (desktop), light

Before (#15010), dark:
Before (#15010): Launcher Browser profile chooser (desktop), dark

After (this PR), dark:
After (this PR): Launcher Browser profile chooser (desktop), dark

+ menu Browser profile submenu (desktop)

Before (#15010), light:
Before (#15010): + menu Browser profile submenu (desktop), light

After (this PR), light:
After (this PR): + menu Browser profile submenu (desktop), light

Before (#15010), dark:
Before (#15010): + menu Browser profile submenu (desktop), dark

After (this PR), dark:
After (this PR): + menu Browser profile submenu (desktop), dark

Incognito Browser tab (desktop)

Before (#15010), light:
Before (#15010): Incognito Browser tab (desktop), light

After (this PR), light:
After (this PR): Incognito Browser tab (desktop), light

Before (#15010), dark:
Before (#15010): Incognito Browser tab (desktop), dark

After (this PR), dark:
After (this PR): Incognito Browser tab (desktop), dark

Browser loading fallback title and icon (desktop)

Before (#15010), light:
Before (#15010): Browser loading fallback title and icon (desktop), light

After (this PR), light:
After (this PR): Browser loading fallback title and icon (desktop), light

Before (#15010), dark:
Before (#15010): Browser loading fallback title and icon (desktop), dark

After (this PR), dark:
After (this PR): Browser loading fallback title and icon (desktop), dark

Browser with the fixture page loaded (desktop)

Before (#15010), light:
Before (#15010): Browser with the fixture page loaded (desktop), light

After (this PR), light:
After (this PR): Browser with the fixture page loaded (desktop), light

Before (#15010), dark:
Before (#15010): Browser with the fixture page loaded (desktop), dark

After (this PR), dark:
After (this PR): Browser with the fixture page loaded (desktop), dark

Annotation: #hero picked in Thread A (desktop)

Before (#15010), light:
Before (#15010): Annotation: #hero picked in Thread A (desktop), light

After (this PR), light:
After (this PR): Annotation: #hero picked in Thread A (desktop), light

Before (#15010), dark:
Before (#15010): Annotation: #hero picked in Thread A (desktop), dark

After (this PR), dark:
After (this PR): Annotation: #hero picked in Thread A (desktop), dark

Annotation sent: message in Thread A (desktop)

Before (#15010), light:
Before (#15010): Annotation sent: message in Thread A (desktop), light

After (this PR), light:
After (this PR): Annotation sent: message in Thread A (desktop), light

Before (#15010), dark:
Before (#15010): Annotation sent: message in Thread A (desktop), dark

After (this PR), dark:
After (this PR): Annotation sent: message in Thread A (desktop), dark

Late pick: Thread B after switching away from a pick in A (desktop)

Before (#15010), light:
Before (#15010): Late pick: Thread B after switching away from a pick in A (desktop), light

After (this PR), light:
After (this PR): Late pick: Thread B after switching away from a pick in A (desktop), light

Before (#15010), dark:
Before (#15010): Late pick: Thread B after switching away from a pick in A (desktop), dark

After (this PR), dark:
After (this PR): Late pick: Thread B after switching away from a pick in A (desktop), dark

Late pick: back in Thread A, still one annotation (desktop)

Before (#15010), light:
Before (#15010): Late pick: back in Thread A, still one annotation (desktop), light

After (this PR), light:
After (this PR): Late pick: back in Thread A, still one annotation (desktop), light

Before (#15010), dark:
Before (#15010): Late pick: back in Thread A, still one annotation (desktop), dark

After (this PR), dark:
After (this PR): Late pick: back in Thread A, still one annotation (desktop), dark

Diff open on branch changes (desktop)

Before (#15010), light:
Before (#15010): Diff open on branch changes (desktop), light

After (this PR), light:
After (this PR): Diff open on branch changes (desktop), light

Before (#15010), dark:
Before (#15010): Diff open on branch changes (desktop), dark

After (this PR), dark:
After (this PR): Diff open on branch changes (desktop), dark

Diff closed (desktop)

Before (#15010), light:
Before (#15010): Diff closed (desktop), light

After (this PR), light:
After (this PR): Diff closed (desktop), light

Before (#15010), dark:
Before (#15010): Diff closed (desktop), dark

After (this PR), dark:
After (this PR): Diff closed (desktop), dark

Diff reopened with ⌘D (desktop)

Before (#15010), light:
Before (#15010): Diff reopened with ⌘D (desktop), light

After (this PR), light:
After (this PR): Diff reopened with ⌘D (desktop), light

Before (#15010), dark:
Before (#15010): Diff reopened with ⌘D (desktop), dark

After (this PR), dark:
After (this PR): Diff reopened with ⌘D (desktop), dark

Diff, Uncommitted scope (desktop)

Before (#15010), light:
Before (#15010): Diff, Uncommitted scope (desktop), light

After (this PR), light:
After (this PR): Diff, Uncommitted scope (desktop), light

Before (#15010), dark:
Before (#15010): Diff, Uncommitted scope (desktop), dark

After (this PR), dark:
After (this PR): Diff, Uncommitted scope (desktop), dark

Pull-requests page: + menu with Browser and Diff unavailable (web)

Before (#15010), light:
Before (#15010): Pull-requests page: + menu with Browser and Diff unavailable (web), light

After (this PR), light:
After (this PR): Pull-requests page: + menu with Browser and Diff unavailable (web), light

Before (#15010), dark:
Before (#15010): Pull-requests page: + menu with Browser and Diff unavailable (web), dark

After (this PR), dark:
After (this PR): Pull-requests page: + menu with Browser and Diff unavailable (web), dark

Pull-requests page: Diff tooltip (web)

Before (#15010), light:
Before (#15010): Pull-requests page: Diff tooltip (web), light

After (this PR), light:
After (this PR): Pull-requests page: Diff tooltip (web), light

Before (#15010), dark:
Before (#15010): Pull-requests page: Diff tooltip (web), dark

After (this PR), dark:
After (this PR): Pull-requests page: Diff tooltip (web), dark

Pixel-diff images for the 9 non-zero desktop pairs are published next to these captures as diff-electron-<theme>-<state>.png.

Checks at this head (8b92a7f06b), re-run 2026-10-05 (CI=true, all exit 0):

  • vp test run apps/web/src/components/RightPanelTabs.test.tsx apps/web/src/components/RightPanelTabs.browserProfile.test.tsx apps/web/src/panels apps/web/src/components/preview/PreviewView.test.tsx: 6 files, 40 tests pass.
    • panels/preview/PreviewSidePanel.test.tsx drives the real RegisteredSidePanel → PreviewSidePanel → PreviewView path, stubbing only the desktop bridge, stores and leaf chrome. Re-rendering with a new host for the same thread keeps an in-flight element pick and does not remount Preview. Switching to another thread also keeps the one mount, and a pick that resolves after the switch still goes to the original thread's sender and draft.
    • RightPanelTabs.browserProfile.test.tsx clicks the empty launcher's Browser row, which opens the default profile, then picks Incognito from the profile chooser, which opens that profile.
    • panelRegistry.test.tsx: duplicate ids rejected without loading; nothing loads until first render, one mount across lookups; two panels with different props load and dispose independently and keep stable lazy identities.
    • bundledPanels.test.tsx: the real registry loads only the selected body (Preview, not Diff) and lends it the host.
    • Recorded during development: with this PR's source reverted to its parent (refactor(web): register the Diff side panel #15010), 3 tests fail and 2 suites cannot load. Each failure is an API this PR adds: rightPanelSurfaceActions, the panelHost module, and the panels.preview launcher prop. The Preview lifecycle and late-pick behaviour is not new. The parent already had it through PreviewPanel, and these tests show it survives the move.
  • vp run --filter @t3tools/web typecheck passes. Recorded during development: the compile-only fixtures (@ts-expect-error: Preview props on Diff, threadRef passed as a prop, wrong input type, unknown id, widened id with Preview props, missing and foreign props on the registry) produce 18 type errors against the parent.
  • vp lint --report-unused-disable-directives and vp fmt --check on the touched files: pass. Lint warnings match the parent for the same files, except RightPanelTabs.tsx goes from 1 to 0. vp run knip:check passes.
  • Not part of the re-run: vp run --filter @t3tools/web build (emits separate DiffSidePanel and PreviewSidePanel chunks), vp run build:desktop and node scripts/release-smoke.ts passed on the previous revision of this PR, which differs only in tests, and all three passed again at the top of this stack (the example plugins PR), which contains this change.

Surfaces

  • Entry points: header toggles, ⌘D / the Browser keybinding, the empty launcher, the + menu and the tab strip still reach the same single mount. The pull-requests page still shows Browser and Diff as unavailable.
  • Web: affected (the only code changed). On web the Browser row stays unavailable with the same copy.
  • Desktop: wraps the same web bundle; Electron shell, preload, IPC and CSP are unchanged. Desktop build and Release Smoke pass.
  • Mobile: not affected. Mobile has no right panel and does not import apps/web.
  • Providers (Codex, Claude, Cursor, Grok, OpenCode, Antigravity): not affected; no adapter or provider output is involved.
  • Contracts: none changed. No wire change.
  • Reverse states: none added; open/close/reopen paths are the existing ones.
  • Local / remote-relay / tunnel: not affected. Panels read the same environment-scoped thread ref; queries and transport are unchanged.
  • Docs: none. How to use Browser and Diff does not change.

Not verified

  • Remote, relay and tunnel connections. This PR makes no wire change, so we did not run a --share pass.
  • Mobile. It has no right panel and does not import apps/web.
  • A live provider reply after the annotation. The providers were not signed in in the isolated homes, so the turn was stopped after the user message landed.
  • A late click physically made in Thread B. After the switch, A's page is hidden, so the late click was delivered to A's retained page programmatically.
  • Browser and Diff shown unavailable on the pull-requests page in the desktop app: the isolated desktop home had no pull requests, so the panel toggle was disabled. This was verified on web with a real pull request.
  • The fixture's red SVG favicon: neither revision displayed it, and both showed the default globe icon.

Claude Opus 5.5 (build), GPT-6.1 Sol (review) and GPT-6 Astra (captures) via T3 Code
🤖 Generated with Claude Code

@github-actions github-actions Bot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:XL 500-999 changed lines (additions + deletions). labels Oct 5, 2026
@macroscopeapp

macroscopeapp Bot commented Oct 5, 2026 •

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Not approved

Macroscope's review found this PR not approvable — This change replaces hand-wired Preview/Diff integration with new shared registry and host abstractions across ChatView, panel launchers, and panel lifecycles. Although tests cover the refactor and visible behavior is intended to remain unchanged, the cross-cutting production restructuring warrants human review.

You can add or adjust custom eligibility rules. Learn more.

@coderabbitai

coderabbitai Bot commented Oct 5, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Important

Review skipped

We couldn't safely recover the incremental review. No full review was started, and the last reviewed checkpoint was preserved. Retry later, or explicitly request a full review by commenting @coderabbitai full review.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Repository: pingdotgg/t3code/.coderabbit.yaml
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 4bb89a77-e12c-4dd6-a79c-c41a4dcc1a68






📥 Commits

Reviewing files that changed from the base of the PR and between 7812230 and 41a991f.







📒 Files selected for processing (18)
  • apps/web/src/components/ChatView.tsx
  • apps/web/src/components/RightPanelTabs.browserProfile.test.tsx
  • apps/web/src/components/RightPanelTabs.test.tsx
  • apps/web/src/components/RightPanelTabs.tsx
  • apps/web/src/components/diffs/DiffFileLoadingBoundary.tsx
  • apps/web/src/components/diffs/DiffLoadingState.tsx
  • apps/web/src/components/preview/PreviewPanel.tsx
  • apps/web/src/components/pullRequest/PullRequestCodeTab.tsx
  • apps/web/src/components/pullRequest/PullRequestDetailPanel.tsx
  • apps/web/src/panels/bundledPanels.test.tsx
  • apps/web/src/panels/bundledPanels.tsx
  • apps/web/src/panels/diff/DiffSidePanel.tsx
  • apps/web/src/panels/panelHost.ts
  • apps/web/src/panels/panelRegistry.test.tsx
  • apps/web/src/panels/panelRegistry.ts
  • apps/web/src/panels/preview/PreviewSidePanel.test.tsx
  • apps/web/src/panels/preview/PreviewSidePanel.tsx
  • apps/web/src/routes/_chat.pull-requests.tsx






💤 Files with no reviewable changes (1)
  • apps/web/src/components/preview/PreviewPanel.tsx






Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 5 remain after this review.








📝 Walkthrough
📝 Walkthrough
📝 Walkthrough
📝 Walkthrough
📝 Walkthrough
📝 Walkthrough

Walkthrough

Diff and preview panels now use a typed registry and shared host context. RightPanelTabs derives launcher actions from panel metadata, and ChatView renders registered panels with thread-specific host state.

Changes

Registered Side Panels

Layer / File(s) Summary
Define and register panels
apps/web/src/panels/panelRegistry.ts, apps/web/src/panels/bundledPanels.tsx, apps/web/src/panels/panelRegistry.test.tsx, apps/web/src/panels/bundledPanels.test.tsx
Panel definitions provide typed props, metadata, and lazy component lookup. Bundled panels register Diff and Browser, expose metadata, and render the selected panel. Tests cover lazy loading, component identity, and panel prop types.
Host context and panel bodies
apps/web/src/panels/panelHost.ts, apps/web/src/panels/diff/DiffSidePanel.tsx, apps/web/src/panels/preview/PreviewSidePanel.tsx, apps/web/src/panels/preview/PreviewSidePanel.test.tsx, apps/web/src/components/preview/PreviewPanel.tsx, apps/web/src/components/diffs/*, apps/web/src/components/pullRequest/PullRequestCodeTab.tsx, apps/web/src/components/pullRequest/PullRequestDetailPanel.tsx
Panel bodies read host state from context and render within embedded frames. Preview picker tests cover host updates and thread changes. The old PreviewPanel is removed, and diff loading components move to DiffLoadingState.
Launcher and ChatView integration
apps/web/src/components/RightPanelTabs.tsx, apps/web/src/components/RightPanelTabs.test.tsx, apps/web/src/components/RightPanelTabs.browserProfile.test.tsx, apps/web/src/components/ChatView.tsx, apps/web/src/routes/_chat.pull-requests.tsx
RightPanelTabs uses shared actions and panel metadata for launcher labels, icons, shortcuts, and availability. ChatView supplies the panel host and launcher configuration. The pull-request route marks preview and diff unavailable. Tests cover launcher actions and browser profile selection.

Priority: ⬇️ Low

Estimated code review effort: 3 (Moderate) | ~25 minutes

Change: Refactor

Sequence Diagram(s)

sequenceDiagram
  participant ChatView
  participant PanelHostContext
  participant RegisteredSidePanel
  participant PanelRegistry
  participant PreviewSidePanel
  ChatView->>PanelHostContext: Provide active thread and panel host state
  ChatView->>RegisteredSidePanel: Render selected panel with ID and props
  RegisteredSidePanel->>PanelRegistry: Get registered panel entry
  RegisteredSidePanel->>PreviewSidePanel: Render lazy panel component
  PreviewSidePanel->>PanelHostContext: Read host state
Loading

Suggested reviewers: juliusmarminge


















Merge Risk: ⚪ Minimal · up to 41a99

This refactor registers the Preview and Diff side panels and is not intended to change what users see. Nothing in the supplied evidence points to a regression, so it looks ready to merge after the usual checks.

Security Architecture Review

Security architecture risk: ⚪ Minimal · up to 41a99

The reviewed panel and launcher flows preserve their existing thread scoping, availability checks, and annotation behavior. No material security risk introduced or worsened by this refactor was identified.

Retained concerns
No architecture-level concerns identified.

Security review details

Security Blast Radius

  • inferred — The changed capability transport affects the selected thread's panel, composer draft, and existing browser annotation workflow. Fixed bundled bodies, preserved scoped inputs, and unchanged mount ownership provide no identified expansion to another environment or thread in the reviewed flows.

Trust Boundaries and Controls

  • observed — PanelHost lends trusted bundled UI code draft and annotation capabilities; it is not an authorization boundary. The changed production call sites choose literal panel IDs, and the registry uses fixed imports. Browser-derived annotation data continues through the existing picker and composer path rather than gaining a new execution or loading channel.

Resilience and Maintainability Implications

  • inferred — Sharing launcher metadata and action construction reduces opportunities for availability controls to drift between the empty launcher and add menu. Runtime support remains combined with caller eligibility rather than replacing it.












Pre-merge checks | Passed 3 | Failed 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Docstring Coverage Warning Docstring coverage is 25.71% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 35 functions across 17 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check Warning The description is detailed and includes the problem, implementation, verification results, evidence, scope, and limitations. However, the required scope approval is missing because the author explici… Obtain explicit maintainer approval in the linked discussion or issue, then update the Scope and approval section with the approval comment. Rename the Fix section to Change to match the repository template.
✅ Passed checks (3 passed)
Check name Status Explanation
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
Title check Passed The title clearly and concisely describes the main change: registering the Preview side panel in the web application.


Full details: Description check

Explanation

The description is detailed and includes the problem, implementation, verification results, evidence, scope, and limitations. However, the required scope approval is missing because the author explicitly states that no maintainer has agreed to the proposed direction.




✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create a new PR












  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Comment @coderabbitai help to get the list of available commands.

@saphid
saphid force-pushed the stack/02-panel-host branch 3 times, most recently from ea5c743 to bce155d Compare October 6, 2026 16:03
@saphid

saphid commented Oct 6, 2026

Copy link
Copy Markdown
Contributor Author

Review requested

Date (UTC) Reviewer Where
2026-10-06 Maria Discord DM

Logged so this PR shows when a maintainer was asked to review it.

saphid and others added 5 commits October 10, 2026 17:21
Preview becomes the second panel on the side-panel registry that Diff
started. Each definition now also carries the panel's title, icon, launcher
letter, client support and unavailable copy, so the tabs, the empty
launcher and the add menu read one ordered list instead of three
hand-kept ones. Labels, letters, order and copy are unchanged.

Panel props are inferred from each lazily loaded body, and the caller is a
closed union, so another panel's props, unknown ids and widened ids do not
compile. ChatView lends the rendered panel a small host (thread, right
panel visibility, composer draft target, workspace mutation id and the
annotation send) instead of drilling the same props into each body; the
annotation send keeps the per-render closure it had before, and PreviewView
still drops a pick that settles after a thread switch.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
ChatView built a new PanelHost on every render, so every usePanelHost
consumer re-rendered even when no host field changed. Memoize it on its
fields and send annotations through onSendRef so the sender stays stable.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Plain server threads reuse one ChatView, so the memoized panel host's
sender could resolve to the next thread's composer when a pick settled
after a switch. The latest sender now carries its thread key, and each
host forwards only to a sender for its own thread.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
ChatView replaced the panel host's annotation sender while rendering. If
React threw that render away, an in-flight preview pick could still call
its onSend, for example one that edits a queued message instead of sending
a turn. Update the sender in a layout effect so only committed renders
lend it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@saphid
saphid force-pushed the stack/02-panel-host branch from f8c9fce to 16877e3 Compare October 10, 2026 07:06

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XL 500-999 changed lines (additions + deletions). vouch:trusted PR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant