@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
@@ -1675,6 +1675,84 @@
1675
1675
  "declarations": [],
1676
1676
  "exports": []
1677
1677
  },
1678
+ {
1679
+ "kind": "javascript-module",
1680
+ "path": "src/provider/ai-provider-switcher.ts",
1681
+ "declarations": [
1682
+ {
1683
+ "kind": "variable",
1684
+ "name": "AIProviderSwitcher",
1685
+ "description": "The DI token for the (AIProviderSwitcher:interface). Resolves a noop until the app\nregisters one."
1686
+ }
1687
+ ],
1688
+ "exports": [
1689
+ {
1690
+ "kind": "js",
1691
+ "name": "AIProviderSwitcher",
1692
+ "declaration": {
1693
+ "name": "AIProviderSwitcher",
1694
+ "module": "src/provider/ai-provider-switcher.ts"
1695
+ }
1696
+ }
1697
+ ]
1698
+ },
1699
+ {
1700
+ "kind": "javascript-module",
1701
+ "path": "src/provider/assistant-app-settings.ts",
1702
+ "declarations": [
1703
+ {
1704
+ "kind": "variable",
1705
+ "name": "AssistantAppSettingsProvider",
1706
+ "description": "The DI token for the (AssistantAppSettingsProvider:interface). Resolves a noop\nuntil the host registers one."
1707
+ }
1708
+ ],
1709
+ "exports": [
1710
+ {
1711
+ "kind": "js",
1712
+ "name": "AssistantAppSettingsProvider",
1713
+ "declaration": {
1714
+ "name": "AssistantAppSettingsProvider",
1715
+ "module": "src/provider/assistant-app-settings.ts"
1716
+ }
1717
+ }
1718
+ ]
1719
+ },
1720
+ {
1721
+ "kind": "javascript-module",
1722
+ "path": "src/main/attachment-controller.ts",
1723
+ "declarations": [],
1724
+ "exports": []
1725
+ },
1726
+ {
1727
+ "kind": "javascript-module",
1728
+ "path": "src/main/budget-controller.ts",
1729
+ "declarations": [],
1730
+ "exports": []
1731
+ },
1732
+ {
1733
+ "kind": "javascript-module",
1734
+ "path": "src/main/composer-menu-controller.ts",
1735
+ "declarations": [],
1736
+ "exports": []
1737
+ },
1738
+ {
1739
+ "kind": "javascript-module",
1740
+ "path": "src/main/composer-resize-controller.ts",
1741
+ "declarations": [],
1742
+ "exports": []
1743
+ },
1744
+ {
1745
+ "kind": "javascript-module",
1746
+ "path": "src/main/context-gate-controller.ts",
1747
+ "declarations": [],
1748
+ "exports": []
1749
+ },
1750
+ {
1751
+ "kind": "javascript-module",
1752
+ "path": "src/main/cost-ledger-controller.ts",
1753
+ "declarations": [],
1754
+ "exports": []
1755
+ },
1678
1756
  {
1679
1757
  "kind": "javascript-module",
1680
1758
  "path": "src/main/index.ts",
@@ -1787,6 +1865,59 @@
1787
1865
  "description": "Foundation AI Assistant component.",
1788
1866
  "name": "FoundationAiAssistant",
1789
1867
  "members": [
1868
+ {
1869
+ "kind": "field",
1870
+ "name": "composerResize",
1871
+ "privacy": "private",
1872
+ "readonly": true,
1873
+ "default": "new ComposerResizeController({\n enabled: () => this.chatConfig.ui?.resizableInput !== false,\n inputEl: () => this.chatInputEl,\n messagesEl: () => this.messagesEl,\n })"
1874
+ },
1875
+ {
1876
+ "kind": "field",
1877
+ "name": "composerMenus",
1878
+ "privacy": "private",
1879
+ "readonly": true,
1880
+ "default": "new ComposerMenuController(this, {\n agentPickerOpen: this.accessor('agentPickerOpen'),\n sessionMenuOpen: this.accessor('sessionMenuOpen'),\n focusInput: () => this.chatInputEl?.focus(),\n resetSessionConfirmations: () => {\n this.confirmingClear = false;\n this.confirmingCompact = false;\n },\n logMeta: (type, detail) => this.logMeta(type, detail),\n })"
1881
+ },
1882
+ {
1883
+ "kind": "field",
1884
+ "name": "attachmentCtrl",
1885
+ "privacy": "private",
1886
+ "readonly": true,
1887
+ "default": "new AttachmentController(this, {\n acceptedFiles: () => this.chatConfig.ui?.acceptedFiles,\n logMeta: (type, detail) => this.logMeta(type, detail),\n })"
1888
+ },
1889
+ {
1890
+ "kind": "field",
1891
+ "name": "contextGateCtrl",
1892
+ "privacy": "private",
1893
+ "readonly": true,
1894
+ "default": "new ContextGateController({\n chatConfig: () => this.chatConfig,\n messages: () => this.messages,\n contextTokens: () => this.contextTokens,\n contextLimit: () => this.contextLimit,\n gateContextTokens: () => this._gateContextTokens,\n contextReserve: () => this.contextReserve,\n preambleTokens: () => this.driver?.getPreambleTokens?.(),\n compactionPlanFor: (options) => this.driver?.getCompactionPlan?.(options) ?? null,\n bannerVisible: () => this.bannerVisible,\n effectiveBlockedReason: () => this.effectiveBlockedReason,\n flowOwnerAgentName: () => this.flowOwnerAgentName,\n })",
1895
+ "description": "Pure derivation — owns no state. Reaches the driver only through the two\ncallbacks below, so it stays clear of the stateful-agent surface (§11.2\nconstraint 2) and of redux entirely."
1896
+ },
1897
+ {
1898
+ "kind": "field",
1899
+ "name": "costLedger",
1900
+ "privacy": "private",
1901
+ "readonly": true,
1902
+ "default": "new CostLedgerController({\n chatConfig: () => this.chatConfig,\n messages: () => this.messages,\n sessionUsage: () => this.sessionUsage,\n providerStatuses: () => this.providerStatuses,\n persistenceActive: () => this.persistenceActive,\n stateKey: () => this.getStateKey(),\n headerTitle: () => this.headerTitle,\n provider: () => this.persistence.provider,\n elementId: () => this.id,\n })",
1903
+ "description": "Cost accounting and the persisted build-history ledger (GENC-1475). Reads the live\nsession through callbacks and owns the rows, the load/write latches and the debounce\ntimer — the element keeps only the names the settings modal binds."
1904
+ },
1905
+ {
1906
+ "kind": "field",
1907
+ "name": "budget",
1908
+ "privacy": "private",
1909
+ "readonly": true,
1910
+ "default": "new BudgetController({\n sessionRef: () => this._sessionRef,\n providerStatuses: () => this.providerStatuses,\n providerRegistry: () => this.providerRegistry,\n chatConfig: () => this.chatConfig,\n })",
1911
+ "description": "The AI-spend budget: vendor walls, the derived composer lock, the banner copy\nand the settings meter (GENC-1464).\n\nTakes the session store itself rather than a callback per field — see\n`BudgetDeps` for why this one differs from its siblings."
1912
+ },
1913
+ {
1914
+ "kind": "field",
1915
+ "name": "settings",
1916
+ "privacy": "private",
1917
+ "readonly": true,
1918
+ "default": "new SettingsModalController({\n chatConfig: () => this.chatConfig,\n contextTokens: () => this.contextTokens,\n contextLimit: () => this.contextLimit,\n budgetUsageVisible: () => this.budget.settingsUsageVisible,\n costConfig: () => this.costLedger.config,\n costHistoryLength: () => this.costLedger.history.length,\n liveCostSessionTitle: () => this.costLedger.liveTitle,\n providerStatusCount: () => this.providerStatuses.length,\n sessionRef: () => this._sessionRef,\n stateKey: () => this.getStateKey(),\n popoutMode: () => this.popoutMode,\n headerTitle: () => this.headerTitle,\n appSettingsProvider: () => this.appSettingsProvider,\n modal: () => this.settingsModal,\n tabsEl: () => this.settingsTabsEl,\n requestBudgetRefresh: () => this.requestBudgetRefresh(),\n logMeta: (type, detail) => this.logMeta(type, detail),\n })",
1919
+ "description": "The settings modal (`docs/code-health.md` §3). Reaches the two design-system\nrefs through callbacks, because the template writes them by name — see\n`SettingsModalDeps`."
1920
+ },
1790
1921
  {
1791
1922
  "kind": "field",
1792
1923
  "name": "providerRegistry",
@@ -2189,7 +2320,7 @@
2189
2320
  "type": {
2190
2321
  "text": "readonly AIProviderType[]"
2191
2322
  },
2192
- "description": "The distinct vendors the registry can currently reach, from the provider\nstatuses this element already loads on connect and refreshes on every\nobservable-registry change.\n\nThis is the element's answer to \"which vendor would the next turn use\" — a\nquestion that has **no** correct answer and deliberately gets no API. The\nprovider is resolved per turn AND per agent: `activeProviderInput` may be an\nasync function of the turn's context, an orchestrated turn picks its agent\nwith an LLM `classify()` call, and sub-agents resolve their own providers.\nAny pre-turn \"peek\" would therefore be a guess that is wrong precisely on the\nmulti-agent hosts per-vendor budgets exist for.\n\nAsking instead which vendors are REACHABLE is answerable, synchronous, and\nfree — and it is enough: the composer must stay live while any reachable\nvendor has headroom, and must lock when none does. It also makes vendor-switch\nrecovery a derivation rather than a mutation: a host that swaps its registry\nto another vendor fires the observable, the statuses reload, the walled vendor\ndrops out of this set, and `blocked` goes false with every latch left intact.",
2323
+ "description": "The distinct vendors the registry can currently reach, from the provider\nstatuses this element already loads on connect and refreshes on every\nobservable-registry change — this element's answer to \"which vendor would the\nnext turn use\", a question that has no correct answer and deliberately gets\nno API. See `BudgetController.reachableVendors` for why reachability is\nthe answerable question and what deriving from it buys.",
2193
2324
  "readonly": true
2194
2325
  },
2195
2326
  {
@@ -2198,7 +2329,7 @@
2198
2329
  "type": {
2199
2330
  "text": "readonly AIProviderType[]"
2200
2331
  },
2201
- "description": "Vendors currently walled by the AI-spend budget, in the order they were\nwalled. Empty for a host that never adopts the per-vendor API.\n\nMay include a vendor this registry cannot reach — either because the registry\nmoved on after the wall was latched, or because a 402's\n`otherVendorAvailable: false` walls every vendor the proxy meters (which is\nwhat that verdict is a statement about; see\nFoundationAiAssistant.latchBlockedFrom). Neither costs anything\ninternally — FoundationAiAssistant.blocked asks only about the\nreachable set, and the banner reads the reachability-filtered\nFoundationAiAssistant.relevantBlockedVendors — but a host rendering\nthis list itself should filter it against its own registry.\n\nRead-only on purpose: a settable array would let a host write a partial list\nand silently orphan the per-vendor banner copy.\nFoundationAiAssistant.setVendorBlocked is the write path.",
2332
+ "description": "Vendors currently walled by the AI-spend budget, in the order they were\nwalled. Empty for a host that never adopts the per-vendor API.\n\nMay include a vendor this registry cannot reach — either because the registry\nmoved on after the wall was latched, or because a 402's\n`otherVendorAvailable: false` walls every vendor the proxy meters. Neither\ncosts anything internally — FoundationAiAssistant.blocked asks only\nabout the reachable set, and the banner reads the reachability-filtered\n`BudgetController.relevantBlockedVendors` — but a host rendering this\nlist itself should filter it against its own registry.\n\nRead-only on purpose: a settable array would let a host write a partial list\nand silently orphan the per-vendor banner copy.\nFoundationAiAssistant.setVendorBlocked is the write path.",
2202
2333
  "readonly": true
2203
2334
  },
2204
2335
  {
@@ -2248,35 +2379,7 @@
2248
2379
  }
2249
2380
  }
2250
2381
  ],
2251
- "description": "Wall (or release) one vendor's budget, optionally with banner copy for it.\nThe per-vendor mirror of FoundationAiAssistant.setBlocked.\n\nWalling a vendor does **not** on its own disable the composer: while another\nreachable vendor has headroom the assistant stays usable and the banner tells\nthe user to switch. Only when every reachable vendor is walled does `blocked`\nbecome true.\n\n`reason` is composed into the banner sentence for that vendor, **however many\nvendors are walled** — see FoundationAiAssistant.effectiveBlockedReason.\nIt is per-vendor copy, not a whole-banner override; FoundationAiAssistant.blockedReason\nis the override. Omitting it keeps whatever explanation that vendor already\ncarried, so a driver latch landing after a host one cannot blank it.\n\n`'none'` is rejected with a warning rather than accepted: it is the \"no\nprovider configured\" sentinel, not a vendor. Latching it would put the raw\nsentinel in the banner (\"none's AI usage limit is reached\") and could never\nbe undone by derivation, because `'none'` never appears in\nFoundationAiAssistant.reachableVendors — so `blocked` could never\nbecome derivable from it either. Use FoundationAiAssistant.setBlocked\nfor a vendor-agnostic block."
2252
- },
2253
- {
2254
- "kind": "field",
2255
- "name": "vendorBudgetWarningIssued",
2256
- "type": {
2257
- "text": "boolean"
2258
- },
2259
- "privacy": "private",
2260
- "default": "false",
2261
- "description": "One warning per element for garbage fed to\nFoundationAiAssistant.setVendorBudget. Once, not per call, because\nthe documented feed re-runs after every turn — a host bug would otherwise\nrepeat the same warning on every send for the life of the page."
2262
- },
2263
- {
2264
- "kind": "method",
2265
- "name": "warnVendorBudgetOnce",
2266
- "privacy": "private",
2267
- "return": {
2268
- "type": {
2269
- "text": "void"
2270
- }
2271
- },
2272
- "parameters": [
2273
- {
2274
- "name": "message",
2275
- "type": {
2276
- "text": "string"
2277
- }
2278
- }
2279
- ]
2382
+ "description": "Wall (or release) one vendor's budget, optionally with banner copy for it.\nThe per-vendor mirror of FoundationAiAssistant.setBlocked.\n\nWalling a vendor does **not** on its own disable the composer: while another\nreachable vendor has headroom the assistant stays usable and the banner tells\nthe user to switch. Only when every reachable vendor is walled does `blocked`\nbecome true.\n\n`reason` is composed into the banner sentence for that vendor, **however many\nvendors are walled** — see `BudgetController.effectiveBlockedReason`.\nIt is per-vendor copy, not a whole-banner override;\nFoundationAiAssistant.blockedReason is the override. Omitting it keeps\nwhatever explanation that vendor already carried, so a driver latch landing\nafter a host one cannot blank it.\n\n`'none'` is rejected with a warning rather than accepted — it is the \"no\nprovider configured\" sentinel, not a vendor, and a wall on it could never be\nundone by derivation. Use FoundationAiAssistant.setBlocked for a\nvendor-agnostic block."
2280
2383
  },
2281
2384
  {
2282
2385
  "kind": "method",
@@ -2308,8 +2411,7 @@
2308
2411
  "type": {
2309
2412
  "text": "number | null"
2310
2413
  },
2311
- "default": "null",
2312
- "description": "When budget figures last arrived — from the host's FoundationAiAssistant.setVendorBudget\nor the wall latch's own feed — as epoch ms; `null` until the first feed.\n\nELEMENT state, not slice state, on purpose: the reducer is idempotent by\nvalue (an unchanged re-feed must not re-render the meter rows), but \"when\nwas this fetched\" must move on every feed, changed figures or not — a\nrefresh that confirms the same dollars is still a refresh. Only the\ntoolbar label observes this, so a stamp re-renders one span, not the rows."
2414
+ "description": "When budget figures last arrived — from the host's\nFoundationAiAssistant.setVendorBudget or the wall latch's own feed —\nas epoch ms; `null` until the first feed.\n\nELEMENT state, not slice state, on purpose: the reducer is idempotent by\nvalue (an unchanged re-feed must not re-render the meter rows), but \"when\nwas this fetched\" must move on every feed, changed figures or not — a\nrefresh that confirms the same dollars is still a refresh. Only the\ntoolbar label observes this, so a stamp re-renders one span, not the rows."
2313
2415
  },
2314
2416
  {
2315
2417
  "kind": "field",
@@ -2330,25 +2432,6 @@
2330
2432
  },
2331
2433
  "description": "Ask the host to re-fetch budget figures now (GENC-1464 refresh affordance).\n\nThe element cannot fetch — the meter is host-fed by design — so this emits\n`budget-refresh-requested` and the host answers with the same fetch that\nalready feeds FoundationAiAssistant.setVendorBudget (Create's handler coalesces in-flight\nfetches, so a spammed button costs one request). Fired by the meter's\nrefresh button and on settings-modal open, so the figures are fresh exactly\nwhen someone is looking at them. Fires unconditionally: a host without a\nlistener simply keeps the per-turn cadence."
2332
2434
  },
2333
- {
2334
- "kind": "method",
2335
- "name": "isVendorSwept",
2336
- "privacy": "private",
2337
- "return": {
2338
- "type": {
2339
- "text": "boolean"
2340
- }
2341
- },
2342
- "parameters": [
2343
- {
2344
- "name": "vendor",
2345
- "type": {
2346
- "text": "AIProviderType"
2347
- }
2348
- }
2349
- ],
2350
- "description": "Whether this vendor's wall came from the sweep alone — see the slice's `sweptVendors`."
2351
- },
2352
2435
  {
2353
2436
  "kind": "field",
2354
2437
  "name": "blockedReason",
@@ -2389,7 +2472,7 @@
2389
2472
  "text": "string | undefined"
2390
2473
  },
2391
2474
  "privacy": "private",
2392
- "description": "Copy the driver writes into the TRANSCRIPT when a turn hits the budget wall,\nor `undefined` to let the driver compose it per wall.\n\nA set `blockedReason` still wins — a white-labelled host must not read its\nown explanation in the banner and shipped copy directly below it, and the\nvendor-agnostic latch's `formatBlockedReason(budget)` form stays advice-free\nexactly as before.\n\n`undefined` (the usual case) is NOT the old `DEFAULT_BUDGET_EXHAUSTED_MESSAGE`\nfallback: it hands composition to the driver, which builds the bubble from\nEACH wall's own 402 — naming the exhausted vendor and advising a switch only\nwhen that refusal's `otherVendorAvailable` positively vouched for headroom\nelsewhere. That is immune to the staleness this getter used to guard against\nby freezing the copy: construction-time advice could outlive its truth\n(`getOrCreateDriver` re-runs on an agents swap and would have baked the\nsentence into every later bubble), but per-wall advice is derived from the\nrefusal it annotates and dies with the turn. Forcing the default here was\nalso what made the driver's composed bubble unreachable in every real\nmount — the banner advised switching while the bubble under it never did\n(the GENC-1464 tester finding, root cause).",
2475
+ "description": "Copy the driver writes into the TRANSCRIPT when a turn hits the budget wall,\nor `undefined` to let the driver compose it per wall — see\n`BudgetController.transcriptMessage`.",
2393
2476
  "readonly": true
2394
2477
  },
2395
2478
  {
@@ -2476,7 +2559,7 @@
2476
2559
  "type": {
2477
2560
  "text": "CostSessionModelEntry[]"
2478
2561
  },
2479
- "description": "Distinct models the registry currently resolves to — the live session's model chips.\n\n`providerStatuses` carries one entry per registry SLOT, so a vendor configured across several\ntiers that happen to resolve to the same model reports it once per tier, and the chips\nrendered it once per tier too. Which tier a model came from is not what these chips are for.\n\nDeduped on the label actually displayed, not on `status.model`: a slot whose provider reports\nno model falls back to the slot's own name, and those are distinct per slot, so keying on the\nlabel keeps them from collapsing into one another.",
2562
+ "description": "The cost ledger's derived surface (GENC-1475). Every member below delegates to\n`CostLedgerController` — the names are this element's public API and the\nsettings modal's bindings, so they stay put while the accounting moves.\n\nNone carries `@volatile`: the decorators live on the controller's getters, and they\nreach the binding observer from there — see `docs/code-health.md` §11.4.",
2480
2563
  "readonly": true
2481
2564
  },
2482
2565
  {
@@ -2485,7 +2568,7 @@
2485
2568
  "type": {
2486
2569
  "text": "CostSessionRecord[]"
2487
2570
  },
2488
- "description": "Build history for display: most recently worked on first.\n\nThe stored order is insertion order (see `upsertRecord`), which leaves a project returned to\nsitting wherever it was first seen — so ordering is applied here, on read, where it also\ncovers rows a host provider supplied.",
2571
+ "description": "Build history for display: most recently worked on first.",
2489
2572
  "readonly": true
2490
2573
  },
2491
2574
  {
@@ -2494,7 +2577,7 @@
2494
2577
  "type": {
2495
2578
  "text": "AggregateUsage"
2496
2579
  },
2497
- "description": "Lifetime usage across every project in the history scope — cost and all four token\nbuckets. What the Usage tab's lifetime section renders.\n\nRows ONLY — deliberately not rows + live session. Each row holds a project's lifetime\nusage and the active project's row is kept current on every transcript change, so\nadding the live total would count this project's spend twice. That double-count is what\nmade the figure grow on every refresh: a page-hide banked a row while the restored\nsession still carried the same spend.",
2580
+ "description": "Lifetime usage across every project in the history scope — rows only, never plus live.",
2498
2581
  "readonly": true
2499
2582
  },
2500
2583
  {
@@ -2512,7 +2595,7 @@
2512
2595
  "type": {
2513
2596
  "text": "boolean"
2514
2597
  },
2515
- "description": "Whether the live session figures amount to this project's whole history, which is true\nexactly when the transcript is restored on load. Drives the Usage tab's live labels\n(\"Project cost\" vs \"Session cost\") so they state the scope rather than imply one.\n\nNote this is NOT what makes the *rows* lifetime — those bank their pre-transcript spend\n(see `CostSessionRecord.banked`) and are lifetime either way.",
2598
+ "description": "Whether the live session figures amount to this project's whole history.",
2516
2599
  "readonly": true
2517
2600
  },
2518
2601
  {
@@ -2524,6 +2607,14 @@
2524
2607
  "description": "Resolved cost tab and build-history settings from `chatConfig.costHistory`.",
2525
2608
  "readonly": true
2526
2609
  },
2610
+ {
2611
+ "kind": "field",
2612
+ "name": "costSessionHistory",
2613
+ "type": {
2614
+ "text": "CostSessionRecord[]"
2615
+ },
2616
+ "description": "Persisted finalized cost sessions shown on the Usage tab build history list.\n\nA get/set pair rather than a read-only projection: the banking tests seed rows to\nmodel a page load that found some, and a host provider's rows arrive the same way."
2617
+ },
2527
2618
  {
2528
2619
  "kind": "field",
2529
2620
  "name": "sessionTokensConsumed",
@@ -2559,26 +2650,7 @@
2559
2650
  }
2560
2651
  }
2561
2652
  ],
2562
- "description": "Write this session's usage into persisted build history (`localStorage`) as a single\nrow keyed by the session key, replacing any existing row for that key.\n\n**Idempotent** — safe to call per turn, on tab close, and on a title change alike.\nThe name is historical: it does NOT end the session or reset the running totals.\nResetting them is what used to double-count spend, since the row was banked while the\npersisted transcript still carried the same usage, so a reload restored it and the\nUsage tab counted both.\n\nNo-op unless `chatConfig.costHistory.enabled` is `true`, when the live session has no\nusage yet, or when the session has no key to upsert against. Gating on the *live*\nfigures (not the banked total) means a reload that restores nothing rewrites nothing."
2563
- },
2564
- {
2565
- "kind": "method",
2566
- "name": "bankedUsageFor",
2567
- "privacy": "private",
2568
- "return": {
2569
- "type": {
2570
- "text": "AggregateUsage"
2571
- }
2572
- },
2573
- "parameters": [
2574
- {
2575
- "name": "projectKey",
2576
- "type": {
2577
- "text": "string"
2578
- }
2579
- }
2580
- ],
2581
- "description": "Spend for `projectKey` that predates the current transcript, resolved once per SESSION and\nshared by every element instance wired to it (see `banked-usage-baselines`), so it cannot\ndrift as the row it came from is rewritten by our own upserts.\n\nThe two cases turn on whether the transcript is authoritative for this project:\n\n- **Transcript restored** (`persistenceActive`): it already accounts for everything after\n the previous row's own banked figure, so carry that figure forward unchanged. Re-banking\n `usage` here would add the restored history to itself on every reload.\n- **Transcript starts empty**: nothing on screen accounts for the previous total, so the\n whole of it becomes banked. Without this the next turn would rewrite the row downwards\n to just that turn, losing the project's earlier spend.\n\nThe baseline is deliberately NOT element state. This element is remounted repeatedly inside\na single session (the host re-rendering its layout, popping out, docking) and a fresh\ninstance re-deriving would take the second branch against rows that already hold the live\ntranscript — banking the session as its own baseline. Keying on the session instead means a\nremount inherits the baseline that transcript was paired with."
2653
+ "description": "Write this session's usage into persisted build history as a single row keyed by the\nsession key, replacing any existing row for that key.\n\n**Idempotent** — safe to call per turn, on tab close, and on a title change alike. The\nname is historical: it does NOT end the session or reset the running totals. See\n`CostLedgerController.finalize` for the full contract and the no-op cases."
2582
2654
  },
2583
2655
  {
2584
2656
  "kind": "method",
@@ -2603,7 +2675,7 @@
2603
2675
  }
2604
2676
  }
2605
2677
  ],
2606
- "description": "Re-baseline this session against an emptied transcript: what came before, plus what the\ntranscript being discarded proved. `discarded` must be read BEFORE the live figures are\nreset, since that is the only record of the latter.\n\nNot the same as dropping the baseline. Dropping it re-derives through `persistenceActive`,\nand with persistence ON that carries the row's *old* banked figure forward — discarding\nevery turn between the last baseline and the clear. The transcript is gone either way, so\nthe branch that reads it as authoritative no longer applies.\n\nDerived from the memoised baseline and the live figure rather than from the project's row,\nbecause a row is only as current as the last `finalizeCostSession` — which skips its write\nwhile the ledger is unloaded, and while the session has no usage yet."
2678
+ "description": "Re-baseline this session against an emptied transcript — see\n`CostLedgerController.rebaselineOnClearedTranscript`. `discarded` must be read\nBEFORE the live figures are reset, since that is the only record of the latter."
2607
2679
  },
2608
2680
  {
2609
2681
  "kind": "method",
@@ -2669,73 +2741,6 @@
2669
2741
  },
2670
2742
  "description": "Clear all persisted build history for this assistant scope."
2671
2743
  },
2672
- {
2673
- "kind": "method",
2674
- "name": "getCostHistoryScope",
2675
- "privacy": "private",
2676
- "return": {
2677
- "type": {
2678
- "text": "string"
2679
- }
2680
- }
2681
- },
2682
- {
2683
- "kind": "method",
2684
- "name": "reloadCostSessionHistory",
2685
- "privacy": "private",
2686
- "return": {
2687
- "type": {
2688
- "text": "void"
2689
- }
2690
- }
2691
- },
2692
- {
2693
- "kind": "method",
2694
- "name": "costHistoryProvider",
2695
- "privacy": "private",
2696
- "description": "The persistence provider, but only when it implements BOTH cost-history methods.\n\nBoth or neither: a provider that can save but not load would silently accumulate a\nledger nobody ever reads back, so a partial implementation is treated as absent and\nfalls back to `localStorage`."
2697
- },
2698
- {
2699
- "kind": "method",
2700
- "name": "persistCostHistory",
2701
- "privacy": "private",
2702
- "return": {
2703
- "type": {
2704
- "text": "void"
2705
- }
2706
- },
2707
- "parameters": [
2708
- {
2709
- "name": "records",
2710
- "type": {
2711
- "text": "CostSessionRecord[]"
2712
- }
2713
- }
2714
- ],
2715
- "description": "Adopt `records` as the live list, and schedule a write to whichever backend owns cost\nhistory — the provider when the host implements it, else `localStorage`.\n\nThe list is adopted synchronously because that is what the Usage tab renders; only the\n*write* is deferred. This runs on every usage change (about once per LLM call, plus once\nper sub-agent turn) and each write serialises the whole ledger, so coalescing cuts a\nper-turn `localStorage.setItem` or network round-trip down to one per quiet period. Safe\nbecause the write is a full replace of idempotent state: the newest call wins and no\nintermediate state is meaningful. Flushed on `pagehide` and disconnect so nothing is lost."
2716
- },
2717
- {
2718
- "kind": "method",
2719
- "name": "flushCostHistory",
2720
- "privacy": "private",
2721
- "return": {
2722
- "type": {
2723
- "text": "void"
2724
- }
2725
- },
2726
- "description": "Write the pending ledger now. Safe to call when nothing is pending.\n\nProvider writes are fire-and-forget and best-effort: the ledger is accounting, not\nconversation content, so a failed write is logged and never surfaced into the chat. On\n`pagehide` the `localStorage` path completes synchronously; a provider write is subject to\nthe same unload race as every other network save the assistant makes."
2727
- },
2728
- {
2729
- "kind": "method",
2730
- "name": "syncActiveCostSessionTitle",
2731
- "privacy": "private",
2732
- "return": {
2733
- "type": {
2734
- "text": "void"
2735
- }
2736
- },
2737
- "description": "Finalize the previous session when the host-provided title changes — a rename should land on\nthe row under its old name rather than silently relabelling accrued spend.\n\nOnly when the session key still matches the one that title accrued under. Hosts change\n`costHistory.title` on project SWITCH too, and if the key has already flipped, this would\nupsert the outgoing project's live usage onto the incoming project's row as\n`banked + old live`, inflating it. Under the old append model a mis-keyed write left a stray\nrow; with the upsert it overwrites a real one."
2738
- },
2739
2744
  {
2740
2745
  "kind": "field",
2741
2746
  "name": "activeModel",
@@ -2782,7 +2787,7 @@
2782
2787
  "type": {
2783
2788
  "text": "ChatAttachment[]"
2784
2789
  },
2785
- "default": "[]"
2790
+ "description": "Attachments pending on the next message."
2786
2791
  },
2787
2792
  {
2788
2793
  "kind": "field",
@@ -2790,7 +2795,7 @@
2790
2795
  "type": {
2791
2796
  "text": "string[]"
2792
2797
  },
2793
- "default": "[]"
2798
+ "description": "Rejection messages for files the last pick refused."
2794
2799
  },
2795
2800
  {
2796
2801
  "kind": "field",
@@ -2807,7 +2812,6 @@
2807
2812
  "type": {
2808
2813
  "text": "number"
2809
2814
  },
2810
- "default": "0",
2811
2815
  "description": "Bump to re-render app settings toggles when the host provider notifies a change."
2812
2816
  },
2813
2817
  {
@@ -2816,7 +2820,6 @@
2816
2820
  "type": {
2817
2821
  "text": "boolean"
2818
2822
  },
2819
- "default": "false",
2820
2823
  "description": "Whether the settings modal is open."
2821
2824
  },
2822
2825
  {
@@ -2834,7 +2837,6 @@
2834
2837
  "type": {
2835
2838
  "text": "SettingsModalTab"
2836
2839
  },
2837
- "default": "'settings'",
2838
2840
  "description": "Active tab inside the settings modal."
2839
2841
  },
2840
2842
  {
@@ -2970,41 +2972,28 @@
2970
2972
  "type": {
2971
2973
  "text": "HTMLElement[]"
2972
2974
  },
2973
- "default": "[]",
2974
- "description": "Light-DOM nodes assigned to settings modal slots (via `slotted()` bindings)."
2975
+ "description": "Light-DOM nodes assigned to settings modal slots. The template's `slotted()`\nbindings write these by property NAME, so each needs a real setter here even\nthough the controller owns the arrays."
2975
2976
  },
2976
2977
  {
2977
2978
  "kind": "field",
2978
2979
  "name": "settingsAppSlotted",
2979
2980
  "type": {
2980
2981
  "text": "HTMLElement[]"
2981
- },
2982
- "default": "[]"
2982
+ }
2983
2983
  },
2984
2984
  {
2985
2985
  "kind": "field",
2986
2986
  "name": "settingsCostCurrentSlotted",
2987
2987
  "type": {
2988
2988
  "text": "HTMLElement[]"
2989
- },
2990
- "default": "[]"
2989
+ }
2991
2990
  },
2992
2991
  {
2993
2992
  "kind": "field",
2994
2993
  "name": "settingsCostHistorySlotted",
2995
2994
  "type": {
2996
2995
  "text": "HTMLElement[]"
2997
- },
2998
- "default": "[]"
2999
- },
3000
- {
3001
- "kind": "field",
3002
- "name": "costSessionHistory",
3003
- "type": {
3004
- "text": "CostSessionRecord[]"
3005
- },
3006
- "default": "[]",
3007
- "description": "Persisted finalized cost sessions shown on the Usage tab build history list."
2996
+ }
3008
2997
  },
3009
2998
  {
3010
2999
  "kind": "field",
@@ -3023,6 +3012,16 @@
3023
3012
  },
3024
3013
  "privacy": "private"
3025
3014
  },
3015
+ {
3016
+ "kind": "field",
3017
+ "name": "orchestrator",
3018
+ "type": {
3019
+ "text": "OrchestratingDriver | undefined"
3020
+ },
3021
+ "privacy": "private",
3022
+ "description": "This element's driver as an orchestrator, or `undefined` when the session is\nrunning a bare `ChatDriver`.\n\nThe five call sites that used to test the type themselves now read this\n(§11.2 constraint 3 of `docs/code-health.md`): the agent pin, the flow-owner\nlock, the wire-up in `wireDriver`, and the two `driverKind` diagnostics. The\ntype test itself lives in `asOrchestrator`, shared with the session\npersister.",
3023
+ "readonly": true
3024
+ },
3026
3025
  {
3027
3026
  "kind": "field",
3028
3027
  "name": "driverCleanup",
@@ -3065,72 +3064,6 @@
3065
3064
  "privacy": "private",
3066
3065
  "description": "Unsubscribe handle for the provider-registry change listener (observable registries only)."
3067
3066
  },
3068
- {
3069
- "kind": "field",
3070
- "name": "unsubAppSettingsProvider",
3071
- "type": {
3072
- "text": "() => void | undefined"
3073
- },
3074
- "privacy": "private",
3075
- "description": "Unsubscribe handle for (AssistantAppSettingsProvider:interface).subscribe."
3076
- },
3077
- {
3078
- "kind": "field",
3079
- "name": "_activeCostSessionTitle",
3080
- "type": {
3081
- "text": "string"
3082
- },
3083
- "privacy": "private",
3084
- "default": "''",
3085
- "description": "Title tracked for the in-flight cost session (used when `header-title` changes)."
3086
- },
3087
- {
3088
- "kind": "field",
3089
- "name": "_activeCostSessionKey",
3090
- "type": {
3091
- "text": "string | undefined"
3092
- },
3093
- "privacy": "private",
3094
- "description": "Session key `_activeCostSessionTitle` was captured under, so a title change can tell a rename\n(same key) from a project switch (key already flipped) — see `syncActiveCostSessionTitle`."
3095
- },
3096
- {
3097
- "kind": "field",
3098
- "name": "_costHistoryLoaded",
3099
- "type": {
3100
- "text": "boolean"
3101
- },
3102
- "privacy": "private",
3103
- "default": "false",
3104
- "description": "Whether the row list reflects storage yet. Gates cost writes so a project's banked\nspend is never derived from a ledger that simply hasn't arrived."
3105
- },
3106
- {
3107
- "kind": "field",
3108
- "name": "_ledgerUnreadable",
3109
- "type": {
3110
- "text": "boolean"
3111
- },
3112
- "privacy": "private",
3113
- "default": "false",
3114
- "description": "Set when a provider read failed. Latches writes off for this page-load: `saveCostHistory`\nreplaces the whole stored list, so writing anything derived from the empty list we fell back\nto would destroy rows we never managed to read."
3115
- },
3116
- {
3117
- "kind": "field",
3118
- "name": "_ledgerDirty",
3119
- "type": {
3120
- "text": "boolean"
3121
- },
3122
- "privacy": "private",
3123
- "default": "false",
3124
- "description": "Pending (adopted but unwritten) ledger changes — see `persistCostHistory`."
3125
- },
3126
- {
3127
- "kind": "field",
3128
- "name": "_ledgerFlushTimer",
3129
- "type": {
3130
- "text": "ReturnType<typeof setTimeout> | undefined"
3131
- },
3132
- "privacy": "private"
3133
- },
3134
3067
  {
3135
3068
  "kind": "field",
3136
3069
  "name": "_handlePageHide",
@@ -3171,52 +3104,31 @@
3171
3104
  "description": "Bound to the rapid-text-area chat input via the template ref."
3172
3105
  },
3173
3106
  {
3174
- "kind": "field",
3175
- "name": "composerHeightPx",
3176
- "type": {
3177
- "text": "number | null"
3178
- },
3179
- "default": "null",
3180
- "description": "Explicit composer height (px) once the user has dragged the resize handle;\n`null` keeps the default auto-grow behaviour. Written into the\n`--composer-height` custom property via the `composerHeightStyle` getter."
3181
- },
3182
- {
3183
- "kind": "field",
3184
- "name": "_resizeStartY",
3185
- "type": {
3186
- "text": "number"
3187
- },
3107
+ "kind": "method",
3108
+ "name": "accessor",
3188
3109
  "privacy": "private",
3189
- "default": "0",
3190
- "description": "Pointer Y and composer height captured at the start of a resize drag."
3191
- },
3192
- {
3193
- "kind": "field",
3194
- "name": "_resizeStartHeight",
3195
- "type": {
3196
- "text": "number"
3110
+ "return": {
3111
+ "type": {
3112
+ "text": "Accessor<this[K]>"
3113
+ }
3197
3114
  },
3198
- "privacy": "private",
3199
- "default": "0"
3115
+ "parameters": [
3116
+ {
3117
+ "name": "key",
3118
+ "type": {
3119
+ "text": "K"
3120
+ }
3121
+ }
3122
+ ],
3123
+ "description": "A get/set pair over one of this element's own properties, for a controller\nthat drives state it does not own. Written as a helper rather than two inline\nclosures per call: a pair of same-typed closures is easy to transpose by\naccident, and a single object cannot be."
3200
3124
  },
3201
3125
  {
3202
3126
  "kind": "field",
3203
- "name": "_composerNaturalHeight",
3127
+ "name": "composerHeightPx",
3204
3128
  "type": {
3205
- "text": "number"
3129
+ "text": "number | null"
3206
3130
  },
3207
- "privacy": "private",
3208
- "default": "0",
3209
- "description": "The composer's natural (un-resized) height, measured on first drag — used\nas the lower bound so the box can't be dragged smaller than it started."
3210
- },
3211
- {
3212
- "kind": "field",
3213
- "name": "_resizeMaxHeight",
3214
- "privacy": "private",
3215
- "default": "400",
3216
- "description": "Upper bound for the current drag — capped to the headroom above so growing\nthe composer never pushes the input row's buttons off the bottom.",
3217
- "type": {
3218
- "text": "number"
3219
- }
3131
+ "description": "Explicit composer height once dragged; `null` keeps the default auto-grow."
3220
3132
  },
3221
3133
  {
3222
3134
  "kind": "field",
@@ -3617,16 +3529,6 @@
3617
3529
  },
3618
3530
  "description": "Push the mid-loop context guard down to the driver (GENC-1567).\n\nCalled wherever the inputs can change — the resolved provider status, and a\nfreshly built driver. A driver that never receives one simply runs without\nthe guard, which is the right default: it fires only when a real context\nwindow is known."
3619
3531
  },
3620
- {
3621
- "kind": "field",
3622
- "name": "contextGuardPolicy",
3623
- "type": {
3624
- "text": "ContextGuardPolicy | undefined"
3625
- },
3626
- "privacy": "private",
3627
- "description": "Guard policy pushed to the driver, or `undefined` when the host has switched\nthe gate off — so `enabled: false` really does mean no local blocking of any\nkind, UI or mid-loop.\n\nA MARGIN, not a threshold: the driver resolves the window of the provider\neach call actually goes to. The fallback window rides along because the\ndriver cannot learn it any other way — without it, a host that knows its\nwindow while its provider does not report one had `fallbackContextLimit`\nprotecting the composer and nothing else.",
3628
- "readonly": true
3629
- },
3630
3532
  {
3631
3533
  "kind": "method",
3632
3534
  "name": "loadProviderStatuses",
@@ -3815,203 +3717,133 @@
3815
3717
  "description": "Record a meta/lifecycle event onto this session's debug-log timeline\n(exported via FoundationAiAssistant.getDebugLog). Every event\nauto-carries `placement` so a session emitted from both the bubble and the\nlayout panel stays disambiguated. Keyed by `stateKey` so the timeline is\nshared across pop-in/pop-out and survives driver rebuilds."
3816
3718
  },
3817
3719
  {
3818
- "kind": "field",
3819
- "name": "_dismissPointerHandler",
3820
- "type": {
3821
- "text": "(e: Event) => void | undefined"
3822
- },
3823
- "privacy": "private",
3720
+ "kind": "method",
3721
+ "name": "toggleAgentPicker",
3824
3722
  "description": "Document-level dismissal for whichever composer menu is open. Bound only\nwhile one IS open — a permanently-attached listener on a component that can\nbe mounted twice (bubble + popout panel) is a cost every click on the page\npays for a menu that is usually closed.\n\nUses `composedPath()` rather than `event.target`: the click lands on a node\ninside this element's shadow root, which retargets to the host, so a plain\n`contains()` check cannot tell \"clicked the menu\" from \"clicked the\ntranscript\". The path crosses the boundary and carries both."
3825
3723
  },
3826
3724
  {
3827
3725
  "kind": "field",
3828
- "name": "_dismissKeyHandler",
3726
+ "name": "confirmingClear",
3829
3727
  "type": {
3830
- "text": "(e: KeyboardEvent) => void | undefined"
3728
+ "text": "boolean"
3831
3729
  },
3832
- "privacy": "private"
3730
+ "default": "false",
3731
+ "description": "Two-step confirm for the destructive Clear action; reset whenever the menu toggles."
3833
3732
  },
3834
3733
  {
3835
3734
  "kind": "field",
3836
- "name": "_dismissResizeHandler",
3735
+ "name": "confirmingCompact",
3837
3736
  "type": {
3838
- "text": "() => void | undefined"
3839
- },
3840
- "privacy": "private"
3841
- },
3842
- {
3843
- "kind": "method",
3844
- "name": "closeOpenComposerMenus",
3845
- "privacy": "private",
3846
- "return": {
3847
- "type": {
3848
- "text": "void"
3849
- }
3850
- }
3851
- },
3852
- {
3853
- "kind": "method",
3854
- "name": "positionComposerMenu",
3855
- "privacy": "private",
3856
- "return": {
3857
- "type": {
3858
- "text": "void"
3859
- }
3737
+ "text": "boolean"
3860
3738
  },
3861
- "parameters": [
3862
- {
3863
- "name": "panelSelector",
3864
- "type": {
3865
- "text": "string"
3866
- }
3867
- },
3868
- {
3869
- "name": "triggerSelector",
3870
- "type": {
3871
- "text": "string"
3872
- }
3873
- }
3874
- ],
3875
- "description": "Places an open composer menu directly above the control that opened it.\n\nCSS alone cannot: the menu is a child of `.composer` — which is what lets its\nwidth be capped against the composer rather than against a button — while its\nanchor is a button several levels down the bar, and no selector can read a\nsibling's offset. So the left edge is measured on open and written as a\ncustom property.\n\nClamped to the composer, so a menu opened from a control near the right edge\nof a narrow panel slides back inside instead of overhanging. The animation\norigin is written separately and keeps pointing at the button's centre even\nafter such a shift — the menu still grows out of the thing that opened it,\nwhich is the whole point of anchoring it there."
3739
+ "default": "false"
3876
3740
  },
3877
3741
  {
3878
- "kind": "method",
3879
- "name": "repositionOpenComposerMenu",
3880
- "privacy": "private",
3881
- "return": {
3882
- "type": {
3883
- "text": "void"
3884
- }
3742
+ "kind": "field",
3743
+ "name": "sessionMenuEnabled",
3744
+ "type": {
3745
+ "text": "boolean"
3885
3746
  },
3886
- "description": "Re-place whichever menu is open. Cheap enough to run from a resize."
3887
- },
3888
- {
3889
- "kind": "method",
3890
- "name": "syncComposerMenuDismissal",
3891
- "privacy": "private",
3892
- "return": {
3893
- "type": {
3894
- "text": "void"
3895
- }
3896
- }
3897
- },
3898
- {
3899
- "kind": "method",
3900
- "name": "teardownComposerMenuDismissal",
3901
- "privacy": "private",
3902
- "return": {
3903
- "type": {
3904
- "text": "void"
3905
- }
3906
- }
3747
+ "description": "Whether the lifecycle-menu button shows. It occupies the stop-button slot,\nso it only appears when idle (mutually exclusive with Stop). Gated on\npersistence — the menu's actions (Compact, Clear, the \"remember chat\" toggle)\nonly make sense with a provider configured, so it's hidden entirely without\none. A host can additionally force it off via `chatConfig.ui.showSessionMenu:\nfalse` even when a provider is present (e.g. a guided onboarding flow where\nclearing/compacting mid-journey would be disruptive).",
3748
+ "readonly": true
3907
3749
  },
3908
3750
  {
3909
3751
  "kind": "field",
3910
- "name": "MENU_HEADROOM_RESERVE",
3752
+ "name": "persistenceToggleable",
3911
3753
  "type": {
3912
- "text": "number"
3754
+ "text": "boolean"
3913
3755
  },
3914
- "privacy": "private",
3915
- "static": true,
3916
- "readonly": true,
3917
- "default": "20",
3918
- "description": "Vertical space an open menu must leave free above the button, covering BOTH\nthe gap the menu sits off the button by (8px, declared in design units in\nmain.styles.ts) and the breathing room wanted above it. The two are lumped\ntogether because only the second half is a free choice: the first is fixed by\nthe stylesheet and simply has to be subtracted from the headroom, and a\nconstant named for only one of them invites someone to \"fix\" a gap that is\ndoing exactly what it should."
3756
+ "description": "The persistence toggle row renders only when a provider is configured.",
3757
+ "readonly": true
3919
3758
  },
3920
3759
  {
3921
3760
  "kind": "field",
3922
- "name": "_agentPickerToggle",
3923
- "privacy": "private",
3924
- "readonly": true,
3925
- "default": "new AnimatedPanelToggle(\n this,\n '.agent-picker-panel',\n AGENT_PICKER_CLOSE_MS,\n () => this.agentPickerOpen,\n (v) => {\n this.agentPickerOpen = v;\n },\n )"
3926
- },
3927
- {
3928
- "kind": "method",
3929
- "name": "toggleAgentPicker"
3761
+ "name": "persistenceActive",
3762
+ "type": {
3763
+ "text": "boolean"
3764
+ },
3765
+ "description": "Whether persistence is currently active (on + a provider) for this session.",
3766
+ "readonly": true
3930
3767
  },
3931
3768
  {
3932
3769
  "kind": "field",
3933
- "name": "confirmingClear",
3770
+ "name": "contextBudget",
3934
3771
  "type": {
3935
- "text": "boolean"
3936
- },
3937
- "default": "false",
3938
- "description": "Two-step confirm for the destructive Clear action; reset whenever the menu toggles."
3772
+ "text": "ResolvedContextBudget"
3773
+ },
3774
+ "description": "The context gate's derived surface (GENC-1567). Every member below delegates to\n`ContextGateController` — the names are this element's public API and the\ntemplate's bindings, so they stay put while the derivation moves.\n\nThe per-agent `contextReserve` IS folded in, via the driver (GENC-1582). It\nused to be described here as a separate, later check made when an agent took\nownership of a flow — but there is no such check, and by flow entry the send\nhas already been allowed. Resolving it before the send is what makes it a gate\nrather than a label.\n\nNone carries `@volatile`: the decorators live on the controller's getters, and\nthey reach the binding observer from there — see `docs/code-health.md` §11.4.",
3775
+ "readonly": true
3939
3776
  },
3940
3777
  {
3941
3778
  "kind": "field",
3942
- "name": "confirmingCompact",
3779
+ "name": "compactionPlan",
3943
3780
  "type": {
3944
- "text": "boolean"
3781
+ "text": "CompactionProjection | null"
3945
3782
  },
3946
- "default": "false"
3783
+ "readonly": true
3947
3784
  },
3948
3785
  {
3949
3786
  "kind": "field",
3950
- "name": "_sessionMenuToggle",
3951
- "privacy": "private",
3952
- "readonly": true,
3953
- "default": "new AnimatedPanelToggle(\n this,\n '.session-menu-panel',\n SESSION_MENU_CLOSE_MS,\n () => this.sessionMenuOpen,\n (v) => {\n this.sessionMenuOpen = v;\n },\n )"
3787
+ "name": "contextGate",
3788
+ "type": {
3789
+ "text": "'ok' | 'warn' | 'blocked'"
3790
+ },
3791
+ "readonly": true
3954
3792
  },
3955
3793
  {
3956
3794
  "kind": "field",
3957
- "name": "sessionMenuEnabled",
3795
+ "name": "contextGateReason",
3958
3796
  "type": {
3959
- "text": "boolean"
3797
+ "text": "string"
3960
3798
  },
3961
- "description": "Whether the lifecycle-menu button shows. It occupies the stop-button slot,\nso it only appears when idle (mutually exclusive with Stop). Gated on\npersistence — the menu's actions (Compact, Clear, the \"remember chat\" toggle)\nonly make sense with a provider configured, so it's hidden entirely without\none. A host can additionally force it off via `chatConfig.ui.showSessionMenu:\nfalse` even when a provider is present (e.g. a guided onboarding flow where\nclearing/compacting mid-journey would be disruptive).",
3962
3799
  "readonly": true
3963
3800
  },
3964
3801
  {
3965
3802
  "kind": "field",
3966
- "name": "persistenceToggleable",
3803
+ "name": "contextGateLabel",
3967
3804
  "type": {
3968
- "text": "boolean"
3805
+ "text": "string"
3969
3806
  },
3970
- "description": "The persistence toggle row renders only when a provider is configured.",
3971
3807
  "readonly": true
3972
3808
  },
3973
3809
  {
3974
3810
  "kind": "field",
3975
- "name": "persistenceActive",
3811
+ "name": "compactHint",
3976
3812
  "type": {
3977
- "text": "boolean"
3813
+ "text": "string"
3978
3814
  },
3979
- "description": "Whether persistence is currently active (on + a provider) for this session.",
3980
3815
  "readonly": true
3981
3816
  },
3982
3817
  {
3983
3818
  "kind": "field",
3984
- "name": "contextBudget",
3819
+ "name": "compactable",
3985
3820
  "type": {
3986
- "text": "ResolvedContextBudget"
3821
+ "text": "boolean"
3987
3822
  },
3988
- "description": "Context-headroom thresholds for the active model (GENC-1567) — the warning\nand block lines, plus the tail budget compaction aims at.\n\nThe per-agent `contextReserve` IS folded in, via the driver (GENC-1582). It\nused to be described here as a separate, later check made when an agent took\nownership of a flow — but there is no such check, and by flow entry the send\nhas already been allowed. Resolving it here, before the send, is what makes it\na gate rather than a label.\n\nOverhead is read off `messages` (the Redux mirror) rather than the driver's\nown history, which the compaction plan uses. That is safe rather than sloppy:\n`estimateSystemOverhead` only looks at messages BEFORE the first usage-bearing\none, and the two lists agree completely that early — the drift between mirror\nand driver history is only ever at the tail.",
3989
3823
  "readonly": true
3990
3824
  },
3991
3825
  {
3992
3826
  "kind": "field",
3993
- "name": "compactionPlan",
3827
+ "name": "composerNoticeVisible",
3994
3828
  "type": {
3995
- "text": "CompactionProjection | null"
3829
+ "text": "boolean"
3996
3830
  },
3997
- "description": "What compacting right now would reclaim, or `null` when it would not be\nworth it (GENC-1567). Sized against FoundationAiAssistant.contextBudget,\nso the plan aims at this model's window rather than a fixed default.\n\nReads `messages` to stay reactive as the conversation grows: the driver's\nhistory is not a tracked observable, so every consumer of this keys its\nre-evaluation off the Redux mirror.",
3998
3831
  "readonly": true
3999
3832
  },
4000
3833
  {
4001
3834
  "kind": "field",
4002
- "name": "contextGate",
3835
+ "name": "composerNoticeText",
4003
3836
  "type": {
4004
- "text": "'ok' | 'warn' | 'blocked'"
3837
+ "text": "string"
4005
3838
  },
4006
3839
  "readonly": true
4007
3840
  },
4008
3841
  {
4009
3842
  "kind": "field",
4010
- "name": "compactable",
3843
+ "name": "composerNoticeSrOnly",
4011
3844
  "type": {
4012
3845
  "text": "boolean"
4013
3846
  },
4014
- "description": "Whether the menu's \"Compact\" action would do anything (GENC-1351 §5.7,\nGENC-1567). Delegates to the driver's plan so the gate uses the exact same\nhistory `compact()` acts on, judged against the same budget — one source of\ntruth for both whether a cut is legal and whether it reclaims enough.",
4015
3847
  "readonly": true
4016
3848
  },
4017
3849
  {
@@ -4130,7 +3962,7 @@
4130
3962
  "type": {
4131
3963
  "text": "boolean"
4132
3964
  },
4133
- "description": "Whether the settings modal AI Model section has slotted app content, context usage or budget figures.",
3965
+ "description": "The settings modal's derived surface. Every member below delegates to\n`SettingsModalController` — the names are this element's public API and the\n943-line template's bindings, so they stay put while the logic moves into the\n`settings-modal.ts` that folder never had (`docs/code-health.md` §3).\n\nNone carries `@volatile`: the decorators live on the controller's getters, and\nthey reach the binding observer from there — see `docs/code-health.md` §11.4.",
4134
3966
  "readonly": true
4135
3967
  },
4136
3968
  {
@@ -4209,26 +4041,6 @@
4209
4041
  }
4210
4042
  ]
4211
4043
  },
4212
- {
4213
- "kind": "method",
4214
- "name": "wireAppSettingsProvider",
4215
- "privacy": "private",
4216
- "return": {
4217
- "type": {
4218
- "text": "void"
4219
- }
4220
- }
4221
- },
4222
- {
4223
- "kind": "method",
4224
- "name": "unwireAppSettingsProvider",
4225
- "privacy": "private",
4226
- "return": {
4227
- "type": {
4228
- "text": "void"
4229
- }
4230
- }
4231
- },
4232
4044
  {
4233
4045
  "kind": "field",
4234
4046
  "name": "settingsCostCurrentSectionVisible",
@@ -4243,7 +4055,7 @@
4243
4055
  "type": {
4244
4056
  "text": "boolean"
4245
4057
  },
4246
- "description": "Whether the build-history section has anything in it — the built-in row list, or a host's own\nmarkup in the `settings-cost-history` slot.\n\nSlotted content is checked FIRST, ahead of `enabled`. That flag governs the *built-in* ledger:\nit gates the row writes, the reload, and the lifetime sum — and it defaults to `false`\n(`cost?.enabled === true`), while the Usage tab itself shows for `showCost`/`showTokens`, which\ndefault true. So a host can legitimately render its own build history into the slot and never\nopt into our `localStorage` ledger. Short-circuiting on `enabled` above the slot check hid that\nhost's markup outright, since the slot is the only thing in the container.",
4058
+ "description": "Whether the build-history section has anything in it — the built-in row list, or a host's own\nmarkup in the `settings-cost-history` slot. See\n`SettingsModalController.costHistorySectionVisible` for why slotted content is\nchecked ahead of `enabled`.",
4247
4059
  "readonly": true
4248
4060
  },
4249
4061
  {
@@ -4252,7 +4064,7 @@
4252
4064
  "type": {
4253
4065
  "text": "boolean"
4254
4066
  },
4255
- "description": "Whether the \"Build history\" heading has anything under it.\n\nBroader than FoundationAiAssistant.settingsCostHistorySectionVisible because the\nheading also covers the lifetime total, which renders before the first row exists (rows are\nwritten as spend accrues, the lifetime section only needs history switched on). Gating the\nheading on the rows alone left that total sitting under nothing.",
4067
+ "description": "Whether the \"Build history\" heading has anything under it — broader than\nFoundationAiAssistant.settingsCostHistorySectionVisible, because the\nheading also covers the lifetime total, which renders before the first row exists.",
4256
4068
  "readonly": true
4257
4069
  },
4258
4070
  {
@@ -4270,7 +4082,7 @@
4270
4082
  "type": {
4271
4083
  "text": "boolean"
4272
4084
  },
4273
- "description": "Whether the lifetime-usage section has anything to say.\n\nRequires build history: with it off there are no rows, so `cumulativeUsage` falls back to\nthe live session and the section would restate the block directly below it under a\n\"lifetime\" heading it hasn't earned.",
4085
+ "description": "Whether the lifetime-usage section has anything to say. Requires build history\nAND a resolvable state key — see `SettingsModalController.costLifetimeVisible`.",
4274
4086
  "readonly": true
4275
4087
  },
4276
4088
  {
@@ -4362,45 +4174,6 @@
4362
4174
  ],
4363
4175
  "description": "Sync the active settings modal tab when the design-system tab bar changes."
4364
4176
  },
4365
- {
4366
- "kind": "method",
4367
- "name": "resetSettingsModalTabsOnOpen",
4368
- "privacy": "private",
4369
- "return": {
4370
- "type": {
4371
- "text": "void"
4372
- }
4373
- },
4374
- "description": "Ensure the settings tab is selected when the modal opens."
4375
- },
4376
- {
4377
- "kind": "method",
4378
- "name": "resetSettingsModalTabsOnClose",
4379
- "privacy": "private",
4380
- "return": {
4381
- "type": {
4382
- "text": "void"
4383
- }
4384
- },
4385
- "description": "Return to the settings tab when the modal closes on Cost."
4386
- },
4387
- {
4388
- "kind": "method",
4389
- "name": "snapSettingsModalTabIndicator",
4390
- "privacy": "private",
4391
- "return": {
4392
- "type": {
4393
- "text": "void"
4394
- }
4395
- },
4396
- "parameters": [
4397
- {
4398
- "name": "retry",
4399
- "default": "true"
4400
- }
4401
- ],
4402
- "description": "Snap the tab underline to the settings tab without animating from Cost.\nUsed after hide/show when aria state is correct but the indicator drifted."
4403
- },
4404
4177
  {
4405
4178
  "kind": "field",
4406
4179
  "name": "pinnedAgentHint",
@@ -4513,7 +4286,7 @@
4513
4286
  "text": "DiagnosticEntry"
4514
4287
  }
4515
4288
  },
4516
- "description": "The single `meta-snapshot` entry for right now — the export-time `meta` block\n(agent summary, active prompt/state, context + cost) plus the `dedupSignature`\nthe forward-capture delta keys on.\n\nSeparate from `buildDiagnosticEntries` because the download path needs a\nFRESH one on its own: the persisted stream only re-appends this block when the\nnear-static config changes, so the newest stored snapshot's volatile half — the\n`context` figures especially — is typically frozen at the session's first flush."
4289
+ "description": "The `meta-snapshot` entry for right now. The assembly is a pure transformation\nin `buildMetaSnapshot`; this gathers what it reports.\n\nThe live agent comes from the DRIVER, not `this.activeAgent` (the redux-stripped\nprojection, no functions) and not a lookup in `this.agents` (which may mint\nfresh, never-activated configs) — only the driver holds the instance whose\n`onActivate` ran, so only its `getDebugSnapshot` closure holds state."
4517
4290
  },
4518
4291
  {
4519
4292
  "kind": "method",
@@ -4735,101 +4508,6 @@
4735
4508
  }
4736
4509
  ]
4737
4510
  },
4738
- {
4739
- "kind": "method",
4740
- "name": "isAcceptedFile",
4741
- "privacy": "private",
4742
- "return": {
4743
- "type": {
4744
- "text": "boolean"
4745
- }
4746
- },
4747
- "parameters": [
4748
- {
4749
- "name": "file",
4750
- "type": {
4751
- "text": "File"
4752
- }
4753
- }
4754
- ]
4755
- },
4756
- {
4757
- "kind": "method",
4758
- "name": "readFileAsText",
4759
- "privacy": "private",
4760
- "return": {
4761
- "type": {
4762
- "text": "Promise<string>"
4763
- }
4764
- },
4765
- "parameters": [
4766
- {
4767
- "name": "file",
4768
- "type": {
4769
- "text": "File"
4770
- }
4771
- }
4772
- ]
4773
- },
4774
- {
4775
- "kind": "method",
4776
- "name": "readFileAsBase64",
4777
- "privacy": "private",
4778
- "return": {
4779
- "type": {
4780
- "text": "Promise<string>"
4781
- }
4782
- },
4783
- "parameters": [
4784
- {
4785
- "name": "file",
4786
- "type": {
4787
- "text": "File"
4788
- }
4789
- }
4790
- ],
4791
- "description": "The base64 payload of a file, without any `data:` prefix — the raw form\n`ChatImageAttachment.data` specifies (transports add their own framing)."
4792
- },
4793
- {
4794
- "kind": "method",
4795
- "name": "processFiles",
4796
- "privacy": "private",
4797
- "return": {
4798
- "type": {
4799
- "text": "Promise<{ attachments: ChatAttachment[]; errors: string[] }>"
4800
- }
4801
- },
4802
- "parameters": [
4803
- {
4804
- "name": "files",
4805
- "type": {
4806
- "text": "File[]"
4807
- }
4808
- },
4809
- {
4810
- "name": "existingBytes",
4811
- "default": "0"
4812
- }
4813
- ]
4814
- },
4815
- {
4816
- "kind": "method",
4817
- "name": "loadSelectedFiles",
4818
- "privacy": "private",
4819
- "return": {
4820
- "type": {
4821
- "text": "Promise<void>"
4822
- }
4823
- },
4824
- "parameters": [
4825
- {
4826
- "name": "e",
4827
- "type": {
4828
- "text": "Event"
4829
- }
4830
- }
4831
- ]
4832
- },
4833
4511
  {
4834
4512
  "kind": "method",
4835
4513
  "name": "handleSendClick"
@@ -4949,7 +4627,7 @@
4949
4627
  "type": {
4950
4628
  "text": "boolean"
4951
4629
  },
4952
- "description": "Whether the message input shows a drag handle to resize its height.\nOn by default; hosts opt out via `chatConfig.ui.resizableInput: false`.",
4630
+ "description": "Whether the message input shows a drag handle to resize its height.",
4953
4631
  "readonly": true
4954
4632
  },
4955
4633
  {
@@ -4958,7 +4636,7 @@
4958
4636
  "type": {
4959
4637
  "text": "string"
4960
4638
  },
4961
- "description": "Inline style for the chat input: writes the chosen height into the\n`--composer-height` custom property (read by `.chat-input` + its control in\n`main.styles.ts`). Empty until the user drags (or when resizing is disabled),\nso the default applies.",
4639
+ "description": "Inline style carrying the dragged height into `--composer-height`.",
4962
4640
  "readonly": true
4963
4641
  },
4964
4642
  {
@@ -4976,8 +4654,7 @@
4976
4654
  "text": "PointerEvent"
4977
4655
  }
4978
4656
  }
4979
- ],
4980
- "description": "Begin a composer resize drag. Captures the pointer so move/up keep firing\non the handle even when the cursor leaves its 7px band."
4657
+ ]
4981
4658
  },
4982
4659
  {
4983
4660
  "kind": "method",
@@ -4994,8 +4671,7 @@
4994
4671
  "text": "PointerEvent"
4995
4672
  }
4996
4673
  }
4997
- ],
4998
- "description": "Resize while dragging. Dragging up (clientY decreases) grows the composer;\nthe result is clamped between the natural height (lower) and the available\nheadroom/max (upper). No-op unless a drag is active (the handle holds pointer\ncapture), so plain hover-moves are ignored."
4674
+ ]
4999
4675
  },
5000
4676
  {
5001
4677
  "kind": "method",
@@ -5012,8 +4688,7 @@
5012
4688
  "text": "PointerEvent"
5013
4689
  }
5014
4690
  }
5015
- ],
5016
- "description": "End a resize drag and release the pointer capture."
4691
+ ]
5017
4692
  }
5018
4693
  ],
5019
4694
  "events": [
@@ -5127,6 +4802,22 @@
5127
4802
  "module": "src/main/main.ts"
5128
4803
  }
5129
4804
  },
4805
+ {
4806
+ "kind": "js",
4807
+ "name": "budgetPercentOf",
4808
+ "declaration": {
4809
+ "name": "budgetPercentOf",
4810
+ "module": "./budget-controller"
4811
+ }
4812
+ },
4813
+ {
4814
+ "kind": "js",
4815
+ "name": "formatBlockedReason",
4816
+ "declaration": {
4817
+ "name": "formatBlockedReason",
4818
+ "module": "./budget-controller"
4819
+ }
4820
+ },
5130
4821
  {
5131
4822
  "kind": "js",
5132
4823
  "name": "FoundationAiAssistant",
@@ -5223,45 +4914,9 @@
5223
4914
  },
5224
4915
  {
5225
4916
  "kind": "javascript-module",
5226
- "path": "src/provider/ai-provider-switcher.ts",
5227
- "declarations": [
5228
- {
5229
- "kind": "variable",
5230
- "name": "AIProviderSwitcher",
5231
- "description": "The DI token for the (AIProviderSwitcher:interface). Resolves a noop until the app\nregisters one."
5232
- }
5233
- ],
5234
- "exports": [
5235
- {
5236
- "kind": "js",
5237
- "name": "AIProviderSwitcher",
5238
- "declaration": {
5239
- "name": "AIProviderSwitcher",
5240
- "module": "src/provider/ai-provider-switcher.ts"
5241
- }
5242
- }
5243
- ]
5244
- },
5245
- {
5246
- "kind": "javascript-module",
5247
- "path": "src/provider/assistant-app-settings.ts",
5248
- "declarations": [
5249
- {
5250
- "kind": "variable",
5251
- "name": "AssistantAppSettingsProvider",
5252
- "description": "The DI token for the (AssistantAppSettingsProvider:interface). Resolves a noop\nuntil the host registers one."
5253
- }
5254
- ],
5255
- "exports": [
5256
- {
5257
- "kind": "js",
5258
- "name": "AssistantAppSettingsProvider",
5259
- "declaration": {
5260
- "name": "AssistantAppSettingsProvider",
5261
- "module": "src/provider/assistant-app-settings.ts"
5262
- }
5263
- }
5264
- ]
4917
+ "path": "src/main/meta-snapshot.ts",
4918
+ "declarations": [],
4919
+ "exports": []
5265
4920
  },
5266
4921
  {
5267
4922
  "kind": "javascript-module",
@@ -12023,6 +11678,46 @@
12023
11678
  "path": "src/components/settings-modal/index.ts",
12024
11679
  "declarations": [],
12025
11680
  "exports": [
11681
+ {
11682
+ "kind": "js",
11683
+ "name": "SettingsModalController",
11684
+ "declaration": {
11685
+ "name": "SettingsModalController",
11686
+ "module": "./settings-modal"
11687
+ }
11688
+ },
11689
+ {
11690
+ "kind": "js",
11691
+ "name": "type",
11692
+ "declaration": {
11693
+ "name": "type",
11694
+ "module": "./settings-modal"
11695
+ }
11696
+ },
11697
+ {
11698
+ "kind": "js",
11699
+ "name": "SettingsModalDeps",
11700
+ "declaration": {
11701
+ "name": "SettingsModalDeps",
11702
+ "module": "./settings-modal"
11703
+ }
11704
+ },
11705
+ {
11706
+ "kind": "js",
11707
+ "name": "type",
11708
+ "declaration": {
11709
+ "name": "type",
11710
+ "module": "./settings-modal"
11711
+ }
11712
+ },
11713
+ {
11714
+ "kind": "js",
11715
+ "name": "SettingsModalTabsElement",
11716
+ "declaration": {
11717
+ "name": "SettingsModalTabsElement",
11718
+ "module": "./settings-modal"
11719
+ }
11720
+ },
12026
11721
  {
12027
11722
  "kind": "js",
12028
11723
  "name": "SettingsModalTemplate",
@@ -12113,6 +11808,12 @@
12113
11808
  }
12114
11809
  ]
12115
11810
  },
11811
+ {
11812
+ "kind": "javascript-module",
11813
+ "path": "src/components/settings-modal/settings-modal.ts",
11814
+ "declarations": [],
11815
+ "exports": []
11816
+ },
12116
11817
  {
12117
11818
  "kind": "javascript-module",
12118
11819
  "path": "src/state/persistence/build-timeline-entries.ts",