Skip to content

feat: allow creating and editing thread titles #6739

Open

Description

@jhedlund

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:

  1. Authorization — mirror kind:9002 (any member, is_member_cached), root author only, or channel owner/admin?
  2. 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?
  3. 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?
  4. 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.

Activity

  1. jhedlund commented on Aug 25, 2026

    @jhedlund
    ContributorAuthor

    One motivating case I left implicit, in regards to question 2 above: the subject doesn't have to be typed by a human.

    The familiar shape from AI chat UIs maps onto this primitive with no protocol change, the AI names the conversation from it's first exchange.

    An auto-labeling agent and the human correcting it are different authors and both events survive.

  2. jhedlund commented on Oct 2, 2026

    @jhedlund
    ContributorAuthor

    Update: since I opened this, NIP-AR channel artifacts landed (spec in #7791, relay in #7919), and they cover most of what this issue was asking for. So rather than a new kind, I built thread titles on top of NIP-AR as a client-only convention. I've been running it locally against my own relay. @baxen, since this builds directly on your artifact work, I'd like your read on whether this works alongside the intended use.

    What it boils down to

    A thread title is a kind 45010 artifact:

    Tag Value
    type buzz.thread-title (I can move it outside the buzz reserved namespace prior to PR, just let me know)
    d UUIDv5 of the thread root event id (fixed namespace), so there is exactly one title per thread and a second concurrent create is rejected
    h the channel
    root the thread root event
    title the title, also copied into content so full-text search finds it

    Renaming is op=update with prev set to the current head, and clearing is op=delete (then restore to set it again). No relay changes were needed.

    What this replaces in my original proposal

    • New kind 30180: not needed. Kind 45010 with a typed artifact covers it.
    • Authorization: settled by NIP-AR. Editing a title requires the same permission as posting a kind:9 in the channel, which includes agents.
    • Precedence between two editors: settled by the prev chain. A stale edit gets conflict: artifact head changed instead of silently winning. The desktop surfaces that as "someone else changed this thread's title".
    • Clearing a title: op=delete / restore.
    • NIP-14 subject tag: I don't think it's needed anymore, since the artifact is the single source of truth.
    • Searchability: still the same trap as issue subjects (search indexes content, not tags), which is why the title is duplicated into content.

    What it looks like

    These are preview screenshots from my local build (mocked data, not a real channel):

    In the channel: the title sits on its own line under the author of the thread's first message. Clicking it opens the thread.

    Image

    In the thread panel: the title replaces "Thread" in the header and can be edited inline (pencil on hover; Enter saves, Escape cancels, empty clears).

    Image

    In the Inbox: mentions and activity from a named thread show the title under "Mentioned in #channel", and the detail header reads "Title · #channel".

    Image

    A new Threads view next to Inbox in the sidebar: every named thread you can read, across channels, sorted by most recent activity, with the same unread dot and "N new" counts that channels use.

    Image

    There's also a CLI side, buzz threads title set|clear|get|list, so agents can name the threads they pick up work in.

    Next step

    If this direction is acceptable, I have the branch ready to open as a PR (SDK convention + CLI + desktop, with tests). I'm also happy to adjust the type name or any part of the convention to match how you plan to define the first buzz.* artifact contracts.

  3. jhedlund commented on Oct 6, 2026

    @jhedlund
    ContributorAuthor

    One thing I have not done yet is automated title creation by the agent. I am thinking that if you start a thread by tagging an agent, then the agent could automatically name the thread (just like in other desktop AI chats like Berd/ChatGPT/etc) but if just tagged later in the thread it doesn't automatically set the title even if it's empty. That could also be an optional setting whether or not to let agents name threads when tagged at top of the thread.

  4. changed the title [-]Threads have no editable, shared subject, feed rows are titled by kind, Inbox rows have no headline[/-] [+]feat: allow creating and editing thread titles[/+] on Oct 6, 2026
  5. jhedlund commented on Oct 8, 2026

    @jhedlund
    ContributorAuthor

    I added auto-thread title creation by agent if an agent is mentioned at the beginning of a thread.

    Also added ability to pin threads in the Threads view just like other apps/websites chats:

    Image Image Image
  6. jhedlund commented on Oct 10, 2026

    @jhedlund
    ContributorAuthor

    Added filter for channels to make it easier to navigate:

    Image

    Also honors the channel open thread wide preference in the Threads view

  7. Giszmo commented on Oct 15, 2026

    @Giszmo

    Looking forward to this feature landing. Please go ahead and open a PR from your feat/thread-titles branch!

    My Gemini 4 clanker also said:

    A few notes from looking through the branch that should help it land cleanly:

    1. Scope vs. feat(read-state): followed threads, follow intents and a threads view #8264: As you offered on feat(read-state): followed threads, follow intents and a threads view #8264, keeping the standalone ThreadsScreen out of the initial PR and shipping the buzz.thread-title NIP-AR convention, buzz threads title CLI, auto-titling, and inline thread-header / channel / Inbox UI avoids colliding with GET /buzz/v1/me/threads (and drops the client-side #e reply-activity scan in list_thread_titles).
    2. Scoping Desktop title queries: Right now useThreadTitle / useThreadTitleForRoot read from a single global list_thread_titles (LIST_LIMIT = 1000) that refetches whenever any kind:45010 event arrives in any channel. With agent auto-titling enabled, active communities will outgrow 1,000 global titles quickly. Querying titles per channel (#h) or by #d for visible roots will keep timelines accurate and lightweight.
    3. Full-text search: Duplicating the title into content is only half the path — fresh installs (migrations/0008_fresh_install_search_allowlist.sql and scripts/maintenance/nip_rs_search_allowlist.sql) only populate search_tsv for kind IN (0, 9, 40002, 45001, 45003) (so 45010 is NULL::tsvector), and Desktop's build_search_messages_filter (desktop/src-tauri/src/commands/messages.rs:178) still hardcodes kinds: [9, 40002, 45001, 45003] without mapping 45010 hits back to their root.
  8. jhedlund commented on Oct 23, 2026

    @jhedlund
    ContributorAuthor

    Thanks @Giszmo, and thanks for passing the review along. I checked all three points against the branch, and all three were right. Here's where each one ended up:

    1. Scope vs. feat(read-state): followed threads, follow intents and a threads view #8264: Agreed and done. feat/thread-titles is now a single commit with the buzz.thread-title convention, the buzz threads title CLI, agent auto-titling, and the thread header / channel / Inbox UI. The Threads screen moved to its own branch, feat/threads-view, stacked on top. list_thread_titles and its #e reply-activity scan exist only on that branch now.
    2. Scoped title queries: Fixed. The desktop now looks up titles per root by their derived d, up to 100 roots per query. Lookups made in the same tick share one query, so a timeline's worth of roots costs one request. The titles branch has no global list and no 1,000-title ceiling. A returned artifact only counts when its d matches the root it was fetched for. Live refresh listens only to artifact events in the open channel.
    3. Full-text search: Correct, and I shouldn't have claimed it. Copying the title into content doesn't make it searchable on its own. Desktop search hardcodes [9, 40002, 45001, 45003], and relays that use the FTS allowlist (fresh installs, or after nip_rs_search_allowlist.sql) leave search_tsv NULL for 45010. I dropped the claim instead of widening search in this PR. Covering 45010 means changing the allowlist, which rewrites a generated column on populated relays, and that's a maintainer call. The title stays in content, so it's ready if search ever covers 45010.

    PRs are up now; details in the next comment.

  9. jhedlund commented on Oct 23, 2026

    @jhedlund
    ContributorAuthor

    I've opened two PRs for this:

    1. #8563 feat: shared thread titles on NIP-AR artifacts, the one I'm proposing to merge.

    • The buzz.thread-title convention lives in buzz_sdk::thread_title, so the CLI and desktop write identical artifacts.
    • CLI: buzz threads title set|clear|get|list. set --if-unset leaves an existing or cleared title alone.
    • Agent auto-titling: when a human starts a thread by mentioning an agent, the first-mentioned agent names it. Replies, edits, DMs and agent-started threads never trigger it, and later mentions never rename a thread.
    • Desktop: an editable title in the thread header, a title line on thread roots in the channel, and titles on Inbox rows and the Inbox detail header. Concurrent renames are reported, not overwritten.
    • No relay changes. The title isn't searchable yet (see the comment above).

    2. #8565 feat(desktop): Threads view of named threads, a draft stacked on the first.

    This is the Threads screen I've been using daily. It lists every titled thread you can read, across channels, sorted by latest activity. It also has pins (synced per user as a self-encrypted kind 30078), a channel filter, a conversation pane with the channel panel's features (unread divider, Mark read, typing and agent activity, ArrowUp edit), and a live agent-working timer on rows and on the sidebar Threads entry.

    It overlaps the threads view @baxen is building in #8264 on the relay follow model, so I'm not asking for it to be merged as is. It's there for anyone who wants to try it, and in case pieces help the #8264 desktop side. The pins, the working badge, and title lookups for /me/threads rows (each row already carries the channel_id + root_id a title is keyed by) are the parts most likely to carry over.

    Until #8563 merges, #8565's diff includes both commits. The Threads-view part on its own is jhedlund/buzz@feat/thread-titles...feat/threads-view.

    Some newer additions to the Threads view since the last screenshots:

    Agents shown working in threads in left panel and in Threads view:

    Image

    Threads pane shows New messages:

    Image

    Thread pane focused view (title is a clickable link to bring to the thread in the channel):

    Image
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions