Skip to content

feat(bestie): choose the private channels Bestie can read - #2036

Open
rileycrane wants to merge 3 commits into
rileycrane/bestie-l2-memoryfrom
rileycrane/bestie-l4-private-channels
Open

rileycrane wants to merge 3 commits into
rileycrane/bestie-l2-memoryfrom
rileycrane/bestie-l4-private-channels

Conversation

@rileycrane

@rileycrane rileycrane commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor

The Bestie tab lists the private channels you share with Bestie. From the tab you can add Bestie to one channel or to all your private channels, and remove it from any of them.

Why only private channels: agents can already read public channels, so only private channels need Bestie added.

What changes

  • Private channels Bestie can read: each channel is listed with Remove.
  • Add to a channel (Choose…): a picker of your private channels that don't have Bestie yet, most recently active first.
  • Add to all my private channels: adds Bestie to every private channel you're in that doesn't have it.
  • Adds and removes are the same NIP-29 membership events the Members dialog sends (kinds 9000/9001). After each one, the tab reads the roster again to confirm.
  • Each action has its own row, so in a narrow panel its button wraps under the label.
  • Also add Bestie to private channels I create is not built yet. It needs a small record of which channels the switch added, and is left for a follow-up.

Try it (just dev, with a local Bestie and two private channels it isn't in)

  1. Open the Bestie tab. Under Private channels Bestie can read, click Add to a channel → Choose… and pick one channel. That channel appears in the list, and its timeline shows that you added Bestie.
  2. Click Add to all. The other channel is added too.
  3. Click Remove on one channel. It leaves the list.

Stack: each PR is based on the one before it: #2034 Bestie tab → #2035 Remembers you → #2036 private channels (this PR) → #2037 check-ins → #2039 Former Bestie → #2041 Remove as Bestie → #2047 Choose your bestie. #2040 (recent notes in Claude Code prompts) is separate and based on main.

Overlap: #1716 also changes features/agents/service.ts and tabs.tsx. Whichever lands second will rebase onto the other.

🤖 Generated with Claude Code

buzz-review-completed

rileycrane and others added 3 commits October 30, 2026 14:33
The Bestie tab's private channels row becomes its own group: the private
channels you share with Bestie, each with Remove, plus Add to a channel…
and Add to all my private channels, most recently active first. Add to
all asks first, because each channel's members see the addition, then
lists each channel that didn't confirm. "Also add Bestie to private
channels I create" is shown disabled until it has a record to keep.

Adds use session.memberAdditions, and removals use member administration
with the role a fresh roster read shows, as the Members dialog does.
Public channels need nothing, since agents read them already.

The sidebar's recent order moves unchanged into compareRecent so both use
the same comparison.

Co-authored-by: eggboy <3986e5374b8c11a274f4a53fa9a68e1e18b53e712f8aa5218add2485bf5a2b09@buzz.block.builderlab.xyz>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Riley Crane <rileycrane@squareup.com>
…urrent

Add to all now adds Bestie to one channel at a time, as adding several
agents does, so a long list doesn't burst the relay. It adds exactly the
channels its dialog counted, even if the list changes while the dialog
is open. A failure stops showing once the channel is as the change
wanted. Remove is unavailable on a cached roster, which the Members
dialog doesn't administer either.

Co-authored-by: eggboy <3986e5374b8c11a274f4a53fa9a68e1e18b53e712f8aa5218add2485bf5a2b09@buzz.block.builderlab.xyz>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Riley Crane <rileycrane@squareup.com>
The private channels header held two buttons that never shrink, so at the
side panel's default 28rem the tab grew to their width and every row ran
off the panel. Add to a channel and Add to all now have rows of their own,
whose buttons wrap under their titles in a narrow panel and are described
by them.

Co-authored-by: eggboy <3986e5374b8c11a274f4a53fa9a68e1e18b53e712f8aa5218add2485bf5a2b09@buzz.block.builderlab.xyz>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Riley Crane <rileycrane@squareup.com>

@wesbillman wesbillman left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Star Lord’s automated source review via Wes’s account, integrating independent Rocket (systems/lifecycle), Mantis (correctness/security/concurrency), and Groot (product/accessibility) coverage.

Two actionable findings are inline: one P2 and one P3. The existing session services retain authorization and membership-readback ownership; the extracted sidebar comparison preserves its previous ordering.

Reviewed head f68837125121e8dfb1360086b40cc7b8fdf5d1dd against base/merge-base a5d42ffbdbd31fdc563ee7c5a245261868cf3e20. Reviewed the six-file patch, relevant callers/lifecycle, public description (no attached images), and commit messages. No concrete public-material disclosure found.

Source-only: no PR code, tests, builds, apps, or live relay operations were executed. The one hosted-check snapshot reports CI required successful; Windows validation was skipped. That does not establish the membership UI, keyboard/screen-reader behavior, narrow-layout rendering, performance, or human acceptance; those remain unverified. This is a non-blocking COMMENT review, not approval or merge authorization.

))}
{!joined.length && (
<PreferenceRow
title="Not in any of your private channels yet"

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 [P2] Distinguish an unread roster from an empty membership list

This empty state depends only on joined.length, although useChannelList also returns status, error, and coverage. On a failed initial discovery the store retains an empty list with status: "error" (features/relay/store.ts:1640–1650), so opening this tab says “Not in any of your private channels yet” even if Bestie already has access. No read error or roster retry appears in this section. Retained partial results can likewise be offered as “Add to all my private channels” without disclosing that only part of the roster is known.

Render loading/read-error state separately, provide the existing session.channels.refreshList() recovery, and gate or explicitly qualify bulk selection while coverage is partial. Preserve usable known rows without presenting missing rows as proof of no membership. This follows DESIGN.md’s rule against treating loading/error as an empty collection; cover failed discovery and partial-roster recovery in the component tests.

<SettingsGroup>
{joined.map((channel) => (
<PreferenceRow
key={channel.id}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 [P3] Preserve keyboard focus when the completed action removes its control

A keyboard-activated Remove stays focusable while loading, but the confirmed roster update removes this entire row from joined. Similarly, adding the last eligible channel makes addable.length zero and unmounts both Choose… and Add to all (lines 164–210), including whichever trigger received focus after the menu/confirmation closed. There is no completion handoff to a surviving control, so the user loses their focus position in this section. Shared popup return-focus behavior cannot retain a trigger that is subsequently removed. This conflicts with DESIGN.md’s focus-restoration guidance.

When an action removes the focused control, hand focus to the next suitable row or a stable section target, without moving it if the user has already navigated elsewhere. Cover confirmed removal and last-channel addition with a roster-updating fixture; the current static-list tests never unmount these controls. This is a source finding; browser focus behavior was not exercised.

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.

2 participants