@genesislcap/ai-assistant 15.23.2 → 15.24.0-FUI-2603.10

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.
Files changed (55) hide show
  1. package/dist/ai-assistant.api.json +421 -52
  2. package/dist/ai-assistant.d.ts +239 -694
  3. package/dist/chat-driver.cjs +138 -120
  4. package/dist/chat-driver.cjs.map +4 -4
  5. package/dist/chat-driver.mjs +138 -120
  6. package/dist/chat-driver.mjs.map +4 -4
  7. package/dist/custom-elements.json +361 -602
  8. package/dist/dts/components/chat-driver/chat-driver.d.ts +22 -11
  9. package/dist/dts/components/chat-driver/chat-driver.d.ts.map +1 -1
  10. package/dist/dts/components/chat-driver/turn-failures.d.ts +96 -0
  11. package/dist/dts/components/chat-driver/turn-failures.d.ts.map +1 -0
  12. package/dist/dts/components/orchestrating-driver/orchestrating-driver.d.ts +16 -0
  13. package/dist/dts/components/orchestrating-driver/orchestrating-driver.d.ts.map +1 -1
  14. package/dist/dts/components/settings-modal/index.d.ts +1 -0
  15. package/dist/dts/components/settings-modal/index.d.ts.map +1 -1
  16. package/dist/dts/components/settings-modal/settings-modal.d.ts +215 -0
  17. package/dist/dts/components/settings-modal/settings-modal.d.ts.map +1 -0
  18. package/dist/dts/components/settings-modal/settings-modal.template.d.ts.map +1 -1
  19. package/dist/dts/main/attachment-controller.d.ts +66 -0
  20. package/dist/dts/main/attachment-controller.d.ts.map +1 -0
  21. package/dist/dts/main/budget-controller.d.ts +368 -0
  22. package/dist/dts/main/budget-controller.d.ts.map +1 -0
  23. package/dist/dts/main/composer-menu-controller.d.ts +113 -0
  24. package/dist/dts/main/composer-menu-controller.d.ts.map +1 -0
  25. package/dist/dts/main/composer-resize-controller.d.ts +80 -0
  26. package/dist/dts/main/composer-resize-controller.d.ts.map +1 -0
  27. package/dist/dts/main/context-gate-controller.d.ts +197 -0
  28. package/dist/dts/main/context-gate-controller.d.ts.map +1 -0
  29. package/dist/dts/main/cost-ledger-controller.d.ts +228 -0
  30. package/dist/dts/main/cost-ledger-controller.d.ts.map +1 -0
  31. package/dist/dts/main/main.d.ts +201 -722
  32. package/dist/dts/main/main.d.ts.map +1 -1
  33. package/dist/dts/main/main.types.d.ts +25 -0
  34. package/dist/dts/main/main.types.d.ts.map +1 -1
  35. package/dist/dts/main/meta-snapshot.d.ts +50 -0
  36. package/dist/dts/main/meta-snapshot.d.ts.map +1 -0
  37. package/dist/dts/state/persistence/session-persister.d.ts.map +1 -1
  38. package/dist/esm/components/chat-driver/chat-driver.js +92 -142
  39. package/dist/esm/components/chat-driver/turn-failures.js +84 -0
  40. package/dist/esm/components/orchestrating-driver/orchestrating-driver.js +18 -0
  41. package/dist/esm/components/settings-modal/index.js +1 -0
  42. package/dist/esm/components/settings-modal/settings-modal.js +436 -0
  43. package/dist/esm/components/settings-modal/settings-modal.template.js +15 -4
  44. package/dist/esm/main/attachment-controller.js +254 -0
  45. package/dist/esm/main/budget-controller.js +745 -0
  46. package/dist/esm/main/composer-menu-controller.js +234 -0
  47. package/dist/esm/main/composer-resize-controller.js +119 -0
  48. package/dist/esm/main/context-gate-controller.js +289 -0
  49. package/dist/esm/main/cost-ledger-controller.js +498 -0
  50. package/dist/esm/main/main.js +501 -2268
  51. package/dist/esm/main/meta-snapshot.js +108 -0
  52. package/dist/esm/state/persistence/session-persister.js +5 -4
  53. package/docs/migration-GENC-1580.md +209 -0
  54. package/package.json +16 -16
  55. package/docs/.gitattributes +0 -2
@@ -2,12 +2,12 @@ import { AgentPickerMode } from '@genesislcap/foundation-ai';
2
2
  import type { AggregateUsage } from '@genesislcap/foundation-ai';
3
3
  import { AIProviderRegistry } from '@genesislcap/foundation-ai';
4
4
  import type { AIProviderRegistryStatusEntry } from '@genesislcap/foundation-ai';
5
- import type { AIProviderType } from '@genesislcap/foundation-ai';
5
+ import { AIProviderType } from '@genesislcap/foundation-ai';
6
6
  import { BudgetExhaustedError } from '@genesislcap/foundation-ai';
7
7
  import type { CachePolicy } from '@genesislcap/foundation-ai';
8
8
  import type { ChatAttachment } from '@genesislcap/foundation-ai';
9
9
  import type { ChatConfig } from '@genesislcap/foundation-ai';
10
- import type { ChatDriverResult } from '@genesislcap/foundation-ai';
10
+ import { ChatDriverResult } from '@genesislcap/foundation-ai';
11
11
  import type { ChatFallback } from '@genesislcap/foundation-ai';
12
12
  import type { ChatInputDuringExecutionMode } from '@genesislcap/foundation-ai';
13
13
  import type { ChatMessage } from '@genesislcap/foundation-ai';
@@ -1719,6 +1719,26 @@ export declare class ChatDriver extends EventTarget implements AiDriver {
1719
1719
  * the historical `{ reason: 'done' }`.
1720
1720
  */
1721
1721
  private turnDone;
1722
+ /**
1723
+ * End the turn on a terminal failure: fail upward when this driver is a sub-agent, speak
1724
+ * to the user when it is not, and hand the reason to the caller either way.
1725
+ *
1726
+ * Only that fork lives here. Each exit keeps its own `recordTurnError` call, because the
1727
+ * telemetry payload is what legitimately differs between them — attempt counts, token
1728
+ * figures, tool names — and folding it in would mean one optional field per condition.
1729
+ *
1730
+ * `overrides.copy` serves the two walls whose prose is composed per occurrence:
1731
+ * `budgetExhaustedBubble` names the vendor and can offer another, `providerRefusedBubble`
1732
+ * prefers a host-supplied message. Every other exit reads its copy from the table.
1733
+ *
1734
+ * One exit stays out: the `BudgetExhaustedError` catch. Not because its shape differs —
1735
+ * its two telemetry payloads are field-for-field identical — but because its main-turn
1736
+ * branch calls `reportBudgetExhausted`, which is public and also serves
1737
+ * `OrchestratingDriver`'s classification phase, where no tool loop is running. That
1738
+ * method has to survive either way, so routing the catch through here would relocate the
1739
+ * duplication rather than remove it.
1740
+ */
1741
+ private endTurnWithFailure;
1722
1742
  /**
1723
1743
  * Terminal budget outcome for a wall hit **outside** the tool loop — today,
1724
1744
  * `OrchestratingDriver`'s classification phase, which calls the provider
@@ -1756,15 +1776,6 @@ export declare class ChatDriver extends EventTarget implements AiDriver {
1756
1776
  * has gone unpaid — so there is nothing here to compose. A host override wins verbatim.
1757
1777
  */
1758
1778
  private providerRefusedBubble;
1759
- /**
1760
- * Terminal provider-refusal outcome. Mirrors `reportBudgetExhausted` so the two walls behave
1761
- * identically from the caller's side, while keeping their diagnostics distinct.
1762
- *
1763
- * `kind` reaches the debug log and the result but never the transcript: it is what lets an operator
1764
- * tell "top up the account" from "rotate the key", and with cause-free user copy this is the only
1765
- * place that distinction survives.
1766
- */
1767
- private reportProviderRefused;
1768
1779
  /** The typed failure reason on a loop result, or `undefined` for a clean turn / handoff. */
1769
1780
  private static failureReasonOf;
1770
1781
  /**
@@ -2209,7 +2220,7 @@ export declare interface ChatDriverConfig {
2209
2220
  * Transcript copy appended when the AI-spend budget wall is hit (GENC-1464),
2210
2221
  * used VERBATIM — set it only to own the copy entirely (white-labelling).
2211
2222
  * When unset, the driver composes the bubble per wall: the neutral
2212
- * `DEFAULT_BUDGET_EXHAUSTED_MESSAGE`, upgraded with "switch to another AI
2223
+ * `TURN_FAILURE_EXITS.budgetExhausted.copy`, upgraded with "switch to another AI
2213
2224
  * provider" advice only when the 402's `otherVendorAvailable` positively says
2214
2225
  * another metered vendor still has headroom — the same rule the blocked
2215
2226
  * banner applies, so the two surfaces can no longer contradict each other.
@@ -2228,7 +2239,7 @@ export declare interface ChatDriverConfig {
2228
2239
  budgetExhaustedMessage?: string;
2229
2240
  /**
2230
2241
  * Overrides the transcript sentence shown when the upstream PROVIDER refuses the account
2231
- * (GENC-1506) — see `DEFAULT_PROVIDER_REFUSED_MESSAGE`.
2242
+ * (GENC-1506) — see `TURN_FAILURE_EXITS.providerRefused.copy`.
2232
2243
  *
2233
2244
  * Separate from `budgetExhaustedMessage` because the two conditions are separate: ours is a spend
2234
2245
  * cap a host administrator can raise, this is the vendor declining to serve us at all. A host that
@@ -2761,6 +2772,35 @@ export declare function formatBlockedReason(budget?: Partial<Extract<ChatDriverR
2761
2772
  * @beta
2762
2773
  */
2763
2774
  export declare class FoundationAiAssistant extends GenesisElement {
2775
+ private readonly composerResize;
2776
+ private readonly composerMenus;
2777
+ private readonly attachmentCtrl;
2778
+ /**
2779
+ * Pure derivation — owns no state. Reaches the driver only through the two
2780
+ * callbacks below, so it stays clear of the stateful-agent surface (§11.2
2781
+ * constraint 2) and of redux entirely.
2782
+ */
2783
+ private readonly contextGateCtrl;
2784
+ /**
2785
+ * Cost accounting and the persisted build-history ledger (GENC-1475). Reads the live
2786
+ * session through callbacks and owns the rows, the load/write latches and the debounce
2787
+ * timer — the element keeps only the names the settings modal binds.
2788
+ */
2789
+ private readonly costLedger;
2790
+ /**
2791
+ * The AI-spend budget: vendor walls, the derived composer lock, the banner copy
2792
+ * and the settings meter (GENC-1464).
2793
+ *
2794
+ * Takes the session store itself rather than a callback per field — see
2795
+ * `BudgetDeps` for why this one differs from its siblings.
2796
+ */
2797
+ private readonly budget;
2798
+ /**
2799
+ * The settings modal (`docs/code-health.md` §3). Reaches the two design-system
2800
+ * refs through callbacks, because the template writes them by name — see
2801
+ * `SettingsModalDeps`.
2802
+ */
2803
+ private readonly settings;
2764
2804
  providerRegistry: AIProviderRegistry;
2765
2805
  appSettingsProvider: AssistantAppSettingsProvider;
2766
2806
  designSystemPrefix: string;
@@ -3050,22 +3090,10 @@ export declare class FoundationAiAssistant extends GenesisElement {
3050
3090
  /**
3051
3091
  * The distinct vendors the registry can currently reach, from the provider
3052
3092
  * statuses this element already loads on connect and refreshes on every
3053
- * observable-registry change.
3054
- *
3055
- * This is the element's answer to "which vendor would the next turn use" — a
3056
- * question that has **no** correct answer and deliberately gets no API. The
3057
- * provider is resolved per turn AND per agent: `activeProviderInput` may be an
3058
- * async function of the turn's context, an orchestrated turn picks its agent
3059
- * with an LLM `classify()` call, and sub-agents resolve their own providers.
3060
- * Any pre-turn "peek" would therefore be a guess that is wrong precisely on the
3061
- * multi-agent hosts per-vendor budgets exist for.
3062
- *
3063
- * Asking instead which vendors are REACHABLE is answerable, synchronous, and
3064
- * free — and it is enough: the composer must stay live while any reachable
3065
- * vendor has headroom, and must lock when none does. It also makes vendor-switch
3066
- * recovery a derivation rather than a mutation: a host that swaps its registry
3067
- * to another vendor fires the observable, the statuses reload, the walled vendor
3068
- * drops out of this set, and `blocked` goes false with every latch left intact.
3093
+ * observable-registry change — this element's answer to "which vendor would the
3094
+ * next turn use", a question that has no correct answer and deliberately gets
3095
+ * no API. See `BudgetController.reachableVendors` for why reachability is
3096
+ * the answerable question and what deriving from it buys.
3069
3097
  *
3070
3098
  * @beta
3071
3099
  */
@@ -3076,13 +3104,11 @@ export declare class FoundationAiAssistant extends GenesisElement {
3076
3104
  *
3077
3105
  * May include a vendor this registry cannot reach — either because the registry
3078
3106
  * moved on after the wall was latched, or because a 402's
3079
- * `otherVendorAvailable: false` walls every vendor the proxy meters (which is
3080
- * what that verdict is a statement about; see
3081
- * {@link FoundationAiAssistant.latchBlockedFrom}). Neither costs anything
3082
- * internally — {@link FoundationAiAssistant.blocked} asks only about the
3083
- * reachable set, and the banner reads the reachability-filtered
3084
- * {@link FoundationAiAssistant.relevantBlockedVendors} — but a host rendering
3085
- * this list itself should filter it against its own registry.
3107
+ * `otherVendorAvailable: false` walls every vendor the proxy meters. Neither
3108
+ * costs anything internally — {@link FoundationAiAssistant.blocked} asks only
3109
+ * about the reachable set, and the banner reads the reachability-filtered
3110
+ * `BudgetController.relevantBlockedVendors` — but a host rendering this
3111
+ * list itself should filter it against its own registry.
3086
3112
  *
3087
3113
  * Read-only on purpose: a settable array would let a host write a partial list
3088
3114
  * and silently orphan the per-vendor banner copy.
@@ -3108,30 +3134,20 @@ export declare class FoundationAiAssistant extends GenesisElement {
3108
3134
  * become true.
3109
3135
  *
3110
3136
  * `reason` is composed into the banner sentence for that vendor, **however many
3111
- * vendors are walled** — see {@link FoundationAiAssistant.effectiveBlockedReason}.
3112
- * It is per-vendor copy, not a whole-banner override; {@link FoundationAiAssistant.blockedReason}
3113
- * is the override. Omitting it keeps whatever explanation that vendor already
3114
- * carried, so a driver latch landing after a host one cannot blank it.
3115
- *
3116
- * `'none'` is rejected with a warning rather than accepted: it is the "no
3117
- * provider configured" sentinel, not a vendor. Latching it would put the raw
3118
- * sentinel in the banner ("none's AI usage limit is reached") and could never
3119
- * be undone by derivation, because `'none'` never appears in
3120
- * {@link FoundationAiAssistant.reachableVendors} — so `blocked` could never
3121
- * become derivable from it either. Use {@link FoundationAiAssistant.setBlocked}
3122
- * for a vendor-agnostic block.
3137
+ * vendors are walled** — see `BudgetController.effectiveBlockedReason`.
3138
+ * It is per-vendor copy, not a whole-banner override;
3139
+ * {@link FoundationAiAssistant.blockedReason} is the override. Omitting it keeps
3140
+ * whatever explanation that vendor already carried, so a driver latch landing
3141
+ * after a host one cannot blank it.
3142
+ *
3143
+ * `'none'` is rejected with a warning rather than accepted — it is the "no
3144
+ * provider configured" sentinel, not a vendor, and a wall on it could never be
3145
+ * undone by derivation. Use {@link FoundationAiAssistant.setBlocked} for a
3146
+ * vendor-agnostic block.
3123
3147
  *
3124
3148
  * @beta
3125
3149
  */
3126
3150
  setVendorBlocked(vendor: AIProviderType, blocked: boolean, reason?: string | null): void;
3127
- /**
3128
- * One warning per element for garbage fed to
3129
- * {@link FoundationAiAssistant.setVendorBudget}. Once, not per call, because
3130
- * the documented feed re-runs after every turn — a host bug would otherwise
3131
- * repeat the same warning on every send for the life of the page.
3132
- */
3133
- private vendorBudgetWarningIssued;
3134
- private warnVendorBudgetOnce;
3135
3151
  /**
3136
3152
  * Feed (or clear) one vendor's AI-spend figures for the settings-modal budget
3137
3153
  * meter — the numeric companion to
@@ -3194,8 +3210,9 @@ export declare class FoundationAiAssistant extends GenesisElement {
3194
3210
  spentUsd: number;
3195
3211
  } | null): boolean;
3196
3212
  /**
3197
- * When budget figures last arrived — from the host's {@link FoundationAiAssistant.setVendorBudget}
3198
- * or the wall latch's own feed — as epoch ms; `null` until the first feed.
3213
+ * When budget figures last arrived — from the host's
3214
+ * {@link FoundationAiAssistant.setVendorBudget} or the wall latch's own feed —
3215
+ * as epoch ms; `null` until the first feed.
3199
3216
  *
3200
3217
  * ELEMENT state, not slice state, on purpose: the reducer is idempotent by
3201
3218
  * value (an unchanged re-feed must not re-render the meter rows), but "when
@@ -3203,7 +3220,8 @@ export declare class FoundationAiAssistant extends GenesisElement {
3203
3220
  * refresh that confirms the same dollars is still a refresh. Only the
3204
3221
  * toolbar label observes this, so a stamp re-renders one span, not the rows.
3205
3222
  */
3206
- budgetMeterLastFedAt: number | null;
3223
+ get budgetMeterLastFedAt(): number | null;
3224
+ set budgetMeterLastFedAt(value: number | null);
3207
3225
  /** "Updated HH:MM:SS" for the meter toolbar, or `undefined` before any feed. */
3208
3226
  get settingsBudgetUpdatedLabel(): string | undefined;
3209
3227
  /**
@@ -3218,83 +3236,24 @@ export declare class FoundationAiAssistant extends GenesisElement {
3218
3236
  * listener simply keeps the per-turn cadence.
3219
3237
  */
3220
3238
  requestBudgetRefresh(): void;
3221
- /** Whether this vendor's wall came from the sweep alone — see the slice's `sweptVendors`. */
3222
- private isVendorSwept;
3223
- /**
3224
- * The walled vendors the user can still be routed to — i.e.
3225
- * {@link FoundationAiAssistant.blockedVendors} narrowed to the reachable set,
3226
- * which is the only set the banner may name.
3227
- *
3228
- * A wall the registry can no longer reach is not news: it cannot be hit, and
3229
- * naming it puts a vendor in front of the user that is not theirs. The concrete
3230
- * failure this exists to stop: a host ships Anthropic-only, Anthropic walls, the
3231
- * host swaps its registry to Gemini, Gemini walls — and the banner reads "AI
3232
- * usage limits are reached for Anthropic and Gemini" to a user who has never
3233
- * had an Anthropic key. The spurious second name also flips the copy onto the
3234
- * plural branch, so even the closing sentence is wrong.
3235
- *
3236
- * When nothing is reachable YET (the statuses are still loading — the exact
3237
- * window the pre-flight and the cross-tab bus latch into), the fallback keeps
3238
- * every wall for LOCKING (`blocked` still derives true) but names only the
3239
- * vendors walled by a REFUSAL, dropping the ones the `otherVendorAvailable`
3240
- * sweep added. A refusal is a fact about a vendor this user just used; a sweep
3241
- * entry is the proxy's headroom verdict about a vendor the host may not even
3242
- * ship — naming it here reproduced the exact failure above from the other
3243
- * direction ("Gemini's AI usage limit is reached" to an Anthropic-only user),
3244
- * and flipped the copy onto the plural branch with it. Self-corrects when the
3245
- * statuses land: from then on reachability, not provenance, decides.
3246
- *
3247
- * @internal
3248
- */
3249
- private get relevantBlockedVendors();
3250
3239
  /**
3251
3240
  * Whether the blocked banner has anything to say — a vendor-agnostic block, or
3252
- * a partial wall that the NEXT send would actually hit.
3253
- *
3254
- * Partial exhaustion is announced only while the ACTIVE vendor (the registry
3255
- * default the next send resolves to) is among the walled ones. A user who has
3256
- * switched to a vendor with headroom has already acted on the advice —
3257
- * keeping the banner up past that point nagged about a vendor they just left
3258
- * (GENC-1464 tester feedback), while switching BACK to the walled vendor
3259
- * brings the banner straight back. Two conservative fallbacks keep it
3260
- * fail-informative: while the statuses are still loading the active vendor is
3261
- * unknowable, so any reachable-and-walled vendor still announces; and a
3262
- * vendor-agnostic block announces always, exactly as before.
3263
- *
3264
- * Reads the reachability-filtered list for the same reason the copy does — a
3265
- * host that has swapped its registry away from the walled vendor has nothing
3266
- * left to announce, and would otherwise get a banner that falls through to the
3267
- * generic default copy over a perfectly usable composer.
3241
+ * a partial wall that the NEXT send would actually hit. See
3242
+ * `BudgetController.bannerVisible` for why partial exhaustion is
3243
+ * announced only while the active vendor is among the walled ones.
3268
3244
  *
3269
3245
  * @internal
3270
3246
  */
3271
3247
  get bannerVisible(): boolean;
3272
3248
  /**
3273
- * Whether a suggestions fetch would hit a wall.
3274
- *
3275
- * Unlike a chat turn, this one CAN be resolved exactly: both suggestion paths
3276
- * go to the registry **default** and never to a per-agent override
3277
- * (`ChatDriver.getSuggestions` calls `providerRegistry.default()`, and
3278
- * `OrchestratingDriver.getSuggestions` just delegates to it). So the vendor a
3279
- * suggestions call would use is knowable, and this is the one place in the
3280
- * feature where that is true.
3281
- *
3282
- * Same rationale as the guard it replaces: a doomed suggestions call burns a
3283
- * `suggestions.failed` meta event and parks a raw transport error in
3284
- * `suggestionsState` for hosts to find.
3249
+ * Whether a suggestions fetch would hit a wall — the one place in this feature
3250
+ * where the vendor a call would use is exactly knowable, because both
3251
+ * suggestion paths go to the registry default. See
3252
+ * `BudgetController.suggestionsBlocked`.
3285
3253
  *
3286
3254
  * @internal
3287
3255
  */
3288
3256
  get suggestionsBlocked(): boolean;
3289
- /**
3290
- * The vendor the NEXT send resolves to — the registry default's provider, as
3291
- * reported through `providerStatuses`. `undefined` while the statuses are
3292
- * still loading (or when the default reports the `'none'` sentinel), which
3293
- * every consumer treats conservatively.
3294
- *
3295
- * @internal
3296
- */
3297
- private get activeVendor();
3298
3257
  /**
3299
3258
  * Explanation shown in the blocked banner, or `null` for the element's default
3300
3259
  * copy. Only meaningful while {@link FoundationAiAssistant.blocked} is true;
@@ -3311,96 +3270,6 @@ export declare class FoundationAiAssistant extends GenesisElement {
3311
3270
  */
3312
3271
  get blockedReason(): string | null;
3313
3272
  set blockedReason(value: string | null);
3314
- /**
3315
- * Latch the backend block off a turn outcome. The single decision point for
3316
- * both latch sites (the `tool-loop-end` bus subscription and `send()`'s
3317
- * return-value check), so the rule cannot diverge between them.
3318
- *
3319
- * Idempotent and one-way **per vendor**: it never unblocks, and it never
3320
- * re-writes an existing wall. That guard is load-bearing, not just an
3321
- * optimisation — the bus fires from the driver's `finally`, i.e. BEFORE
3322
- * `sendMessage()` resolves, so a host subscribed to `tool-loop-end` (what the
3323
- * migration guide's Option C recommends) sets its own detailed reason and the
3324
- * return-value latch would otherwise land a moment later and blank it.
3325
- *
3326
- * The guard being per-vendor rather than global is the whole difference. A
3327
- * single global "already blocked, do nothing" would swallow a second vendor's
3328
- * wall — so a session walled on Anthropic could never record that Gemini went
3329
- * too, and under a mixed registry an Anthropic-only wall would lock a composer
3330
- * that Gemini could still serve.
3331
- *
3332
- * **Which field is authoritative**, in order:
3333
- *
3334
- * 1. `budget.vendorLabel` — stamped by the transport that was actually refused,
3335
- * so it can never be stale. But it is a static per-transport string, so a
3336
- * white-labelled or multiplexing gateway fronting several upstreams leaves
3337
- * it unclaimed by any vendor.
3338
- * 2. `budget.vendor` — which the driver resolved from the label where it could,
3339
- * and otherwise from the proxy's own `vendor` field on the 402. It is
3340
- * therefore NOT simply `vendorLabel` normalised, and the two can disagree;
3341
- * that fallback is the only attribution on offer for the gateway case above.
3342
- * 3. `vendorHint` — the bus detail's top-level vendor, i.e. the driver's
3343
- * last-resolved provider. Last resort because it CAN be stale: an
3344
- * orchestrated turn classifies against the registry default, a provider the
3345
- * chat driver may never have resolved, so its last-resolved provider there is
3346
- * the previous turn's vendor or nothing at all.
3347
- *
3348
- * A wall that names no recognised vendor falls back to the vendor-agnostic
3349
- * block — fail-safe, and identical to the pre-per-vendor behaviour.
3350
- *
3351
- * **`otherVendorAvailable: false` is server-known truth and outranks every
3352
- * inference made here.** The proxy meters the pots, so only it can say whether
3353
- * anything else has headroom; this element can only observe which vendors the
3354
- * registry can REACH, which says nothing about their remaining spend. So when
3355
- * the proxy says no, every other *budgeted* vendor is walled in the same pass.
3356
- * That is what makes `blocked` derive true — locking the composer on the first
3357
- * response instead of after a second doomed turn — and what empties the "free"
3358
- * set the banner would otherwise have advised switching to.
3359
- *
3360
- * The sweep runs over `BUDGETED_VENDORS`, deliberately **not** over
3361
- * {@link FoundationAiAssistant.reachableVendors}. Reachability is loaded
3362
- * asynchronously (`activateSession` kicks off `loadProviderStatuses()` and does
3363
- * not await it), so a wall landing before the statuses resolve would have swept
3364
- * an empty set — silently discarding the one fact on the 402 the client cannot
3365
- * re-derive, with nothing to re-run it when the statuses arrived. Worse, it let
3366
- * the verdict effectively un-latch: the single wall derived `blocked` only
3367
- * while the reachable set was empty, so the moment the statuses landed the
3368
- * composer came back to life against a budget the proxy had already said was
3369
- * gone. Sweeping the metered vendor list instead makes the verdict independent
3370
- * of load order and of registry membership, which is exactly what it is: a
3371
- * statement about the proxy's pots, not about this user's registry.
3372
- *
3373
- * Walling a vendor the registry cannot reach costs nothing: `blocked` only asks
3374
- * whether every REACHABLE vendor is walled, and the banner reads
3375
- * {@link FoundationAiAssistant.relevantBlockedVendors}, which filters the walls
3376
- * back down to the reachable set before naming any of them.
3377
- *
3378
- * One boundary remains on the sweep: it walls only vendors the proxy meters,
3379
- * enforced structurally by iterating `BUDGETED_VENDORS` itself rather than
3380
- * filtering a wider set through a predicate. Chrome runs on-device with no pot
3381
- * to exhaust, so "no other vendor has budget" is not a statement about it, and
3382
- * "Switch to Chrome to keep going." stays honest advice.
3383
- *
3384
- * The sweep runs last but sits OUTSIDE the per-vendor idempotence guard, and
3385
- * both halves of that matter. Last, so the vendor that actually refused this
3386
- * turn heads the wall order the multi-vendor banner reads. Outside the guard,
3387
- * because the verdict can arrive on a LATER 402 for a vendor that is already
3388
- * walled — turn 1 walls Anthropic while Gemini still has headroom, turn 2 walls
3389
- * it again and reports that Gemini has since gone too — and an early return
3390
- * would drop exactly the news the second turn was there to deliver.
3391
- *
3392
- * @param reason - the turn's failure reason; anything but `'budget-exhausted'` is ignored.
3393
- * @param ref - session store to write through; defaults to the live one. `send()`
3394
- * passes the ref it captured before its awaits, since a lifecycle event during the
3395
- * turn may already have cleared `_sessionRef`.
3396
- * @param budget - what the 402 reported: figures when the proxy sent any, the
3397
- * refusing vendor, and its `otherVendorAvailable` verdict.
3398
- * @param vendorHint - the `tool-loop-end` detail's top-level vendor, used only
3399
- * when the budget payload names none.
3400
- *
3401
- * @internal
3402
- */
3403
- private latchBlockedFrom;
3404
3273
  /**
3405
3274
  * Block or unblock the assistant, optionally with custom banner copy.
3406
3275
  * Equivalent to the {@link FoundationAiAssistant.blocked} setter, plus the
@@ -3416,66 +3285,18 @@ export declare class FoundationAiAssistant extends GenesisElement {
3416
3285
  */
3417
3286
  setBlocked(blocked: boolean, reason?: string | null): void;
3418
3287
  /**
3419
- * Copy shown in the blocked banner, in ascending specificity:
3420
- *
3421
- * 1. the host's `blockedReason` — a whole-banner override, unchanged;
3422
- * 2. copy composed from the walled vendors (their per-vendor reasons when set,
3423
- * otherwise their names) plus the action that is actually available;
3424
- * 3. the default budget-exhaustion message.
3425
- *
3426
- * The action clause is computed here rather than stored, because whether
3427
- * "switch vendor" or "contact your administrator" is honest depends on whether
3428
- * another reachable vendor still has headroom — which changes when the registry
3429
- * changes, long after the wall was latched. Telling a user to switch to a
3430
- * vendor that is also exhausted is worse than saying nothing.
3431
- *
3432
- * The `free` set is a client-side inference — "reachable and not known to be
3433
- * walled" — and it is only ever allowed to be one. Where the proxy has told us
3434
- * otherwise (`otherVendorAvailable: false`) the latch has already walled those
3435
- * vendors, so they are not in `free` here and no "switch to X" clause can be
3436
- * composed from them. Both lists are filtered by reachability, so the copy
3437
- * names only vendors this user's registry can actually route to.
3438
- *
3439
- * **Per-vendor copy is composed however many vendors are walled**, which is
3440
- * what {@link FoundationAiAssistant.setVendorBlocked} and
3441
- * `docs/migration-GENC-1464.md` both promise. It previously reached only the
3442
- * single-vendor branch, so a host that had carefully set copy for each of its
3443
- * vendors watched all of it vanish the moment a second one walled — replaced by
3444
- * generic copy, at the exact moment the situation got worse.
3445
- *
3446
- * The compact list is the DEFAULT shape for several walls, now carrying each
3447
- * vendor's figures: "AI usage limits are reached for Gemini ($62.34 of $50.00)
3448
- * and Anthropic ($31.10 of $30.00)." A driver latch stores no copy of its own
3449
- * (it stores the figures, which this composes from), so that shape is what the
3450
- * real budget flow produces. As soon as any walled vendor carries HOST copy,
3451
- * which cannot be folded into a list item, the branch lists one statement per
3452
- * vendor instead — falling back to composed copy for the ones that have none, so
3453
- * the sentence set stays complete rather than silently naming a subset.
3288
+ * Copy shown in the blocked banner — the host's override, else copy composed
3289
+ * from the walled vendors, else the default. See
3290
+ * `BudgetController.effectiveBlockedReason`, which explains why the
3291
+ * action clause is composed on read rather than stored at latch time.
3454
3292
  *
3455
3293
  * @internal
3456
3294
  */
3457
3295
  get effectiveBlockedReason(): string;
3458
3296
  /**
3459
3297
  * Copy the driver writes into the TRANSCRIPT when a turn hits the budget wall,
3460
- * or `undefined` to let the driver compose it per wall.
3461
- *
3462
- * A set `blockedReason` still wins — a white-labelled host must not read its
3463
- * own explanation in the banner and shipped copy directly below it, and the
3464
- * vendor-agnostic latch's `formatBlockedReason(budget)` form stays advice-free
3465
- * exactly as before.
3466
- *
3467
- * `undefined` (the usual case) is NOT the old `DEFAULT_BUDGET_EXHAUSTED_MESSAGE`
3468
- * fallback: it hands composition to the driver, which builds the bubble from
3469
- * EACH wall's own 402 — naming the exhausted vendor and advising a switch only
3470
- * when that refusal's `otherVendorAvailable` positively vouched for headroom
3471
- * elsewhere. That is immune to the staleness this getter used to guard against
3472
- * by freezing the copy: construction-time advice could outlive its truth
3473
- * (`getOrCreateDriver` re-runs on an agents swap and would have baked the
3474
- * sentence into every later bubble), but per-wall advice is derived from the
3475
- * refusal it annotates and dies with the turn. Forcing the default here was
3476
- * also what made the driver's composed bubble unreachable in every real
3477
- * mount — the banner advised switching while the bubble under it never did
3478
- * (the GENC-1464 tester finding, root cause).
3298
+ * or `undefined` to let the driver compose it per wall — see
3299
+ * `BudgetController.transcriptMessage`.
3479
3300
  */
3480
3301
  private get transcriptBudgetExhaustedMessage();
3481
3302
  /**
@@ -3537,49 +3358,32 @@ export declare class FoundationAiAssistant extends GenesisElement {
3537
3358
  */
3538
3359
  get sessionCostUsd(): number;
3539
3360
  /**
3540
- * Distinct models the registry currently resolves to — the live session's model chips.
3541
- *
3542
- * `providerStatuses` carries one entry per registry SLOT, so a vendor configured across several
3543
- * tiers that happen to resolve to the same model reports it once per tier, and the chips
3544
- * rendered it once per tier too. Which tier a model came from is not what these chips are for.
3361
+ * The cost ledger's derived surface (GENC-1475). Every member below delegates to
3362
+ * `CostLedgerController` — the names are this element's public API and the
3363
+ * settings modal's bindings, so they stay put while the accounting moves.
3545
3364
  *
3546
- * Deduped on the label actually displayed, not on `status.model`: a slot whose provider reports
3547
- * no model falls back to the slot's own name, and those are distinct per slot, so keying on the
3548
- * label keeps them from collapsing into one another.
3365
+ * None carries `@volatile`: the decorators live on the controller's getters, and they
3366
+ * reach the binding observer from there — see `docs/code-health.md` §11.4.
3549
3367
  */
3550
3368
  get uniqueProviderModels(): CostSessionModelEntry[];
3551
- /**
3552
- * Build history for display: most recently worked on first.
3553
- *
3554
- * The stored order is insertion order (see `upsertRecord`), which leaves a project returned to
3555
- * sitting wherever it was first seen — so ordering is applied here, on read, where it also
3556
- * covers rows a host provider supplied.
3557
- */
3369
+ /** Build history for display: most recently worked on first. */
3558
3370
  get sortedCostSessionHistory(): CostSessionRecord[];
3559
- /**
3560
- * Lifetime usage across every project in the history scope — cost and all four token
3561
- * buckets. What the Usage tab's lifetime section renders.
3562
- *
3563
- * Rows ONLY — deliberately not rows + live session. Each row holds a project's lifetime
3564
- * usage and the active project's row is kept current on every transcript change, so
3565
- * adding the live total would count this project's spend twice. That double-count is what
3566
- * made the figure grow on every refresh: a page-hide banked a row while the restored
3567
- * session still carried the same spend.
3568
- */
3371
+ /** Lifetime usage across every project in the history scope — rows only, never plus live. */
3569
3372
  get cumulativeUsage(): AggregateUsage;
3570
3373
  /** Lifetime USD across every project in the history scope. */
3571
3374
  get cumulativeCostUsd(): number;
3572
- /**
3573
- * Whether the live session figures amount to this project's whole history, which is true
3574
- * exactly when the transcript is restored on load. Drives the Usage tab's live labels
3575
- * ("Project cost" vs "Session cost") so they state the scope rather than imply one.
3576
- *
3577
- * Note this is NOT what makes the *rows* lifetime — those bank their pre-transcript spend
3578
- * (see `CostSessionRecord.banked`) and are lifetime either way.
3579
- */
3375
+ /** Whether the live session figures amount to this project's whole history. */
3580
3376
  get liveUsageIsProjectLifetime(): boolean;
3581
3377
  /** Resolved cost tab and build-history settings from `chatConfig.costHistory`. */
3582
3378
  get resolvedCostHistory(): ResolvedCostHistoryConfig;
3379
+ /**
3380
+ * Persisted finalized cost sessions shown on the Usage tab build history list.
3381
+ *
3382
+ * A get/set pair rather than a read-only projection: the banking tests seed rows to
3383
+ * model a page load that found some, and a host provider's rows arrive the same way.
3384
+ */
3385
+ get costSessionHistory(): CostSessionRecord[];
3386
+ set costSessionHistory(value: CostSessionRecord[]);
3583
3387
  /**
3584
3388
  * Total tokens across every chat turn in this session — all four buckets added up.
3585
3389
  * Read-only; the per-bucket split is on `sessionUsage`.
@@ -3588,56 +3392,20 @@ export declare class FoundationAiAssistant extends GenesisElement {
3588
3392
  /** Label shown on the Usage tab for the in-flight session (app title or first user phrase). */
3589
3393
  get liveCostSessionTitle(): string | undefined;
3590
3394
  /**
3591
- * Write this session's usage into persisted build history (`localStorage`) as a single
3592
- * row keyed by the session key, replacing any existing row for that key.
3593
- *
3594
- * **Idempotent** — safe to call per turn, on tab close, and on a title change alike.
3595
- * The name is historical: it does NOT end the session or reset the running totals.
3596
- * Resetting them is what used to double-count spend, since the row was banked while the
3597
- * persisted transcript still carried the same usage, so a reload restored it and the
3598
- * Usage tab counted both.
3395
+ * Write this session's usage into persisted build history as a single row keyed by the
3396
+ * session key, replacing any existing row for that key.
3599
3397
  *
3600
- * No-op unless `chatConfig.costHistory.enabled` is `true`, when the live session has no
3601
- * usage yet, or when the session has no key to upsert against. Gating on the *live*
3602
- * figures (not the banked total) means a reload that restores nothing rewrites nothing.
3398
+ * **Idempotent** — safe to call per turn, on tab close, and on a title change alike. The
3399
+ * name is historical: it does NOT end the session or reset the running totals. See
3400
+ * `CostLedgerController.finalize` for the full contract and the no-op cases.
3603
3401
  */
3604
3402
  finalizeCostSession(options?: {
3605
3403
  title?: string;
3606
3404
  }): void;
3607
3405
  /**
3608
- * Spend for `projectKey` that predates the current transcript, resolved once per SESSION and
3609
- * shared by every element instance wired to it (see `banked-usage-baselines`), so it cannot
3610
- * drift as the row it came from is rewritten by our own upserts.
3611
- *
3612
- * The two cases turn on whether the transcript is authoritative for this project:
3613
- *
3614
- * - **Transcript restored** (`persistenceActive`): it already accounts for everything after
3615
- * the previous row's own banked figure, so carry that figure forward unchanged. Re-banking
3616
- * `usage` here would add the restored history to itself on every reload.
3617
- * - **Transcript starts empty**: nothing on screen accounts for the previous total, so the
3618
- * whole of it becomes banked. Without this the next turn would rewrite the row downwards
3619
- * to just that turn, losing the project's earlier spend.
3620
- *
3621
- * The baseline is deliberately NOT element state. This element is remounted repeatedly inside
3622
- * a single session (the host re-rendering its layout, popping out, docking) and a fresh
3623
- * instance re-deriving would take the second branch against rows that already hold the live
3624
- * transcript — banking the session as its own baseline. Keying on the session instead means a
3625
- * remount inherits the baseline that transcript was paired with.
3626
- */
3627
- private bankedUsageFor;
3628
- /**
3629
- * Re-baseline this session against an emptied transcript: what came before, plus what the
3630
- * transcript being discarded proved. `discarded` must be read BEFORE the live figures are
3631
- * reset, since that is the only record of the latter.
3632
- *
3633
- * Not the same as dropping the baseline. Dropping it re-derives through `persistenceActive`,
3634
- * and with persistence ON that carries the row's *old* banked figure forward — discarding
3635
- * every turn between the last baseline and the clear. The transcript is gone either way, so
3636
- * the branch that reads it as authoritative no longer applies.
3637
- *
3638
- * Derived from the memoised baseline and the live figure rather than from the project's row,
3639
- * because a row is only as current as the last `finalizeCostSession` — which skips its write
3640
- * while the ledger is unloaded, and while the session has no usage yet.
3406
+ * Re-baseline this session against an emptied transcript — see
3407
+ * `CostLedgerController.rebaselineOnClearedTranscript`. `discarded` must be read
3408
+ * BEFORE the live figures are reset, since that is the only record of the latter.
3641
3409
  */
3642
3410
  private rebaselineOnClearedTranscript;
3643
3411
  showConfirmationDialog(confirmationMessage: string, modalParent: HTMLElement, onConfirm: () => void, onCancel?: () => void): void;
@@ -3645,48 +3413,6 @@ export declare class FoundationAiAssistant extends GenesisElement {
3645
3413
  removeCostSessionFromHistory(id: string): void;
3646
3414
  /** Clear all persisted build history for this assistant scope. */
3647
3415
  clearCostSessionHistory(): void;
3648
- private getCostHistoryScope;
3649
- private reloadCostSessionHistory;
3650
- /**
3651
- * The persistence provider, but only when it implements BOTH cost-history methods.
3652
- *
3653
- * Both or neither: a provider that can save but not load would silently accumulate a
3654
- * ledger nobody ever reads back, so a partial implementation is treated as absent and
3655
- * falls back to `localStorage`.
3656
- */
3657
- private costHistoryProvider;
3658
- /**
3659
- * Adopt `records` as the live list, and schedule a write to whichever backend owns cost
3660
- * history — the provider when the host implements it, else `localStorage`.
3661
- *
3662
- * The list is adopted synchronously because that is what the Usage tab renders; only the
3663
- * *write* is deferred. This runs on every usage change (about once per LLM call, plus once
3664
- * per sub-agent turn) and each write serialises the whole ledger, so coalescing cuts a
3665
- * per-turn `localStorage.setItem` or network round-trip down to one per quiet period. Safe
3666
- * because the write is a full replace of idempotent state: the newest call wins and no
3667
- * intermediate state is meaningful. Flushed on `pagehide` and disconnect so nothing is lost.
3668
- */
3669
- private persistCostHistory;
3670
- /**
3671
- * Write the pending ledger now. Safe to call when nothing is pending.
3672
- *
3673
- * Provider writes are fire-and-forget and best-effort: the ledger is accounting, not
3674
- * conversation content, so a failed write is logged and never surfaced into the chat. On
3675
- * `pagehide` the `localStorage` path completes synchronously; a provider write is subject to
3676
- * the same unload race as every other network save the assistant makes.
3677
- */
3678
- private flushCostHistory;
3679
- /**
3680
- * Finalize the previous session when the host-provided title changes — a rename should land on
3681
- * the row under its old name rather than silently relabelling accrued spend.
3682
- *
3683
- * Only when the session key still matches the one that title accrued under. Hosts change
3684
- * `costHistory.title` on project SWITCH too, and if the key has already flipped, this would
3685
- * upsert the outgoing project's live usage onto the incoming project's row as
3686
- * `banked + old live`, inflating it. Under the old append model a mis-keyed write left a stray
3687
- * row; with the upsert it overwrites a real one.
3688
- */
3689
- private syncActiveCostSessionTitle;
3690
3416
  /** Active model id resolved from the AIProvider, if exposed. */
3691
3417
  get activeModel(): string | undefined;
3692
3418
  set activeModel(value: string | undefined);
@@ -3699,14 +3425,20 @@ export declare class FoundationAiAssistant extends GenesisElement {
3699
3425
  private _suggestionsGeneration;
3700
3426
  get inputValue(): string;
3701
3427
  set inputValue(value: string);
3702
- attachments: ChatAttachment[];
3703
- attachmentErrors: string[];
3428
+ /** Attachments pending on the next message. */
3429
+ get attachments(): ChatAttachment[];
3430
+ set attachments(value: ChatAttachment[]);
3431
+ /** Rejection messages for files the last pick refused. */
3432
+ get attachmentErrors(): string[];
3433
+ set attachmentErrors(value: string[]);
3704
3434
  /** Whether the loading spinner is currently visible. Controlled by the loading delay timer. */
3705
3435
  showLoadingIndicator: boolean;
3706
3436
  /** Bump to re-render app settings toggles when the host provider notifies a change. */
3707
- appSettingsRevision: number;
3437
+ get appSettingsRevision(): number;
3438
+ set appSettingsRevision(value: number);
3708
3439
  /** Whether the settings modal is open. */
3709
- settingsOpen: boolean;
3440
+ get settingsOpen(): boolean;
3441
+ set settingsOpen(value: boolean);
3710
3442
  /**
3711
3443
  * True while `downloadDebugLog` is building the log (GENC-1351 §5.8). Drives the
3712
3444
  * download button's disabled + spinner state — the lifetime-stitch path is async
@@ -3716,7 +3448,8 @@ export declare class FoundationAiAssistant extends GenesisElement {
3716
3448
  */
3717
3449
  downloadingDebugLog: boolean;
3718
3450
  /** Active tab inside the settings modal. */
3719
- settingsModalTab: SettingsModalTab;
3451
+ get settingsModalTab(): SettingsModalTab;
3452
+ set settingsModalTab(value: SettingsModalTab);
3720
3453
  /** Bound to the design-system modal via the template ref. */
3721
3454
  settingsModal?: Modal;
3722
3455
  /**
@@ -3795,16 +3528,33 @@ export declare class FoundationAiAssistant extends GenesisElement {
3795
3528
  persistenceModal?: Modal;
3796
3529
  /** Bound to the settings modal tab bar — used to resync selection after reopen. */
3797
3530
  settingsTabsEl?: SettingsModalTabsElement;
3798
- /** Light-DOM nodes assigned to settings modal slots (via `slotted()` bindings). */
3799
- settingsModelSlotted: HTMLElement[];
3800
- settingsAppSlotted: HTMLElement[];
3801
- settingsCostCurrentSlotted: HTMLElement[];
3802
- settingsCostHistorySlotted: HTMLElement[];
3803
- /** Persisted finalized cost sessions shown on the Usage tab build history list. */
3804
- costSessionHistory: CostSessionRecord[];
3531
+ /**
3532
+ * Light-DOM nodes assigned to settings modal slots. The template's `slotted()`
3533
+ * bindings write these by property NAME, so each needs a real setter here even
3534
+ * though the controller owns the arrays.
3535
+ */
3536
+ get settingsModelSlotted(): HTMLElement[];
3537
+ set settingsModelSlotted(value: HTMLElement[]);
3538
+ get settingsAppSlotted(): HTMLElement[];
3539
+ set settingsAppSlotted(value: HTMLElement[]);
3540
+ get settingsCostCurrentSlotted(): HTMLElement[];
3541
+ set settingsCostCurrentSlotted(value: HTMLElement[]);
3542
+ get settingsCostHistorySlotted(): HTMLElement[];
3543
+ set settingsCostHistorySlotted(value: HTMLElement[]);
3805
3544
  /** Whether the splash overlay is currently showing (no messages and showSplash is enabled). Reflected as a boolean attribute on the host. */
3806
3545
  showingSplash: boolean;
3807
3546
  private driver?;
3547
+ /**
3548
+ * This element's driver as an orchestrator, or `undefined` when the session is
3549
+ * running a bare `ChatDriver`.
3550
+ *
3551
+ * The five call sites that used to test the type themselves now read this
3552
+ * (§11.2 constraint 3 of `docs/code-health.md`): the agent pin, the flow-owner
3553
+ * lock, the wire-up in `wireDriver`, and the two `driverKind` diagnostics. The
3554
+ * type test itself lives in `asOrchestrator`, shared with the session
3555
+ * persister.
3556
+ */
3557
+ private get orchestrator();
3808
3558
  private driverCleanup?;
3809
3559
  private loadingTimer;
3810
3560
  private unsubBus?;
@@ -3812,29 +3562,6 @@ export declare class FoundationAiAssistant extends GenesisElement {
3812
3562
  private _unsubBudgetLatch?;
3813
3563
  /** Unsubscribe handle for the provider-registry change listener (observable registries only). */
3814
3564
  private unsubProviderRegistry?;
3815
- /** Unsubscribe handle for {@link (AssistantAppSettingsProvider:interface).subscribe}. */
3816
- private unsubAppSettingsProvider?;
3817
- /** Title tracked for the in-flight cost session (used when `header-title` changes). */
3818
- private _activeCostSessionTitle;
3819
- /**
3820
- * Session key `_activeCostSessionTitle` was captured under, so a title change can tell a rename
3821
- * (same key) from a project switch (key already flipped) — see `syncActiveCostSessionTitle`.
3822
- */
3823
- private _activeCostSessionKey?;
3824
- /**
3825
- * Whether the row list reflects storage yet. Gates cost writes so a project's banked
3826
- * spend is never derived from a ledger that simply hasn't arrived.
3827
- */
3828
- private _costHistoryLoaded;
3829
- /**
3830
- * Set when a provider read failed. Latches writes off for this page-load: `saveCostHistory`
3831
- * replaces the whole stored list, so writing anything derived from the empty list we fell back
3832
- * to would destroy rows we never managed to read.
3833
- */
3834
- private _ledgerUnreadable;
3835
- /** Pending (adopted but unwritten) ledger changes — see `persistCostHistory`. */
3836
- private _ledgerDirty;
3837
- private _ledgerFlushTimer?;
3838
3565
  private _handlePageHide;
3839
3566
  private haloStartPublished;
3840
3567
  /** Fingerprint of the agents array used to build the current driver. Used by agentsChanged to skip spurious rebuilds. */
@@ -3844,20 +3571,15 @@ export declare class FoundationAiAssistant extends GenesisElement {
3844
3571
  /** Bound to the rapid-text-area chat input via the template ref. */
3845
3572
  chatInputEl?: HTMLElement;
3846
3573
  /**
3847
- * Explicit composer height (px) once the user has dragged the resize handle;
3848
- * `null` keeps the default auto-grow behaviour. Written into the
3849
- * `--composer-height` custom property via the `composerHeightStyle` getter.
3850
- */
3851
- composerHeightPx: number | null;
3852
- /** Pointer Y and composer height captured at the start of a resize drag. */
3853
- private _resizeStartY;
3854
- private _resizeStartHeight;
3855
- /** The composer's natural (un-resized) height, measured on first drag — used
3856
- * as the lower bound so the box can't be dragged smaller than it started. */
3857
- private _composerNaturalHeight;
3858
- /** Upper bound for the current drag — capped to the headroom above so growing
3859
- * the composer never pushes the input row's buttons off the bottom. */
3860
- private _resizeMaxHeight;
3574
+ * A get/set pair over one of this element's own properties, for a controller
3575
+ * that drives state it does not own. Written as a helper rather than two inline
3576
+ * closures per call: a pair of same-typed closures is easy to transpose by
3577
+ * accident, and a single object cannot be.
3578
+ */
3579
+ private accessor;
3580
+ /** Explicit composer height once dragged; `null` keeps the default auto-grow. */
3581
+ get composerHeightPx(): number | null;
3582
+ set composerHeightPx(value: number | null);
3861
3583
  /**
3862
3584
  * True when the user's last interaction (pointer or keyboard focus) was inside
3863
3585
  * the assistant. Defaults to true so the assistant claims focus on first mount.
@@ -4048,18 +3770,6 @@ export declare class FoundationAiAssistant extends GenesisElement {
4048
3770
  * window is known.
4049
3771
  */
4050
3772
  private syncContextGuard;
4051
- /**
4052
- * Guard policy pushed to the driver, or `undefined` when the host has switched
4053
- * the gate off — so `enabled: false` really does mean no local blocking of any
4054
- * kind, UI or mid-loop.
4055
- *
4056
- * A MARGIN, not a threshold: the driver resolves the window of the provider
4057
- * each call actually goes to. The fallback window rides along because the
4058
- * driver cannot learn it any other way — without it, a host that knows its
4059
- * window while its provider does not report one had `fallbackContextLimit`
4060
- * protecting the composer and nothing else.
4061
- */
4062
- private get contextGuardPolicy();
4063
3773
  private loadProviderStatuses;
4064
3774
  chatConfigChanged(): void;
4065
3775
  /**
@@ -4141,46 +3851,10 @@ export declare class FoundationAiAssistant extends GenesisElement {
4141
3851
  * `contains()` check cannot tell "clicked the menu" from "clicked the
4142
3852
  * transcript". The path crosses the boundary and carries both.
4143
3853
  */
4144
- private _dismissPointerHandler?;
4145
- private _dismissKeyHandler?;
4146
- private _dismissResizeHandler?;
4147
- private closeOpenComposerMenus;
4148
- /**
4149
- * Places an open composer menu directly above the control that opened it.
4150
- *
4151
- * CSS alone cannot: the menu is a child of `.composer` — which is what lets its
4152
- * width be capped against the composer rather than against a button — while its
4153
- * anchor is a button several levels down the bar, and no selector can read a
4154
- * sibling's offset. So the left edge is measured on open and written as a
4155
- * custom property.
4156
- *
4157
- * Clamped to the composer, so a menu opened from a control near the right edge
4158
- * of a narrow panel slides back inside instead of overhanging. The animation
4159
- * origin is written separately and keeps pointing at the button's centre even
4160
- * after such a shift — the menu still grows out of the thing that opened it,
4161
- * which is the whole point of anchoring it there.
4162
- */
4163
- private positionComposerMenu;
4164
- /** Re-place whichever menu is open. Cheap enough to run from a resize. */
4165
- private repositionOpenComposerMenu;
4166
- private syncComposerMenuDismissal;
4167
- private teardownComposerMenuDismissal;
4168
- /**
4169
- * Vertical space an open menu must leave free above the button, covering BOTH
4170
- * the gap the menu sits off the button by (8px, declared in design units in
4171
- * main.styles.ts) and the breathing room wanted above it. The two are lumped
4172
- * together because only the second half is a free choice: the first is fixed by
4173
- * the stylesheet and simply has to be subtracted from the headroom, and a
4174
- * constant named for only one of them invites someone to "fix" a gap that is
4175
- * doing exactly what it should.
4176
- */
4177
- private static readonly MENU_HEADROOM_RESERVE;
4178
- private readonly _agentPickerToggle;
4179
3854
  toggleAgentPicker(): void;
4180
3855
  /** Two-step confirm for the destructive Clear action; reset whenever the menu toggles. */
4181
3856
  confirmingClear: boolean;
4182
3857
  confirmingCompact: boolean;
4183
- private readonly _sessionMenuToggle;
4184
3858
  /**
4185
3859
  * Whether the lifecycle-menu button shows. It occupies the stop-button slot,
4186
3860
  * so it only appears when idle (mutually exclusive with Stop). Gated on
@@ -4218,20 +3892,18 @@ export declare class FoundationAiAssistant extends GenesisElement {
4218
3892
  /** Whether persistence is currently active (on + a provider) for this session. */
4219
3893
  get persistenceActive(): boolean;
4220
3894
  /**
4221
- * Context-headroom thresholds for the active model (GENC-1567) — the warning
4222
- * and block lines, plus the tail budget compaction aims at.
3895
+ * The context gate's derived surface (GENC-1567). Every member below delegates to
3896
+ * `ContextGateController` — the names are this element's public API and the
3897
+ * template's bindings, so they stay put while the derivation moves.
4223
3898
  *
4224
3899
  * The per-agent `contextReserve` IS folded in, via the driver (GENC-1582). It
4225
3900
  * used to be described here as a separate, later check made when an agent took
4226
3901
  * ownership of a flow — but there is no such check, and by flow entry the send
4227
- * has already been allowed. Resolving it here, before the send, is what makes it
4228
- * a gate rather than a label.
3902
+ * has already been allowed. Resolving it before the send is what makes it a gate
3903
+ * rather than a label.
4229
3904
  *
4230
- * Overhead is read off `messages` (the Redux mirror) rather than the driver's
4231
- * own history, which the compaction plan uses. That is safe rather than sloppy:
4232
- * `estimateSystemOverhead` only looks at messages BEFORE the first usage-bearing
4233
- * one, and the two lists agree completely that early — the drift between mirror
4234
- * and driver history is only ever at the tail.
3905
+ * None carries `@volatile`: the decorators live on the controller's getters, and
3906
+ * they reach the binding observer from there — see `docs/code-health.md` §11.4.
4235
3907
  */
4236
3908
  get contextBudget(): ResolvedContextBudget;
4237
3909
  /**
@@ -4252,150 +3924,31 @@ export declare class FoundationAiAssistant extends GenesisElement {
4252
3924
  * `contextReserve` is a plain number by design — see `ContextReserveInput` for
4253
3925
  * why a resolver form was dropped rather than accepted and ignored.
4254
3926
  *
3927
+ * Stays on the element rather than moving into `ContextGateController` because it
3928
+ * reaches the driver, which no extracted controller may do (`docs/code-health.md`
3929
+ * §11.2 constraint 2). The controller receives the resolved value as a callback.
3930
+ *
4255
3931
  * @internal
4256
3932
  */
4257
3933
  private get contextReserve();
4258
- /**
4259
- * What compacting right now would reclaim, or `null` when it would not be
4260
- * worth it (GENC-1567). Sized against {@link FoundationAiAssistant.contextBudget},
4261
- * so the plan aims at this model's window rather than a fixed default.
4262
- *
4263
- * Reads `messages` to stay reactive as the conversation grows: the driver's
4264
- * history is not a tracked observable, so every consumer of this keys its
4265
- * re-evaluation off the Redux mirror.
4266
- */
4267
3934
  get compactionPlan(): CompactionProjection | null;
4268
- /**
4269
- * Tooltip and hint for the Compact row — why it is or is not available.
4270
- *
4271
- * Replaces a fixed "Not enough conversation yet to be worth compacting", which
4272
- * since GENC-1567 could be flatly wrong: a long conversation whose tail is one
4273
- * indivisible tool result has plenty of conversation and still cannot be
4274
- * compacted. Reading the plan means the hint states the actual reason.
4275
- *
4276
- * @internal
4277
- */
4278
- get compactHint(): string;
4279
- /**
4280
- * How close this conversation is to the model's context window (GENC-1567).
4281
- *
4282
- * **Derived, never latched** — the opposite of {@link FoundationAiAssistant.blocked}.
4283
- * A budget wall stays set because nothing the user does fixes it; this clears
4284
- * itself the moment the conversation shrinks, which is precisely what the user
4285
- * is being asked to do. The two must not be conflated: `blocked` is also
4286
- * deliberately preserved across `resetSession`, and a context gate obviously
4287
- * must not survive starting a new chat.
4288
- *
4289
- * **A flow in progress is never blocked, only warned.** Machine state lives
4290
- * outside the transcript, so a stateful flow is not desynced by compaction —
4291
- * but the summary can still drop detail the flow's own prompt leans on, and
4292
- * blocking mid-journey would strand the user between a flow they cannot finish
4293
- * and a compaction that is a poor idea right there. Letting the flow complete
4294
- * and gating on the way out is the lesser harm; the headroom reserve exists so
4295
- * this is rare rather than routine.
4296
- */
4297
3935
  /**
4298
3936
  * WHY the composer is blocked, when it is (GENC-1582). `undefined` unless
4299
- * {@link FoundationAiAssistant.contextGate} is `'blocked'`.
4300
- *
4301
- * A cause rather than a fourth gate level: the severity is identical — sends are
4302
- * refused either way — and every consumer switches on the three levels. What
4303
- * differs is what the user can do about it, which is worth splitting because the
4304
- * two answers have no remedy in common.
4305
- *
4306
- * - `'transcript'` — the conversation has grown into the reserve. Compacting or
4307
- * clearing helps, and the copy says so.
4308
- * - `'model'` — the active agent's reserve meets or exceeds the whole window, so
4309
- * `blockAt` is 0 and EVERY message is refused, including the first of an empty
4310
- * chat. Neither compacting nor clearing can help: the constraint is the model,
4311
- * not the transcript. Before this existed the user was told to clear the chat,
4312
- * did, and hit the identical block with no way to understand why.
4313
- *
4314
- * Keyed off `blockAt === 0`, deliberately NOT off whether a compaction plan
4315
- * exists. A null plan has two unrelated causes — nothing left worth summarizing,
4316
- * versus a window that cannot host the agent at all — and conflating them is what
4317
- * produced the misleading copy.
4318
- *
4319
- * Note the block itself is correct in both cases and must stay: an agent that
4320
- * measurably cannot complete a turn in this window would otherwise start one and
4321
- * die mid-loop on a provider context error, with partial writes and no clean
4322
- * exit. Refusing up front is the better failure.
3937
+ * {@link FoundationAiAssistant.contextGate} is `'blocked'`. See
3938
+ * `ContextGateController.blockCause` for the `'transcript'` / `'model'` split
3939
+ * and why it is a cause rather than a fourth gate level.
4323
3940
  *
4324
3941
  * @internal
4325
3942
  */
4326
3943
  get contextBlockCause(): 'transcript' | 'model' | undefined;
4327
3944
  get contextGate(): 'ok' | 'warn' | 'blocked';
4328
- /**
4329
- * What the composer notice says about the context gate.
4330
- *
4331
- * The copy is a function of whether compaction can actually clear the gate,
4332
- * which is why it reads {@link FoundationAiAssistant.compactionPlan} rather
4333
- * than assuming. Telling someone to compact when the projection says it would
4334
- * not free enough — a tail that is one indivisible tool result, say — sends
4335
- * them into a summarizer call that leaves them exactly where they were.
4336
- *
4337
- * @internal
4338
- */
4339
3945
  get contextGateReason(): string;
4340
- /**
4341
- * Short label the session-menu pill wears while the gate is active — the
4342
- * primary visible signal, in place of a banner.
4343
- *
4344
- * Borrowed from the agent pin, which is the established way this composer says
4345
- * "a mode is in force": a pill that grows a label rather than a strip of prose
4346
- * above the input. It states the CONDITION only; the remedy lives in the
4347
- * placeholder once the composer is actually blocked, which is the moment the
4348
- * user needs to be told what to do rather than merely warned.
4349
- *
4350
- * @internal
4351
- */
4352
3946
  get contextGateLabel(): string;
4353
- /**
4354
- * Whether the composer notice has anything to show — a budget wall or the
4355
- * context gate. One region for both, so the live region stays single and
4356
- * registered (see the banner template's accessibility note).
4357
- *
4358
- * @internal
4359
- */
3947
+ get compactHint(): string;
3948
+ get compactable(): boolean;
4360
3949
  get composerNoticeVisible(): boolean;
4361
- /**
4362
- * Text for that region. A budget wall outranks the context gate: it is the
4363
- * condition the user cannot clear, so it must not be hidden behind advice
4364
- * about compacting that would not restore sending anyway.
4365
- *
4366
- * For the context gate the region is present and announced but not drawn — see
4367
- * {@link FoundationAiAssistant.composerNoticeSrOnly}.
4368
- *
4369
- * @internal
4370
- */
4371
3950
  get composerNoticeText(): string;
4372
- /**
4373
- * Whether the notice region should be announced but not drawn.
4374
- *
4375
- * The context gate shows itself visually through the pill's label, the
4376
- * composer outline and the placeholder — a strip of prose on top of those
4377
- * would be a fourth statement of the same thing. But dropping the region
4378
- * outright would be an accessibility regression, and a documented one: a
4379
- * `role="status"` region announces MUTATIONS to a region already being
4380
- * observed, and it is the only announcement available here, because the
4381
- * composer flips to `disabled` at the same moment and a disabled control is out
4382
- * of the tab order — so neither the placeholder nor an `aria-label` on it is
4383
- * ever voiced.
4384
- *
4385
- * So the region stays mounted, keeps its text and keeps announcing; only the
4386
- * visual treatment is withheld. A budget wall still draws its banner, because
4387
- * that condition has no other visible expression.
4388
- *
4389
- * @internal
4390
- */
4391
3951
  get composerNoticeSrOnly(): boolean;
4392
- /**
4393
- * Whether the menu's "Compact" action would do anything (GENC-1351 §5.7,
4394
- * GENC-1567). Delegates to the driver's plan so the gate uses the exact same
4395
- * history `compact()` acts on, judged against the same budget — one source of
4396
- * truth for both whether a cut is legal and whether it reclaims enough.
4397
- */
4398
- get compactable(): boolean;
4399
3952
  toggleSessionMenu(): void;
4400
3953
  /**
4401
3954
  * Set persistence on/off for this session (the menu switch). Reassigns the
@@ -4480,33 +4033,40 @@ export declare class FoundationAiAssistant extends GenesisElement {
4480
4033
  }): boolean;
4481
4034
  /** Whether the picker toggle button should appear. Mirrors the picker's own visibility rule. */
4482
4035
  get agentPickerEnabled(): boolean;
4483
- /** Whether the settings modal AI Model section has slotted app content, context usage or budget figures. */
4036
+ /**
4037
+ * The settings modal's derived surface. Every member below delegates to
4038
+ * `SettingsModalController` — the names are this element's public API and the
4039
+ * 943-line template's bindings, so they stay put while the logic moves into the
4040
+ * `settings-modal.ts` that folder never had (`docs/code-health.md` §3).
4041
+ *
4042
+ * None carries `@volatile`: the decorators live on the controller's getters, and
4043
+ * they reach the binding observer from there — see `docs/code-health.md` §11.4.
4044
+ */
4484
4045
  get settingsModelSectionVisible(): boolean;
4485
4046
  /** Built-in context window indicator (generic platform concern). */
4486
4047
  get settingsContextUsageVisible(): boolean;
4048
+ /**
4049
+ * The context indicator's figures, pre-formatted, or `undefined` when there is
4050
+ * nothing measurable to show — see {@link SettingsContextUsage}.
4051
+ *
4052
+ * @internal
4053
+ */
4054
+ get settingsContextUsage(): SettingsContextUsage | undefined;
4487
4055
  /**
4488
4056
  * Built-in per-vendor budget meter (GENC-1464) — the money sibling of
4489
4057
  * {@link FoundationAiAssistant.settingsContextUsageVisible}, with the same
4490
- * shape of gate: not configured off, and there is data to show. "Data" here
4491
- * is "at least one vendor has figures", so a host that feeds nothing (or only
4492
- * unlimited vendors) gets no meter and no empty container — exactly the
4493
- * context indicator's absent case.
4494
- *
4495
- * Answered from the RAW slice state rather than from
4496
- * {@link FoundationAiAssistant.settingsBudgetRows}, whose `.length` would
4497
- * format every row (a `formatUsd` pair and a `budgetPercentOf` each) only to
4498
- * ask whether any exist. This is read several times per render pass — the
4499
- * getter, the section gate, the template's `when` — so it stays as cheap as
4500
- * the context sibling's `!= null`.
4058
+ * shape of gate: not configured off, and there is data to show. See
4059
+ * `BudgetController.settingsUsageVisible` for why it reads the raw slice
4060
+ * rather than counting the rows.
4501
4061
  *
4502
4062
  * @internal
4503
4063
  */
4504
4064
  get settingsBudgetUsageVisible(): boolean;
4505
4065
  /**
4506
4066
  * The meter's rows — one per vendor that currently has figures, in
4507
- * `BUDGETED_VENDORS` order (the proxy's own metered list) so the rendering is
4508
- * stable however the host ordered its feed calls. Pre-formatted; see
4509
- * {@link SettingsBudgetRow} for why the text is unclamped while the bar is.
4067
+ * `BUDGETED_VENDORS` order so the rendering is stable however the host ordered
4068
+ * its feed calls. Pre-formatted; see {@link SettingsBudgetRow} for why the text
4069
+ * is unclamped while the bar is.
4510
4070
  *
4511
4071
  * @internal
4512
4072
  */
@@ -4523,38 +4083,25 @@ export declare class FoundationAiAssistant extends GenesisElement {
4523
4083
  get appSettingsTogglesHeading(): AssistantAppSettingsHeading | undefined;
4524
4084
  getAppSettingValue(id: string): boolean;
4525
4085
  setAppSettingValue(id: string, value: boolean): void;
4526
- private wireAppSettingsProvider;
4527
- private unwireAppSettingsProvider;
4528
4086
  get settingsCostCurrentSectionVisible(): boolean;
4529
4087
  /**
4530
4088
  * Whether the build-history section has anything in it — the built-in row list, or a host's own
4531
- * markup in the `settings-cost-history` slot.
4532
- *
4533
- * Slotted content is checked FIRST, ahead of `enabled`. That flag governs the *built-in* ledger:
4534
- * it gates the row writes, the reload, and the lifetime sum — and it defaults to `false`
4535
- * (`cost?.enabled === true`), while the Usage tab itself shows for `showCost`/`showTokens`, which
4536
- * default true. So a host can legitimately render its own build history into the slot and never
4537
- * opt into our `localStorage` ledger. Short-circuiting on `enabled` above the slot check hid that
4538
- * host's markup outright, since the slot is the only thing in the container.
4089
+ * markup in the `settings-cost-history` slot. See
4090
+ * `SettingsModalController.costHistorySectionVisible` for why slotted content is
4091
+ * checked ahead of `enabled`.
4539
4092
  */
4540
4093
  get settingsCostHistorySectionVisible(): boolean;
4541
4094
  /**
4542
- * Whether the "Build history" heading has anything under it.
4543
- *
4544
- * Broader than {@link FoundationAiAssistant.settingsCostHistorySectionVisible} because the
4545
- * heading also covers the lifetime total, which renders before the first row exists (rows are
4546
- * written as spend accrues, the lifetime section only needs history switched on). Gating the
4547
- * heading on the rows alone left that total sitting under nothing.
4095
+ * Whether the "Build history" heading has anything under it — broader than
4096
+ * {@link FoundationAiAssistant.settingsCostHistorySectionVisible}, because the
4097
+ * heading also covers the lifetime total, which renders before the first row exists.
4548
4098
  */
4549
4099
  get settingsCostHistoryHeadingVisible(): boolean;
4550
4100
  /** Whether the built-in Usage tab summary block has anything to show. */
4551
4101
  get settingsCostSummaryVisible(): boolean;
4552
4102
  /**
4553
- * Whether the lifetime-usage section has anything to say.
4554
- *
4555
- * Requires build history: with it off there are no rows, so `cumulativeUsage` falls back to
4556
- * the live session and the section would restate the block directly below it under a
4557
- * "lifetime" heading it hasn't earned.
4103
+ * Whether the lifetime-usage section has anything to say. Requires build history
4104
+ * AND a resolvable state key — see `SettingsModalController.costLifetimeVisible`.
4558
4105
  */
4559
4106
  get settingsCostLifetimeVisible(): boolean;
4560
4107
  /** Whether the Usage tab should appear in the settings modal. */
@@ -4569,15 +4116,6 @@ export declare class FoundationAiAssistant extends GenesisElement {
4569
4116
  setSettingsModalTab(tab: SettingsModalTab): void;
4570
4117
  /** Sync the active settings modal tab when the design-system tab bar changes. */
4571
4118
  onSettingsModalTabsChange(event: Event): void;
4572
- /** Ensure the settings tab is selected when the modal opens. */
4573
- private resetSettingsModalTabsOnOpen;
4574
- /** Return to the settings tab when the modal closes on Cost. */
4575
- private resetSettingsModalTabsOnClose;
4576
- /**
4577
- * Snap the tab underline to the settings tab without animating from Cost.
4578
- * Used after hide/show when aria state is correct but the indicator drifted.
4579
- */
4580
- private snapSettingsModalTabIndicator;
4581
4119
  /** Hint text for the currently pinned agent, if any. Used in the toggle button tooltip. */
4582
4120
  get pinnedAgentHint(): string | undefined;
4583
4121
  /**
@@ -4630,14 +4168,13 @@ export declare class FoundationAiAssistant extends GenesisElement {
4630
4168
  */
4631
4169
  private buildDiagnosticEntries;
4632
4170
  /**
4633
- * The single `meta-snapshot` entry for right now — the export-time `meta` block
4634
- * (agent summary, active prompt/state, context + cost) plus the `dedupSignature`
4635
- * the forward-capture delta keys on.
4171
+ * The `meta-snapshot` entry for right now. The assembly is a pure transformation
4172
+ * in `buildMetaSnapshot`; this gathers what it reports.
4636
4173
  *
4637
- * Separate from `buildDiagnosticEntries` because the download path needs a
4638
- * FRESH one on its own: the persisted stream only re-appends this block when the
4639
- * near-static config changes, so the newest stored snapshot's volatile half — the
4640
- * `context` figures especially — is typically frozen at the session's first flush.
4174
+ * The live agent comes from the DRIVER, not `this.activeAgent` (the redux-stripped
4175
+ * projection, no functions) and not a lookup in `this.agents` (which may mint
4176
+ * fresh, never-activated configs) — only the driver holds the instance whose
4177
+ * `onActivate` ran, so only its `getDebugSnapshot` closure holds state.
4641
4178
  */
4642
4179
  private buildMetaSnapshot;
4643
4180
  downloadDebugLog(): Promise<void>;
@@ -4743,13 +4280,6 @@ export declare class FoundationAiAssistant extends GenesisElement {
4743
4280
  handleFileSelect(e: Event): void;
4744
4281
  removeAttachment(attachment: ChatAttachment): void;
4745
4282
  removeAttachmentError(message: string): void;
4746
- private isAcceptedFile;
4747
- private readFileAsText;
4748
- /** The base64 payload of a file, without any `data:` prefix — the raw form
4749
- * `ChatImageAttachment.data` specifies (transports add their own framing). */
4750
- private readFileAsBase64;
4751
- private processFiles;
4752
- private loadSelectedFiles;
4753
4283
  handleSendClick(): void;
4754
4284
  /**
4755
4285
  * User clicked Stop. Cancels the current turn via the driver and flips the
@@ -4826,23 +4356,12 @@ export declare class FoundationAiAssistant extends GenesisElement {
4826
4356
  * mid-turn.
4827
4357
  */
4828
4358
  get sendButtonVisible(): boolean;
4829
- /** Whether the message input shows a drag handle to resize its height.
4830
- * On by default; hosts opt out via `chatConfig.ui.resizableInput: false`. */
4359
+ /** Whether the message input shows a drag handle to resize its height. */
4831
4360
  get isComposerResizable(): boolean;
4832
- /** Inline style for the chat input: writes the chosen height into the
4833
- * `--composer-height` custom property (read by `.chat-input` + its control in
4834
- * `main.styles.ts`). Empty until the user drags (or when resizing is disabled),
4835
- * so the default applies. */
4361
+ /** Inline style carrying the dragged height into `--composer-height`. */
4836
4362
  get composerHeightStyle(): string;
4837
- /** Begin a composer resize drag. Captures the pointer so move/up keep firing
4838
- * on the handle even when the cursor leaves its 7px band. */
4839
4363
  onComposerResizeStart(e: PointerEvent): void;
4840
- /** Resize while dragging. Dragging up (clientY decreases) grows the composer;
4841
- * the result is clamped between the natural height (lower) and the available
4842
- * headroom/max (upper). No-op unless a drag is active (the handle holds pointer
4843
- * capture), so plain hover-moves are ignored. */
4844
4364
  onComposerResizeMove(e: PointerEvent): void;
4845
- /** End a resize drag and release the pointer capture. */
4846
4365
  onComposerResizeEnd(e: PointerEvent): void;
4847
4366
  }
4848
4367
 
@@ -5704,6 +5223,32 @@ export declare interface SettingsBudgetRow {
5704
5223
  barValue: number;
5705
5224
  }
5706
5225
 
5226
+ /**
5227
+ * The settings-modal context-window indicator, pre-formatted — the context
5228
+ * sibling of {@link SettingsBudgetRow}, and pre-formatted for the same reason:
5229
+ * the template stays a pure binding layer, and a test can pin the exact text.
5230
+ *
5231
+ * `undefined` when there is nothing measurable to show, which is what keeps the
5232
+ * template free of non-null assertions. It used to read
5233
+ * `x.contextTokens!.toLocaleString()`, and the assertion was not merely untidy:
5234
+ * those bindings sit inside a `when(...)` block and re-evaluate in the SAME
5235
+ * notify batch as the guard that unmounts them, so a session Clear with the
5236
+ * modal open on the Usage tab threw two `TypeError`s per render. A shape that
5237
+ * can be absent cannot have that bug.
5238
+ *
5239
+ * @beta
5240
+ */
5241
+ export declare interface SettingsContextUsage {
5242
+ /** Prompt tokens on the last call, e.g. `12,480`. */
5243
+ used: string;
5244
+ /** The model's context window, e.g. `200,000`. */
5245
+ limit: string;
5246
+ /** Whole-number percent consumed. Unclamped, like the budget row's text. */
5247
+ percent: number;
5248
+ /** The bar's value, clamped to `0..100` — only the bar clamps. */
5249
+ barValue: number;
5250
+ }
5251
+
5707
5252
  /**
5708
5253
  * Tab identifiers for the modal tab bar.
5709
5254
  *