dsh-model-think-level 1.0.0 → 1.0.1
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 +21 -4
- package/lib/client/EffortEditor.d.ts +54 -2
- package/lib/client/KeyManager.d.ts +48 -0
- package/lib/client/auto-effort.d.ts +35 -0
- package/lib/client/injection/auto-effort-seat.d.ts +50 -0
- package/lib/client/injection/dom.d.ts +22 -0
- package/lib/client/injection/editor-tab-bar.d.ts +33 -0
- package/lib/client/injection/editor-tabs.d.ts +94 -0
- package/lib/client/injection/icon.d.ts +20 -0
- package/lib/client/injection/key-manager.d.ts +64 -0
- package/lib/client/injection/key-reveal.d.ts +84 -0
- package/lib/client/injection/models-page-editor.d.ts +30 -0
- package/lib/client/injection/provider-card-slot.d.ts +9 -4
- package/lib/client/injection/provider-delete-seat.d.ts +38 -0
- package/lib/client/key-manager-client.d.ts +71 -0
- package/lib/client/locales.d.ts +35 -0
- package/lib/client/styles.d.ts +1 -1
- package/lib/client/types.d.ts +31 -0
- package/lib/client.js +552 -4
- package/lib/client.js.map +4 -4
- package/lib/constants.d.ts +34 -0
- package/lib/headers-pi-ai.d.ts +54 -0
- package/lib/index.js +785 -13
- package/lib/index.js.map +4 -4
- package/lib/key-index.d.ts +91 -0
- package/lib/key-refs.d.ts +81 -0
- package/package.json +4 -1
package/README.md
CHANGED
|
@@ -32,13 +32,16 @@ The `llm-pi-ai` adapter natively supports per-model `reasoningEfforts` and `inpu
|
|
|
32
32
|
## Features
|
|
33
33
|
|
|
34
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.
|
|
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. Beside the official **Get available models** link at the head of a provider's model list sits the plugin's own **Auto-detect thinking levels** (自动获取思考等级): one click runs that same adaptation over **every model of that provider at once**, so a newly added gateway does not need one click per row. It is deliberately the per-model action repeated, not a second write path — each mounted row runs its own auto-adapt and commits through the card's own pending state — so the result is byte-identical to clicking each row in turn, a read-only row or a save already in flight is skipped rather than raced, and the passes are queued one after another because each one reads the provider's own model list.
|
|
36
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
37
|
- **Three intents** — all levels off = unset (back to inheritance); only `off` = disable reasoning; levels armed = write the declaration.
|
|
38
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
39
|
- **Composer model search** — a filter box above the official model list (provider / model / id tokens), always on.
|
|
40
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
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
|
+
- **Tabbed provider editor** — the provider card's one long column (identity fields, model list, request headers) is re-presented as three tabs — **Provider / Models / Advanced** (供应商配置 / 模型配置 / 高级配置) — with a segmented control that the plugin renders inside its own request-header section, above the panes. The official fields never move: each region is tagged and the visibility is decided in CSS, so the card's own Save/Cancel row stays put and an official redesign degrades back to the single-column editor untouched. The official "Customized" disclosure that folds the identity fields and the model list together is sorted into its panes and stays expanded for as long as the tabs are in charge. The editor's own header — the provider name and route tag printed at the top of the pane — repeats the card row sitting directly above it, so it is hidden on every tab; and the Models pane drops both of its hairlines — the "Customized" group's separator and the catalogue's own — together with every top padding that held content off them. Neither has anything to divide here: the identity fields the group's rule separated it from are not in this pane, and the tab bar above already ends the pane. What is left runs from the tab bar into the catalogue with only the editor's own 14px gap between blocks — no doubled line, and no blank space where a line used to be. The tags land in the same frame the official editor is committed in, so opening a card shows the tabbed editor directly — never the single column first, re-sorting itself a moment later. The same visit dresses the API-key field with a trailing **eye**: one click unmasks what is being typed or pasted just now, and on a saved provider it **fetches the stored key and puts it back into the field**. The eye belongs to that input, so both step aside on the cards the key manager takes over: the list below is where a key gets written there, and two ways to write one key on one card read as a conflict. The official page is write-only by design — the settings document carries only the profile's `apiKeyEnv` reference and the credential service never returns a value — so the plugin's own host route (`GET /dsh-model-think-level/provider-key?route=<route>`, the same trust fence as the raw-models probe) resolves it for the page, uncached and never logged. The read happens only on that click and only while the field is empty: a draft you typed is never overwritten, hiding gives a borrowed key back (an empty field is exactly what the official form reads as "keep the stored key"), and a resolve that comes back empty leaves the field masked with the reason in the button's tooltip. The mask and the button both go when the takeover withdraws. The **Advanced** tab is never collapsible: it holds the request-header section alone now, so folding it would leave the pane empty — the section keeps its disclosure header only while other regions still share the pane, and renders as a plain heading on its own tab.
|
|
43
|
+
- **Per-provider key manager** — the API-key field grows the provider's whole key list **inline, directly beneath the field**: one bordered list whose rows are every key configured for that provider route, closed by a full-width **Add another key** row. There is no icon chip, no `+` button and no floating panel — no key glyph sitting beside the official "API key" label printing the same word twice, and no layer to position against the dialog's opaque mask. Each row is the key's name — the alias — followed by its **masked** value, or the masked value alone when the row was never named (the mask is composed host-side, because only the host can resolve a stored credential), and closes on the **enable** radio: one circle per row, one enabled key per provider, so picking a row is what puts that key in use. Hovering a row — or focusing into it — reveals exactly three actions and nothing else: **Delete** (asks twice), **Alias** (renames only) and **Change key** (replaces that row's stored value). All three, plus adding, use the one form that opens **below the affected row** — name on the left, `Paste API key` on the right, then **Cancel** and the confirm button — never a dialog, and the value never leaves the host except as a mask. Exactly one key per route is in use, and enabling one writes the profile's `apiKeyEnv` — the single reference the adapter resolves — so the key in use is the same one the official page, the composer and every other consumer of that profile use, not a plugin-local notion. Labels are plugin state: the credential store has no enumeration and no labels, so the list lives in `<harness home>/dsh-model-think-level/key-index.json` — references and labels only, never a value — kept beside the harness home rather than inside the pi-ai profile, because the official add-provider card writes the whole profile at `providers.<route>`. Adding a key mints the next free reference server-side in one write (`ROUTE_API_KEY`, then `_2`, `_3`, …), stores the value **first** and lists it only once the store accepted it; removing the key in use hands the seat to the next listed key **before** the value is forgotten, and removing the last key clears the reference. A key whose value the store no longer holds stays listed and is flagged, so it can be re-added or removed. The list and its form are plugin-owned DOM in a host div appended as the key field's **next sibling** — the same anchored leaf the reveal eye is measured against — so they sit inside the dialog's own layout and need no z-index of their own; the form closes on **Cancel** or on a successful commit, and the host is swept on teardown, on a route change and when the pane leaves the provider tab.
|
|
44
|
+
- **Provider-list rows** — the official list of provider cards is dressed from the plugin side: each row carries an enable **switch** (switching a provider off greys its name and identity) and can be reordered by dragging its grip — the insertion line that says which side the row would land on is drawn above the settings dialog, so it is not swallowed by the overlay's opaque mask. Its action column also stays aligned, because the host renders a delete button only for a provider it allows removing — a built-in such as DeepSeek rendered none at all, so that row's remaining actions started further right than its neighbours'. The plugin fills that hole with a disabled **Delete** placeholder wearing the official row metrics (28px tall, the row's 12px label) at the host's own 40% disabled opacity, labelled with the host's own word for the action and tooltipped "a built-in provider cannot be deleted". The placeholder is handed back the moment the host renders its own button, and swept on teardown.
|
|
42
45
|
- **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
46
|
- Bilingual copy (中文 / English).
|
|
44
47
|
|
|
@@ -74,9 +77,12 @@ Restart `dsh web` and hard-refresh the browser.
|
|
|
74
77
|
- Toggle **Image input** under *Input modalities* to declare what the model accepts;
|
|
75
78
|
- 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
79
|
- 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.
|
|
78
|
-
4.
|
|
79
|
-
5.
|
|
80
|
+
3. Above the model list, beside the official **Get available models** link, **Auto-detect thinking levels** runs that same adaptation over **every model of the provider at once** — one click instead of one per row. Each mounted row still runs its own auto-adapt and commits through the card's own pending state, so the outcome is what clicking **Auto-adapt** on every row in turn would produce; the passes are queued, because each one reads the provider's own model list.
|
|
81
|
+
4. 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`.
|
|
82
|
+
5. All levels off + Save = unset the declaration; only `off` checked + Save = disable reasoning (`false`); *Clear declaration* + Save = back to inheriting the provider default.
|
|
83
|
+
6. 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.
|
|
84
|
+
|
|
85
|
+
7. The API-key field carries that provider's key list **inline**: the radio marks the key in use (the one the profile's `apiKeyEnv` points at), **Add another key** opens the form beneath the list, and hovering a row offers **Delete**, **Alias** and **Change key**.
|
|
80
86
|
|
|
81
87
|
Declared models are immediately selectable for reasoning effort in the composer, and image-declared models accept attachments end to end.
|
|
82
88
|
|
|
@@ -114,10 +120,20 @@ Browser (lib/client.js) Host (lib/index.js)
|
|
|
114
120
|
│ input-modality toggle /
|
|
115
121
|
│ auto-adapt (zoned suggestions) / committed with the card's Save
|
|
116
122
|
│ └─ writes settings.mutate (llm-pi-ai)
|
|
123
|
+
├─ Key manager (provider card)
|
|
124
|
+
│ inline key list under the key field; add / alias / change-value
|
|
125
|
+
│ forms open under the affected row (one key in use per route)
|
|
126
|
+
│ └─ host route: key index + credentials (enable = apiKeyEnv write)
|
|
127
|
+
├─ Auto-detect thinking levels (catalogue head)
|
|
128
|
+
│ a seat beside the official "Get available models" link → one
|
|
129
|
+
│ event → every mounted row of that provider runs its own auto-adapt
|
|
117
130
|
```
|
|
118
131
|
|
|
119
132
|
- `suggestEfforts()` in `src/knowledge.ts` is the knowledge base + inference engine — a pure function shared by host and browser.
|
|
120
133
|
- `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/`.
|
|
134
|
+
- `reconcileEditorTabs()` in `src/client/injection/editor-tabs.ts` re-presents the open editor card as three tabs (Provider / Models / Advanced) — it tags each region and stamps the card with the active tab; the visibility is decided in the stylesheet, so no official node ever moves. The bar is React-rendered by `src/client/injection/editor-tab-bar.ts` inside the plugin's own request-header mount, so it is plugin-owned DOM rather than a foreign node in the official child list; the pass only classifies, tags and labels it. The same visit calls `revealKeyField()` in `src/client/injection/key-reveal.ts`, which gives the API-key field its reveal eye — the one node this plugin appends to official DOM, a positioned leaf removed again on teardown. It unmasks a draft in place, and for a saved provider it asks the host for the stored credential through `PROVIDER_KEY_PATH` in `src/constants.ts` (registered in `src/index.ts` beside the raw-models probe, under the same trust fence and explicitly uncached), writing the answer into the official input through the prototype value setter plus a bubbling `input` event so the official component adopts it; a reveal is always borrowed, so hiding empties the field again — which is what the official form reads as "keep the stored key". The compare-before-write helpers shared by the tab pass and the key reveal live in `src/client/injection/dom.ts`.
|
|
135
|
+
- `enhanceKeyManagerField()` in `src/client/injection/key-manager.ts` owns the inline key list under the API-key field (the eye stays in `key-reveal.ts`) and the host div the list and its form mount into; while the list is mounted the field's own input is hidden by the stylesheet, because the list says the same thing once per key and both in one card read as a conflict — the input stays in the DOM, so the official form still owns it and unmanaging the field brings it back; the list itself is the React component in `src/client/KeyManager.tsx`, which talks to the host through `src/client/key-manager-client.ts` (`KEY_INDEX_PATH`) — `list`, `add`, `enable`, `rename`, `replace` and `remove`. The host route in `src/index.ts` reads and writes the plugin's index through `src/key-index.ts` and moves the "in use" seat with `settings.mutate(llm-pi-ai, { op: 'set' | 'unset', path: ['providers', route, 'apiKeyEnv'] })`, retrying once on a revision conflict; reference minting, validation and alias normalisation are shared with the browser half through `src/key-refs.ts`, which also composes the mask (`first4…last4`, or a fixed dot run below twelve characters) so no unmasked value ever crosses the host boundary. The injected leaf is compared against the field's next sibling before it is appended, so a settled pass emits no mutation and cannot retrigger the page observer.
|
|
136
|
+
- `reconcileAutoEffortSeats()` in `src/client/injection/auto-effort-seat.ts` renders the provider-wide seat into the open card's catalogue head — one raw-DOM button after the official **Get available models** link, matched by position because that link's own label swaps to a fetching state mid-request, and identified by `data-bre-auto-effort`, which carries the route. The click publishes `AUTO_EFFORT_EVENT` (`src/client/auto-effort.ts`) instead of writing anything: the seat holds no write path of its own, so `EffortEditor` listens, checks the route against its own, and queues its own `autoAdapt()` — the queue is a single promise chain, so passes run one at a time in arrival order and a failure never stops the next. The seat reconciles on every pass (idempotently — a settled pass emits no mutation), re-reads the route at click time so a retyped Provider ID is followed, and is swept both in the pass's own early-exit branches and on teardown.
|
|
121
137
|
- `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
138
|
|
|
123
139
|
## Development
|
|
@@ -144,6 +160,7 @@ Compiled and gated against the `0.2.0-rc.1` official packages; see [docs/compati
|
|
|
144
160
|
- **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
161
|
- **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
162
|
- 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.
|
|
163
|
+
- **The per-provider key list is plugin state** (`<harness home>/dsh-model-think-level/key-index.json`): references and labels only, never a value, but removing a key forgets its value from the credential store, and uninstalling the plugin leaves the file behind. If a key is renamed or enabled from another window while the card is open, reload it (the list reads when the card opens, not live).
|
|
147
164
|
|
|
148
165
|
## Acknowledgements
|
|
149
166
|
|
|
@@ -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;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The eye on a provider's API-key field.
|
|
3
|
+
*
|
|
4
|
+
* Two jobs, one control. The official input is `<input type="password">` whose
|
|
5
|
+
* value is React's own draft state (`keyDraft`), and a SAVED key is never part
|
|
6
|
+
* of it: a configured provider shows a placeholder instead, because the harness
|
|
7
|
+
* keeps provider keys write-only (the settings document carries only the
|
|
8
|
+
* profile's `apiKeyEnv` reference, and the credential service reports
|
|
9
|
+
* `{ configured }` — never the value). So on a settled card the eye has nothing
|
|
10
|
+
* to unmask, and the plugin asks its own host for the stored value
|
|
11
|
+
* ({@link loadProviderKey}, the `PROVIDER_KEY_PATH` route) when — and only
|
|
12
|
+
* when — the user clicks it with an empty field. A field holding a draft the
|
|
13
|
+
* user just typed is never touched: the fetch is skipped and the click only
|
|
14
|
+
* swaps the mask.
|
|
15
|
+
*
|
|
16
|
+
* The read is deliberately constrained:
|
|
17
|
+
*
|
|
18
|
+
* - it happens on an explicit click, never on render, and answers `undefined`
|
|
19
|
+
* for every failure (no route, refused fence, reference-free profile,
|
|
20
|
+
* unconfigured credential), which leaves the field masked;
|
|
21
|
+
* - it is never cached (the request says `no-store` and the route answers
|
|
22
|
+
* with the same header) and never logged;
|
|
23
|
+
* - hiding the field again takes the borrowed value back out — the draft
|
|
24
|
+
* returns to empty, which the official form reads as "keep the stored key",
|
|
25
|
+
* so revealing can never overwrite the credential by accident. A value the
|
|
26
|
+
* user edits after revealing is theirs, and is left alone.
|
|
27
|
+
*
|
|
28
|
+
* The button is the one node this plugin inserts into official DOM, and it is
|
|
29
|
+
* inserted deliberately: the affordance only reads as an eye at the END of the
|
|
30
|
+
* input if it sits inside the input's own box, and the official field has no
|
|
31
|
+
* wrapper to hang it on. The trade is contained —
|
|
32
|
+
*
|
|
33
|
+
* - the field itself is never restructured, and nothing official is moved or
|
|
34
|
+
* removed: the button is an absolutely positioned LEAF appended last;
|
|
35
|
+
* - React ignores trailing foreign children when it reconciles the field's
|
|
36
|
+
* known children, so the node survives every re-render (each keystroke
|
|
37
|
+
* re-renders this field);
|
|
38
|
+
* - React never writes the input's `type` back (its props do not change), so
|
|
39
|
+
* the revealed state survives too;
|
|
40
|
+
* - the pass removes the button, the marker and the class on cleanup, and
|
|
41
|
+
* restores the mask, so a withdrawn takeover leaves nothing behind.
|
|
42
|
+
*
|
|
43
|
+
* @module dsh-model-think-level/client/injection/key-reveal
|
|
44
|
+
*/
|
|
45
|
+
/** Set on the field that carries the key input. */
|
|
46
|
+
export declare const KEY_FIELD_CLASS = "bre-key-field";
|
|
47
|
+
/** The eye button inside that field. */
|
|
48
|
+
export declare const KEY_EYE_CLASS = "bre-key-eye";
|
|
49
|
+
/** The repo's inline translator shape (see `HeadersEditor`, `ComposerSlider`). */
|
|
50
|
+
export type Translator = (key: string, params?: Record<string, string | number>) => string;
|
|
51
|
+
/** What a reconcile tells the field's eye. */
|
|
52
|
+
export interface KeyRevealDeps {
|
|
53
|
+
/** Copy for the button's labels and its failure tooltip. */
|
|
54
|
+
readonly t: Translator;
|
|
55
|
+
/**
|
|
56
|
+
* The provider route this card edits, when the card announces one. Absent on
|
|
57
|
+
* the add-provider form (no route exists yet), which is what keeps the eye
|
|
58
|
+
* there a plain mask toggle.
|
|
59
|
+
*/
|
|
60
|
+
readonly route?: string | undefined;
|
|
61
|
+
/** How to resolve the stored credential; injectable for tests. */
|
|
62
|
+
readonly loadKey?: ((route: string) => Promise<string | undefined>) | undefined;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Give an editor field an eye at the end of its input, if it holds the API key.
|
|
66
|
+
*
|
|
67
|
+
* Runs on every reconcile of the card, so it only ever writes what differs; a
|
|
68
|
+
* field with no key input is handed to {@link resetKeyField} instead, which is
|
|
69
|
+
* how a takeover that has moved on (a provider whose key field is gone) drops
|
|
70
|
+
* the affordance it no longer needs.
|
|
71
|
+
*/
|
|
72
|
+
export declare function revealKeyField(field: HTMLElement, deps: KeyRevealDeps): void;
|
|
73
|
+
/** Undo {@link revealKeyField}: drop the button, the class, the marker, and
|
|
74
|
+
* re-mask the input, leaving the official field exactly as it was rendered. */
|
|
75
|
+
export declare function resetKeyField(field: HTMLElement): void;
|
|
76
|
+
/**
|
|
77
|
+
* Ask the host for the credential a provider profile names.
|
|
78
|
+
*
|
|
79
|
+
* Fails closed by design: every refusal — an unknown route, a profile with no
|
|
80
|
+
* reference, a credential that is not configured, a refused trust fence, a
|
|
81
|
+
* network error — is `undefined`, and the caller leaves the field masked rather
|
|
82
|
+
* than uncovering an empty box.
|
|
83
|
+
*/
|
|
84
|
+
export declare function loadProviderKey(route: string): Promise<string | undefined>;
|