/** * RoleSwitchDock — the rolebox contribution to the dsh * `'conversation.input.dock'` slot (browser half). * * The dock is a list/session-scoped full-width row above the composer card * (declared by `@deepseek-ai/dsh-client-ui-conversation` as * `{ kind: 'list', scope: 'session', owner: InputZone }`). This component is * registered into that slot by the client plugin entry (`client.ts`). * * The dock answers a STATUS question — "which role is running?" — so the * redesign makes the VALUE the title. The shipped static label ("Role") is * gone; the header renders the active role's display NAME. A label the user * already knows (the strip is 8px above a composer, glyph-marked, and the only * such strip) is replaced by the one fact the user does not have. * * - a 40px toggle header — lead glyph, the active role NAME, an * `Active`/`Base` chip, a chevron — plus a sibling status seat outside * the button. The dock starts COLLAPSED on mount and on every session * change so it never blocks the composer; * - the active role is carried by FIVE redundant channels and never by a * coloured side border (a banned anti-slop tell): the spelled-out chip in * the header (which survives total loss of hue perception), a reserved * 20px trailing mark seat on every row holding a check glyph, an inherited * font-weight step (400 -> 500) on the active row, the host active-nav * fill, and `aria-current`. The shipped 6px dot is gone; * - the status seat is a SIBLING of the toggle button, not a descendant: * a live region nested inside an interactive control rewrites the * control's accessible name on every status change. It is rendered * UNCONDITIONALLY so the region stays mounted (mounting a live region * together with its text is unreliable across screen readers), and it is * empty and silent at rest — the value seat and the chip carry the steady * state. A successful mutation writes a confirmation into it visually * hidden, so a collapse is still announced; * - the disclosure is ALWAYS MOUNTED and animated via * `grid-template-rows: 0fr -> 1fr` plus `visibility`, so closing can * animate and closed content is neither focusable nor exposed to the * accessibility tree. The button carries `aria-expanded` and * `aria-controls`; * - a filter row (shown while roles exist) narrows the list client-side by * name and description as the user types, with a clear affordance, a live * `n of N` count, and an explicit no-match row. The query survives * collapse/expand and resets on session change; * - on mount (and on every `sessionId` change) the session's persisted * active role is hydrated from `GET /rolebox/roles/active?session=…`. A * tri-state (`loading` | `ready` | `unknown`) prevents the dock from * claiming "Base agent" before the probe has answered; * - a successful switch or clear collapses the dock — the collapse IS the * confirmation (no toast, no checkmark flash, no colour pulse) — and a * FAILED mutation keeps the list open so the Retry row stays reachable; * - a clear-to-base row (visible only while a role is active) issues * `DELETE /rolebox/roles/active?session=…`; * - a failed LOAD is recoverable: the empty state offers a Reload action * that preserves the open list and the typed query. * * The slot contract (dsh-client-ui-slots' `SlotCore.register` + * `PropsRuntime` / `InjectFace` / `PropsLocale`) is consumed STRUCTURALLY: * `@deepseek-ai/dsh-client-ui-slots` is not installed yet, so * `RoleSwitchDockProps` duck-types the composed four-share intersection * against the observed `.d.ts` shapes — see the module docstring of * `client.ts` for the citation map. The only external module this file * imports is `react`, whose type surface is supplied by the temporary * `react.stub.d.ts` in this directory — which declares `useState`, * `useEffect`, `useRef`, `createElement` and `Fragment`. No hook beyond * those five may be used. * * This module is BROWSER code: it must not import node builtins, and it uses * the browser `fetch` global with relative (same-origin) paths. It touches * the DOM in exactly ONE place, by design: a single `useRef` on the header * toggle, used to restore focus after a successful switch/clear. Collapsing * the disclosure hides the row the user just activated, so without that call * focus falls to `
` and the keyboard user's next keystroke goes nowhere. * No scroll listeners, no measurement, no other DOM reads. * * @module */ /** * Structural role DTO — the `GET /rolebox/roles` list item. Mirrors the * route's `RoleSwitchRoleDto` (`web-role-switch-route.ts`): all five * keys are always present; `model` / `mode` are `null` when the definition * carries no override. */ export interface RoleSwitchRoleDto { id: string; name: string; description: string; model: string | null; mode: string | null; } /** Structural success body of `POST /rolebox/roles/switch`. */ export interface RoleSwitchOkBody { ok: true; session: string; role: string; } /** Structural success body of `GET /rolebox/roles/active` (`role` is `null` for the base agent). */ export interface RoleSwitchActiveBody { session: string; role: string | null; } /** Structural success body of `DELETE /rolebox/roles/active` (`role` is always `null`). */ export interface RoleSwitchClearOkBody { ok: true; session: string; role: null; } /** Structural error body of `POST /rolebox/roles/switch` (non-2xx). */ export interface RoleSwitchErrorBody { ok: false; error: string; } /** * Composed props of the dock entry — a duck-type of the slot framework's * `PropsRuntime<'conversation.input.dock'> & InjectFace<...> & PropsLocale<'conversation'>` * intersection, restricted to the two seats this component consumes: * * - `sessionId` — the framework-resolved session id, delivered through the * entry's inject factory (`client.ts` passes `inject: (sessionId) => * ({ sessionId })`, per the InjectParams of a `scope: 'session'` slot). * - `t` — the locale seat promised by declaring `locale: 'conversation'`. * Declared (optional) so the component satisfies the four-share * composition, but the dock renders hardcoded English text: the * 'conversation' dictionary keys are not known at this layer, and unknown * keys must not be routed through `t`. */ export interface RoleSwitchDockProps { /** Framework-resolved session id, delivered via the entry's inject factory. */ sessionId: string; /** Locale seat (declared `locale: 'conversation'`); accepted, not used. */ t?: (key: string, params?: Record