Add @amika/reviews — code review library with diffs/trees UI - #194
Draft
dbmikus wants to merge 14 commits into
Draft
Add @amika/reviews — code review library with diffs/trees UI#194dbmikus wants to merge 14 commits into
dbmikus wants to merge 14 commits into
Conversation
Plan for a TypeScript/React code review library at js/reviews. Bootstraps a pnpm workspace at the repo root, mirrors conventions from amika-mono/js/components, and breaks the work into 12 commits covering parser, store, React UI, deep linking, an example SPA, and a Playwright E2E suite.
Adds repo-root pnpm workspace (pnpm@10.18.2, packages: js/*) and the first JS package, @amika/reviews, with conventions copied from amika-mono/js/components: TS strict, ESLint flat config + typescript-eslint, Prettier, Vitest with jsdom + @testing-library/jest-dom setup. Empty src/index.ts, src/headless.ts, and src/styles.css will be filled in subsequent commits. All four scripts (typecheck, lint, formatcheck, test) pass on the empty package.
Introduces the public data model (ReviewItem, ParsedPatch, Hunk, etc.) and two entry points for loading patches: loadFromFiles for upload events and loadFromText for raw strings. parsePatch wraps parse-diff and normalizes to a library-agnostic shape so we can swap parsers later. Stable IDs come from a synchronous FNV-1a hash of the patch text (util/hashItemId), so shareable deep-links survive reloads. Bumps tsconfig target to ES2020 to allow BigInt literals. 11 unit tests over five fixtures (single, multi-file, rename, binary, malformed) all pass.
Pure reducer covers add/edit/delete/reply/resolve, idempotency on SET_RESOLVED, recursive descendant removal on DELETE_COMMENT, and import/load resets selection. createReviewStore is framework-agnostic (getState/subscribe + full ReviewAPI), takes injectable now() and newCommentId() for deterministic tests, and accepts a pluggable PersistenceAdapter (localStorage adapter included; key-scoped, ignores quota/SSR errors, listens for storage events). ReviewExportV1 is flat: items + comments arrays plus schemaVersion and exportedAt. import() validates the shape before dispatching. The package index now re-exports the full headless surface; @amika/reviews and @amika/reviews/headless deliver the same TS API today (UI lands in later commits). Coverage: 35 tests across reducer, store, parser. All green.
ReviewProvider creates (or accepts) a store and exposes it via a single
context. State subscription is via useSyncExternalStore so re-renders are
driven directly by the store's listener set.
Hooks: useReview (the API, stable identity), useReviewState (full state),
usePatches, useComments (memoized by filter slices), useSelectedFile
(resolves selection against current items, returns null when stale).
Index re-exports the React surface alongside the headless API, so
consumers can `import { ReviewProvider, useReview } from "@amika/reviews"`
without pulling pierre deps until they reach for a UI component.
8 RTL tests cover stable identity, mount-only construction, reactive
updates, filtered comment queries, and stale-selection resolution. Total
43 tests across the package, all green.
FileTreePanel reads the unique set of file paths across all loaded items, renders them via useFileTree, and routes selection back to the store (or to an optional onSelectFile prop). Comment counts at file or line scope appear as row decorations on each file. Path collisions across items resolve to the most recently loaded item so a single tree view stays coherent when patches in a series touch the same file. (A future revision could namespace paths per item.) Two RTL smoke tests confirm the panel mounts in both loaded and empty states; deeper interaction lives in the Playwright suite since the tree's interior is rendered inside a shadow root.
ItemView dispatches by ReviewItem kind to the right @pierre/diffs/react component (PatchDiff, MultiFileDiff, FileDiff, File, UnresolvedFile), forwarding line-scope comments as DiffLineAnnotation / LineAnnotation. Side mapping: amika's "old" / "new" maps to pierre's "deletions" / "additions" at the annotation boundary so the diff renderer puts comments on the correct gutter. CommentForm is a small headless composer (textarea + submit + cancel). CommentThread renders root + replies with edit / delete / resolve actions and an inline reply form. FileCommentPanel composes the thread + a new- comment form scoped to a single file. tsconfig wires Vitest cleanup so each component test gets a fresh DOM (@testing-library/react auto-cleanup isn't detected under vitest). 57 tests pass: reducer, store, parser, annotations, plus full RTL coverage of CommentForm, CommentThread, FileCommentPanel, FileTreePanel mount, and ReviewProvider hooks.
Two sidebar panels mirroring FileCommentPanel at coarser scopes:
ItemCommentPanel for a single uploaded patch/commit, SeriesCommentPanel
for the entire review (all items together). Both reuse CommentThread for
reply/edit/delete/resolve.
Documents the convention in the panel chrome: one uploaded .diff/.patch
file is one ReviewItem ("patch comments"); the in-memory collection of
items is the series ("review comments").
4 new RTL tests; 61 total in the package.
CodeReview is the top-level composition: three-pane layout (file tree left, diff middle, comment sidebar right) with a header bar holding the upload dropzone and export button. Provides initialItems, persistKey, author, store, and headerExtras props; default-selects the first file when items arrive. UploadDropzone accepts .diff / .patch files via drag-drop or native picker and routes them through store.loadFromFiles. ExportButton serializes ReviewExportV1 and triggers a browser download (or hands JSON to a custom onExport handler). styles.css ships minimal selectors against the data-amika attributes our components emit; hosts can drop it in once or skip it entirely. Pierre internals stay self-styled via their shadow DOM. CodeReview tests stub @pierre/diffs/react at vi.mock level since jsdom lacks CSSStyleSheet.replaceSync; full diff rendering is covered in the Playwright suite (commit 11). 65 vitest tests still green.
ReviewLocation describes what's focused (none / item / file / line /
comment). Serializes to flat URL search params (?item=...&file=...
&line=...&side=...) or just ?comment=<id>; the comment form self-resolves
to its stored scope.
Store gains navigate(), getLocation(), locationToSearch/locationFromSearch,
and an onScrollRequest emitter so the UI can scroll the diff viewer to a
target line/comment. Comment-link navigation walks the comment's scope,
selects the right item+file, and fires the scroll request.
ReviewProvider gets a linkSync prop (boolean or {read, write, subscribe}
adapter). When true, the provider binds to window.location.search: hydrates
on mount, pushes on selection change, listens for popstate. Adapter form
lets Next.js / React Router callers route through their own history.
useReviewLocation hook + CopyLinkButton (writes baseHref + ?search to
clipboard) round out the public surface.
19 new tests (store.navigate, location serialize/parse round-trip,
CopyLinkButton). Total: 84 vitest tests, all green.
A real Vite + React app at js/reviews-example demonstrating the full @amika/reviews surface. Wires <CodeReview> inside a <ReviewProvider> with linkSync enabled and a small toolbar for loading fixture patches or resetting state. URL flags supported by the demo: - ?persistKey=<name> — persist state to localStorage - ?author=<name> — default comment author - ?fixtures=1 — auto-load fixtures on mount (used by Playwright) Fixtures (simple.patch, multi-file.patch, rename.patch) live in public/fixtures and are copies of the unit-test fixtures so behavior is identical between layers. `pnpm --filter @amika/reviews-example run typecheck build` succeeds. Also surfaces the React API from @amika/reviews's main entry point — the package now exports every component, hook, and provider directly (the headless entry continues to expose only the framework-agnostic pieces).
Twelve scenarios covering the full flow: auto-loading fixtures, adding comments at each scope, replying, file-level commenting, persistence across reloads via persistKey, deep linking by item/file (linkSync roundtrips through window.location), and Export JSON download with the ReviewExportV1 schema check. Tests target the data-amika="..." hooks the components emit so they remain stable across CSS or layout tweaks and don't reach inside pierre's shadow DOM. Run via: pnpm --filter @amika/reviews-example run test:e2e In environments where the chromium binary needs glib/libnss3/etc, run `pnpm exec playwright install --with-deps chromium` (or install the system packages: libglib2.0-0 libnss3 libatk1.0-0 libatk-bridge2.0-0 libcups2 libxkbcommon0 libatspi2.0-0 libxcomposite1 libxdamage1 libxrandr2 libgbm1 libasound2) before running. Browser launch was verified locally; not yet verified in this build environment because system libs are unavailable.
README covers install, two quick-start flows (drop-in <CodeReview> vs. composed primitives), the imperative loading API, the five ReviewItem kinds and which pierre component each maps to, the public ReviewStore TypeScript shape, comment scopes + replies, the ReviewExportV1 schema, opt-in localStorage persistence, deep linking with linkSync + ReviewLocation, headless usage from @amika/reviews/headless, styling/theming pointers into pierre's CSS variables, and how to run the example SPA + Playwright suite. Closes the package build-out: every commit from the plan is in.
- Add PatchNavigator to the example toolbar: ← label (N/M) → buttons that step through loaded items, auto-selecting the first file of each patch so the diff viewer shows content immediately. - Thread the outer ReviewProvider store into CodeReview so the toolbar and diff viewer share a single store instead of operating on two independent ones. - Fix ItemView to extract a single-file slice from multi-file patches before passing to PierrePatchDiff, which requires exactly 1 file diff. Uses the selectedPath when available, falls back to the first parsed file to avoid the race before useDefaultSelection fires. - Extract patch subject from git-format Subject: header for navigator label; falls back to truncated content hash.
dbmikus
force-pushed
the
js-reviews-package
branch
from
June 9, 2026 23:44
523ac3d to
c09770d
Compare
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.
Summary
amika) and adds@amika/reviewsatjs/reviews: a TypeScript/React library for reviewing code with.diff/.patchupload, file tree, per-file diff viewer, threaded comments at line / file / patch / series scope, deep linking, opt-inlocalStoragepersistence, and aReviewExportV1JSON export.@pierre/diffs/reactand@pierre/trees/react; state is plain React (useReducer+ split contexts +useSyncExternalStore) — no Jotai. Same surface is also exported headlessly as@amika/reviews/headless.@amika/reviews-exampleatjs/reviews-example: a real Vite + React SPA demonstrating the package with checked-in fixture patches and a Playwright E2E suite (12 scenarios covering load / comment / reply / file-comments / persistence / deep-link / export).plans/2026-05-15-amika-reviews-package.md.Built across 13 commits so reviewers can read it incrementally.
Test plan
pnpm installsucceeds at the repo rootpnpm --filter @amika/reviews run typecheckis greenpnpm --filter @amika/reviews run lintis greenpnpm --filter @amika/reviews run formatcheckis greenpnpm --filter @amika/reviews run test— 84 vitest tests pass (reducer, store, parser, location serializer, annotations, plus RTL coverage of CommentForm / CommentThread / FileCommentPanel / ItemCommentPanel / SeriesCommentPanel / FileTreePanel / ReviewProvider hooks / CodeReview / UploadDropzone / ExportButton / CopyLinkButton)pnpm --filter @amika/reviews-example run typecheck buildsucceedspnpm --filter @amika/reviews-example exec playwright install --with-deps chromiumthenpnpm --filter @amika/reviews-example run test:e2epasses (browsers need system libs — sandbox in this run did not have them; verified test file shape only)pnpm --filter @amika/reviews-example run dev, openhttp://localhost:5173/?fixtures=1, drop a patch, leave a comment at each scope, hit Export, check the JSONNotable choices baked in
.diff/.patchfile = oneReviewItem(kindpatch); the in-memory collection = the series. Documented in the README.ReviewItemkinds map 1:1 to the five@pierre/diffs/reactcomponents (PatchDiff,MultiFileDiff,FileDiff,File,UnresolvedFile). Upload always producespatch; the other kinds are for programmatic callers.data-amika="..."selectors so hosts can replace or removestyles.cssfreely.?item=...&file=...&line=...&side=...or?comment=...).linkSyncprop binds towindow.location; pass an adapter for Next.js / React Router.ReviewItems directly.