Files
buzz/docs/buzz-entity-links.md
cls 9dfa06ffee
Docker image / Build (linux/amd64) (push) Has been cancelled
Docker image / Build (linux/arm64) (push) Has been cancelled
Docker image / Merge release multi-arch manifest (push) Has been cancelled
Docker image / Merge debug multi-arch manifest (push) Has been cancelled
Docker image / Build public push gateway (linux/amd64) (push) Has been cancelled
Docker image / Build public push gateway (linux/arm64) (push) Has been cancelled
Docker image / Publish public push gateway image (push) Has been cancelled
Sprig image / Build (linux/amd64) (push) Has been cancelled
Sprig image / Build (linux/arm64) (push) Has been cancelled
Sprig image / Merge multi-arch manifest (push) Has been cancelled
Harbor Buzz Orchestra / Python tests and lint (push) Has been cancelled
CI / Detect Changed Paths (push) Has been cancelled
CI / Rust Lint (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Desktop Core (push) Has been cancelled
CI / Desktop Smoke E2E (1) (push) Has been cancelled
CI / Desktop Smoke E2E (2) (push) Has been cancelled
CI / Desktop Smoke E2E (3) (push) Has been cancelled
CI / Desktop Smoke E2E (4) (push) Has been cancelled
CI / Desktop (push) Has been cancelled
CI / Desktop E2E Relay (push) Has been cancelled
CI / Desktop E2E Integration (1/2) (push) Has been cancelled
CI / Desktop E2E Integration (2/2) (push) Has been cancelled
CI / Desktop E2E Integration (push) Has been cancelled
CI / Backend Integration (relay e2e) (push) Has been cancelled
CI / Relay E2E (push) Has been cancelled
CI / Web (push) Has been cancelled
CI / Mobile (push) Has been cancelled
CI / Security (push) Has been cancelled
CI / Dead Token Reference Guard (push) Has been cancelled
CI / Server Cross-Compile (aarch64-unknown-linux-musl) (push) Has been cancelled
CI / Server Cross-Compile (x86_64-unknown-linux-musl) (push) Has been cancelled
CI / Windows Rust (x86_64-pc-windows-msvc) (push) Has been cancelled
CI / Desktop Build (macOS) (push) Has been cancelled
helm chart / lint + unittest + render matrix (push) Has been cancelled
helm chart / install on kind (gated) (push) Has been cancelled
helm chart / publish chart to GHCR (push) Has been cancelled
Mesh Lifecycle / Relay-Driven Mesh Lifecycle Smoke (push) Has been cancelled
Sprig / Build (aarch64-unknown-linux-musl) (push) Has been cancelled
Sprig / Build (x86_64-unknown-linux-musl) (push) Has been cancelled
Sprig / Publish rolling release (push) Has been cancelled
Sprig / Publish tagged release (push) Has been cancelled
feat: import Chinese-localized Buzz source snapshot
Signed-off-by: cls_宁波本机 <908705107@qq.com>
2026-08-13 18:34:25 +08:00

13 KiB
Raw Permalink Blame History

Buzz Entity Links

Status: partially implemented. Done on this branch:

  • Slice 0 — HTTPS relay git clone URLs ({relay-origin}/git/<pubkey>/<repo>) render as Buzz repository preview cards in chat (desktop/src/shared/lib/linkPreview.ts).
  • Slice 1 — buzz://pr|issue|repo deep links: entityLink.ts builders/parser, preview cards with relay title enrichment, in-timeline click navigation to /projects/$projectId.
  • Slice 3 (create-command part) — crates/buzz-cli/src/links.rs, link output field on pr open / issues create / repos create, base prompt guidance, cross-language golden-format tests.

Still unimplemented: OS-level deep links (slice 2), link on get commands, the buzz://project scheme (waiting on NIP-MP landing), and the follow-ups in slice 4.

Problem

When a message contains a GitHub URL, the desktop client renders a rich preview card ("GitHub · PR block/buzz #4020") below the message. Those cards are produced entirely client-side by URL parsing in desktop/src/shared/lib/linkPreview.ts and rendered by desktop/src/shared/ui/link-preview-attachment.tsx.

Buzz-hosted entities have no equivalent. There is no link format at all for a Buzz repository, project, pull request, or issue:

  • The only rich deep link today is buzz://message?channel=…&id=… (desktop/src/features/messages/lib/messageLink.ts), rendered as an inline pill via remarkMessageLinks.ts + MessageLinkPill.tsx.
  • OS-level deep links (desktop/src-tauri/src/deep_link.rs, desktop/src/shared/deep-link.ts) support connect, join, add-community, message, and nostr-bind — no git entities.
  • buzz pr open / buzz issues create return raw event ids; there is no URL in their output and no guidance in the agent base prompt (crates/buzz-acp/src/base_prompt.md) for referencing Buzz work items in chat. Agents can only say "PR up" with a hex id.
  • The relay-served web client only has /repos/$repoId; no PR/issue pages.

So an agent that opens a PR on a Buzz-hosted repository cannot produce anything clickable, while the same agent opening a GitHub PR gets a card for free.

Goals

  1. A canonical, shareable link format for Buzz repositories, projects, pull requests, and issues.
  2. Rich preview cards in the desktop message timeline for those links, with parity to (and better data than) the GitHub cards — titles come from the actual Nostr events, not URL text.
  3. Clicking a link navigates in-app to the existing project detail views.
  4. CLI output includes the link so agents (and the base prompt) can emit it when announcing work.

Non-goals (v1)

  • Web (browser) pages for PRs/issues — the web client has no such views yet, so links are app-only, same as buzz://message today.
  • Cross-community links. Like buzz://message, links are interpreted against the community the message was received in. A relay= query parameter is reserved for a future cross-community version but not emitted or consumed.
  • Generic OpenGraph unfurling for arbitrary URLs — that is the separate proto/rich-link-previews prototype and stays orthogonal.
  • Mobile rendering. Mobile should degrade gracefully (plain link) in v1; pill/card parity is a follow-up.

Extend the existing buzz:// scheme, mirroring buzz://message:

buzz://repo?owner=<pubkey-hex>&d=<repo-dtag>
buzz://project?owner=<pubkey-hex>&d=<project-dtag>
buzz://pr?id=<event-id-hex>&owner=<pubkey-hex>&d=<repo-dtag>
buzz://issue?id=<event-id-hex>&owner=<pubkey-hex>&d=<repo-dtag>
  • owner is the 64-char lowercase hex pubkey of the repository/project announcement author (the NIP-34 / NIP-MP coordinate owner).
  • d is the addressable d-tag. For repo/project links the (owner, d) pair is the full 30617:<owner>:<d> / 30621:<owner>:<d> coordinate.
  • For pr/issue links, id identifies the kind 1618 / 1621 event; owner + d are the routing coordinate that lets the client navigate (and render a fallback card) without an event lookup. v1 decision: the implemented parser requires all three parameters — the CLI always emits them, and accepting hint-less links would force an event lookup before any navigation. A future revision can relax this without breaking existing links.

Validation rules match the existing codebase: owner and id are /^[a-f0-9]{64}$/; d follows addressable d-tag rules already enforced in projectModels.ts / buzz-sdk.

HTTPS URLs

Agents naturally paste HTTPS clone URLs ({relay-origin}/git/<pubkey>/<repo>) when announcing work, so those are recognized first — implemented on this branch. Detection keys on the path shape (/git/ + 64-hex pubkey segment) rather than a host allow-list, since relay hosts differ per community. The preview href is normalized to the canonical buzz://repo?owner=…&d=… deep link (the raw transport URL is not a browsable page), so clone-URL cards and inline clone-URL anchors get the same in-app click navigation as explicit entity links, and both spellings of the same repository dedupe to one card.

PRs, issues, and projects have no HTTPS page to link to (the web client has no such routes), which is why they use the buzz:// scheme above: it is community-relative by construction, matches the established buzz://message precedent, and requires no new relay surface. If web views land later, the desktop can additionally recognize those {relay-origin}/… URLs with the same card treatment.

Rendering in chat (desktop)

Two presentations, consistent with how GitHub links and message links behave today:

  1. Autolinked bare URL (<buzz://pr?…> or bare in text): render an attachment card below the message in the existing AttachmentGroup, exactly like GitHub cards. Provider label Buzz, type label PR / issue / repo / project.
  2. Explicitly labeled markdown link ([fix the tooltip](buzz://pr?…)): keep the author's label inline (same rule as resolveMessageLinkRenderTarget in messageLink.ts), still clickable.

Card content and enrichment

Unlike GitHub (title derived from URL path only), Buzz entities live on the same relay, so the card can show real data:

Entity Title source Fallback
PR subject tag of the kind 1618 event PR <id-prefix>
Issue subject tag of the kind 1621 event issue <id-prefix>
Repo name tag of the kind 30617 event d-tag
Project name tag of the kind 30621 event d-tag

Enrichment is a single relay query by event id (PR/issue) or coordinate (repo/project) through the existing relayClient, cached per event id. Kind filters must always be included in the query (relay p-gate). Cards render immediately with the fallback title and upgrade in place when the lookup resolves — same progressive pattern as useResolvedLinkPreviews.ts uses for Google titles.

Open/merged/closed status chips (from kind 16301633 status events) are a nice-to-have and explicitly deferred to a follow-up.

New module

desktop/src/shared/lib/entityLink.ts (placed in shared/lib rather than the projects feature so linkPreview.ts — also shared/lib — can import it without a feature→shared boundary violation):

  • buildRepoLink, buildPullRequestLink, buildIssueLink (buildProjectLink deferred with the project scheme)
  • parseEntityLink(url): EntityLinkParseResult (discriminated union, same shape as parseMessageLink)
  • isEntityLink(href) cheap pre-check for the markdown renderer

Detection: extend extractSupportedLinkPreviews in linkPreview.ts with a buzz:// pattern (new SupportedLinkPreviewKind members buzz-pull-request, buzz-issue, buzz-repository, buzz-project), or — if mixing schemes into the URL regex is awkward — a parallel extractor composed in markdown.tsx. Code blocks / spoiler / image-link masking rules are shared either way, and the existing MAX_PREVIEWS cap applies across both sources.

In-timeline click (implemented): navigate via useAppNavigation.goProject(). The /projects/$projectId route id is the canonical 30617:<owner>:<d> coordinate (see entityLinkProjectRouteId in shared/lib/entityLink.ts). Route resolution on the feat/multi-repository-projects branch (#4671) resolves this coordinate to the correct project and repository regardless of container grouping — #4671 must merge before #4695 to avoid unresolved routes at runtime:

  • pr / issue/projects/30617:<owner>:<d>?pullRequestId=<id> (or issueId).
  • repo/projects/30617:<owner>:<d>.

If resolution fails (entity not visible in this community), show the same kind of toast fallback used for unresolvable message links.

OS-level: register repo / project / pr / issue hosts in desktop/src-tauri/src/deep_link.rs and dispatch to a new listener hook (sibling to useMessageDeepLinks.ts). This makes links pasted outside Buzz (e.g. in a terminal or another app) open the desktop app correctly.

CLI (buzz-cli)

Add a link field to the JSON output of the write commands that create linkable entities:

  • buzz pr open{ event_id, accepted, message, link }
  • buzz issues create → same
  • buzz repos create → link built from owner pubkey + d-tag
  • buzz projects create → same

The builder lives in one Rust helper (e.g. crates/buzz-cli/src/links.rs) so the format has exactly one definition on the Rust side; the TypeScript entityLink.ts is its mirror and both are covered by shared-format tests (golden strings asserted on both sides, like the NIP-MP fixture pattern).

buzz pr get / buzz issues get / buzz repos get also include link in their output so agents can link to existing entities, not just ones they just created.

Agent guidance

One addition to crates/buzz-acp/src/base_prompt.md, next to the existing --channel rule for PR opens:

When you announce a pull request, issue, repository, or project in a channel message, include the link value from the command output as a bare URL on its own line so it renders as a preview card.

No persona changes needed — the base prompt applies to all managed agents.

Interaction with existing work

  • proto/rich-link-previews (generic OpenGraph cards): orthogonal. Entity links never hit the network beyond a relay event query; no overlap in code paths except the shared AttachmentGroup rendering slot.
  • feat/multi-repository-projects (NIP-MP): independent. Entity links reference single repositories/PRs/issues by coordinate/event id; the PR→project resolution step simply uses whatever project read models exist on main at implementation time.

Implementation plan (suggested PR slices)

  1. HTTPS clone-URL repo cards (done, this branch) — recognize relay /git/<pubkey>/<repo> URLs in linkPreview.ts, Buzz provider card with the BuzzMark logo, href normalized to the buzz://repo deep link for in-app navigation.
  2. Link core + cards (done, this branch)entityLink.ts, detection in linkPreview.ts, Buzz card variant in link-preview-attachment.tsx, in-timeline click navigation, relay title enrichment (with resetLinkPreviewTitleCache() wired into resetCommunityState()). Unit tests (entityLink.test.mjs, extended linkPreview.test.mjs).
  3. OS deep linksdeep_link.rs + listener hook + deep-link.ts parity tests.
  4. CLI + agent prompt (create commands done, this branch)links.rs helper, link output field on pr open / issues create / repos create, base prompt paragraph, cross-language golden-format test. Still open: link on the get commands.
  5. Follow-ups (separate) — status chips on PR/issue cards, mobile pill/card rendering, web PR/issue routes + HTTPS link recognition, cross-community relay= parameter.

Security considerations

  • All identifiers are validated before use (owner/id strict hex-64, d-tag charset rules). Parse failures render the raw text as a plain, non-clickable string — never an anchor with an unvalidated href.
  • Title enrichment queries go through the already-authenticated relayClient with explicit kinds filters; no new HTTP surface and no outbound fetches to third parties.
  • Card titles come from event tags authored by arbitrary users; they must be rendered as text (existing card components already do this — verify no dangerouslySetInnerHTML in the new variant).
  • Deep links arriving from the OS are untrusted input; the new listener must apply the same validation as the in-timeline parser before navigating.