Skip to content

Optimize fragment rendering and homepage link generation - #67

Merged
baanish merged 8 commits into
mainfrom
codex/optimize-codebase
Jun 19, 2026
Merged

baanish merged 8 commits into
mainfrom
codex/optimize-codebase

Conversation

@baanish

@baanish baanish commented May 12, 2026

Copy link
Copy Markdown
Owner

Summary

  • Tightened fragment, ARX, diff, markdown, CSV, and JSON hot paths with loop-based and cached implementations.
  • Reduced homepage and viewer shell work by precomputing sample metadata and caching fragment module loading.
  • Added build-budget enforcement, base-path helpers, and refreshed docs, assets, and tests to match the current static-export flow.

Testing

  • npm run check
  • npm run test:e2e
  • git diff --check
  • Browser verification with Playwright against the built preview, including homepage, generated links, and ARX2 rendering

Summary by CodeRabbit

  • New Features
    • Added arx3 fragment codec support with improved visible-length selection and updated ARX/ARX2 payload behavior.
    • Introduced an artifact-first viewer experience with raw/render toggle, improved copy/download, sample links, and enhanced JSON native raw source view.
    • Server now supports Brotli-precompressed .br asset delivery with correct headers; builds also add assets:compress.
  • Bug Fixes
    • Improved base-path handling and preview subpath auto-detection; corrected Brotli response headers and cache behavior.
  • Documentation
    • Updated deployment, architecture, payload/codec, testing, and URL fragment docs for the new transport/renderer behavior.
  • Tests
    • Expanded unit, integration, and E2E coverage, including build-budget checks, renderer readiness, codec selection, and Brotli integrity.

Review response (commit 32e3efc)

Addressed the automated review of this branch.

Fortified the two heuristic numbers into defended contracts

  • arx3 selection metric: reframed as an explicitly owned visible-length policy and pinned with a characterization test (arx3 baseBMP is selected over arx2 by visible length, not a real byte-size win).
  • build budgets: added per-budget rationale, extracted a pure evaluateBudget helper (+ .d.mts types), and pinned the policy table and boundary semantics.

Coverage + docs

  • Documented the deliberate un-highlighted raw view (plain <pre>, no CodeMirror on the raw path) and pinned it with a test.
  • Added server-level tests for the precompressed .br header contract (Content-Encoding: br + decompressed Content-Type + Vary) and immutable _next/static caching, for both serve-export and the self-hosted server.
  • Diff renderer: CR-tolerant binary-marker gate, named the hash-based reset key as a deliberate bound, and a CRLF binary-patch regression test.
  • Pinned the showcase benchmark figures to scripts/bench-baseline.json.

Two review suggestions reverted after verification — the build-budget gate caught them

  • getVisibleHashLength in the viewer shell is intentionally a local copy of getVisibleFragmentLength: deduping it pulls the codec module (lz-string/fflate) into the homepage initial chunk and breaches check:build-budgets. Documented in place instead.
  • defaultLinkCreatorDraft/getBodyFieldLabel were never duplicated — they live only in the home component; moving them into the lib pulled codec deps into the homepage chunk. Reverted.

npm run check (lint + unit + bench + typecheck + build + budgets) is green; homepage route JS is 107.4 / 115.0 KiB.

Scope note

This branch is a cohesive optimization pass. The removed deps (clsx, tailwind-merge, next-themes, @tanstack/react-table, @codemirror/commands/search, @git-diff-view/file) are load-bearing consequences of the code changes and can't be separated without dead deps or a broken build. The one cleanly separable change is the Next 15.1 → 15.5 bump (+ matching eslint-config-next and the postcss override) — happy to peel that into its own prerequisite PR if you'd prefer it reviewed in isolation.

@coderabbitai

coderabbitai Bot commented May 12, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds ARX3 transport and dictionary loading, updates fragment sizing and selection, rewires viewers and renderers for lazy client loading and raw-source paths, normalizes base paths, and expands static-serving, scripts, docs, and tests around the new codec and asset flow.

Changes

ARX3 transport and fragment flow

Layer / File(s) Summary
Transport codec changes
src/lib/payload/arx-codec.ts, src/lib/payload/fragment-arx.ts, src/lib/payload/fragment.ts, src/lib/payload/link-creator.ts, src/lib/payload/schema.ts, scripts/bench-codecs.mjs, scripts/bench-baseline.json, public/arx2-dictionary.json
Adds ARX3 and precompressed dictionary loading, refactors trie/build and tuple-envelope handling, updates fragment sizing and candidate selection, and records the new benchmark and codec baseline data.
Viewer and renderer updates
src/components/viewer-shell.tsx, src/components/viewer/artifact-stage.tsx, src/components/renderers/*, src/components/theme/*, src/components/viewer/artifact-selector.tsx, src/app/layout.tsx, src/app/globals.css, src/app/security/page.tsx, src/app/url-explainer/page.tsx
Switches viewer and renderer surfaces to lazy client loading, readiness refs, raw-source rendering, and theme-controller wiring while adjusting navigation and styling to the new base-path-aware layout.
Base path, hosting, and static assets
src/lib/site/base-path.ts, src/lib/site/canonical-base.ts, next.config.ts, next-env.d.ts, scripts/serve-export.mjs, selfhosted/server.ts, public/_headers, public/vendor/diff-view-pure.css
Adds shared base-path helpers, normalizes deployment paths, precomputes static file serving with Brotli and cache headers, and adds vendored diff-view CSS plus matching response headers.
Scripts and config
scripts/compress-dictionary.mjs, scripts/check-build-budgets.mjs, scripts/check-build-budgets.d.mts, scripts/ensure-next-types.mjs, scripts/clean-build-output.mjs, package.json, playwright.config.ts, eslint.config.mjs, tsconfig.json
Updates compression, build-budget, type-generation, cleanup, test, and lint scripts/configuration to support the new asset and budget checks.
Docs and skills
README.md, docs/*, skills/*
Documents ARX3, precompressed dictionary behavior, preview/base-path handling, build-budget and asset-compression commands, and updated transport/linking guidance.
Tests and fixtures
tests/*, tests/e2e/viewer.spec.ts, tests/fixtures/baanish-code-bench-report.md
Adds and updates unit, component, integration, and e2e coverage for ARX3 encoding/decoding, raw views, readiness, static headers, server routing, and build budgets.

Sequence Diagram(s)

sequenceDiagram
  participant Browser
  participant ViewerShell
  participant FragmentModule
  participant ArtifactStage
  participant StaticServer
  Browser->>ViewerShell: hash change / initial payload
  ViewerShell->>FragmentModule: decodeFragmentAsync(hash, options)
  FragmentModule-->>ViewerShell: { envelope, activeArtifact } or error
  ViewerShell->>ArtifactStage: render(activeArtifact) (dynamic import)
  ArtifactStage->>ArtifactStage: load renderer (code/json/markdown/diff/csv)
  ArtifactStage-->>ViewerShell: onRendererReady(readyKey)
  Browser->>StaticServer: GET /arx-dictionary.json.br or /vendor/diff-view-pure.css.br
  StaticServer-->>Browser: precompressed asset (Content-Encoding: br)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • baanish/agent-render#4: Extends the earlier ARX codec and fragment transport pipeline in src/lib/payload/arx-codec.ts and src/lib/payload/fragment*.ts.
  • baanish/agent-render#11: Related link-creator codec-selection work in src/components/home/link-creator.tsx and src/lib/payload/link-creator.ts.
  • baanish/agent-render#27: Touches the same self-hosted static serving path in selfhosted/server.ts.

Poem

A rabbit packed bytes in a tidy row,
Brotli zipped them nice and slow.
Hop by hop the hashes shine,
Raw and rendered both align.
Little ears applaud the flow.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.52% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Optimize fragment rendering and homepage link generation' is clear, concise, and accurately describes the primary change of the PR.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/optimize-codebase

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented May 12, 2026

Copy link
Copy Markdown

Deploying agent-render with  Cloudflare Pages  Cloudflare Pages

Latest commit: 648ed11
Status: ✅  Deploy successful!
Preview URL: https://1599fd60.agent-render.pages.dev
Branch Preview URL: https://codex-optimize-codebase.agent-render.pages.dev

View logs

@baanish
baanish marked this pull request as ready for review May 12, 2026 14:35

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3b61cac405

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/components/viewer-shell.tsx Outdated
Comment on lines +114 to +115
fragmentModulePromise ??= import("@/lib/payload/fragment");
return fragmentModulePromise;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Reset cached fragment import promise on load failure

If the dynamic import of @/lib/payload/fragment fails once (for example due to a transient chunk/network error), fragmentModulePromise stays set to that rejected promise forever, so every later decode/encode attempt in the same session immediately fails even after connectivity recovers. This turns a temporary asset-load issue into a persistent viewer/link-creator outage until full page reload; clear the cache when the import rejects so retries can succeed.

Useful? React with 👍 / 👎.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/components/viewer-shell.tsx (1)

201-204: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Clear injected mode on internal navigation.

After a UUID-injected load, setFragmentHash() and handleGoHome() update hash without a hashchange event, so injectedPayloadRef.current stays true. Every later decode in that session then keeps using { skipFragmentBudget: true }, which silently disables the normal fragment budget.

🔧 Suggested fix
   const setFragmentHash = useCallback((nextHash: string) => {
+    injectedPayloadRef.current = false;
     if (window.location.hash === nextHash) {
       return;
     }

     window.history.replaceState(null, "", nextHash);
     setHash(nextHash);
   }, []);

   const handleGoHome = useCallback(() => {
+    injectedPayloadRef.current = false;
     const url = window.location.pathname + (window.location.search || "");
     window.history.replaceState(null, "", url);
     setHash("");
   }, []);

Also applies to: 233-235, 297-310

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/viewer-shell.tsx` around lines 201 - 204, The
injectedPayloadRef flag must be cleared whenever the app updates the location
hash internally so subsequent decodes don't keep using skipFragmentBudget;
update the routines that mutate hash — specifically syncHash, setFragmentHash,
handleGoHome and the similar hash-update handlers around the other occurrences
(the blocks at the locations analogous to 233-235 and 297-310) to set
injectedPayloadRef.current = false immediately before calling
setHash(window.location.hash) (or before updating the hash state) so injected
mode is cleared on internal navigation.
🧹 Nitpick comments (9)
tests/envelope.test.ts (1)

13-25: ⚡ Quick win

Add a regression test for empty bundles.

This PR now rejects artifacts.length === 0, but that new path is still untested here.

Suggested test addition
 describe("normalizeEnvelope", () => {
   it("normalizes single-artifact bundles to the only artifact id", () => {
@@
     expect(result.envelope.activeArtifactId).toBe("one");
   });
+
+  it("rejects bundles with no artifacts", () => {
+    const result = normalizeEnvelope({
+      ...baseEnvelope,
+      artifacts: [],
+    });
+
+    expect(result.ok).toBe(false);
+    if (result.ok) {
+      return;
+    }
+    expect(result.message).toMatch(/at least one artifact/i);
+  });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/envelope.test.ts` around lines 13 - 25, Add a regression test that
exercises the new code path rejecting envelopes with zero artifacts: create a
variant of baseEnvelope where artifacts is an empty array and call
normalizeEnvelope; assert the result is a failure (result.ok is false) and
optionally assert the error or rejection reason exists. Place this alongside the
existing tests (e.g., near the "normalizes single-artifact bundles..." test) and
reference normalizeEnvelope and baseEnvelope so the CI covers the new
artifacts.length === 0 branch.
scripts/bench-codecs.mjs (2)

176-179: 💤 Low value

Note: median mutates the input.

values.sort(...) sorts in place. Callers (measure, line 189) pass samples and then return { result, ms: median(samples) } without using samples again, so it's safe today—but future readers may be surprised. A defensive [...values].sort(...) is a one-line guard.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/bench-codecs.mjs` around lines 176 - 179, The median function
currently mutates its input by calling values.sort(...); update median to sort a
shallow copy instead (e.g., use [...values] or Array.from(values]) before sort)
so callers like measure (which passes samples) are not affected by in-place
mutation; keep the rest of median logic the same and return 0 for empty arrays
as before.

211-219: 💤 Low value

Minor: repeatedFixture may slice mid-character for multi-byte input.

fixture.slice(0, targetLength) operates on JavaScript string units (UTF-16 code units), not bytes. With ASCII fixtures this is fine, but if anyone passes a block with multi-byte/emoji content the slice could split a surrogate pair. Not an issue with current fixtures—just flagging for future use.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/bench-codecs.mjs` around lines 211 - 219, The current repeatedFixture
builds a string then uses fixture.slice(0, targetLength), which can split UTF-16
surrogate pairs for multi-byte characters; update repeatedFixture to trim by
Unicode code points instead of UTF-16 code units — e.g., replace the final slice
with a safe truncation using Array.from(fixture) (or [...fixture]) and join the
first targetLength code points (Array.from(fixture).slice(0,
targetLength).join('')) so block, segmentSuffix and index logic remain unchanged
but surrogate pairs/emoji won't be sliced in half.
src/components/home/link-creator.tsx (1)

36-79: 💤 Low value

Consider co-locating constants in a module helper if reuse grows.

fieldHints, fieldPlaceholders, codecOptions, defaultLinkCreatorDraft, and getBodyFieldLabel are now owned by this component. If the same draft/labels logic is ever needed by another surface (e.g., a docs preview or the viewer shell), pulling them into a small link-creator-presets.ts would avoid drift. Not required for this PR.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/home/link-creator.tsx` around lines 36 - 79, The constants and
small helpers used only in this component—fieldHints, fieldPlaceholders,
codecOptions, defaultLinkCreatorDraft, and getBodyFieldLabel—should be moved
into a shared module so they can be reused and kept in sync; create a new module
(e.g., link-creator-presets) exporting those symbols, update
src/components/home/link-creator.tsx to import fieldHints, fieldPlaceholders,
codecOptions, defaultLinkCreatorDraft, and getBodyFieldLabel from that module,
and ensure any types (ArtifactKind, LinkCreatorDraft) remain imported or
re-exported as needed so behavior of the existing functions (getBaseUrl,
numberFormatter, etc.) is unchanged.
scripts/serve-export.mjs (2)

60-92: 💤 Low value

Consider extending Brotli handling to other pre-compressed assets.

The current logic only flags .json.br and .css.br as Brotli-encoded. If the build ever emits other .br variants (e.g., .js.br, .svg.br, .html.br), they'll be served with Content-Type: application/octet-stream and no Content-Encoding: br, breaking the client. A general .br handler that derives the underlying MIME from the inner extension would be safer.

♻️ Proposed generalization
 function contentTypeFor(filePath) {
   if (filePath.endsWith(`${path.sep}.well-known${path.sep}api-catalog`)) {
     return apiCatalogContentType;
   }

-  if (filePath.endsWith(".json.br")) {
-    return "application/json; charset=utf-8";
-  }
-
-  if (filePath.endsWith(".css.br")) {
-    return "text/css; charset=utf-8";
-  }
+  if (filePath.endsWith(".br")) {
+    const innerExt = path.extname(filePath.slice(0, -3));
+    return contentTypes.get(innerExt) || "application/octet-stream";
+  }

   return contentTypes.get(path.extname(filePath)) || "application/octet-stream";
 }
-  if (filePath.endsWith(".json.br") || filePath.endsWith(".css.br")) {
+  if (filePath.endsWith(".br")) {
     headers["Content-Encoding"] = "br";
     headers.Vary = "Accept-Encoding";
   }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/serve-export.mjs` around lines 60 - 92, The code only treats .json.br
and .css.br as Brotli files; update contentTypeFor(filePath) and
headersFor(filePath) to handle any ".br" suffix: detect if
filePath.endsWith(".br"), derive the real ext by removing the ".br" suffix and
lookup the MIME via contentTypes.get(path.extname(realPath)) (falling back to
"application/octet-stream"), return that as the Content-Type in contentTypeFor
(and in headersFor set "Content-Encoding": "br" and "Vary": "Accept-Encoding"
whenever a ".br" file is detected), preserving existing logic for
isNextStaticAsset(filePath) and the api catalog Link header in headersFor.

159-178: 💤 Low value

Consider adding Content-Length for HEAD/GET responses.

stat() is already called and yields details.size, but it's discarded. Setting Content-Length lets clients (and CI HEAD checks for .br assets) verify byte counts and avoids chunked-transfer mode, which can confuse some tooling and proxies. Worth doing here since this is the preview/self-hosted entrypoint validated in the PR's tests.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/serve-export.mjs` around lines 159 - 178, The responses currently
send headers from headersFor(finalPath) but omit Content-Length even though a
prior stat() call yields details.size; update the handler to call fs.statSync
(or reuse the existing stat result) for finalPath and add a "Content-Length"
header with details.size to the headers passed to response.writeHead (for both
GET and HEAD paths), ensuring the HEAD branch still ends after sending headers
and the GET branch streams via createReadStream(finalPath); reference the
symbols finalPath, headersFor, response.writeHead, method, and createReadStream
when making the change.
src/components/renderers/diff-renderer.tsx (2)

198-201: 💤 Low value

Edge case: module-level stylesheet state isn't reference-counted across instances.

diffViewStylesheetPromise and the <link id="agent-render-diff-view-styles"> element are module globals. If two DiffRenderer instances coexist (e.g., a future split layout, or a stale rich instance + a newly mounted fallback during transitions), the fallback effect at line 624 will call removeDiffViewStylesheet() and strip the stylesheet that another rich instance is still using.

Today the viewer shell renders one artifact at a time, so this is not user-visible. If multi-diff layouts ever land, consider reference-counting active rich instances before removing the link.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/renderers/diff-renderer.tsx` around lines 198 - 201, The
stylesheet globals (diffViewStylesheetPromise and DIFF_VIEW_STYLESHEET_ID) are
not reference-counted, so removeDiffViewStylesheet() can remove the shared
<link> while another DiffRenderer still needs it; add a module-level counter
(e.g., diffViewInstanceCount) and update the lifecycle helpers so mounting a
rich renderer increments the count and unmounting decrements it, and change
removeDiffViewStylesheet() to only remove the DOM node and clear
diffViewStylesheetPromise when the count reaches 0; update the places that
previously called removeDiffViewStylesheet() (the effect in DiffRenderer that
tears down the fallback) to use the new increment/decrement semantics so the
stylesheet is retained while any instance remains.

621-642: ⚡ Quick win

Stylesheet load failure is silently swallowed.

loadDiffViewStylesheet().catch(() => undefined).then(() => setStylesReady(true)) marks styles "ready" even when both the .br and plain .css URLs fail. The rich diff then mounts unstyled with no console signal. Consider surfacing the error (e.g., falling back to the raw patch state, or at least console.warn) so production breakage is observable.

♻️ Proposed refinement
   setStylesReady(false);
   loadDiffViewStylesheet()
-    .catch(() => undefined)
-    .then(() => {
+    .then(() => {
       if (!cancelled) {
         setStylesReady(true);
       }
-    });
+    })
+    .catch((error) => {
+      if (!cancelled) {
+        console.warn("Failed to load rich diff stylesheet", error);
+        setStylesReady(true);
+      }
+    });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/renderers/diff-renderer.tsx` around lines 621 - 642, The
current useEffect in diff-renderer.tsx swallows stylesheet load errors; update
the loadDiffViewStylesheet promise handling so failures are surfaced and you
don't silently mark styles as ready: call loadDiffViewStylesheet().then(() => {
if (!cancelled) setStylesReady(true) }).catch((err) => { console.warn('Failed to
load diff stylesheet', err); if (!cancelled) { removeDiffViewStylesheet(); /*
fall back to raw/unstyled diff */ setStylesReady(true); } }); keep the cancelled
guard and the existing checks around renderedDiff, and reference renderedDiff,
loadDiffViewStylesheet, removeDiffViewStylesheet, setStylesReady and
diffFilesHaveRenderableFile to locate the code.
src/lib/diff/git-patch.ts (1)

48-74: 💤 Low value

Minor: leading content before first diff --git is silently dropped.

In parsePatchSections, if the patch contains preamble text before the first diff --git header (e.g., a From ... mbox-style header from git format-patch), that prefix is skipped because the loop only starts accumulating after the first match. Today’s callers (UI/test fixtures) always feed plain diff --git bundles so this is not user-facing, but it’s worth noting as a known limitation if patches from git format-patch are ever piped in.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/diff/git-patch.ts` around lines 48 - 74, parsePatchSections currently
drops any leading preamble before the first DIFF_SECTION_HEADER_RE match (e.g.,
mbox "From ..." headers); update the loop in parsePatchSections so when the
first match has match.index > 0 you call parsePatchSection(normalized.slice(0,
match.index).trim(), sectionIndex) (using parsePatchSection and the current
sectionIndex), increment sectionIndex, and then set previousStart = match.index;
keep the existing logic for subsequent matches and the final push so all text
before the first "diff --git" is preserved.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@package.json`:
- Around line 95-99: Add a short explanatory comment for the "overrides" entry
that documents why "next" forces "postcss": "8.5.14" — e.g., note that Tailwind
CSS v4 requires postcss ^8.5.6 while Next.js ships postcss 8.4.31, and pinning
to 8.5.14 bridges that gap; place this comment adjacent to the overrides block
(the "overrides" -> "next" -> "postcss" entry) in package.json or add the same
note to docs/dependency-notes.md so future maintainers understand the
constraint.

In `@public/vendor/diff-view-pure.css`:
- Line 138: The vendored CSS triggers stylelint errors (e.g.,
declaration-empty-line-before and keyword-case) for lines like the rule
containing "border-spacing: var(--tw-border-spacing-x)
var(--tw-border-spacing-y);"—either normalize the file to comply with our lint
rules (fix empty-line-before formatting around declarations and normalize
keyword casing to lowercase across the affected selectors/rules) or exclude the
vendored files from linting by adding "public/vendor/**" to the stylelint ignore
pattern in the Stylelint config; update only the code around the offending
declarations or the stylelint config accordingly and ensure the change
references the same symbols (the border-spacing declaration and the listed
offending rules) so CI stops failing.

In `@src/components/renderers/markdown-renderer.tsx`:
- Around line 47-73: The getCodeLanguage function strips non-alphanumeric
characters so languages like "c++" and "c#" are reduced to "c"; update
getCodeLanguage (and its use of LANGUAGE_CLASS_PREFIX) to allow additional valid
characters such as '+' and '#' (and optionally '.' or other common alias chars)
when scanning the language token, so the loop does not break on those characters
and the returned slice preserves them (then still lower-case it and fallback to
"text" when empty).

In `@src/components/theme/use-theme-controller.ts`:
- Around line 73-125: Add a JSDoc block immediately above the exported
useThemeController function that documents what the hook does, its behavior, and
its return shape; include a short description, the fact that it reads/writes
theme via localStorage and syncs with system and cross-tab events, and an
`@returns` entry describing the returned object properties (preference,
resolvedTheme, setTheme) and their types/meanings so consumers and editors get
proper intellisense for useThemeController.

In `@src/components/viewer-shell.tsx`:
- Around line 258-263: The selection should be local-first: restore and use a
local state activeArtifactId in viewer-shell.tsx to control which artifact opens
immediately (set activeArtifactId when the user selects an artifact), derive
activeArtifact from that id (fallback to getActiveArtifact(envelope) only if
activeArtifactId is null), and avoid waiting for
encodeEnvelopeAsync()/parsed/hash round-trip to update the visible selection;
keep rendererReadyKey = activeArtifact ? hash : "" or use hash only for renderer
syncing but do not gate the UI selection on encodeEnvelopeAsync completion;
update any logic in functions/components referencing activeArtifact and
rendererReadyKey so they read from activeArtifactId-first and only reconcile
with parsed/hash results when encoding completes.

In `@src/lib/payload/arx-codec.ts`:
- Around line 499-518: In envelopeToArx2Tuple, the activeIndex check uses
`activeIndex > 0` which treats index 0 as absent; change the condition to
include zero (e.g., `activeIndex >= 0 ? activeIndex : undefined`) so the
activeArtifactId that maps to the first artifact is preserved in the tuple;
update the return that calls trimOptionalTuple ([2, artifacts, envelope.title,
activeIndex ...]) accordingly and keep the artifact loop and activeIndex
assignment (artifact.id === activeArtifactId) as-is.

In `@tests/arx-codec.test.ts`:
- Around line 543-559: The test currently only calls loadArxDictionary and never
verifies the arx2 overlay is fetched separately; update the test to also call
loadArx2OverlayDictionary (after stubbing fetch) so both URLs are requested, and
assert requests includes both "/arx-dictionary.json" and the overlay path (e.g.,
the arx2 overlay URL) in the correct order or as separate entries; keep the
vi.unstubAllGlobals() and loadArxDictionarySync(arxDictionaryJson) in finally as
before. Ensure you reference loadArxDictionary, loadArx2OverlayDictionary, and
loadArxDictionarySync when adding the second call and the new assertion.

In `@tests/components/link-creator.test.tsx`:
- Around line 72-79: The test is using the wrong element type for getByLabelText
— change the generic from HTMLInputElement to HTMLTextAreaElement for the
"Generated agent-render link" queries in link-creator.test.tsx (the lines
calling screen.getByLabelText<...>("Generated agent-render link").value and the
subsequent assertions) so the type matches the component's <textarea
aria-label="Generated agent-render link">; update all occurrences of that
generic to HTMLTextAreaElement.

---

Outside diff comments:
In `@src/components/viewer-shell.tsx`:
- Around line 201-204: The injectedPayloadRef flag must be cleared whenever the
app updates the location hash internally so subsequent decodes don't keep using
skipFragmentBudget; update the routines that mutate hash — specifically
syncHash, setFragmentHash, handleGoHome and the similar hash-update handlers
around the other occurrences (the blocks at the locations analogous to 233-235
and 297-310) to set injectedPayloadRef.current = false immediately before
calling setHash(window.location.hash) (or before updating the hash state) so
injected mode is cleared on internal navigation.

---

Nitpick comments:
In `@scripts/bench-codecs.mjs`:
- Around line 176-179: The median function currently mutates its input by
calling values.sort(...); update median to sort a shallow copy instead (e.g.,
use [...values] or Array.from(values]) before sort) so callers like measure
(which passes samples) are not affected by in-place mutation; keep the rest of
median logic the same and return 0 for empty arrays as before.
- Around line 211-219: The current repeatedFixture builds a string then uses
fixture.slice(0, targetLength), which can split UTF-16 surrogate pairs for
multi-byte characters; update repeatedFixture to trim by Unicode code points
instead of UTF-16 code units — e.g., replace the final slice with a safe
truncation using Array.from(fixture) (or [...fixture]) and join the first
targetLength code points (Array.from(fixture).slice(0, targetLength).join(''))
so block, segmentSuffix and index logic remain unchanged but surrogate
pairs/emoji won't be sliced in half.

In `@scripts/serve-export.mjs`:
- Around line 60-92: The code only treats .json.br and .css.br as Brotli files;
update contentTypeFor(filePath) and headersFor(filePath) to handle any ".br"
suffix: detect if filePath.endsWith(".br"), derive the real ext by removing the
".br" suffix and lookup the MIME via contentTypes.get(path.extname(realPath))
(falling back to "application/octet-stream"), return that as the Content-Type in
contentTypeFor (and in headersFor set "Content-Encoding": "br" and "Vary":
"Accept-Encoding" whenever a ".br" file is detected), preserving existing logic
for isNextStaticAsset(filePath) and the api catalog Link header in headersFor.
- Around line 159-178: The responses currently send headers from
headersFor(finalPath) but omit Content-Length even though a prior stat() call
yields details.size; update the handler to call fs.statSync (or reuse the
existing stat result) for finalPath and add a "Content-Length" header with
details.size to the headers passed to response.writeHead (for both GET and HEAD
paths), ensuring the HEAD branch still ends after sending headers and the GET
branch streams via createReadStream(finalPath); reference the symbols finalPath,
headersFor, response.writeHead, method, and createReadStream when making the
change.

In `@src/components/home/link-creator.tsx`:
- Around line 36-79: The constants and small helpers used only in this
component—fieldHints, fieldPlaceholders, codecOptions, defaultLinkCreatorDraft,
and getBodyFieldLabel—should be moved into a shared module so they can be reused
and kept in sync; create a new module (e.g., link-creator-presets) exporting
those symbols, update src/components/home/link-creator.tsx to import fieldHints,
fieldPlaceholders, codecOptions, defaultLinkCreatorDraft, and getBodyFieldLabel
from that module, and ensure any types (ArtifactKind, LinkCreatorDraft) remain
imported or re-exported as needed so behavior of the existing functions
(getBaseUrl, numberFormatter, etc.) is unchanged.

In `@src/components/renderers/diff-renderer.tsx`:
- Around line 198-201: The stylesheet globals (diffViewStylesheetPromise and
DIFF_VIEW_STYLESHEET_ID) are not reference-counted, so
removeDiffViewStylesheet() can remove the shared <link> while another
DiffRenderer still needs it; add a module-level counter (e.g.,
diffViewInstanceCount) and update the lifecycle helpers so mounting a rich
renderer increments the count and unmounting decrements it, and change
removeDiffViewStylesheet() to only remove the DOM node and clear
diffViewStylesheetPromise when the count reaches 0; update the places that
previously called removeDiffViewStylesheet() (the effect in DiffRenderer that
tears down the fallback) to use the new increment/decrement semantics so the
stylesheet is retained while any instance remains.
- Around line 621-642: The current useEffect in diff-renderer.tsx swallows
stylesheet load errors; update the loadDiffViewStylesheet promise handling so
failures are surfaced and you don't silently mark styles as ready: call
loadDiffViewStylesheet().then(() => { if (!cancelled) setStylesReady(true)
}).catch((err) => { console.warn('Failed to load diff stylesheet', err); if
(!cancelled) { removeDiffViewStylesheet(); /* fall back to raw/unstyled diff */
setStylesReady(true); } }); keep the cancelled guard and the existing checks
around renderedDiff, and reference renderedDiff, loadDiffViewStylesheet,
removeDiffViewStylesheet, setStylesReady and diffFilesHaveRenderableFile to
locate the code.

In `@src/lib/diff/git-patch.ts`:
- Around line 48-74: parsePatchSections currently drops any leading preamble
before the first DIFF_SECTION_HEADER_RE match (e.g., mbox "From ..." headers);
update the loop in parsePatchSections so when the first match has match.index >
0 you call parsePatchSection(normalized.slice(0, match.index).trim(),
sectionIndex) (using parsePatchSection and the current sectionIndex), increment
sectionIndex, and then set previousStart = match.index; keep the existing logic
for subsequent matches and the final push so all text before the first "diff
--git" is preserved.

In `@tests/envelope.test.ts`:
- Around line 13-25: Add a regression test that exercises the new code path
rejecting envelopes with zero artifacts: create a variant of baseEnvelope where
artifacts is an empty array and call normalizeEnvelope; assert the result is a
failure (result.ok is false) and optionally assert the error or rejection reason
exists. Place this alongside the existing tests (e.g., near the "normalizes
single-artifact bundles..." test) and reference normalizeEnvelope and
baseEnvelope so the CI covers the new artifacts.length === 0 branch.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 3e70ead9-0bf9-4889-9914-f182ec60c6b6

📥 Commits

Reviewing files that changed from the base of the PR and between 4c63929 and 3b61cac.

⛔ Files ignored due to path filters (8)
  • package-lock.json is excluded by !**/package-lock.json
  • tests/e2e/visual.spec.ts-snapshots/code-light-chromium.png is excluded by !**/*.png
  • tests/e2e/visual.spec.ts-snapshots/csv-compact-light-chromium.png is excluded by !**/*.png
  • tests/e2e/visual.spec.ts-snapshots/diff-light-chromium.png is excluded by !**/*.png
  • tests/e2e/visual.spec.ts-snapshots/empty-state-light-chromium.png is excluded by !**/*.png
  • tests/e2e/visual.spec.ts-snapshots/json-light-chromium.png is excluded by !**/*.png
  • tests/e2e/visual.spec.ts-snapshots/markdown-dark-chromium.png is excluded by !**/*.png
  • tests/e2e/visual.spec.ts-snapshots/markdown-light-chromium.png is excluded by !**/*.png
📒 Files selected for processing (81)
  • README.md
  • docs/architecture.md
  • docs/dependency-notes.md
  • docs/deployment.md
  • docs/payload-format.md
  • docs/testing.md
  • eslint.config.mjs
  • next-env.d.ts
  • next.config.ts
  • package.json
  • playwright.config.ts
  • public/_headers
  • public/arx2-dictionary.json
  • public/arx2-dictionary.json.br
  • public/vendor/diff-view-pure.css
  • public/vendor/diff-view-pure.css.br
  • scripts/bench-baseline.json
  • scripts/bench-codecs.mjs
  • scripts/check-build-budgets.mjs
  • scripts/clean-build-output.mjs
  • scripts/compress-dictionary.mjs
  • scripts/ensure-next-types.mjs
  • scripts/serve-export.mjs
  • selfhosted/server.ts
  • skills/agent-render-linking/SKILL.md
  • src/app/globals.css
  • src/app/layout.tsx
  • src/app/security/page.tsx
  • src/app/url-explainer/page.tsx
  • src/components/home/link-creator.tsx
  • src/components/home/sample-link-data.ts
  • src/components/home/sample-links.tsx
  • src/components/renderers/code-renderer.tsx
  • src/components/renderers/csv-renderer.tsx
  • src/components/renderers/diff-renderer.tsx
  • src/components/renderers/json-renderer.tsx
  • src/components/renderers/markdown-renderer.tsx
  • src/components/renderers/mermaid-block.tsx
  • src/components/theme-provider.tsx
  • src/components/theme-toggle.tsx
  • src/components/theme/use-theme-controller.ts
  • src/components/viewer-shell.tsx
  • src/components/viewer/artifact-selector.tsx
  • src/components/viewer/artifact-stage.tsx
  • src/components/viewer/fragment-details-disclosure.tsx
  • src/lib/code/language.ts
  • src/lib/diff/git-patch.ts
  • src/lib/payload/arx-codec.ts
  • src/lib/payload/envelope.ts
  • src/lib/payload/fragment-arx.ts
  • src/lib/payload/fragment.ts
  • src/lib/payload/link-creator.ts
  • src/lib/payload/schema.ts
  • src/lib/payload/wire-format.ts
  • src/lib/site/base-path.ts
  • src/lib/site/canonical-base.ts
  • src/lib/utils.ts
  • tests/arx-codec.test.ts
  • tests/base-path.test.ts
  • tests/code-language.test.ts
  • tests/components/code-renderer.test.tsx
  • tests/components/csv-renderer.test.tsx
  • tests/components/diff-renderer.test.tsx
  • tests/components/json-renderer.test.tsx
  • tests/components/link-creator.test.tsx
  • tests/components/markdown-renderer.test.tsx
  • tests/components/mermaid-block.test.tsx
  • tests/components/theme-toggle.test.tsx
  • tests/components/viewer-shell-artifact-select.test.tsx
  • tests/components/viewer-shell.test.tsx
  • tests/diff-style-asset.test.ts
  • tests/e2e/viewer.spec.ts
  • tests/envelope.test.ts
  • tests/fragment.test.ts
  • tests/git-patch.test.ts
  • tests/headers.test.ts
  • tests/sample-link-data.test.ts
  • tests/selfhosted/api-catalog.test.ts
  • tests/setup.tsx
  • tests/utils.test.ts
  • tsconfig.json
💤 Files with no reviewable changes (3)
  • src/components/viewer/fragment-details-disclosure.tsx
  • tests/setup.tsx
  • src/components/theme-provider.tsx

Comment thread package.json
.diff-tailwindcss-wrapper .border-spacing-0 {
--tw-border-spacing-x: 0px;
--tw-border-spacing-y: 0px;
border-spacing: var(--tw-border-spacing-x) var(--tw-border-spacing-y);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Stylelint violations in vendored CSS should be resolved or excluded from lint scope.

This file currently contains stylelint-reported violations (declaration-empty-line-before and keyword casing at Line 200). Please either normalize the file to match the active rules or exclude public/vendor/** from stylelint targeting to keep checks stable.

Also applies to: 145-145, 149-149, 153-153, 200-200, 252-252, 656-656

🧰 Tools
🪛 Stylelint (17.11.0)

[error] 138-138: Expected empty line before declaration (declaration-empty-line-before)

(declaration-empty-line-before)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@public/vendor/diff-view-pure.css` at line 138, The vendored CSS triggers
stylelint errors (e.g., declaration-empty-line-before and keyword-case) for
lines like the rule containing "border-spacing: var(--tw-border-spacing-x)
var(--tw-border-spacing-y);"—either normalize the file to comply with our lint
rules (fix empty-line-before formatting around declarations and normalize
keyword casing to lowercase across the affected selectors/rules) or exclude the
vendored files from linting by adding "public/vendor/**" to the stylelint ignore
pattern in the Stylelint config; update only the code around the offending
declarations or the stylelint config accordingly and ensure the change
references the same symbols (the border-spacing declaration and the listed
offending rules) so CI stops failing.

Comment thread src/components/renderers/markdown-renderer.tsx
Comment thread src/components/theme/use-theme-controller.ts
Comment thread src/components/viewer-shell.tsx
Comment thread src/lib/payload/arx-codec.ts
Comment thread tests/arx-codec.test.ts
Comment thread tests/components/link-creator.test.tsx Outdated
Comment thread scripts/serve-export.mjs Fixed

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
scripts/serve-export.mjs (1)

156-162: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Avoid the unchecked stat() after existsSync().

This adds a TOCTOU window: if the file disappears or is replaced between those calls, the request handler throws instead of returning a normal 404.

Suggested fix
-  if (!existsSync(finalPath)) {
-    response.writeHead(404);
-    response.end("Not found");
-    return;
-  }
-
-  const finalDetails = await stat(finalPath);
+  let finalDetails;
+  try {
+    finalDetails = await stat(finalPath);
+  } catch {
+    response.writeHead(404);
+    response.end("Not found");
+    return;
+  }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/serve-export.mjs` around lines 156 - 162, Replace the unsafe
existsSync() + stat() pattern with a single awaited stat() inside a try/catch:
call stat(finalPath) in a try block, on success verify finalDetails.isFile() (or
other required checks) and proceed, and on any error (ENOENT or other) catch it
and respond with response.writeHead(404); response.end("Not found"); return;
this eliminates the TOCTOU window and ensures missing or inaccessible files are
handled safely.
🧹 Nitpick comments (3)
tests/components/viewer-shell-artifact-select.test.tsx (1)

106-129: ⚡ Quick win

Consider more robust hash parsing in the mock.

The activeArtifactId derivation at line 112 uses simple string includes (hash.includes("three")), which is fragile. If hash formats evolve or if these substrings appear elsewhere, the mock could return unexpected artifacts. Consider using a more explicit pattern like splitting on delimiters or regex matching.

♻️ Example: More robust parsing
-    const activeArtifactId = hash.includes("three") ? "three" : hash.includes("two") ? "two" : "one";
+    // Extract artifact ID from hash pattern: agent-render=v1.plain.<artifactId>
+    const match = hash.match(/agent-render=v1\.plain\.(\w+)/);
+    const activeArtifactId = match?.[1] ?? "one";
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/components/viewer-shell-artifact-select.test.tsx` around lines 106 -
129, The mock's activeArtifactId derivation in decodeFragmentAsync is fragile
because it uses hash.includes(...); change it to explicitly parse the artifact
id (e.g., use a regex like /(?:^|[:\-])(?:one|two|three)(?:$|[:\-])/, or split
the hash on a known delimiter and pick the token) so decodeFragmentAsync
deterministically maps hashes to "one", "two", or "three" and update any tests
expecting createEnvelope(activeArtifactId) accordingly; reference the
decodeFragmentAsync mock, fragmentMock.decodes, and createEnvelope to locate
where to apply this parsing fix.
tests/components/link-creator.test.tsx (2)

17-23: ⚡ Quick win

Consider adding error handling tests.

The mock includes a reject callback but no tests verify error handling behavior. Consider adding a test that rejects a generation request to ensure the component handles errors gracefully.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/components/link-creator.test.tsx` around lines 17 - 23, The mock for
createGeneratedArtifactLinkAsync exposes a reject callback via
generationMock.pending but no test exercises it; add a test that triggers
rejection (call the pending item's reject with an Error or rejection value)
after initiating generation and then assert the LinkCreator component's error
handling — e.g., that an error message is rendered or an error state callback is
invoked. Target the mocked function createGeneratedArtifactLinkAsync and the
generationMock.pending queue to perform the rejection, await any async UI
updates (e.g., using findByText or waitFor) and verify the expected error
UI/behavior, then clean up/reset mocks.

1-119: ⚡ Quick win

Consider error handling and visual state verification.

While E2E Playwright tests cover LinkCreator workflows (tests/e2e/viewer.spec.ts), the unit tests verify visual assertions (textarea content, warning message visibility) without corresponding Playwright coverage for those specific state changes. Additionally, error handling is not tested—the mock's reject function is never invoked, leaving error scenarios uncovered.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/components/link-creator.test.tsx` around lines 1 - 119, Tests for
LinkCreator lack coverage for rejection/error flows and miss asserting visual
states after errors; update tests in link-creator.test.tsx to simulate and
assert those cases by using generationMock.pending[].reject(...) to trigger
createGeneratedArtifactLinkAsync failure and then verify UI reacts (e.g., an
error message, disabled/cleared Generated agent-render link textarea, and any
visual error state) and also add assertions after UI interactions (like clicking
format/quality buttons) that visual state remains consistent; reference
generationMock.pending, createGeneratedLink, and the LinkCreator component to
locate where to add a new it() block(s) that rejects pending promises and
asserts the expected DOM changes.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@scripts/serve-export.mjs`:
- Around line 97-103: The code slices the percent-encoded request URL (urlPath)
into cleanPath and assigns it to relativePath, so filesystem lookups will see
raw percent sequences; fix by decoding the path before resolving on disk:
compute cleanPath = urlPath.slice(0, pathEnd) and then set relativePath =
decodeURIComponent(cleanPath) (wrap decodeURIComponent in try/catch if you want
to handle malformed encodings). Apply the same decoding change to the other
occurrence referenced (lines around 121-122) so all file-resolution uses of
relativePath operate on a decoded path.
- Around line 91-94: The current isInsideOutputDirectory(filePath) check can be
bypassed via symlinks; update the check to resolve and compare real filesystem
targets instead of raw path strings (or alternatively reject any symlink segment
while walking the path). Specifically, in isInsideOutputDirectory and the
similar logic around the other export checks (the block at lines 122-128), call
fs.promises.realpath() (or synchronous realpath if appropriate) on both
outputDirectory and the resolved filePath and then ensure the file's real path
startsWith the outputDirectory real path; or walk the filePath with fs.lstat()
and fail if any path segment is a symbolic link before performing
stat/createReadStream. Ensure you reference and update isInsideOutputDirectory
and the corresponding export validation block to prevent symlink escape.

In `@src/components/renderers/diff-renderer.tsx`:
- Around line 20-23: Remove the Brotli sidecar entry from the stylesheet list:
update the diffViewStylesheetHrefs array (used where withBasePath is called) to
omit "/vendor/diff-view-pure.css.br" and only include the actual stylesheet path
("/vendor/diff-view-pure.css") so the renderer uses standard content-negotiated
CSS rather than attempting to load raw .br bytes as a stylesheet.

In `@src/components/renderers/markdown-renderer.tsx`:
- Around line 190-208: The markBlockReady callback currently reassigns
readyKeyRef.current and clears readyBlockIdsRef when it detects a mismatch,
which adopts a stale readyKey; instead, detect when readyKeyRef.current !==
readyKey and simply ignore/drop the callback by returning early. Update
markBlockReady (the function using readyKeyRef, readyKey, readyBlockIdsRef,
reportedReadyKeyRef, setReadyBlockState, embeddedBlockCount, and reportReady) so
it does not mutate refs/state on mismatch — just return — and keep the rest of
the logic (adding blockId, updating setReadyBlockState, and calling reportReady)
unchanged.
- Around line 35-50: The readiness counter undercounts fenced code blocks
because countEmbeddedCodeBlocks() uses EMBEDDED_CODE_BLOCK_PATTERN that
disallows up-to-3-space indented fences; replace the raw-regex counting approach
with deriving the embeddedBlockCount from actual rendered nodes instead: remove
or stop using EMBEDDED_CODE_BLOCK_PATTERN and instead increment the ready target
when ReactMarkdown/remarkGfm produces a code/mermaid/embedded block (e.g.,
inside the render path where EmbeddedCodeRenderer and MermaidBlock are
instantiated), then keep markBlockReady() logic unchanged so nextCount compares
against the real rendered block count rather than a fragile regex result.

In `@src/lib/diff/git-patch.ts`:
- Around line 64-69: The code currently converts leading preamble text into a
ParsedPatchFile by calling parsePatchSection(preamble, sectionIndex) and pushing
it into files; instead, do not promote preamble into the files array. Change the
branch that handles a non-empty preamble (the block using match.index,
normalized, preamble, parsePatchSection, and sectionIndex) to either discard the
preamble or collect it in a separate variable/array (e.g., preambleText or
preambleSections) for non-file metadata, and remove the files.push(...) call and
the sectionIndex increment so files remains strictly file-only.

In `@tests/components/markdown-renderer.test.tsx`:
- Around line 62-125: Add a real-browser Playwright test that exercises the
fenced-code presentation path instead of relying solely on mocks: create a new
Playwright test that mounts the MarkdownRenderer (or navigates to a page that
renders it) with a fenced code sample (e.g., "```ts\nconst a = 1;\n```") and
assert the visible language chip text ("ts") and the rendered code content
("const a = 1") in the browser DOM; reference the component name
MarkdownRenderer and the existing unit test cases in
tests/components/markdown-renderer.test.tsx as the examples to mirror, and place
the new test alongside other Playwright/e2e tests so CI will run it.

---

Outside diff comments:
In `@scripts/serve-export.mjs`:
- Around line 156-162: Replace the unsafe existsSync() + stat() pattern with a
single awaited stat() inside a try/catch: call stat(finalPath) in a try block,
on success verify finalDetails.isFile() (or other required checks) and proceed,
and on any error (ENOENT or other) catch it and respond with
response.writeHead(404); response.end("Not found"); return; this eliminates the
TOCTOU window and ensures missing or inaccessible files are handled safely.

---

Nitpick comments:
In `@tests/components/link-creator.test.tsx`:
- Around line 17-23: The mock for createGeneratedArtifactLinkAsync exposes a
reject callback via generationMock.pending but no test exercises it; add a test
that triggers rejection (call the pending item's reject with an Error or
rejection value) after initiating generation and then assert the LinkCreator
component's error handling — e.g., that an error message is rendered or an error
state callback is invoked. Target the mocked function
createGeneratedArtifactLinkAsync and the generationMock.pending queue to perform
the rejection, await any async UI updates (e.g., using findByText or waitFor)
and verify the expected error UI/behavior, then clean up/reset mocks.
- Around line 1-119: Tests for LinkCreator lack coverage for rejection/error
flows and miss asserting visual states after errors; update tests in
link-creator.test.tsx to simulate and assert those cases by using
generationMock.pending[].reject(...) to trigger createGeneratedArtifactLinkAsync
failure and then verify UI reacts (e.g., an error message, disabled/cleared
Generated agent-render link textarea, and any visual error state) and also add
assertions after UI interactions (like clicking format/quality buttons) that
visual state remains consistent; reference generationMock.pending,
createGeneratedLink, and the LinkCreator component to locate where to add a new
it() block(s) that rejects pending promises and asserts the expected DOM
changes.

In `@tests/components/viewer-shell-artifact-select.test.tsx`:
- Around line 106-129: The mock's activeArtifactId derivation in
decodeFragmentAsync is fragile because it uses hash.includes(...); change it to
explicitly parse the artifact id (e.g., use a regex like
/(?:^|[:\-])(?:one|two|three)(?:$|[:\-])/, or split the hash on a known
delimiter and pick the token) so decodeFragmentAsync deterministically maps
hashes to "one", "two", or "three" and update any tests expecting
createEnvelope(activeArtifactId) accordingly; reference the decodeFragmentAsync
mock, fragmentMock.decodes, and createEnvelope to locate where to apply this
parsing fix.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 4722b931-85f1-43b5-a188-c9b3689c7907

📥 Commits

Reviewing files that changed from the base of the PR and between 3b61cac and 671cc85.

📒 Files selected for processing (14)
  • docs/dependency-notes.md
  • scripts/bench-codecs.mjs
  • scripts/serve-export.mjs
  • src/components/renderers/diff-renderer.tsx
  • src/components/renderers/markdown-renderer.tsx
  • src/components/theme/use-theme-controller.ts
  • src/components/viewer-shell.tsx
  • src/lib/diff/git-patch.ts
  • tests/arx-codec.test.ts
  • tests/components/link-creator.test.tsx
  • tests/components/markdown-renderer.test.tsx
  • tests/components/viewer-shell-artifact-select.test.tsx
  • tests/envelope.test.ts
  • tests/git-patch.test.ts
🚧 Files skipped from review as they are similar to previous changes (5)
  • tests/envelope.test.ts
  • src/components/theme/use-theme-controller.ts
  • tests/arx-codec.test.ts
  • src/components/viewer-shell.tsx
  • scripts/bench-codecs.mjs

Comment thread scripts/serve-export.mjs
Comment thread scripts/serve-export.mjs Outdated
Comment on lines +20 to +23
const diffViewStylesheetHrefs = [
withBasePath("/vendor/diff-view-pure.css.br"),
withBasePath("/vendor/diff-view-pure.css"),
];

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

Do browsers automatically decompress a .css.brfile when it is linked via, or is Brotli only applied when the response is served with Content-Encoding: brfor the.css URL?

💡 Result:

Browsers do not decide “this is Brotli because the URL ends with.br (e.g. /styles.css.br)”. Brotli is only applied when the HTTP response indicates that the body is Brotli-compressed via the response header Content-Encoding: br (selected via Accept-Encoding negotiation). This is independent of how you name the file; the extension by itself doesn’t trigger decompression. When you link the browser will fetch that URL as a stylesheet. If the server returns the resource with Content-Encoding: br, the browser will decompress it to obtain the underlying CSS. If Content-Encoding: br is missing (or wrong), the browser will treat the bytes as-is and CSS parsing will fail (or you’ll get an invalid resource / decoding error). Content-Encoding is explicitly defined as telling the recipient how to decode the response body, and the client negotiates supported encodings with Accept-Encoding; the server then signals the chosen encoding with Content-Encoding.[1][2] Practical implication for precompressed static assets: servers commonly serve the precompressed.br variant only to clients that accept br, and they do so by rewriting to the.br file and setting Content-Encoding: br on the response.[3] So, to answer your question directly: - Yes: a browser can decompress a linked /styles.css.br file, but only because the response has Content-Encoding: br. - No: Brotli is not automatically inferred solely from the filename (.css.br) or the tag.

Citations:


🏁 Script executed:

rg -n "diffViewStylesheetHrefs" src/components/renderers/diff-renderer.tsx -A 5 -B 2

Repository: baanish/agent-render

Length of output: 655


🏁 Script executed:

rg -n "loadStylesheetHref" src/components/renderers/diff-renderer.tsx -A 10 -B 2

Repository: baanish/agent-render

Length of output: 910


🏁 Script executed:

sed -n '126,150p' src/components/renderers/diff-renderer.tsx

Repository: baanish/agent-render

Length of output: 831


Remove the Brotli sidecar from the stylesheet array.

Linking /vendor/diff-view-pure.css.br via <link rel="stylesheet"> requests the file directly without Content-Encoding: br headers. The server returns the file with HTTP 200, the browser's load event fires, and this prevents fallback to the actual stylesheet—leaving the rich diff unstyled while attempting to parse raw Brotli bytes as CSS.

Brotli decompression only occurs when the server explicitly sends Content-Encoding: br for the .css file; the .br filename extension alone has no effect. Rely on server-side content negotiation instead.

Suggested change
 const diffViewStylesheetHrefs = [
-  withBasePath("/vendor/diff-view-pure.css.br"),
   withBasePath("/vendor/diff-view-pure.css"),
 ];
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const diffViewStylesheetHrefs = [
withBasePath("/vendor/diff-view-pure.css.br"),
withBasePath("/vendor/diff-view-pure.css"),
];
const diffViewStylesheetHrefs = [
withBasePath("/vendor/diff-view-pure.css"),
];
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/renderers/diff-renderer.tsx` around lines 20 - 23, Remove the
Brotli sidecar entry from the stylesheet list: update the
diffViewStylesheetHrefs array (used where withBasePath is called) to omit
"/vendor/diff-view-pure.css.br" and only include the actual stylesheet path
("/vendor/diff-view-pure.css") so the renderer uses standard content-negotiated
CSS rather than attempting to load raw .br bytes as a stylesheet.

Comment on lines +35 to +50
const EMBEDDED_CODE_BLOCK_PATTERN = /(?:^|\n)```[^\n]*\n[\s\S]*?\n```(?=\n|$)/g;
const LANGUAGE_CLASS_PREFIX = "language-";

type ReadyBlockState = {
key: string;
count: number;
};

function countEmbeddedCodeBlocks(content: string): number {
EMBEDDED_CODE_BLOCK_PATTERN.lastIndex = 0;
let count = 0;
while (EMBEDDED_CODE_BLOCK_PATTERN.exec(content)) {
count += 1;
}
return count;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

# First, locate the file and check its structure
find . -type f -name "markdown-renderer.tsx" 2>/dev/null

Repository: baanish/agent-render

Length of output: 113


🏁 Script executed:

# Read the markdown-renderer.tsx file
cat -n ./src/components/renderers/markdown-renderer.tsx

Repository: baanish/agent-render

Length of output: 10893


The readiness counter undercounts fenced blocks when the regex pattern is stricter than the parsed markdown format.

countEmbeddedCodeBlocks() uses a regex that requires zero indentation before the opening fence ((?:^|\n)```), but the GFM spec allows up to 3 spaces of indentation. ReactMarkdown with remarkGfm parses these indented fences, so blocks that match the spec but not the regex will still render and call markBlockReady()afterdata-renderer-readyhas already flipped to "true" (once the undercountednextCount >= embeddedBlockCount at line 205). This causes the ready state to signal completion while async renderers (EmbeddedCodeRenderer, MermaidBlock`) are still loading. Derive the ready count from the actual rendered blocks instead of re-parsing raw markdown with a narrower regex.

🧰 Tools
🪛 OpenGrep (1.20.0)

[ERROR] 46-46: Dynamic command passed to child_process.exec/execSync. Use child_process.execFile or spawn with an argument array instead.

(coderabbit.command-injection.exec-js)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/renderers/markdown-renderer.tsx` around lines 35 - 50, The
readiness counter undercounts fenced code blocks because
countEmbeddedCodeBlocks() uses EMBEDDED_CODE_BLOCK_PATTERN that disallows
up-to-3-space indented fences; replace the raw-regex counting approach with
deriving the embeddedBlockCount from actual rendered nodes instead: remove or
stop using EMBEDDED_CODE_BLOCK_PATTERN and instead increment the ready target
when ReactMarkdown/remarkGfm produces a code/mermaid/embedded block (e.g.,
inside the render path where EmbeddedCodeRenderer and MermaidBlock are
instantiated), then keep markBlockReady() logic unchanged so nextCount compares
against the real rendered block count rather than a fragile regex result.

Comment thread src/components/renderers/markdown-renderer.tsx
Comment thread src/lib/diff/git-patch.ts
Comment on lines +62 to +125
describe("MarkdownRenderer", () => {
it("renders language-less fenced code blocks with the text fallback language", async () => {
const { container } = render(<MarkdownRenderer artifact={createArtifact({ content: "```\nplain text\n```" })} />);

await waitFor(() => {
expect(
Array.from(container.querySelectorAll(".markdown-code-chip")).map((node) => node.textContent),
).toEqual(["text"]);
expect(container).toHaveTextContent("plain text");
});
});

it("renders every fenced code block with its language chip", async () => {
const { container } = render(
<MarkdownRenderer
artifact={createArtifact({
content: "```ts\nconst a = 1;\n```\n\n```json\n{\"ok\":true}\n```",
})}
/>,
);

await waitFor(() => {
expect(
Array.from(container.querySelectorAll(".markdown-code-chip")).map((node) => node.textContent),
).toEqual(["ts", "json"]);
expect(container).toHaveTextContent("const a = 1");
expect(container).toHaveTextContent('{"ok":true}');
});
});

it("preserves non-alphanumeric fenced code language ids", async () => {
const { container } = render(
<MarkdownRenderer
artifact={createArtifact({
content: "```c++\nint main() {}\n```\n\n```c#\nConsole.WriteLine();\n```",
})}
/>,
);

await waitFor(() => {
expect(
Array.from(container.querySelectorAll(".markdown-code-chip")).map((node) => node.textContent),
).toEqual(["c++", "c#"]);
});
});

it("does not wait forever on tilde blocks that are not mounted as embedded renderers", async () => {
const onReady = vi.fn();

render(
<MarkdownRenderer
artifact={createArtifact({
content: "~~~json\n{\"ok\":true}\n~~~",
})}
onReady={onReady}
/>,
);

await waitFor(() => {
expect(screen.getByTestId("renderer-markdown")).toHaveAttribute("data-renderer-ready", "true");
expect(onReady).toHaveBeenCalled();
});
});
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Add one real-browser Playwright case for the fenced-code presentation path.

These assertions run entirely through mocks, so they won't catch regressions in the actual dynamic renderer/chip rendering path. Please mirror at least one fenced-code example in Playwright as well.

As per coding guidelines, "Verify visual changes with Playwright when they affect layout or renderer presentation in test files".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/components/markdown-renderer.test.tsx` around lines 62 - 125, Add a
real-browser Playwright test that exercises the fenced-code presentation path
instead of relying solely on mocks: create a new Playwright test that mounts the
MarkdownRenderer (or navigates to a page that renders it) with a fenced code
sample (e.g., "```ts\nconst a = 1;\n```") and assert the visible language chip
text ("ts") and the rendered code content ("const a = 1") in the browser DOM;
reference the component name MarkdownRenderer and the existing unit test cases
in tests/components/markdown-renderer.test.tsx as the examples to mirror, and
place the new test alongside other Playwright/e2e tests so CI will run it.

@socket-security

socket-security Bot commented May 12, 2026

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Updatednext@​15.1.11 ⏵ 15.5.1852 -13100 +7591 -19970
Updatedeslint-config-next@​15.1.11 ⏵ 15.5.1899 +110065 +198100
Updatedmermaid@​11.14.0 ⏵ 11.15.075 -23100 +6100 +193100

View full report

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
scripts/serve-export.mjs (1)

146-153: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Decode the request path before route lookup.

Line 152 still keeps %xx escapes intact, so requests like /images/My%20File.png or non-ASCII filenames won't match the precomputed route map and will 404.

Suggested fix
-  const cleanPath = urlPath.slice(0, pathEnd);
+  let cleanPath;
+  try {
+    cleanPath = decodeURIComponent(urlPath.slice(0, pathEnd));
+  } catch {
+    return null;
+  }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/serve-export.mjs` around lines 146 - 153, The toRoutePath function
currently slices the raw URL and leaves percent-escapes intact, causing
mismatches for encoded filenames; decode the cleaned path (cleanPath) using
decodeURIComponent before building routePath so `/images/My%20File.png` matches
the precomputed routes, and wrap the decode in a try/catch to fall back to the
original cleanPath if decodeURIComponent throws (handle malformed
percent-encodings), then ensure the decoded value still gets normalized to start
with a leading slash when assigning routePath.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@scripts/serve-export.mjs`:
- Around line 193-199: The code sends a 200 response before ensuring the file
stream is readable; change the flow so you do not call response.writeHead(200,
...) before the ReadStream opens and add an error handler on the stream: for
method === "HEAD" you can still call response.writeHead(200, headersFor(...))
and response.end() immediately, but for GET create the
ReadStream(staticFile.filePath), attach stream.once('open', () => {
response.writeHead(200, headersFor(staticFile.filePath, staticFile.size));
stream.pipe(response); }), and attach stream.on('error', err => {
response.destroy(); response.writeHead(err.code === 'ENOENT' ? 404 : 500);
response.end(); }) so missing/unreadable files don’t cause a broken 200 response
or crash.

---

Duplicate comments:
In `@scripts/serve-export.mjs`:
- Around line 146-153: The toRoutePath function currently slices the raw URL and
leaves percent-escapes intact, causing mismatches for encoded filenames; decode
the cleaned path (cleanPath) using decodeURIComponent before building routePath
so `/images/My%20File.png` matches the precomputed routes, and wrap the decode
in a try/catch to fall back to the original cleanPath if decodeURIComponent
throws (handle malformed percent-encodings), then ensure the decoded value still
gets normalized to start with a leading slash when assigning routePath.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d980e80b-513f-4f26-8432-77e1152f48df

📥 Commits

Reviewing files that changed from the base of the PR and between 671cc85 and e638be4.

📒 Files selected for processing (1)
  • scripts/serve-export.mjs

Comment thread scripts/serve-export.mjs Outdated
Comment on lines +193 to +199
response.writeHead(200, headersFor(staticFile.filePath, staticFile.size));
if (method === "HEAD") {
response.end();
return;
}

createReadStream(finalPath).pipe(response);
createReadStream(staticFile.filePath).pipe(response);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Don't send 200 before the file stream opens.

Line 199 pipes a ReadStream without an error handler after the 200 headers are already sent. If the file disappears or becomes unreadable after startup, the preview server can crash or return a broken 200 response.

Suggested fix
-  response.writeHead(200, headersFor(staticFile.filePath, staticFile.size));
-  if (method === "HEAD") {
-    response.end();
-    return;
-  }
-
-  createReadStream(staticFile.filePath).pipe(response);
+  if (method === "HEAD") {
+    response.writeHead(200, headersFor(staticFile.filePath, staticFile.size));
+    response.end();
+    return;
+  }
+
+  const stream = createReadStream(staticFile.filePath);
+  stream.once("error", () => {
+    if (response.headersSent) {
+      response.destroy();
+      return;
+    }
+    response.writeHead(500);
+    response.end("Internal server error");
+  });
+  stream.once("open", () => {
+    response.writeHead(200, headersFor(staticFile.filePath, staticFile.size));
+    stream.pipe(response);
+  });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
response.writeHead(200, headersFor(staticFile.filePath, staticFile.size));
if (method === "HEAD") {
response.end();
return;
}
createReadStream(finalPath).pipe(response);
createReadStream(staticFile.filePath).pipe(response);
if (method === "HEAD") {
response.writeHead(200, headersFor(staticFile.filePath, staticFile.size));
response.end();
return;
}
const stream = createReadStream(staticFile.filePath);
stream.once("error", () => {
if (response.headersSent) {
response.destroy();
return;
}
response.writeHead(500);
response.end("Internal server error");
});
stream.once("open", () => {
response.writeHead(200, headersFor(staticFile.filePath, staticFile.size));
stream.pipe(response);
});
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/serve-export.mjs` around lines 193 - 199, The code sends a 200
response before ensuring the file stream is readable; change the flow so you do
not call response.writeHead(200, ...) before the ReadStream opens and add an
error handler on the stream: for method === "HEAD" you can still call
response.writeHead(200, headersFor(...)) and response.end() immediately, but for
GET create the ReadStream(staticFile.filePath), attach stream.once('open', () =>
{ response.writeHead(200, headersFor(staticFile.filePath, staticFile.size));
stream.pipe(response); }), and attach stream.on('error', err => {
response.destroy(); response.writeHead(err.code === 'ENOENT' ? 404 : 500);
response.end(); }) so missing/unreadable files don’t cause a broken 200 response
or crash.

@greptile-apps

greptile-apps Bot commented May 14, 2026

Copy link
Copy Markdown

Greptile Summary

This PR refactors the fragment encoding/decoding pipeline, viewer UI, and build tooling around a new arx3 codec variant and cleaner code-splitting boundaries. The ARX dictionary loading path, base-path URL helpers, static-export server, and viewer shell are all touched.

  • New fragment-arx.ts module: ARX/ARX2/ARX3 candidate building and decoding are split into a dynamically-imported chunk so the main fragment module stays light; arx-codec.ts gains arxCompressPayloads to compress once and return all four wire encodings instead of running Brotli four times.
  • New artifact-stage.tsx: Renderer loading and toolbar logic extracted from the monolithic viewer-shell.tsx, with fragment module import cached via a module-level promise.
  • serve-export.mjs overhaul: Static files are pre-indexed at startup (collectStaticFiles) with route aliases pre-computed, replacing per-request stat calls; method guard now runs before any I/O.
  • check-build-budgets.mjs: New CI gate measures gzip size of Next.js chunk groups against fixed budgets and exits non-zero on failure.

Confidence Score: 5/5

Safe to merge; no correctness bugs found in the codec, fragment, or server paths.

The ARX3 codec path, reversed-trie optimisation, brotli dictionary fallback, and artifact-stage extraction all look correct. The serve-export.mjs static-file pre-indexing is sound, and the selfhosted path-traversal guard is tightened correctly. The only open questions are minor: a startup UX gap in the preview server and the manifest key used by the budget checker, both of which would surface immediately in local testing rather than causing silent data loss.

scripts/check-build-budgets.mjs — confirm the /page manifest key against a real build output; scripts/serve-export.mjs — the top-level collectStaticFiles call produces a raw crash if out/ is missing.

Important Files Changed

Filename Overview
src/lib/payload/fragment-arx.ts New module splitting ARX candidate-building and decoding out of fragment.ts; introduces dictionary load caching with module-level promises and the intentional double-try decode pattern for backward compatibility.
src/lib/payload/arx-codec.ts Adds arx3 compress/decompress, arxCompressPayloads (compress-once, encode-four-ways), brotli-first dictionary fallback fetch, and trie-reversed parameter replacing the extra map allocation.
src/lib/payload/fragment.ts ARX imports removed; build-candidate functions now dynamic-import fragment-arx.ts; arx3 plumbed through codec selection; minor hot-path optimisations (loop-based base64, codepoint integer checks).
scripts/serve-export.mjs Static file map pre-built at startup via collectStaticFiles; method guard moved before I/O; Content-Length and brotli headers added; basePath normalization refactored. Startup will hard-crash if out/ is absent rather than returning 404.
scripts/check-build-budgets.mjs New CI budget gate measuring gzip size of route and react-loadable chunks; self-validates by throwing on unknown manifest keys.
src/components/viewer/artifact-stage.tsx New component extracting renderer loading, toolbar, copy/download, and raw-source view from viewer-shell.tsx for improved code-splitting.
src/lib/site/base-path.ts New utility normalising NEXT_PUBLIC_BASE_PATH and providing withBasePath; replaces duplicated inline env-var reads across arx-codec and viewer-shell.
selfhosted/server.ts Adds .json.br and .css.br special-case Content-Encoding/Content-Type, Cache-Control for _next/static assets, and isNextStaticAsset helper; path traversal check improved with pre-computed outputDirectoryWithSeparator.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    VS[viewer-shell.tsx] -->|dynamic import| AS[artifact-stage.tsx]
    VS -->|cached promise| FM[fragment.ts]
    FM -->|sync codecs| SC[plain / deflate / lz]
    FM -->|dynamic import| FAX[fragment-arx.ts]
    FAX -->|ensureArxLoaded| AC[arx-codec.ts]
    FAX -->|ensureArx2Loaded| AC
    AC -->|fetch with .br fallback| DICT[arx-dictionary.json]
    AC -->|fetch with .br fallback| DICT2[arx2-dictionary.json]
    AC -->|arxCompressPayloads| WP[ArxWirePayloads: base76 / base1k / baseBMP / base64url]
    FAX -->|buildArxCandidates| WP
    FAX -->|buildArx2Candidates| WP
    FAX -->|buildArx3Candidates
prefers baseBMP visible len| WP
    BP[base-path.ts
normalizeBasePath / withBasePath] -->|resolves URL| DICT
    BP -->|resolves URL| DICT2
    SX[serve-export.mjs] -->|collectStaticFiles at startup| SF[staticFiles Map]
    SF -->|pre-computed aliases| ROUTES[/ /index.html /page /page/]
Loading

Fix All in Codex

Reviews (2): Last reviewed commit: "Add ARX3 homepage sample and auto-encode..." | Re-trigger Greptile

Comment thread selfhosted/server.ts Outdated
Comment on lines 49 to 81
if (filePath.endsWith(".json.br")) {
return "application/json; charset=utf-8";
}

if (filePath.endsWith(".css.br")) {
return "text/css; charset=utf-8";
}

return contentTypes.get(path.extname(filePath)) || "application/octet-stream";
}

function isNextStaticAsset(filePath: string): boolean {
return filePath.includes(`${path.sep}_next${path.sep}static${path.sep}`);
}

function headersFor(filePath: string): Record<string, string> {
const headers: Record<string, string> = { "Content-Type": contentTypeFor(filePath) };

if (filePath.endsWith(`${path.sep}.well-known${path.sep}api-catalog`)) {
headers.Link = API_CATALOG_LINK_HEADER;
}

if (filePath.endsWith(".json.br") || filePath.endsWith(".css.br")) {
headers["Content-Encoding"] = "br";
headers.Vary = "Accept-Encoding";
}

if (isNextStaticAsset(filePath)) {
headers["Cache-Control"] = "public, max-age=31536000, immutable";
}

return headers;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Brotli handling limited to two known types while serve-export.mjs is generic

serve-export.mjs handles every .br file by stripping the extension to determine content type and unconditionally sets Content-Encoding: br. The selfhosted server only applies the correct content type and Content-Encoding: br for .json.br and .css.br. Any future pre-compressed file (e.g., a .wasm.br or .js.br) served through the selfhosted path would be returned as application/octet-stream without Content-Encoding: br, causing the browser to receive raw brotli bytes. Aligning the two servers to use the same generic extension-stripping approach would avoid this drift.

Fix in Codex

Comment on lines +172 to +188

if (parsedDictVersion !== null) {
try {
return await decodeArxAttempt(codec, decodeArxEncodedPayload(remainder));
} catch (error) {
lastError = normalizeArxDecodeError(error);
}
}

try {
return await decodeArxAttempt(codec, decodeArxEncodedPayload(versionedPayload));
} catch (error) {
lastError = normalizeArxDecodeError(error);
}

throw lastError ?? new Error("Failed to decode arx fragment");
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 First decode attempt always fails for correctly versioned ARX fragments

When parsedDictVersion !== null the function first attempts to decompress remainder (the full "<version>.<payload>" string). For any correctly-formed, version-prefixed ARX fragment this attempt will always fail at the decompressor level, the error is silently swallowed, and the second attempt with just versionedPayload succeeds. This double-try is preserved from the old fragment.ts code for backward compatibility with pre-versioning links where the payload might start with digits and a dot. The behavior is intentional, but the first failed decompress is wasted CPU on every modern fragment decode — a comment explaining why the full remainder is tried first would help readers not "fix" this.

Fix in Codex

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/lib/payload/schema.ts (1)

146-157: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Validate diff.view at runtime in isPayloadEnvelope.

isPayloadEnvelope currently allows any view value on diff artifacts. Invalid values should fail schema validation before mount.

Proposed fix
     if (artifact.kind === "diff") {
       const diffArtifact = artifact as {
         patch?: unknown;
         oldContent?: unknown;
         newContent?: unknown;
+        view?: unknown;
       };

       if (!hasString(diffArtifact.patch) && (!hasString(diffArtifact.oldContent) || !hasString(diffArtifact.newContent))) {
         return false;
       }
+      if (
+        diffArtifact.view !== undefined &&
+        diffArtifact.view !== "unified" &&
+        diffArtifact.view !== "split"
+      ) {
+        return false;
+      }
       continue;
     }

As per coding guidelines, src/lib/payload/{schema,envelope}.ts: "Fail clearly on malformed or oversized payloads before renderer mount when possible in envelope.ts and schema.ts" and src/lib/payload/schema.ts: "Support view property with values unified or split for diff artifacts in schema.ts and diff-renderer.tsx".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/payload/schema.ts` around lines 146 - 157, The diff-artifact branch
in isPayloadEnvelope currently doesn't validate the diff.view value; update the
check in the artifact.kind === "diff" block (where diffArtifact is defined and
hasString is used) to also validate diffArtifact.view and return false for any
value other than "unified" or "split" (i.e., if view is present and not one of
those two strings, return false). Ensure the combined condition still enforces
the existing patch/oldContent+newContent requirement and uses the same hasString
helper for view validation.
🧹 Nitpick comments (1)
scripts/bench-codecs.mjs (1)

514-533: ⚡ Quick win

Add decode round-trip assertions in the benchmark loop.

The loop measures decode time but never verifies decoded content matches the expected payload shape for each codec. A parseable-but-wrong decode can pass the benchmark gate unnoticed.

Proposed patch
 for (const entry of corpus) {
   for (const codec of ["arx", "arx2", "arx3"]) {
     const [encoder, decoder] = codecImplementations[codec];
     const rawJson = JSON.stringify({ ...entry.envelope, codec });
+    const expectedDecoded =
+      codec === "arx"
+        ? { ...entry.envelope, codec }
+        : tupleEnvelope({ ...entry.envelope, codec });
     const encodedInput = encoder(entry.envelope);
     const encode = measure(() => brotli(encodedInput));
     const decode = measure(() => decoder(encode.result));
+    if (JSON.stringify(decode.result) !== JSON.stringify(expectedDecoded)) {
+      failures.push(`${entry.name}:${codec} failed decode round-trip validation.`);
+    }

     rows.push({
       id: `${entry.name}:${codec}`,
       codec,
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/bench-codecs.mjs` around lines 514 - 533, Add a decode round-trip
assertion after measuring decode in the corpus loop: after const decode =
measure(() => decoder(encode.result)); verify the decoded value matches the
original envelope (entry.envelope) using a deep-equality check (e.g.
assert.deepStrictEqual or a JSON.stringify comparison) so any
parseable-but-wrong output fails the benchmark; do the check outside the timed
measure and before pushing the row, referencing encoder/decoder, measure,
encode/decode results, and rows.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/lib/payload/fragment-arx.ts`:
- Around line 39-40: The cached promise arxDictionaryLoadPromise can remain
rejected and block future loads; modify the usage where you set
arxDictionaryLoadPromise = loadArxDictionary()... so that you attach a .catch
handler which clears arxDictionaryLoadPromise (set it back to undefined/null)
before rethrowing the error, then await the promise; do the same fix for the
other occurrence around lines 50-53 so any transient failure resets the cache
and allows retries (refer to arxDictionaryLoadPromise and loadArxDictionary to
locate both spots).

---

Outside diff comments:
In `@src/lib/payload/schema.ts`:
- Around line 146-157: The diff-artifact branch in isPayloadEnvelope currently
doesn't validate the diff.view value; update the check in the artifact.kind ===
"diff" block (where diffArtifact is defined and hasString is used) to also
validate diffArtifact.view and return false for any value other than "unified"
or "split" (i.e., if view is present and not one of those two strings, return
false). Ensure the combined condition still enforces the existing
patch/oldContent+newContent requirement and uses the same hasString helper for
view validation.

---

Nitpick comments:
In `@scripts/bench-codecs.mjs`:
- Around line 514-533: Add a decode round-trip assertion after measuring decode
in the corpus loop: after const decode = measure(() => decoder(encode.result));
verify the decoded value matches the original envelope (entry.envelope) using a
deep-equality check (e.g. assert.deepStrictEqual or a JSON.stringify comparison)
so any parseable-but-wrong output fails the benchmark; do the check outside the
timed measure and before pushing the row, referencing encoder/decoder, measure,
encode/decode results, and rows.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 55f640dc-aa1c-4cf4-b628-a0f87ef607c0

📥 Commits

Reviewing files that changed from the base of the PR and between e638be4 and 2a2cd0f.

⛔ Files ignored due to path filters (2)
  • tests/e2e/visual.spec.ts-snapshots/bundle-switcher-light-chromium.png is excluded by !**/*.png
  • tests/e2e/visual.spec.ts-snapshots/json-light-chromium.png is excluded by !**/*.png
📒 Files selected for processing (24)
  • README.md
  • docs/architecture.md
  • docs/dependency-notes.md
  • docs/payload-format.md
  • docs/testing.md
  • docs/url-fragments.md
  • scripts/bench-baseline.json
  • scripts/bench-codecs.mjs
  • skills/agent-render-linking/SKILL.md
  • skills/selfhosted-agent-render/SKILL.md
  • src/components/home/sample-link-data.ts
  • src/components/viewer-shell.tsx
  • src/lib/payload/arx-codec.ts
  • src/lib/payload/examples.ts
  • src/lib/payload/fragment-arx.ts
  • src/lib/payload/fragment.ts
  • src/lib/payload/link-creator.ts
  • src/lib/payload/schema.ts
  • tests/arx-codec.test.ts
  • tests/e2e/viewer.spec.ts
  • tests/fixtures/baanish-code-bench-report.md
  • tests/fragment.test.ts
  • tests/link-creator.test.ts
  • tests/sample-link-data.test.ts
✅ Files skipped from review due to trivial changes (3)
  • README.md
  • src/lib/payload/examples.ts
  • skills/agent-render-linking/SKILL.md
🚧 Files skipped from review as they are similar to previous changes (6)
  • src/components/home/sample-link-data.ts
  • tests/e2e/viewer.spec.ts
  • src/lib/payload/link-creator.ts
  • src/components/viewer-shell.tsx
  • src/lib/payload/fragment.ts
  • src/lib/payload/arx-codec.ts

Comment thread src/lib/payload/fragment-arx.ts Outdated

@baanish baanish left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated multi-agent review of the optimization pass. No correctness blockers — codecs round-trip, loop rewrites are behavior-preserving, raw rendering is XSS-safe, and the static-server traversal posture is improved. The inline comments below are the SHOULD-FIX items: two authoritative numbers that need fortifying into defended contracts, two duplicated helpers, one silent UX change to confirm, and a server-header test gap. I'm pushing fixes for all of these to this branch.

value,
codec: "arx3",
packed: false,
transportLength: preferVisibleChars ? value.length : computeTransportLength(value),

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SHOULD-FIX — arx3 baseBMP wins by yardstick, not by size. The baseBMP candidate is measured with raw value.length, while every other candidate in the shared pool — including arx2's byte-identical baseBMP payload — is measured with percent-escape-inflated computeTransportLength (~9x for BMP chars). Since selectCandidate picks the global minimum, arx3 essentially always wins via the inconsistent metric, not a real difference. This is the intended outcome, but per our 'fortify heuristics into defended contracts' bar it should be (1) an explicitly-owned policy in the doc-comment (the visible-length-wins decision and its consequence), and (2) pinned by a characterization test asserting arx3-baseBMP is selected over arx2 in auto mode. Today no test exercises that head-to-head.

Comment thread scripts/check-build-budgets.mjs Outdated
const reactLoadableManifestPath = join(nextDir, "react-loadable-manifest.json");
const gzipSizeCache = new Map();

const budgets = [

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SHOULD-FIX — budgets are undefended magic numbers. The four maxBytes ceilings carry no comment on why each was chosen, what regression each guards, or what a maintainer should do when one trips (tighten vs. accept-and-bump), and nothing pins the enforcement semantics. Per the 'defended contracts' bar: add a per-budget rationale comment naming the policy, and a characterization test pinning the boundary behavior (at-budget passes, over-budget fails) so a refactor that silently doubles a chunk is caught. No test currently references this script.

const inspectorAnimationStyle: CSSProperties = { animationDelay: "220ms" };
const initializeAnimationStyle: CSSProperties = { animationDelay: "260ms" };

function getVisibleHashLength(hash: string): number {

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SHOULD-FIX — duplicates the budget-length contract. getVisibleHashLength is byte-for-byte identical to the exported getVisibleFragmentLength in src/lib/payload/fragment.ts (same decodeURIComponent(...).length + try/catch). This is exactly the length metric the lib enforces at fragment time — it should have one owner. Import and reuse getVisibleFragmentLength; the switch from raw hash.length - 1 to decoded visible length is a genuine improvement, so keep the behavior, just dedupe the function.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Retracting this after verifying against the build budget. getVisibleHashLength is a deliberate local copy, not an oversight: importing getVisibleFragmentLength statically pulls the fragment/codec module (lz-string, fflate) into the homepage initial chunk, which pushed check:build-budgets over (homepage 115.7 KiB > 115.0 KiB; the shell otherwise loads that module dynamically). Reverted the dedupe and added a comment documenting the bundle-splitting reason so it isn't re-flagged. Fixed in 32e3efc.


const codecOptions = ["auto", ...codecs] as const;

const defaultLinkCreatorDraft: LinkCreatorDraft = {

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SHOULD-FIX — duplicated source of truth. defaultLinkCreatorDraft and getBodyFieldLabel are redefined here but still exported (currently identical) from @/lib/payload/link-creator. They will drift. These constants don't pull in the encoder, so a static import from the lib is fine — only createGeneratedArtifactLinkAsync needs the lazy import() for chunk-splitting. Import them from the lib and delete the local copies (or remove the now-unused lib exports).

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Retracting — this was a bad observation on my part. At HEAD defaultLinkCreatorDraft/getBodyFieldLabel existed only in the component, not in @/lib/payload/link-creator, so there was no duplication to remove. Moving them into the lib and importing them statically also pulled link-creator.ts → fragment.ts (codec deps) into the homepage chunk and breached the build budget. Reverted in 32e3efc; the helpers stay local to the component.

}, [onReady]);

return (
<pre className="artifact-raw-source" data-testid={testId}>

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SHOULD-FIX — confirm the raw-view de-highlighting is intentional. Raw mode for markdown/CSV previously rendered through CodeRenderer (CodeMirror-highlighted source); RawArtifactSource now emits a plain <pre>. Testids/e2e still pass, but it's a visible UX change (no highlighting) that's silent in the diff. If deliberate (lighter chunk, no CodeMirror on the raw path) — which is consistent with the optimization goal — call it out in a comment and pin it with a unit test asserting raw markdown/CSV does not mount CodeMirror, so it can't silently regress.

Comment thread scripts/serve-export.mjs
}

if (filePath.endsWith(".br")) {
headers["Content-Encoding"] = "br";

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SHOULD-FIX — no server-level test pins the .br header contract. tests/headers.test.ts only asserts the static Cloudflare public/_headers file. Neither this script nor selfhosted/server.ts has a test asserting a .br request returns Content-Encoding: br + the decompressed Content-Type + Vary: Accept-Encoding (and that _next/static gets immutable). These are security-sensitive headers — worth pinning.

/^diff --git /m.test(patch) ||
(/^--- /m.test(patch) && /^\+\+\+ /m.test(patch)) ||
/^@@ /m.test(patch) ||
/^Binary files .* differ$/m.test(patch) ||

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NIT — CRLF binary-marker gate. These two $-anchored checks won't match on CRLF input (the \r sits before the line end), so a CRLF binary-only patch falls to the raw view instead of the rich view. Low impact, but cheap to fix: differ\r?$ / patch\r?$, or normalize once before testing.

…plitting

Responds to the automated review of the optimization branch:

- arx3: reframe the visible-length selection metric as an explicitly owned
  policy and pin it with a characterization test (arx3 baseBMP is chosen over
  arx2 by visible length, not a real byte-size difference).
- build budgets: add per-budget rationale, extract a pure evaluateBudget
  helper (+ .d.mts types), and pin the policy table and boundary semantics.
- viewer raw view: document the deliberate un-highlighted plain-<pre> render
  and pin that the raw path never mounts CodeMirror.
- static servers: add tests asserting the precompressed (.br) header contract
  (Content-Encoding: br + decompressed Content-Type + Vary) and immutable
  _next/static caching, for serve-export and the self-hosted server.
- diff renderer: make the binary-marker gate CR-tolerant, name the hash-based
  reset key as a deliberate bound, and add a CRLF binary-patch regression test.
- examples: pin the showcase benchmark figures to scripts/bench-baseline.json.
- viewer shell: document why getVisibleHashLength is intentionally a local copy
  — deduping it pulls the codec module (lz-string/fflate) into the homepage
  chunk and breaches check:build-budgets.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@greptile-apps

greptile-apps Bot commented Jun 19, 2026

Copy link
Copy Markdown

Too many files changed for review. (102 files found, 100 file limit)

port = await getFreePort();
child = spawn(
process.execPath,
["--import", "tsx", path.join(repoRoot, "selfhosted", "server.ts")],

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: The self-hosted server is spawned with cwd: repoRoot, so this fixture never points the process at the temporary export directory it just creates. That means the test is validating the real repository output rather than the isolated fixture, which makes the header contract assertion brittle and can hide regressions in the generated export tree.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

@kilo-code-bot

kilo-code-bot Bot commented Jun 19, 2026

Copy link
Copy Markdown

Code Review Summary

Status: 1 Issue Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 1
SUGGESTION 0
Other Observations (not in diff)

Issues found in unchanged code that cannot receive inline comments:

File Line Issue
tests/selfhosted/static-headers.test.ts 107 The self-hosted server is spawned with cwd: repoRoot, so this fixture does not actually point the process at the temporary export directory it creates. The test therefore exercises the repository tree rather than an isolated export fixture.
Files Reviewed (4 files)
  • scripts/serve-export.mjs - 0 issues
  • selfhosted/server.ts - 0 issues
  • src/components/renderers/markdown-renderer.tsx - 0 issues
  • src/lib/payload/fragment-arx.ts - 0 issues
Previous Review Summary (commit 32e3efc)

Current summary above is authoritative. Previous snapshots are kept for context only.

Previous review (commit 32e3efc)

Status: 1 Issue Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 1
SUGGESTION 0
Issue Details (click to expand)

WARNING

File Line Issue
tests/selfhosted/static-headers.test.ts 107 The self-hosted server is spawned with cwd: repoRoot, so this fixture does not actually point the process at the temporary export directory it creates. The test therefore exercises the repository tree rather than an isolated export fixture.
Files Reviewed (4 files)
  • tests/selfhosted/static-headers.test.ts - 1 issue
  • tests/serve-export-headers.test.ts
  • src/lib/payload/fragment.ts
  • tests/fragment.test.ts

Reviewed by gpt-5.4-mini-2026-03-17 · 88,588 tokens

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
tests/selfhosted/static-headers.test.ts (1)

19-95: ⚡ Quick win

Consider extracting shared test helpers to reduce duplication.

The helper functions getFreePort, createExportFixture, waitForReady, and stopServer (lines 19-95) are duplicated nearly verbatim in tests/serve-export-headers.test.ts. Extracting these to a shared test utility module (e.g., tests/helpers/server-testing.ts) would improve maintainability and reduce code duplication.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/selfhosted/static-headers.test.ts` around lines 19 - 95, Extract the
helper functions getFreePort, createExportFixture, waitForReady, and stopServer
from this test file and create a shared test utility module at
tests/helpers/server-testing.ts. Move these four functions to the new module,
then import and use them in both tests/selfhosted/static-headers.test.ts and
tests/serve-export-headers.test.ts to eliminate the duplication. This will
centralize the shared test infrastructure and make future maintenance easier.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@tests/selfhosted/static-headers.test.ts`:
- Around line 19-95: Extract the helper functions getFreePort,
createExportFixture, waitForReady, and stopServer from this test file and create
a shared test utility module at tests/helpers/server-testing.ts. Move these four
functions to the new module, then import and use them in both
tests/selfhosted/static-headers.test.ts and tests/serve-export-headers.test.ts
to eliminate the duplication. This will centralize the shared test
infrastructure and make future maintenance easier.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 0d83e75f-2fbc-416d-990f-091b67e32524

📥 Commits

Reviewing files that changed from the base of the PR and between 2a2cd0f and 32e3efc.

📒 Files selected for processing (14)
  • scripts/check-build-budgets.d.mts
  • scripts/check-build-budgets.mjs
  • src/components/renderers/diff-renderer.tsx
  • src/components/viewer-shell.tsx
  • src/components/viewer/artifact-stage.tsx
  • src/lib/payload/examples.ts
  • src/lib/payload/fragment-arx.ts
  • tests/build-budgets.test.ts
  • tests/components/artifact-stage-raw.test.tsx
  • tests/components/diff-renderer.test.tsx
  • tests/examples-arx3-benchmark.test.ts
  • tests/fragment-arx-selection.test.ts
  • tests/selfhosted/static-headers.test.ts
  • tests/serve-export-headers.test.ts
🚧 Files skipped from review as they are similar to previous changes (6)
  • tests/components/diff-renderer.test.tsx
  • src/components/viewer/artifact-stage.tsx
  • src/lib/payload/examples.ts
  • src/components/renderers/diff-renderer.tsx
  • src/components/viewer-shell.tsx
  • src/lib/payload/fragment-arx.ts

…e, arx resilience

- serve-export.mjs: decode percent-escaped request paths (files with spaces/
  non-ASCII names now resolve) and attach a read-stream error handler so a file
  removed after startup tears down the response instead of crashing the server.
- markdown-renderer: drop stale block-ready callbacks on key mismatch instead of
  adopting them into the new artifact's key (the readyKey effect already resets
  the tracking refs), avoiding a premature/incorrect onReady for new content.
- fragment-arx: reset the cached dictionary-load promise on failure so a transient
  load error can retry; document the intentional pre-versioning double-decode.
- selfhosted/server.ts: strip a trailing .br generically for content-type and
  Content-Encoding so future *.js.br / *.wasm.br don't regress to octet-stream
  (matches serve-export.mjs).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
baanish and others added 2 commits June 19, 2026 21:20
isPayloadEnvelope validated diff content but not the optional `view` field,
so a payload with `view` set to anything other than "unified"/"split" passed
validation and reached the diff renderer with an unsupported value. Reject it
at the schema boundary, matching the declared `view?: "unified" | "split"`
type, with a regression test.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Integrates the copy-current-URL-as-markdown-link feature (PR #68) into the
refactored ArtifactStage toolbar so #67's bundle-splitting is preserved.
@socket-security

Copy link
Copy Markdown

Warning

Review the following alerts detected in dependencies.

According to your organization's Security Policy, it is recommended to resolve "Warn" alerts. Learn more about Socket for GitHub.

Action Severity Alert  (click "▶" to expand/collapse)
Warn High
Obfuscated code: npm mermaid is 90.0% likely obfuscated

Confidence: 0.90

Location: Package overview

From: package-lock.jsonnpm/mermaid@11.15.0

ℹ Read more on: This package | This alert | What is obfuscated code?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Packages should not obfuscate their code. Consider not using packages with obfuscated code.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore npm/mermaid@11.15.0. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

Warn High
Obfuscated code: npm mermaid is 90.0% likely obfuscated

Confidence: 0.90

Location: Package overview

From: package-lock.jsonnpm/mermaid@11.15.0

ℹ Read more on: This package | This alert | What is obfuscated code?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Packages should not obfuscate their code. Consider not using packages with obfuscated code.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore npm/mermaid@11.15.0. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

Warn High
Obfuscated code: npm mermaid is 90.0% likely obfuscated

Confidence: 0.90

Location: Package overview

From: package-lock.jsonnpm/mermaid@11.15.0

ℹ Read more on: This package | This alert | What is obfuscated code?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Packages should not obfuscate their code. Consider not using packages with obfuscated code.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore npm/mermaid@11.15.0. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

Warn High
Obfuscated code: npm next is 90.0% likely obfuscated

Confidence: 0.90

Location: Package overview

From: package-lock.jsonnpm/next@15.5.18

ℹ Read more on: This package | This alert | What is obfuscated code?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Packages should not obfuscate their code. Consider not using packages with obfuscated code.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore npm/next@15.5.18. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

View full report

@baanish
baanish merged commit 50515cf into main Jun 19, 2026
9 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