@genesislcap/ai-assistant 15.23.3 → 15.24.0

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 (46) hide show
  1. package/dist/ai-assistant.api.json +419 -50
  2. package/dist/ai-assistant.d.ts +217 -683
  3. package/dist/chat-driver.cjs.map +1 -1
  4. package/dist/chat-driver.mjs.map +1 -1
  5. package/dist/custom-elements.json +310 -609
  6. package/dist/dts/components/orchestrating-driver/orchestrating-driver.d.ts +16 -0
  7. package/dist/dts/components/orchestrating-driver/orchestrating-driver.d.ts.map +1 -1
  8. package/dist/dts/components/settings-modal/index.d.ts +1 -0
  9. package/dist/dts/components/settings-modal/index.d.ts.map +1 -1
  10. package/dist/dts/components/settings-modal/settings-modal.d.ts +215 -0
  11. package/dist/dts/components/settings-modal/settings-modal.d.ts.map +1 -0
  12. package/dist/dts/components/settings-modal/settings-modal.template.d.ts.map +1 -1
  13. package/dist/dts/main/attachment-controller.d.ts +66 -0
  14. package/dist/dts/main/attachment-controller.d.ts.map +1 -0
  15. package/dist/dts/main/budget-controller.d.ts +368 -0
  16. package/dist/dts/main/budget-controller.d.ts.map +1 -0
  17. package/dist/dts/main/composer-menu-controller.d.ts +113 -0
  18. package/dist/dts/main/composer-menu-controller.d.ts.map +1 -0
  19. package/dist/dts/main/composer-resize-controller.d.ts +80 -0
  20. package/dist/dts/main/composer-resize-controller.d.ts.map +1 -0
  21. package/dist/dts/main/context-gate-controller.d.ts +197 -0
  22. package/dist/dts/main/context-gate-controller.d.ts.map +1 -0
  23. package/dist/dts/main/cost-ledger-controller.d.ts +228 -0
  24. package/dist/dts/main/cost-ledger-controller.d.ts.map +1 -0
  25. package/dist/dts/main/main.d.ts +201 -722
  26. package/dist/dts/main/main.d.ts.map +1 -1
  27. package/dist/dts/main/main.types.d.ts +25 -0
  28. package/dist/dts/main/main.types.d.ts.map +1 -1
  29. package/dist/dts/main/meta-snapshot.d.ts +50 -0
  30. package/dist/dts/main/meta-snapshot.d.ts.map +1 -0
  31. package/dist/dts/state/persistence/session-persister.d.ts.map +1 -1
  32. package/dist/esm/components/orchestrating-driver/orchestrating-driver.js +18 -0
  33. package/dist/esm/components/settings-modal/index.js +1 -0
  34. package/dist/esm/components/settings-modal/settings-modal.js +436 -0
  35. package/dist/esm/components/settings-modal/settings-modal.template.js +15 -4
  36. package/dist/esm/main/attachment-controller.js +254 -0
  37. package/dist/esm/main/budget-controller.js +745 -0
  38. package/dist/esm/main/composer-menu-controller.js +234 -0
  39. package/dist/esm/main/composer-resize-controller.js +119 -0
  40. package/dist/esm/main/context-gate-controller.js +289 -0
  41. package/dist/esm/main/cost-ledger-controller.js +498 -0
  42. package/dist/esm/main/main.js +501 -2268
  43. package/dist/esm/main/meta-snapshot.js +108 -0
  44. package/dist/esm/state/persistence/session-persister.js +5 -4
  45. package/docs/migration-GENC-1580.md +209 -0
  46. package/package.json +16 -16
@@ -1,23 +1,18 @@
1
- import type { AggregateUsage, AIProviderRegistryStatusEntry, AIProviderType, ChatAttachment, ChatConfig, ChatDriverResult, ChatInputDuringExecutionMode, ChatMessage } from '@genesislcap/foundation-ai';
1
+ import type { AggregateUsage, AIProviderRegistryStatusEntry, AIProviderType, ChatAttachment, ChatConfig, ChatInputDuringExecutionMode, ChatMessage } from '@genesislcap/foundation-ai';
2
2
  import { AIProviderRegistry } from '@genesislcap/foundation-ai';
3
3
  import type { Modal } from '@genesislcap/rapid-design-system';
4
4
  import { GenesisElement } from '@genesislcap/web-core';
5
- import type { SettingsModalTab } from '../components/settings-modal';
5
+ import { type SettingsModalTab, type SettingsModalTabsElement } from '../components/settings-modal';
6
6
  import type { AgentConfig } from '../config/config';
7
7
  import { AssistantAppSettingsProvider, type AssistantAppSettingsHeading, type AssistantAppSettingsToggle } from '../provider/assistant-app-settings';
8
- import type { VendorBudgetFigures } from '../state/ai-assistant-slice';
9
8
  import type { DebugLog } from '../state/persistence/diagnostics';
10
9
  import type { SessionPersistenceConfig } from '../state/persistence/session-persistence-provider';
11
10
  import type { InteractionContext } from '../types/interaction-context';
12
11
  import { type CompactionProjection } from '../utils/context-tokens';
13
- import { type CostSessionModelEntry, type CostSessionRecord } from '../utils/cost-session-history';
14
- import { type ResolvedContextBudget } from '../utils/resolve-context-budget';
15
- import { type ResolvedCostHistoryConfig } from '../utils/resolve-cost-history-config';
16
- import type { AgentPickerMode, AiAssistantAnimation, AiAssistantState, PopoutMode, SettingsBudgetRow, SubmitMessageResult, SuggestionsState } from './main.types';
17
- /** Design-system tabs element bound to the settings modal tab bar. */
18
- type SettingsModalTabsElement = HTMLElement & {
19
- activeid?: string;
20
- };
12
+ import type { CostSessionModelEntry, CostSessionRecord } from '../utils/cost-session-history';
13
+ import type { ResolvedContextBudget } from '../utils/resolve-context-budget';
14
+ import type { ResolvedCostHistoryConfig } from '../utils/resolve-cost-history-config';
15
+ import type { AgentPickerMode, AiAssistantAnimation, AiAssistantState, PopoutMode, SettingsBudgetRow, SettingsContextUsage, SubmitMessageResult, SuggestionsState } from './main.types';
21
16
  /** Context window sizes (in tokens) for known models. */
22
17
  /** Three-star AI "sparkle" mark — the conventional AI signifier; default assistant avatar. */
23
18
  export declare const sparkleIconSvg = "<svg viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\"><path d=\"M9.5 5 C9.5 11.4 11.1 13 17.5 13 C11.1 13 9.5 14.6 9.5 21 C9.5 14.6 7.9 13 1.5 13 C7.9 13 9.5 11.4 9.5 5 Z\" fill=\"currentColor\"/><path d=\"M17 2.5 C17 5.7 17.8 6.5 21 6.5 C17.8 6.5 17 7.3 17 10.5 C17 7.3 16.2 6.5 13 6.5 C16.2 6.5 17 5.7 17 2.5 Z\" fill=\"currentColor\"/><path d=\"M17.5 15 C17.5 17 18 17.5 20 17.5 C18 17.5 17.5 18 17.5 20 C17.5 18 17 17.5 15 17.5 C17 17.5 17.5 17 17.5 15 Z\" fill=\"currentColor\"/></svg>";
@@ -26,36 +21,13 @@ export declare const userIconSvg = "<svg viewBox=\"0 0 24 24\" fill=\"none\" xml
26
21
  /** Genesis G-mark — opt-in assistant avatar (the default before the sparkle mark). */
27
22
  export declare const genesisIconSvg = "<svg viewBox=\"-4 0 30 26\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\"><path d=\"M18.7702 23.0437C18.7702 22.4675 18.8526 21.8089 19.0995 21.3149C19.3465 20.7386 19.6758 20.2447 20.0874 19.833C20.4991 19.4214 20.993 19.0921 21.5693 18.8451C22.1456 18.5982 22.7219 18.5158 23.2981 18.5158H25.9326V11.6828H19.1819V14.3172C19.1819 15.5521 18.6879 16.7047 17.8647 17.5279C17.0414 18.3512 15.8888 18.8451 14.7363 18.9275H6.7507V6.99025H16.4651V0.157227H3.54C2.63442 0.157227 1.72884 0.568855 1.07023 1.22746C0.411628 1.88606 0 2.79165 0 3.77955V22.2205C0 23.1261 0.411628 24.0316 1.07023 24.7726C1.72884 25.4312 2.63442 25.8428 3.54 25.8428H18.7702V23.0437Z\" fill=\"currentColor\"/></svg>";
28
23
  /**
29
- * Banner copy built from the figures a 402 actually reported, or `undefined`
30
- * when the proxy sent none.
31
- *
32
- * `undefined` is the meaningful return, not a fallback: paired with the
33
- * slice's "a re-latch without a reason keeps the existing explanation" rule, it
34
- * makes a figureless driver latch leave a host-supplied reason intact rather
35
- * than blanking it back to the default at the exact moment the wall is hit.
36
- *
37
- * Exported for the unit test that pins the formatting; not part of the element
38
- * API.
39
- *
40
- * @internal
24
+ * Budget formatting helpers, re-exported so they stay on this module's public
25
+ * surface (`src/index.ts` does `export * from './main/main'`). They live in
26
+ * `budget-controller.ts` with the code that composes them; moving them without
27
+ * this line would have quietly dropped two `@internal` exports from the package
28
+ * entry point, and broken the two test files that import them from here.
41
29
  */
42
- export declare function formatBlockedReason(budget?: Partial<Extract<ChatDriverResult, {
43
- reason: 'done';
44
- }>['budget']>, vendor?: AIProviderType): string | undefined;
45
- /**
46
- * Whole-number percentage of a budget consumed, for the meter (GENC-1464).
47
- *
48
- * Deliberately unclamped — spend can genuinely overshoot the cap (the last
49
- * allowed call may cross it), and the meter's TEXT shows the truth; only the
50
- * bar clamps, at the render site. The one special case is a `$0` cap, which the
51
- * platform treats as "no spend allowed" (not "unlimited"): it has no headroom
52
- * by definition, so the meter reads full rather than dividing by zero.
53
- *
54
- * Exported for the unit test that pins the arithmetic; not element API.
55
- *
56
- * @internal
57
- */
58
- export declare function budgetPercentOf(figures: VendorBudgetFigures): number;
30
+ export { budgetPercentOf, formatBlockedReason } from './budget-controller';
59
31
  /**
60
32
  * Foundation AI Assistant component.
61
33
  *
@@ -83,6 +55,35 @@ export declare function budgetPercentOf(figures: VendorBudgetFigures): number;
83
55
  * @beta
84
56
  */
85
57
  export declare class FoundationAiAssistant extends GenesisElement {
58
+ private readonly composerResize;
59
+ private readonly composerMenus;
60
+ private readonly attachmentCtrl;
61
+ /**
62
+ * Pure derivation — owns no state. Reaches the driver only through the two
63
+ * callbacks below, so it stays clear of the stateful-agent surface (§11.2
64
+ * constraint 2) and of redux entirely.
65
+ */
66
+ private readonly contextGateCtrl;
67
+ /**
68
+ * Cost accounting and the persisted build-history ledger (GENC-1475). Reads the live
69
+ * session through callbacks and owns the rows, the load/write latches and the debounce
70
+ * timer — the element keeps only the names the settings modal binds.
71
+ */
72
+ private readonly costLedger;
73
+ /**
74
+ * The AI-spend budget: vendor walls, the derived composer lock, the banner copy
75
+ * and the settings meter (GENC-1464).
76
+ *
77
+ * Takes the session store itself rather than a callback per field — see
78
+ * `BudgetDeps` for why this one differs from its siblings.
79
+ */
80
+ private readonly budget;
81
+ /**
82
+ * The settings modal (`docs/code-health.md` §3). Reaches the two design-system
83
+ * refs through callbacks, because the template writes them by name — see
84
+ * `SettingsModalDeps`.
85
+ */
86
+ private readonly settings;
86
87
  providerRegistry: AIProviderRegistry;
87
88
  appSettingsProvider: AssistantAppSettingsProvider;
88
89
  designSystemPrefix: string;
@@ -372,22 +373,10 @@ export declare class FoundationAiAssistant extends GenesisElement {
372
373
  /**
373
374
  * The distinct vendors the registry can currently reach, from the provider
374
375
  * statuses this element already loads on connect and refreshes on every
375
- * observable-registry change.
376
- *
377
- * This is the element's answer to "which vendor would the next turn use" — a
378
- * question that has **no** correct answer and deliberately gets no API. The
379
- * provider is resolved per turn AND per agent: `activeProviderInput` may be an
380
- * async function of the turn's context, an orchestrated turn picks its agent
381
- * with an LLM `classify()` call, and sub-agents resolve their own providers.
382
- * Any pre-turn "peek" would therefore be a guess that is wrong precisely on the
383
- * multi-agent hosts per-vendor budgets exist for.
384
- *
385
- * Asking instead which vendors are REACHABLE is answerable, synchronous, and
386
- * free — and it is enough: the composer must stay live while any reachable
387
- * vendor has headroom, and must lock when none does. It also makes vendor-switch
388
- * recovery a derivation rather than a mutation: a host that swaps its registry
389
- * to another vendor fires the observable, the statuses reload, the walled vendor
390
- * drops out of this set, and `blocked` goes false with every latch left intact.
376
+ * observable-registry change — this element's answer to "which vendor would the
377
+ * next turn use", a question that has no correct answer and deliberately gets
378
+ * no API. See `BudgetController.reachableVendors` for why reachability is
379
+ * the answerable question and what deriving from it buys.
391
380
  *
392
381
  * @beta
393
382
  */
@@ -398,13 +387,11 @@ export declare class FoundationAiAssistant extends GenesisElement {
398
387
  *
399
388
  * May include a vendor this registry cannot reach — either because the registry
400
389
  * moved on after the wall was latched, or because a 402's
401
- * `otherVendorAvailable: false` walls every vendor the proxy meters (which is
402
- * what that verdict is a statement about; see
403
- * {@link FoundationAiAssistant.latchBlockedFrom}). Neither costs anything
404
- * internally — {@link FoundationAiAssistant.blocked} asks only about the
405
- * reachable set, and the banner reads the reachability-filtered
406
- * {@link FoundationAiAssistant.relevantBlockedVendors} — but a host rendering
407
- * this list itself should filter it against its own registry.
390
+ * `otherVendorAvailable: false` walls every vendor the proxy meters. Neither
391
+ * costs anything internally — {@link FoundationAiAssistant.blocked} asks only
392
+ * about the reachable set, and the banner reads the reachability-filtered
393
+ * `BudgetController.relevantBlockedVendors` — but a host rendering this
394
+ * list itself should filter it against its own registry.
408
395
  *
409
396
  * Read-only on purpose: a settable array would let a host write a partial list
410
397
  * and silently orphan the per-vendor banner copy.
@@ -430,30 +417,20 @@ export declare class FoundationAiAssistant extends GenesisElement {
430
417
  * become true.
431
418
  *
432
419
  * `reason` is composed into the banner sentence for that vendor, **however many
433
- * vendors are walled** — see {@link FoundationAiAssistant.effectiveBlockedReason}.
434
- * It is per-vendor copy, not a whole-banner override; {@link FoundationAiAssistant.blockedReason}
435
- * is the override. Omitting it keeps whatever explanation that vendor already
436
- * carried, so a driver latch landing after a host one cannot blank it.
437
- *
438
- * `'none'` is rejected with a warning rather than accepted: it is the "no
439
- * provider configured" sentinel, not a vendor. Latching it would put the raw
440
- * sentinel in the banner ("none's AI usage limit is reached") and could never
441
- * be undone by derivation, because `'none'` never appears in
442
- * {@link FoundationAiAssistant.reachableVendors} — so `blocked` could never
443
- * become derivable from it either. Use {@link FoundationAiAssistant.setBlocked}
444
- * for a vendor-agnostic block.
420
+ * vendors are walled** — see `BudgetController.effectiveBlockedReason`.
421
+ * It is per-vendor copy, not a whole-banner override;
422
+ * {@link FoundationAiAssistant.blockedReason} is the override. Omitting it keeps
423
+ * whatever explanation that vendor already carried, so a driver latch landing
424
+ * after a host one cannot blank it.
425
+ *
426
+ * `'none'` is rejected with a warning rather than accepted — it is the "no
427
+ * provider configured" sentinel, not a vendor, and a wall on it could never be
428
+ * undone by derivation. Use {@link FoundationAiAssistant.setBlocked} for a
429
+ * vendor-agnostic block.
445
430
  *
446
431
  * @beta
447
432
  */
448
433
  setVendorBlocked(vendor: AIProviderType, blocked: boolean, reason?: string | null): void;
449
- /**
450
- * One warning per element for garbage fed to
451
- * {@link FoundationAiAssistant.setVendorBudget}. Once, not per call, because
452
- * the documented feed re-runs after every turn — a host bug would otherwise
453
- * repeat the same warning on every send for the life of the page.
454
- */
455
- private vendorBudgetWarningIssued;
456
- private warnVendorBudgetOnce;
457
434
  /**
458
435
  * Feed (or clear) one vendor's AI-spend figures for the settings-modal budget
459
436
  * meter — the numeric companion to
@@ -516,8 +493,9 @@ export declare class FoundationAiAssistant extends GenesisElement {
516
493
  spentUsd: number;
517
494
  } | null): boolean;
518
495
  /**
519
- * When budget figures last arrived — from the host's {@link FoundationAiAssistant.setVendorBudget}
520
- * or the wall latch's own feed — as epoch ms; `null` until the first feed.
496
+ * When budget figures last arrived — from the host's
497
+ * {@link FoundationAiAssistant.setVendorBudget} or the wall latch's own feed —
498
+ * as epoch ms; `null` until the first feed.
521
499
  *
522
500
  * ELEMENT state, not slice state, on purpose: the reducer is idempotent by
523
501
  * value (an unchanged re-feed must not re-render the meter rows), but "when
@@ -525,7 +503,8 @@ export declare class FoundationAiAssistant extends GenesisElement {
525
503
  * refresh that confirms the same dollars is still a refresh. Only the
526
504
  * toolbar label observes this, so a stamp re-renders one span, not the rows.
527
505
  */
528
- budgetMeterLastFedAt: number | null;
506
+ get budgetMeterLastFedAt(): number | null;
507
+ set budgetMeterLastFedAt(value: number | null);
529
508
  /** "Updated HH:MM:SS" for the meter toolbar, or `undefined` before any feed. */
530
509
  get settingsBudgetUpdatedLabel(): string | undefined;
531
510
  /**
@@ -540,83 +519,24 @@ export declare class FoundationAiAssistant extends GenesisElement {
540
519
  * listener simply keeps the per-turn cadence.
541
520
  */
542
521
  requestBudgetRefresh(): void;
543
- /** Whether this vendor's wall came from the sweep alone — see the slice's `sweptVendors`. */
544
- private isVendorSwept;
545
- /**
546
- * The walled vendors the user can still be routed to — i.e.
547
- * {@link FoundationAiAssistant.blockedVendors} narrowed to the reachable set,
548
- * which is the only set the banner may name.
549
- *
550
- * A wall the registry can no longer reach is not news: it cannot be hit, and
551
- * naming it puts a vendor in front of the user that is not theirs. The concrete
552
- * failure this exists to stop: a host ships Anthropic-only, Anthropic walls, the
553
- * host swaps its registry to Gemini, Gemini walls — and the banner reads "AI
554
- * usage limits are reached for Anthropic and Gemini" to a user who has never
555
- * had an Anthropic key. The spurious second name also flips the copy onto the
556
- * plural branch, so even the closing sentence is wrong.
557
- *
558
- * When nothing is reachable YET (the statuses are still loading — the exact
559
- * window the pre-flight and the cross-tab bus latch into), the fallback keeps
560
- * every wall for LOCKING (`blocked` still derives true) but names only the
561
- * vendors walled by a REFUSAL, dropping the ones the `otherVendorAvailable`
562
- * sweep added. A refusal is a fact about a vendor this user just used; a sweep
563
- * entry is the proxy's headroom verdict about a vendor the host may not even
564
- * ship — naming it here reproduced the exact failure above from the other
565
- * direction ("Gemini's AI usage limit is reached" to an Anthropic-only user),
566
- * and flipped the copy onto the plural branch with it. Self-corrects when the
567
- * statuses land: from then on reachability, not provenance, decides.
568
- *
569
- * @internal
570
- */
571
- private get relevantBlockedVendors();
572
522
  /**
573
523
  * Whether the blocked banner has anything to say — a vendor-agnostic block, or
574
- * a partial wall that the NEXT send would actually hit.
575
- *
576
- * Partial exhaustion is announced only while the ACTIVE vendor (the registry
577
- * default the next send resolves to) is among the walled ones. A user who has
578
- * switched to a vendor with headroom has already acted on the advice —
579
- * keeping the banner up past that point nagged about a vendor they just left
580
- * (GENC-1464 tester feedback), while switching BACK to the walled vendor
581
- * brings the banner straight back. Two conservative fallbacks keep it
582
- * fail-informative: while the statuses are still loading the active vendor is
583
- * unknowable, so any reachable-and-walled vendor still announces; and a
584
- * vendor-agnostic block announces always, exactly as before.
585
- *
586
- * Reads the reachability-filtered list for the same reason the copy does — a
587
- * host that has swapped its registry away from the walled vendor has nothing
588
- * left to announce, and would otherwise get a banner that falls through to the
589
- * generic default copy over a perfectly usable composer.
524
+ * a partial wall that the NEXT send would actually hit. See
525
+ * `BudgetController.bannerVisible` for why partial exhaustion is
526
+ * announced only while the active vendor is among the walled ones.
590
527
  *
591
528
  * @internal
592
529
  */
593
530
  get bannerVisible(): boolean;
594
531
  /**
595
- * Whether a suggestions fetch would hit a wall.
596
- *
597
- * Unlike a chat turn, this one CAN be resolved exactly: both suggestion paths
598
- * go to the registry **default** and never to a per-agent override
599
- * (`ChatDriver.getSuggestions` calls `providerRegistry.default()`, and
600
- * `OrchestratingDriver.getSuggestions` just delegates to it). So the vendor a
601
- * suggestions call would use is knowable, and this is the one place in the
602
- * feature where that is true.
603
- *
604
- * Same rationale as the guard it replaces: a doomed suggestions call burns a
605
- * `suggestions.failed` meta event and parks a raw transport error in
606
- * `suggestionsState` for hosts to find.
532
+ * Whether a suggestions fetch would hit a wall — the one place in this feature
533
+ * where the vendor a call would use is exactly knowable, because both
534
+ * suggestion paths go to the registry default. See
535
+ * `BudgetController.suggestionsBlocked`.
607
536
  *
608
537
  * @internal
609
538
  */
610
539
  get suggestionsBlocked(): boolean;
611
- /**
612
- * The vendor the NEXT send resolves to — the registry default's provider, as
613
- * reported through `providerStatuses`. `undefined` while the statuses are
614
- * still loading (or when the default reports the `'none'` sentinel), which
615
- * every consumer treats conservatively.
616
- *
617
- * @internal
618
- */
619
- private get activeVendor();
620
540
  /**
621
541
  * Explanation shown in the blocked banner, or `null` for the element's default
622
542
  * copy. Only meaningful while {@link FoundationAiAssistant.blocked} is true;
@@ -633,96 +553,6 @@ export declare class FoundationAiAssistant extends GenesisElement {
633
553
  */
634
554
  get blockedReason(): string | null;
635
555
  set blockedReason(value: string | null);
636
- /**
637
- * Latch the backend block off a turn outcome. The single decision point for
638
- * both latch sites (the `tool-loop-end` bus subscription and `send()`'s
639
- * return-value check), so the rule cannot diverge between them.
640
- *
641
- * Idempotent and one-way **per vendor**: it never unblocks, and it never
642
- * re-writes an existing wall. That guard is load-bearing, not just an
643
- * optimisation — the bus fires from the driver's `finally`, i.e. BEFORE
644
- * `sendMessage()` resolves, so a host subscribed to `tool-loop-end` (what the
645
- * migration guide's Option C recommends) sets its own detailed reason and the
646
- * return-value latch would otherwise land a moment later and blank it.
647
- *
648
- * The guard being per-vendor rather than global is the whole difference. A
649
- * single global "already blocked, do nothing" would swallow a second vendor's
650
- * wall — so a session walled on Anthropic could never record that Gemini went
651
- * too, and under a mixed registry an Anthropic-only wall would lock a composer
652
- * that Gemini could still serve.
653
- *
654
- * **Which field is authoritative**, in order:
655
- *
656
- * 1. `budget.vendorLabel` — stamped by the transport that was actually refused,
657
- * so it can never be stale. But it is a static per-transport string, so a
658
- * white-labelled or multiplexing gateway fronting several upstreams leaves
659
- * it unclaimed by any vendor.
660
- * 2. `budget.vendor` — which the driver resolved from the label where it could,
661
- * and otherwise from the proxy's own `vendor` field on the 402. It is
662
- * therefore NOT simply `vendorLabel` normalised, and the two can disagree;
663
- * that fallback is the only attribution on offer for the gateway case above.
664
- * 3. `vendorHint` — the bus detail's top-level vendor, i.e. the driver's
665
- * last-resolved provider. Last resort because it CAN be stale: an
666
- * orchestrated turn classifies against the registry default, a provider the
667
- * chat driver may never have resolved, so its last-resolved provider there is
668
- * the previous turn's vendor or nothing at all.
669
- *
670
- * A wall that names no recognised vendor falls back to the vendor-agnostic
671
- * block — fail-safe, and identical to the pre-per-vendor behaviour.
672
- *
673
- * **`otherVendorAvailable: false` is server-known truth and outranks every
674
- * inference made here.** The proxy meters the pots, so only it can say whether
675
- * anything else has headroom; this element can only observe which vendors the
676
- * registry can REACH, which says nothing about their remaining spend. So when
677
- * the proxy says no, every other *budgeted* vendor is walled in the same pass.
678
- * That is what makes `blocked` derive true — locking the composer on the first
679
- * response instead of after a second doomed turn — and what empties the "free"
680
- * set the banner would otherwise have advised switching to.
681
- *
682
- * The sweep runs over `BUDGETED_VENDORS`, deliberately **not** over
683
- * {@link FoundationAiAssistant.reachableVendors}. Reachability is loaded
684
- * asynchronously (`activateSession` kicks off `loadProviderStatuses()` and does
685
- * not await it), so a wall landing before the statuses resolve would have swept
686
- * an empty set — silently discarding the one fact on the 402 the client cannot
687
- * re-derive, with nothing to re-run it when the statuses arrived. Worse, it let
688
- * the verdict effectively un-latch: the single wall derived `blocked` only
689
- * while the reachable set was empty, so the moment the statuses landed the
690
- * composer came back to life against a budget the proxy had already said was
691
- * gone. Sweeping the metered vendor list instead makes the verdict independent
692
- * of load order and of registry membership, which is exactly what it is: a
693
- * statement about the proxy's pots, not about this user's registry.
694
- *
695
- * Walling a vendor the registry cannot reach costs nothing: `blocked` only asks
696
- * whether every REACHABLE vendor is walled, and the banner reads
697
- * {@link FoundationAiAssistant.relevantBlockedVendors}, which filters the walls
698
- * back down to the reachable set before naming any of them.
699
- *
700
- * One boundary remains on the sweep: it walls only vendors the proxy meters,
701
- * enforced structurally by iterating `BUDGETED_VENDORS` itself rather than
702
- * filtering a wider set through a predicate. Chrome runs on-device with no pot
703
- * to exhaust, so "no other vendor has budget" is not a statement about it, and
704
- * "Switch to Chrome to keep going." stays honest advice.
705
- *
706
- * The sweep runs last but sits OUTSIDE the per-vendor idempotence guard, and
707
- * both halves of that matter. Last, so the vendor that actually refused this
708
- * turn heads the wall order the multi-vendor banner reads. Outside the guard,
709
- * because the verdict can arrive on a LATER 402 for a vendor that is already
710
- * walled — turn 1 walls Anthropic while Gemini still has headroom, turn 2 walls
711
- * it again and reports that Gemini has since gone too — and an early return
712
- * would drop exactly the news the second turn was there to deliver.
713
- *
714
- * @param reason - the turn's failure reason; anything but `'budget-exhausted'` is ignored.
715
- * @param ref - session store to write through; defaults to the live one. `send()`
716
- * passes the ref it captured before its awaits, since a lifecycle event during the
717
- * turn may already have cleared `_sessionRef`.
718
- * @param budget - what the 402 reported: figures when the proxy sent any, the
719
- * refusing vendor, and its `otherVendorAvailable` verdict.
720
- * @param vendorHint - the `tool-loop-end` detail's top-level vendor, used only
721
- * when the budget payload names none.
722
- *
723
- * @internal
724
- */
725
- private latchBlockedFrom;
726
556
  /**
727
557
  * Block or unblock the assistant, optionally with custom banner copy.
728
558
  * Equivalent to the {@link FoundationAiAssistant.blocked} setter, plus the
@@ -738,66 +568,18 @@ export declare class FoundationAiAssistant extends GenesisElement {
738
568
  */
739
569
  setBlocked(blocked: boolean, reason?: string | null): void;
740
570
  /**
741
- * Copy shown in the blocked banner, in ascending specificity:
742
- *
743
- * 1. the host's `blockedReason` — a whole-banner override, unchanged;
744
- * 2. copy composed from the walled vendors (their per-vendor reasons when set,
745
- * otherwise their names) plus the action that is actually available;
746
- * 3. the default budget-exhaustion message.
747
- *
748
- * The action clause is computed here rather than stored, because whether
749
- * "switch vendor" or "contact your administrator" is honest depends on whether
750
- * another reachable vendor still has headroom — which changes when the registry
751
- * changes, long after the wall was latched. Telling a user to switch to a
752
- * vendor that is also exhausted is worse than saying nothing.
753
- *
754
- * The `free` set is a client-side inference — "reachable and not known to be
755
- * walled" — and it is only ever allowed to be one. Where the proxy has told us
756
- * otherwise (`otherVendorAvailable: false`) the latch has already walled those
757
- * vendors, so they are not in `free` here and no "switch to X" clause can be
758
- * composed from them. Both lists are filtered by reachability, so the copy
759
- * names only vendors this user's registry can actually route to.
760
- *
761
- * **Per-vendor copy is composed however many vendors are walled**, which is
762
- * what {@link FoundationAiAssistant.setVendorBlocked} and
763
- * `docs/migration-GENC-1464.md` both promise. It previously reached only the
764
- * single-vendor branch, so a host that had carefully set copy for each of its
765
- * vendors watched all of it vanish the moment a second one walled — replaced by
766
- * generic copy, at the exact moment the situation got worse.
767
- *
768
- * The compact list is the DEFAULT shape for several walls, now carrying each
769
- * vendor's figures: "AI usage limits are reached for Gemini ($62.34 of $50.00)
770
- * and Anthropic ($31.10 of $30.00)." A driver latch stores no copy of its own
771
- * (it stores the figures, which this composes from), so that shape is what the
772
- * real budget flow produces. As soon as any walled vendor carries HOST copy,
773
- * which cannot be folded into a list item, the branch lists one statement per
774
- * vendor instead — falling back to composed copy for the ones that have none, so
775
- * the sentence set stays complete rather than silently naming a subset.
571
+ * Copy shown in the blocked banner — the host's override, else copy composed
572
+ * from the walled vendors, else the default. See
573
+ * `BudgetController.effectiveBlockedReason`, which explains why the
574
+ * action clause is composed on read rather than stored at latch time.
776
575
  *
777
576
  * @internal
778
577
  */
779
578
  get effectiveBlockedReason(): string;
780
579
  /**
781
580
  * Copy the driver writes into the TRANSCRIPT when a turn hits the budget wall,
782
- * or `undefined` to let the driver compose it per wall.
783
- *
784
- * A set `blockedReason` still wins — a white-labelled host must not read its
785
- * own explanation in the banner and shipped copy directly below it, and the
786
- * vendor-agnostic latch's `formatBlockedReason(budget)` form stays advice-free
787
- * exactly as before.
788
- *
789
- * `undefined` (the usual case) is NOT the old `DEFAULT_BUDGET_EXHAUSTED_MESSAGE`
790
- * fallback: it hands composition to the driver, which builds the bubble from
791
- * EACH wall's own 402 — naming the exhausted vendor and advising a switch only
792
- * when that refusal's `otherVendorAvailable` positively vouched for headroom
793
- * elsewhere. That is immune to the staleness this getter used to guard against
794
- * by freezing the copy: construction-time advice could outlive its truth
795
- * (`getOrCreateDriver` re-runs on an agents swap and would have baked the
796
- * sentence into every later bubble), but per-wall advice is derived from the
797
- * refusal it annotates and dies with the turn. Forcing the default here was
798
- * also what made the driver's composed bubble unreachable in every real
799
- * mount — the banner advised switching while the bubble under it never did
800
- * (the GENC-1464 tester finding, root cause).
581
+ * or `undefined` to let the driver compose it per wall — see
582
+ * `BudgetController.transcriptMessage`.
801
583
  */
802
584
  private get transcriptBudgetExhaustedMessage();
803
585
  /**
@@ -859,49 +641,32 @@ export declare class FoundationAiAssistant extends GenesisElement {
859
641
  */
860
642
  get sessionCostUsd(): number;
861
643
  /**
862
- * Distinct models the registry currently resolves to — the live session's model chips.
863
- *
864
- * `providerStatuses` carries one entry per registry SLOT, so a vendor configured across several
865
- * tiers that happen to resolve to the same model reports it once per tier, and the chips
866
- * rendered it once per tier too. Which tier a model came from is not what these chips are for.
644
+ * The cost ledger's derived surface (GENC-1475). Every member below delegates to
645
+ * `CostLedgerController` — the names are this element's public API and the
646
+ * settings modal's bindings, so they stay put while the accounting moves.
867
647
  *
868
- * Deduped on the label actually displayed, not on `status.model`: a slot whose provider reports
869
- * no model falls back to the slot's own name, and those are distinct per slot, so keying on the
870
- * label keeps them from collapsing into one another.
648
+ * None carries `@volatile`: the decorators live on the controller's getters, and they
649
+ * reach the binding observer from there — see `docs/code-health.md` §11.4.
871
650
  */
872
651
  get uniqueProviderModels(): CostSessionModelEntry[];
873
- /**
874
- * Build history for display: most recently worked on first.
875
- *
876
- * The stored order is insertion order (see `upsertRecord`), which leaves a project returned to
877
- * sitting wherever it was first seen — so ordering is applied here, on read, where it also
878
- * covers rows a host provider supplied.
879
- */
652
+ /** Build history for display: most recently worked on first. */
880
653
  get sortedCostSessionHistory(): CostSessionRecord[];
881
- /**
882
- * Lifetime usage across every project in the history scope — cost and all four token
883
- * buckets. What the Usage tab's lifetime section renders.
884
- *
885
- * Rows ONLY — deliberately not rows + live session. Each row holds a project's lifetime
886
- * usage and the active project's row is kept current on every transcript change, so
887
- * adding the live total would count this project's spend twice. That double-count is what
888
- * made the figure grow on every refresh: a page-hide banked a row while the restored
889
- * session still carried the same spend.
890
- */
654
+ /** Lifetime usage across every project in the history scope — rows only, never plus live. */
891
655
  get cumulativeUsage(): AggregateUsage;
892
656
  /** Lifetime USD across every project in the history scope. */
893
657
  get cumulativeCostUsd(): number;
894
- /**
895
- * Whether the live session figures amount to this project's whole history, which is true
896
- * exactly when the transcript is restored on load. Drives the Usage tab's live labels
897
- * ("Project cost" vs "Session cost") so they state the scope rather than imply one.
898
- *
899
- * Note this is NOT what makes the *rows* lifetime — those bank their pre-transcript spend
900
- * (see `CostSessionRecord.banked`) and are lifetime either way.
901
- */
658
+ /** Whether the live session figures amount to this project's whole history. */
902
659
  get liveUsageIsProjectLifetime(): boolean;
903
660
  /** Resolved cost tab and build-history settings from `chatConfig.costHistory`. */
904
661
  get resolvedCostHistory(): ResolvedCostHistoryConfig;
662
+ /**
663
+ * Persisted finalized cost sessions shown on the Usage tab build history list.
664
+ *
665
+ * A get/set pair rather than a read-only projection: the banking tests seed rows to
666
+ * model a page load that found some, and a host provider's rows arrive the same way.
667
+ */
668
+ get costSessionHistory(): CostSessionRecord[];
669
+ set costSessionHistory(value: CostSessionRecord[]);
905
670
  /**
906
671
  * Total tokens across every chat turn in this session — all four buckets added up.
907
672
  * Read-only; the per-bucket split is on `sessionUsage`.
@@ -910,56 +675,20 @@ export declare class FoundationAiAssistant extends GenesisElement {
910
675
  /** Label shown on the Usage tab for the in-flight session (app title or first user phrase). */
911
676
  get liveCostSessionTitle(): string | undefined;
912
677
  /**
913
- * Write this session's usage into persisted build history (`localStorage`) as a single
914
- * row keyed by the session key, replacing any existing row for that key.
915
- *
916
- * **Idempotent** — safe to call per turn, on tab close, and on a title change alike.
917
- * The name is historical: it does NOT end the session or reset the running totals.
918
- * Resetting them is what used to double-count spend, since the row was banked while the
919
- * persisted transcript still carried the same usage, so a reload restored it and the
920
- * Usage tab counted both.
678
+ * Write this session's usage into persisted build history as a single row keyed by the
679
+ * session key, replacing any existing row for that key.
921
680
  *
922
- * No-op unless `chatConfig.costHistory.enabled` is `true`, when the live session has no
923
- * usage yet, or when the session has no key to upsert against. Gating on the *live*
924
- * figures (not the banked total) means a reload that restores nothing rewrites nothing.
681
+ * **Idempotent** — safe to call per turn, on tab close, and on a title change alike. The
682
+ * name is historical: it does NOT end the session or reset the running totals. See
683
+ * `CostLedgerController.finalize` for the full contract and the no-op cases.
925
684
  */
926
685
  finalizeCostSession(options?: {
927
686
  title?: string;
928
687
  }): void;
929
688
  /**
930
- * Spend for `projectKey` that predates the current transcript, resolved once per SESSION and
931
- * shared by every element instance wired to it (see `banked-usage-baselines`), so it cannot
932
- * drift as the row it came from is rewritten by our own upserts.
933
- *
934
- * The two cases turn on whether the transcript is authoritative for this project:
935
- *
936
- * - **Transcript restored** (`persistenceActive`): it already accounts for everything after
937
- * the previous row's own banked figure, so carry that figure forward unchanged. Re-banking
938
- * `usage` here would add the restored history to itself on every reload.
939
- * - **Transcript starts empty**: nothing on screen accounts for the previous total, so the
940
- * whole of it becomes banked. Without this the next turn would rewrite the row downwards
941
- * to just that turn, losing the project's earlier spend.
942
- *
943
- * The baseline is deliberately NOT element state. This element is remounted repeatedly inside
944
- * a single session (the host re-rendering its layout, popping out, docking) and a fresh
945
- * instance re-deriving would take the second branch against rows that already hold the live
946
- * transcript — banking the session as its own baseline. Keying on the session instead means a
947
- * remount inherits the baseline that transcript was paired with.
948
- */
949
- private bankedUsageFor;
950
- /**
951
- * Re-baseline this session against an emptied transcript: what came before, plus what the
952
- * transcript being discarded proved. `discarded` must be read BEFORE the live figures are
953
- * reset, since that is the only record of the latter.
954
- *
955
- * Not the same as dropping the baseline. Dropping it re-derives through `persistenceActive`,
956
- * and with persistence ON that carries the row's *old* banked figure forward — discarding
957
- * every turn between the last baseline and the clear. The transcript is gone either way, so
958
- * the branch that reads it as authoritative no longer applies.
959
- *
960
- * Derived from the memoised baseline and the live figure rather than from the project's row,
961
- * because a row is only as current as the last `finalizeCostSession` — which skips its write
962
- * while the ledger is unloaded, and while the session has no usage yet.
689
+ * Re-baseline this session against an emptied transcript — see
690
+ * `CostLedgerController.rebaselineOnClearedTranscript`. `discarded` must be read
691
+ * BEFORE the live figures are reset, since that is the only record of the latter.
963
692
  */
964
693
  private rebaselineOnClearedTranscript;
965
694
  showConfirmationDialog(confirmationMessage: string, modalParent: HTMLElement, onConfirm: () => void, onCancel?: () => void): void;
@@ -967,48 +696,6 @@ export declare class FoundationAiAssistant extends GenesisElement {
967
696
  removeCostSessionFromHistory(id: string): void;
968
697
  /** Clear all persisted build history for this assistant scope. */
969
698
  clearCostSessionHistory(): void;
970
- private getCostHistoryScope;
971
- private reloadCostSessionHistory;
972
- /**
973
- * The persistence provider, but only when it implements BOTH cost-history methods.
974
- *
975
- * Both or neither: a provider that can save but not load would silently accumulate a
976
- * ledger nobody ever reads back, so a partial implementation is treated as absent and
977
- * falls back to `localStorage`.
978
- */
979
- private costHistoryProvider;
980
- /**
981
- * Adopt `records` as the live list, and schedule a write to whichever backend owns cost
982
- * history — the provider when the host implements it, else `localStorage`.
983
- *
984
- * The list is adopted synchronously because that is what the Usage tab renders; only the
985
- * *write* is deferred. This runs on every usage change (about once per LLM call, plus once
986
- * per sub-agent turn) and each write serialises the whole ledger, so coalescing cuts a
987
- * per-turn `localStorage.setItem` or network round-trip down to one per quiet period. Safe
988
- * because the write is a full replace of idempotent state: the newest call wins and no
989
- * intermediate state is meaningful. Flushed on `pagehide` and disconnect so nothing is lost.
990
- */
991
- private persistCostHistory;
992
- /**
993
- * Write the pending ledger now. Safe to call when nothing is pending.
994
- *
995
- * Provider writes are fire-and-forget and best-effort: the ledger is accounting, not
996
- * conversation content, so a failed write is logged and never surfaced into the chat. On
997
- * `pagehide` the `localStorage` path completes synchronously; a provider write is subject to
998
- * the same unload race as every other network save the assistant makes.
999
- */
1000
- private flushCostHistory;
1001
- /**
1002
- * Finalize the previous session when the host-provided title changes — a rename should land on
1003
- * the row under its old name rather than silently relabelling accrued spend.
1004
- *
1005
- * Only when the session key still matches the one that title accrued under. Hosts change
1006
- * `costHistory.title` on project SWITCH too, and if the key has already flipped, this would
1007
- * upsert the outgoing project's live usage onto the incoming project's row as
1008
- * `banked + old live`, inflating it. Under the old append model a mis-keyed write left a stray
1009
- * row; with the upsert it overwrites a real one.
1010
- */
1011
- private syncActiveCostSessionTitle;
1012
699
  /** Active model id resolved from the AIProvider, if exposed. */
1013
700
  get activeModel(): string | undefined;
1014
701
  set activeModel(value: string | undefined);
@@ -1021,14 +708,20 @@ export declare class FoundationAiAssistant extends GenesisElement {
1021
708
  private _suggestionsGeneration;
1022
709
  get inputValue(): string;
1023
710
  set inputValue(value: string);
1024
- attachments: ChatAttachment[];
1025
- attachmentErrors: string[];
711
+ /** Attachments pending on the next message. */
712
+ get attachments(): ChatAttachment[];
713
+ set attachments(value: ChatAttachment[]);
714
+ /** Rejection messages for files the last pick refused. */
715
+ get attachmentErrors(): string[];
716
+ set attachmentErrors(value: string[]);
1026
717
  /** Whether the loading spinner is currently visible. Controlled by the loading delay timer. */
1027
718
  showLoadingIndicator: boolean;
1028
719
  /** Bump to re-render app settings toggles when the host provider notifies a change. */
1029
- appSettingsRevision: number;
720
+ get appSettingsRevision(): number;
721
+ set appSettingsRevision(value: number);
1030
722
  /** Whether the settings modal is open. */
1031
- settingsOpen: boolean;
723
+ get settingsOpen(): boolean;
724
+ set settingsOpen(value: boolean);
1032
725
  /**
1033
726
  * True while `downloadDebugLog` is building the log (GENC-1351 §5.8). Drives the
1034
727
  * download button's disabled + spinner state — the lifetime-stitch path is async
@@ -1038,7 +731,8 @@ export declare class FoundationAiAssistant extends GenesisElement {
1038
731
  */
1039
732
  downloadingDebugLog: boolean;
1040
733
  /** Active tab inside the settings modal. */
1041
- settingsModalTab: SettingsModalTab;
734
+ get settingsModalTab(): SettingsModalTab;
735
+ set settingsModalTab(value: SettingsModalTab);
1042
736
  /** Bound to the design-system modal via the template ref. */
1043
737
  settingsModal?: Modal;
1044
738
  /**
@@ -1117,16 +811,33 @@ export declare class FoundationAiAssistant extends GenesisElement {
1117
811
  persistenceModal?: Modal;
1118
812
  /** Bound to the settings modal tab bar — used to resync selection after reopen. */
1119
813
  settingsTabsEl?: SettingsModalTabsElement;
1120
- /** Light-DOM nodes assigned to settings modal slots (via `slotted()` bindings). */
1121
- settingsModelSlotted: HTMLElement[];
1122
- settingsAppSlotted: HTMLElement[];
1123
- settingsCostCurrentSlotted: HTMLElement[];
1124
- settingsCostHistorySlotted: HTMLElement[];
1125
- /** Persisted finalized cost sessions shown on the Usage tab build history list. */
1126
- costSessionHistory: CostSessionRecord[];
814
+ /**
815
+ * Light-DOM nodes assigned to settings modal slots. The template's `slotted()`
816
+ * bindings write these by property NAME, so each needs a real setter here even
817
+ * though the controller owns the arrays.
818
+ */
819
+ get settingsModelSlotted(): HTMLElement[];
820
+ set settingsModelSlotted(value: HTMLElement[]);
821
+ get settingsAppSlotted(): HTMLElement[];
822
+ set settingsAppSlotted(value: HTMLElement[]);
823
+ get settingsCostCurrentSlotted(): HTMLElement[];
824
+ set settingsCostCurrentSlotted(value: HTMLElement[]);
825
+ get settingsCostHistorySlotted(): HTMLElement[];
826
+ set settingsCostHistorySlotted(value: HTMLElement[]);
1127
827
  /** Whether the splash overlay is currently showing (no messages and showSplash is enabled). Reflected as a boolean attribute on the host. */
1128
828
  showingSplash: boolean;
1129
829
  private driver?;
830
+ /**
831
+ * This element's driver as an orchestrator, or `undefined` when the session is
832
+ * running a bare `ChatDriver`.
833
+ *
834
+ * The five call sites that used to test the type themselves now read this
835
+ * (§11.2 constraint 3 of `docs/code-health.md`): the agent pin, the flow-owner
836
+ * lock, the wire-up in `wireDriver`, and the two `driverKind` diagnostics. The
837
+ * type test itself lives in `asOrchestrator`, shared with the session
838
+ * persister.
839
+ */
840
+ private get orchestrator();
1130
841
  private driverCleanup?;
1131
842
  private loadingTimer;
1132
843
  private unsubBus?;
@@ -1134,29 +845,6 @@ export declare class FoundationAiAssistant extends GenesisElement {
1134
845
  private _unsubBudgetLatch?;
1135
846
  /** Unsubscribe handle for the provider-registry change listener (observable registries only). */
1136
847
  private unsubProviderRegistry?;
1137
- /** Unsubscribe handle for {@link (AssistantAppSettingsProvider:interface).subscribe}. */
1138
- private unsubAppSettingsProvider?;
1139
- /** Title tracked for the in-flight cost session (used when `header-title` changes). */
1140
- private _activeCostSessionTitle;
1141
- /**
1142
- * Session key `_activeCostSessionTitle` was captured under, so a title change can tell a rename
1143
- * (same key) from a project switch (key already flipped) — see `syncActiveCostSessionTitle`.
1144
- */
1145
- private _activeCostSessionKey?;
1146
- /**
1147
- * Whether the row list reflects storage yet. Gates cost writes so a project's banked
1148
- * spend is never derived from a ledger that simply hasn't arrived.
1149
- */
1150
- private _costHistoryLoaded;
1151
- /**
1152
- * Set when a provider read failed. Latches writes off for this page-load: `saveCostHistory`
1153
- * replaces the whole stored list, so writing anything derived from the empty list we fell back
1154
- * to would destroy rows we never managed to read.
1155
- */
1156
- private _ledgerUnreadable;
1157
- /** Pending (adopted but unwritten) ledger changes — see `persistCostHistory`. */
1158
- private _ledgerDirty;
1159
- private _ledgerFlushTimer?;
1160
848
  private _handlePageHide;
1161
849
  private haloStartPublished;
1162
850
  /** Fingerprint of the agents array used to build the current driver. Used by agentsChanged to skip spurious rebuilds. */
@@ -1166,20 +854,15 @@ export declare class FoundationAiAssistant extends GenesisElement {
1166
854
  /** Bound to the rapid-text-area chat input via the template ref. */
1167
855
  chatInputEl?: HTMLElement;
1168
856
  /**
1169
- * Explicit composer height (px) once the user has dragged the resize handle;
1170
- * `null` keeps the default auto-grow behaviour. Written into the
1171
- * `--composer-height` custom property via the `composerHeightStyle` getter.
1172
- */
1173
- composerHeightPx: number | null;
1174
- /** Pointer Y and composer height captured at the start of a resize drag. */
1175
- private _resizeStartY;
1176
- private _resizeStartHeight;
1177
- /** The composer's natural (un-resized) height, measured on first drag — used
1178
- * as the lower bound so the box can't be dragged smaller than it started. */
1179
- private _composerNaturalHeight;
1180
- /** Upper bound for the current drag — capped to the headroom above so growing
1181
- * the composer never pushes the input row's buttons off the bottom. */
1182
- private _resizeMaxHeight;
857
+ * A get/set pair over one of this element's own properties, for a controller
858
+ * that drives state it does not own. Written as a helper rather than two inline
859
+ * closures per call: a pair of same-typed closures is easy to transpose by
860
+ * accident, and a single object cannot be.
861
+ */
862
+ private accessor;
863
+ /** Explicit composer height once dragged; `null` keeps the default auto-grow. */
864
+ get composerHeightPx(): number | null;
865
+ set composerHeightPx(value: number | null);
1183
866
  /**
1184
867
  * True when the user's last interaction (pointer or keyboard focus) was inside
1185
868
  * the assistant. Defaults to true so the assistant claims focus on first mount.
@@ -1370,18 +1053,6 @@ export declare class FoundationAiAssistant extends GenesisElement {
1370
1053
  * window is known.
1371
1054
  */
1372
1055
  private syncContextGuard;
1373
- /**
1374
- * Guard policy pushed to the driver, or `undefined` when the host has switched
1375
- * the gate off — so `enabled: false` really does mean no local blocking of any
1376
- * kind, UI or mid-loop.
1377
- *
1378
- * A MARGIN, not a threshold: the driver resolves the window of the provider
1379
- * each call actually goes to. The fallback window rides along because the
1380
- * driver cannot learn it any other way — without it, a host that knows its
1381
- * window while its provider does not report one had `fallbackContextLimit`
1382
- * protecting the composer and nothing else.
1383
- */
1384
- private get contextGuardPolicy();
1385
1056
  private loadProviderStatuses;
1386
1057
  chatConfigChanged(): void;
1387
1058
  /**
@@ -1463,46 +1134,10 @@ export declare class FoundationAiAssistant extends GenesisElement {
1463
1134
  * `contains()` check cannot tell "clicked the menu" from "clicked the
1464
1135
  * transcript". The path crosses the boundary and carries both.
1465
1136
  */
1466
- private _dismissPointerHandler?;
1467
- private _dismissKeyHandler?;
1468
- private _dismissResizeHandler?;
1469
- private closeOpenComposerMenus;
1470
- /**
1471
- * Places an open composer menu directly above the control that opened it.
1472
- *
1473
- * CSS alone cannot: the menu is a child of `.composer` — which is what lets its
1474
- * width be capped against the composer rather than against a button — while its
1475
- * anchor is a button several levels down the bar, and no selector can read a
1476
- * sibling's offset. So the left edge is measured on open and written as a
1477
- * custom property.
1478
- *
1479
- * Clamped to the composer, so a menu opened from a control near the right edge
1480
- * of a narrow panel slides back inside instead of overhanging. The animation
1481
- * origin is written separately and keeps pointing at the button's centre even
1482
- * after such a shift — the menu still grows out of the thing that opened it,
1483
- * which is the whole point of anchoring it there.
1484
- */
1485
- private positionComposerMenu;
1486
- /** Re-place whichever menu is open. Cheap enough to run from a resize. */
1487
- private repositionOpenComposerMenu;
1488
- private syncComposerMenuDismissal;
1489
- private teardownComposerMenuDismissal;
1490
- /**
1491
- * Vertical space an open menu must leave free above the button, covering BOTH
1492
- * the gap the menu sits off the button by (8px, declared in design units in
1493
- * main.styles.ts) and the breathing room wanted above it. The two are lumped
1494
- * together because only the second half is a free choice: the first is fixed by
1495
- * the stylesheet and simply has to be subtracted from the headroom, and a
1496
- * constant named for only one of them invites someone to "fix" a gap that is
1497
- * doing exactly what it should.
1498
- */
1499
- private static readonly MENU_HEADROOM_RESERVE;
1500
- private readonly _agentPickerToggle;
1501
1137
  toggleAgentPicker(): void;
1502
1138
  /** Two-step confirm for the destructive Clear action; reset whenever the menu toggles. */
1503
1139
  confirmingClear: boolean;
1504
1140
  confirmingCompact: boolean;
1505
- private readonly _sessionMenuToggle;
1506
1141
  /**
1507
1142
  * Whether the lifecycle-menu button shows. It occupies the stop-button slot,
1508
1143
  * so it only appears when idle (mutually exclusive with Stop). Gated on
@@ -1540,20 +1175,18 @@ export declare class FoundationAiAssistant extends GenesisElement {
1540
1175
  /** Whether persistence is currently active (on + a provider) for this session. */
1541
1176
  get persistenceActive(): boolean;
1542
1177
  /**
1543
- * Context-headroom thresholds for the active model (GENC-1567) — the warning
1544
- * and block lines, plus the tail budget compaction aims at.
1178
+ * The context gate's derived surface (GENC-1567). Every member below delegates to
1179
+ * `ContextGateController` — the names are this element's public API and the
1180
+ * template's bindings, so they stay put while the derivation moves.
1545
1181
  *
1546
1182
  * The per-agent `contextReserve` IS folded in, via the driver (GENC-1582). It
1547
1183
  * used to be described here as a separate, later check made when an agent took
1548
1184
  * ownership of a flow — but there is no such check, and by flow entry the send
1549
- * has already been allowed. Resolving it here, before the send, is what makes it
1550
- * a gate rather than a label.
1185
+ * has already been allowed. Resolving it before the send is what makes it a gate
1186
+ * rather than a label.
1551
1187
  *
1552
- * Overhead is read off `messages` (the Redux mirror) rather than the driver's
1553
- * own history, which the compaction plan uses. That is safe rather than sloppy:
1554
- * `estimateSystemOverhead` only looks at messages BEFORE the first usage-bearing
1555
- * one, and the two lists agree completely that early — the drift between mirror
1556
- * and driver history is only ever at the tail.
1188
+ * None carries `@volatile`: the decorators live on the controller's getters, and
1189
+ * they reach the binding observer from there — see `docs/code-health.md` §11.4.
1557
1190
  */
1558
1191
  get contextBudget(): ResolvedContextBudget;
1559
1192
  /**
@@ -1574,150 +1207,31 @@ export declare class FoundationAiAssistant extends GenesisElement {
1574
1207
  * `contextReserve` is a plain number by design — see `ContextReserveInput` for
1575
1208
  * why a resolver form was dropped rather than accepted and ignored.
1576
1209
  *
1210
+ * Stays on the element rather than moving into `ContextGateController` because it
1211
+ * reaches the driver, which no extracted controller may do (`docs/code-health.md`
1212
+ * §11.2 constraint 2). The controller receives the resolved value as a callback.
1213
+ *
1577
1214
  * @internal
1578
1215
  */
1579
1216
  private get contextReserve();
1580
- /**
1581
- * What compacting right now would reclaim, or `null` when it would not be
1582
- * worth it (GENC-1567). Sized against {@link FoundationAiAssistant.contextBudget},
1583
- * so the plan aims at this model's window rather than a fixed default.
1584
- *
1585
- * Reads `messages` to stay reactive as the conversation grows: the driver's
1586
- * history is not a tracked observable, so every consumer of this keys its
1587
- * re-evaluation off the Redux mirror.
1588
- */
1589
1217
  get compactionPlan(): CompactionProjection | null;
1590
- /**
1591
- * Tooltip and hint for the Compact row — why it is or is not available.
1592
- *
1593
- * Replaces a fixed "Not enough conversation yet to be worth compacting", which
1594
- * since GENC-1567 could be flatly wrong: a long conversation whose tail is one
1595
- * indivisible tool result has plenty of conversation and still cannot be
1596
- * compacted. Reading the plan means the hint states the actual reason.
1597
- *
1598
- * @internal
1599
- */
1600
- get compactHint(): string;
1601
- /**
1602
- * How close this conversation is to the model's context window (GENC-1567).
1603
- *
1604
- * **Derived, never latched** — the opposite of {@link FoundationAiAssistant.blocked}.
1605
- * A budget wall stays set because nothing the user does fixes it; this clears
1606
- * itself the moment the conversation shrinks, which is precisely what the user
1607
- * is being asked to do. The two must not be conflated: `blocked` is also
1608
- * deliberately preserved across `resetSession`, and a context gate obviously
1609
- * must not survive starting a new chat.
1610
- *
1611
- * **A flow in progress is never blocked, only warned.** Machine state lives
1612
- * outside the transcript, so a stateful flow is not desynced by compaction —
1613
- * but the summary can still drop detail the flow's own prompt leans on, and
1614
- * blocking mid-journey would strand the user between a flow they cannot finish
1615
- * and a compaction that is a poor idea right there. Letting the flow complete
1616
- * and gating on the way out is the lesser harm; the headroom reserve exists so
1617
- * this is rare rather than routine.
1618
- */
1619
1218
  /**
1620
1219
  * WHY the composer is blocked, when it is (GENC-1582). `undefined` unless
1621
- * {@link FoundationAiAssistant.contextGate} is `'blocked'`.
1622
- *
1623
- * A cause rather than a fourth gate level: the severity is identical — sends are
1624
- * refused either way — and every consumer switches on the three levels. What
1625
- * differs is what the user can do about it, which is worth splitting because the
1626
- * two answers have no remedy in common.
1627
- *
1628
- * - `'transcript'` — the conversation has grown into the reserve. Compacting or
1629
- * clearing helps, and the copy says so.
1630
- * - `'model'` — the active agent's reserve meets or exceeds the whole window, so
1631
- * `blockAt` is 0 and EVERY message is refused, including the first of an empty
1632
- * chat. Neither compacting nor clearing can help: the constraint is the model,
1633
- * not the transcript. Before this existed the user was told to clear the chat,
1634
- * did, and hit the identical block with no way to understand why.
1635
- *
1636
- * Keyed off `blockAt === 0`, deliberately NOT off whether a compaction plan
1637
- * exists. A null plan has two unrelated causes — nothing left worth summarizing,
1638
- * versus a window that cannot host the agent at all — and conflating them is what
1639
- * produced the misleading copy.
1640
- *
1641
- * Note the block itself is correct in both cases and must stay: an agent that
1642
- * measurably cannot complete a turn in this window would otherwise start one and
1643
- * die mid-loop on a provider context error, with partial writes and no clean
1644
- * exit. Refusing up front is the better failure.
1220
+ * {@link FoundationAiAssistant.contextGate} is `'blocked'`. See
1221
+ * `ContextGateController.blockCause` for the `'transcript'` / `'model'` split
1222
+ * and why it is a cause rather than a fourth gate level.
1645
1223
  *
1646
1224
  * @internal
1647
1225
  */
1648
1226
  get contextBlockCause(): 'transcript' | 'model' | undefined;
1649
1227
  get contextGate(): 'ok' | 'warn' | 'blocked';
1650
- /**
1651
- * What the composer notice says about the context gate.
1652
- *
1653
- * The copy is a function of whether compaction can actually clear the gate,
1654
- * which is why it reads {@link FoundationAiAssistant.compactionPlan} rather
1655
- * than assuming. Telling someone to compact when the projection says it would
1656
- * not free enough — a tail that is one indivisible tool result, say — sends
1657
- * them into a summarizer call that leaves them exactly where they were.
1658
- *
1659
- * @internal
1660
- */
1661
1228
  get contextGateReason(): string;
1662
- /**
1663
- * Short label the session-menu pill wears while the gate is active — the
1664
- * primary visible signal, in place of a banner.
1665
- *
1666
- * Borrowed from the agent pin, which is the established way this composer says
1667
- * "a mode is in force": a pill that grows a label rather than a strip of prose
1668
- * above the input. It states the CONDITION only; the remedy lives in the
1669
- * placeholder once the composer is actually blocked, which is the moment the
1670
- * user needs to be told what to do rather than merely warned.
1671
- *
1672
- * @internal
1673
- */
1674
1229
  get contextGateLabel(): string;
1675
- /**
1676
- * Whether the composer notice has anything to show — a budget wall or the
1677
- * context gate. One region for both, so the live region stays single and
1678
- * registered (see the banner template's accessibility note).
1679
- *
1680
- * @internal
1681
- */
1230
+ get compactHint(): string;
1231
+ get compactable(): boolean;
1682
1232
  get composerNoticeVisible(): boolean;
1683
- /**
1684
- * Text for that region. A budget wall outranks the context gate: it is the
1685
- * condition the user cannot clear, so it must not be hidden behind advice
1686
- * about compacting that would not restore sending anyway.
1687
- *
1688
- * For the context gate the region is present and announced but not drawn — see
1689
- * {@link FoundationAiAssistant.composerNoticeSrOnly}.
1690
- *
1691
- * @internal
1692
- */
1693
1233
  get composerNoticeText(): string;
1694
- /**
1695
- * Whether the notice region should be announced but not drawn.
1696
- *
1697
- * The context gate shows itself visually through the pill's label, the
1698
- * composer outline and the placeholder — a strip of prose on top of those
1699
- * would be a fourth statement of the same thing. But dropping the region
1700
- * outright would be an accessibility regression, and a documented one: a
1701
- * `role="status"` region announces MUTATIONS to a region already being
1702
- * observed, and it is the only announcement available here, because the
1703
- * composer flips to `disabled` at the same moment and a disabled control is out
1704
- * of the tab order — so neither the placeholder nor an `aria-label` on it is
1705
- * ever voiced.
1706
- *
1707
- * So the region stays mounted, keeps its text and keeps announcing; only the
1708
- * visual treatment is withheld. A budget wall still draws its banner, because
1709
- * that condition has no other visible expression.
1710
- *
1711
- * @internal
1712
- */
1713
1234
  get composerNoticeSrOnly(): boolean;
1714
- /**
1715
- * Whether the menu's "Compact" action would do anything (GENC-1351 §5.7,
1716
- * GENC-1567). Delegates to the driver's plan so the gate uses the exact same
1717
- * history `compact()` acts on, judged against the same budget — one source of
1718
- * truth for both whether a cut is legal and whether it reclaims enough.
1719
- */
1720
- get compactable(): boolean;
1721
1235
  toggleSessionMenu(): void;
1722
1236
  /**
1723
1237
  * Set persistence on/off for this session (the menu switch). Reassigns the
@@ -1802,33 +1316,40 @@ export declare class FoundationAiAssistant extends GenesisElement {
1802
1316
  }): boolean;
1803
1317
  /** Whether the picker toggle button should appear. Mirrors the picker's own visibility rule. */
1804
1318
  get agentPickerEnabled(): boolean;
1805
- /** Whether the settings modal AI Model section has slotted app content, context usage or budget figures. */
1319
+ /**
1320
+ * The settings modal's derived surface. Every member below delegates to
1321
+ * `SettingsModalController` — the names are this element's public API and the
1322
+ * 943-line template's bindings, so they stay put while the logic moves into the
1323
+ * `settings-modal.ts` that folder never had (`docs/code-health.md` §3).
1324
+ *
1325
+ * None carries `@volatile`: the decorators live on the controller's getters, and
1326
+ * they reach the binding observer from there — see `docs/code-health.md` §11.4.
1327
+ */
1806
1328
  get settingsModelSectionVisible(): boolean;
1807
1329
  /** Built-in context window indicator (generic platform concern). */
1808
1330
  get settingsContextUsageVisible(): boolean;
1331
+ /**
1332
+ * The context indicator's figures, pre-formatted, or `undefined` when there is
1333
+ * nothing measurable to show — see {@link SettingsContextUsage}.
1334
+ *
1335
+ * @internal
1336
+ */
1337
+ get settingsContextUsage(): SettingsContextUsage | undefined;
1809
1338
  /**
1810
1339
  * Built-in per-vendor budget meter (GENC-1464) — the money sibling of
1811
1340
  * {@link FoundationAiAssistant.settingsContextUsageVisible}, with the same
1812
- * shape of gate: not configured off, and there is data to show. "Data" here
1813
- * is "at least one vendor has figures", so a host that feeds nothing (or only
1814
- * unlimited vendors) gets no meter and no empty container — exactly the
1815
- * context indicator's absent case.
1816
- *
1817
- * Answered from the RAW slice state rather than from
1818
- * {@link FoundationAiAssistant.settingsBudgetRows}, whose `.length` would
1819
- * format every row (a `formatUsd` pair and a `budgetPercentOf` each) only to
1820
- * ask whether any exist. This is read several times per render pass — the
1821
- * getter, the section gate, the template's `when` — so it stays as cheap as
1822
- * the context sibling's `!= null`.
1341
+ * shape of gate: not configured off, and there is data to show. See
1342
+ * `BudgetController.settingsUsageVisible` for why it reads the raw slice
1343
+ * rather than counting the rows.
1823
1344
  *
1824
1345
  * @internal
1825
1346
  */
1826
1347
  get settingsBudgetUsageVisible(): boolean;
1827
1348
  /**
1828
1349
  * The meter's rows — one per vendor that currently has figures, in
1829
- * `BUDGETED_VENDORS` order (the proxy's own metered list) so the rendering is
1830
- * stable however the host ordered its feed calls. Pre-formatted; see
1831
- * {@link SettingsBudgetRow} for why the text is unclamped while the bar is.
1350
+ * `BUDGETED_VENDORS` order so the rendering is stable however the host ordered
1351
+ * its feed calls. Pre-formatted; see {@link SettingsBudgetRow} for why the text
1352
+ * is unclamped while the bar is.
1832
1353
  *
1833
1354
  * @internal
1834
1355
  */
@@ -1845,38 +1366,25 @@ export declare class FoundationAiAssistant extends GenesisElement {
1845
1366
  get appSettingsTogglesHeading(): AssistantAppSettingsHeading | undefined;
1846
1367
  getAppSettingValue(id: string): boolean;
1847
1368
  setAppSettingValue(id: string, value: boolean): void;
1848
- private wireAppSettingsProvider;
1849
- private unwireAppSettingsProvider;
1850
1369
  get settingsCostCurrentSectionVisible(): boolean;
1851
1370
  /**
1852
1371
  * Whether the build-history section has anything in it — the built-in row list, or a host's own
1853
- * markup in the `settings-cost-history` slot.
1854
- *
1855
- * Slotted content is checked FIRST, ahead of `enabled`. That flag governs the *built-in* ledger:
1856
- * it gates the row writes, the reload, and the lifetime sum — and it defaults to `false`
1857
- * (`cost?.enabled === true`), while the Usage tab itself shows for `showCost`/`showTokens`, which
1858
- * default true. So a host can legitimately render its own build history into the slot and never
1859
- * opt into our `localStorage` ledger. Short-circuiting on `enabled` above the slot check hid that
1860
- * host's markup outright, since the slot is the only thing in the container.
1372
+ * markup in the `settings-cost-history` slot. See
1373
+ * `SettingsModalController.costHistorySectionVisible` for why slotted content is
1374
+ * checked ahead of `enabled`.
1861
1375
  */
1862
1376
  get settingsCostHistorySectionVisible(): boolean;
1863
1377
  /**
1864
- * Whether the "Build history" heading has anything under it.
1865
- *
1866
- * Broader than {@link FoundationAiAssistant.settingsCostHistorySectionVisible} because the
1867
- * heading also covers the lifetime total, which renders before the first row exists (rows are
1868
- * written as spend accrues, the lifetime section only needs history switched on). Gating the
1869
- * heading on the rows alone left that total sitting under nothing.
1378
+ * Whether the "Build history" heading has anything under it — broader than
1379
+ * {@link FoundationAiAssistant.settingsCostHistorySectionVisible}, because the
1380
+ * heading also covers the lifetime total, which renders before the first row exists.
1870
1381
  */
1871
1382
  get settingsCostHistoryHeadingVisible(): boolean;
1872
1383
  /** Whether the built-in Usage tab summary block has anything to show. */
1873
1384
  get settingsCostSummaryVisible(): boolean;
1874
1385
  /**
1875
- * Whether the lifetime-usage section has anything to say.
1876
- *
1877
- * Requires build history: with it off there are no rows, so `cumulativeUsage` falls back to
1878
- * the live session and the section would restate the block directly below it under a
1879
- * "lifetime" heading it hasn't earned.
1386
+ * Whether the lifetime-usage section has anything to say. Requires build history
1387
+ * AND a resolvable state key — see `SettingsModalController.costLifetimeVisible`.
1880
1388
  */
1881
1389
  get settingsCostLifetimeVisible(): boolean;
1882
1390
  /** Whether the Usage tab should appear in the settings modal. */
@@ -1891,15 +1399,6 @@ export declare class FoundationAiAssistant extends GenesisElement {
1891
1399
  setSettingsModalTab(tab: SettingsModalTab): void;
1892
1400
  /** Sync the active settings modal tab when the design-system tab bar changes. */
1893
1401
  onSettingsModalTabsChange(event: Event): void;
1894
- /** Ensure the settings tab is selected when the modal opens. */
1895
- private resetSettingsModalTabsOnOpen;
1896
- /** Return to the settings tab when the modal closes on Cost. */
1897
- private resetSettingsModalTabsOnClose;
1898
- /**
1899
- * Snap the tab underline to the settings tab without animating from Cost.
1900
- * Used after hide/show when aria state is correct but the indicator drifted.
1901
- */
1902
- private snapSettingsModalTabIndicator;
1903
1402
  /** Hint text for the currently pinned agent, if any. Used in the toggle button tooltip. */
1904
1403
  get pinnedAgentHint(): string | undefined;
1905
1404
  /**
@@ -1952,14 +1451,13 @@ export declare class FoundationAiAssistant extends GenesisElement {
1952
1451
  */
1953
1452
  private buildDiagnosticEntries;
1954
1453
  /**
1955
- * The single `meta-snapshot` entry for right now — the export-time `meta` block
1956
- * (agent summary, active prompt/state, context + cost) plus the `dedupSignature`
1957
- * the forward-capture delta keys on.
1454
+ * The `meta-snapshot` entry for right now. The assembly is a pure transformation
1455
+ * in `buildMetaSnapshot`; this gathers what it reports.
1958
1456
  *
1959
- * Separate from `buildDiagnosticEntries` because the download path needs a
1960
- * FRESH one on its own: the persisted stream only re-appends this block when the
1961
- * near-static config changes, so the newest stored snapshot's volatile half — the
1962
- * `context` figures especially — is typically frozen at the session's first flush.
1457
+ * The live agent comes from the DRIVER, not `this.activeAgent` (the redux-stripped
1458
+ * projection, no functions) and not a lookup in `this.agents` (which may mint
1459
+ * fresh, never-activated configs) — only the driver holds the instance whose
1460
+ * `onActivate` ran, so only its `getDebugSnapshot` closure holds state.
1963
1461
  */
1964
1462
  private buildMetaSnapshot;
1965
1463
  downloadDebugLog(): Promise<void>;
@@ -2065,13 +1563,6 @@ export declare class FoundationAiAssistant extends GenesisElement {
2065
1563
  handleFileSelect(e: Event): void;
2066
1564
  removeAttachment(attachment: ChatAttachment): void;
2067
1565
  removeAttachmentError(message: string): void;
2068
- private isAcceptedFile;
2069
- private readFileAsText;
2070
- /** The base64 payload of a file, without any `data:` prefix — the raw form
2071
- * `ChatImageAttachment.data` specifies (transports add their own framing). */
2072
- private readFileAsBase64;
2073
- private processFiles;
2074
- private loadSelectedFiles;
2075
1566
  handleSendClick(): void;
2076
1567
  /**
2077
1568
  * User clicked Stop. Cancels the current turn via the driver and flips the
@@ -2148,24 +1639,12 @@ export declare class FoundationAiAssistant extends GenesisElement {
2148
1639
  * mid-turn.
2149
1640
  */
2150
1641
  get sendButtonVisible(): boolean;
2151
- /** Whether the message input shows a drag handle to resize its height.
2152
- * On by default; hosts opt out via `chatConfig.ui.resizableInput: false`. */
1642
+ /** Whether the message input shows a drag handle to resize its height. */
2153
1643
  get isComposerResizable(): boolean;
2154
- /** Inline style for the chat input: writes the chosen height into the
2155
- * `--composer-height` custom property (read by `.chat-input` + its control in
2156
- * `main.styles.ts`). Empty until the user drags (or when resizing is disabled),
2157
- * so the default applies. */
1644
+ /** Inline style carrying the dragged height into `--composer-height`. */
2158
1645
  get composerHeightStyle(): string;
2159
- /** Begin a composer resize drag. Captures the pointer so move/up keep firing
2160
- * on the handle even when the cursor leaves its 7px band. */
2161
1646
  onComposerResizeStart(e: PointerEvent): void;
2162
- /** Resize while dragging. Dragging up (clientY decreases) grows the composer;
2163
- * the result is clamped between the natural height (lower) and the available
2164
- * headroom/max (upper). No-op unless a drag is active (the handle holds pointer
2165
- * capture), so plain hover-moves are ignored. */
2166
1647
  onComposerResizeMove(e: PointerEvent): void;
2167
- /** End a resize drag and release the pointer capture. */
2168
1648
  onComposerResizeEnd(e: PointerEvent): void;
2169
1649
  }
2170
- export {};
2171
1650
  //# sourceMappingURL=main.d.ts.map