dsh-model-think-level 1.1.0 → 1.1.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.
@@ -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
@@ -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 {
@@ -391,6 +399,65 @@ export interface AutofillFootprint {
391
399
  * position (the label's trailing number IS the position).
392
400
  */
393
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
+ }
394
461
  /**
395
462
  * Adapt every model of one provider, the rows the user never unfolded
396
463
  * included.
@@ -428,10 +495,7 @@ export declare function inputValueByLabel(card: HTMLElement, labels: readonly st
428
495
  * click: how many models it held an adaptation for, how many had no
429
496
  * suggestion, and -- when nothing could be adapted at all -- why.
430
497
  */
431
- export declare function adaptEveryModel(state: ScanState, deps: InjectorDeps, route: string, visibleRows?: ReadonlyArray<{
432
- id: string;
433
- name: string;
434
- }>): Promise<AutoAdaptReport>;
498
+ export declare function adaptEveryModel(state: ScanState, deps: InjectorDeps, route: string, visibleRows?: readonly ScreenModel[]): Promise<AutoAdaptReport>;
435
499
  /**
436
500
  * Hand a viewport delta back to whichever container actually moved the card.
437
501
  *
@@ -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;
@@ -0,0 +1,128 @@
1
+ /**
2
+ * Deferred request headers: the create-provider card's 高级配置 draft, held
3
+ * until the provider it belongs to exists.
4
+ *
5
+ * The create card's 高级配置 pane edits a route the settings document does not
6
+ * declare — the card's own 保存 is what declares it. So there is nothing this
7
+ * plugin may write at the moment the user types: `writeProviderHeaders` refuses
8
+ * a route no raw layer declares, and for a good reason (the write would leave a
9
+ * stray profile carrying nothing but headers). The pane's draft therefore waits
10
+ * here as an INTENT, and lands on the first scan that finds the route really
11
+ * declared. That finding is also the proof the creation succeeded: it is the
12
+ * card's own commit, never this plugin's, that puts the route in the document.
13
+ *
14
+ * The wait is bounded, and deliberately so. A pane's unmount cannot tell "the
15
+ * create landed and the card closed" from "the user gave up", so an intent is
16
+ * RELEASED at unmount rather than dropped, and a released intent whose route
17
+ * never appears is retired after {@link RELEASE_GRACE_MS} — which is what keeps
18
+ * an abandoned card's headers from being inherited by a later provider that
19
+ * happens to take the same id.
20
+ *
21
+ * What decides whether the draft may be written is not the route appearing on its
22
+ * own — a card whose id names a provider that ALREADY exists sees its route
23
+ * declared from the first keystroke, and those headers are that provider's, not
24
+ * this draft's. So each route carries a BASELINE: whether it was already declared
25
+ * when this card's draft was staged. A route that was not, and now is, was
26
+ * declared by the card (that is the only thing that could have declared it) and
27
+ * the draft lands — on the very scan that reads it, whether or not the card has
28
+ * closed yet. A route that already was gets nothing, exactly as before.
29
+ *
30
+ * Nothing is persisted. A reload discards a held intent exactly as it discards
31
+ * the card's own unsaved fields, which is the contract the pane's copy states.
32
+ *
33
+ * @module dsh-model-think-level/client/injection/staged-headers
34
+ */
35
+ import type { RemoteApi } from '../types.js';
36
+ /**
37
+ * How long a released intent keeps waiting for its route to appear.
38
+ *
39
+ * The window only has to cover the gap between the card's own 保存 landing and
40
+ * the next scan reading it — a mutation schedules that scan within the debounce
41
+ * window, so this is generous by two orders of magnitude. It is a window and not
42
+ * a deadline: a draft whose route never appears is dropped, because after that
43
+ * long the card it belonged to is not coming back.
44
+ */
45
+ export declare const RELEASE_GRACE_MS = 15000;
46
+ /** One pane's held draft, keyed by the route it will be written to. */
47
+ export interface StagedHeaderIntent {
48
+ /** The complete dict the pane holds — the same whole-dict value the editor's
49
+ * own Save writes, so the two commit paths cannot write different shapes. */
50
+ headers: Record<string, string>;
51
+ /** Whether this route was already declared when the draft was staged; see the
52
+ * module doc. Left undefined while that read is in flight — the drain cannot
53
+ * tell a typed id from a created one until it knows, so it waits. */
54
+ declaredBefore?: boolean;
55
+ /** When the pane that staged this intent unmounted, if it has. */
56
+ releasedAt?: number;
57
+ /** Whether the drain has already said — once — that this route has no
58
+ * baseline, which is the one holding state that ends in silence. */
59
+ noted?: boolean;
60
+ }
61
+ /** The session's deferred-header ledger. */
62
+ export interface StagedHeadersState {
63
+ /** Held drafts, by route. Readable so a test (and the console) can see what
64
+ * the session is still holding. */
65
+ readonly intents: Map<string, StagedHeaderIntent>;
66
+ /** What each route's baseline read found, kept for the session so a draft
67
+ * restaged after the drain retired its intent does not have to ask again. */
68
+ readonly baseline: Map<string, boolean>;
69
+ /** One drain at a time: an unmount's release-triggered flush and a scan's
70
+ * drain must not both walk the ledger and write the same route. */
71
+ flushing: boolean;
72
+ }
73
+ /** A fresh, empty ledger. */
74
+ export declare function createStagedHeadersState(): StagedHeadersState;
75
+ /**
76
+ * Hold one pane's draft for a route that is not declared yet.
77
+ *
78
+ * An empty draft WITHDRAWS instead of staging: "no headers" is the same state
79
+ * the route is already in (a card that creates a provider with no headers), so
80
+ * staging it would only arm a write that changes nothing — and it would make the
81
+ * pane's first, still-blank report look like an edit.
82
+ * @param state - the session's ledger.
83
+ * @param route - the route the pane's id field currently reads.
84
+ * @param headers - the complete draft dict.
85
+ */
86
+ export declare function stageRequestHeaders(state: StagedHeadersState, route: string, headers: Record<string, string>): void;
87
+ /** Drop the draft held for one route. */
88
+ export declare function withdrawRequestHeaders(state: StagedHeadersState, route: string): void;
89
+ /**
90
+ * Mark one route's draft as orphaned: the pane that staged it is gone and the
91
+ * card's own 保存 has already been pressed or will never be.
92
+ * @param state - the session's ledger.
93
+ * @param route - the route the pane last read.
94
+ */
95
+ export declare function releaseRequestHeaders(state: StagedHeadersState, route: string): void;
96
+ /**
97
+ * Record whether `route` was already declared when this card's draft was staged.
98
+ *
99
+ * The pane reads this off the settings document as soon as it has a route to ask
100
+ * about, which is long before the card's own 保存 can declare anything — so the
101
+ * answer tells the drain whether the declaration it later sees is this card's
102
+ * doing or someone else's provider. Kept per session, so a draft restaged after
103
+ * the drain retired its intent does not ask twice.
104
+ * @param state - the session's ledger.
105
+ * @param route - the route to record.
106
+ * @param declared - whether the route was declared at that moment.
107
+ */
108
+ export declare function markRouteBaseline(state: StagedHeadersState, route: string, declared: boolean): void;
109
+ /**
110
+ * Whether `route` still needs its baseline read, and has no draft to wait on.
111
+ * @param state - the session's ledger.
112
+ * @param route - the route the pane's id field currently reads.
113
+ * @returns True when the pane should ask the settings document.
114
+ */
115
+ export declare function needsRouteBaseline(state: StagedHeadersState, route: string): boolean;
116
+ /**
117
+ * Write every held draft whose route is now declared.
118
+ *
119
+ * Runs on the debounced scan, which is the only place that already does wire
120
+ * work — the synchronous pass in the mutation microtask must stay wire-free.
121
+ * @param state - the session's ledger.
122
+ * @param api - the settings Remote.
123
+ * @returns How many RELEASED drafts are still waiting for their route. The
124
+ * caller keeps the scan coming while that is non-zero: the card's own 保存 may
125
+ * declare the route after the card — and its pane — are gone, and by then no
126
+ * further DOM mutation is left to trigger a scan.
127
+ */
128
+ export declare function flushStagedRequestHeaders(state: StagedHeadersState, api: RemoteApi): Promise<number>;
@@ -175,6 +175,22 @@ export declare function providerHeadersOf(namespace: SettingsNamespaceView | und
175
175
  * @returns whether the write landed, and the refusal text when it did not.
176
176
  */
177
177
  export declare function writeProviderHeaders(api: RemoteApi, route: string, headers: Record<string, string>): Promise<ProviderHeadersReply>;
178
+ /**
179
+ * Whether this document declares the route: its RESOLVED providers map, or
180
+ * either raw layer, holding the key itself (own key, never a prototype name).
181
+ *
182
+ * The resolved map is the one the models-page editor stages against, and it is
183
+ * the only one a settings join is guaranteed to carry — a mirror-backed join can
184
+ * hand back `value` with the raw layers absent or thin, and a check that reads
185
+ * only the raw layers then answers "not declared" for a provider that plainly
186
+ * exists. The raw layers are still asked first-class, because they are where the
187
+ * official card's own save lands.
188
+ *
189
+ * Exported for the deferred-header ledger, which asks the same question for the
190
+ * same reason `writeProviderHeaders` does: a route nothing declares is a route
191
+ * nothing may be written to yet.
192
+ */
193
+ export declare function routeIsDeclared(namespace: SettingsNamespaceView | undefined, route: string): boolean;
178
194
  /**
179
195
  * Describe the pi-ai namespace plus writability.
180
196
  *