@genesislcap/ai-assistant 15.6.2 → 15.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/dist/ai-assistant.api.json +422 -6
  2. package/dist/ai-assistant.d.ts +654 -19
  3. package/dist/chat-driver.cjs +285 -26
  4. package/dist/chat-driver.cjs.map +3 -3
  5. package/dist/chat-driver.mjs +285 -26
  6. package/dist/chat-driver.mjs.map +3 -3
  7. package/dist/custom-elements.json +289 -32
  8. package/dist/dts/channel/ai-activity-channel.d.ts +51 -1
  9. package/dist/dts/channel/ai-activity-channel.d.ts.map +1 -1
  10. package/dist/dts/components/chat-driver/chat-driver.d.ts +99 -1
  11. package/dist/dts/components/chat-driver/chat-driver.d.ts.map +1 -1
  12. package/dist/dts/components/chat-driver/chat-driver.test.d.ts.map +1 -1
  13. package/dist/dts/components/orchestrating-driver/orchestrating-driver.budget.test.d.ts +2 -0
  14. package/dist/dts/components/orchestrating-driver/orchestrating-driver.budget.test.d.ts.map +1 -0
  15. package/dist/dts/components/orchestrating-driver/orchestrating-driver.d.ts +14 -0
  16. package/dist/dts/components/orchestrating-driver/orchestrating-driver.d.ts.map +1 -1
  17. package/dist/dts/components/settings-modal/settings-modal.styles.d.ts.map +1 -1
  18. package/dist/dts/components/settings-modal/settings-modal.template.d.ts.map +1 -1
  19. package/dist/dts/main/blocked-state.test.d.ts +2 -0
  20. package/dist/dts/main/blocked-state.test.d.ts.map +1 -0
  21. package/dist/dts/main/main.d.ts +435 -19
  22. package/dist/dts/main/main.d.ts.map +1 -1
  23. package/dist/dts/main/main.styles.d.ts.map +1 -1
  24. package/dist/dts/main/main.styles.test.d.ts +2 -0
  25. package/dist/dts/main/main.styles.test.d.ts.map +1 -0
  26. package/dist/dts/main/main.template.d.ts +53 -0
  27. package/dist/dts/main/main.template.d.ts.map +1 -1
  28. package/dist/dts/state/ai-assistant-slice.d.ts +162 -6
  29. package/dist/dts/state/ai-assistant-slice.d.ts.map +1 -1
  30. package/dist/dts/state/debug-event-log.d.ts +6 -1
  31. package/dist/dts/state/debug-event-log.d.ts.map +1 -1
  32. package/dist/dts/state/session-store.d.ts +11 -0
  33. package/dist/dts/state/session-store.d.ts.map +1 -1
  34. package/dist/dts/utils/banked-usage-baselines.d.ts +51 -0
  35. package/dist/dts/utils/banked-usage-baselines.d.ts.map +1 -0
  36. package/dist/esm/components/chat-driver/chat-driver.js +263 -21
  37. package/dist/esm/components/chat-driver/chat-driver.test.js +464 -1
  38. package/dist/esm/components/orchestrating-driver/orchestrating-driver.budget.test.js +312 -0
  39. package/dist/esm/components/orchestrating-driver/orchestrating-driver.js +89 -4
  40. package/dist/esm/components/settings-modal/settings-modal.styles.js +63 -18
  41. package/dist/esm/components/settings-modal/settings-modal.template.js +36 -7
  42. package/dist/esm/main/blocked-state.test.js +969 -0
  43. package/dist/esm/main/cost-session-banking.test.js +238 -8
  44. package/dist/esm/main/main.js +826 -48
  45. package/dist/esm/main/main.styles.js +72 -0
  46. package/dist/esm/main/main.styles.test.js +99 -0
  47. package/dist/esm/main/main.template.js +121 -4
  48. package/dist/esm/state/ai-assistant-slice.js +145 -7
  49. package/dist/esm/state/ai-assistant-slice.test.js +138 -1
  50. package/dist/esm/state/debug-event-log.js +7 -2
  51. package/dist/esm/state/debug-event-log.test.js +49 -1
  52. package/dist/esm/state/persistence/session-snapshot.test.js +18 -0
  53. package/dist/esm/utils/banked-usage-baselines.js +84 -0
  54. package/dist/tsconfig.tsbuildinfo +1 -1
  55. package/docs/migration-GENC-1464.md +562 -0
  56. package/docs/sub_agent.md +20 -3
  57. package/package.json +17 -17
  58. package/src/channel/ai-activity-channel.ts +56 -2
  59. package/src/components/chat-driver/chat-driver.test.ts +549 -0
  60. package/src/components/chat-driver/chat-driver.ts +324 -14
  61. package/src/components/orchestrating-driver/orchestrating-driver.budget.test.ts +438 -0
  62. package/src/components/orchestrating-driver/orchestrating-driver.ts +101 -6
  63. package/src/components/settings-modal/settings-modal.styles.ts +63 -18
  64. package/src/components/settings-modal/settings-modal.template.ts +38 -7
  65. package/src/main/blocked-state.test.ts +1316 -0
  66. package/src/main/cost-session-banking.test.ts +283 -11
  67. package/src/main/main.styles.test.ts +130 -0
  68. package/src/main/main.styles.ts +72 -0
  69. package/src/main/main.template.ts +131 -4
  70. package/src/main/main.ts +827 -42
  71. package/src/state/ai-assistant-slice.test.ts +215 -0
  72. package/src/state/ai-assistant-slice.ts +218 -8
  73. package/src/state/debug-event-log.test.ts +63 -0
  74. package/src/state/debug-event-log.ts +7 -2
  75. package/src/state/persistence/session-snapshot.test.ts +22 -0
  76. package/src/utils/banked-usage-baselines.ts +90 -0
@@ -1728,9 +1728,20 @@
1728
1728
  "text": "boolean"
1729
1729
  },
1730
1730
  "privacy": "private",
1731
- "description": "True when a new send must be refused: a turn is running (`busy`), a page-reload\nrestore is loading history that would clobber it (`restoring`), or a manual\ncompaction is rewriting history (`compacting`). Mirrors the composer's\n`?disabled` gate so a **programmatic** `send`/`submitMessage` (e.g. a host's\ncustom input while the built-in composer is hidden) can't slip past it and have\nits message wiped when the restored/compacted history lands (GENC-1351 §6).",
1731
+ "description": "True when a new send must be refused: a turn is running (`busy`), a page-reload\nrestore is loading history that would clobber it (`restoring`), a manual\ncompaction is rewriting history (`compacting`), or a backend condition has\nlocked the assistant outright (`blocked` — e.g. an exhausted AI budget).\nMirrors the composer's `?disabled` gate so a **programmatic**\n`send`/`submitMessage` (e.g. a host's custom input while the built-in composer\nis hidden) can't slip past it and have its message wiped when the\nrestored/compacted history lands (GENC-1351 §6).",
1732
1732
  "readonly": true
1733
1733
  },
1734
+ {
1735
+ "kind": "method",
1736
+ "name": "sendRefusalReason",
1737
+ "privacy": "private",
1738
+ "return": {
1739
+ "type": {
1740
+ "text": "string"
1741
+ }
1742
+ },
1743
+ "description": "Why a programmatic send was refused, for `submitMessage`'s `errors`. A\nbackend block is called out distinctly because — unlike the transient\n\"busy\" cases — waiting and retrying will never clear it, and a caller\nlooping on \"Assistant is busy\" would spin forever."
1744
+ },
1734
1745
  {
1735
1746
  "kind": "method",
1736
1747
  "name": "applyDeferredAgentsSwap",
@@ -1845,6 +1856,143 @@
1845
1856
  },
1846
1857
  "description": "Whether a page-reload restore is in flight (GENC-1351 §6). On the session\nstore (like `compacting`) so the \"Restoring…\" view + input-disable stay in\nsync across pop-in/out. Gating input while true prevents a mid-load send from\nbeing clobbered when the restored history loads."
1847
1858
  },
1859
+ {
1860
+ "kind": "field",
1861
+ "name": "blocked",
1862
+ "type": {
1863
+ "text": "boolean"
1864
+ },
1865
+ "description": "Whether the assistant is blocked by a backend condition and cannot send —\ntoday, an exhausted AI-spend budget (GENC-1464). While true the composer is\ndisabled, `send`/`submitMessage` refuse, suggestions stop being fetched, and\na persistent banner (`part=\"blocked-banner\"`) sits above the composer\nexplaining why. The transcript stays visible and scrollable throughout —\nunlike `compacting` and `restoring`, this does not replace the conversation.\n\n**Latched.** Nothing in the element clears it: not a new turn, not \"Clear\"\n/ \"New chat\", not a pop-in/out. It stays set until the host writes\n`false` — because nothing the user can do inside the assistant refills a\nbudget. Hosts typically set it from their own pre-flight budget check on\nmount, and clear it once a later check shows headroom again.\n\nThe driver also latches it automatically when a turn ends with the\n`'budget-exhausted'` failure reason — off the `tool-loop-end` activity bus\n(which also covers a sub-agent wall, a turn this element did not start, and\nan element swapped in mid-turn) and off the driver's return value (which\ncovers a wall hit during multi-agent classification, where no tool loop ran\nand so no bus event fires). So a host that does no pre-flight at all still\ngets a correct locked UI the moment the first 402 lands. The bus topic is\ntab-scoped, so a wall hit in a popped-out window latches the main window\ntoo — correct for a shared spend cap.\n\n**Lifetime — read this before relying on it as your source of truth.** The\nlatch is in-memory and per-`stateKey`:\n\n- It does **not** survive `switchSession`. Switching away tears the outgoing\n session's store down entirely, so switching back yields a fresh, unblocked\n store.\n- It does **not** survive a page reload. It is deliberately absent from the\n persisted session snapshot: the snapshot is long-lived, so a persisted\n latch would outlive an out-of-band budget raise with no in-element way to\n clear it — a stale lock is worse than re-deriving the state.\n\nThe durable source of truth is therefore the **host's pre-flight**, which\nshould run on mount and on every session switch. See\n`docs/migration-GENC-1464.md` §\"How to adopt\", Option B."
1866
+ },
1867
+ {
1868
+ "kind": "field",
1869
+ "name": "reachableVendors",
1870
+ "type": {
1871
+ "text": "readonly AIProviderType[]"
1872
+ },
1873
+ "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.",
1874
+ "readonly": true
1875
+ },
1876
+ {
1877
+ "kind": "field",
1878
+ "name": "blockedVendors",
1879
+ "type": {
1880
+ "text": "readonly AIProviderType[]"
1881
+ },
1882
+ "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.",
1883
+ "readonly": true
1884
+ },
1885
+ {
1886
+ "kind": "method",
1887
+ "name": "isVendorBlocked",
1888
+ "return": {
1889
+ "type": {
1890
+ "text": "boolean"
1891
+ }
1892
+ },
1893
+ "parameters": [
1894
+ {
1895
+ "name": "vendor",
1896
+ "type": {
1897
+ "text": "AIProviderType"
1898
+ }
1899
+ }
1900
+ ],
1901
+ "description": "Whether this specific vendor's budget is walled — regardless of whether any\nother vendor still has headroom."
1902
+ },
1903
+ {
1904
+ "kind": "method",
1905
+ "name": "setVendorBlocked",
1906
+ "return": {
1907
+ "type": {
1908
+ "text": "void"
1909
+ }
1910
+ },
1911
+ "parameters": [
1912
+ {
1913
+ "name": "vendor",
1914
+ "type": {
1915
+ "text": "AIProviderType"
1916
+ }
1917
+ },
1918
+ {
1919
+ "name": "blocked",
1920
+ "type": {
1921
+ "text": "boolean"
1922
+ }
1923
+ },
1924
+ {
1925
+ "name": "reason",
1926
+ "optional": true,
1927
+ "type": {
1928
+ "text": "string | null"
1929
+ }
1930
+ }
1931
+ ],
1932
+ "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."
1933
+ },
1934
+ {
1935
+ "kind": "method",
1936
+ "name": "isVendorSwept",
1937
+ "privacy": "private",
1938
+ "return": {
1939
+ "type": {
1940
+ "text": "boolean"
1941
+ }
1942
+ },
1943
+ "parameters": [
1944
+ {
1945
+ "name": "vendor",
1946
+ "type": {
1947
+ "text": "AIProviderType"
1948
+ }
1949
+ }
1950
+ ],
1951
+ "description": "Whether this vendor's wall came from the sweep alone — see the slice's `sweptVendors`."
1952
+ },
1953
+ {
1954
+ "kind": "field",
1955
+ "name": "blockedReason",
1956
+ "type": {
1957
+ "text": "string | null"
1958
+ },
1959
+ "description": "Explanation shown in the blocked banner, or `null` for the element's default\ncopy. Only meaningful while FoundationAiAssistant.blocked is true;\nsetting `blocked = false` clears it.\n\nWritable, and symmetric with every other store-backed accessor on this\nclass: writing it re-latches with the CURRENT `blocked` value, so it changes\nthe copy without disturbing the flag. Assigning `null` clears the\nexplanation while staying blocked (the banner falls back to the default\ncopy). FoundationAiAssistant.setBlocked remains the way to write\nboth in one atomic action."
1960
+ },
1961
+ {
1962
+ "kind": "method",
1963
+ "name": "setBlocked",
1964
+ "return": {
1965
+ "type": {
1966
+ "text": "void"
1967
+ }
1968
+ },
1969
+ "parameters": [
1970
+ {
1971
+ "name": "blocked",
1972
+ "type": {
1973
+ "text": "boolean"
1974
+ }
1975
+ },
1976
+ {
1977
+ "name": "reason",
1978
+ "optional": true,
1979
+ "type": {
1980
+ "text": "string | null"
1981
+ }
1982
+ }
1983
+ ],
1984
+ "description": "Block or unblock the assistant, optionally with custom banner copy.\nEquivalent to the FoundationAiAssistant.blocked setter, plus the\nreason in one atomic write.\n\nBlocking writes the **vendor-agnostic** block — what a host with one budget\npot means — and does not populate\nFoundationAiAssistant.blockedVendors. Unblocking clears the\nper-vendor walls as well, so a host asserting \"the wall is gone\" after its\nown pre-flight cannot be silently overruled by a driver latch it never saw."
1985
+ },
1986
+ {
1987
+ "kind": "field",
1988
+ "name": "transcriptBudgetExhaustedMessage",
1989
+ "type": {
1990
+ "text": "string"
1991
+ },
1992
+ "privacy": "private",
1993
+ "description": "Copy the driver writes into the TRANSCRIPT when a turn hits the budget wall.\n\nA host-set `blockedReason` still wins, so a white-labelled host does not read\nits own explanation in the banner and the shipped default directly below it —\nthat is the whole reason the driver takes this at all.\n\nWhat it deliberately is NOT is\nFoundationAiAssistant.effectiveBlockedReason. That getter\ncomposes advice — `\"…Switch to Gemini to keep going.\"` — which is true only\nwhile that vendor has headroom, and the transcript is a permanent record. The\ndriver reads this once at CONSTRUCTION, and `getOrCreateDriver` keys on the\nagents list, so an agents swap *after* a wall re-runs `createDriver` and would\nfreeze that sentence into every later turn's bubble. Reading the latch's own\ncopy instead is structurally incapable of carrying the advice: the only\nwriter of `blockedReason` is the vendor-agnostic branch of\n`latchBlockedFrom`, whose `formatBlockedReason(budget)` form has no switch\nclause. Per-vendor copy lives in `blockedVendorReasons` and reaches the\nbanner alone.",
1994
+ "readonly": true
1995
+ },
1848
1996
  {
1849
1997
  "kind": "field",
1850
1998
  "name": "pinnedAgentName",
@@ -2031,18 +2179,32 @@
2031
2179
  }
2032
2180
  }
2033
2181
  ],
2034
- "description": "Spend for `projectKey` that predates the current transcript, resolved once per project\nand memoised (`_bankedFor`) so it cannot drift mid-session as the row it came from is\nrewritten 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."
2182
+ "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."
2035
2183
  },
2036
2184
  {
2037
2185
  "kind": "method",
2038
- "name": "resetBankedUsage",
2186
+ "name": "rebaselineOnClearedTranscript",
2039
2187
  "privacy": "private",
2040
2188
  "return": {
2041
2189
  "type": {
2042
2190
  "text": "void"
2043
2191
  }
2044
2192
  },
2045
- "description": "Discard the banked figure — for when the spend it stands for has been deliberately\nthrown away (the user clearing history, or deleting this project's row).\n\nNOT for reloads. A reload re-reads rows that already include our own upserts, so\nre-deriving from them would fold the live session into its own baseline; only a genuine\nproject change should re-derive, which `bankedUsageFor`'s per-project memo handles."
2193
+ "parameters": [
2194
+ {
2195
+ "name": "key",
2196
+ "type": {
2197
+ "text": "string"
2198
+ }
2199
+ },
2200
+ {
2201
+ "name": "discarded",
2202
+ "type": {
2203
+ "text": "AggregateUsage"
2204
+ }
2205
+ }
2206
+ ],
2207
+ "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."
2046
2208
  },
2047
2209
  {
2048
2210
  "kind": "method",
@@ -2375,6 +2537,15 @@
2375
2537
  },
2376
2538
  "privacy": "private"
2377
2539
  },
2540
+ {
2541
+ "kind": "field",
2542
+ "name": "_unsubBudgetLatch",
2543
+ "type": {
2544
+ "text": "() => void | undefined"
2545
+ },
2546
+ "privacy": "private",
2547
+ "description": "Unsubscribe handle for the `tool-loop-end` budget latch (GENC-1464)."
2548
+ },
2378
2549
  {
2379
2550
  "kind": "field",
2380
2551
  "name": "unsubProviderRegistry",
@@ -2412,24 +2583,6 @@
2412
2583
  "privacy": "private",
2413
2584
  "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`."
2414
2585
  },
2415
- {
2416
- "kind": "field",
2417
- "name": "_bankedUsage",
2418
- "type": {
2419
- "text": "AggregateUsage"
2420
- },
2421
- "privacy": "private",
2422
- "description": "Memoised pre-transcript spend for `_bankedFor` — see `bankedUsageFor`."
2423
- },
2424
- {
2425
- "kind": "field",
2426
- "name": "_bankedFor",
2427
- "type": {
2428
- "text": "string | undefined"
2429
- },
2430
- "privacy": "private",
2431
- "description": "Project key `_bankedUsage` was resolved for; `undefined` means \"not yet resolved\"."
2432
- },
2433
2586
  {
2434
2587
  "kind": "field",
2435
2588
  "name": "_costHistoryLoaded",
@@ -3411,6 +3564,16 @@
3411
3564
  "type": {
3412
3565
  "text": "boolean"
3413
3566
  },
3567
+ "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.",
3568
+ "readonly": true
3569
+ },
3570
+ {
3571
+ "kind": "field",
3572
+ "name": "settingsCostHistoryHeadingVisible",
3573
+ "type": {
3574
+ "text": "boolean"
3575
+ },
3576
+ "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.",
3414
3577
  "readonly": true
3415
3578
  },
3416
3579
  {
@@ -4475,8 +4638,8 @@
4475
4638
  "type": {
4476
4639
  "text": "readonly string[]"
4477
4640
  },
4478
- "default": "[\n 'This is an exported debug log for the Genesis AI assistant. Read it top-to-bottom.',\n '`timeline` is the entire session as one array, already sorted chronologically by `timestamp` (ISO 8601). Every entry has a `kind`.',\n 'Timestamps are millisecond-resolution; entries that share the same millisecond are ordered by a fixed kind rank (event, then turn, then message), which is a heuristic and may not reflect exact causal order within that millisecond — e.g. a user message and the turn it triggered, or a final assistant message and its turn.end event, can appear in either order depending on whether they landed in the same millisecond. Read the logical structure of a turn rather than over-interpreting the micro-ordering of co-timestamped entries of different kinds.',\n \"kind:'message' — the conversation. `role` is user/assistant/tool/system-event/synthetic-user; `agentName` says which agent produced it; `toolCalls`/`toolResult`/`interaction` carry tool and widget activity; `inputTokens`/`outputTokens`/`cost` are per-message LLM usage, where `inputTokens` is the WHOLE prompt for that request and `cacheReadTokens`/`cacheWriteTokens` BREAK IT DOWN rather than add to it — uncached input is `inputTokens` minus those two, and adding the cache fields to `inputTokens` counts the prompt twice. The cache fields are absent on providers that report no cache split (Gemini reports reads only, since implicit caching bills no write) and on messages persisted before they existed, so read them as 0 when missing. Each bucket bills at a different rate — cache reads a fraction of uncached input, cache writes a premium, output highest — so a large token count at a small cost means the prompt was mostly cache hits. `externalCostUsd` is any non-LLM cost a widget reported for its own external service calls (folded into the session cost total alongside `cost`). On model-produced assistant messages, `model` is the concrete model id that generated it (e.g. 'gemini-2.5-flash-lite') and `providerName` is the registry slot it resolved under (e.g. a tier name like 'high'/'low', or the default); together they attribute the message — and any tool calls it carries — to an exact model even across a mid-session vendor/tier switch, where one slot name can map to different models before and after the switch. Both are undefined on any entry that is NOT an LLM response: non-assistant roles (user/tool/system-event) and 'synthetic-user' echoes; assistant interaction/widget entries (empty content carrying an `interaction` — a rendered widget, not a model turn); driver-authored assistant fallbacks (the timeout, repeated-malformed-call, and empty-response apology messages); and messages restored from a session persisted before these fields existed. One partial case: on a genuine model turn whose provider exposes no `getStatus` (or reports no model), `providerName` is still set but `model` alone is undefined. A 'synthetic-user' message is a display-only echo of an interaction outcome (e.g. the answer a widget reported): it renders on the user's side of the chat and `agentName` is the agent that created it, but it is never sent to the LLM — so it has no matching 'turn' and the model learns the outcome only from the corresponding tool result.\",\n \"Sub-agent messages appear inline. When a tool delegates to a sub-agent (via `requestSubAgent`), the sub-agent's whole conversation — its own assistant/tool messages, each with their own `content`/`thinking`/`toolCalls`/`toolResult` and per-message `model`/`providerName`/`inputTokens`/`outputTokens`/`cacheReadTokens`/`cacheWriteTokens`/`cost` — is hoisted into the timeline as ordinary kind:'message' entries, interleaved by timestamp right after the tool call that spawned them (so you read the delegation top-to-bottom). A hoisted entry is marked: `subAgentDepth` is its delegation depth (1 for a sub-agent, 2 for a sub-agent's sub-agent, …), `subAgentOf` is the id of the parent tool call that spawned it (correlates it back even when two sub-agents run in one parent turn), `subAgentName` is the sub-agent's own name, and `agentName` is rewritten to a `\\\"<parent> › <sub-agent>\\\"` breadcrumb (composing when nested, e.g. `\\\"UI Builder › Planner › Grounding\\\"`). The sub-agent's per-LLM-call snapshots also surface as kind:'turn' entries with an N-M `turnIndex`, and subagent.started/completed (or subagent.failed) events bracket the run. Per-message `cost` on hoisted entries is already part of the session total (it is summed from the un-flattened history), so summing the top-level timeline does NOT double-count.\",\n \"kind:'turn' — one LLM call. `turnIndex` is a string: a top-level turn is the bare counter ('0', '1', …); a sub-agent's turns are numbered under the parent turn that activated them ('3-1', '3-2', …, and a nested sub-agent contributes '3-2-1', …), and `agentName` names the agent that ran the turn. `systemPrompt` and `toolNames` are what the model saw. A systemPrompt of '<repeated — identical to turn N>' was byte-identical to turn N and de-duplicated; the full prompt is shown whenever it changes (often because a stateful agent advanced), so prompt evolution is visible.\",\n \"kind:'turn'.`agentSnapshot` — the active agent's own view of its internal state, captured at that turn. An agent opts into this by exposing a `getDebugSnapshot()` that returns JSON-serializable per-state info; stateful/flow agents wire it automatically, so you can watch a flow advance turn-by-turn (e.g. current step, cursor, collected fields, pending changes). Absent for agents that don't expose one.\",\n \"kind:'event' — a meta/lifecycle event. `type` names it (see below); `detail` carries structured data. `detail.placement` is the emitting UI instance: 'bubble' (collapsed), 'panel' (popped-out), or 'standalone'.\",\n \"Each 'event' also has an `importance`: 'high' (failures/limits — turn.error, tool.failed, subagent.failed, file.read-failed, suggestions.failed, context.threshold-crossed), 'normal' (session flow — connects, turns, retries, handoffs, agent/provider changes, interactions, sub-agent start/complete), or 'low' (skippable UI/bookkeeping noise — panel.toggled, attachment.added, driver.wired/unwired, context.updated, context.condensed). To skim, ignore importance:'low'; to triage a failure, filter to importance:'high' then read the nearby messages and turns. A 'high' turn.error is often preceded by one or more 'normal' turn.retry events for the same reason — read them together to see how many attempts were made before bailing. 'message' and 'turn' entries carry no importance — they are the substance, always read them.\",\n 'Event types: assistant.connected/disconnected (mount + placement + whether the session was created or restored), assistant.popout/popin (window placement), driver.created/wired/unwired (which driver is live and why it stops/starts responding across a popout), state.changed (idle↔loading), turn.start/turn.end (turn boundary; turn.end carries durationMs), turn.retry (a recoverable in-turn retry — detail.reason plus attempt/maxAttempts; for malformed calls also finishMessage; for empty responses also the provider finishReason + thoughtsTokens + parts breakdown), turn.error (a turn failed or hit a guardrail — detail.reason is one of exception/malformed-function-call/empty-response/unknown-tool-limit/max-iterations/response-truncated, plus reason-specific diagnostics: attempts (for empty-response also finishReason + thoughtsTokens + a parts breakdown, distinguishing a thinking-only STOP from a truly empty turn), finishMessage, for response-truncated the model + maxTokens + outputTokens + tools, unknownTools (split into staleTools — real earlier this activation but retired by the current state or hidden behind an open exclusive fold — and hallucinatedTools — never advertised) + availableTools, iterations + limit, or name + message for exceptions), tool.failed (a tool threw), tool.unresolved (the model called a tool that could not be dispatched — detail.kind is folded/fold-hidden/stale/unknown, plus tool + agent and, for the counted kinds, the consecutive streak; the recurring lead-up to an unknown-tool-limit turn.error), subagent.started/completed/failed (the lifecycle of a `requestSubAgent` delegation — detail.agent names the sub-agent; these bracket the sub-agent turns that appear as kind:turn entries with an N-M `turnIndex`; subagent.failed also carries detail.reason, one of max_iterations/malformed_tool_call/empty_response/unknown_tool_limit/timeout/response_truncated), agent.handoff (routing; from=null is the initial activation), agent.pinned/unpinned (forced routing), provider.selected (model/provider for the upcoming turns), interaction.requested/resolved (blocking user widgets — explain quiet gaps; note that when a sub-agent opens a widget, detail.agent — and the agentName on the interaction message — is the HOST agent that owns the widget, NOT the sub-agent that asked, because widgets render and resolve on the host driver), context.updated/threshold-crossed (token + cost), context.condensed (a stale tool payload was collapsed out of the model-bound history by a `condenseWhen` declaration on the tool — detail.tool + toolCallId, target args|response, trigger (superseded:<key> or age:<n>), stubLen, and an estimated tokensSaved; stored history and this log keep the FULL payload, so the model-visible slice at any point is the full history minus the condensations recorded up to then), panel.toggled, attachment.added, file.read-failed, suggestions.failed.',\n 'Sub-agent meta events: a sub-agent\\'s own turn.retry/turn.error/tool.failed/tool.unresolved events are merged into this same timeline, tagged with `detail.subAgent` — a `\"<parent> › <sub-agent>\"` breadcrumb that composes when nested (e.g. `\"UI Builder › Planner › Grounding\"`) — and interleaved by their original timestamps within the subagent.started→completed/failed bracket. These are the per-attempt/per-failure signals that do NOT appear among the sub-agent\\'s (hoisted) messages: a malformed/empty attempt that gets retried produces no message, and the stale-vs-hallucinated split and streak counts live only on the event. A sub-agent\\'s high-volume, message-derivable events (turn.start/turn.end, provider.selected, context.updated) are intentionally NOT merged — read its hoisted messages for model/tokens/cost and turn-by-turn activity, and the bracketing subagent.* events for the run\\'s span.',\n \"`meta` holds context captured at export time: agentSummary (full agent configs), context (active model, token usage, session cost), activeDebugSnapshot (the active agent's `getDebugSnapshot()` taken fresh at export — reflects state NOW, which may have advanced beyond the last turn's agentSnapshot), debug (optional host-supplied debug state), host, and the export timestamp.\",\n 'To debug a failure: find the last turn.error or tool.failed, then read upward for the user message, the turn(s), and the agent/provider/state events that led into it.',\n]",
4479
- "description": "Human/agent-facing guide emitted as the first key of the exported debug log,\nso whoever opens the JSON (often an AI agent) knows how to read it without\nreverse-engineering the shape. Kept here next to the event catalogue it\ndescribes so the two stay in sync."
4641
+ "default": "[\n 'This is an exported debug log for the Genesis AI assistant. Read it top-to-bottom.',\n '`timeline` is the entire session as one array, already sorted chronologically by `timestamp` (ISO 8601). Every entry has a `kind`.',\n 'Timestamps are millisecond-resolution; entries that share the same millisecond are ordered by a fixed kind rank (event, then turn, then message), which is a heuristic and may not reflect exact causal order within that millisecond — e.g. a user message and the turn it triggered, or a final assistant message and its turn.end event, can appear in either order depending on whether they landed in the same millisecond. Read the logical structure of a turn rather than over-interpreting the micro-ordering of co-timestamped entries of different kinds.',\n \"kind:'message' — the conversation. `role` is user/assistant/tool/system-event/synthetic-user; `agentName` says which agent produced it; `toolCalls`/`toolResult`/`interaction` carry tool and widget activity; `inputTokens`/`outputTokens`/`cost` are per-message LLM usage, where `inputTokens` is the WHOLE prompt for that request and `cacheReadTokens`/`cacheWriteTokens` BREAK IT DOWN rather than add to it — uncached input is `inputTokens` minus those two, and adding the cache fields to `inputTokens` counts the prompt twice. The cache fields are absent on providers that report no cache split (Gemini reports reads only, since implicit caching bills no write) and on messages persisted before they existed, so read them as 0 when missing. Each bucket bills at a different rate — cache reads a fraction of uncached input, cache writes a premium, output highest — so a large token count at a small cost means the prompt was mostly cache hits. `externalCostUsd` is any non-LLM cost a widget reported for its own external service calls (folded into the session cost total alongside `cost`). On model-produced assistant messages, `model` is the concrete model id that generated it (e.g. 'gemini-2.5-flash-lite') and `providerName` is the registry slot it resolved under (e.g. a tier name like 'high'/'low', or the default); together they attribute the message — and any tool calls it carries — to an exact model even across a mid-session vendor/tier switch, where one slot name can map to different models before and after the switch. Both are undefined on any entry that is NOT an LLM response: non-assistant roles (user/tool/system-event) and 'synthetic-user' echoes; assistant interaction/widget entries (empty content carrying an `interaction` — a rendered widget, not a model turn); driver-authored assistant fallbacks (the timeout, repeated-malformed-call, and empty-response apology messages); and messages restored from a session persisted before these fields existed. One partial case: on a genuine model turn whose provider exposes no `getStatus` (or reports no model), `providerName` is still set but `model` alone is undefined. A 'synthetic-user' message is a display-only echo of an interaction outcome (e.g. the answer a widget reported): it renders on the user's side of the chat and `agentName` is the agent that created it, but it is never sent to the LLM — so it has no matching 'turn' and the model learns the outcome only from the corresponding tool result.\",\n \"Sub-agent messages appear inline. When a tool delegates to a sub-agent (via `requestSubAgent`), the sub-agent's whole conversation — its own assistant/tool messages, each with their own `content`/`thinking`/`toolCalls`/`toolResult` and per-message `model`/`providerName`/`inputTokens`/`outputTokens`/`cacheReadTokens`/`cacheWriteTokens`/`cost` — is hoisted into the timeline as ordinary kind:'message' entries, interleaved by timestamp right after the tool call that spawned them (so you read the delegation top-to-bottom). A hoisted entry is marked: `subAgentDepth` is its delegation depth (1 for a sub-agent, 2 for a sub-agent's sub-agent, …), `subAgentOf` is the id of the parent tool call that spawned it (correlates it back even when two sub-agents run in one parent turn), `subAgentName` is the sub-agent's own name, and `agentName` is rewritten to a `\\\"<parent> › <sub-agent>\\\"` breadcrumb (composing when nested, e.g. `\\\"UI Builder › Planner › Grounding\\\"`). The sub-agent's per-LLM-call snapshots also surface as kind:'turn' entries with an N-M `turnIndex`, and subagent.started/completed (or subagent.failed) events bracket the run. Per-message `cost` on hoisted entries is already part of the session total (it is summed from the un-flattened history), so summing the top-level timeline does NOT double-count.\",\n \"kind:'turn' — one LLM call. `turnIndex` is a string: a top-level turn is the bare counter ('0', '1', …); a sub-agent's turns are numbered under the parent turn that activated them ('3-1', '3-2', …, and a nested sub-agent contributes '3-2-1', …), and `agentName` names the agent that ran the turn. `systemPrompt` and `toolNames` are what the model saw. A systemPrompt of '<repeated — identical to turn N>' was byte-identical to turn N and de-duplicated; the full prompt is shown whenever it changes (often because a stateful agent advanced), so prompt evolution is visible.\",\n \"kind:'turn'.`agentSnapshot` — the active agent's own view of its internal state, captured at that turn. An agent opts into this by exposing a `getDebugSnapshot()` that returns JSON-serializable per-state info; stateful/flow agents wire it automatically, so you can watch a flow advance turn-by-turn (e.g. current step, cursor, collected fields, pending changes). Absent for agents that don't expose one.\",\n \"kind:'event' — a meta/lifecycle event. `type` names it (see below); `detail` carries structured data. `detail.placement` is the emitting UI instance: 'bubble' (collapsed), 'panel' (popped-out), or 'standalone'.\",\n \"Each 'event' also has an `importance`: 'high' (failures/limits — turn.error, tool.failed, subagent.failed, file.read-failed, suggestions.failed, context.threshold-crossed), 'normal' (session flow — connects, turns, retries, handoffs, agent/provider changes, interactions, sub-agent start/complete), or 'low' (skippable UI/bookkeeping noise — panel.toggled, attachment.added, driver.wired/unwired, context.updated, context.condensed). To skim, ignore importance:'low'; to triage a failure, filter to importance:'high' then read the nearby messages and turns. A 'high' turn.error is often preceded by one or more 'normal' turn.retry events for the same reason — read them together to see how many attempts were made before bailing. 'message' and 'turn' entries carry no importance — they are the substance, always read them.\",\n 'Event types: assistant.connected/disconnected (mount + placement + whether the session was created or restored), assistant.popout/popin (window placement), driver.created/wired/unwired (which driver is live and why it stops/starts responding across a popout), state.changed (idle↔loading), turn.start/turn.end (turn boundary; turn.end carries durationMs), turn.retry (a recoverable in-turn retry — detail.reason plus attempt/maxAttempts; for malformed calls also finishMessage; for empty responses also the provider finishReason + thoughtsTokens + parts breakdown), turn.error (a turn failed or hit a guardrail — detail.reason is one of exception/malformed-function-call/empty-response/unknown-tool-limit/max-iterations/response-truncated/refusal/budget-exhausted, plus reason-specific diagnostics: attempts (for empty-response also finishReason + thoughtsTokens + a parts breakdown, distinguishing a thinking-only STOP from a truly empty turn), finishMessage, for response-truncated the model + maxTokens + outputTokens + tools, unknownTools (split into staleTools — real earlier this activation but retired by the current state or hidden behind an open exclusive fold — and hallucinatedTools — never advertised) + availableTools, iterations + limit, for budget-exhausted the budgetUsd + spentUsd figures reported by the proxy plus the resolved vendor, or name + message for exceptions), tool.failed (a tool threw), tool.unresolved (the model called a tool that could not be dispatched — detail.kind is folded/fold-hidden/stale/unknown, plus tool + agent and, for the counted kinds, the consecutive streak; the recurring lead-up to an unknown-tool-limit turn.error), subagent.started/completed/failed (the lifecycle of a `requestSubAgent` delegation — detail.agent names the sub-agent; these bracket the sub-agent turns that appear as kind:turn entries with an N-M `turnIndex`; subagent.failed also carries detail.reason, one of max_iterations/malformed_tool_call/empty_response/unknown_tool_limit/timeout/response_truncated/refusal/budget_exhausted; budget_exhausted is terminal for the PARENT turn too — the parent stops rather than calling the model again into the same wall), agent.handoff (routing; from=null is the initial activation), agent.pinned/unpinned (forced routing), provider.selected (model/provider for the upcoming turns), interaction.requested/resolved (blocking user widgets — explain quiet gaps; note that when a sub-agent opens a widget, detail.agent — and the agentName on the interaction message — is the HOST agent that owns the widget, NOT the sub-agent that asked, because widgets render and resolve on the host driver), context.updated/threshold-crossed (token + cost), context.condensed (a stale tool payload was collapsed out of the model-bound history by a `condenseWhen` declaration on the tool — detail.tool + toolCallId, target args|response, trigger (superseded:<key> or age:<n>), stubLen, and an estimated tokensSaved; stored history and this log keep the FULL payload, so the model-visible slice at any point is the full history minus the condensations recorded up to then), panel.toggled, attachment.added, file.read-failed, suggestions.failed.',\n 'Sub-agent meta events: a sub-agent\\'s own turn.retry/turn.error/tool.failed/tool.unresolved events are merged into this same timeline, tagged with `detail.subAgent` — a `\"<parent> › <sub-agent>\"` breadcrumb that composes when nested (e.g. `\"UI Builder › Planner › Grounding\"`) — and interleaved by their original timestamps within the subagent.started→completed/failed bracket. These are the per-attempt/per-failure signals that do NOT appear among the sub-agent\\'s (hoisted) messages: a malformed/empty attempt that gets retried produces no message, and the stale-vs-hallucinated split and streak counts live only on the event. A sub-agent\\'s high-volume, message-derivable events (turn.start/turn.end, provider.selected, context.updated) are intentionally NOT merged — read its hoisted messages for model/tokens/cost and turn-by-turn activity, and the bracketing subagent.* events for the run\\'s span.',\n \"`meta` holds context captured at export time: agentSummary (full agent configs), context (active model, token usage, session cost), activeDebugSnapshot (the active agent's `getDebugSnapshot()` taken fresh at export — reflects state NOW, which may have advanced beyond the last turn's agentSnapshot), debug (optional host-supplied debug state), host, and the export timestamp.\",\n 'To debug a failure: find the last turn.error or tool.failed, then read upward for the user message, the turn(s), and the agent/provider/state events that led into it.',\n]",
4642
+ "description": "Human/agent-facing guide emitted as the first key of the exported debug log,\nso whoever opens the JSON (often an AI agent) knows how to read it without\nreverse-engineering the shape. Kept here next to the event catalogue it\ndescribes but proximity alone demonstrably did NOT keep the two in sync\n(`refusal` was missing from both reason lists for a whole release), so\n`debug-event-log.test.ts` now asserts that every `TurnFailureReason` and every\n`SubAgentFailureReason` is named here, and fails the build when one is added\nwithout a matching sentence. Enumerations below read as exhaustive and WILL be\ntrusted by an agent reading the JSON — keep them so."
4480
4643
  }
4481
4644
  ],
4482
4645
  "exports": [
@@ -5012,6 +5175,12 @@
5012
5175
  "declarations": [],
5013
5176
  "exports": []
5014
5177
  },
5178
+ {
5179
+ "kind": "javascript-module",
5180
+ "path": "src/utils/banked-usage-baselines.ts",
5181
+ "declarations": [],
5182
+ "exports": []
5183
+ },
5015
5184
  {
5016
5185
  "kind": "javascript-module",
5017
5186
  "path": "src/utils/collect-session-models.ts",
@@ -7254,10 +7423,10 @@
7254
7423
  "kind": "field",
7255
7424
  "name": "subAgentFailure",
7256
7425
  "type": {
7257
- "text": "{ reason: SubAgentFailureReason } | undefined"
7426
+ "text": "{ reason: SubAgentFailureReason; budget?: BudgetDetail } | undefined"
7258
7427
  },
7259
7428
  "privacy": "private",
7260
- "description": "Set when a sub-agent's tool loop ends without `completeSubAgent` being\ncalled. Read by the parent's `invokeSubAgent` to build the `{ ok: false }`\nbranch of `requestSubAgent`. Only ever set when `isSubAgent` is true."
7429
+ "description": "Set when a sub-agent's tool loop ends without `completeSubAgent` being\ncalled. Read by the parent's `invokeSubAgent` to build the `{ ok: false }`\nbranch of `requestSubAgent`. Only ever set when `isSubAgent` is true.\n\n`budget` rides along on a `'budget_exhausted'` failure so the parent inherits\nthe child's ATTRIBUTION, not just the fact of a wall — see\n`budgetWallDetail`."
7261
7430
  },
7262
7431
  {
7263
7432
  "kind": "field",
@@ -7538,6 +7707,46 @@
7538
7707
  "description": "Injected activity bus; defaults to a no-op off-browser (Node/tests/headless).",
7539
7708
  "default": "activityBus"
7540
7709
  },
7710
+ {
7711
+ "kind": "field",
7712
+ "name": "budgetExhaustedMessage",
7713
+ "type": {
7714
+ "text": "string"
7715
+ },
7716
+ "privacy": "private",
7717
+ "readonly": true,
7718
+ "description": "Transcript copy for a budget wall — see `ChatDriverConfig.budgetExhaustedMessage`.",
7719
+ "default": "budgetExhaustedMessage"
7720
+ },
7721
+ {
7722
+ "kind": "field",
7723
+ "name": "budgetExhaustedThisTurn",
7724
+ "type": {
7725
+ "text": "boolean"
7726
+ },
7727
+ "privacy": "private",
7728
+ "default": "false",
7729
+ "description": "Set the moment a budget wall is observed anywhere in this turn — this\ndriver's own 402, or a sub-agent's (which surfaces here only as a\n`'budget_exhausted'` tool result). Read at the top of the tool loop to end\nthe turn before issuing another model call that would hit the same wall.\nReset per turn alongside the other per-turn counters."
7730
+ },
7731
+ {
7732
+ "kind": "field",
7733
+ "name": "budgetWallDetail",
7734
+ "type": {
7735
+ "text": "BudgetDetail | undefined"
7736
+ },
7737
+ "privacy": "private",
7738
+ "description": "The refusing vendor's own attribution for the wall `budgetExhaustedThisTurn`\nrecords, when it was knowable. Kept SEPARATE from the flag rather than\nreplacing it: a figure-less 402 from a transport no vendor claims yields no\ndetail at all (`budgetDetailOf` returns `undefined`), and folding the two\nwould make that case stop ending the turn.\n\nIt matters most for a sub-agent's wall. The child can sit on a different\nvendor from its parent — `applyAgent` reads `config.provider` — so without\nthis the parent's short-circuit reports `lastResolvedProvider`, i.e. the one\nvendor that did NOT refuse. Under a mixed registry that walls Gemini because\nan Anthropic child 402'd, and if those are the only two reachable vendors the\nhost then derives `blocked` and locks a composer that still had headroom."
7739
+ },
7740
+ {
7741
+ "kind": "field",
7742
+ "name": "budgetWallViaSubAgent",
7743
+ "type": {
7744
+ "text": "boolean"
7745
+ },
7746
+ "privacy": "private",
7747
+ "default": "false",
7748
+ "description": "Whether this turn's budget wall came from a SUB-AGENT rather than this\ndriver's own request. Decides whether `lastResolvedProvider` is a valid\nattribution fallback: for an own wall it is the refusing vendor, for a\nchild's wall it is the parent's vendor — the one known NOT to have refused.\nReset per turn alongside `budgetWallDetail`."
7749
+ },
7541
7750
  {
7542
7751
  "kind": "method",
7543
7752
  "name": "dispose",
@@ -7607,6 +7816,13 @@
7607
7816
  "type": {
7608
7817
  "text": "TurnFailureReason"
7609
7818
  }
7819
+ },
7820
+ {
7821
+ "name": "budget",
7822
+ "optional": true,
7823
+ "type": {
7824
+ "text": "BudgetDetail"
7825
+ }
7610
7826
  }
7611
7827
  ],
7612
7828
  "description": "Build the `done` loop result, carrying the typed failure reason when the turn\nbailed (PTC-0). The discriminant stays `'done'` either way — the same value a\nclean turn returns — so consumers matching on `reason === 'done'` are unchanged;\n`failureReason` is simply present on a failure and absent on success. Omitted\n(rather than set to `undefined`) so a happy-path result stays byte-identical to\nthe historical `{ reason: 'done' }`."
@@ -7635,10 +7851,9 @@
7635
7851
  "kind": "method",
7636
7852
  "name": "loopEndDetail",
7637
7853
  "privacy": "private",
7638
- "static": true,
7639
7854
  "return": {
7640
7855
  "type": {
7641
- "text": "{ failureReason: TurnFailureReason } | undefined"
7856
+ "text": "| {\n failureReason: TurnFailureReason;\n vendor?: AIProviderType;\n budget?: BudgetDetail;\n }\n | undefined"
7642
7857
  }
7643
7858
  },
7644
7859
  "parameters": [
@@ -7649,7 +7864,7 @@
7649
7864
  }
7650
7865
  }
7651
7866
  ],
7652
- "description": "Build the `tool-loop-end` event detail for a turn's result. A failure carries a\n`{ failureReason }` detail; a clean turn emits `undefined` — the historical shape,\nkept byte-identical so subscribers see exactly what they always have."
7867
+ "description": "Build the `tool-loop-end` event detail for a turn's result. A failure carries a\n`{ failureReason }` detail; a clean turn emits `undefined` — the historical shape,\nkept byte-identical so subscribers see exactly what they always have.\n\nA budget failure additionally carries `vendor` — the concrete vendor\n(`'anthropic'`/`'gemini'`) the walled turn resolved to, which the driver knows\nand used to discard. Optional and additive: a subscriber reading only\n`failureReason` is unaffected, a non-budget failure still emits the historical\n`{ failureReason }` with no `vendor` key, and the value is a plain string so\nthe detail stays structured-cloneable for the cross-tab hop. It is the field a\nper-vendor budget model needs and the one that would be awkward to retrofit."
7653
7868
  },
7654
7869
  {
7655
7870
  "kind": "method",
@@ -7829,7 +8044,7 @@
7829
8044
  "name": "getSubAgentFailure",
7830
8045
  "return": {
7831
8046
  "type": {
7832
- "text": "{ reason: SubAgentFailureReason } | undefined"
8047
+ "text": "{ reason: SubAgentFailureReason; budget?: BudgetDetail } | undefined"
7833
8048
  }
7834
8049
  },
7835
8050
  "description": "Returns the typed failure recorded when a sub-agent run ended without\n`completeSubAgent`, if any. Called by a parent `ChatDriver` after running\nthis instance as a sub-agent."
@@ -7849,6 +8064,13 @@
7849
8064
  "type": {
7850
8065
  "text": "SubAgentFailureReason"
7851
8066
  }
8067
+ },
8068
+ {
8069
+ "name": "budget",
8070
+ "optional": true,
8071
+ "type": {
8072
+ "text": "BudgetDetail"
8073
+ }
7852
8074
  }
7853
8075
  ],
7854
8076
  "description": "Record a sub-agent failure reason (first one wins). No-op for top-level\nagents, so loop-exit sites can call it unconditionally. The parent reads\nthis via `getSubAgentFailure()` and emits the `subagent.failed` meta event\nunder its *own* session — see `invokeSubAgent`. (A child sub-agent runs\nunder a separate session key, so recording here would orphan the event off\nthe user-visible debug-log timeline.)"
@@ -8867,7 +9089,7 @@
8867
9089
  },
8868
9090
  "privacy": "private",
8869
9091
  "readonly": true,
8870
- "default": "new ChatDriver(providerRegistry, {\n maxToolIterations: options.maxToolIterations,\n maxFoldOperations: options.maxFoldOperations,\n maxTurnSnapshots: options.maxTurnSnapshots,\n sessionKey: this.sessionKey,\n activityBus: options.activityBus,\n })"
9092
+ "default": "new ChatDriver(providerRegistry, {\n maxToolIterations: options.maxToolIterations,\n maxFoldOperations: options.maxFoldOperations,\n maxTurnSnapshots: options.maxTurnSnapshots,\n sessionKey: this.sessionKey,\n activityBus: options.activityBus,\n budgetExhaustedMessage: options.budgetExhaustedMessage,\n })"
8871
9093
  },
8872
9094
  {
8873
9095
  "kind": "field",
@@ -8941,6 +9163,16 @@
8941
9163
  "default": "false",
8942
9164
  "description": "Set by `cancel()` for the duration of a `sendMessage` run. Breaks the\nhandoff loop so we don't classify or start another agent turn after the\nuser stops. Reset at the start of each `sendMessage`."
8943
9165
  },
9166
+ {
9167
+ "kind": "field",
9168
+ "name": "userMessageAppended",
9169
+ "type": {
9170
+ "text": "boolean"
9171
+ },
9172
+ "privacy": "private",
9173
+ "default": "false",
9174
+ "description": "Whether the CURRENT turn's user message has reached the inner driver's\nhistory. False through the pre-first-turn classify (where the only echo of\nthe message is an optimistic `history-updated` dispatch), true from the\nmoment `chatDriver.sendMessage` is entered — including through every later\nhandoff classify. Read by the budget-wall catch in `sendMessage` to decide\nwhether `reportBudgetExhausted` must append the message itself: appending\nit when already appended duplicated it; not appending it when unappended\nmade it vanish. Reset at the top of each `runOrchestratedTurn`."
9175
+ },
8944
9176
  {
8945
9177
  "kind": "field",
8946
9178
  "name": "pinnedAgentName",
@@ -9199,6 +9431,31 @@
9199
9431
  }
9200
9432
  ]
9201
9433
  },
9434
+ {
9435
+ "kind": "method",
9436
+ "name": "runOrchestratedTurn",
9437
+ "privacy": "private",
9438
+ "return": {
9439
+ "type": {
9440
+ "text": "Promise<ChatDriverResult>"
9441
+ }
9442
+ },
9443
+ "parameters": [
9444
+ {
9445
+ "name": "input",
9446
+ "type": {
9447
+ "text": "string"
9448
+ }
9449
+ },
9450
+ {
9451
+ "name": "attachments",
9452
+ "optional": true,
9453
+ "type": {
9454
+ "text": "ChatAttachment[]"
9455
+ }
9456
+ }
9457
+ ]
9458
+ },
9202
9459
  {
9203
9460
  "kind": "method",
9204
9461
  "name": "continueFromHistory",
@@ -9675,7 +9932,7 @@
9675
9932
  {
9676
9933
  "kind": "variable",
9677
9934
  "name": "settingsModalStyles",
9678
- "default": "css`\n /*\n * Section headings come from the shared fragment the package also exports, so a host titling\n * its own slotted section applies the same rule rather than a copy of it.\n */\n ${assistantSettingsSectionTitleStyles}\n\n *,\n *::before,\n *::after {\n box-sizing: border-box;\n }\n\n .settings-modal {\n --ai-tier-badge-bg-mix: 14%;\n --ai-tier-badge-text-lighten: 18%;\n }\n\n .settings-modal::part(dialog) {\n max-width: 620px;\n width: calc(100vw - 48px);\n max-height: calc(100vh - 48px);\n padding: 0;\n }\n\n .settings-modal::part(content) {\n display: flex;\n flex-direction: column;\n min-height: 0;\n padding: 0;\n width: 100%;\n }\n\n .settings-modal::part(top) {\n padding: calc(var(--design-unit) * 4px) calc(var(--design-unit) * 5px);\n border-bottom: 1px solid var(--neutral-stroke-rest);\n }\n\n .settings-modal::part(bottom) {\n padding: calc(var(--design-unit) * 4px) calc(var(--design-unit) * 5px);\n border-top: 1px solid var(--neutral-stroke-rest);\n border-bottom-left-radius: var(--card-border-radius, calc(var(--control-corner-radius) * 1px));\n border-bottom-right-radius: var(--card-border-radius, calc(var(--control-corner-radius) * 1px));\n background-color: var(--neutral-layer-2);\n }\n\n .settings-modal-header {\n display: flex;\n align-items: center;\n min-width: 0;\n }\n\n .settings-modal-title {\n margin: 0;\n font-size: 16px;\n font-weight: 600;\n display: flex;\n align-items: center;\n gap: calc(var(--design-unit) * 2px);\n color: var(--neutral-foreground-rest);\n }\n\n .settings-modal-content {\n display: flex;\n flex-direction: column;\n min-height: 0;\n flex: 1 1 auto;\n }\n\n .settings-modal-tabs::part(tablist) {\n border-bottom: 0;\n padding: 0 calc(var(--design-unit) * 5px);\n column-gap: calc(var(--design-unit) * 5px);\n }\n\n .settings-modal-tabs::part(activeIndicator) {\n height: 2px;\n }\n\n .settings-modal-tab {\n padding: 0;\n font-weight: 600;\n gap: 6px;\n }\n\n .settings-modal-tab[aria-selected='true'] {\n color: var(--neutral-foreground-rest);\n }\n\n .settings-modal-tabs::part(tabpanel) {\n border-top: 1px solid var(--neutral-stroke-rest);\n }\n\n .settings-modal-tab-panel {\n display: flex;\n flex-direction: column;\n min-height: 0;\n flex: 1 1 auto;\n padding: 0;\n border-radius: 0;\n }\n\n .settings-modal-tab-panel[hidden] {\n display: none !important;\n }\n\n .settings-modal-body {\n overflow-y: auto;\n padding: 0 calc(var(--design-unit) * 5px) calc(var(--design-unit) * 2px);\n max-height: min(60vh, 520px);\n }\n\n .settings-modal-section {\n padding: calc(var(--design-unit) * 5px) 0;\n border-bottom: 1px solid var(--neutral-stroke-rest);\n }\n\n .settings-modal-section:last-child {\n border-bottom: none;\n }\n\n /*\n * Only the spacing. Everything else about this heading comes from the exported fragment\n * above, which the assistant and its hosts share; margin is deliberately not in there,\n * since the gap below a heading depends on the layout around it.\n */\n .ai-settings-section-title {\n margin: 0 0 calc(var(--design-unit) * 3px);\n }\n\n .settings-modal-section-note {\n margin: calc(var(--design-unit) * -2px) 0 calc(var(--design-unit) * 3px);\n font-size: 12px;\n line-height: 1.5;\n color: var(--neutral-foreground-hint);\n }\n\n .settings-modal-section-body {\n display: flex;\n flex-direction: column;\n gap: calc(var(--design-unit) * 4px);\n }\n\n .settings-toggle-row {\n display: flex;\n align-items: flex-start;\n gap: calc(var(--design-unit) * 3px);\n padding: 0 0 calc(var(--design-unit) * 4px) 0;\n border-bottom: 1px solid var(--neutral-stroke-rest);\n }\n\n .settings-toggle-row:last-child {\n border-bottom: none;\n padding-bottom: calc(var(--design-unit) * 1px);\n }\n\n .settings-toggle-row.is-stacked {\n flex-direction: column;\n gap: calc(var(--design-unit) * 2px);\n }\n\n /*\n * For rows whose control is a button rather than a switch. The base row top-aligns, which suits\n * a switch (it lands level with the title), but a button is tall enough that top-aligning leaves\n * it above the title-plus-description block it acts on.\n */\n .settings-toggle-row.is-centered {\n align-items: center;\n }\n\n .settings-animation-list {\n display: flex;\n flex-direction: column;\n width: 100%;\n border: 1px solid var(--neutral-stroke-rest);\n border-radius: calc(var(--control-corner-radius) * 1.2px);\n padding: calc(var(--design-unit) * 2px);\n gap: calc(var(--design-unit) * 2px);\n }\n\n .settings-animation-list .settings-toggle-row {\n border: 0;\n }\n\n .settings-animation-category {\n display: flex;\n flex-direction: column;\n gap: calc(var(--design-unit) * 4px);\n }\n\n .settings-animation-category-label {\n font-weight: 600;\n color: var(--neutral-foreground-hint);\n }\n\n .settings-toggle-row + .settings-animation-category-label {\n border-top: 1px solid var(--neutral-stroke-rest);\n padding-top: calc(var(--design-unit) * 2px);\n }\n\n .settings-animation-option {\n padding: 0;\n margin: 0;\n }\n\n .settings-animation-option:last-child {\n border-bottom: none;\n padding-bottom: calc(var(--design-unit) * 2px);\n }\n\n .settings-toggle-text {\n flex: 1;\n min-width: 0;\n }\n\n .settings-toggle-title {\n font-size: 14px;\n font-weight: 500;\n color: var(--neutral-foreground-rest);\n }\n\n .settings-toggle-desc {\n font-size: 12px;\n color: var(--neutral-foreground-hint);\n line-height: 1.5;\n max-width: 92%;\n }\n\n .settings-toggle-control {\n flex: 0 0 auto;\n min-width: 0;\n }\n\n .settings-toggle-control-wide {\n width: 100%;\n max-width: 280px;\n }\n\n .settings-toggle-row.is-stacked .settings-toggle-desc {\n max-width: none;\n }\n\n .settings-toggle-row.is-stacked .settings-toggle-control-wide {\n width: 100%;\n max-width: none;\n }\n\n /*\n * Sub-heading over the provider toggle group. Unlike the section headings it sits INSIDE\n * .settings-modal-section-body, so the parent's 4-unit flex gap already separates it from the\n * first toggle — and flex does not collapse margins, so the shared rule's 3-unit margin-bottom\n * would add to that gap rather than replace it, leaving the label adrift from the rows it\n * names. Zero the margin and pull back 2 of the 4 gap units, expressed in the same token so it\n * tracks if that gap changes.\n */\n .settings-app-toggles-title {\n margin: 0 0 calc(var(--design-unit) * -2px);\n }\n\n .settings-app-toggle-block {\n display: flex;\n flex-direction: column;\n gap: calc(var(--design-unit) * 1px);\n }\n\n .settings-toggle-hint {\n font-size: 12px;\n color: var(--neutral-foreground-hint);\n line-height: 1.4;\n padding-left: calc(var(--design-unit) * 1px);\n font-style: italic;\n }\n\n .settings-context-header {\n display: flex;\n justify-content: space-between;\n gap: calc(var(--design-unit) * 2px);\n margin-bottom: calc(var(--design-unit) * 2px);\n font-size: 12px;\n }\n\n .settings-context-label {\n color: var(--neutral-foreground-hint);\n font-size: 11px;\n font-weight: 600;\n text-transform: uppercase;\n letter-spacing: 0.06em;\n }\n\n .settings-context-tokens {\n color: var(--neutral-foreground-rest);\n }\n\n .settings-current-build,\n .settings-lifetime-usage {\n background: var(--neutral-layer-2);\n border: 1px solid var(--neutral-stroke-rest);\n border-radius: calc(var(--control-corner-radius) * 1.5px);\n padding: calc(var(--design-unit) * 4px);\n margin: calc(var(--design-unit) * 3px) 0;\n }\n\n /*\n * Deliberately quieter than the live block: no pulsing badge and no accent, so the\n * eye still lands on what is being built now. It leads only because it is the wider\n * figure, not because it is the more urgent one.\n */\n .settings-lifetime-usage-top {\n display: flex;\n align-items: center;\n flex-wrap: wrap;\n gap: calc(var(--design-unit) * 2px);\n margin-bottom: calc(var(--design-unit) * 3px);\n }\n\n .settings-lifetime-label {\n font-size: 10px;\n line-height: 1;\n font-weight: 700;\n text-transform: uppercase;\n letter-spacing: 0.06em;\n color: var(--neutral-foreground-hint);\n }\n\n .settings-current-build-top {\n display: flex;\n align-items: center;\n flex-wrap: wrap;\n gap: calc(var(--design-unit) * 2px);\n margin-bottom: calc(var(--design-unit) * 3px);\n }\n\n /*\n * Optically centring these 10px caps against the 14px project name beside them. Flex offers\n * only two alignments and neither lands it, because a box centre and a text baseline are\n * different lines:\n * - align-items: center sits the badge baseline about 1.2px high\n * - align-items: baseline matches baselines, but the caps then read as bottom-aligned\n * against the name's ascenders, so the badge looks low\n * The eye wants the midpoint, so: centre the boxes, then translate down by the ~1px of\n * overshoot. This is a nudge, but it corrects a fixed relationship between two font sizes\n * rather than some neighbouring element's layout, so unlike a margin tuned to a sibling it\n * cannot rot when that sibling changes. In em, so it tracks the badge text.\n *\n * display: block, not inline-flex: a flex container has no text baseline of its own and\n * synthesizes one from its first child, which here is the dot, an empty span. As a block box\n * the badge takes the baseline of its first line, i.e. the text.\n */\n .settings-live-badge {\n display: block;\n font-size: 10px;\n line-height: 1;\n font-weight: 700;\n text-transform: uppercase;\n letter-spacing: 0.06em;\n color: var(--accent-fill-rest);\n white-space: nowrap;\n transform: translateY(0.1em);\n }\n\n .settings-live-dot {\n display: inline-block;\n width: 7px;\n height: 7px;\n\n /* Replaces the flex gap the badge no longer has. */\n margin-right: calc(var(--design-unit) * 1px);\n\n /* Centres the dot on the text beside it without disturbing that text's baseline. */\n vertical-align: middle;\n border-radius: 50%;\n background: var(--accent-fill-rest);\n animation: settings-live-pulse 1.6s ease-out infinite;\n }\n\n @keyframes settings-live-pulse {\n 0% {\n box-shadow: 0 0 0 0 color-mix(in srgb, var(--accent-fill-rest) 55%, transparent);\n }\n\n 70% {\n box-shadow: 0 0 0 7px transparent;\n }\n\n 100% {\n box-shadow: 0 0 0 0 transparent;\n }\n }\n\n .settings-current-build-name {\n font-size: 14px;\n line-height: 1;\n font-weight: 600;\n }\n\n /*\n * Cost and the token buckets side by side: one number in its own box, the four buckets\n * sharing the box next to it. Stretch, so the two are the same height however tall the 22px\n * cost value makes the left one.\n */\n .settings-usage-row {\n display: flex;\n flex-wrap: wrap;\n align-items: stretch;\n gap: calc(var(--design-unit) * 3px);\n margin-bottom: calc(var(--design-unit) * 3px);\n }\n\n .settings-usage-row-tokens {\n display: flex;\n flex: 1 1 auto;\n min-width: 0;\n }\n\n .settings-build-stat {\n min-width: 0;\n background: var(--neutral-layer-1);\n border: 1px solid var(--neutral-stroke-rest);\n border-radius: calc(var(--control-corner-radius) * 1px);\n padding: calc(var(--design-unit) * 3px);\n }\n\n .settings-build-stat-label {\n font-size: 10px;\n font-weight: 600;\n text-transform: uppercase;\n letter-spacing: 0.06em;\n color: var(--neutral-foreground-hint);\n }\n\n .settings-build-stat-value {\n font-size: 22px;\n font-weight: 700;\n margin-top: calc(var(--design-unit) * 1px);\n color: var(--neutral-foreground-rest);\n }\n\n .settings-build-stat-value .prefix,\n .settings-build-stat-value .suffix {\n color: var(--accent-fill-rest);\n }\n\n /*\n * Per-bucket token split, beneath the headline cost. Small paired label/value cells\n * rather than four more stat cards: they are the detail behind the cost above, so they\n * have to read as subordinate to it rather than compete with it. The four buckets ARE\n * the token figure now — there is no merged total anywhere for them to break down.\n */\n .settings-token-breakdown {\n display: flex;\n flex-direction: column;\n\n /*\n * Tight against the buckets below. One design unit rather than two, because each bucket\n * separates its own label from its value by only 2px — at two units the title sat four\n * times further from the bucket labels than they sit from their own numbers, which read\n * as a gap between unrelated blocks instead of a heading over its content.\n */\n gap: calc(var(--design-unit) * 1px);\n margin-bottom: calc(var(--design-unit) * 3px);\n padding: calc(var(--design-unit) * 2px) calc(var(--design-unit) * 3px);\n background: var(--neutral-layer-1);\n border: 1px solid var(--neutral-stroke-rest);\n border-radius: calc(var(--control-corner-radius) * 1px);\n }\n\n /*\n * Titles the box, because nothing else in it named the unit. Same styling and position as\n * the cost box's label, so two boxes sitting side by side are titled the same way; the\n * hierarchy against the bucket labels comes from position, not a second type size.\n */\n .settings-token-breakdown-label {\n font-size: 10px;\n line-height: 1;\n font-weight: 600;\n text-transform: uppercase;\n letter-spacing: 0.06em;\n color: var(--neutral-foreground-hint);\n }\n\n .settings-token-buckets {\n display: flex;\n flex-wrap: wrap;\n gap: calc(var(--design-unit) * 4px);\n }\n\n .settings-token-bucket {\n display: flex;\n min-width: 0;\n flex-direction: column;\n gap: 2px;\n }\n\n .settings-token-bucket-label {\n font-size: 10px;\n font-weight: 600;\n text-transform: uppercase;\n letter-spacing: 0.06em;\n color: var(--neutral-foreground-hint);\n }\n\n .settings-token-bucket-value {\n font-size: 13px;\n font-weight: 600;\n color: var(--neutral-foreground-rest);\n }\n\n /*\n * Cache reads bill at a fraction of the uncached input rate, and in an agentic\n * session they are usually the largest bucket by a wide margin. Muted so the biggest\n * number on the row doesn't read as the dominant cost driver when it is the cheapest.\n */\n .settings-token-bucket.is-discounted .settings-token-bucket-value {\n font-weight: 500;\n color: var(--neutral-foreground-hint);\n }\n\n /*\n * Usage-row overrides. Placed after the base rules they refine rather than up beside\n * .settings-usage-row itself, because stylelint's no-descending-specificity rejects a\n * compound selector appearing before the plain one it overrides.\n */\n\n /* Sized by its content; the buckets take the rest, since four cells need the width. */\n .settings-usage-row > .settings-build-stat {\n flex: 0 1 auto;\n }\n\n .settings-usage-row .settings-token-breakdown {\n flex: 1 1 auto;\n\n /* The row owns the spacing now; this centres the column against the taller cost box. */\n justify-content: center;\n margin-bottom: 0;\n }\n\n /* Equal columns, so the four buckets line up as a grid rather than bunching left. */\n .settings-usage-row .settings-token-bucket {\n flex: 1 1 0;\n }\n\n .settings-model-tags {\n display: flex;\n flex-wrap: wrap;\n gap: calc(var(--design-unit) * 2px);\n }\n\n .settings-model-tag {\n font-size: 11px;\n font-weight: 600;\n padding: 3px 9px;\n border-radius: 20px;\n font-family: ui-monospace, monospace;\n border: 1px solid var(--neutral-stroke-rest);\n }\n\n .settings-model-tag,\n .settings-cost-history-icon {\n --model-tag-color: var(--neutral-foreground-rest);\n\n background: color-mix(\n in srgb,\n var(--model-tag-color) var(--ai-tier-badge-bg-mix),\n var(--neutral-layer-1)\n );\n border-color: color-mix(in srgb, var(--model-tag-color) 20%, var(--neutral-layer-1));\n color: color-mix(in srgb, var(--model-tag-color), white var(--ai-tier-badge-text-lighten));\n }\n\n .settings-cost-history-title {\n font-size: 11px;\n text-transform: uppercase;\n letter-spacing: 0.08em;\n color: var(--neutral-foreground-hint);\n font-weight: 600;\n margin: calc(var(--design-unit) * 4px) 0 calc(var(--design-unit) * 2px);\n }\n\n .settings-cost-history {\n display: flex;\n flex-direction: column;\n gap: calc(var(--design-unit) * 2px);\n padding-bottom: calc(var(--design-unit) * 2px);\n }\n\n .settings-cost-history-icon {\n flex: 0 0 auto;\n width: 36px;\n height: 36px;\n border-radius: calc(var(--control-corner-radius) * 1px);\n display: inline-flex;\n align-items: center;\n justify-content: center;\n font-size: 16px;\n font-weight: 700;\n border: 1px solid var(--neutral-stroke-rest);\n }\n\n .settings-cost-history-row {\n display: flex;\n flex-direction: column;\n background: var(--neutral-layer-1);\n border: 1px solid var(--neutral-stroke-rest);\n border-radius: calc(var(--control-corner-radius) * 1.5px);\n padding: calc(var(--design-unit) * 4px);\n }\n\n .settings-cost-history-row-content {\n display: flex;\n align-items: flex-start;\n gap: calc(var(--design-unit) * 2px);\n }\n\n /*\n * Both of these carry a ?hidden binding (showTokens), and both declare display: flex — which\n * silently beat the UA [hidden] { display: none } rule, because author-origin declarations\n * outrank user-agent origin whatever the specificity. Without this, showTokens: false still\n * rendered the full breakdown in the lifetime block, the live block and every history row.\n * Sibling elements that hide correctly do so only by declaring no display at all. Same\n * treatment as .settings-modal-tab-panel[hidden] near the top of this file.\n */\n .settings-usage-row-tokens[hidden],\n .settings-cost-history-row-content[hidden] {\n display: none !important;\n }\n\n /*\n * The buckets inside a history row drop the panel treatment they carry in the summary\n * blocks — the row is already a bordered card, and a second box inside it reads as a\n * nested panel. The adjacent-sibling rule above supplies the separator instead.\n */\n .settings-cost-history-row-breakdown .settings-token-breakdown {\n flex: 1 1 auto;\n margin-bottom: 0;\n padding: 0;\n background: none;\n border: none;\n }\n\n .settings-cost-history-row-content + .settings-cost-history-row-content {\n border-top: 1px solid var(--neutral-stroke-rest);\n margin-top: calc(var(--design-unit) * 2px);\n padding-top: calc(var(--design-unit) * 2px);\n }\n\n .settings-cost-history-row-body {\n flex: 1 1 auto;\n min-width: 0;\n display: flex;\n gap: calc(var(--design-unit) * 2px);\n justify-content: space-between;\n }\n\n /*\n * Holds the cost and the delete button as one centred pair. The button used to be a sibling\n * of the whole row body, kept in place by a -2px nudge tuned for when the metrics column had\n * two lines (cost plus a merged token total). Dropping that second line left the nudge\n * compensating for nothing. Centring the two against each other needs no magic number and\n * cannot drift again if the metrics content changes.\n *\n * NB this rule previously set align-items with no display:flex, so it had no effect at all.\n */\n .settings-cost-history-row-right {\n display: flex;\n align-items: center;\n gap: calc(var(--design-unit) * 1px);\n flex: 0 0 auto;\n }\n\n .settings-cost-history-row-cost,\n .settings-cost-history-row-title {\n font-size: 14px;\n font-weight: 600;\n color: var(--neutral-foreground-rest);\n min-width: 0;\n }\n\n .settings-cost-history-row-metrics {\n display: flex;\n flex-direction: column;\n flex: 0 0 auto;\n font-size: 13px;\n font-family: ui-monospace, monospace;\n color: var(--neutral-foreground-rest);\n white-space: nowrap;\n }\n\n .settings-cost-history-row-date {\n font-size: 12px;\n color: var(--neutral-foreground-hint);\n }\n\n .settings-cost-history-delete {\n flex: 0 0 auto;\n color: var(--neutral-foreground-hint);\n }\n\n .settings-cost-history-delete:hover {\n color: var(--error-color);\n }\n\n .settings-cost-history ::slotted(*) {\n display: block;\n }\n\n .settings-modal-footer {\n width: 100%;\n }\n\n .settings-done-button {\n margin-left: auto;\n }\n\n .settings-modal-footer-actions {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: calc(var(--design-unit) * 2px);\n width: 100%;\n }\n\n .settings-disclaimer {\n font-size: 12px;\n color: var(--neutral-foreground-hint);\n max-width: 60%;\n line-height: 1.4;\n }\n\n /*\n * Inline spinner shown in the \"Download agent log\" button while the log builds\n * (GENC-1351 §5.8) — the lifetime-stitch path is an async provider round-trip.\n * Sized to the button label and tinted with currentColor so it tracks the\n * button's text colour in every appearance / theme.\n */\n [part='download-button'] .download-spinner {\n display: inline-block;\n width: 12px;\n height: 12px;\n margin-right: calc(var(--design-unit) * 1px);\n vertical-align: -1px;\n border: 2px solid currentColor;\n border-right-color: transparent;\n border-radius: 50%;\n animation: download-spin 0.6s linear infinite;\n }\n\n @keyframes download-spin {\n to {\n transform: rotate(360deg);\n }\n }\n`",
9935
+ "default": "css`\n /*\n * Section headings come from the shared fragment the package also exports, so a host titling\n * its own slotted section applies the same rule rather than a copy of it.\n */\n ${assistantSettingsSectionTitleStyles}\n\n *,\n *::before,\n *::after {\n box-sizing: border-box;\n }\n\n .settings-modal {\n --ai-tier-badge-bg-mix: 14%;\n --ai-tier-badge-text-lighten: 18%;\n }\n\n .settings-modal::part(dialog) {\n max-width: 620px;\n width: calc(100vw - 48px);\n max-height: calc(100vh - 48px);\n padding: 0;\n }\n\n .settings-modal::part(content) {\n display: flex;\n flex-direction: column;\n min-height: 0;\n padding: 0;\n width: 100%;\n }\n\n .settings-modal::part(top) {\n padding: calc(var(--design-unit) * 4px) calc(var(--design-unit) * 5px);\n border-bottom: 1px solid var(--neutral-stroke-rest);\n }\n\n .settings-modal::part(bottom) {\n padding: calc(var(--design-unit) * 4px) calc(var(--design-unit) * 5px);\n border-top: 1px solid var(--neutral-stroke-rest);\n border-bottom-left-radius: var(--card-border-radius, calc(var(--control-corner-radius) * 1px));\n border-bottom-right-radius: var(--card-border-radius, calc(var(--control-corner-radius) * 1px));\n background-color: var(--neutral-layer-2);\n }\n\n .settings-modal-header {\n display: flex;\n align-items: center;\n min-width: 0;\n }\n\n .settings-modal-title {\n margin: 0;\n font-size: 16px;\n font-weight: 600;\n display: flex;\n align-items: center;\n gap: calc(var(--design-unit) * 2px);\n color: var(--neutral-foreground-rest);\n }\n\n .settings-modal-content {\n display: flex;\n flex-direction: column;\n min-height: 0;\n flex: 1 1 auto;\n }\n\n .settings-modal-tabs::part(tablist) {\n border-bottom: 0;\n padding: 0 calc(var(--design-unit) * 5px);\n column-gap: calc(var(--design-unit) * 5px);\n }\n\n .settings-modal-tabs::part(activeIndicator) {\n height: 2px;\n }\n\n .settings-modal-tab {\n padding: 0;\n font-weight: 600;\n gap: 6px;\n }\n\n .settings-modal-tab[aria-selected='true'] {\n color: var(--neutral-foreground-rest);\n }\n\n .settings-modal-tabs::part(tabpanel) {\n border-top: 1px solid var(--neutral-stroke-rest);\n }\n\n .settings-modal-tab-panel {\n display: flex;\n flex-direction: column;\n min-height: 0;\n flex: 1 1 auto;\n padding: 0;\n border-radius: 0;\n }\n\n .settings-modal-tab-panel[hidden] {\n display: none !important;\n }\n\n .settings-modal-body {\n overflow-y: auto;\n padding: 0 calc(var(--design-unit) * 5px) calc(var(--design-unit) * 2px);\n max-height: min(60vh, 520px);\n }\n\n .settings-modal-section {\n padding: calc(var(--design-unit) * 5px) 0;\n border-bottom: 1px solid var(--neutral-stroke-rest);\n }\n\n .settings-modal-section:last-child {\n border-bottom: none;\n }\n\n /*\n * Only the spacing. Everything else about this heading comes from the exported fragment\n * above, which the assistant and its hosts share; margin is deliberately not in there,\n * since the gap below a heading depends on the layout around it.\n */\n .ai-settings-section-title {\n margin: 0 0 calc(var(--design-unit) * 3px);\n }\n\n .settings-modal-section-note {\n margin: calc(var(--design-unit) * -2px) 0 calc(var(--design-unit) * 3px);\n font-size: 12px;\n line-height: 1.5;\n color: var(--neutral-foreground-hint);\n }\n\n .settings-modal-section-body {\n display: flex;\n flex-direction: column;\n gap: calc(var(--design-unit) * 4px);\n }\n\n .settings-toggle-row {\n display: flex;\n align-items: flex-start;\n gap: calc(var(--design-unit) * 3px);\n padding: 0 0 calc(var(--design-unit) * 4px) 0;\n border-bottom: 1px solid var(--neutral-stroke-rest);\n }\n\n .settings-toggle-row:last-child {\n border-bottom: none;\n padding-bottom: calc(var(--design-unit) * 1px);\n }\n\n .settings-toggle-row.is-stacked {\n flex-direction: column;\n gap: calc(var(--design-unit) * 2px);\n }\n\n /*\n * For rows whose control is a button rather than a switch. The base row top-aligns, which suits\n * a switch (it lands level with the title), but a button is tall enough that top-aligning leaves\n * it above the title-plus-description block it acts on.\n */\n .settings-toggle-row.is-centered {\n align-items: center;\n }\n\n .settings-animation-list {\n display: flex;\n flex-direction: column;\n width: 100%;\n border: 1px solid var(--neutral-stroke-rest);\n border-radius: calc(var(--control-corner-radius) * 1.2px);\n padding: calc(var(--design-unit) * 2px);\n gap: calc(var(--design-unit) * 2px);\n }\n\n .settings-animation-list .settings-toggle-row {\n border: 0;\n }\n\n .settings-animation-category {\n display: flex;\n flex-direction: column;\n gap: calc(var(--design-unit) * 4px);\n }\n\n .settings-animation-category-label {\n font-weight: 600;\n color: var(--neutral-foreground-hint);\n }\n\n .settings-toggle-row + .settings-animation-category-label {\n border-top: 1px solid var(--neutral-stroke-rest);\n padding-top: calc(var(--design-unit) * 2px);\n }\n\n .settings-animation-option {\n padding: 0;\n margin: 0;\n }\n\n .settings-animation-option:last-child {\n border-bottom: none;\n padding-bottom: calc(var(--design-unit) * 2px);\n }\n\n .settings-toggle-text {\n flex: 1;\n min-width: 0;\n }\n\n .settings-toggle-title {\n font-size: 14px;\n font-weight: 500;\n color: var(--neutral-foreground-rest);\n }\n\n .settings-toggle-desc {\n font-size: 12px;\n color: var(--neutral-foreground-hint);\n line-height: 1.5;\n max-width: 92%;\n }\n\n .settings-toggle-control {\n flex: 0 0 auto;\n min-width: 0;\n }\n\n .settings-toggle-control-wide {\n width: 100%;\n max-width: 280px;\n }\n\n .settings-toggle-row.is-stacked .settings-toggle-desc {\n max-width: none;\n }\n\n .settings-toggle-row.is-stacked .settings-toggle-control-wide {\n width: 100%;\n max-width: none;\n }\n\n /*\n * Sub-heading over the provider toggle group. Unlike the section headings it sits INSIDE\n * .settings-modal-section-body, so the parent's 4-unit flex gap already separates it from the\n * first toggle — and flex does not collapse margins, so the shared rule's 3-unit margin-bottom\n * would add to that gap rather than replace it, leaving the label adrift from the rows it\n * names. Zero the margin and pull back 2 of the 4 gap units, expressed in the same token so it\n * tracks if that gap changes.\n */\n .settings-app-toggles-title {\n margin: 0 0 calc(var(--design-unit) * -2px);\n }\n\n .settings-app-toggle-block {\n display: flex;\n flex-direction: column;\n gap: calc(var(--design-unit) * 1px);\n }\n\n .settings-toggle-hint {\n font-size: 12px;\n color: var(--neutral-foreground-hint);\n line-height: 1.4;\n padding-left: calc(var(--design-unit) * 1px);\n font-style: italic;\n }\n\n .settings-context-header {\n display: flex;\n justify-content: space-between;\n gap: calc(var(--design-unit) * 2px);\n margin-bottom: calc(var(--design-unit) * 2px);\n font-size: 12px;\n }\n\n .settings-context-label {\n color: var(--neutral-foreground-hint);\n font-size: 11px;\n font-weight: 600;\n text-transform: uppercase;\n letter-spacing: 0.06em;\n }\n\n .settings-context-tokens {\n color: var(--neutral-foreground-rest);\n }\n\n .settings-current-build,\n .settings-lifetime-usage {\n background: var(--neutral-layer-2);\n border: 1px solid var(--neutral-stroke-rest);\n border-radius: calc(var(--control-corner-radius) * 1.5px);\n padding: calc(var(--design-unit) * 4px);\n margin: calc(var(--design-unit) * 3px) 0;\n }\n\n /*\n * Deliberately quieter than the live block: no pulsing badge and no accent, so the eye lands\n * on what is being built now — which is why that block leads the tab and this one follows,\n * under the history heading whose rows it totals.\n */\n .settings-lifetime-usage-top {\n display: flex;\n align-items: center;\n flex-wrap: wrap;\n gap: calc(var(--design-unit) * 2px);\n margin-bottom: calc(var(--design-unit) * 3px);\n }\n\n .settings-lifetime-label {\n font-size: 10px;\n line-height: 1;\n font-weight: 700;\n text-transform: uppercase;\n letter-spacing: 0.06em;\n color: var(--neutral-foreground-hint);\n }\n\n .settings-current-build-top {\n display: flex;\n align-items: center;\n flex-wrap: wrap;\n gap: calc(var(--design-unit) * 2px);\n margin-bottom: calc(var(--design-unit) * 3px);\n }\n\n /*\n * Optically centring these 10px caps against the 14px project name beside them. Flex offers\n * only two alignments and neither lands it, because a box centre and a text baseline are\n * different lines:\n * - align-items: center sits the badge baseline about 1.2px high\n * - align-items: baseline matches baselines, but the caps then read as bottom-aligned\n * against the name's ascenders, so the badge looks low\n * The eye wants the midpoint, so: centre the boxes, then translate down by the ~1px of\n * overshoot. This is a nudge, but it corrects a fixed relationship between two font sizes\n * rather than some neighbouring element's layout, so unlike a margin tuned to a sibling it\n * cannot rot when that sibling changes. In em, so it tracks the badge text.\n *\n * display: block, not inline-flex: a flex container has no text baseline of its own and\n * synthesizes one from its first child, which here is the dot, an empty span. As a block box\n * the badge takes the baseline of its first line, i.e. the text.\n */\n .settings-live-badge {\n display: block;\n font-size: 10px;\n line-height: 1;\n font-weight: 700;\n text-transform: uppercase;\n letter-spacing: 0.06em;\n color: var(--accent-fill-rest);\n white-space: nowrap;\n transform: translateY(0.1em);\n }\n\n .settings-live-dot {\n display: inline-block;\n width: 7px;\n height: 7px;\n\n /* Replaces the flex gap the badge no longer has. */\n margin-right: calc(var(--design-unit) * 1px);\n\n /* Centres the dot on the text beside it without disturbing that text's baseline. */\n vertical-align: middle;\n border-radius: 50%;\n background: var(--accent-fill-rest);\n animation: settings-live-pulse 1.6s ease-out infinite;\n }\n\n @keyframes settings-live-pulse {\n 0% {\n box-shadow: 0 0 0 0 color-mix(in srgb, var(--accent-fill-rest) 55%, transparent);\n }\n\n 70% {\n box-shadow: 0 0 0 7px transparent;\n }\n\n 100% {\n box-shadow: 0 0 0 0 transparent;\n }\n }\n\n .settings-current-build-name {\n font-size: 14px;\n line-height: 1;\n font-weight: 600;\n }\n\n /*\n * Cost and the token buckets side by side: one number in its own box, the four buckets\n * sharing the box next to it. Stretch, so the two are the same height however tall the 22px\n * cost value makes the left one.\n */\n .settings-usage-row {\n display: flex;\n flex-wrap: wrap;\n align-items: stretch;\n gap: calc(var(--design-unit) * 3px);\n margin-bottom: calc(var(--design-unit) * 3px);\n }\n\n .settings-usage-row-tokens {\n display: flex;\n flex: 1 1 auto;\n min-width: 0;\n }\n\n .settings-build-stat {\n min-width: 0;\n background: var(--neutral-layer-1);\n border: 1px solid var(--neutral-stroke-rest);\n border-radius: calc(var(--control-corner-radius) * 1px);\n padding: calc(var(--design-unit) * 3px);\n }\n\n .settings-build-stat-label {\n font-size: 10px;\n font-weight: 600;\n text-transform: uppercase;\n letter-spacing: 0.06em;\n color: var(--neutral-foreground-hint);\n }\n\n .settings-build-stat-value {\n font-size: 22px;\n font-weight: 700;\n margin-top: calc(var(--design-unit) * 1px);\n color: var(--neutral-foreground-rest);\n }\n\n .settings-build-stat-value .prefix,\n .settings-build-stat-value .suffix {\n color: var(--accent-fill-rest);\n }\n\n /*\n * Per-bucket token split, beneath the headline cost. Small paired label/value cells\n * rather than four more stat cards: they are the detail behind the cost above, so they\n * have to read as subordinate to it rather than compete with it. The four buckets ARE\n * the token figure now — there is no merged total anywhere for them to break down.\n */\n .settings-token-breakdown {\n display: flex;\n flex-direction: column;\n\n /*\n * Tight against the buckets below. One design unit rather than two, because each bucket\n * separates its own label from its value by only 2px — at two units the title sat four\n * times further from the bucket labels than they sit from their own numbers, which read\n * as a gap between unrelated blocks instead of a heading over its content.\n */\n gap: calc(var(--design-unit) * 1px);\n margin-bottom: calc(var(--design-unit) * 3px);\n padding: calc(var(--design-unit) * 2px) calc(var(--design-unit) * 3px);\n background: var(--neutral-layer-1);\n border: 1px solid var(--neutral-stroke-rest);\n border-radius: calc(var(--control-corner-radius) * 1px);\n }\n\n /*\n * Titles the box, because nothing else in it named the unit. Same styling and position as\n * the cost box's label, so two boxes sitting side by side are titled the same way; the\n * hierarchy against the bucket labels comes from position, not a second type size.\n */\n .settings-token-breakdown-label {\n font-size: 10px;\n line-height: 1;\n font-weight: 600;\n text-transform: uppercase;\n letter-spacing: 0.06em;\n color: var(--neutral-foreground-hint);\n }\n\n .settings-token-buckets {\n display: flex;\n flex-wrap: wrap;\n gap: calc(var(--design-unit) * 4px);\n }\n\n .settings-token-bucket {\n display: flex;\n\n /*\n * min-content (not 0) so a bucket cannot be shrunk narrower than its own unbreakable label.\n * With the label set to nowrap, a zero minimum would overflow it into the next column instead\n * of wrapping — so the container's flex-wrap takes over and moves a bucket to a second row,\n * which is the degradation we want when the modal is dragged narrow.\n */\n min-width: min-content;\n flex-direction: column;\n gap: 2px;\n }\n\n .settings-token-bucket-label {\n font-size: 10px;\n font-weight: 600;\n text-transform: uppercase;\n letter-spacing: 0.06em;\n color: var(--neutral-foreground-hint);\n\n /*\n * One line, always. The rate marker is a separate inline box after collapsible whitespace, so\n * a wrappable label gives it a break opportunity of its own — in a narrow column it orphaned\n * onto line 2, which also pushed that bucket's value a line below the other three and broke\n * the row of figures. nowrap removes every break opportunity in the label, the space before\n * the marker included, so no glue character is needed.\n */\n white-space: nowrap;\n }\n\n .settings-token-bucket-value {\n font-size: 13px;\n font-weight: 600;\n color: var(--neutral-foreground-rest);\n }\n\n /*\n * The price-direction marker on a cache bucket's label — down for reads (a fraction of the\n * uncached input rate), up for writes (a premium over it). Three tiers, so a bucket with no\n * marker is the reference rate rather than an unlabelled fourth case.\n *\n * Carried by the label rather than the value, which is what this used to do: cache reads are\n * usually the largest bucket by a wide margin in an agentic session, so the value was muted to\n * stop the biggest number reading as the dominant cost driver. That priced the treatment to the\n * wrong thing — a dimmed 0 (every Gemini cache write) looked disabled next to a normal 0, and a\n * rate is a property of the bucket, not of this turn's figure.\n *\n * Sized down and nudged up rather than tinted: the label is already the hint colour, so a\n * dimmer marker would vanish and a brighter one would out-shout the word it qualifies.\n */\n .settings-token-bucket-rate {\n font-size: 8px;\n line-height: 1;\n vertical-align: 0.15em;\n letter-spacing: 0;\n }\n\n /*\n * Usage-row overrides. Placed after the base rules they refine rather than up beside\n * .settings-usage-row itself, because stylelint's no-descending-specificity rejects a\n * compound selector appearing before the plain one it overrides.\n */\n\n /* Sized by its content; the buckets take the rest, since four cells need the width. */\n .settings-usage-row > .settings-build-stat {\n flex: 0 1 auto;\n }\n\n .settings-usage-row .settings-token-breakdown {\n flex: 1 1 auto;\n\n /* The row owns the spacing now; this centres the column against the taller cost box. */\n justify-content: center;\n margin-bottom: 0;\n }\n\n /*\n * Basis auto, not 0: a zero basis splits the row into four equal columns, so CACHE WRITE is\n * squeezed exactly as hard as INPUT despite being twice the label. Sizing from content gives the\n * long labels the room their text needs and lets the short ones concede it, which is what makes\n * white-space: nowrap above safe — every column starts at least as wide as its own content,\n * then shares the slack. Still grows and shrinks, so the four stay flush edge to edge.\n */\n .settings-usage-row .settings-token-bucket {\n flex: 1 1 auto;\n }\n\n .settings-model-tags {\n display: flex;\n flex-wrap: wrap;\n gap: calc(var(--design-unit) * 2px);\n }\n\n .settings-model-tag {\n font-size: 11px;\n font-weight: 600;\n padding: 3px 9px;\n border-radius: 20px;\n font-family: ui-monospace, monospace;\n border: 1px solid var(--neutral-stroke-rest);\n }\n\n .settings-model-tag,\n .settings-cost-history-icon {\n --model-tag-color: var(--neutral-foreground-rest);\n\n background: color-mix(\n in srgb,\n var(--model-tag-color) var(--ai-tier-badge-bg-mix),\n var(--neutral-layer-1)\n );\n border-color: color-mix(in srgb, var(--model-tag-color) 20%, var(--neutral-layer-1));\n color: color-mix(in srgb, var(--model-tag-color), white var(--ai-tier-badge-text-lighten));\n }\n\n .settings-cost-history-title {\n font-size: 11px;\n text-transform: uppercase;\n letter-spacing: 0.08em;\n color: var(--neutral-foreground-hint);\n font-weight: 600;\n margin: calc(var(--design-unit) * 4px) 0 calc(var(--design-unit) * 2px);\n }\n\n /*\n * The heading labels the lifetime card as well as the rows, so the card gives up its own top\n * margin and the gap becomes the heading's alone. Adjacent siblings collapse to the LARGER of\n * the two margins, so leaving both would space the caps 3 units off the block they label while\n * the rows below sit at 2 — reading as though the heading belonged to neither.\n */\n .settings-cost-history-title + .settings-lifetime-usage {\n margin-top: 0;\n }\n\n .settings-cost-history {\n display: flex;\n flex-direction: column;\n gap: calc(var(--design-unit) * 2px);\n padding-bottom: calc(var(--design-unit) * 2px);\n }\n\n .settings-cost-history-icon {\n flex: 0 0 auto;\n width: 36px;\n height: 36px;\n border-radius: calc(var(--control-corner-radius) * 1px);\n display: inline-flex;\n align-items: center;\n justify-content: center;\n font-size: 16px;\n font-weight: 700;\n border: 1px solid var(--neutral-stroke-rest);\n }\n\n .settings-cost-history-row {\n display: flex;\n flex-direction: column;\n background: var(--neutral-layer-1);\n border: 1px solid var(--neutral-stroke-rest);\n border-radius: calc(var(--control-corner-radius) * 1.5px);\n padding: calc(var(--design-unit) * 4px);\n }\n\n .settings-cost-history-row-content {\n display: flex;\n align-items: flex-start;\n gap: calc(var(--design-unit) * 2px);\n }\n\n /*\n * All of these carry a ?hidden binding, and all declare display: flex — which silently beat the\n * UA [hidden] { display: none } rule, because author-origin declarations outrank user-agent\n * origin whatever the specificity. Without this, showTokens: false still rendered the full\n * breakdown in the lifetime block, the live block and every history row, and an empty row list\n * still occupied its padding under the lifetime total. Sibling elements that hide correctly do\n * so only by declaring no display at all. Same treatment as .settings-modal-tab-panel[hidden]\n * near the top of this file.\n */\n .settings-usage-row-tokens[hidden],\n .settings-cost-history[hidden],\n .settings-cost-history-row-content[hidden] {\n display: none !important;\n }\n\n /*\n * The buckets inside a history row drop the panel treatment they carry in the summary\n * blocks — the row is already a bordered card, and a second box inside it reads as a\n * nested panel. The adjacent-sibling rule above supplies the separator instead.\n */\n .settings-cost-history-row-breakdown .settings-token-breakdown {\n flex: 1 1 auto;\n margin-bottom: 0;\n padding: 0;\n background: none;\n border: none;\n }\n\n .settings-cost-history-row-content + .settings-cost-history-row-content {\n border-top: 1px solid var(--neutral-stroke-rest);\n margin-top: calc(var(--design-unit) * 2px);\n padding-top: calc(var(--design-unit) * 2px);\n }\n\n .settings-cost-history-row-body {\n flex: 1 1 auto;\n min-width: 0;\n display: flex;\n gap: calc(var(--design-unit) * 2px);\n justify-content: space-between;\n }\n\n /*\n * Holds the cost and the delete button as one centred pair. The button used to be a sibling\n * of the whole row body, kept in place by a -2px nudge tuned for when the metrics column had\n * two lines (cost plus a merged token total). Dropping that second line left the nudge\n * compensating for nothing. Centring the two against each other needs no magic number and\n * cannot drift again if the metrics content changes.\n *\n * NB this rule previously set align-items with no display:flex, so it had no effect at all.\n */\n .settings-cost-history-row-right {\n display: flex;\n align-items: center;\n gap: calc(var(--design-unit) * 1px);\n flex: 0 0 auto;\n }\n\n .settings-cost-history-row-cost,\n .settings-cost-history-row-title {\n font-size: 14px;\n font-weight: 600;\n color: var(--neutral-foreground-rest);\n min-width: 0;\n }\n\n .settings-cost-history-row-metrics {\n display: flex;\n flex-direction: column;\n flex: 0 0 auto;\n font-size: 13px;\n font-family: ui-monospace, monospace;\n color: var(--neutral-foreground-rest);\n white-space: nowrap;\n }\n\n .settings-cost-history-row-date {\n font-size: 12px;\n color: var(--neutral-foreground-hint);\n }\n\n .settings-cost-history-delete {\n flex: 0 0 auto;\n color: var(--neutral-foreground-hint);\n }\n\n .settings-cost-history-delete:hover {\n color: var(--error-color);\n }\n\n .settings-cost-history ::slotted(*) {\n display: block;\n }\n\n .settings-modal-footer {\n width: 100%;\n }\n\n .settings-done-button {\n margin-left: auto;\n }\n\n .settings-modal-footer-actions {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: calc(var(--design-unit) * 2px);\n width: 100%;\n }\n\n .settings-disclaimer {\n font-size: 12px;\n color: var(--neutral-foreground-hint);\n max-width: 60%;\n line-height: 1.4;\n }\n\n /*\n * Inline spinner shown in the \"Download agent log\" button while the log builds\n * (GENC-1351 §5.8) — the lifetime-stitch path is an async provider round-trip.\n * Sized to the button label and tinted with currentColor so it tracks the\n * button's text colour in every appearance / theme.\n */\n [part='download-button'] .download-spinner {\n display: inline-block;\n width: 12px;\n height: 12px;\n margin-right: calc(var(--design-unit) * 1px);\n vertical-align: -1px;\n border: 2px solid currentColor;\n border-right-color: transparent;\n border-radius: 50%;\n animation: download-spin 0.6s linear infinite;\n }\n\n @keyframes download-spin {\n to {\n transform: rotate(360deg);\n }\n }\n`",
9679
9936
  "description": "Styles for the assistant settings modal (tabs, sections, toggles, cost summary)."
9680
9937
  }
9681
9938
  ],