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.
- package/LICENSE +21 -0
- package/README.md +112 -2
- package/README.zh.md +98 -0
- package/client.js +2378 -0
- package/cordis.patch.yml +3 -0
- package/docs/DETAIL.md +220 -0
- package/docs/model-picker.jpeg +0 -0
- package/icon.svg +11 -0
- package/index.js +12 -0
- package/locale/en.json +7 -0
- package/locale/zh.json +7 -0
- package/package.json +26 -4
package/cordis.patch.yml
ADDED
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
package/package.json
CHANGED
|
@@ -1,6 +1,28 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-model-flatmap",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
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
|
+
}
|