Skip to content

Embed code review surface in frontend app#755

Open
backnotprop wants to merge 31 commits into
feat/frontend-initial-viewfrom
feat/frontend-code-review-surface
Open

Embed code review surface in frontend app#755
backnotprop wants to merge 31 commits into
feat/frontend-initial-viewfrom
feat/frontend-code-review-surface

Conversation

@backnotprop
Copy link
Copy Markdown
Owner

Summary

Layer 6 in the daemon stack. Embeds the production code review component inside the frontend app's session route, with session-scoped API routing via React context.

  • useSessionFetch hook + SessionProvider — React context that shadows fetch with a session-scoped version. When inside a SessionProvider, fetch("/api/diff") rewrites to fetch("/s/:sessionId/api/diff"). Fallback returns global fetch when no provider is present. 9 unit tests.
  • Migrated 53 fetch call sites — 28 in shared hooks (packages/ui/hooks/), 25 in code review package. Each gets const fetch = useSessionFetch() at the top — existing fetch calls unchanged via variable shadowing.
  • ReviewAppEmbedded export — strips ThemeProvider, TooltipProvider, Toaster (shell provides these). Keeps ReviewStateProvider + JobLogsProvider (internal). Uses h-full instead of h-screen. Accepts headerLeft prop for global sidebar trigger.
  • Session route mounts ReviewAppEmbedded wrapped in SessionProvider when session.mode === "review". Other modes show placeholder.
  • Single Tailwind build — removed duplicate @import "tailwindcss" from code review CSS. Frontend's styles.css scans all packages via @source directives.
  • CSS cleanup — renamed conflicting @keyframes fade-in, removed duplicate scrollbar rules, fixed dark: variant with @custom-variant dark
  • Integration fixes — Vite proxy narrowed to API calls only (page refreshes serve SPA), temp dirs excluded from project registry, ~ resolved in addProject, document.title and CSS properties cleaned up on unmount, __APP_VERSION__ defined

Stack

Layer PR Branch
L1 #733 feat/single-server-runtime
L2 #734 feat/plannotator-daemon-runtime
L3 #738 feat/runtime-frontend-shell
L4 #744 feat/websocket-event-hub
L5 #753 feat/frontend-initial-view
L6 this feat/frontend-code-review-surface

Test plan

  • bun run dev:frontend — daemon + frontend start
  • Add a project, click Code Review — session creates, navigates to /s/:id
  • Code review renders — diff, file tree, annotations, dockview tabs all work
  • Network tab: API calls go to /s/:sessionId/api/...
  • Global sidebar trigger (top-left) opens shell sidebar
  • File tree toggle (next to sidebar trigger) toggles file panel
  • Sidebar logo links to homepage
  • Navigate away and back — draft restore works
  • Page refresh on /s/:id — SPA loads (not debug shell)
  • Theme toggle in sidebar switches light/dark across review surface
  • bun run --cwd apps/frontend check passes
  • bun test — 1,460 pass (3 pre-existing flaky daemon runtime tests)

packages/plannotator-code-review — copy of packages/review-editor
packages/plannotator-plan-review — copy of packages/editor

Unmodified copies to start. These will be refactored to strip
standalone providers (ThemeProvider, TooltipProvider, Toaster)
and accept session-scoped API context from the frontend shell.
The original packages remain untouched for the legacy single-file
HTML flow.
React context that scopes fetch calls to a daemon session.
When inside a SessionProvider, fetch("/api/diff") rewrites to
fetch("/s/:sessionId/api/diff"). Without a provider, returns
the global fetch unchanged.
Add const fetch = useSessionFetch() to 10 hooks in packages/ui/hooks/.
The shadowed fetch variable routes /api/ calls through the session
context when a SessionProvider is present, and falls back to global
fetch when not.

configStore.ts uses apiFetch (import-based) since it's a class method.
Add const fetch = useSessionFetch() to all 11 files in
packages/plannotator-code-review/ that call fetch("/api/...").
The shadowed fetch variable routes calls through the session
context. No fetch call sites were modified — only the function
that provides the fetch was changed.
ReviewApp accepts __embedded prop to skip ThemeProvider,
TooltipProvider, and Toaster (shell provides these). Uses h-full
instead of h-screen when embedded. ReviewAppEmbedded is a named
export that passes the prop. Default export unchanged.

Shell Layout gains TooltipProvider for code review tooltips.
When session.mode === "review", the /s/:sessionId route wraps
ReviewAppEmbedded in a SessionProvider and renders the full
code review UI. Other modes keep the placeholder.

- Added @plannotator/code-review as frontend dependency
- Created App.d.ts type declaration for the code review package
- Added plannotator-ui.d.ts for SessionProvider and ThemeProvider types
- Added PNG module declaration for asset imports
- Fixed settings.ts satisfies type for strict-mode compatibility
- Vite alias resolves to package source for bundling
- TypeScript uses .d.ts for type checking (avoids strict-checking loose package source)
- Add @custom-variant dark to code-review CSS for .light class toggle
- Remove forced theme cookies from main.tsx (use defaultColorTheme prop)
- Clean up document.title on review unmount (restore previous)
- Clean up CSS custom properties on review unmount
- Define __APP_VERSION__ from root package.json in vite config
- Narrow Vite proxy to /s/:id/api/ only (page loads stay with Vite SPA)
- Skip auto-registering temp directory projects in session factory
- Add max-height scroll to project table for overflow
- Add headerLeft prop to ReviewAppEmbedded for global sidebar trigger
- Fix header padding when sidebar trigger is present
The code review's index.css had its own @import "tailwindcss" with
separate @source and @theme directives, producing a second Tailwind
build that competed with the frontend's styles.css. This caused
buttons, dialogs, and other components to render with wrong styles.

Fix: remove Tailwind, theme import, and @source from the code
review's index.css (keep only dockview + custom CSS). Add @source
directives to the frontend's styles.css to scan the code review
package and shared UI components. One Tailwind build, one theme,
no conflicts.
- Rename code review's @Keyframes fade-in to cr-fade-in to avoid
  collision with the frontend's fade-in (different animation)
- Remove redundant panel scrollbar rules from styles.css (global
  scrollbar rules already cover all elements)
- Add comment noting intentional scrollbar override of theme.css
- Single Tailwind build, single theme import, zero duplications
- Strip machine-generated prefixes from session labels (plugin-review-,
  claude-code-, etc.) to show just the project/PR name
- Add pr-7 padding to menu buttons so truncation ellipsis doesn't
  overlap with the status badge dot
- Full label still visible on hover via tooltip
The useSessionFetch test replaced globalThis.fetch with a mock in
beforeEach but never restored it. Other test files running in the
same process (daemon runtime tests) got the mock instead of real
fetch, causing JSON parse failures on "ok" responses.

Added afterEach to restore the original fetch. All 1,463 tests pass.
Sessions now stay alive when the user navigates away. Instead of
unmounting and remounting on each navigation, visited sessions are
hidden via React's <Activity mode="hidden"> and restored instantly
when the user returns.

- AppStore tracks visitedSessions (keyed by session ID) and activeSessionId
- Layout renders all visited sessions in <Activity> wrappers — only
  the active one is visible, the rest are hidden but preserved
- Session route registers its bootstrap data with the store and
  renders nothing — Layout owns the rendering
- Landing page deactivates the current session when navigated to
- SessionSurface component extracted to handle mode-based rendering

Effects clean up when hidden (WebSocket subs, timers stop) and
restart when visible. DOM, React state, scroll position, annotations,
dock layout all survive navigation.
Activity uses display:none which breaks Pierre diffs' virtualizer —
it measures the container at zero height and renders no content.
When made visible again, the virtualizer doesn't recalculate.

Switch to visibility:hidden + position:absolute which preserves
element dimensions. Pierre diffs keeps its measurements, Dockview
keeps its layout. The tradeoff is effects don't pause for hidden
sessions, but that's preferable to broken diffs.
Drafts are keyed by diff content hash on the server. Same diff = same
draft. When a draft exists on mount, it's now restored automatically
with a subtle toast notification instead of a blocking dialog.

- useCodeAnnotationDraft takes an onRestore callback instead of
  returning draftBanner/restoreDraft/dismissDraft
- ConfirmDialog for draft restore removed from App.tsx
- Toast shows "Restored N annotations" on auto-restore
…cceeds

- registerProject now finds existing entries by cwd (not name), so two
  repos with the same name get separate entries
- removeProject takes cwd instead of name for unambiguous deletion
- Server DELETE /daemon/projects now accepts JSON body with cwd
- Session factory defers registerProject until after session creation
  succeeds, avoiding phantom entries from failed requests
- Hub-client: add missing scheduleReconnect after protocol error frames
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.

1 participant