feat(messaging-ui): channels — browse, join, leave, create (contract + mock + UI)

Adds a third membership beyond dm/group: a discoverable, joinable room.

- contract: Membership += 'channel'; Conversation.topic; ChannelSummary +
  CreateChannelInput; optional adapter methods browseChannels/createChannel/
  joinChannel/leaveChannel (capability-degrading — absent hides the UI)
- MockAdapter implements them (seeds a joined public, a joinable public, and a
  private channel); listConversations now returns only threads I'm in
- useChannels hook (browse/create/join/leave + supported flag)
- UI: sidebar sections (Channels vs Direct messages), a + that opens a
  ChannelBrowser (join + create), # / lock glyphs; themeable styles
- KernelClientAdapter passes channel membership + topic through
- 4 channel tests (mock browse/join/create + render browse→join); 52 total green

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-21 20:04:46 +05:30
parent 3f1f89dfbe
commit 00a2cc474a
14 changed files with 544 additions and 28 deletions
@@ -23,9 +23,8 @@ describe('<Messenger /> (rendered UI over an adapter)', () => {
it('sends a message through the adapter and shows it in the thread', async () => {
mount();
await screen.findByText('Crew is rolling out at 7.');
const input = screen.getByLabelText('Message') as HTMLInputElement;
// Wait for the auto-selected thread's composer (avoids a race with the auto-select effect).
const input = (await screen.findByLabelText('Message')) as HTMLInputElement;
fireEvent.change(input, { target: { value: 'on our way' } });
fireEvent.click(screen.getByText('Send'));