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.
@@ -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>;
@@ -1,26 +1,50 @@
1
1
  /**
2
- * Locating the official composer model menu — the seat root's open popover.
2
+ * Locating the official composer model seat — the ONE surface this plugin may
3
+ * decorate with a provider prefix.
4
+ *
5
+ * Everything in this module is scoped to the composer card
6
+ * (`[data-composer-card]`, owned by InputBar) and, inside it, to the model
7
+ * seat's own slot anchor. That scope is not a convenience: the shell renders
8
+ * several `button[aria-haspopup="menu"]` elements (the settings sidebar's
9
+ * account button, the composer's permission selector, …), so a document-wide
10
+ * search hands back a button this plugin does not own and paints the provider
11
+ * prefix on a host surface's label. When the seat cannot be identified the
12
+ * finders return undefined — the prefix is simply not shown, never stamped on
13
+ * someone else's control.
3
14
  *
4
15
  * @module dsh-model-think-level/client/injection/model-menu
5
16
  */
6
17
  /**
7
18
  * The official composer model menu: the seat root's open popover.
8
19
  *
9
- * Shape-tolerant across kernels: 0.1.5 portals the menu to document.body
10
- * and links it from the seat trigger via aria-controls, while older kernels
11
- * render it inline right after the trigger. Both shapes keep
20
+ * Shape-tolerant across kernels: newer kernels portal the menu to
21
+ * document.body and link it from the seat trigger via aria-controls, while
22
+ * older kernels render it inline right after the trigger. Both shapes keep
12
23
  * aria-haspopup="menu" on the trigger and role="menu" on the menu, so the
13
- * controls link is the primary route and the sibling check stays as the
14
- * fallback. Trigger search stays scoped to the composer card, so other
15
- * seats' menus never match; no copy text is read, so every locale matches.
24
+ * controls link is the primary route and the sibling check stays the fallback.
25
+ *
26
+ * The search NEVER leaves the composer card. When no card is mounted there is
27
+ * no model seat, and answering with some other surface's menu is exactly the
28
+ * cross-surface confusion this scoping exists to prevent.
29
+ *
30
+ * @param doc - the document to search.
31
+ * @returns the open model menu, or undefined while it is closed.
16
32
  */
17
33
  export declare function findModelMenu(doc?: Document): HTMLElement | undefined;
18
34
  /**
19
35
  * The official composer model trigger.
20
36
  *
21
- * The trigger remains in the composer card while its menu is closed, so this
22
- * finder keeps the menu lookup's linked and inline checks for an open menu but
23
- * falls back to the card's model-menu trigger when the popover is absent.
37
+ * The trigger stays in the card while its menu is closed, so this finder keeps
38
+ * the menu lookup's linked and inline checks and then falls back to the seat
39
+ * the closed card still renders:
40
+ *
41
+ * - inside the seat's anchor, every candidate IS the seat;
42
+ * - without an anchor, the seat is the LAST menu trigger of the card, since
43
+ * the shell renders it at the end of the composer's trailing control row
44
+ * (after the tools row that hosts the permission selector).
45
+ *
46
+ * @param doc - the document to search.
47
+ * @returns the composer's model trigger, or undefined while no composer exists.
24
48
  */
25
49
  export declare function findModelTrigger(doc?: Document): HTMLButtonElement | undefined;
26
50
  /** The official composer model menu: the seat root's open popover. */
@@ -212,6 +212,13 @@ export interface ScanState {
212
212
  * dismissed without saving is dropped, exactly like the card's own fields.
213
213
  */
214
214
  committing: Set<string>;
215
+ /**
216
+ * Routes with a provider-wide adapt in flight (the auto-adapt seat's click).
217
+ * Nothing debounces that click: a second one arriving while the first pass is
218
+ * still describing and suggesting would re-ask every model and re-commit the
219
+ * same bytes, so the route is simply not re-entered until its pass ends.
220
+ */
221
+ adapting: Set<string>;
215
222
  /**
216
223
  * Whether the official action row has failed to yield usable buttons for
217
224
  * every card seen so far. When it has, the landing decision degrades to
@@ -374,6 +381,29 @@ export interface AutofillFootprint {
374
381
  * position (the label's trailing number IS the position).
375
382
  */
376
383
  export declare function inputValueByLabel(card: HTMLElement, labels: readonly string[]): string;
384
+ /**
385
+ * Adapt every model of one provider, the rows the user never unfolded
386
+ * included.
387
+ *
388
+ * The seat's click reaches the per-row editors through a document event, but
389
+ * an editor only exists for a row with a disclosure container, and a collapsed
390
+ * row renders none — so the visible answer is structurally incomplete. This
391
+ * routine answers from the settings DOCUMENT instead of the DOM: every model
392
+ * the provider declares goes through the same suggestion the row's own
393
+ * auto-adapt would have asked for, is mapped by the same pure functions
394
+ * ({@link ladderIntent}, {@link compatDraftOf}, {@link pendingWriteOf}) so both
395
+ * paths write identical bytes, and lands by the same rule — nothing is written
396
+ * while an official card holds the document; the write is held on the route
397
+ * and the card's own Save commits it (issue #7 / C2).
398
+ *
399
+ * The rows that ARE on screen keep answering for themselves: their editors
400
+ * hold the user's unsaved draft, which this document walk cannot see, and two
401
+ * writers for one row would let whichever ran last overwrite the other.
402
+ * @param state - mutable scan state: the adapting guard and the held ledger.
403
+ * @param deps - the injection dependencies.
404
+ * @param route - the provider route whose models are adapted.
405
+ */
406
+ export declare function adaptEveryModel(state: ScanState, deps: InjectorDeps, route: string): Promise<void>;
377
407
  /**
378
408
  * Scan the settings DOM for official model rows and reconcile the injected
379
409
  * editors. Idempotent: existing editors are left alone, new disclosures get
@@ -13,6 +13,11 @@
13
13
  * its directory row as owner props, so the route arrives on the component at
14
14
  * render time and one registration serves every card.
15
15
  *
16
+ * The mount also hosts the editor's tab bar ({@link EditorTabBar}) and is the
17
+ * element the editor-tabs pass resolves its card from, so the plugin's own
18
+ * subtree is what carries both the takeover's control and the state the
19
+ * stylesheet reveals the section on.
20
+ *
16
21
  * @module dsh-model-think-level/client/injection/provider-card-slot
17
22
  */
18
23
  import type { ReactNode } from 'react';
@@ -36,10 +41,10 @@ export type ProviderCardSlotProps = ProviderCardOccurrence & {
36
41
  t: Translate;
37
42
  };
38
43
  /**
39
- * The occurrence component: resolve this card's route, render the editor, and
40
- * publish the card's open/closed state so the section reveals itself with the
41
- * official editor. Exported for its own tests (the registration below is the
42
- * production path).
44
+ * The occurrence component: resolve this card's route, render the editor with
45
+ * its tab bar, and publish the card's open/closed state so the section reveals
46
+ * itself with the official editor. Exported for its own tests (the registration
47
+ * below is the production path).
43
48
  */
44
49
  export declare function ProviderCardSlot({ provider, api, t }: ProviderCardSlotProps): ReactNode;
45
50
  /**
@@ -0,0 +1,38 @@
1
+ /**
2
+ * The reserved delete seat on a provider row of the official Models page.
3
+ *
4
+ * The official row renders its destructive button only for a provider the host
5
+ * still considers removable (`row.removable`); a built-in provider such as
6
+ * deepseek renders nothing there at all. The action group is `margin-left: auto`,
7
+ * so the rows that keep the button are wider than the rows that do not and the
8
+ * whole column of buttons lines up raggedly against the card's right edge.
9
+ *
10
+ * The seat is therefore kept warm: a row the host left without a delete button
11
+ * gets a disabled one of the same metrics, which is enough for every row's
12
+ * actions to start at the same place. It is `disabled`, so it takes no click,
13
+ * no focus and no keyboard activation — the button is the alignment, not an
14
+ * action — and it still owns its gesture as far as the drag engine is concerned,
15
+ * because that engine refuses a drag beginning on any `button`.
16
+ *
17
+ * Its word is the host's own: the label is read off a delete button the page
18
+ * still renders, so the odd row out is not the one row written in another
19
+ * language. A page that renders none at all (every provider built in) falls back
20
+ * to this plugin's dictionary.
21
+ *
22
+ * @module dsh-model-think-level/client/injection/provider-delete-seat
23
+ */
24
+ import type { HostLabels } from './models-page-editor.js';
25
+ export interface ProviderDeleteDeps {
26
+ /** The plugin's own translator, for the tooltip and the fallback label. */
27
+ t: (key: string, params?: Record<string, string | number>) => string;
28
+ /** The official `settings.models` label sets, resolved fresh on every scan. */
29
+ labels: () => HostLabels;
30
+ }
31
+ /**
32
+ * Give every provider row the delete seat the host left out. Idempotent: a row
33
+ * that already has one is only relabelled, and a row whose provider became
34
+ * removable hands the seat back to the host's own button.
35
+ */
36
+ export declare function reconcileProviderDeleteSeat(root: HTMLElement, deps: ProviderDeleteDeps): void;
37
+ /** Take every seat out again (plugin dispose, HMR teardown). */
38
+ export declare function teardownProviderDeleteSeats(): void;
@@ -0,0 +1,71 @@
1
+ /**
2
+ * The browser half's client for the host's API-key manager.
3
+ *
4
+ * Every key fact — which references a provider lists, which label each carries,
5
+ * which one is in use, and whether the credential store actually holds a value
6
+ * — is host state: the list is the plugin's own index file, the value lives in
7
+ * the credential store, and the "in use" seat is the profile's `apiKeyEnv`
8
+ * field. All six operations therefore ride ONE same-origin route
9
+ * ({@link KEY_INDEX_PATH}), and the browser half only ever handles references,
10
+ * labels, booleans and masked previews. It never sees a secret it did not
11
+ * explicitly ask for: a key value is fetched one reference at a time through the
12
+ * existing reveal route, on a click, and is never cached.
13
+ *
14
+ * Fails closed. A refused fence, a route that is not in the settings document,
15
+ * a store that cannot be written, a network error — each answers `{ ok: false }`
16
+ * (with the host's own wording when it sent one) and the panel says so; nothing
17
+ * here invents success, and nothing here invents an error message either.
18
+ *
19
+ * @module dsh-model-think-level/client/key-manager-client
20
+ */
21
+ /** One key row as the host reports it. */
22
+ export interface KeyRow {
23
+ /** Credential reference holding the value. */
24
+ readonly ref: string;
25
+ /** Stored label; absent means "show the reference itself". */
26
+ readonly alias?: string;
27
+ /** Whether this is the reference the profile currently names. */
28
+ readonly enabled: boolean;
29
+ /** Whether the credential store holds a value; null when it could not say. */
30
+ readonly configured: boolean | null;
31
+ /**
32
+ * The stored value in the host's display form — a head, an ellipsis and a
33
+ * tail. Absent when the store would not let the value be read, so a row shows
34
+ * nothing rather than a made-up preview.
35
+ */
36
+ readonly masked?: string;
37
+ }
38
+ /** A successful answer: the whole list, in the host's order. */
39
+ export interface KeyState {
40
+ readonly ok: true;
41
+ readonly route: string;
42
+ /** The reference in use, or null when the provider names none. */
43
+ readonly enabledRef: string | null;
44
+ readonly entries: readonly KeyRow[];
45
+ }
46
+ /** A refused answer, carrying the host's own words when it sent any. */
47
+ export interface KeyFailure {
48
+ readonly ok: false;
49
+ readonly error?: string;
50
+ }
51
+ /** What one call to the key route answers. */
52
+ export type KeyAnswer = KeyState | KeyFailure;
53
+ /** The manager's transport, injectable so the panel can be tested without one. */
54
+ export interface KeyManagerClient {
55
+ /** Read the provider's whole key list. */
56
+ list(route: string): Promise<KeyAnswer>;
57
+ /** Store a new key and list it, returning the list it produced. */
58
+ add(route: string, value: string, alias: string | undefined): Promise<KeyAnswer>;
59
+ /** Make one listed reference the key in use. */
60
+ enable(route: string, ref: string): Promise<KeyAnswer>;
61
+ /** Relabel one listed reference (undefined clears the label). */
62
+ rename(route: string, ref: string, alias: string | undefined): Promise<KeyAnswer>;
63
+ /** Replace the stored secret of one listed reference, keeping its name. */
64
+ replace(route: string, ref: string, value: string): Promise<KeyAnswer>;
65
+ /** Forget one listed reference and its value. */
66
+ remove(route: string, ref: string): Promise<KeyAnswer>;
67
+ /** The stored value of one listed reference, or undefined. */
68
+ reveal(route: string, ref: string): Promise<string | undefined>;
69
+ }
70
+ /** The live client: same-origin fetch against the host's route. */
71
+ export declare const httpKeyManagerClient: KeyManagerClient;
@@ -5,6 +5,8 @@ export declare const en: {
5
5
  thinkingMode: string;
6
6
  thinkingModeOff: string;
7
7
  autoAdapt: string;
8
+ autoAdaptAll: string;
9
+ autoAdaptAllHint: string;
8
10
  apply: string;
9
11
  reset: string;
10
12
  pendingSave: string;
@@ -79,6 +81,37 @@ export declare const en: {
79
81
  modelSearchNoMatches: string;
80
82
  modelSearchClear: string;
81
83
  modelSearchFailed: string;
84
+ editorTabsLabel: string;
85
+ editorTabProvider: string;
86
+ editorTabModels: string;
87
+ editorTabAdvanced: string;
88
+ keyRevealShow: string;
89
+ keyRevealHide: string;
90
+ keyRevealFailed: string;
91
+ keysLoading: string;
92
+ keysAdd: string;
93
+ keysAddConfirm: string;
94
+ keysEnable: string;
95
+ keysEnabled: string;
96
+ keysMissing: string;
97
+ keysEdit: string;
98
+ keysRemove: string;
99
+ keysConfirmRemove: string;
100
+ keysCancel: string;
101
+ keysSave: string;
102
+ keysAlias: string;
103
+ keysAliasPlaceholder: string;
104
+ keysValue: string;
105
+ keysValuePlaceholder: string;
106
+ keysNewValuePlaceholder: string;
107
+ keysAddIllegal: string;
108
+ keysAdded: string;
109
+ keysRenamed: string;
110
+ keysReplaced: string;
111
+ keysRemoved: string;
112
+ keysEnabledNote: string;
113
+ keysUnreachable: string;
114
+ keysPanelFailed: string;
82
115
  headersTitle: string;
83
116
  headersHint: string;
84
117
  headersEmpty: string;
@@ -106,6 +139,8 @@ export declare const en: {
106
139
  providerReorderHint: string;
107
140
  providerToggleAria: string;
108
141
  providerToggleHint: string;
142
+ providerRemoveDisabled: string;
143
+ providerRemoveLocked: string;
109
144
  modelReorderAria: string;
110
145
  modelReorderHint: string;
111
146
  };