The roster already has sections, but only automatic ones: one per gateway
connection plus the group-chat bucket. Those answer "where does this bot
run", which is not the question being asked when someone wants two client
bots filed together under "Clients" and the internal ones under "Team".
This adds a second axis that composes with the first: gateway sections keep
the top level whenever more than one connection is showing, and user
sections group the flat list underneath.
Design choices, each deliberate:
- Membership lives on the BOT (`ui_meta.sectionId`), not as a member list on
the section. A bot can only be in one place, deleting a section cannot
orphan anybody, and the assignment rides the same profile.yaml sync every
other bot setting already uses, so it follows the profile to another
machine. Section records (id, name, icon) live in plugin storage.
- "Unassigned" is not a section. It is whatever is left, always drawn last,
and it is where members of a deleted section land. No record, so nothing
to keep in sync.
- Three gestures, one rule: drag a row onto a section heading; cmd/ctrl-click
and shift-click build a multi-selection (shift ranges in DOCUMENT order,
anchored Finder-style); and the row's context menu gets "Move to section…"
with the same targets the drag would use. Dragging a row that is part of
the selection drags the whole selection.
- The drag uses a private MIME type, so a bot dropped on the composer or the
transcript is simply not a valid payload there instead of pasting its key
as text.
- Section headings rename inline (double-click / menu), reorder, hide their
glyph, and delete (keeping their bots). Right-click and the ⋯ button open
the same menu so neither can drift.
With no sections created the roster renders exactly as before.
Tests: user-sections.test.ts covers the pure model (normalisation, grouping
with unknown/deleted sections falling to Unassigned, drag payload
round-trip). The existing hermes-bots suite passes; tsc and eslint clean.