dsh-model-think-level 1.0.7 → 1.1.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.
@@ -143,6 +143,22 @@ export declare function ladderIntent(nextThinking: boolean, built: EffortIntent,
143
143
  * @returns the draft, or undefined when it holds no field at all.
144
144
  */
145
145
  export declare function compatDraftOf(compat: CompatSuggestion | undefined): CompatSuggestion | undefined;
146
+ /**
147
+ * The compat advice three MANUAL field values spell, from the values handed in.
148
+ *
149
+ * The editor's three compat controls are its own edit surface, so the handler
150
+ * that just changed one must report the value it computed: re-reading the
151
+ * component state there reports the PRE-update field, because React commits
152
+ * that state after the event — the queued write then carries the previous
153
+ * advice while the control already shows the new one.
154
+ * @param parts - the three field values as they will read after the event.
155
+ * @returns the draft, or undefined when all three are empty.
156
+ */
157
+ export declare function manualCompatOf(parts: {
158
+ budgetField: string;
159
+ priorityText: string;
160
+ maxOutput: string;
161
+ }): CompatSuggestion | undefined;
146
162
  /**
147
163
  * Assemble one complete pending write from the parts a caller resolved: the
148
164
  * compat merge (the applied suggestion's block with the caller's own fields on
@@ -14,6 +14,12 @@
14
14
  * card's own fields. The headers land through `llm-pi-ai.providers.<route>.headers`,
15
15
  * the one channel the adapter actually sends.
16
16
  *
17
+ * Deferred mode (`deferred`) is this same section rendered where there is no
18
+ * route to write TO yet — the create-provider card's 高级配置 pane, whose route
19
+ * the card's own 保存 declares. There the draft is reported instead of written,
20
+ * and the whole-dict discipline is what makes the two commit seams the same
21
+ * write: see {@link DeferredHeaders}.
22
+ *
17
23
  * @module dsh-model-think-level/HeadersEditor
18
24
  */
19
25
  import type { ReactNode } from 'react';
@@ -54,6 +60,22 @@ export interface HeaderEnvironment {
54
60
  preferOfficialLayer: boolean;
55
61
  };
56
62
  }
63
+ /**
64
+ * Deferred mode: how a pane that has no route yet reports its draft.
65
+ *
66
+ * The create-provider card's 高级配置 pane edits an id the settings document
67
+ * does not declare — the card's own 保存 is what declares it — so that pane has
68
+ * nothing to write and no button to press. It hands the editor this instead:
69
+ * every change to the draft goes out as the complete dict its Save would write,
70
+ * and a Cancel reports that the draft is gone. Same component, same whole-dict
71
+ * discipline; only the commit seam changes, from a write to a report.
72
+ */
73
+ export interface DeferredHeaders {
74
+ /** Every change to the draft, as the dict the editor's Save would write. */
75
+ onDraft: (headers: Record<string, string>) => void;
76
+ /** The user cleared the draft: whatever was staged for it must go. */
77
+ onCancel: () => void;
78
+ }
57
79
  /** Props of {@link HeadersEditor}. */
58
80
  export interface HeadersEditorProps {
59
81
  /** The provider route this card edits. */
@@ -62,6 +84,24 @@ export interface HeadersEditorProps {
62
84
  api: RemoteApi;
63
85
  /** Localized copy. */
64
86
  t: (key: string, params?: Record<string, string | number>) => string;
87
+ /**
88
+ * Close the whole provider card, as the official footer's `取消` does.
89
+ *
90
+ * Only the mount that owns the card's DOM can press that button, so the
91
+ * editor takes the action as a prop instead of reaching for the DOM itself
92
+ * (this component needs no injection pass — see the module doc). Called by
93
+ * {@link cancel} in pane mode only; omitted, cancel stays a draft reset.
94
+ */
95
+ onDismiss?: () => void;
96
+ /**
97
+ * Deferred mode: report the draft instead of writing it.
98
+ *
99
+ * Set by the create-provider card's pane, whose route does not exist yet. The
100
+ * editor then edits in place — no disclosure, no Edit step — and has no Save
101
+ * of its own: its copy states that the draft lands with the card's own 保存,
102
+ * which is the write that declares the route the draft is addressed to.
103
+ */
104
+ deferred?: DeferredHeaders;
65
105
  }
66
106
  /** One editable header row; a stable `id` keeps React keys off the name. */
67
107
  interface DraftRow {
@@ -79,8 +119,8 @@ interface Draft {
79
119
  export declare function headersFromDraft(draft: Draft): Record<string, string>;
80
120
  /**
81
121
  * Render the provider card's request-header section.
82
- * @param props - route identity, write seam and copy.
122
+ * @param props - route identity, write seam, copy and the card dismiss seam.
83
123
  * @returns the section.
84
124
  */
85
- export declare function HeadersEditor({ route, api, t }: HeadersEditorProps): ReactNode;
125
+ export declare function HeadersEditor({ route, api, t, onDismiss, deferred }: HeadersEditorProps): ReactNode;
86
126
  export {};
@@ -0,0 +1,72 @@
1
+ /**
2
+ * The add-provider card: the custom-API form on its own, carrying the tab bar.
3
+ *
4
+ * The official card offers two ways to add a provider — adopt one from the
5
+ * built-in catalogue, or declare a custom API endpoint — behind a segmented
6
+ * "How to add" control. The catalogue half only duplicates the configured
7
+ * provider rows above it, so this pass reduces the card to the custom half: it
8
+ * selects that mode, hides the switch, and gives the form the same three panes
9
+ * its sibling editor cards carry — the bar, and the 高级配置 section the card
10
+ * has nowhere else to put ({@link CreateCardPane}).
11
+ *
12
+ * The card is the SAME card the tab takeover dresses (`bre-tabbed`), and that
13
+ * takeover scopes its regions to the visible half of the form
14
+ * ({@link activeScopeOf}). What it cannot do is hang the bar: the official card
15
+ * renders the provider-card slot only inside the catalogue half, so the custom
16
+ * form has no plugin mount to render a bar into. The mount is created here
17
+ * instead, at the top of the visible form, and it is the ONLY node this module
18
+ * inserts. Everything else is attributes on nodes the official page owns.
19
+ *
20
+ * Degradation is total: a card with no custom half (no API protocol to declare,
21
+ * or no `llm-pi-ai` namespace to declare one into) is left exactly as the
22
+ * official page built it, switch and all, and a card whose dressing throws is
23
+ * rolled back to that same state.
24
+ *
25
+ * @module dsh-model-think-level/client/injection/add-card
26
+ */
27
+ import type { ReactNode } from 'react';
28
+ import type { Translate } from '@deepseek-ai/dsh-client-ui-slots';
29
+ import { type ForeignMount } from './mount.js';
30
+ import type { StagedHeadersState } from './staged-headers.js';
31
+ import type { RemoteApi } from '../types.js';
32
+ export interface AddCardDeps {
33
+ /** The plugin's own translator, for the tab labels and the pane's copy. */
34
+ t: Translate;
35
+ /** The write seam the pane's header editor reads the settings document with. */
36
+ api: RemoteApi;
37
+ /** The official labels of the form's provider-id field, by locale. */
38
+ routeIdLabels: () => readonly string[];
39
+ /** The session's deferred-header ledger (see `staged-headers`). */
40
+ staged: StagedHeadersState;
41
+ /** The plugin's locale-refresh seam, so the pane's copy follows a language
42
+ * switch exactly as the seats the official slot mounts do. */
43
+ refreshed: (children: () => ReactNode) => ReactNode;
44
+ }
45
+ /** One card's mount, remembered by the card it belongs to. */
46
+ interface AddCardHost {
47
+ /** The node the root renders into, so a re-created form can be told from a
48
+ * settled one. */
49
+ readonly host: HTMLElement;
50
+ readonly mount: ForeignMount;
51
+ }
52
+ export interface AddCardState {
53
+ /** Cards this module mounted a bar into. The mount goes with the card, so a
54
+ * card the page has dropped is dropped here on the next visit. */
55
+ readonly hosts: Map<HTMLElement, AddCardHost>;
56
+ }
57
+ export declare function createAddCardState(): AddCardState;
58
+ /**
59
+ * Reduce every add-provider card on the page to its custom form.
60
+ *
61
+ * Idempotent by construction: a card already showing the custom form with its
62
+ * bar up is left untouched, and switching the mode is driven by the official
63
+ * control's own selected state, so a re-visit writes nothing.
64
+ */
65
+ export declare function reconcileAddCards(root: HTMLElement, deps: AddCardDeps, state: AddCardState): void;
66
+ /**
67
+ * Give every card back to the official page: the mounts go, and the switch
68
+ * comes out of hiding. The mode the card is left in is the official page's own
69
+ * state, and it is not this module's to reset — React unmounts the card whole.
70
+ */
71
+ export declare function teardownAddCards(state: AddCardState): void;
72
+ export {};
@@ -0,0 +1,53 @@
1
+ /**
2
+ * The create-provider card's 高级配置 pane: the tab bar and the request-header
3
+ * editor, for a provider that does not exist yet.
4
+ *
5
+ * This is the half {@link add-card} used to leave out. On an existing provider
6
+ * the same editor arrives through the official `settings.models.provider-card`
7
+ * slot, which the official card renders only inside its CATALOGUE half — so the
8
+ * custom-only card this plugin reduces it to had a tab bar whose 高级配置 button
9
+ * could never light up: nothing in that half carried a `.bre-headers` section for
10
+ * the takeover to reveal. This pane is that section, mounted at the same seat
11
+ * the bar is.
12
+ *
13
+ * The route the pane edits is the id the user is TYPING, which is the one thing
14
+ * about this card the settings document does not have yet — the card's own 保存
15
+ * is what declares it. So the pane cannot write as the official card can:
16
+ *
17
+ * - the id is read off the live form (the panel's own label, never a position)
18
+ * and re-read on every keystroke, because the form's React state is not ours
19
+ * to subscribe to;
20
+ * - the rows are editable whether or not the id has been typed: a draft is held
21
+ * by the session ledger once it has a route to belong to, and until then it
22
+ * simply waits for the id — which the pane does NOT announce, because the id
23
+ * field is on the 供应商配置 tab of this same card and the official form
24
+ * already speaks for it;
25
+ * - once there is an id the editor runs in DEFERRED mode: it edits, and reports
26
+ * the draft to the session ledger, but the write happens when the route
27
+ * appears in the document — the proof the card's own create landed. The pane
28
+ * also asks, one keystroke in, whether that id was ALREADY declared, because
29
+ * only a route that was not can be this card's work. See
30
+ * {@link staged-headers} for why that is a gate and not a delay.
31
+ *
32
+ * @module dsh-model-think-level/client/injection/create-card-pane
33
+ */
34
+ import type { ReactNode } from 'react';
35
+ import type { Translate } from '@deepseek-ai/dsh-client-ui-slots';
36
+ import type { RemoteApi } from '../types.js';
37
+ import { type StagedHeadersState } from './staged-headers.js';
38
+ export interface CreateCardPaneProps {
39
+ /** The plugin's own mount inside this card; the bar resolves the card from it. */
40
+ host: {
41
+ readonly current: HTMLDivElement | null;
42
+ };
43
+ /** The write seam, for the editor's own settings read. */
44
+ api: RemoteApi;
45
+ /** The plugin's translator. */
46
+ t: Translate;
47
+ /** The official labels of the form's provider-id field, by locale. */
48
+ routeIdLabels: () => readonly string[];
49
+ /** The session's deferred-header ledger. */
50
+ staged: StagedHeadersState;
51
+ }
52
+ /** One create card's pane: its bar, and the headers pane once there is a route. */
53
+ export declare function CreateCardPane({ host, api, t, routeIdLabels, staged }: CreateCardPaneProps): ReactNode;
@@ -75,6 +75,23 @@ export declare function createEditorTabsState(): EditorTabsState;
75
75
  * other.
76
76
  */
77
77
  export declare function editorCardOf(element: Element | null): HTMLElement | null;
78
+ /**
79
+ * The part of a card that presents its editor.
80
+ *
81
+ * A provider ROW has one editor and this returns the card itself. The
82
+ * create-provider card has TWO form halves — the built-in catalogue and the
83
+ * custom API declaration — and the official page mounts each once it is visited
84
+ * and hides the inactive one with `hidden`. Every lookup below is scoped to the
85
+ * visible half, because the hidden one carries a provider-card slot of its own:
86
+ * the plugin's mount inside it publishes a route, a tab bar and a headers
87
+ * section, and reading those would dress the visible form with the hidden
88
+ * form's provider.
89
+ *
90
+ * A card with both halves hidden (mid-swap, before React commits the show) and
91
+ * a card with none are both scoped to the card, which is the rule this module
92
+ * has always applied to a single-form card.
93
+ */
94
+ export declare function activeScopeOf(card: HTMLElement): HTMLElement;
78
95
  /**
79
96
  * Switch one card to a tab. The card attribute is the single source of truth:
80
97
  * the stylesheet swaps the pane on the attribute alone, and the bar's buttons
@@ -79,5 +79,3 @@ export declare function reconcileModelOrder(root: HTMLElement, deps: ModelOrderD
79
79
  * persisting that order — on behalf of a fiber that no longer exists.
80
80
  */
81
81
  export declare function teardownModelOrder(state: ModelOrderState): void;
82
- /** The stored sequence for one provider, for callers that only need to read it. */
83
- export declare function storedModelOrder(provider: string): readonly string[];
@@ -74,6 +74,14 @@ export interface HostLabels {
74
74
  * offers this plugin no seat to stamp that name on.
75
75
  */
76
76
  editProvider: readonly string[];
77
+ /**
78
+ * The official input-types control's image choice ("Image" / "图片"). When a
79
+ * row ships that control, it — not the plugin's ledger — is what tells the
80
+ * user (and the official Save) that a model takes images, so the copy is the
81
+ * anchor that finds the box a verdict belongs in: see
82
+ * {@link echoOfficialModality}.
83
+ */
84
+ inputImage: readonly string[];
77
85
  }
78
86
  /** The join the injector renders from. */
79
87
  export interface InjectorDeps {
@@ -103,6 +111,8 @@ export interface InjectorDeps {
103
111
  * editing" is the only moment this side may safely write the document.
104
112
  */
105
113
  onIdle?(): void;
114
+ /** Wake the debounced scan after provider-wide adaptation stages a row. */
115
+ onPendingChanged?(): void;
106
116
  /**
107
117
  * Arm a timed retry after a failed idle pass. The backoff clock only helps
108
118
  * if something wakes the injector when it expires; a settled page has no DOM
@@ -389,6 +399,65 @@ export interface AutofillFootprint {
389
399
  * position (the label's trailing number IS the position).
390
400
  */
391
401
  export declare function inputValueByLabel(card: HTMLElement, labels: readonly string[]): string;
402
+ /**
403
+ * The official input-types checkbox an image verdict belongs to, or undefined
404
+ * when this row ships no such control (a legacy kernel) — or ships one whose
405
+ * boxes cannot be told apart.
406
+ *
407
+ * The official fieldset maps its two choices in a fixed order (`["text",
408
+ * "image"]`) and labels each one, so the image box is the one whose own label
409
+ * carries the image copy — matched like every other anchor, by copy, with the
410
+ * second box as the fallback for a host whose copy is in a language neither
411
+ * this plugin's locale nor its English floor knows: the order still identifies
412
+ * it.
413
+ */
414
+ export declare function officialImageCheckboxOf(container: HTMLElement, imageLabels: readonly string[]): HTMLInputElement | undefined;
415
+ /**
416
+ * Print a verdict about images on the OFFICIAL input-types control, and report
417
+ * whether that control now owns the row's modality.
418
+ *
419
+ * The official checkbox renders the official card's own DRAFT, not the
420
+ * settings document, so a document write of the plugin's can never tick it
421
+ * before the card is saved and reopened — the gap the user hit. Clicking the
422
+ * box is a real user-equivalent edit: the official form holds the tick in its
423
+ * draft and its own Save is what persists it. That is also why a row reaching
424
+ * this path must keep the modality OUT of its own write (see
425
+ * {@link officialModalityIntent}): two writers for one field would let this
426
+ * plugin's held write land after that Save and revert whatever the user chose
427
+ * in the meantime.
428
+ *
429
+ * A missing box, or a DISABLED one (a read-only card, which will not carry the
430
+ * click anywhere), reports `false` so the caller keeps its own write — the
431
+ * only remaining way the verdict can reach the document.
432
+ */
433
+ export declare function echoOfficialModality(container: HTMLElement, image: boolean, imageLabels: readonly string[]): boolean;
434
+ /**
435
+ * The modality intent a row's own write may carry, once the official
436
+ * input-types control has been offered the verdict.
437
+ *
438
+ * `undefined` out means the write states nothing about the modality: either it
439
+ * never carried one, or the official control took the verdict over. A row with
440
+ * no such control — a legacy kernel, or a row the user has left folded — hands
441
+ * its intent through unchanged, which keeps the document write as the way the
442
+ * verdict travels.
443
+ *
444
+ * `null` is the plugin's deliberate UNSET, which the official control cannot
445
+ * express at all (it knows only text and text+image): it passes through
446
+ * untouched.
447
+ */
448
+ export declare function officialModalityIntent(container: HTMLElement | undefined, input: InputModalities | null | undefined, imageLabels: readonly string[]): InputModalities | null | undefined;
449
+ /**
450
+ * One model row a card shows on screen: what the walk needs to name it, plus
451
+ * the row element itself, so a verdict about its modality can be printed on
452
+ * the row's own official control (which lives inside the row, collapsed or
453
+ * not). The element is optional because a caller may know the rows only by
454
+ * their ids.
455
+ */
456
+ export interface ScreenModel {
457
+ readonly id: string;
458
+ readonly name: string;
459
+ readonly row?: HTMLElement;
460
+ }
392
461
  /**
393
462
  * Adapt every model of one provider, the rows the user never unfolded
394
463
  * included.
@@ -414,11 +483,91 @@ export declare function inputValueByLabel(card: HTMLElement, labels: readonly st
414
483
  * @param state - mutable scan state: the adapting guard and the held ledger.
415
484
  * @param deps - the injection dependencies.
416
485
  * @param route - the provider route whose models are adapted.
486
+ * @param state - mutable scan state: the adapting guard and the held ledger.
487
+ * @param deps - the injection dependencies.
488
+ * @param route - the provider route whose models are adapted.
489
+ * @param visibleRows - the models the route's card shows on screen, from the
490
+ * scan's own rows (id and display name). The document cannot know the rows
491
+ * the card holds but has not saved yet, so the pass extends its walk with
492
+ * them: their holds land with the card's own Save, exactly like a row's own
493
+ * auto-adapt.
417
494
  * @returns what the pass did, so the seat can say something true about the
418
495
  * click: how many models it held an adaptation for, how many had no
419
496
  * suggestion, and -- when nothing could be adapted at all -- why.
420
497
  */
421
- export declare function adaptEveryModel(state: ScanState, deps: InjectorDeps, route: string): Promise<AutoAdaptReport>;
498
+ export declare function adaptEveryModel(state: ScanState, deps: InjectorDeps, route: string, visibleRows?: readonly ScreenModel[]): Promise<AutoAdaptReport>;
499
+ /**
500
+ * Hand a viewport delta back to whichever container actually moved the card.
501
+ *
502
+ * The card's distance from the top of the viewport is the sum of every scroll
503
+ * offset between it and the document, so the correction belongs to the
504
+ * container that really moved: usually the nearest scrollable one, but a
505
+ * deletion near the bottom of a long page moves the PAGE's offset instead. Each
506
+ * container is asked for as much of the delta as it can still give, the ones
507
+ * this window has already written first and the rest from the inside out, and
508
+ * every container that answers is watched from then on. A container whose
509
+ * offset is no longer the one this loop left is being scrolled by somebody
510
+ * else, and is skipped rather than fought.
511
+ */
512
+ export declare function absorbDelta(chain: readonly HTMLElement[], watched: Set<HTMLElement>, lastSet: Map<HTMLElement, number>, delta: number): void;
513
+ /**
514
+ * Start recording where a click inside a model list leaves its scroll host.
515
+ *
516
+ * Document-level and capture-phase, like the stand-down listeners a re-pin
517
+ * window installs: the position has to be read before the official handler
518
+ * (and React's commit behind it) can shorten the list. Reference counted, so
519
+ * a reload that lands a second fiber over a page that already has one does not
520
+ * take the listeners away from the first.
521
+ */
522
+ export declare function watchModelListOffsets(): void;
523
+ /**
524
+ * Stop watching, drop every blank, and forget the last position.
525
+ *
526
+ * The blanks are elements this plugin owns inside the official page: a fiber
527
+ * that goes away must not leave a scroll container holding a gap nothing
528
+ * maintains. Removing one can move the view (the range it held goes with it),
529
+ * which is the right thing to do to a page whose watcher is gone.
530
+ */
531
+ export declare function unwatchModelListOffsets(): void;
532
+ /**
533
+ * Hold every open provider card's top across a model row burst, and hold the
534
+ * scroll host's own range open while it does.
535
+ *
536
+ * The official list keys rows by array index, so a deletion removes the LAST
537
+ * element and re-fills the survivors: the card loses a row's height INSIDE
538
+ * itself, above whatever its tail holds. Left to its anchoring the browser
539
+ * keeps a node near the hole visually stable instead — it buys the shrink
540
+ * back by moving the whole view, and deleting a model slides the page DOWN
541
+ * rather than letting the content below the list contract upward into the
542
+ * hole it opened. Suppressing anchoring from the burst's own microtask (the
543
+ * injector runs this before the browser has laid the deletion out) holds the
544
+ * card's top where it was: the header does not move, and the tail rises.
545
+ *
546
+ * Anchoring is only half of it, and the half that shows up when the list is
547
+ * scrolled near its end. There the shrink also takes the scroll RANGE away,
548
+ * and the browser clamps the offset down to the new maximum on the first
549
+ * flush that sees the shorter content — which is this very microtask.
550
+ * Measured on the live page (list scrolled to its end, eight deletions): every
551
+ * step moved the card's top by exactly the amount the clamp took, 54px per
552
+ * row, and no anchor suppression could stop it because nothing was anchoring:
553
+ * the range was simply gone. {@link holdListSlack} is what gives it back, from
554
+ * the position the row's own click recorded before the change.
555
+ *
556
+ * The window also covers the burst's second height change. The re-filled rows
557
+ * leave the mounted editor of the model that moved out of them STALE, and the
558
+ * debounced scan re-renders (or mounts) it a frame or two after the deletion
559
+ * itself — the same two-stage change {@link pinCardTop} was built for, now
560
+ * opened by the mutation rather than by a click.
561
+ *
562
+ * The card vocabulary is the same three flavours {@link wireEditRepin} knows:
563
+ * a deletion inside a NEW provider's setup card (or the add card) has no
564
+ * rowCard to climb into at all, and a pin that names only saved providers
565
+ * silently skips the very card the user is editing.
566
+ *
567
+ * @param root - the panel to search for model lists.
568
+ * @param windowMs - how long each found card stays pinned.
569
+ */
570
+ export declare function pinModelListCards(root: HTMLElement, windowMs?: number): void;
422
571
  /**
423
572
  * Scan the settings DOM for official model rows and reconcile the injected
424
573
  * editors. Idempotent: existing editors are left alone, new disclosures get
@@ -0,0 +1,56 @@
1
+ /**
2
+ * The folded group of switched-off providers on the official Models page.
3
+ *
4
+ * A switched-off provider is not deleted: its row is the only place it can be
5
+ * switched back on, and its configuration is still the user's. It does not
6
+ * belong in the list the user reads either — a page whose built-in providers all
7
+ * stay in the way makes the two or three real ones hard to find. So the rows of
8
+ * the switched-off providers fold away behind one control at the end of the
9
+ * list, and that control says how many are folded and unfolds them again.
10
+ *
11
+ * Unfolding does not scatter them back among the providers that are on: the
12
+ * folded rows are ORDERED behind the control, so the group reads as one group
13
+ * under its own control. The order comes from the plugin's stylesheet
14
+ * (`.bre-row-folded`, `.bre-provider-disabled-seat`), where `!important` outranks
15
+ * the inline orders the drag passes write — this module therefore never has to
16
+ * fight them for the attribute, and the rows are never moved or rebuilt: their
17
+ * DOM slot, which the drag passes read back out of the DOM, stays exactly where
18
+ * the host put them.
19
+ *
20
+ * The only node this module inserts is its own control, as the list's last item:
21
+ * a control outside the list could not head rows inside it, and the list is a
22
+ * flex column the host itself lays out. Both the class writes and the insertion
23
+ * are guarded on the state they would produce: a scan that changes nothing
24
+ * writes nothing, because every write here would otherwise feed the observer
25
+ * that scheduled the scan.
26
+ *
27
+ * The folded state is per session and deliberately not stored. The point of the
28
+ * group is that the switched-off providers are out of the way, and a reload
29
+ * showing them folded again is the behaviour that was asked for.
30
+ *
31
+ * @module dsh-model-think-level/client/injection/provider-disabled-group
32
+ */
33
+ import type { HostLabels } from './models-page-editor.js';
34
+ /** Per-scan bookkeeping (one instance lives in `models-page.ts`). */
35
+ export interface ProviderDisabledGroupState {
36
+ /** Whether the folded rows are currently unfolded. */
37
+ expanded: boolean;
38
+ }
39
+ export interface ProviderDisabledGroupDeps {
40
+ /** The plugin's own translator, for the control's label and tooltip. */
41
+ t: (key: string, params?: Record<string, string | number>) => string;
42
+ /** The official `settings.models` label sets, resolved fresh on every scan. */
43
+ labels: () => HostLabels;
44
+ }
45
+ export declare function createProviderDisabledGroupState(): ProviderDisabledGroupState;
46
+ /**
47
+ * Fold the switched-off providers away — or put them back when the control says
48
+ * so — and keep the control's own label, count and position in step.
49
+ *
50
+ * Idempotent: a row already folded stays folded, and the control is only ever
51
+ * written when a value genuinely changed, so a scan never mutates the page on
52
+ * behalf of a state that is already on it.
53
+ */
54
+ export declare function reconcileProviderDisabledGroup(root: HTMLElement, deps: ProviderDisabledGroupDeps, state: ProviderDisabledGroupState): void;
55
+ /** Remove the control and unfold every row it hid. */
56
+ export declare function teardownProviderDisabledGroup(): void;
@@ -62,6 +62,13 @@ export interface DragSession {
62
62
  frame: number | undefined;
63
63
  dropped: boolean;
64
64
  }
65
+ /** What a list looked like the last time a scan read it. */
66
+ export interface ListSighting {
67
+ /** The ids in the sequence they screened, in screen order. */
68
+ readonly ids: readonly string[];
69
+ /** The row elements that were on screen. */
70
+ readonly rows: readonly Element[];
71
+ }
65
72
  /** Per-scan bookkeeping; one instance per module that owns a list. */
66
73
  export interface DragState {
67
74
  /** The drag sources already carrying listeners. */
@@ -70,6 +77,16 @@ export interface DragState {
70
77
  readonly lists: WeakSet<Element>;
71
78
  /** The insertion line; created on the first drag and reused until teardown. */
72
79
  line: HTMLElement | undefined;
80
+ /**
81
+ * What each list looked like on the last pass, by list element.
82
+ *
83
+ * Needed because a removal cannot be told from a rename or an addition by
84
+ * looking at the current DOM: when the host keys its rows by index, deleting
85
+ * a model leaves every row below it drawn with the next model, so the rows on
86
+ * screen are the same elements holding the same `order`s they always had. See
87
+ * {@link arrangement} for what that does to the screen order.
88
+ */
89
+ readonly sighted: WeakMap<Element, ListSighting>;
73
90
  drag: DragSession | undefined;
74
91
  }
75
92
  export declare function createDragState(): DragState;
@@ -78,9 +95,11 @@ export declare function rowElements(parent: HTMLElement): HTMLElement[];
78
95
  /** The rows in the sequence they currently screen. */
79
96
  export declare function visualRows(list: HTMLElement): HTMLElement[];
80
97
  /**
81
- * Put `sequence` on screen: each row it names takes the slot a named row
82
- * already held, and a row it does not name keeps the slot it has. Every child
83
- * gets an explicit `order` once anything moves, so no two rows share a slot.
98
+ * Put `sequence` on screen against the order the rows currently screen in.
99
+ *
100
+ * Only for a gesture in flight: the pointer's arrangement IS the screen order,
101
+ * so that is the sequence the rows are placed against. Everything outside a
102
+ * gesture goes through {@link applyIdOrder}, which places against the DOM.
84
103
  */
85
104
  export declare function placeRows(list: HTMLElement, sequence: readonly HTMLElement[]): void;
86
105
  /**
@@ -96,6 +115,14 @@ export declare function orderedIds(list: HTMLElement, idOf: (row: HTMLElement) =
96
115
  /**
97
116
  * Place the rows in `ids` order. Rows without a known id are left exactly where
98
117
  * they are: a row we cannot name is a row we must not move.
118
+ *
119
+ * The slots come from the DOM order. React owns the child list and keeps it
120
+ * compacted and truthful across a removal, while the inline `order`s still on
121
+ * the survivors describe where those models were before it. Taking the slots
122
+ * from the DOM and the sequence from {@link arrangement} is what lets the two
123
+ * meet: the rows land in the order the user had, in the slots the host now
124
+ * genuinely has, so the gap a deletion left closes instead of leaving a row
125
+ * behind the end of the list.
99
126
  */
100
127
  export declare function applyIdOrder(list: HTMLElement, ids: readonly string[], idOf: (row: HTMLElement) => string | undefined): void;
101
128
  /** Hand every row back the inline geometry a drag wrote on it. */
@@ -121,6 +148,21 @@ export declare function dropLineTop(row: HTMLElement, after: boolean): number;
121
148
  export declare function makeGhost(row: HTMLElement, label?: string): HTMLElement;
122
149
  /** The nearest ancestor that actually scrolls, for the edge auto-scroll. */
123
150
  export declare function scrollHostOf(node: HTMLElement): HTMLElement | undefined;
151
+ /**
152
+ * Every container that can hold `node` in place, nearest first.
153
+ *
154
+ * {@link scrollHostOf} answers the one container an edge-drag has to drive. A
155
+ * window that CORRECTS the view needs the rest of the chain as well: the offset
156
+ * a change takes away is held by whichever ancestor the browser is using as the
157
+ * scrolling box, and the nearest container is only the innermost candidate for
158
+ * that — an ancestor the nearest one delegates to can be the one that moved.
159
+ *
160
+ * The page's own scroller is the last candidate even though it never declares
161
+ * an overflow of its own: a long settings page scrolls there when nothing
162
+ * inside it does, and a row deletion near the bottom of that page takes its
163
+ * range away exactly like a deletion inside a small panel would.
164
+ */
165
+ export declare function scrollChainOf(node: HTMLElement): HTMLElement[];
124
166
  /** The pixels per frame `host` should scroll while the pointer is at `clientY`. */
125
167
  export declare function edgeScrollStep(host: HTMLElement, clientY: number): number;
126
168
  /**
@@ -0,0 +1,28 @@
1
+ /**
2
+ * The official "Saved &lt;provider&gt;." line, taken out of the flow and
3
+ * answered by a bubble at the top of the window instead.
4
+ *
5
+ * The host confirms a save by inserting a paragraph above the provider list
6
+ * (`ModelsSection`'s `savedNotice`, `role="status"`) that stays there until the
7
+ * next edit, remove or add clears it. It is feedback, not state — the row it
8
+ * names is on screen and shows the save itself — but it holds a whole line of
9
+ * the panel's height the whole time the user works in the card, which is the
10
+ * "the hint eats space" report. So the line is hidden (never removed: React
11
+ * owns it, and it must survive the page's own re-renders) and the same words go
12
+ * up as the plugin's fixed top-centre bubble, which takes itself away after
13
+ * {@link NOTE_TTL_MS} — the treatment the auto-adapt seat's verdict already
14
+ * gets, and the one the host's own toast uses.
15
+ *
16
+ * The pass owns no wire and no state of its own: it is DOM-only, idempotent and
17
+ * compare-before-write, so a settled page emits no mutation of its own.
18
+ *
19
+ * @module dsh-model-think-level/client/injection/saved-notice
20
+ */
21
+ /**
22
+ * Hide every official saved line inside the panel, and put up a bubble for the
23
+ * one that just appeared.
24
+ * @param root - the Models panel the passes all decorate.
25
+ */
26
+ export declare function reconcileSavedNotice(root: HTMLElement): void;
27
+ /** Put every official line back and take the bubble down (plugin dispose). */
28
+ export declare function teardownSavedNotice(): void;