dsh-model-flatmap 0.0.0-stage → 1.0.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.
@@ -0,0 +1,3 @@
1
+ - insert:
2
+ - id: dsh-model-flatmap
3
+ name: dsh-model-flatmap
package/docs/DETAIL.md ADDED
@@ -0,0 +1,220 @@
1
+ # Flat model picker
2
+
3
+ Replaces the composer's model selector with a single flat panel. There are no
4
+ second-level menus: the model grid, the reasoning levels of the current model,
5
+ and the agent teamwork switch are all visible at once.
6
+
7
+ ## What it does
8
+
9
+ - **One surface.** The trigger opens one panel capped at 320 px. The header,
10
+ and one adaptive footer holding the reasoning group and the teamwork row,
11
+ stay fixed; only the model grid scrolls. The two footer groups share a single
12
+ line when both fit with a 30 px gap, and otherwise fall back to one row each.
13
+ - **Global search.** The search box filters across every provider and every
14
+ family. Groups that end up empty are not rendered, so no stray headings
15
+ remain. Prefix matches rank first, then substring matches, then ordered
16
+ character matches.
17
+ - **Word-wise search.** Whitespace splits the query into tokens, and every
18
+ token has to land somewhere in the name, so `mod inte` finds
19
+ `ModelScope International` even though the two words are never contiguous.
20
+ A hit ranks by its weakest token, so a multi-word query never outranks a
21
+ plain prefix. A single-token query keeps the original ranking exactly.
22
+ - **Provider-wide search.** A query that matches a *provider* — its id, its
23
+ catalog name, or the label its section draws — selects that provider as a
24
+ whole: every model it offers stays in the grid, not only the ones whose names
25
+ contain the query. Typing `dou` therefore surfaces `豆包(doubao)` with its
26
+ entire catalog. Models matched by their own name still rank first; the rest
27
+ follow in catalog order. Provider matching is word-wise too, and tokens may
28
+ land in different faces of the same provider — the id answers `mod`, the
29
+ localized name answers `inte`. It accepts prefixes and substrings only: an
30
+ ordered-subsequence pass would let a two-letter query drag every provider in,
31
+ which is noise rather than a match. This applies to both grouping modes.
32
+ - **Two grouping modes.** A segment control switches the grid between
33
+ *By provider* and *By family*. Grouping and search are independent: the
34
+ segment only changes how the filtered result is organised.
35
+ - **Model families.** A built-in dictionary recognises DeepSeek, Grok, Opus,
36
+ Haiku, Sonnet, Claude, Qwen, GLM, Kimi, GPT, Gemini, Llama, Mistral and
37
+ others. A model the dictionary does not cover is inferred from its id;
38
+ anything unjudgeable falls into *Others*, which always sorts last. Families
39
+ stay flat — no nesting.
40
+ - **Registered brands keep their generations apart.** Merging `hy3` and `hy4`
41
+ into one `HY` family (DEC-004) was wrong: the generation has to be visible.
42
+ A registered brand (`FAMILY_BRANDS`, currently `HY`) resolves to
43
+ `BRAND + generation` when its id carries a generation digit, so `hy3` /
44
+ `hy3-x` → `HY3`, `hy4-preview` / `hy4free` / `hy4std` → `HY4`, and a bare
45
+ `hy` → `HY`. Generations rank next to their bare brand in numeric order.
46
+ Unregistered brands still collapse their version tail (`acme4-turbo` and
47
+ `acme-7b` are one `Acme`), and dictionary families are never split by
48
+ generation. A provider prefix does not hide the brand (`codebuddy-hy3` →
49
+ `HY3`).
50
+ - **Family-grouped rows are provider-primary.** A family heading says what the
51
+ models *are*, so the family name is not repeated in a row. The row's primary
52
+ text is the provider name — the one thing a heading cannot tell you when a
53
+ family spans providers. The full model name and description stay in the row
54
+ tooltip. Only when one provider offers several models in the same family do
55
+ those rows carry a short secondary variant derived from the id tail (e.g.
56
+ `hy4free` → `free`, `hy3-x` → `x`); the plain base model carries no tag.
57
+ Colliding variants fall back to the unique model id. Under *By provider* the
58
+ heading already is the provider, so the primary text stays the model name and
59
+ no secondary tag is drawn.
60
+ - **Adaptive columns.** The panel measures one group title and one model row,
61
+ subtracts the fixed sections from the 320 px cap, and picks the smallest
62
+ column count from 1 to 3 that fits. If even three columns overflow, it keeps
63
+ three and the grid scrolls. A group title never separates from its first
64
+ model, and the last column's deficit is balanced against its neighbour.
65
+ - **Reasoning levels.** The current model's advertised levels render as a
66
+ caption ("推理等级" / "Reasoning effort") with a sparkle icon and a hint
67
+ tooltip, on the same line as a multi-segmentation control — the track wraps
68
+ to its own line only when the row cannot fit. The segments are radio
69
+ choices (`role="radiogroup"` / `role="radio"`, exactly one `aria-checked`
70
+ at a time, arrow keys move the selection) drawn with the header segment's
71
+ visual: a toned track and a floating active segment, so the active level is
72
+ unmistakable. A click submits through `session.selectModel` and the group
73
+ refreshes from the directory once it updates. A model without reasoning
74
+ metadata hides the whole group, and it costs no height.
75
+ The group shares a wrapping footer line with the teamwork row: when
76
+ reasoning + 30 px + teamwork fits the panel's inner width they sit side by
77
+ side, otherwise the footer wraps back to the original two-line stack. The
78
+ break is decided by CSS flex wrapping and the 30 px `column-gap`, never by a
79
+ JS width threshold, so it stays correct across panel resizes, font loads and
80
+ both themes. The single grey rule above the reasoning group stays where it
81
+ is in both states; the teamwork row draws no divider of its own (DEC-011).
82
+ The fixed height budget measures the footer container once, not its two
83
+ children, so a shared line is not double-counted against the grid.
84
+ - **Selected model.** The current model reads as a solid deep-blue band with
85
+ white, bold text — no checkmark on the right. Selection lives on the row
86
+ itself (`aria-pressed="true"`), so nothing is reserved at the trailing edge:
87
+ unselected rows keep their full width for the name, the provider tag, and the
88
+ badge, and the measurement probe no longer mirrors a tail column. The blue
89
+ is `--dsw-static-deepseek-600` and the text is
90
+ `--dsw-static-neutral-bluish-00`, both from the theme-invariant static
91
+ palette, so the band looks the same in light and dark themes. Hover skips the
92
+ selected row so the neutral hover wash never covers the highlight.
93
+ - **Badges.** A model's catalog badge (for example a third-party rate
94
+ multiplier such as `×0.1`) renders inside its row. A model with no badge
95
+ renders no empty slot. On the selected row the badge switches to a
96
+ translucent white chip so it stays legible on the blue fill.
97
+ - **Out-of-balance models are struck through.** When a model call fails because
98
+ the account has no usable balance, credit, or quota left, that model's row
99
+ text is drawn with a `line-through` decoration, and the strike stays until a
100
+ later call for the same model returns something other than an
101
+ insufficient-balance failure. The row is not recoloured, so the strike reads
102
+ the same on the selected row's blue band and in both themes, and it does not
103
+ compete with the error strip's red. Only the row's text (the name and a
104
+ family variant tag) is struck; the badge chip and the pending spinner are
105
+ not.
106
+ - **What counts.** The Host already classifies an exhausted account into the
107
+ provider-neutral `QUOTA` / `ACCOUNT_QUOTA` codes and maps a provider HTTP
108
+ `402` to `QUOTA`. The panel accepts those codes, a structured `status: 402`
109
+ (or an `HTTP_402` code), and — for a failure the Host flattened to another
110
+ code with the provider's wording kept — the provider text itself
111
+ (`insufficient balance` / `insufficient credits` / `quota exceeded` /
112
+ `out of credits` / `not enough quota` / `no credits remaining` /
113
+ `额度已用尽` / `余额不足` / `预算已用完`, among others).
114
+ - **A `429` is decided by its body, not by the status.** Several providers
115
+ answer an exhausted balance with HTTP **429** rather than 402 — for example
116
+ `429: {"message":"insufficient balance","request_id":"…"}`, which the Host
117
+ classifies as `RATE_LIMIT`. A `429` therefore neither strikes nor clears on
118
+ its status alone: the message decides, so a 429 carrying balance wording
119
+ strikes while a genuine rate limit (`Rate limit exceeded`,
120
+ `访问量过大`, `使用量已超出频率限制`) does not. The rule is pinned against
121
+ real recorded failures (see the `REAL_INSUFFICIENT` / `REAL_TRANSIENT`
122
+ tables in the render tests, harvested from local session history).
123
+ - **What does not count.** A transient limit leaves the account usable, so
124
+ `RATE_LIMIT` with rate-limit wording, `TIMEOUT`, `TRANSPORT`,
125
+ `CONTEXT_WINDOW_EXCEEDED`, a `SERVER` failure, and a credential that simply
126
+ cannot use the model never strike a row. Nor does a message that only
127
+ *mentions* 额度 — a context-overflow failure says “与网络、账号、额度都无关”
128
+ and a transport failure merely suggests waiting for quota to recover; both
129
+ are excluded, and the tests pin them.
130
+ - **Which model.** The failing call's model is read from the `request/header`
131
+ event that immediately precedes the failing `turn/end`, so the mark lands
132
+ on the route (`provider/model`) the request was actually dispatched to —
133
+ not on the currently selected model, which may have changed since.
134
+ - **When the strike ends.** A `turn/end` for the same route that proves the
135
+ model answered clears it: a completed turn, a `max-tokens` turn, or a turn
136
+ that failed for a different reason. A cancellation (`aborted`), a `blocked`
137
+ turn, and the synthetic `interrupted` / `forked` closers clear nothing —
138
+ the model never answered, so they say nothing about the account.
139
+ - **Where it comes from.** The seat folds the Session's own durable event
140
+ feed (`sessions.binding(id).eventSource`), which is the same source the
141
+ Chat reads its quota notice from. No Host change is involved. A history
142
+ replacement refolds from scratch; paging in older history cannot change a
143
+ mark and is ignored.
144
+ - **Agent teamwork.** The switch keeps the host's semantics: the same label,
145
+ the same confirmation before disabling it mid-conversation, and no optimistic
146
+ update — the state comes from the `agentTeam` projection. The row lives in
147
+ the shared footer line described under *Reasoning levels*, immediately right
148
+ of the reasoning group when both fit on one line.
149
+
150
+ ## Data and interaction
151
+
152
+ Everything goes through public Client APIs:
153
+
154
+ - `ctx.modelDirectories.directoryFor(sessionId)` supplies the catalog, the
155
+ current selection, and the pending selection; `load()` refreshes it and
156
+ `select()` submits.
157
+ - The directory store is subscribed through `useSyncExternalStore`, so a
158
+ catalog update — including one caused by a selection — repaints the panel.
159
+ - `ctx.sessions.binding(sessionId).eventSource` is the Session's contiguous
160
+ durable event window. The seat folds it to derive the per-model
161
+ insufficient-balance marks, and subscribes through `useSyncExternalStore` so
162
+ a mark transition repaints the panel while every other event stays quiet.
163
+ - `ctx.get('remote').agentTeams.setEnabled()` writes the teamwork selection.
164
+ - `ctx.locale` supplies the copy, resolves catalog badge text, and prefers the
165
+ host's own `agent-team` wording when that namespace is registered.
166
+
167
+ No host component, stylesheet, or DOM is read or restyled.
168
+
169
+ ## Layout behaviour
170
+
171
+ - Window resize, a grouping switch, and search input all re-plan the columns,
172
+ debounced, and the plan runs once more after fonts finish loading so the
173
+ first frame's measurements do not stick.
174
+ - The panel opens above the trigger and flips below when there is no room,
175
+ staying inside the viewport.
176
+ - An unbound Session, an empty catalog, a failed provider, and a search with no
177
+ hits each have their own state. A directory that has not resolved yet still
178
+ renders a trigger rather than blanking the composer.
179
+
180
+ ## Styling
181
+
182
+ Colours, radii, spacing, and elevation come from the `--dsw-alias-*`,
183
+ `--dsw-static-*`, `--dsw-elevation-*`, `--dsw-menu-*`, and `--dsw-radius-*`
184
+ tokens, so light and dark themes both follow the host palette. The selected
185
+ row's blue is deliberately drawn from the static palette (`--dsw-static-*`),
186
+ which is theme-invariant: the alias `--dsw-alias-state-business-primary`
187
+ flips to a light blue with dark text in the dark theme, which would break the
188
+ requested deep-blue-with-white-text treatment there. The panel uses the same menu surface
189
+ material as the host's own menus (menu fill, backdrop blur, prominent
190
+ elevation, large radius), and hover, focus, selected, and disabled states follow
191
+ the shipped picker's visual language.
192
+
193
+ Each provider / family label is deliberately set apart from the model rows
194
+ beneath it: it draws as a filled, rounded band that floats on
195
+ `--dsw-alias-bg-layer-1` with `--dsw-elevation-soft` — the same recipe as the
196
+ active segment. That fill is opaque white in light themes and near-black in
197
+ dark, so it steps clearly away from the translucent panel surface (and the
198
+ transparent model rows) in both themes, with no hard-coded colour. The bare
199
+ `--dsw-alias-menu-group-header-fill` token was only a few levels from the
200
+ surface and read as almost the same background as the rows (see DEC-005). The
201
+ label keeps smaller but bold type with a stronger label colour. The model rows
202
+ stay 13 px regular text on a transparent background, so the band reads as a
203
+ section header rather than another selectable row.
204
+
205
+ The band also carries a low-key frame: a hairline border
206
+ (`--dsw-alias-border-l2`) and a short brand-blue left edge
207
+ (`--dsw-alias-state-business-primary`, the panel's own accent). That marks each
208
+ group start clearly without boxing the group's rows — a group's rows may flow
209
+ across columns, so only its label is framed (see DEC-003).
210
+
211
+ ## Limitations
212
+
213
+ - The panel replaces the composer seat only; the `/model` popup is untouched.
214
+ - The panel does not host the `conversation.input.model.label` and
215
+ `conversation.input.model.options` child slots. The shipped registration keeps
216
+ those declarations alive, so other plugins' contributions stay valid; their
217
+ content simply is not drawn inside this panel.
218
+ - Addressed subagent sessions hide the seat, matching the shipped selector.
219
+ - The badge comes from the catalog entry, so a provider that publishes no badge
220
+ shows none.
Binary file
package/icon.svg ADDED
@@ -0,0 +1,11 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64" role="img" aria-label="Flat model picker">
2
+ <title>Flat model picker</title>
3
+ <rect x="4" y="8" width="56" height="48" rx="10" fill="none" stroke="currentColor" stroke-width="3" opacity="0.85"/>
4
+ <rect x="11" y="15" width="20" height="5" rx="2.5" fill="currentColor" opacity="0.55"/>
5
+ <rect x="35" y="15" width="18" height="5" rx="2.5" fill="currentColor" opacity="0.25"/>
6
+ <rect x="11" y="25" width="20" height="5" rx="2.5" fill="currentColor" opacity="0.55"/>
7
+ <rect x="35" y="25" width="18" height="5" rx="2.5" fill="currentColor" opacity="0.25"/>
8
+ <rect x="11" y="35" width="20" height="5" rx="2.5" fill="currentColor" opacity="0.55"/>
9
+ <rect x="35" y="35" width="18" height="5" rx="2.5" fill="currentColor" opacity="0.25"/>
10
+ <rect x="11" y="45" width="42" height="4" rx="2" fill="currentColor" opacity="0.18"/>
11
+ </svg>
package/index.js ADDED
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Host half of the flat model picker bundle.
3
+ *
4
+ * The plugin is browser-only: it replaces the composer's `conversation.input.model`
5
+ * seat with a flat panel rendered in the Web GUI, and reads the Session's model
6
+ * directory, projections and `session.selectModel` through public Client APIs.
7
+ * The Host therefore contributes no service, tool or event.
8
+ */
9
+ export const name = 'dsh-model-flatmap'
10
+ export const inject = []
11
+
12
+ export function apply() {}
package/locale/en.json ADDED
@@ -0,0 +1,7 @@
1
+ {
2
+ "meta": {
3
+ "title": "Flat model picker",
4
+ "description": "Replaces the composer model selector with one flat panel: search, provider or model-family grouping, every model and every reasoning level visible at once, and the agent teamwork switch on the same surface. No second-level menus.",
5
+ "detail": "docs/DETAIL.md"
6
+ }
7
+ }
package/locale/zh.json ADDED
@@ -0,0 +1,7 @@
1
+ {
2
+ "meta": {
3
+ "title": "模型平铺选择面板",
4
+ "description": "用一块平铺面板替换输入框的模型选择器:搜索、按 Provider 或模型家族分组,所有模型与全部推理等级一次可见,智能体团队协作开关也在同一面板内,没有任何二级菜单。",
5
+ "detail": "docs/DETAIL.md"
6
+ }
7
+ }
package/package.json CHANGED
@@ -1,6 +1,28 @@
1
1
  {
2
2
  "name": "dsh-model-flatmap",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "1.0.0",
4
+ "description": "Flat, two-level-free model picker for the DSH composer: every model and reasoning level on one surface.",
5
+ "type": "module",
6
+ "exports": {
7
+ ".": "./index.js",
8
+ "./client": "./client.js",
9
+ "./package.json": "./package.json",
10
+ "./locale/*.json": "./locale/*.json",
11
+ "./icon": "./icon.svg"
12
+ },
13
+ "files": [
14
+ "index.js",
15
+ "client.js",
16
+ "cordis.patch.yml",
17
+ "locale/*.json",
18
+ "docs",
19
+ "icon.svg"
20
+ ],
21
+ "dsh": {
22
+ "bundle": { "patch": "./cordis.patch.yml" },
23
+ "client": {
24
+ "platform": "web",
25
+ "immediately": true
26
+ }
27
+ }
28
+ }