Give a blanked link one immediate second chance so a momentary failure
recovers right away instead of waiting out the full self-heal window.
- A transient fetch failure (timeout/5xx/network) earns one inline retry after
a short backoff before falling into the 30s self-heal wait. If that retry
succeeds the card shows immediately.
- A rate limit (429) skips the inline retry (an immediate retry would just be
throttled again) and waits out its Retry-After instead, so we do not re-trip
the limit. The whole sequence stays under one coalesced promise inside the
2-concurrent scheduler.
- Move the rate-limit / transient-status classification into the rate_limit
submodule where retry_after_duration already lives, keeping link_preview.rs
within the file-size ratchet.
Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
A valid link (e.g. a GitHub PR page) could render no preview card for five
minutes after a single unlucky fetch, then quietly work again. One slow
handshake, a rate-limit blip, or a 5xx would poison the card.
Two paths funneled a transient failure into the same sticky negative cache as
a genuine "no metadata" miss:
- The resolver's fetch loader caught any rejection (timeout, network error) as
a plain null and cached it under NULL_METADATA_RETRY_MS (5 minutes).
- The native fetcher returned Ok(None) for a retryable page status (429, 408,
425, 5xx), which the resolver also cached for the full miss TTL.
Distinguish transient failures from hard misses:
- The loader now records a rejected fetch as transient and caches it for the
short transient TTL (30s) instead of the 5-minute miss TTL. A resolved null
(200 + HTML + no metadata) still holds the full miss TTL.
- The native fetcher surfaces a retryable page status as an error (via a shared
is_transient_status helper, reused by the image path) so it takes the short
transient TTL rather than the sticky negative cache.
Also widen the per-request timeout from 4s to 6s (total 10s to 15s) so slow
networks stop tripping the transient path in the first place.
Tests: JS regression that a rejected fetch clears well before the 5-minute
miss TTL while a genuine null miss stays cached; the rejected-retry test now
asserts the 30s transient boundary; Rust unit test that retryable statuses are
transient and permanent ones (404/403/401/410) are hard misses. Full desktop
unit suite 4580 passed; link_preview Rust suite 22 passed.
Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
## Overview
**Category:** improvement
**User impact:** Link previews appear in the composer and travel as
privacy-safe sender-authored snapshots, so recipients never contact the
linked site merely by opening a conversation.
**Problem:** Cold-cache link paste could freeze the composer before the
URL painted; recipient-side unfurling leaked visits; invalid or
unresolved preview work could interfere with sending or leave dead cards
behind.
**Solution:** Paint pasted links before starting cold resolver work,
resolve only in the sender's composer, attach only complete validated
snapshots at Send, and render authored snapshots without recipient
fallback fetching.
## Behavior
- **Cold paste stays responsive:** bare and angle-bracket URL paste
paths commit the visible link before resolver work begins.
- **Sender-only fetching:** metadata is resolved while composing;
recipients render only the sender-authored snapshot.
- **Send never waits:** pending, failed, invalid, and unsendable
previews are omitted. They do not block or cancel the message.
- **Terminal misses disappear:** failed, timed-out, or 404 resolver
results remove the composer card while preserving visible link text.
- **Display-text links work:** Markdown links such as `[review the pull
request](…)` produce and send the same snapshots as bare URLs.
- **Compact and Rich presentation:** Compact remains the default; Rich
preserves source description line breaks and paragraphs.
- **Immediate draft-wide dismissal:** clicking × immediately hides all
previews for the draft, suppresses links pasted later, and emits only
`["link-preview", "none"]`. No confirmation detour. Suppression resets
after send or clearing the draft.
- **Zero recipient fallback:** missing, stale, malformed, off-relay,
unsupported, or suppressed snapshots remain ordinary visible links;
recipients never regenerate them.
## Implementation
- Resolve previews from deferred composer URL state so paste can paint
first.
- Upload finished preview media to the active community relay and
snapshot only valid, sendable media references.
- Atomically capture ready snapshots at submit time; never append a late
preview after send.
- Validate snapshot and suppression tags in desktop/native and relay
ingestion, rejecting duplicate or mixed forms.
- Render composer previews as stable 55px attachment cards at desktop
and narrow widths.
- Add deterministic E2E coverage for cold paste,
ready/pending/failed/invalid previews, display-text links, multiline
Rich descriptions, immediate dismissal, later-pasted links, and
suppression reset.
## Validation
Validated head: `9807ba8952f190e76153834abf8ab61dd40be5e2`
- Push hooks passed: `check-push-org`, branch skew, desktop check,
mobile tests, desktop tests, Rust tests, and desktop Tauri checks.
- Focused screenshot E2E at the validated head: 5/5 passed across
Compact/Rich composer and recipient states, 800px/420px geometry,
display-text links, multiline descriptions, and immediate dismissal.
- PR CI was triggered for this exact head and is currently running;
completed checks are green at the time of this update.
- Worktree is clean and both PR head and validated branch resolve to
`9807ba895…`.
## Screenshots
### Compact composer
| Loading | Ready |
|---|---|
| 
| 
|
### Rich composer
| Loading | Ready |
|---|---|
| 
| 
|
### Responsive composer
| 800px loading | 800px ready |
|---|---|
| 
| 
|
| 420px loading | 420px ready |
|---|---|
| 
| 
|
### Recipient presentation
| Compact | Rich |
|---|---|
| 
| 
|
### Display-text Markdown link
| Composer | Recipient |
|---|---|
| 
| 
|
### Rich multiline description

### Immediate dismissal
| Before × | Immediately after × |
|---|---|
| 
| 
|
---------
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
## Summary
Gives Buzz-hosted git entities the same "GitHub-style" chat experience
GitHub links already get: rich preview cards, real titles, and
click-through — except clicks navigate **in-app** to the Projects view
instead of a browser.
- **Spec**: `docs/buzz-entity-links.md` — link scheme, slices, and
deferred work (`buzz://project`, OS deep links, web routes).
- **Canonical `buzz://` deep links**: new
`desktop/src/shared/lib/entityLink.ts` with builders + strict parser for
`buzz://pr?id=…&owner=…&d=…`, `buzz://issue?…`, and
`buzz://repo?owner=…&d=…`, mirrored by a Rust module
(`crates/buzz-cli/src/links.rs`) with a shared golden-format test so the
two implementations can't drift.
- **Preview cards**: `linkPreview.ts` recognizes `buzz://` entity links
*and* HTTPS relay clone URLs (`{origin}/git/<pubkey>/<repo>`, the shape
agents paste today). Both normalize onto the canonical `buzz://` href,
so the two spellings of a repo dedupe to one `Buzz`-provider card
(`BuzzMark` logo) rendered by `link-preview-attachment.tsx`.
- **Title enrichment**: PR/issue cards fetch the real subject from the
relay event (`subject` tag or first content line) via
`useResolvedLinkPreviews.ts`; the cache is community-scoped and reset in
`resetCommunityState()`.
- **In-app navigation**: clicking a card or inline anchor (including
HTTPS relay clone URLs whose origin matches the active relay) routes to
the canonical `30617:<owner>:<d>` coordinate via `goProject()`
(`markdown/entityLinks.tsx`). **Merge dependency: #4671 must merge
first** — route resolution for `30617:` coordinates is implemented on
that branch (`feat/multi-repository-projects`). Entity-link and
external-anchor logic were extracted out of `markdown.tsx` to stay under
the file-size ratchet.
- **Agent side**: `buzz pr open`, `buzz issues create`, and `buzz repos
create` now return a ready-made `link` field (omitted when the relay
returns `accepted: false`), and `base_prompt.md` instructs agents to
paste it verbatim when announcing work.
## Test plan
- [x] Desktop unit tests: pass, including new `entityLink.test.mjs` and
`linkPreview.test.mjs` coverage (golden formats, malformed-link
rejection, clone-URL/`buzz://` dedupe, origin-gated anchor behavior,
label-must-win invariant, cache epoch)
- [x] Rust: `cargo test -p buzz-cli` golden-format test +
accepted/rejected link guard assertions, clippy + fmt clean
- [x] Biome + `tsc --noEmit` clean; pre-push hooks
(desktop-tauri-checks, rust-tests, desktop-test) pass
- [ ] Manual: paste a relay clone URL and a `buzz://pr` link in a
channel — verify one card each, real PR title, and in-app navigation to
the Projects view
Related: [#4671](https://github.com/block/buzz/pull/4671)
---------
Signed-off-by: Thomas Petersen <thomasp@squareup.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: npub1g8493u0xfsjrvflg4n08ezd7vec99mnwzlv0qgwpr9d7gvjwhuzqx59rhw <41ea58f1e64c243627e8acde7c89be667052ee6e17d8f021c1195be4324ebf04@buzz.block.builderlab.xyz>
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>