dsh-model-flatmap 1.0.3 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -0
- package/README.zh.md +14 -0
- package/client.js +964 -42
- package/docs/DETAIL.md +136 -4
- package/package.json +1 -1
package/docs/DETAIL.md
CHANGED
|
@@ -195,6 +195,126 @@ and the agent teamwork switch are all visible at once.
|
|
|
195
195
|
the shared footer line described under *Reasoning levels*, immediately right
|
|
196
196
|
of the reasoning group when both fit on one line.
|
|
197
197
|
|
|
198
|
+
## The trigger
|
|
199
|
+
|
|
200
|
+
The trigger is the same chip the shipped picker draws (13/20 regular, 28 px,
|
|
201
|
+
rounded-sm), and it shows the same *content* — which is more than a model name.
|
|
202
|
+
|
|
203
|
+
**The name is a slot, not a string.** The shipped `ModelSelect` does not paint
|
|
204
|
+
`model.name` on its trigger: it renders `conversation.input.model.label` with
|
|
205
|
+
`placement: 'trigger'` and lets the winning contributor decorate the name. That
|
|
206
|
+
is where a connection plugin puts things the catalog does not carry: a promotion
|
|
207
|
+
chip (`夜间折扣`), a rate, a "new on this machine" marker, and — in the case of
|
|
208
|
+
`dsh-connect-workbuddy` — a load mark: one, two or three queued people, or a
|
|
209
|
+
rocket when nothing is waiting. A bare name therefore shows strictly less than
|
|
210
|
+
the native trigger, which is what this plugin used to do.
|
|
211
|
+
|
|
212
|
+
**This plugin renders that slot's winner.** It cannot *declare* the child slot
|
|
213
|
+
(the shipped entry already did, and a second declaration throws
|
|
214
|
+
`already declared`) — but declaring is not needed to read it. The declaration
|
|
215
|
+
outlives the shadowing entry, so `slots.entriesOfSlot('conversation.input.model.label')`
|
|
216
|
+
still names the winning entry, and its `component` can be rendered with the
|
|
217
|
+
seats the renderer would have supplied:
|
|
218
|
+
|
|
219
|
+
- `t` — bound from the entry's own `locale` namespace through `locale.bind(ns)`;
|
|
220
|
+
- the business face — `entry.inject(sessionId)`, the renderer's own contract for
|
|
221
|
+
a session-scoped entry;
|
|
222
|
+
- the owner props — `{ provider, model, placement: 'trigger', selected: true }`,
|
|
223
|
+
exactly the share the shipped picker passes.
|
|
224
|
+
|
|
225
|
+
**The occupant is third-party code, so it is contained.** It runs without the
|
|
226
|
+
renderer's per-entry error boundary, so a separate `LabelBoundary` wraps it. A
|
|
227
|
+
crash costs the decoration and never the seat: the boundary answers `undefined`
|
|
228
|
+
and the trigger falls through to the catalog name. The failure is logged once.
|
|
229
|
+
|
|
230
|
+
**No occupant still gets the catalog badge.** When the cell is empty — or the
|
|
231
|
+
occupant is absent, crashed, or on a Host without `entriesOfSlot` — the trigger
|
|
232
|
+
draws the shipped fallback: the model's own `model.badge` as a chip before the
|
|
233
|
+
name. This is reproduced rather than imported (the host's `Tag` lives in a
|
|
234
|
+
primitives package this plugin does not depend on), and it matters even with an
|
|
235
|
+
occupant installed: an occupant that does not reproduce a sibling's `model.badge`
|
|
236
|
+
would otherwise lose the promotion marker.
|
|
237
|
+
|
|
238
|
+
**Registrations that land late still appear.** The trigger subscribes to the
|
|
239
|
+
slot through `slots.subscribe(key, fn)` / `slots.getVersion(key)`, so a
|
|
240
|
+
connection plugin that registers a moment after boot gets its chip on the
|
|
241
|
+
trigger without a page reload. A Host without those accessors keeps the first
|
|
242
|
+
read and never notifies.
|
|
243
|
+
|
|
244
|
+
Because native mode (below) hands the seat to the Host, which renders the same
|
|
245
|
+
slot, the trigger is now *one* implementation in both modes — the consistency
|
|
246
|
+
this feature exists for.
|
|
247
|
+
|
|
248
|
+
## Native mode
|
|
249
|
+
|
|
250
|
+
The footer's trailing `默认样式` / `Default style` button hands the composer's model seat
|
|
251
|
+
back to whatever the Host ships in `conversation.input.model`, and back again.
|
|
252
|
+
|
|
253
|
+
**How the switch works.** The plugin does NOT render the host's control — it
|
|
254
|
+
stops shadowing it. This entry registers at a negative priority, so it wins the
|
|
255
|
+
`single` slot; a `single` slot keeps every registration and renders only the
|
|
256
|
+
highest-priority live one. Native mode therefore `dispose()`s this plugin's
|
|
257
|
+
entry, which elects the shipped one underneath: the host mounts its own
|
|
258
|
+
`ModelSelect`, whatever that is in the installed version. Nothing is imported,
|
|
259
|
+
copied, or re-mounted, so an upgraded DSH shows its own newest picker and this
|
|
260
|
+
mode structurally cannot drift out of date.
|
|
261
|
+
|
|
262
|
+
The alternative — pulling the shipped component into our own tree — is not
|
|
263
|
+
available: the `model-selection` package exports no component, and every
|
|
264
|
+
`renderSlot` child (`conversation.input.model.label` / `.options`) is bound to
|
|
265
|
+
the entry that declares it, so another entry calling it throws
|
|
266
|
+
`StaleAuthorizationError` / `SlotOwnershipError`. See DEC-027.
|
|
267
|
+
|
|
268
|
+
**Stepping aside is verified, never assumed.** Before disposing, the plugin asks
|
|
269
|
+
`slots.entries('conversation.input.model')` for the registrations other than its
|
|
270
|
+
own. If there is none — the shipped package disabled, uninstalled, or a Host too
|
|
271
|
+
old to enumerate entries — the panel stays mounted and the button reads disabled
|
|
272
|
+
with a tooltip explaining why. A persisted `native` mode from a previous install
|
|
273
|
+
therefore lands on the flat panel instead of an empty composer.
|
|
274
|
+
|
|
275
|
+
**The way back.** Native mode disposes the entry that owns the footer button, so
|
|
276
|
+
the button cannot switch back. The return trip instead rides the HOST picker's
|
|
277
|
+
own panel: `conversation.input.model.options` is the list child slot the shipped
|
|
278
|
+
`ModelSelect` declares and renders directly after its Model / Effort rows, so our
|
|
279
|
+
control appears at the bottom of the native menu itself. It belongs to the
|
|
280
|
+
shipped entry, not to ours, so it outlives the disposal; and it renders `null` in
|
|
281
|
+
flat mode, so the shipped menu looks exactly as shipped while the panel is on
|
|
282
|
+
screen. Clicking it dismisses the host menu first (the entry that owns the menu
|
|
283
|
+
is about to be disposed) and then flips the mode back.
|
|
284
|
+
|
|
285
|
+
**It does not look like a menu row.** Every row above it answers "which value?";
|
|
286
|
+
this one performs an action. So it is drawn as a separated bottom band — a 1px
|
|
287
|
+
`--dsw-alias-border-l2` rule above, then a full-width 30 px outlined button
|
|
288
|
+
(`--dsw-alias-border-l3`, `--dsw-radius-sm`, 12/18 medium label, 14 px icon) with
|
|
289
|
+
a hover fill and a focus ring. The slot gives occupants only a `display: contents`
|
|
290
|
+
anchor, so that whole box is ours; nothing about the host menu's own rows has to
|
|
291
|
+
be imitated. The distinction is pinned by a test that asserts the button carries
|
|
292
|
+
no `role="menuitem"`.
|
|
293
|
+
|
|
294
|
+
Nothing here is a copy of the host control: the row is one entry in a slot the
|
|
295
|
+
installed package publicly declares. The former placement — a chip in
|
|
296
|
+
`conversation.input.right`, beside the native
|
|
297
|
+
trigger — was dropped because it read as a toolbar control belonging to nothing
|
|
298
|
+
in particular, and because it forced the mode control to sit outside the panel
|
|
299
|
+
it acts on.
|
|
300
|
+
|
|
301
|
+
**One shared store.** Both controls read one page-wide mode store, so a flip made
|
|
302
|
+
from either is what the other sees. The store persists the choice to
|
|
303
|
+
`localStorage` (`dshmf:mode`, the same origin-scoped mechanism as the grouping
|
|
304
|
+
choice) and degrades to `flat` whenever storage is unavailable — a missing store
|
|
305
|
+
must never leave the composer without a model control.
|
|
306
|
+
|
|
307
|
+
**Reported state is the visible state.** The panel only renders while it owns the
|
|
308
|
+
cell, so the stored mode and the on-screen mode can disagree in exactly one case:
|
|
309
|
+
`native` persisted with nothing to inherit the cell. The button's `aria-pressed`
|
|
310
|
+
then reports flat, because it describes what is on screen, not what is in
|
|
311
|
+
storage.
|
|
312
|
+
|
|
313
|
+
**What changes across a switch.** Coming back from native mode remounts the
|
|
314
|
+
panel, so it opens closed with an empty query. Focus follows the disposed
|
|
315
|
+
button down to the document; cross-plugin DOM queries for focus are not allowed,
|
|
316
|
+
so it is not chased.
|
|
317
|
+
|
|
198
318
|
## Data and interaction
|
|
199
319
|
|
|
200
320
|
Everything goes through public Client APIs:
|
|
@@ -217,6 +337,12 @@ Everything goes through public Client APIs:
|
|
|
217
337
|
- `ctx.get('remote').agentTeams.setEnabled()` writes the teamwork selection.
|
|
218
338
|
- `ctx.locale` supplies the copy, resolves catalog badge text, and prefers the
|
|
219
339
|
host's own `agent-team` wording when that namespace is registered.
|
|
340
|
+
- `slots.register()` / its disposer and `slots.entries()` / `slots.subscribe()`
|
|
341
|
+
drive native mode: disposing this plugin's `conversation.input.model` entry
|
|
342
|
+
elects whatever the Host registered underneath, and `entries()` is what makes
|
|
343
|
+
that verifiable before the switch. `slots.subscribe()` re-runs the reconcile,
|
|
344
|
+
so a shipped picker that appears (or disappears) later enables or disables the
|
|
345
|
+
button without a reload.
|
|
220
346
|
|
|
221
347
|
No host component, stylesheet, or DOM is read or restyled.
|
|
222
348
|
|
|
@@ -284,10 +410,16 @@ the measured overflow, and the duration is that distance divided by 50 px/s
|
|
|
284
410
|
## Limitations
|
|
285
411
|
|
|
286
412
|
- The panel replaces the composer seat only; the `/model` popup is untouched.
|
|
287
|
-
- The panel does not host the `conversation.input.model.label`
|
|
288
|
-
`
|
|
289
|
-
|
|
290
|
-
|
|
413
|
+
- The panel's GRID does not host the `conversation.input.model.label` child
|
|
414
|
+
slot: rows draw the catalog name plus `model.badge`. Only the TRIGGER renders
|
|
415
|
+
that slot's winner, because that is what the native trigger does. The
|
|
416
|
+
`conversation.input.model.options` slot is not drawn anywhere in the panel.
|
|
417
|
+
The shipped registration keeps both declarations alive, so other plugins'
|
|
418
|
+
contributions stay valid either way.
|
|
419
|
+
- The trigger's label cell renders ANOTHER PLUGIN'S component. It is written
|
|
420
|
+
against the documented `ModelLabelOwner` props, but nothing guarantees every
|
|
421
|
+
future contributor stays compatible; `LabelBoundary` degrades a crash to the
|
|
422
|
+
catalog name, which is the same fallback the host uses for an empty cell.
|
|
291
423
|
- Addressed subagent sessions hide the seat, matching the shipped selector.
|
|
292
424
|
- The badge comes from the catalog entry, so a provider that publishes no badge
|
|
293
425
|
shows none.
|