From 2fa2f42d412a5299fa0200bcd44baee38dffe62c Mon Sep 17 00:00:00 2001
From: teknium1 <127238744+teknium1@users.noreply.github.com>
Date: Thu, 1 Oct 2026 22:19:34 -0700
Subject: [PATCH] docs(desktop-sdk): document model menu row decorations and
data-session-switching
Both seams are public contracts for plugin/theme authors (first consumer:
t3-code-theme, #129600); the page documents the arbitration, failure
isolation and lifetime guarantees so a theme never needs DOM reads.
---
.../developer-guide/desktop-plugin-sdk.md | 96 ++++++++++++++++++-
1 file changed, 95 insertions(+), 1 deletion(-)
diff --git a/website/docs/developer-guide/desktop-plugin-sdk.md b/website/docs/developer-guide/desktop-plugin-sdk.md
index 081bbb9c3c..051c3767f0 100644
--- a/website/docs/developer-guide/desktop-plugin-sdk.md
+++ b/website/docs/developer-guide/desktop-plugin-sdk.md
@@ -234,6 +234,7 @@ Import the area constants from the SDK; each area has its own `data` payload.
| Keybind | `KEYBINDS_AREA` | `data: KeybindContribution` |
| Theme | `THEMES_AREA` | `data` as a `DesktopTheme` |
| Composer | `COMPOSER_AREAS.*` | render slots, or middleware / attachment providers |
+| Model menu rows | `MODEL_MENU_ROW_AREA` | `data: ModelMenuRowContribution` — a leading icon / trailing badge per model |
| Appearance settings | `APPEARANCE_AREAS.extra` | `render` — controls appended to Settings → Appearance |
### Panes
@@ -421,6 +422,45 @@ manual pick. To tint the *active* theme rather than replace it, use
[Accent Picker](https://github.com/NousResearch/hermes-desktop-accent-picker)
plugin is the worked example (it is also a complete, installable disk plugin).
+#### Styling the chat switch — `data-session-switching`
+
+Opening a chat places its transcript in steps: the session loads, the rows
+land, then the restored scroll position settles a few frames later. A theme
+that wants that hidden (or faded) targets one documented attribute instead of
+watching the route or the DOM: core sets `data-session-switching="true"` on the
+chat surface root (`[data-chat-surface]`) from the frame the switch starts until
+the new transcript's rows are on screen and its scroll position has settled,
+then removes it.
+
+```css
+/* Hide the transcript while it is being placed, fade it in when it lands. */
+:root[data-hermes-theme="noir"] [data-chat-surface] [data-slot="aui_thread-viewport"] {
+ transition: opacity 0.12s ease-out;
+}
+:root[data-hermes-theme="noir"] [data-chat-surface][data-session-switching] [data-slot="aui_thread-viewport"] {
+ opacity: 0;
+ transition: none;
+}
+```
+
+- **Per surface.** The primary chat and every tile has its own
+ `[data-chat-surface]`; the attribute marks only the one switching. Narrow to
+ the primary pane with `[data-composer-target="main"]`.
+- **Always ends.** It is held by core's own load and scroll-restore phases:
+ the load phase ends when the transcript arrives or the resume gives up, the
+ restore phase on settle (a bounded number of frames) or the first user
+ scroll/key/pointer input, and both on unmount — a theme that hides content
+ under it cannot strand the chat hidden. A brand-new empty draft never sets it.
+- **The contract is the attribute.** Target `[data-session-switching]` and the
+ `data-chat-surface` / `data-slot` hooks; internal class names are not a
+ contract and change without notice. Do not toggle the attribute yourself or
+ reproduce it with a route listener or `MutationObserver` (catalog rule 8).
+
+This replaces the t3-code-theme `installSwitchFade` pattern (a focus-store
+listener plus a `requestAnimationFrame` loop that polled the transcript's rows
+and scroll position, then toggled its own root attribute): the CSS above is the
+whole migration.
+
### Composer extensions
`COMPOSER_AREAS` (`top`, `bottom`, `underside`, `leading`, `actions`,
@@ -652,6 +692,60 @@ register(ctx) {
The reasoning-pill visibility CSS the plugin also injected has no hook; it is
only needed if the app ever hides that label at narrow widths.
+#### Model menu row decorations
+
+`MODEL_MENU_ROW_AREA` puts a per-model mark inside the native model menu — the
+one the composer's pill opens, and every other surface that renders
+`ModelCatalogMenu`. A contribution supplies `decorate(row)`; core paints what it
+returns in two fixed slots of the row: a **leading icon** before the model name
+and a **trailing badge** after core's own chips. The row's markup, name,
+star, submenu and click stay core's.
+
+```ts
+import { MODEL_MENU_ROW_AREA, type ModelMenuRowContribution } from '@hermes/plugin-sdk'
+
+interface ModelMenuRowContext {
+ provider: string // provider slug: 'anthropic', 'openrouter', …
+ model: string // the model id the row commits
+ label: string // the display name core paints on the row
+}
+interface ModelMenuRowDecoration {
+ icon?: ReactNode // element (,