Skip to content

feat(conversation): let plugin rows manage messages without visiting a channel - #2512

Open
kazemek21 wants to merge 3 commits into
block:mainfrom
kazemek21:kylez_NEXUS-4193_no-visit-conversation-scope
Open

kazemek21 wants to merge 3 commits into
block:mainfrom
kazemek21:kylez_NEXUS-4193_no-visit-conversation-scope

Conversation

@kazemek21

@kazemek21 kazemek21 commented Nov 4, 2026 •

Copy link
Copy Markdown
Contributor

TL;DR

Adds conversation.ui.Conversation, a public host-owned scope that lets a plugin's own ui.Message rows and ui.Composer for one channel get native Edit, confirmed Delete with recovery and retry, and host media review. Mounting it never marks anything read, follows a thread or enters the channel visit. Cross-channel overview cards need this; today a bare ui.Message row has neither Edit nor Delete, and ui.Thread always visits.

Fixes NEXUS-4193

How it works

ui.Conversation({ session, scope, channelId, channelName?, threadId?, children(interactions) }) renders the existing ConversationChannel in its host-dialog, non-visiting mode (host plus active={false}). Inbox Drafts already uses this mode internally. Inside it:

  • MessageManagement supplies the row menu's Edit/Delete under the existing permission checks, the delete confirmation and the failed/unconfirmed recovery notice.
  • MessageEditScope makes Edit target the ui.Composer in the same scope. A scope without a composer offers no Edit, as in other previews.
  • ConversationHost owns media review and panel overlays, focus return, and the existing stale-session guard (nothing opens once the relay snapshot is no longer this ready session).

children receives the host's onOpenLink, canOpenLink and onOpenMediaReview to pass to the rows. EmbeddedThread and the new scope now share one HostedChannel wrapper, so ui.Thread behavior is unchanged. No changes to MessageManagement, ConversationHost, MessageComposer or contracts.ts.

Evidence

src/features/conversation/EmbeddedConversation.test.tsx mounts two fictional channel cards (real relay session, real ui.Message rows and ui.Composer from ConversationService; only MediaReviewViewer is reduced, since it has its own suites). It covers:

  • the same rows without the scope have no Edit/Delete; inside it, each card offers both
  • mount, background presentation, re-present and unmount never call enterChannel, leaveChannel, follow, markMessageRead, markChannelRead or the roster's profiles.reserve, and publish nothing
  • Edit fills only that card's composer and publishes a kind-40003 edit tagged with that card's channel. The other card's message is untouched.
  • Delete: cancel publishes nothing; confirm publishes kind 5; a relay rejection shows the recovery notice in that card only; Retry resends the same event id and the row goes away
  • media review opens with the card's channel and message, and stops opening after the session stops being ready

Fail-then-pass: with active={false} removed from the scope, all 4 tests fail (enterChannel and profiles.reserve are called). The scope restored, all pass.

The packed @buzz/author consumer fixture (tests/fixtures/conversation-consumer) now renders its rows and composer inside ui.Conversation and passes interactions.onOpenLink to its rows. So the generated declarations must export the new component and its children(interactions) type, and the existing conversation.spec.mjs journey mounts it in the real app. The fixture type-checks against freshly built declarations. As a negative control, a misspelled interactions field fails tsc.

Run on a Linux Blox box on base a34ba4f (the local host-build guard refused under load):

  • vitest run src/features/conversation src/features/messages/MessageManagement.test.tsx src/bundled/inbox/DraftsView.test.tsx: 19 files, 221 tests passed
  • tsc --noEmit and biome check --error-on-warnings on the touched files: clean
  • pnpm author:build, packed and installed into a copy of the consumer fixture, then pnpm build (tsc): clean

No browser journeys were added or removed. The existing consumer journey now mounts the scope; it runs only in hosted CI, not locally. Focus, overlay and native-edit geometry are unchanged existing ConversationHost/composer behavior that existing suites already cover.

Limits and coordination

Initiative context

Buzz Threads status

🤖 Generated with Claude Code

kazemek21 and others added 3 commits November 4, 2026 14:38
…a channel

Add conversation.ui.Conversation, a host-owned scope for a page's own
ui.Message rows and ui.Composer in one channel. It reuses the existing
non-visiting ConversationChannel host mode, so rows get native edit,
confirmed delete with recovery and retry, a per-scope edit target and
host media review, while mounting never marks read, follows or holds the
channel roster.

Fixes NEXUS-4193

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Kyle Zemek <kylez@squareup.com>
… consumer

Observe the profile reservation an open channel makes, exercise
ui.Conversation and its interactions in the packed author consumer
fixture, and name the instance rather than the scope prop in the docs.

Refs NEXUS-4193

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Kyle Zemek <kylez@squareup.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Kyle Zemek <kylez@squareup.com>
@kazemek21
kazemek21 marked this pull request as ready for review November 4, 2026 23:39
@kazemek21
kazemek21 requested a review from a team as a code owner November 4, 2026 23:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant