Optimize fragment rendering and homepage link generation#67
Conversation
📝 WalkthroughWalkthroughAdds arx3 and arx2 dictionary .br support, refactors ARX compressors/decoders and fragment selection, rewrites renderers and ViewerShell for lazy client rendering and readiness, adds base-path helpers, improves static/selfhosted serving and headers, adds asset compression and build-budget scripts, and expands tests and docs. ChangesTransport, renderers, hosting, and tests
Sequence DiagramsequenceDiagram
participant Browser
participant ViewerShell
participant FragmentModule
participant ArtifactStage
participant StaticServer
Browser->>ViewerShell: location.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)
Estimated code review effort: Possibly related PRs:
Poem:
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
|
Deploying agent-render with
|
| Latest commit: |
2a2cd0f
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://d30c41c0.agent-render.pages.dev |
| Branch Preview URL: | https://codex-optimize-codebase.agent-render.pages.dev |
There was a problem hiding this comment.
💡 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".
| fragmentModulePromise ??= import("@/lib/payload/fragment"); | ||
| return fragmentModulePromise; |
There was a problem hiding this comment.
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 👍 / 👎.
There was a problem hiding this comment.
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 winClear injected mode on internal navigation.
After a UUID-injected load,
setFragmentHash()andhandleGoHome()updatehashwithout ahashchangeevent, soinjectedPayloadRef.currentstaystrue. 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 winAdd 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 valueNote:
medianmutates the input.
values.sort(...)sorts in place. Callers (measure, line 189) passsamplesand then return{ result, ms: median(samples) }without usingsamplesagain, 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 valueMinor:
repeatedFixturemay 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 valueConsider co-locating constants in a module helper if reuse grows.
fieldHints,fieldPlaceholders,codecOptions,defaultLinkCreatorDraft, andgetBodyFieldLabelare 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 smalllink-creator-presets.tswould 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 valueConsider extending Brotli handling to other pre-compressed assets.
The current logic only flags
.json.brand.css.bras Brotli-encoded. If the build ever emits other.brvariants (e.g.,.js.br,.svg.br,.html.br), they'll be served withContent-Type: application/octet-streamand noContent-Encoding: br, breaking the client. A general.brhandler 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 valueConsider adding
Content-Lengthfor HEAD/GET responses.
stat()is already called and yieldsdetails.size, but it's discarded. SettingContent-Lengthlets clients (and CI HEAD checks for.brassets) 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 valueEdge case: module-level stylesheet state isn't reference-counted across instances.
diffViewStylesheetPromiseand the<link id="agent-render-diff-view-styles">element are module globals. If twoDiffRendererinstances 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 callremoveDiffViewStylesheet()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 winStylesheet load failure is silently swallowed.
loadDiffViewStylesheet().catch(() => undefined).then(() => setStylesReady(true))marks styles "ready" even when both the.brand plain.cssURLs 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 leastconsole.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 valueMinor: leading content before first
diff --gitis silently dropped.In
parsePatchSections, if the patch contains preamble text before the firstdiff --githeader (e.g., aFrom ...mbox-style header fromgit format-patch), that prefix is skipped because the loop only starts accumulating after the first match. Today’s callers (UI/test fixtures) always feed plaindiff --gitbundles so this is not user-facing, but it’s worth noting as a known limitation if patches fromgit format-patchare 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
⛔ Files ignored due to path filters (8)
package-lock.jsonis excluded by!**/package-lock.jsontests/e2e/visual.spec.ts-snapshots/code-light-chromium.pngis excluded by!**/*.pngtests/e2e/visual.spec.ts-snapshots/csv-compact-light-chromium.pngis excluded by!**/*.pngtests/e2e/visual.spec.ts-snapshots/diff-light-chromium.pngis excluded by!**/*.pngtests/e2e/visual.spec.ts-snapshots/empty-state-light-chromium.pngis excluded by!**/*.pngtests/e2e/visual.spec.ts-snapshots/json-light-chromium.pngis excluded by!**/*.pngtests/e2e/visual.spec.ts-snapshots/markdown-dark-chromium.pngis excluded by!**/*.pngtests/e2e/visual.spec.ts-snapshots/markdown-light-chromium.pngis excluded by!**/*.png
📒 Files selected for processing (81)
README.mddocs/architecture.mddocs/dependency-notes.mddocs/deployment.mddocs/payload-format.mddocs/testing.mdeslint.config.mjsnext-env.d.tsnext.config.tspackage.jsonplaywright.config.tspublic/_headerspublic/arx2-dictionary.jsonpublic/arx2-dictionary.json.brpublic/vendor/diff-view-pure.csspublic/vendor/diff-view-pure.css.brscripts/bench-baseline.jsonscripts/bench-codecs.mjsscripts/check-build-budgets.mjsscripts/clean-build-output.mjsscripts/compress-dictionary.mjsscripts/ensure-next-types.mjsscripts/serve-export.mjsselfhosted/server.tsskills/agent-render-linking/SKILL.mdsrc/app/globals.csssrc/app/layout.tsxsrc/app/security/page.tsxsrc/app/url-explainer/page.tsxsrc/components/home/link-creator.tsxsrc/components/home/sample-link-data.tssrc/components/home/sample-links.tsxsrc/components/renderers/code-renderer.tsxsrc/components/renderers/csv-renderer.tsxsrc/components/renderers/diff-renderer.tsxsrc/components/renderers/json-renderer.tsxsrc/components/renderers/markdown-renderer.tsxsrc/components/renderers/mermaid-block.tsxsrc/components/theme-provider.tsxsrc/components/theme-toggle.tsxsrc/components/theme/use-theme-controller.tssrc/components/viewer-shell.tsxsrc/components/viewer/artifact-selector.tsxsrc/components/viewer/artifact-stage.tsxsrc/components/viewer/fragment-details-disclosure.tsxsrc/lib/code/language.tssrc/lib/diff/git-patch.tssrc/lib/payload/arx-codec.tssrc/lib/payload/envelope.tssrc/lib/payload/fragment-arx.tssrc/lib/payload/fragment.tssrc/lib/payload/link-creator.tssrc/lib/payload/schema.tssrc/lib/payload/wire-format.tssrc/lib/site/base-path.tssrc/lib/site/canonical-base.tssrc/lib/utils.tstests/arx-codec.test.tstests/base-path.test.tstests/code-language.test.tstests/components/code-renderer.test.tsxtests/components/csv-renderer.test.tsxtests/components/diff-renderer.test.tsxtests/components/json-renderer.test.tsxtests/components/link-creator.test.tsxtests/components/markdown-renderer.test.tsxtests/components/mermaid-block.test.tsxtests/components/theme-toggle.test.tsxtests/components/viewer-shell-artifact-select.test.tsxtests/components/viewer-shell.test.tsxtests/diff-style-asset.test.tstests/e2e/viewer.spec.tstests/envelope.test.tstests/fragment.test.tstests/git-patch.test.tstests/headers.test.tstests/sample-link-data.test.tstests/selfhosted/api-catalog.test.tstests/setup.tsxtests/utils.test.tstsconfig.json
💤 Files with no reviewable changes (3)
- src/components/viewer/fragment-details-disclosure.tsx
- tests/setup.tsx
- src/components/theme-provider.tsx
| .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); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 winAvoid the unchecked
stat()afterexistsSync().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 winConsider more robust hash parsing in the mock.
The
activeArtifactIdderivation 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 winConsider adding error handling tests.
The mock includes a
rejectcallback 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 winConsider 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
rejectfunction 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
📒 Files selected for processing (14)
docs/dependency-notes.mdscripts/bench-codecs.mjsscripts/serve-export.mjssrc/components/renderers/diff-renderer.tsxsrc/components/renderers/markdown-renderer.tsxsrc/components/theme/use-theme-controller.tssrc/components/viewer-shell.tsxsrc/lib/diff/git-patch.tstests/arx-codec.test.tstests/components/link-creator.test.tsxtests/components/markdown-renderer.test.tsxtests/components/viewer-shell-artifact-select.test.tsxtests/envelope.test.tstests/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
| const queryIndex = urlPath.indexOf("?"); | ||
| const hashIndex = urlPath.indexOf("#"); | ||
| let pathEnd = urlPath.length; | ||
| if (queryIndex !== -1) pathEnd = Math.min(pathEnd, queryIndex); | ||
| if (hashIndex !== -1) pathEnd = Math.min(pathEnd, hashIndex); | ||
| const cleanPath = urlPath.slice(0, pathEnd); | ||
| let relativePath = cleanPath; |
There was a problem hiding this comment.
Decode the request path before resolving it on disk.
request.url is still percent-encoded here, so valid requests like /images/My%20File.png or non-ASCII route segments resolve to literal %20 / %C3... filenames and 404 in preview.
Suggested fix
- const cleanPath = urlPath.slice(0, pathEnd);
+ let cleanPath;
+ try {
+ cleanPath = decodeURIComponent(urlPath.slice(0, pathEnd));
+ } catch {
+ return null;
+ }Also applies to: 121-122
🤖 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 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.
| const diffViewStylesheetHrefs = [ | ||
| withBasePath("/vendor/diff-view-pure.css.br"), | ||
| withBasePath("/vendor/diff-view-pure.css"), | ||
| ]; |
There was a problem hiding this comment.
🧩 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:
- 1: http://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Content-Encoding
- 2: https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Accept-Encoding
- 3: https://css-tricks.com/brotli-static-compression/
🏁 Script executed:
rg -n "diffViewStylesheetHrefs" src/components/renderers/diff-renderer.tsx -A 5 -B 2Repository: baanish/agent-render
Length of output: 655
🏁 Script executed:
rg -n "loadStylesheetHref" src/components/renderers/diff-renderer.tsx -A 10 -B 2Repository: baanish/agent-render
Length of output: 910
🏁 Script executed:
sed -n '126,150p' src/components/renderers/diff-renderer.tsxRepository: 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.
| 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.
| 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; | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, locate the file and check its structure
find . -type f -name "markdown-renderer.tsx" 2>/dev/nullRepository: baanish/agent-render
Length of output: 113
🏁 Script executed:
# Read the markdown-renderer.tsx file
cat -n ./src/components/renderers/markdown-renderer.tsxRepository: 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.
| const markBlockReady = useCallback((blockId: string) => { | ||
| if (readyKeyRef.current !== readyKey) { | ||
| readyKeyRef.current = readyKey; | ||
| readyBlockIdsRef.current.clear(); | ||
| reportedReadyKeyRef.current = null; | ||
| } | ||
|
|
||
| if (readyBlockIdsRef.current.has(blockId)) { | ||
| return; | ||
| } | ||
|
|
||
| readyBlockIdsRef.current.add(blockId); | ||
| const nextCount = readyBlockIdsRef.current.size; | ||
| setReadyBlockState({ key: readyKey, count: nextCount }); | ||
|
|
||
| if (nextCount >= embeddedBlockCount) { | ||
| reportReady(readyKey); | ||
| } | ||
| }, [embeddedBlockCount, readyKey, reportReady]); |
There was a problem hiding this comment.
Ignore stale block-ready callbacks instead of switching back to the old artifact key.
If an async child from the previous artifact.content resolves late, this mismatch branch rewrites the refs/state back to the stale readyKey and can report onReady for obsolete content. The stale callback should be dropped here, not adopted.
🔧 Suggested fix
const markBlockReady = useCallback((blockId: string) => {
if (readyKeyRef.current !== readyKey) {
- readyKeyRef.current = readyKey;
- readyBlockIdsRef.current.clear();
- reportedReadyKeyRef.current = null;
+ return;
}
if (readyBlockIdsRef.current.has(blockId)) {
return;
}📝 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.
| const markBlockReady = useCallback((blockId: string) => { | |
| if (readyKeyRef.current !== readyKey) { | |
| readyKeyRef.current = readyKey; | |
| readyBlockIdsRef.current.clear(); | |
| reportedReadyKeyRef.current = null; | |
| } | |
| if (readyBlockIdsRef.current.has(blockId)) { | |
| return; | |
| } | |
| readyBlockIdsRef.current.add(blockId); | |
| const nextCount = readyBlockIdsRef.current.size; | |
| setReadyBlockState({ key: readyKey, count: nextCount }); | |
| if (nextCount >= embeddedBlockCount) { | |
| reportReady(readyKey); | |
| } | |
| }, [embeddedBlockCount, readyKey, reportReady]); | |
| const markBlockReady = useCallback((blockId: string) => { | |
| if (readyKeyRef.current !== readyKey) { | |
| return; | |
| } | |
| if (readyBlockIdsRef.current.has(blockId)) { | |
| return; | |
| } | |
| readyBlockIdsRef.current.add(blockId); | |
| const nextCount = readyBlockIdsRef.current.size; | |
| setReadyBlockState({ key: readyKey, count: nextCount }); | |
| if (nextCount >= embeddedBlockCount) { | |
| reportReady(readyKey); | |
| } | |
| }, [embeddedBlockCount, readyKey, reportReady]); |
🤖 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 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.
| } else if (match.index > 0) { | ||
| const preamble = normalized.slice(0, match.index).trim(); | ||
| if (preamble) { | ||
| files.push(parsePatchSection(preamble, sectionIndex)); | ||
| sectionIndex += 1; | ||
| } |
There was a problem hiding this comment.
Don't turn patch preambles into synthetic file entries.
This branch promotes mail headers / other leading preamble text into a ParsedPatchFile, so downstream consumers treat it like a real diff file. That produces a bogus file-1 entry in the file nav and can also push non-file content into the rich diff path. Please keep preamble text separate from ParsedPatchFile[] (or drop it here) so the parser stays file-only.
🤖 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 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.
| 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(); | ||
| }); | ||
| }); | ||
| }); |
There was a problem hiding this comment.
🛠️ 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.
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
There was a problem hiding this comment.
Actionable comments posted: 1
♻️ Duplicate comments (1)
scripts/serve-export.mjs (1)
146-153:⚠️ Potential issue | 🟠 Major | ⚡ Quick winDecode the request path before route lookup.
Line 152 still keeps
%xxescapes intact, so requests like/images/My%20File.pngor 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
| response.writeHead(200, headersFor(staticFile.filePath, staticFile.size)); | ||
| if (method === "HEAD") { | ||
| response.end(); | ||
| return; | ||
| } | ||
|
|
||
| createReadStream(finalPath).pipe(response); | ||
| createReadStream(staticFile.filePath).pipe(response); |
There was a problem hiding this comment.
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.
| 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.
|
| 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/]
Reviews (2): Last reviewed commit: "Add ARX3 homepage sample and auto-encode..." | Re-trigger Greptile
| 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; | ||
| } |
There was a problem hiding this comment.
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.
|
|
||
| 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"); | ||
| } |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 winValidate
diff.viewat runtime inisPayloadEnvelope.
isPayloadEnvelopecurrently allows anyviewvalue 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" andsrc/lib/payload/schema.ts: "Supportviewproperty with valuesunifiedorsplitfor 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 winAdd 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
⛔ Files ignored due to path filters (2)
tests/e2e/visual.spec.ts-snapshots/bundle-switcher-light-chromium.pngis excluded by!**/*.pngtests/e2e/visual.spec.ts-snapshots/json-light-chromium.pngis excluded by!**/*.png
📒 Files selected for processing (24)
README.mddocs/architecture.mddocs/dependency-notes.mddocs/payload-format.mddocs/testing.mddocs/url-fragments.mdscripts/bench-baseline.jsonscripts/bench-codecs.mjsskills/agent-render-linking/SKILL.mdskills/selfhosted-agent-render/SKILL.mdsrc/components/home/sample-link-data.tssrc/components/viewer-shell.tsxsrc/lib/payload/arx-codec.tssrc/lib/payload/examples.tssrc/lib/payload/fragment-arx.tssrc/lib/payload/fragment.tssrc/lib/payload/link-creator.tssrc/lib/payload/schema.tstests/arx-codec.test.tstests/e2e/viewer.spec.tstests/fixtures/baanish-code-bench-report.mdtests/fragment.test.tstests/link-creator.test.tstests/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
| arxDictionaryLoadPromise ??= loadArxDictionary().then(() => undefined); | ||
| await arxDictionaryLoadPromise; |
There was a problem hiding this comment.
Reset cached dictionary-load promises after failures.
If one load attempt rejects, the cached rejected promise is reused forever. Subsequent calls keep failing even after transient network issues recover.
Proposed fix
async function ensureArxDictionaryLoaded(): Promise<void> {
if (isExternalDictionaryLoaded()) {
return;
}
- arxDictionaryLoadPromise ??= loadArxDictionary().then(() => undefined);
+ arxDictionaryLoadPromise ??= loadArxDictionary()
+ .then(() => undefined)
+ .catch((error) => {
+ arxDictionaryLoadPromise = null;
+ throw error;
+ });
await arxDictionaryLoadPromise;
}
@@
- arx2OverlayDictionaryLoadPromise ??= loadArx2OverlayDictionary().then(
- () => undefined,
- );
+ arx2OverlayDictionaryLoadPromise ??= loadArx2OverlayDictionary()
+ .then(() => undefined)
+ .catch((error) => {
+ arx2OverlayDictionaryLoadPromise = null;
+ throw error;
+ });
await arx2OverlayDictionaryLoadPromise;
}Also applies to: 50-53
🤖 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/fragment-arx.ts` around lines 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).
Summary
Testing
npm run checknpm run test:e2egit diff --checkSummary by CodeRabbit
New Features
Bug Fixes
Documentation
Tests