dsh-model-think-level 1.0.0 → 1.0.2

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 CHANGED
@@ -1,164 +1,12 @@
1
1
  # DSH Model Think Level
2
2
 
3
- <p align="center">
4
- <picture>
5
- <source media="(prefers-color-scheme: dark)" srcset="docs/banner-dark.svg">
6
- <img src="docs/banner.svg" alt="DSH Model Think Level" width="720">
7
- </picture>
8
- </p>
3
+ DSH 的第三方模型补齐思考等级:在官方 Models 页每个模型行注入 Thinking 开关和强度/模态编辑器,在输入框模型菜单加供应商→模型选择器、视觉徽章与等级行,并带搜索、拖拽排序、供应商启用开关;以及按路由接管请求头与 user-agent。
9
4
 
10
- [![License](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
11
- [![npm version](https://img.shields.io/npm/v/dsh-model-think-level)](https://www.npmjs.com/package/dsh-model-think-level)
12
- [![npm downloads](https://img.shields.io/npm/dw/dsh-model-think-level)](https://www.npmjs.com/package/dsh-model-think-level)
13
- ![DeepSeek Harness](https://img.shields.io/badge/DeepSeek%20Harness-plugin-4d6bfe)
14
- [![Awesome DSH Plugin](https://awesome-dsh-plugin.com/badge.svg)](https://awesome-dsh-plugin.com)
5
+ 模型选择器
6
+ ![alt text](./docs/image.png)
7
+ ![alt text](./docs/image2.png)
15
8
 
16
- **English** | [中文](README.zh.md)
17
-
18
- Thinking levels, vision badges and request headers for **third-party models** in DeepSeek Harness — a provider → model picker with capability icons inside the official composer menu, a one-switch thinking gate plus auto-adapted levels inside the official Models page card, and per-route request headers. A renamed derivative of [dsh-better-reasoning-effort](https://github.com/HaoyueQin/dsh-better-reasoning-effort) — see [Acknowledgements](#acknowledgements).
19
-
20
- <p align="center">
21
- <img src="docs/demo.svg" alt="demo" width="640">
22
- </p>
23
-
24
- <p align="center">
25
- <img src="assets/models-page-effort-editor.png" alt="The thinking-effort editor injected into a model row on the official Models page" width="720">
26
- </p>
27
-
28
- ## Why
29
-
30
- The `llm-pi-ai` adapter natively supports per-model `reasoningEfforts` and `input` declarations, but the official Models page editor deliberately keeps both fields out of reach. As a result, third-party models get **no thinking-level picker** in the composer, only the official DeepSeek API can set reasoning effort, hand-declared models are treated as **text-only**, and configuring any of this meant hand-writing `settings.yaml` blocks. This plugin brings both configuration surfaces back into the UI: edit inside the official model editor card, plus one-click auto-adapt.
31
-
32
- ## Features
33
-
34
- - **In-page editor** — a **Thinking** switch plus a reasoning-effort block appears under each model row's disclosure on the official Models page (levels, default effort, input modalities, endpoint compatibility), committing with the card's own **Save**; changes stay pending until then, and **Cancel** discards them with the card's fields. The switch sits **outside** that block and gates it: switched off, the model is written as not reasoning and the whole block is not rendered — one row is all an un-thinking model shows.
35
- - **Auto-adapt** — one click fills recommended levels, wire spellings and modalities from a built-in knowledge base (65 entries across 15 vendors — see [Supported models](docs/supported-models.md)), wire-protocol inference, and a same-origin probe of the provider's raw `/models` listing, every suggestion labeled by confidence; reference capacities show as read-only hints you copy yourself.
36
- - **Auto-fill** — models without a declaration are filled at boot and when added mid-session (opt out via `autofill: false` / `modalityAutofill: false`); explicit declarations, `false` and deliberately-unset markers are never touched.
37
- - **Three intents** — all levels off = unset (back to inheritance); only `off` = disable reasoning; levels armed = write the declaration.
38
- - **Composer model & reasoning rows** — the official model menu's body renders a **Model** row and a **Reasoning level** row. The model row opens a provider column with that provider's models beside it, every entry carrying vision / thinking badges and the current pick marked in accent colour (no tick, so it cannot be mistaken for a badge); the reasoning row opens the model's own level list. The official trigger stays untouched, picks are optimistic and roll back on refusal, the popover stays open after a model switch so the level can be chosen next, and model switches keep your level through a per-session memory chain (issue #4's per-model default effort outranks it across sessions).
39
- - **Composer model search** — a filter box above the official model list (provider / model / id tokens), always on.
40
- - **Per-model default effort** — a "Default effort" picker on each model row, stored in the settings document; every new session starts the model there.
41
- - **Request headers & `user-agent`** — a provider-card section edits the official `headers` field (masked, path-merged, Save-gated), and the plugin performs the `user-agent` override at the fetch layer per **provider route**, because the official adapter reserves that name. Routes that share one endpoint are told apart by their `baseURL` path and by the `model` carried in the request body, so each provider keeps its own identity; a request that identifies no single route falls back to the first declarer and the overlap is reported rather than guessed. Same-origin `/models` probes are covered.
42
- - **Defensive injection** — everything keys off the official page's DOM; if an official upgrade changes the structure, injection simply pauses and the official page is unaffected.
43
- - Bilingual copy (中文 / English).
44
-
45
- ## Supported models
46
-
47
- The auto-adapt knowledge base carries **65 curated entries across 15 vendors** (DeepSeek, OpenAI, Anthropic Claude, Gemini, Grok, Qwen, GLM, Kimi, Mistral, MiniMax, MiMo, Doubao, Hunyuan, Step, ERNIE — re-verified against official docs 2026-08/09, including vision-capable variants and no-effort-control families). The full table — match patterns, level → wire-spelling ladders, defaults, modalities, reference capacities — lives in **[docs/supported-models.md](docs/supported-models.md)** (generated from [`src/knowledge.ts`](src/knowledge.ts), the authoritative source). Unlisted models fall back to protocol inference + generic levels, adjustable by hand.
48
-
49
- ## Install
50
-
51
- Requires DeepSeek Harness **`0.1.5-alpha.1` or later** (per-line peer ranges; the `0.1.2-rc` / `0.1.3-alpha` lines are no longer supported — use plugin `0.3.7` there). Compiled and gated against `0.2.0-rc.1`. The per-kernel seam re-checks behind this live in [docs/compatibility-notes.md](docs/compatibility-notes.md).
52
-
53
- ```bash
54
- # from npm, under the dsh web profile
55
- dsh plugin --profile web add dsh-model-think-level
56
-
57
- # or from GitHub (source install; `lib/` builds via the prepare hook — the
58
- # installer prints the `allowBuilds` key it needs, follow that and re-add)
59
- dsh plugin --profile web add github:GaoHu1997/dsh-model-think-level
60
-
61
- # or link a local checkout for development
62
- npm install && npm run build
63
- dsh plugin --profile web add link:D:/Project/dsh-model-think-level
64
- ```
65
-
66
- Restart `dsh web` and hard-refresh the browser.
67
-
68
- ## Usage
69
-
70
- 1. Configure a third-party provider (API key etc.) on the official Models page.
71
- 2. Expand a model row: the **Thinking** switch sits above the effort block, and the block sits under the official capacity fields.
72
- - Leave **Thinking** on to configure levels. Switch it **off** and the model is saved as not reasoning — the whole effort block disappears with it, leaving just the switch row.
73
- - Check levels (off / minimal / low / medium / high / xhigh / max) and fill the wire values (e.g. give `high` the spelling `ultra`, and the gateway receives `ultra` when you pick High in the composer);
74
- - Toggle **Image input** under *Input modalities* to declare what the model accepts;
75
- - Click **Auto-adapt** to fill recommended levels and modalities — reference capacities show up as read-only hints you copy into the official fields yourself;
76
- - Any change is **pending** and lands when you press the card's own **Save**; **Cancel** (or a reload) discards it with the card's fields.
77
- 3. On a compatible protocol, the *Endpoint compatibility* section appears at the bottom — thinking budget field / vLLM priority on `openai-completions`, `max_output_tokens` handling on `openai-responses`.
78
- 4. All levels off + Save = unset the declaration; only `off` checked + Save = disable reasoning (`false`); *Clear declaration* + Save = back to inheriting the provider default.
79
- 5. In the composer, open the model menu: the **Model** row lists providers on the left and that provider's models on the right — each model carries an eye badge when it accepts images and a sparkle badge when it reasons — and the **Reasoning level** row lists the levels of the current model. Picking a model closes its list but keeps the menu open, so the level can be picked right after.
80
-
81
- Declared models are immediately selectable for reasoning effort in the composer, and image-declared models accept attachments end to end.
82
-
83
- ## Configuration
84
-
85
- Optional on the plugin's profile row (values shown are the defaults):
86
-
87
- ```yaml
88
- - insert:
89
- - id: dsh-model-think-level
90
- name: dsh-model-think-level
91
- config:
92
- autofill: true # auto-fill undeclared models at boot
93
- modalityAutofill: true # whether the boot fill also covers modalities
94
- probeTimeoutMs: 15000 # /models probe fetch timeout
95
- bootRetryDelaysMs: [1000, 2000, 4000, 8000, 16000, 30000]
96
- defaultGuard: true # map effort-less calls on forced-thinking
97
- # ladders to the vendor default
98
- ```
99
-
100
- ## How it works
101
-
102
- ```
103
- Browser (lib/client.js) Host (lib/index.js)
104
- ├─ DOM injector └─ Auto-fill
105
- │ MutationObserver on the models page settings/document-updated →
106
- │ → mounts EffortEditor in each invalidates the host cache;
107
- │ model row's disclosure browser idle pass fills models
108
- ├─ Composer injection
109
- │ MutationObserver on the document
110
- │ → ComposerSlider (root pane)
111
- │ → model search box (model-list pane)
112
- ├─ EffortEditor (React component) (knowledge base + inference)
113
- │ level checkboxes / wire values /
114
- │ input-modality toggle /
115
- │ auto-adapt (zoned suggestions) / committed with the card's Save
116
- │ └─ writes settings.mutate (llm-pi-ai)
117
- ```
118
-
119
- - `suggestEfforts()` in `src/knowledge.ts` is the knowledge base + inference engine — a pure function shared by host and browser.
120
- - `reconcile()` in `src/client/injection/models-page-editor.ts` locates model rows and mounts the editor; `src/client/index.ts` assembles the browser half, one module per seam in `src/client/injection/`.
121
- - `createEditorApi()` in `src/client/ops.ts` writes the declarations via `settings.mutate`, preserving every other row field and retrying once on a revision conflict.
122
-
123
- ## Development
124
-
125
- ```bash
126
- npm run typecheck # tsc strict check on src
127
- npm test # vitest: knowledge / inference / autofill / DOM injection / writing
128
- npm run build # lib/*.js + lib/client.js (module-loader bundle)
129
- ```
130
-
131
- Compiled and gated against the `0.2.0-rc.1` official packages; see [docs/compatibility-notes.md](docs/compatibility-notes.md) for the per-kernel records.
132
-
133
- ## Known limitations
134
-
135
- - Injection depends on the official Models page's DOM (aria-label/class); an official upgrade may pause injection until adapted — the official page is unaffected meanwhile.
136
- - The auto-adapt probe route answers **loopback and IP-literal hosts only** (the core `/api` Host-allowlist discipline without `trustedHosts`), and **never follows redirects** — a gateway listing its models only behind a 30x simply yields no endpoint evidence; Auto-adapt falls back to the knowledge base and protocol inference.
137
- - `reasoningEfforts` declarations are suggestions — what an endpoint actually accepts is up to its docs; tweak in the UI. The knowledge base is not exhaustive; families without an effort ladder carry no entry at all.
138
- - Endpoint-compatibility switches are never auto-filled by design: they describe a gateway, not a model.
139
- - The modality vocabulary follows pi-ai's core (`text` / `image` today); wider gateway support (PDF, audio, video) is recorded per family until the core vocabulary grows.
140
- - Name-heuristic modality advice (vision-flavored ids) is deliberately low-confidence and labeled as such.
141
- - Self-hosted relays: auto-fill pins `supportsDeveloperRole: false` on routes no official host claims (some upstreams reject the `developer` role); explicit values are never overwritten.
142
- - Forced-thinking models (ladders without `off`, e.g. GLM-5.3): effort-less calls map to the vendor default instead of sending `thinking: disabled` — set `defaultGuard: false` to restore raw behavior.
143
- - **Credentials inside `headers` are not redacted on disk**: the read-only view masks them, but the settings document still holds them in clear text — treat it like an API key.
144
- - **The request-header section's edit-state detection reads an unofficial signal** (the official row exposes no data attribute for its editor state); if an official build renames that class root, the section stops appearing — never breaking the page.
145
- - **Only one `user-agent` rewrite should be active**: sibling header plugins land on the same layer; the plugin detects and reports known ones, but the last writer on the wire wins.
146
- - The request-layer takeover relies on the official adapter creating a fresh SDK client per request — guarded by an end-to-end test that fails loudly if that changes.
147
-
148
- ## Acknowledgements
149
-
150
- This plugin is a renamed derivative of **[dsh-better-reasoning-effort](https://github.com/HaoyueQin/dsh-better-reasoning-effort) by [HaoyueQin](https://github.com/HaoyueQin)** (MIT). The request-header layer, the model knowledge base, the auto-adapt / auto-fill pipeline and the Models-page editor all come from that project — thank you for the original work. What changed here: the composer's effort slider was replaced by a provider → model picker plus a reasoning-level list, model entries gained vision / thinking badges, the selection tick was dropped in favour of an accent colour, and the per-model editor gained the master **Thinking** switch that gates it.
151
-
152
- That project's own composer slider was **adapted from [dsh-reasoning-effort](https://github.com/HanaAyane/dsh-reasoning-effort) by [HanaAyane](https://github.com/HanaAyane)** (MIT); it is no longer shipped here, but the credit stands for the lineage. If you used the upstream plugin, remove it to avoid two effort controls on the same seat:
153
-
154
- ```bash
155
- dsh plugin --profile web remove dsh-reasoning-effort
156
- ```
157
-
158
- ## Activity
159
-
160
- [![GaoHu1997/dsh-model-think-level GitStock K-Line Chart](https://gitstock.org/GaoHu1997/dsh-model-think-level/stock.svg)](https://gitstock.org/GaoHu1997/dsh-model-think-level/stock.svg)
161
-
162
- ## License
163
-
164
- MIT
9
+ 供应商配置
10
+ ![alt text](./docs/image3.png)
11
+ ![alt text](./docs/image4.png)
12
+ ![alt text](./docs/image5.png)
@@ -14,8 +14,8 @@
14
14
  */
15
15
  import type { ReactNode } from 'react';
16
16
  import type { CompatSuggestion, InputModalities, ReasoningEfforts } from '../knowledge.js';
17
- import { type DraftLevels } from './effort.js';
18
- import type { EffortEditorApi } from './types.js';
17
+ import { type DraftLevels, type EffortIntent } from './effort.js';
18
+ import type { EffortEditorApi, EffortWriteIntent, PendingWrite, PendingWriteParts } from './types.js';
19
19
  /** One model row's data the editor needs. */
20
20
  export interface EffortModel {
21
21
  /** Model id (the settings key). */
@@ -77,6 +77,17 @@ export interface EffortEditorProps {
77
77
  /** Localized copy. */
78
78
  t: (key: string, params?: Record<string, string | number>) => string;
79
79
  }
80
+ /** Draft state of the modality part: declared or inheriting, image or not. */
81
+ export interface DraftModality {
82
+ declared: boolean;
83
+ image: boolean;
84
+ }
85
+ /** Read the stored declaration into a draft. Empty mirrors undefined: the
86
+ * resolved settings layer materializes absent arrays as [], and core reads
87
+ * that as "no answer here" -- never as a phantom text-only declaration. */
88
+ export declare function modalityFrom(input: InputModalities | undefined): DraftModality;
89
+ /** Resolve a modality draft to the write intent (null = durably unset). */
90
+ export declare function buildModalityIntent(draft: DraftModality): InputModalities | null;
80
91
  /**
81
92
  * The compat fields this editor can SET, per protocol — which is also the set
82
93
  * it may clear. A key from neither list survives an apply untouched: a
@@ -103,6 +114,47 @@ export declare function clearedCompatKeys(routeApi: string | undefined, draft: C
103
114
  export declare function compatClearIntent(routeApi: string | undefined, writeCompat: CompatSuggestion | undefined): {
104
115
  clearCompatKeys?: readonly string[];
105
116
  };
117
+ /** Semantic equality between a modality draft and a stored declaration. */
118
+ export declare function sameModality(draft: DraftModality, stored: InputModalities | undefined): boolean;
119
+ /**
120
+ * The ladder part of a write, from a switch state and the draft it resolves to.
121
+ *
122
+ * `'keep'` when nothing is armed and nothing is stored: that is a no-op, NOT an
123
+ * unset — an undefined there would stamp the durable unset marker onto a
124
+ * never-declared model and silence host auto-fill.
125
+ * @param nextThinking - the (pre-update) switch state the edit resolved to.
126
+ * @param built - `buildIntent` of the draft the edit resolved to, which is the
127
+ * explicit `false` when the only armed level is 'off'.
128
+ * @param initialEfforts - the row's stored declaration.
129
+ * @returns the ladder intent to write.
130
+ */
131
+ export declare function ladderIntent(nextThinking: boolean, built: EffortIntent, initialEfforts: EffortWriteIntent | undefined): EffortWriteIntent;
132
+ /**
133
+ * The compat draft a FRESH editor on this row would hold.
134
+ *
135
+ * The editor's three compat inputs are seeded from the stored block, so an
136
+ * untouched editor's draft is exactly the stored block restricted to the three
137
+ * fields it shows. The injector's provider-wide adapt has no editor to ask for
138
+ * a draft, so it derives the same value here: a hand-tuned field the UI does
139
+ * not show stays untouchable, and one it does show survives the write.
140
+ * @param compat - the row's stored compat block.
141
+ * @returns the draft, or undefined when it holds no field at all.
142
+ */
143
+ export declare function compatDraftOf(compat: CompatSuggestion | undefined): CompatSuggestion | undefined;
144
+ /**
145
+ * Assemble one complete pending write from the parts a caller resolved: the
146
+ * compat merge (the applied suggestion's block with the caller's own fields on
147
+ * top), the thinking-budget default an openai-completions endpoint needs, and
148
+ * the clear list for the fields this write OWNS and left empty.
149
+ *
150
+ * The single implementation of that assembly: the row's own editor reports the
151
+ * user's edits through it, and the injector's provider-wide adapt reports
152
+ * collapsed rows through it, so the same suggestion always writes the same
153
+ * bytes whichever path applied it.
154
+ * @param parts - the resolved parts (see {@link PendingWriteParts}).
155
+ * @returns the write to hand the injector's ledger.
156
+ */
157
+ export declare function pendingWriteOf(parts: PendingWriteParts): PendingWrite;
106
158
  /**
107
159
  * Render one model's thinking-effort + input-modality editor: the level
108
160
  * checkboxes, the modality toggle, the auto-adapt action, and the
@@ -0,0 +1,48 @@
1
+ /**
2
+ * The API-key list: one provider's keys, which one is in use, and the one
3
+ * inline form that adds, relabels or replaces a key.
4
+ *
5
+ * Rendered inline under the official key field (see `injection/key-manager`),
6
+ * not as a floating panel: the field's label already says "API key", so its list
7
+ * belongs directly beneath it, inside the card that owns the profile — the list
8
+ * reads as part of the field rather than as a surface that has to be opened,
9
+ * positioned and dismissed.
10
+ *
11
+ * The list is a view over host state and holds no truth of its own — every
12
+ * mutation answers with the whole list, and the list is adopted as it arrives
13
+ * (`adopt`), so the GUI can never drift from what the settings document and the
14
+ * credential store actually say. Three facts it deliberately reflects:
15
+ *
16
+ * - **the key in use is the profile's reference.** Choosing a row writes
17
+ * `providers.<route>.apiKeyEnv`, so it holds for every request the route
18
+ * makes, not only the ones this GUI starts. Exactly one key is in use per
19
+ * provider, which is why the mark at the head of a row is a radio.
20
+ * - **a value is shown masked, or not at all.** The host reads the stored
21
+ * secret and answers a head-and-tail preview; the middle never leaves it,
22
+ * and the eye beside the field stays the one gesture that reveals a value.
23
+ * - **one form serves all three edits.** Adding, relabelling and replacing a
24
+ * value differ only in which fields it shows, so they share a form rendered
25
+ * as the row below the row they act on — for an add, the list's last row.
26
+ *
27
+ * @module dsh-model-think-level/client/KeyManager
28
+ */
29
+ import type { ReactNode } from 'react';
30
+ import { type KeyManagerClient } from './key-manager-client.js';
31
+ /** The repo's inline translator shape. */
32
+ type Translate = (key: string, params?: Record<string, string | number>) => string;
33
+ /** Props of {@link KeyManager}. */
34
+ export interface KeyManagerProps {
35
+ /** The provider route whose keys this list manages. */
36
+ readonly route: string;
37
+ /** Copy for the list's labels. */
38
+ readonly t: Translate;
39
+ /** Transport; injectable for tests. */
40
+ readonly client?: KeyManagerClient | undefined;
41
+ }
42
+ /**
43
+ * Render the key list for one provider route.
44
+ * @param props - see {@link KeyManagerProps}.
45
+ * @returns the list's root element.
46
+ */
47
+ export declare function KeyManager(props: KeyManagerProps): ReactNode;
48
+ export {};
@@ -0,0 +1,35 @@
1
+ /**
2
+ * The "adapt every model of this provider" request (user request ⑤).
3
+ *
4
+ * The seat beside the official fetch-models link and the per-row reasoning
5
+ * editor are two DIFFERENT React roots: the seat is raw DOM this plugin writes
6
+ * into the official catalogue head, while the editor is a root the injector
7
+ * mounts into each model row's disclosure. They share no props, so the request
8
+ * travels as one document event, and every editor whose own route matches
9
+ * answers it by running the very same `autoAdapt()` its own 自动适配 link runs.
10
+ *
11
+ * That indirection is the point: adapting N models this way is byte-identical
12
+ * to clicking that link N times, so the seat cannot drift from the control the
13
+ * user already trusts — and it still reaches the models whose thinking switch
14
+ * is OFF, which the link itself cannot (with the switch off the whole effort
15
+ * card, link included, is not rendered at all).
16
+ *
17
+ * @module dsh-model-think-level/client/auto-effort
18
+ */
19
+ /** The event a seat dispatches and every mounted editor listens for. */
20
+ export declare const AUTO_EFFORT_EVENT = "bre:auto-effort";
21
+ /** The event's payload: the provider route whose models are to be adapted. */
22
+ export interface AutoEffortDetail {
23
+ /** The provider route the request belongs to. */
24
+ readonly route: string;
25
+ }
26
+ /** Run one auto-adapt after every earlier one settled. */
27
+ export declare function queueAutoEffort(run: () => Promise<void> | void): void;
28
+ /** Ask every editor of `route` on this document to adapt its own model. */
29
+ export declare function requestAutoEffort(route: string, target?: Document): void;
30
+ /**
31
+ * Answer this document's auto-adapt requests until the returned disposer runs.
32
+ * A request naming another route is ignored — one card is open at a time, but a
33
+ * re-render can leave a previous card's rows mounted for a scan.
34
+ */
35
+ export declare function listenAutoEffort(handler: (route: string) => void, target?: Document): () => void;
@@ -0,0 +1,50 @@
1
+ /**
2
+ * The "adapt every model of this provider" seat, beside the official
3
+ * fetch-models link (user request ⑤).
4
+ *
5
+ * The official catalogue head carries exactly one control — the link that
6
+ * fetches the provider's model list — so a provider with twenty models makes
7
+ * the user open twenty rows and click 自动适配 in each. The seat stands next to
8
+ * that link and asks every model of the provider to adapt at once.
9
+ *
10
+ * It is raw DOM inside the host's own head for the same reason the provider
11
+ * delete seat is: the plugin owns no slot there. It never moves a React-owned
12
+ * node — it only inserts itself as the anchor's next sibling and re-inserts
13
+ * itself when a re-render drops it — and every write is compare-before-write,
14
+ * so a settled pass emits no mutation of its own.
15
+ *
16
+ * The click does not write anything itself: it publishes a request on the
17
+ * document (see `../auto-effort.js`), which the per-model editors answer for
18
+ * their own rows, and hands the route to `deps.onRequest`, which answers for
19
+ * the models that have no editor on screen -- a collapsed row renders no
20
+ * disclosure container, so nothing else on the page can serve it. The seat
21
+ * therefore holds no write path of its own, and neither answer stages a
22
+ * document the open card is already holding.
23
+ *
24
+ * @module dsh-model-think-level/client/injection/auto-effort-seat
25
+ */
26
+ /** One card's catalogue, paired with the route that card edits. */
27
+ export interface AutoEffortSeatTarget {
28
+ /** The card's catalogue section. */
29
+ readonly catalogue: HTMLElement;
30
+ /** The provider route the card edits. */
31
+ readonly route: string;
32
+ }
33
+ export interface AutoEffortSeatDeps {
34
+ /** The plugin's own translator, for the label and the tooltip. */
35
+ t: (key: string, params?: Record<string, string | number>) => string;
36
+ /**
37
+ * Adapt every model of the route that has no on-screen editor: the injector
38
+ * owns the settings writes, so the seat only reports the click. Absent in
39
+ * tests that only care where the seat is placed.
40
+ */
41
+ onRequest?: (route: string) => void;
42
+ }
43
+ /**
44
+ * Give every target's catalogue head an auto-adapt seat. Idempotent: a head
45
+ * that already has one is only relabelled and re-seated, and a seat whose
46
+ * catalogue this pass no longer names is taken out.
47
+ */
48
+ export declare function reconcileAutoEffortSeats(root: HTMLElement, targets: readonly AutoEffortSeatTarget[], deps: AutoEffortSeatDeps): void;
49
+ /** Take every seat out again (plugin dispose, HMR teardown). */
50
+ export declare function teardownAutoEffortSeats(): void;
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Compare-before-write DOM helpers.
3
+ *
4
+ * Every write in the injection layer goes through one of these. An unchanged
5
+ * `setAttribute` still fires a mutation record, and the passes listen for
6
+ * mutations on the whole panel, so a blind write would have the scan chasing
7
+ * its own tail: mark → record → rescan → mark. Comparing first is what makes
8
+ * a settled page produce no mutations at all.
9
+ *
10
+ * @module dsh-model-think-level/client/injection/dom
11
+ */
12
+ /** setAttribute only when the value would change. */
13
+ export declare function setAttr(el: Element, name: string, value: string): void;
14
+ /** `hidden` is a property, not an attribute these passes can compare textually. */
15
+ export declare function setHidden(el: HTMLElement | undefined, hidden: boolean): void;
16
+ /** Class writes compare first, like {@link setAttr}. `classList.add` is a
17
+ * no-op for a token it already holds in a spec-conforming engine, but it is
18
+ * not one in jsdom — and this helper's promise ("a settled page writes
19
+ * nothing") has to hold in both, since a write here feeds the same scan. */
20
+ export declare function setClass(el: HTMLElement, name: string, on: boolean): void;
21
+ /** Write text only when it differs, for the same reason as {@link setAttr}. */
22
+ export declare function setText(el: HTMLElement | undefined, text: string): void;
@@ -0,0 +1,33 @@
1
+ /**
2
+ * The provider editor's segmented control (供应商配置 / 模型配置 / 高级配置).
3
+ *
4
+ * This is the bar the editor-tabs pass drives. It lives HERE, in the plugin's
5
+ * own React subtree, rather than being built by the pass and inserted into the
6
+ * official card: the card's children belong to React, and a foreign node in
7
+ * that list survives only until the next render. The component is rendered
8
+ * inside the provider-card slot's own mount — a child of the card the slot
9
+ * already resolves — and the stylesheet reveals it as soon as the pass marks
10
+ * the card (`bre-tabbed`), which happens in the same mutation batch as the
11
+ * official render.
12
+ *
13
+ * The component renders STRUCTURE only: it holds no state, because the card's
14
+ * `data-bre-tab` attribute is the source of truth for both the pane switch
15
+ * (pure CSS) and the selected button. The pass owns the copy, the per-pane
16
+ * availability and `aria-selected`, so a language switch relabels the bar
17
+ * without a re-render and a pane that is not on the card hides its own button.
18
+ *
19
+ * @module dsh-model-think-level/client/injection/editor-tab-bar
20
+ */
21
+ import type { ReactNode } from 'react';
22
+ import type { Translate } from '@deepseek-ai/dsh-client-ui-slots';
23
+ export interface EditorTabBarProps {
24
+ /** The plugin's own mount inside the card this bar switches. It hangs
25
+ * directly off the card, which is how the card is resolved on click. */
26
+ host: {
27
+ readonly current: HTMLDivElement | null;
28
+ };
29
+ /** The plugin's translator. */
30
+ t: Translate;
31
+ }
32
+ /** One provider editor's tab bar. */
33
+ export declare function EditorTabBar({ host, t }: EditorTabBarProps): ReactNode;
@@ -0,0 +1,94 @@
1
+ /**
2
+ * The provider editor's three-tab takeover for the official Models settings
3
+ * page.
4
+ *
5
+ * The official editor presents one provider as a single long column — identity
6
+ * fields, then the model list, then (one section further up the card) this
7
+ * plugin's request-header editor — and the column keeps growing with every
8
+ * feature. This pass re-presents the same content as three tabs:
9
+ *
10
+ * `provider` — the official identity fields (display name, key, base URL,
11
+ * protocol); the editor's own header and action row stay put.
12
+ * `models` — the official model list, the rows the effort editor seats
13
+ * into.
14
+ * `advanced` — the plugin's request-header section, which the provider-card
15
+ * slot already mounts and which owns its own save/cancel.
16
+ *
17
+ * The takeover is pure CSS, by the same iron rule every pass here follows:
18
+ * React owns the official children, so none of them move. The card carries the
19
+ * active tab (`data-bre-tab`), each region carries its tab membership
20
+ * (`data-bre-region`), and the stylesheet — not this module — decides what is
21
+ * visible.
22
+ *
23
+ * The bar is NOT part of that DOM, and this module never inserts it. It is
24
+ * React-rendered by {@link EditorTabBar} inside the plugin's own request-header
25
+ * mount, which the provider-card slot already guarantees is a child of the
26
+ * card; the stylesheet reveals it the moment the card carries the takeover
27
+ * class. An earlier revision inserted the bar straight into the official child
28
+ * list from here, which made it a foreign node in a React-reconciled list: it
29
+ * was at the mercy of the next render, and the insertion itself needed the
30
+ * headers mount to be a DIRECT child of the card — an anchor this module cannot
31
+ * verify and which threw (aborting the whole scan) when it did not hold. Owning
32
+ * the bar inside our own subtree removes both hazards: the pass below only ever
33
+ * writes attributes, and the slot component publishes the card's open state the
34
+ * same way.
35
+ *
36
+ * Everything degrades to the official page: a card whose structure this pass
37
+ * cannot recognise (host redesign, partial render) is left exactly as the
38
+ * official page built it, a card whose takeover throws is rolled back the same
39
+ * way, and every attribute write is compared before it lands so the scan never
40
+ * feeds the MutationObserver its own tail.
41
+ *
42
+ * The same visit also dresses the provider's API-key field with its reveal eye
43
+ * ({@link revealKeyField}) and its key controls ({@link enhanceKeyManagerField}),
44
+ * because that field's shape is decided by exactly the same classification and
45
+ * its affordances have to come and go with the takeover.
46
+ *
47
+ * @module dsh-model-think-level/client/injection/editor-tabs
48
+ */
49
+ declare const TAB_IDS: readonly ["provider", "models", "advanced"];
50
+ /** A tab id, as both the bar and the card attribute spell it. */
51
+ export type EditorTabId = (typeof TAB_IDS)[number];
52
+ /** The tab ids, in display order, for the tab-bar component. */
53
+ export declare const EDITOR_TAB_IDS: readonly EditorTabId[];
54
+ /** The bar's class, a button's class and a button's tab-id attribute, exported
55
+ * so the component that renders the bar and the pass that labels it spell the
56
+ * contract once. */
57
+ export declare const EDITOR_TABS_CLASS = "bre-editor-tabs";
58
+ export declare const EDITOR_TAB_BUTTON_CLASS = "bre-editor-tab";
59
+ export declare const EDITOR_TAB_ID_ATTR = "data-bre-tab-id";
60
+ /** Per-scan bookkeeping (one instance lives in `models-page.ts`). */
61
+ export interface EditorTabsState {
62
+ /** Cards whose takeover threw and had to be rolled back. A card that cannot
63
+ * be taken over must not repeat its complaint on every scan. */
64
+ readonly warned: WeakSet<Element>;
65
+ }
66
+ export interface EditorTabsDeps {
67
+ /** The plugin's own translator, for the tab labels. */
68
+ t: (key: string, params?: Record<string, string | number>) => string;
69
+ }
70
+ export declare function createEditorTabsState(): EditorTabsState;
71
+ /**
72
+ * The card a plugin mount sits in. Both the slot component (publishing the
73
+ * card's open state, switching tabs) and this pass resolve the card through
74
+ * this one rule, so a card can never be taken over by one and addressed by the
75
+ * other.
76
+ */
77
+ export declare function editorCardOf(element: Element | null): HTMLElement | null;
78
+ /**
79
+ * Switch one card to a tab. The card attribute is the single source of truth:
80
+ * the stylesheet swaps the pane on the attribute alone, and the bar's buttons
81
+ * follow it here.
82
+ */
83
+ export declare function selectEditorTab(card: HTMLElement, id: EditorTabId): void;
84
+ /**
85
+ * Visit every open provider editor and make sure it presents as tabs.
86
+ * Idempotent: a card that already carries the takeover is only relabelled,
87
+ * which is also how a language switch reaches the page. Cards the pass no
88
+ * longer recognises as open fall back to the official one-column layout.
89
+ */
90
+ export declare function reconcileEditorTabs(root: HTMLElement, deps: EditorTabsDeps, state: EditorTabsState): void;
91
+ /** Remove every takeover mark. The bar is the tab-bar component's own DOM and
92
+ * is hidden, not removed, by dropping the class it is revealed under. */
93
+ export declare function teardownEditorTabs(): void;
94
+ export {};
@@ -0,0 +1,20 @@
1
+ /**
2
+ * The plugin's own outline glyphs.
3
+ *
4
+ * Shared because two injected controls draw one: the API-key eye, which swaps
5
+ * between two glyphs by attribute, and the key field's alias chip, which is
6
+ * icon-only so that it does not repeat the official "API key" label with the
7
+ * word for the same thing. Every glyph is a stroke-only outline on the same
8
+ * 16px grid, inherits `currentColor`, and is hidden from assistive tech — the
9
+ * button holding it carries the meaning in its own `aria-label`.
10
+ *
11
+ * @module dsh-model-think-level/client/injection/icon
12
+ */
13
+ /** The namespace every SVG node here is created in. */
14
+ export declare const SVG_NS = "http://www.w3.org/2000/svg";
15
+ /** Marks a glyph, so the stylesheet can show one of several, and a sweep find it. */
16
+ export declare const ICON_ATTR = "data-bre-icon";
17
+ /** One outline path of a glyph, in the 16px viewBox. */
18
+ export declare function svgPath(d: string): SVGPathElement;
19
+ /** A stroke-only 16px glyph built from outline paths. */
20
+ export declare function svgGlyph(kind: string, paths: readonly string[]): SVGElement;
@@ -0,0 +1,64 @@
1
+ /**
2
+ * The key list's seat: the official key field grows a plugin-owned host, and the
3
+ * list is rendered inline into it, directly under the input.
4
+ *
5
+ * The field is not reachable from the settings page's React tree — the plugin
6
+ * only gets keyed slots, and none of them sits beside a form field — so the seat
7
+ * is a DOM leaf inserted directly AFTER the field. The list then falls under the
8
+ * input, which is where a provider's keys belong: the label above already says
9
+ * "API key", and the list reads as part of the field rather than as a surface
10
+ * that has to be opened, positioned and dismissed.
11
+ *
12
+ * A sibling rather than a last child: the reveal eye hangs on the field's own
13
+ * bottom edge (`bottom: 0` against the field, which its validation-message rule
14
+ * also measures from), so a list inside the field would carry the eye down with
15
+ * it and leave it sitting on the list's last row. React tolerates the foreign
16
+ * sibling — it resolves a node's next sibling by walking past nodes it does not
17
+ * own — and the list mirrors the field's region, so a pane hides it along with
18
+ * the field it belongs to.
19
+ *
20
+ * The host carries the route its list was built for — that attribute is how the
21
+ * pass knows whether the mounted list still belongs to the field it follows, and
22
+ * how a test asks what is mounted there.
23
+ *
24
+ * The whole seat is skipped when the field is not a key field, or when its card
25
+ * announces no route: the add-provider form has no route yet, and keys belong to
26
+ * a route that exists.
27
+ *
28
+ * @module dsh-model-think-level/client/injection/key-manager
29
+ */
30
+ import type { KeyManagerClient } from '../key-manager-client.js';
31
+ /** Set on a field whose keys the plugin lists. */
32
+ export declare const KEY_MANAGED_CLASS = "bre-key-managed";
33
+ /** The repo's inline translator shape. */
34
+ export type Translator = (key: string, params?: Record<string, string | number>) => string;
35
+ /** What a reconcile tells the field's key list. */
36
+ export interface KeyManagerFieldDeps {
37
+ /** Copy for the list and its form. */
38
+ readonly t: Translator;
39
+ /** The provider route this card edits; absent on the add-provider form. */
40
+ readonly route?: string | undefined;
41
+ /** List transport; injectable for tests. */
42
+ readonly client?: KeyManagerClient | undefined;
43
+ }
44
+ /**
45
+ * Mount the key list into a field, replacing whatever this module mounted there
46
+ * before. Idempotent: a field whose list is already up and current is left
47
+ * exactly as it is, so a settled page sees no write at all.
48
+ */
49
+ export declare function enhanceKeyManagerField(field: HTMLElement, deps: KeyManagerFieldDeps): void;
50
+ /**
51
+ * Drop the list of one field, leaving the official field exactly as the page
52
+ * rendered it. Called when the takeover ends, and for the stray sweep of a page
53
+ * that is being torn down.
54
+ */
55
+ export declare function resetKeyManagerField(field: HTMLElement): void;
56
+ /** Drop every list this module mounted. */
57
+ export declare function teardownKeyManager(): void;
58
+ /**
59
+ * The route a field's list was built for, or undefined when it has none.
60
+ *
61
+ * A test hook: the route lives on the host, so what the pass decided can be read
62
+ * back from the DOM alone.
63
+ */
64
+ export declare function keyManagerRoute(field: HTMLElement): string | undefined;