You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
feat: allow creating and editing thread titles #6739
Update (Oct 2026): NIP-AR channel artifacts (#7791 spec, #7919 relay) cover most of this, so the proposal below is superseded. Thread titles are now built as a buzz.thread-title artifact. Current design and screenshots: #6739 (comment). Agent auto-titling idea: #6739 (comment)
Original proposal (Aug 2026): dedicated kind 30180, kept for context
Motivation
A thread in Buzz has no name anyone can set, so every surface that wants to title one invents something.
The Home feed titles rows by kind.feedHeadline() (desktop/src/features/home/ui/FeedSection.tsx:61) is a pure kind→string switch rendered as the row headline at :196 — "Forum post", "Forum reply", "Mention", "Channel update". Ten conversations render as ten rows reading "Mention" / "Channel update".
The Inbox has no headline at all — sender, a type label from getInboxTypeLabel(), then the raw message preview (InboxListPane.tsx:306, :443). Nothing says what the conversation is about.
Someone already started this and it never landed. A second feedHeadline() at desktop/src/features/home/lib/inbox.ts:137 does the right thing — subject tag → first line → fallback — and assigns it to InboxItem.subject (declared :59, computed :591, set :621). Nothing renders it. Those four lines are every subject hit under features/home.
Prior art: NIP-14 defines a subject tag for threaded events, and Buzz uses subject on issues and PRs (crates/buzz-sdk/src/builders.rs:1116, :1501). What's missing is an editable, shared, searchable subject for kind:9 / 45001 threads. NIP-14's lives on the root, so it's write-once — build_edit (builders.rs:389) carries only h, e and content, and nothing in Buzz mutates a tag on a stored event — and tags aren't full-text indexed.
Proposed solution
A stored, user-signed, channel-scoped parameterized-replaceable kind whose content is the subject text.
Kind in 30000–39999 so the NIP-33 replace path applies (30180 looks free; kind.rs is authoritative).
d = the thread root event id — the replacement key, so relabeling is just publishing again and old threads behave like new ones. e = the same root id, h = the channel. replace_parameterized_event(community, &event, &d_tag, channel_id) (crates/buzz-relay/src/handlers/ingest.rs:3153) already takes a channel_id, so channel-scoped 30xxx is supported — existing 30xxx kinds just haven't used it that way.
Subject in content, not a tag. The FTS generated column indexes content only (migrations/0008_fresh_install_search_allowlist.sql:15). A tag would be permanently unsearchable — which is why issue and PR subjects aren't full-text searchable today.
Unlabeled stays first-class: no prompt, no empty-state nag, no required field. Plenty of channels need no labels at all.
This needs a dedicated ingest validator — the generic NIP-33 path enforces none of it. Invariants: exact tag cardinality and d == e, both valid 64-hex; target event exists, is top-level, and belongs to the channel in h; content non-empty after trimming and ≤256 chars; plus an explicit authorization rule. Note that check_channel_membership (handlers/ingest.rs:742) returns Ok(()) for non-members in open channels, while the closest precedent — the kind:9002 channel topic — calls is_member_cached() and rejects them outright (handlers/side_effects.rs:623-633).
Decisions I'd like from a maintainer before writing code:
Precedence — NIP-33 keeps one event per (pubkey, kind, d), so distinct authors survive side by side and clients need a deterministic pick. "Root author wins" breaks the case this feature is partly for: an agent that authors both root and subject is the root author, so its owning human could never override it. Client-side rule, or one relay-selected canonical value with an explicit writer list?
Clearing — non-empty content plus replace-only means there's no way back to unlabeled. NIP-09 delete of the coordinate, or a defined tombstone?
Interop — also write a NIP-14 subject tag on the root at creation? I'd default no: write-once, unsearchable in Buzz, and permanently divergent once the real subject is edited. Only worth it if a specific non-Buzz client is known to read subject on kinds 9/45001.
Alternatives considered
subject tag on the root (the NIP-14 shape). Write-once, per above — can't label an existing thread, can't correct a bad label, can't be set by anyone but the author, and unsearchable.
Extend the 39005 thread summary. Relay-only, synthesized at query time, never stored — and absent entirely for zero-reply threads (api/bridge.rs:539-543), which is exactly the state a freshly created, freshly labeled thread is in.
Client-local titles.feat(desktop): add persistent pinned Thread Rail #6350 (draft, pinned Thread Rail) has per-identity local titles — fine for one person's rail, but not shared, not signed, invisible to other members and to agents.
Additional context
Known required work — this is not a one-file change, which is why I'd rather agree the shape here than open a speculative PR:
area
work
buzz-core
kind constant, ALL_KINDS
relay ingest
the dedicated validator above
relay bridge
add the kind to WINDOW_AUX_KINDS (api/bridge.rs:384-389) — it's a closed 4-kind list, so an e tag alone gets a new kind nothing and a subject would store fine then vanish on reload
migrations
new additive migration for the FTS allowlist (0007/0008 are applied on running relays and their sqlx checksums are immutable — crates/buzz-db/src/migration.rs:772-773), plus scripts/maintenance/nip_rs_search_allowlist.sql for populated installs
desktop
aux parsing, render, edit affordance, search kinds (src-tauri/src/commands/messages.rs:178 hard-codes [9, 40002, 45001, 45003]), and result→root mapping so a hit opens the thread rather than the metadata event
mobile
aux parsing, render, search kinds (mobile/lib/features/search/search_provider.dart:139)
docs
docs/nips/NIP-**.md, kind.rs
I have the full tracing behind the migration and search rows available if it's useful.
Related:#6067 (working set of labelled items — a shared signed subject would be an input, not a competitor); #4266 and draft PR #6350 (thread rail); #5254 (search/filter threads in a forum channel).
Searched open issues and PRs for topic, subject, rename, and thread title before filing; the only topic hits are channel-level (set_channel_topic). Point me at a duplicate and I'll close this.
Happy to implement once the four questions are settled.
Update (Oct 2026): NIP-AR channel artifacts (#7791 spec, #7919 relay) cover most of this, so the proposal below is superseded. Thread titles are now built as a
buzz.thread-titleartifact. Current design and screenshots: #6739 (comment). Agent auto-titling idea: #6739 (comment)Original proposal (Aug 2026): dedicated kind 30180, kept for context
Motivation
A thread in Buzz has no name anyone can set, so every surface that wants to title one invents something.
feedHeadline()(desktop/src/features/home/ui/FeedSection.tsx:61) is a pure kind→string switch rendered as the row headline at:196—"Forum post","Forum reply","Mention","Channel update". Ten conversations render as ten rows reading "Mention" / "Channel update".getInboxTypeLabel(), then the raw message preview (InboxListPane.tsx:306,:443). Nothing says what the conversation is about.feedHeadline()atdesktop/src/features/home/lib/inbox.ts:137does the right thing —subjecttag → first line → fallback — and assigns it toInboxItem.subject(declared:59, computed:591, set:621). Nothing renders it. Those four lines are everysubjecthit underfeatures/home.Prior art: NIP-14 defines a
subjecttag for threaded events, and Buzz usessubjecton issues and PRs (crates/buzz-sdk/src/builders.rs:1116,:1501). What's missing is an editable, shared, searchable subject for kind:9 / 45001 threads. NIP-14's lives on the root, so it's write-once —build_edit(builders.rs:389) carries onlyh,eand content, and nothing in Buzz mutates a tag on a stored event — and tags aren't full-text indexed.Proposed solution
A stored, user-signed, channel-scoped parameterized-replaceable kind whose
contentis the subject text.30180looks free;kind.rsis authoritative).d= the thread root event id — the replacement key, so relabeling is just publishing again and old threads behave like new ones.e= the same root id,h= the channel.replace_parameterized_event(community, &event, &d_tag, channel_id)(crates/buzz-relay/src/handlers/ingest.rs:3153) already takes achannel_id, so channel-scoped 30xxx is supported — existing 30xxx kinds just haven't used it that way.content, not a tag. The FTS generated column indexescontentonly (migrations/0008_fresh_install_search_allowlist.sql:15). A tag would be permanently unsearchable — which is why issue and PR subjects aren't full-text searchable today.Unlabeled stays first-class: no prompt, no empty-state nag, no required field. Plenty of channels need no labels at all.
This needs a dedicated ingest validator — the generic NIP-33 path enforces none of it. Invariants: exact tag cardinality and
d == e, both valid 64-hex; target event exists, is top-level, and belongs to the channel inh;contentnon-empty after trimming and ≤256 chars; plus an explicit authorization rule. Note thatcheck_channel_membership(handlers/ingest.rs:742) returnsOk(())for non-members in open channels, while the closest precedent — the kind:9002 channel topic — callsis_member_cached()and rejects them outright (handlers/side_effects.rs:623-633).Decisions I'd like from a maintainer before writing code:
is_member_cached), root author only, or channel owner/admin?(pubkey, kind, d), so distinct authors survive side by side and clients need a deterministic pick. "Root author wins" breaks the case this feature is partly for: an agent that authors both root and subject is the root author, so its owning human could never override it. Client-side rule, or one relay-selected canonical value with an explicit writer list?subjecttag on the root at creation? I'd default no: write-once, unsearchable in Buzz, and permanently divergent once the real subject is edited. Only worth it if a specific non-Buzz client is known to readsubjecton kinds 9/45001.Alternatives considered
subjecttag on the root (the NIP-14 shape). Write-once, per above — can't label an existing thread, can't correct a bad label, can't be set by anyone but the author, and unsearchable.api/bridge.rs:539-543), which is exactly the state a freshly created, freshly labeled thread is in.Additional context
Known required work — this is not a one-file change, which is why I'd rather agree the shape here than open a speculative PR:
buzz-coreALL_KINDSWINDOW_AUX_KINDS(api/bridge.rs:384-389) — it's a closed 4-kind list, so anetag alone gets a new kind nothing and a subject would store fine then vanish on reloadcrates/buzz-db/src/migration.rs:772-773), plusscripts/maintenance/nip_rs_search_allowlist.sqlfor populated installssrc-tauri/src/commands/messages.rs:178hard-codes[9, 40002, 45001, 45003]), and result→root mapping so a hit opens the thread rather than the metadata eventmobile/lib/features/search/search_provider.dart:139)docs/nips/NIP-**.md,kind.rsI have the full tracing behind the migration and search rows available if it's useful.
Related: #6067 (working set of labelled items — a shared signed subject would be an input, not a competitor); #4266 and draft PR #6350 (thread rail); #5254 (search/filter threads in a forum channel).
Searched open issues and PRs for
topic,subject,rename, andthread titlebefore filing; the onlytopichits are channel-level (set_channel_topic). Point me at a duplicate and I'll close this.Happy to implement once the four questions are settled.