The diff endpoint walked back from the head commit and stopped at the
first commit equal to `base`. `surf::History` is an unhidden revwalk in
commit-date order, so a head that merges the target branch in reaches
`base` through one of its parents and truncates the walk there, dropping
every commit the other parent contributes. A patch whose head is such a
merge showed a single commit no matter how many it really carried.
Resolve the merge target from `target` and the project payload, and name
it in the byline next to the patch ID.
Co-Authored-By: Brandon Oxendine <brandon.oxendine@gmail.com>
Cloudflare's single-page-application fallback percent-encodes the
path on a fresh load or reload, so `rad:z4V1...` arrives as
`rad%3Az4V1...` and leaks into links and the seed command.
Only identifiers are decoded: revision and path segments stay
encoded, because branch and tag lookups are keyed on the encoded
name.
The seed command needs a second fix: the repo path segment is an
alias when one was used, and `rad seed <alias>` is not valid, the
`rad seed` command only supports RIDs and not aliases.
Co-Authored-By: Guillaume <guillaume.alabre@gmail.com>
Listing remotes verifies signed refs for every peer and took seconds
on large repositories, blocking navigation.
Resolve revisions from the canonical refs already on the repo object,
fetching a single remote when the route names a peer, and load the
full listing after render. Tags living only on a remote no longer
resolve as bare revisions.
The cache only stored resolved values, so callers asking for the same
key while a request was still in flight each issued their own. The
peer selector makes this easy to hit: Source and History render their
own Header, so switching tabs while the remote listing loads fires a
second copy of a request that takes seconds.
Cache the promise instead, evicting it on rejection so failures still
retry.
So the badges stop fetching the full diff just for insertions and
deletions; the patch view did it on every load. Falls back to the
full diff when the backend doesn't serve the new endpoint.
The patch badge only needs insertions/deletions, but the full diff
endpoint builds every hunk and loads every changed blob.
This favours speed over exactness: `git diff --numstat` does not do
the rename/copy detection radicle-surf does, so on patches with copied
or renamed files the badge can diverge slightly from the full diff
view. In return it is roughly 6x faster on small diffs and 15x on
large ones.
So the header and history stop fetching full tree stats just for the
commit count. Falls back to the previous behaviour when the backend
doesn't serve the new endpoint.
The header and history only need a commit count, but /stats/tree walks
the full history and builds a contributor set to produce it.
Add /stats/commits, counting with `git rev-list --count`
(bitmap/commit-graph), falling back to a libgit2 revwalk.
surf's blob() walks the whole history to find the commit that last
touched a file, taking seconds to minutes on large repositories.
Read the blob directly and resolve its last commit with
`git rev-list -1`, which uses the commit-graph, falling back to surf.
Resolving a repo's signed refs opens a descriptor per packfile, and
radicle storage can hold thousands, so the default limit is exhausted
and reads fail with spurious "missing object" errors.
This also upgrades dompurify to the latest version, outside our cooldown
window, for a security fix (verified safe with extra security checks).
Newer dompurify versions expose a bug where happy-dom renders one of our
custom elements incorrectly in tests, unlike real browsers. Switch the unit
test environment from happy-dom to jsdom to fix the failing tests.
The canonical radicle.network deployment serves the marketing landing
page at `/`, while self-hosted deployments keep the node view. Opt in
at build time without changing the default.
radicle-search now ships in the same release as radicle-httpd: one
tag, one changelog, one reproducible build, with per-binary artifacts
in sibling folders on files.radicle.dev.
meilisearch-sdk: use the pure-Rust JWT backend (jwt_rust_crypto) instead
of jwt_aws_lc_rs, dropping aws-lc-sys, which fails to cross-compile to
the Apple targets under cargo-zigbuild.
The source and commit views hit the /raw archive endpoint with a
HEAD request on every page load to decide whether to show the
download UI. That endpoint runs `git archive` server-side.
Move detection to user interaction: the clone popover probes when
the Download tab is opened, and the commit view checks on click,
navigating on success or showing a message on failure.
Cache only successful results per URL so transient or moving-ref 404s
aren't latched for the session.
The same run can appear under multiple job COBs at a commit,
causing an each_key_duplicate crash. Keep one view per runId,
letting a terminal status override a stale "started" one.
Public seeds return `"agent": null` because their deployed httpd
binaries can't parse i2p/onion rows that newer nodes write to the
address table — `AddressStore::get` fails and the handler falls back
to `None`. Allow null in the zod schema and hide the user-agent row
in the UI when it's missing.
Workaround until heartwood ships `StoreExt::agent_of` (reads only
the `agent` column, no address hydration) and httpd adopts it.
This prevents flakes in the visual test suite due to stack frame naming
mismatches in the command copy code box.
E.g.
```diff
-{"stack":"Error: Response error\n at Dx.fet
+{"stack":"Error: Response error\n at wx.fet
```
When mermaid.render fails, suppress mermaid's "Syntax error"
overlay and show our own small warning above the original code
block so the source stays visible. Subsequent diagrams in the
same document still render.
Hoist the dynamic import and initialize() into a
module-scoped promise. Previous code re-ran initialize()
on every afterUpdate that had a mermaid block.
Switch to a dynamic import in afterUpdate and drop
manualChunks so Rolldown auto-splits the lazy chunk.
Eager bundle drops from 1.3 MB to 610 KB gzipped.
Replace global setConfig with an exported sanitizeConfig passed
inline. The global config leaked into mermaid's internal
strict-mode sanitization, stripping valid SVG diagrams.
Changes which were needed after the upgrade:
- Replace vite-plugin-virtual (vite 8 incompatible) with vite's
built-in `define` for the buildTimeConfig injection
- tsconfig: drop deprecated `baseUrl` (TS 5+ resolves `paths`
relative to tsconfig)
- Vitest 4: import `defineConfig` from `vitest/config` so `test`
is valid in vite configs
- Zod 4: rename `.errors` to `.issues`, make `ZodSchema` a
type-only import, drop the removed `SafeParseReturnType`
- Tighten/loosen generic casts in localStore to match stricter
zod 4 inference
- ESLint 9 `no-useless-assignment`: remove redundant initializers
and dead reassignments; disable the rule where Svelte's reactive
blocks confuse the analyser
- Svelte/eslint `prefer-svelte-reactivity`: disable for legacy
Svelte usages of URL / Set
- Misc naming-convention and prefer-const fixes
Playwright 1.52 hangs under Node 24 when loading test files that
transitively import modules with top-level await (tests/support/
support.ts). Node 24 enables require(esm) by default, and the old
CJS-based .ts transformer deadlocks on TLA.
Also update CLAUDE.md: playwright 1.59's --project flag is variadic
and greedily consumes trailing args, so the file path must come
before --project.
This also drops .tool-versions to keep a single source of truth for the
node version in .nvmrc. We also point the github actions to .nvmrc now.
I'm the only dev who's using asdf to manage my tooling and I decided to
just configure it to fall back to .nvmrc via `legacy_version_file =
yes`.
Deploys two workers: og-injector intercepts social crawler requests and
injects Open Graph meta tags; open-graph renders 1200×630 PNG cards
using Satori with repo/user avatars ported from avatar.ts.