mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
## Summary Projects v3 makes repository work shareable, discussion-aware, and easier to scan in one coherent workspace. People can copy canonical links, reopen the exact workspace tab, understand issue and pull-request context at a glance, find related channel conversations, and assign or unassign issues across Desktop and CLI. - **Unified workspace** — top-level sections sit above repository controls in one rounded workspace, with navigation positioned close to the page heading. README and Files retain branch selection; every section has a labeled icon header, and Issues and Pull Requests expose creation from a consistent right-aligned action. - **Repository management** — the repository selector is always available, including single-repository projects. Its integrated add flow lets project owners create a repository manually or select an existing repository without a separate toolbar button. - **Readable work-item lists** — issue and pull-request rows use plain-language context instead of opaque metadata. Files, commits, issues, pull requests, channels, and contributors share consistent row density and right-aligned timestamps, while deterministic fallback-avatar colors keep participants distinct on light backgrounds. Inbox pull-request metadata wraps between complete phrases and truncates long channel names instead of compressing copy into narrow columns. - **Reliable entity links** — projects, repositories, issues, pull requests, and commits have canonical `buzz://` links, preview cards, OS deep-link routing, and tab-aware navigation. Reopening the same link re-applies its destination instead of leaving the user on a locally selected tab. - **Related conversations** — repository and work-item views surface channels discussing the current entity, including participants, channel navigation, message context, and an explicit notice when discovery reaches its 500-result cap. - **Reversible issue ownership** — trusted assignment and unassignment events work across Desktop, Tauri, `buzz-sdk`, and `buzz issues`. Assignees appear in project views and the assigned inbox, while authorized users can remove assignments directly from the assignee row. Assignment state is derived chronologically from labeled Nostr notes. Issue authors and repository owners may change any assignee; other users may only assign or unassign themselves. Shared golden fixtures keep entity-link grammar and validation aligned across TypeScript and Rust. The branch also updates `webbrowser` to the patched release for RUSTSEC-2026-0257. ### Related issue N/A. ### Testing - [x] `just ci` — formatting, lint, typechecking, unit tests, and builds passed - [x] Full pre-push suite — organization, branch-skew, Desktop checks, typechecking, and tests passed on the latest push - [x] `cargo test -p buzz-cli` and focused `buzz-sdk` assignment tests passed - [x] Focused Tauri recipient-note and 500-result search-limit tests passed - [x] Desktop entity-link and issue-assignment unit tests passed - [x] Playwright smoke coverage passed for assignment, repeated entity-link navigation, repository create/select flows, section headers and actions, timestamp alignment, timeline icons, sentence-style issue/PR metadata, header spacing, avatar contrast, and Inbox metadata at stacked and side-rail breakpoints - [ ] Manual staging pass: link round-trips, Channels tab, assignment flows, and inbox routing ### Screenshots Pull requests explain who opened the request, where it lives, and which branch it comes from; fallback avatars remain visually distinct.  Issues use the same sentence-style hierarchy while keeping status and recency easy to scan.  The wide Inbox detail keeps author, timestamp, and origin context readable beside its metadata rail.  [View the complete six-state Projects v3 screenshot set](https://github.com/block/buzz/pull/5624#issuecomment-5268039672) and [the compact/wide Inbox comparison](https://github.com/block/buzz/pull/5624#issuecomment-5268614585). --- > Supersedes #5624, whose head commit accumulated permanently-queued required check suites (block-dco-check et al.) that GitHub never dispatched. History flattened into a single signed-off commit on latest main; tree verified byte-identical (`git merge-tree`) to merging the original branch into main. --------- Signed-off-by: Thomas Petersen <thomasp@squareup.com> Co-authored-by: Wintermute <3f1797424fd9ad6653a83665c660517777cd7f8c228c0d5907f49e01537f3ca5@buzz.block.builderlab.xyz>
289 lines
14 KiB
Markdown
289 lines
14 KiB
Markdown
# 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|project` deep links: `entityLink.ts`
|
||
builders/parser, preview cards with relay title enrichment (repo and
|
||
project titles resolve from their announcement events), in-timeline click
|
||
navigation to `/projects/$projectId`.
|
||
- Slice 2 — OS-level deep links: the `repo`/`project`/`pr`/`issue` hosts in
|
||
`desktop/src-tauri/src/deep_link.rs` emit `deep-link-entity`, and
|
||
`useEntityDeepLinks` routes them through the same handler as in-timeline
|
||
clicks.
|
||
- Slice 3 (create-command part) — `crates/buzz-cli/src/links.rs`, `link`
|
||
output field on `pr open` / `issues create` / `repos create` /
|
||
`projects create`, base prompt guidance, cross-language golden-format tests.
|
||
- Sharing from the UI — `lib/projectShareLinks.ts` maps the Projects read
|
||
models onto links, surfaced as "Copy link" in the project, repository,
|
||
issue, and pull request row menus and as a copy button in the project,
|
||
issue, and pull request detail headers.
|
||
|
||
Still unimplemented: `link` on get commands 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.
|
||
|
||
## Link format
|
||
|
||
Extend the existing `buzz://` scheme, mirroring `buzz://message`:
|
||
|
||
```
|
||
buzz://repo?owner=<pubkey-hex>&d=<repo-dtag>[&tab=<tab>]
|
||
buzz://project?owner=<pubkey-hex>&d=<project-dtag>[&tab=<tab>]
|
||
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.
|
||
- `tab` (coordinate links only, optional) selects a workspace tab instead
|
||
of the default readme overview: `files`, `commits`, `issues`, `prs`,
|
||
`contributors`, or `channels`. The overview has no spelling (canonical links omit the
|
||
parameter), unknown values are rejected, and event links accept no `tab`.
|
||
The desktop's copy-link button emits it automatically when a non-overview
|
||
tab is active, so "link to the PR list" is just the project link copied
|
||
from the Pull Request tab.
|
||
- 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 `1630`–`1633` 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`, `buildProjectLink`, `buildPullRequestLink`,
|
||
`buildIssueLink`
|
||
- `parseEntityLink(url): EntityLinkParseResult` (discriminated union, same
|
||
shape as `parseMessageLink`)
|
||
- `isEntityLink(href)` cheap pre-check for the markdown renderer
|
||
- `isLinkableCoordinate(owner, dtag)` — addressable d-tags allow a wider
|
||
charset (and 1024 bytes) than the link format's
|
||
`[a-zA-Z0-9._-]{1,64}`, so callers that build links from read models check
|
||
first and hide the share affordance instead of surfacing a builder throw
|
||
|
||
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.
|
||
|
||
## Click handling and OS deep links
|
||
|
||
**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** *(implemented)*: the `repo` / `project` / `pr` / `issue` hosts
|
||
in `desktop/src-tauri/src/deep_link.rs` validate the link's canonical form
|
||
(so a malformed link does not raise and focus the window for a navigation
|
||
that would then be declined), then emit `deep-link-entity` with the URL
|
||
verbatim. `useEntityDeepLinks` — sibling to `useMessageDeepLinks.ts`, mounted
|
||
in `AppShell` for the main window only — re-parses it with `parseEntityLink`
|
||
and reuses `useOpenEntityLink`, so a link opened from the OS lands on the
|
||
same view as one clicked in a message.
|
||
|
||
## 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)
|
||
|
||
0. **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.
|
||
1. **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`).
|
||
2. **OS deep links** *(done, this branch)* — `deep_link.rs` +
|
||
`useEntityDeepLinks` + `deep-link.ts` parity tests.
|
||
3. **CLI + agent prompt** *(create commands done, this branch)* — `links.rs`
|
||
helper, `link` output field on `pr open` / `issues create` /
|
||
`repos create` / `projects create`, base prompt paragraph, cross-language
|
||
golden-format test. Still open: `link` on the get commands.
|
||
4. **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.
|