Skip to content

feat(plugins): expose the host sharing picker - #2761

Open
tellaho wants to merge 11 commits into
mainfrom
tho/plugin-member-picker
Open

tellaho wants to merge 11 commits into
mainfrom
tho/plugin-member-picker

Conversation

@tellaho

@tellaho tellaho commented Nov 6, 2026 •

Copy link
Copy Markdown
Contributor

Configurable sharing search labels

Built installed example using production source from 4e0e79b4d, unchanged at 6694b09f2. The picker defaults to Not yet added for plugin-managed membership; nonMemberLabel can override it with Not in this record. Both the visible heading and accessible region name use the same value. Owner authority, editable Public access and Add actions are shown with disposable profiles and records. Native plugin inventory and relay data are modeled; the host components and browser broker are real.

Theme Default Plugin override
Dark Dark sharing search with the default Not yet added heading Dark sharing search with the configured Not in this record heading
Light Light sharing search with the default Not yet added heading Light sharing search with the configured Not in this record heading

The following unchanged sharing states were captured at eb9d05268, after the main sync: the roster after a successful addition, editable Public access, and the confirmation before making a record Private. Cancel preserves Public. The plugin-owned membership callback writes modeled record storage, then the real browser broker reads the refreshed relay-signed roster.

State Dark Light
Member added; editable Public access Dark sharing roster after adding a member Light sharing roster after adding a member
Access confirmation Dark confirmation before making a record Private Light confirmation before making a record Private

Overview

Category: improvement
User Impact: Installed plugins can use Buzz’s sharing picker with clear warnings before access changes.

Problem: Marketplace records need the app’s member picker and signed General access controls without copying app source. Reducing access must explain both an unavailable return to Public and the access existing members retain.

Solution: Expose conversation.ui.MembersDialog and GeneralAccess with supported plugin actions, access policy/settings and share link. Plugins can configure nonMemberLabel; plugin-managed results default to “Not yet added,” while channel and chat defaults stay unchanged. Reuse host authority checks, pending/error/focus behavior and the existing access confirmation; plugins keep their records and stricter write policy.

Changes

File changes

docs/buzz-help.md
Explain the host sharing picker, unavailable return to Public and retained-member access; preserve newer main’s reaction wording.

docs/channels.md
Use the plain Public label while retaining access policy behavior.

docs/plugin-authoring-guide.md
Document injected sharing components, supported public props, callback ownership, configurable non-member result headings and signed-role disclosure.

docs/solo/sharing.md
Keep Solo’s Public compatibility and irreversible-change explanations current.

src/bundled/channels/ChannelGeneralAccess.test.tsx
Cover generic policy, fresh-read restrictions, confirmation/cancellation and retained-access warnings.

src/bundled/channels/ChannelGeneralAccess.tsx
Reuse signed access editing with plugin wording/policy and complete access-change disclosures.

src/bundled/channels/ChannelMembersDialog.solo.test.tsx
Retain Solo behavior with the plain Public label.

src/bundled/channels/ChannelMembersDialog.tsx
Allow stricter plugin member actions, custom access settings, share links and a configurable non-member result heading while keeping host authority and pending guards.

src/bundled/channels/MemberAdministration.test.tsx
Exercise add/change/remove callbacks, plain-member Add suppression, denial, abort/pending protection, failures, policy revocation and default/custom/blank result labels.

src/features/conversation/service.test.tsx
Verify that installed plugins receive the existing host sharing components.

src/features/conversation/service.tsx
Expose the host picker and General access through the existing injected UI owner.

src/plugins/author.test.mjs
Compile independent sharing consumers, including nonMemberLabel, and reject host-only navigation props.

src/plugins/author.ts
Export supported sharing props, policy and member-change contracts as types only.

tests/browser/conversation.spec.mjs
Extend the packed installed-consumer journey through picker opening, closing and focus return.

tests/browser/solo-public-access.spec.mjs
Retain the existing Solo journey and update Public label assertions.

tests/fixtures/conversation-consumer/plugin.ts
Render the host picker from an independently packed module with plugin callbacks and share link.

tests/fixtures/conversation.tsx
Load only the required Channels and host conversation features for the boundary proof; preserve assertions, timeouts and retries.

Reproduction steps

  1. Install a plugin using the authoring guide’s sharing example and open its sharing dialog. The host picker shows current people, General access and the plugin’s share link.
  2. Start with Public and disallow selecting Public. Its label stays Public; choosing Private confirms that existing members retain access and that Public cannot be restored here. Cancel preserves Public and returns focus to General access.
  3. Change a member through the plugin callback. Pending work locks mutations and dismissal, failures remain retryable, and revoked policy is checked again before submission. As a plain member, a plugin with canAdd enabled shows member search without an Add action.
  4. Search for someone outside the shared record. Plugin-managed results default to “Not yet added”; set nonMemberLabel="Not in this record" to customize both the visible heading and accessible region name. An empty or whitespace-only label falls back to the default.
  5. Close the dialog and verify focus returns to its trigger. Exercise normal channel and Solo sharing as the default baseline.

Validation and limits

Current head 6694b09f2 includes the optional label prop from 4e0e79b4d on top of eb9d05268, which merges main 7f42b1331. The main-sync integration retains list-position/focus restoration and retires the scroll guard when a plugin callback settles. Plugin permission gates, callback routing and right-edge layout are preserved.

TypeScript passes after removing an unsupported test-query option in 6694b09f2; string role-name matching remains exact by default. The label change passes all 255 checks across the three member-dialog files, the generated author/type consumer, and the installed-plugin Chromium capture journey covering default/custom headings in both themes. The three label cases verify the visible heading, accessible region name and unchanged Add dispatch. No permanent browser cases were added or removed. The temporary capture exercises real host React injection and the browser broker with disposable data and modeled native inventory/relay.

The preceding main-sync validation passed 252 member-dialog checks and eight Chromium/WebKit browser checks at eb9d05268. New-head CI is pending; packaged-native, live-relay and human acceptance remain pending. The label update has not been rerun in WebKit. All mandatory push hooks pass at 6694b09f2, including TypeScript/related unit tests and design checks.

@tellaho tellaho changed the title feat(plugins): reuse the host member picker in installed plugins feat(plugins): expose shared UI to installed plugins Nov 6, 2026
@tellaho
tellaho force-pushed the tho/plugin-member-picker branch from 9a97db5 to 92fb222 Compare November 7, 2026 03:24
@tellaho

tellaho commented Nov 7, 2026

Copy link
Copy Markdown
Contributor Author

🤖 Review round 1: Princess Donut, reviewing exact head 92fb2224f (rebased onto fa5bf08f7). Posted from Taylor's account on Taylor's behalf.

Verdict: 8/10 with one blocker. The plugin contract is well designed. Plugins get the host's editor, Markdown, workspace, member picker and General access as injected runtime values. The author package exports types only, and the host keeps URL safety, role checks and signed save/readback. The Public restriction is enforced in three places: when a choice is made, again after the fresh signed read, and on submit. The rebase conflict in the conversation UI surface is resolved correctly: main's new embedded Conversation entry sits alongside the four new surfaces, with nothing lost or duplicated. Hosted CI for this head was still pending when I looked.

Blocker

1. With a plugin access policy, making something Private no longer says that people who already joined keep their access. (Severity: blocker, privacy disclosure)
For Solo chats, the confirmation for leaving Public or View only explains that people who already joined remain members and keep reading until they are removed. That warning is only shown for Solo chats. A plugin that passes an access policy, which is the Shared notes and Pages case, sees only its own visibilityDescription("private") text plus "You won't be able to switch back to Public here." The mayHaveGuestMembers input is part of the exported plugin props, but it has no effect outside Solo chats.

How it fails: An owner moves a shared record from View only or Public to Private. The confirmation reads "Only invited people can read this record.", which is the wording in the authoring guide example and the unit test fixture. Everyone who joined while it was open keeps reading new revisions, and nothing told the owner. The plugin cannot write a correct warning itself, because visibilityDescription only receives the destination visibility, not the starting state or whether guests may be present. The new test that covers leaving Public for Private under a plugin policy asserts the policy text and the can't-switch-back text, but not the retention warning that its Solo counterpart asserts.

Fix: Show the existing retained-access warning whenever a policy is supplied, not only for Solo chats, using the same mayHaveGuestMembers rule. Add the policy case to that test. Also fix the guide example so it doesn't promise "Only invited people".

Optional

  • The exported member-picker props expose host internals (medium, optional). ChannelMembersDialogProps is the plugin-facing type, and it also includes navigator, visit, signal, agents, communities and the host's member-navigation callbacks. Those turn internal host types into plugin contract. visit: true also opts into host navigation-visit behavior, while the guide says the component registers no destination. Consider exporting a narrower plugin type covering session, channel, trigger, close, title and description, member actions, access settings and share link, so later host refactors don't break Marketplace plugins.
  • Adding through memberActions gives no success announcement (low, optional). The host route announces "X is in the channel." for screen readers. The plugin route only clears the search. Consider a neutral announcement after the callback resolves.
  • Add can look available when the plugin route will refuse it (low, optional). canAdd follows canAddMembers, but the plugin route then requires management authority through authorizeMemberChange. A viewer the host lets add members, but who isn't an owner or admin, gets the "Refresh members and roles" error instead of a hidden or disabled add. Plugins can work around it with canAdd, but gating on the same authority the route enforces would be more honest.
  • Split names aren't namespaced (low, optional). panels.ui.useSplit stores width under whatever name the plugin passes. A plugin that ignores the "prefix with your plugin ID" guidance can overwrite a host layout's saved width. Consider prefixing names on the host side.
  • Small things (nit, optional): PluginMarkdown removes the viewer from the session scope with a regex, where splitPartition already exists for this. In General access, the read-only "keep their access" sentence uses policy?.subject ?? "channel" instead of the place value computed just above it. Link destinations are keyed by href, so duplicate destinations would collide.

Verified, no action needed: the editor's first-edit round trip keeps unsupported syntax unchanged, with tests covering headings, tables, task lists, HTML and unsafe links. DocumentEditor performs no relay writes. Plugin member changes are locked while pending, cancelled on unmount or session/channel change, and block dismissal. Plugin-supplied link destinations still pass the host link validator. Commits are Taylor-authored and signed off.

@tellaho

tellaho commented Nov 7, 2026

Copy link
Copy Markdown
Contributor Author

🤖 Review round 1 repairs — Carl, AI agent, posted on Taylor’s behalf.

At 22e7eb5ee23e80d7ec36069b4883a705ba193d0f:

  • Fixed the retained-access disclosure: plugin records becoming Private now warn that people who already joined keep access until removed. The verified-no-Guests exception for View only remains. Corrected the guide example and help.
  • Narrowed the public member-picker props; host navigation and visit lifetimes remain internal. Generated-author checks reject the excluded props.
  • Investigated both failed Chromium shards at 92fb2224f: cold navigation timed out before assertions. Restricted the fixture to the Channels plugin, existing host conversation features and installed modules. Full-journey source requests dropped from 1,303 to 755 locally; assertions, cases, timeouts and retries are unchanged.

Validation: 44 General access tests, the independent generated-author consumer, both affected full browser files in Chromium/WebKit (4/4), and real built-app confirmation captures in both engines (2/2). New regression checks fail against the prior implementation. Mandatory commit/push hooks pass at this head.

Other optional suggestions are deferred to keep the repair bounded. Hosted confirmation, repaired-head review, native/live-relay and human acceptance remain pending; the PR remains draft. Updated confirmation screenshots and timing details are in the description.

@tellaho

tellaho commented Nov 7, 2026

Copy link
Copy Markdown
Contributor Author

🤖 Review round 2: Princess Donut, reviewing exact head 22e7eb5ee (3 commits on top of round 1's 92fb2224f). Posted from Taylor's account on Taylor's behalf.

Verdict: 9/10. The round 1 blocker is fixed and the fix commits introduce no new defects. I checked only the blocker and these three commits, as scoped. Hosted CI for this head was still pending when I looked (4 passed, 63 pending).

Round 1 blocker: fixed

Plugin-policy Private confirmations now say that existing members keep access. The retained-access warning now shows whenever a plugin supplies an access policy, not only for Solo chats, using the same rules:

  • Leaving Public: it always warns that people who already joined remain members.
  • Leaving View only: it warns about readers who joined, unless the plugin passes mayHaveGuestMembers: false from verified signed roles.
  • A record that is already Private: no warning is added.

The default stays conservative: a plugin that never passes the flag always gets the warning. The tests now cover the plugin case for leaving Public and every flag value for leaving View only. The guide's example no longer promises "Only invited people", and it documents the flag accurately.

The three fix commits: no new defects

  • Narrower member-picker props (looks good). The plugin-facing type now covers only session and channel, trigger and close, title and description, member actions, access settings and share link. The host's navigation, visit and lifetime props are a private extension used only inside the host. The author-package test now checks this at compile time: it confirms none of the host keys are in the plugin type, and that passing visit fails to compile. Host callers are unaffected.
  • Trimmed conversation fixture (looks good). The fixture now loads only Channels, plus the same host conversation features and installed plugins. Its Channels entry matches the production bundled entry. All assertions are kept. I ran the test-routing graph check (tests/integration/flake-check.test.mjs) at this head because the import graph changed: 25 of 25 passed.
  • Help text (looks good). The new buzz-help line matches the behavior.

Optional

  • Plain JavaScript plugins can still pass the host props (low, optional). The narrower type is a compile-time contract only, so a plugin written in plain JavaScript could still pass visit or navigator at runtime. That's acceptable for type-only author packages; it only matters if the host ever wants to enforce it. Round 1's remaining optional items are unchanged and not reopened.

@tellaho
tellaho marked this pull request as ready for review November 7, 2026 04:05
@tellaho
tellaho requested a review from a team as a code owner November 7, 2026 04:05
@tellaho
tellaho force-pushed the tho/plugin-member-picker branch from 22e7eb5 to 0614864 Compare November 7, 2026 10:27
@tellaho tellaho changed the title feat(plugins): expose shared UI to installed plugins feat(plugins): expose the host sharing picker Nov 7, 2026
@tellaho

tellaho commented Nov 7, 2026

Copy link
Copy Markdown
Contributor Author

🤖 Review round 3: Princess Donut, scoped review of exact head 06148644b (picker and General access only, on main 70a8aebdc). Posted from Taylor's account on Taylor's behalf.

Verdict: 9/10, no blockers. I answered the three scoped questions and did not reopen other areas.

1. Does it work alone on main? Yes.

  • Nothing from the other slices remains. Compared with the reference recombined tree 8f70f3ee8, the only differences across this PR's 17 files are removals of DocumentEditor, Markdown and workspace pieces: their service entries and imports, author type exports, the editor and document type-test consumers, and the two guide sections.
  • The shared code they live in is untouched. The messages and panels feature folders match main exactly, and neither the PluginMarkdown nor the DocumentEditor file is present.
  • Nothing the picker needs was removed. The packed consumer fixture, the conversation browser additions and the picker guide section reference none of the removed surfaces. The author type test still compiles a picker plus General access consumer, and still includes the check that host-only props are rejected.

2. Are the sharing pieces intact after the split? Yes.
The picker and General access components, their tests, the member-administration tests and the help, channels and Solo-sharing docs are identical to the reference tree. Specifically, all four requested pieces are present:

  • "Public" label: existing Public access shows as Public whenever Public is restricted.
  • One-way confirmation: "You won't be able to switch back to Public here." appears when leaving Public.
  • Retained-access notice: plugin policies get the retained-access warning on Private, and the mayHaveGuestMembers behavior is documented and covered by the per-flag test.
  • Narrowed public props: ChannelMembersDialogProps stays narrowed, with host navigation props only in the private host type.

3. Is the fixture change identical to #2762's? Yes.
This PR's change to the conversation test fixture is byte-identical to #2762's: the patches against main hash the same, and the file matches #2762's head exactly. Git applies an identical change on both sides cleanly, so the two can land in either order without conflict.

Hosted CI for this head was still pending when I looked (6 passed, 56 pending). Earlier optional findings on the picker, such as the missing add announcement and the add authority mismatch, are unchanged and still optional.

@tellaho
tellaho force-pushed the tho/plugin-member-picker branch from 0614864 to acb00bb Compare November 7, 2026 20:29
@tellaho

tellaho commented Nov 7, 2026

Copy link
Copy Markdown
Contributor Author

🤖 Conflict-only rebase onto main b6289eb59; head acb00bb8e. Preserved main’s URL-install and decisions API documentation alongside the picker/access guide. Sharing implementation and tests are unchanged. Range-diff, documentation checks and mandatory push hooks pass. Ready-for-review state preserved; new-head CI pending.

@kalvinnchau kalvinnchau left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🤖 One minor permission-state UI defect: the plugin Add affordance can offer an action that host authority always rejects.

@tellaho

tellaho commented Nov 8, 2026

Copy link
Copy Markdown
Contributor Author

🤖 Fixed the plugin Add permission finding in 2b7c7482c.

Plugin additions now require a ready authority snapshot with canManage, as well as the plugin’s canAdd policy. A plain member gets member search and no Add action. The built-in picker policy and submission-time authority checks remain in their existing owners.

The new plain-member regression fails against the previous code; all 99 member-administration tests pass with the fix. Two temporary installed-module journeys pass in Chromium and WebKit (4 checks total): the plain-member search/no-Add flow, and the existing owner sharing/confirmation/focus flow. Mandatory hooks pass. No permanent browser cases were added or removed.

Built app at this head, installed example, disposable signed data and modeled relay/native inventory. These images show the ordinary-member state; narrow uses enlarged text. New-head CI and review, native packaged/live-relay and human acceptance remain pending.

Theme Wide Narrow
Dark Dark member sharing picker Dark member sharing picker, narrow
Light Light member sharing picker Light member sharing picker, narrow

tellaho and others added 11 commits November 12, 2026 16:22
Expose MembersDialog and GeneralAccess through conversation.ui. Let plugins supply stricter member actions, access settings, resource wording, and share links while retaining the existing channel defaults.

Co-authored-by: Carl <acda9e433d19dcd0e6b6840f7f4b98f3a56f1fab98049d444c087019e6d36560@feedback.communities.buzz.xyz>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Keep staged member additions and public participation changes disabled if the plugin revokes that permission before confirmation.

Co-authored-by: Carl <acda9e433d19dcd0e6b6840f7f4b98f3a56f1fab98049d444c087019e6d36560@feedback.communities.buzz.xyz>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Show the current policy as Public and warn in the existing confirmation when Public is unavailable as a future choice.

Co-authored-by: Carl <acda9e433d19dcd0e6b6840f7f4b98f3a56f1fab98049d444c087019e6d36560@feedback.communities.buzz.xyz>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Reuse the existing membership warning when plugin records become Private, and correct the public authoring example.

Co-authored-by: Carl <acda9e433d19dcd0e6b6840f7f4b98f3a56f1fab98049d444c087019e6d36560@feedback.communities.buzz.xyz>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Separate supported plugin picker props from host navigation and lifecycle wiring, and verify the generated author contract rejects host-only fields.

Co-authored-by: Carl <acda9e433d19dcd0e6b6840f7f4b98f3a56f1fab98049d444c087019e6d36560@feedback.communities.buzz.xyz>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Load the Channels plugin and existing host conversation features required by the installed-consumer proofs, instead of transforming every bundled plugin during navigation. Preserve all assertions, timeouts and retries.

Co-authored-by: Carl <acda9e433d19dcd0e6b6840f7f4b98f3a56f1fab98049d444c087019e6d36560@feedback.communities.buzz.xyz>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Match the plugin Add affordance to the ready authority checked on submission. Preserve built-in channel addition policy and cover a plain member despite plugin canAdd being true.

Co-authored-by: Carl <acda9e433d19dcd0e6b6840f7f4b98f3a56f1fab98049d444c087019e6d36560@feedback.communities.buzz.xyz>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Let plugins set nonMemberLabel for the search section and its accessible name. Default plugin-managed membership to “Not yet added” while preserving channel/chat wording and blank-label fallback. Document the prop and cover label choices without changing addition routing.

Co-authored-by: Carl <acda9e433d19dcd0e6b6840f7f4b98f3a56f1fab98049d444c087019e6d36560@feedback.communities.buzz.xyz>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Remove the unsupported exact option from findByRole; string accessible-name matching is exact by default. The heading and addition assertions are unchanged.

Co-authored-by: Carl <acda9e433d19dcd0e6b6840f7f4b98f3a56f1fab98049d444c087019e6d36560@feedback.communities.buzz.xyz>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Carry forward the callback-completion scroll guard and regression cases from the previous main integration. Keep the current main focus restoration and filter behavior.

Co-authored-by: Carl <acda9e433d19dcd0e6b6840f7f4b98f3a56f1fab98049d444c087019e6d36560@feedback.communities.buzz.xyz>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Use the current identity and access filter setters when a plugin addition completes, matching the built-in addition path after the main sync.

Co-authored-by: Carl <acda9e433d19dcd0e6b6840f7f4b98f3a56f1fab98049d444c087019e6d36560@feedback.communities.buzz.xyz>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
@tellaho
tellaho force-pushed the tho/plugin-member-picker branch from 6694b09 to 67faf60 Compare November 13, 2026 00:27

This branch has not been deployed

No deployments
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