@genesislcap/ai-assistant 15.7.3-alpha-e7d4aa5.0 → 15.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/dist/ai-assistant.api.json +191 -1
  2. package/dist/ai-assistant.d.ts +155 -1
  3. package/dist/custom-elements.json +114 -2
  4. package/dist/dts/components/settings-modal/settings-modal.styles.d.ts.map +1 -1
  5. package/dist/dts/components/settings-modal/settings-modal.styles.test.d.ts +2 -0
  6. package/dist/dts/components/settings-modal/settings-modal.styles.test.d.ts.map +1 -0
  7. package/dist/dts/components/settings-modal/settings-modal.template.d.ts +22 -0
  8. package/dist/dts/components/settings-modal/settings-modal.template.d.ts.map +1 -1
  9. package/dist/dts/components/settings-modal/settings-modal.template.test.d.ts +2 -0
  10. package/dist/dts/components/settings-modal/settings-modal.template.test.d.ts.map +1 -0
  11. package/dist/dts/main/budget-meter.test.d.ts +2 -0
  12. package/dist/dts/main/budget-meter.test.d.ts.map +1 -0
  13. package/dist/dts/main/main.d.ts +113 -2
  14. package/dist/dts/main/main.d.ts.map +1 -1
  15. package/dist/dts/main/main.types.d.ts +25 -0
  16. package/dist/dts/main/main.types.d.ts.map +1 -1
  17. package/dist/dts/state/ai-assistant-slice.d.ts +47 -0
  18. package/dist/dts/state/ai-assistant-slice.d.ts.map +1 -1
  19. package/dist/dts/state/session-store.d.ts +4 -0
  20. package/dist/dts/state/session-store.d.ts.map +1 -1
  21. package/dist/dts/utils/format-usd.d.ts +24 -0
  22. package/dist/dts/utils/format-usd.d.ts.map +1 -0
  23. package/dist/esm/components/settings-modal/settings-modal.styles.js +33 -3
  24. package/dist/esm/components/settings-modal/settings-modal.styles.test.js +80 -0
  25. package/dist/esm/components/settings-modal/settings-modal.template.js +64 -14
  26. package/dist/esm/components/settings-modal/settings-modal.template.test.js +91 -0
  27. package/dist/esm/main/budget-meter.test.js +317 -0
  28. package/dist/esm/main/main.js +189 -3
  29. package/dist/esm/state/ai-assistant-slice.js +38 -0
  30. package/dist/esm/utils/format-usd.js +23 -0
  31. package/dist/tsconfig.tsbuildinfo +1 -1
  32. package/docs/migration-GENC-1464.md +63 -0
  33. package/docs/styling.md +23 -0
  34. package/package.json +17 -17
  35. package/src/components/settings-modal/settings-modal.styles.test.ts +94 -0
  36. package/src/components/settings-modal/settings-modal.styles.ts +33 -3
  37. package/src/components/settings-modal/settings-modal.template.test.ts +124 -0
  38. package/src/components/settings-modal/settings-modal.template.ts +82 -13
  39. package/src/main/budget-meter.test.ts +427 -0
  40. package/src/main/main.ts +198 -3
  41. package/src/main/main.types.ts +26 -0
  42. package/src/state/ai-assistant-slice.ts +74 -0
  43. package/src/utils/format-usd.ts +26 -0
@@ -17,6 +17,22 @@ export interface InputOverride {
17
17
  /** The mode requested for this invocation. */
18
18
  mode: ChatInputDuringExecutionMode;
19
19
  }
20
+ /**
21
+ * One vendor's AI-spend figures, as fed by the host from the budget endpoint
22
+ * (GENC-1464 — the settings-modal budget meter).
23
+ *
24
+ * Dollars, not a percentage: the meter's text shows the real figures (spend CAN
25
+ * exceed the cap — the last allowed call may overshoot it), and only the bar
26
+ * clamps. Storing a pre-computed percentage would destroy the overshoot.
27
+ *
28
+ * @beta
29
+ */
30
+ export interface VendorBudgetFigures {
31
+ /** The vendor's budget cap in USD. Finite and `>= 0`. */
32
+ budgetUsd: number;
33
+ /** Metered spend against that cap in USD. Finite and `>= 0`; may exceed the cap. */
34
+ spentUsd: number;
35
+ }
20
36
  /**
21
37
  * Shape of a single AI assistant session's serializable state.
22
38
  *
@@ -172,6 +188,20 @@ export interface AiAssistantSessionState {
172
188
  * copy composed from the vendor's display name.
173
189
  */
174
190
  blockedVendorReasons: Partial<Record<AIProviderType, string>>;
191
+ /**
192
+ * Per-vendor AI-spend figures for the settings-modal budget meter (GENC-1464),
193
+ * fed by the host via the element's `setVendorBudget`. A vendor with no entry
194
+ * renders no meter row — which is also how "unlimited" is expressed: the host
195
+ * simply never feeds a vendor the server marks unlimited.
196
+ *
197
+ * NOT persisted to the session snapshot, deliberately. The server is
198
+ * authoritative for spend and the host re-feeds on boot (and after each turn),
199
+ * so a persisted copy could only ever show stale dollars across a reload —
200
+ * wrong money is worse than a beat of no meter. The snapshot assembler
201
+ * (`session-snapshot.ts`) builds from an explicit field list, so absence is
202
+ * structural, not an accident of serialization.
203
+ */
204
+ vendorBudgets: Partial<Record<AIProviderType, VendorBudgetFigures>>;
175
205
  /** Draft text in the input box — preserved across pop-in/pop-out cycles. */
176
206
  inputValue: string;
177
207
  /** Live trace from a currently-executing sub-agent. Cleared on sub-agent-stop. */
@@ -295,6 +325,23 @@ export declare const aiAssistantSlice: import("@reduxjs/toolkit").Slice<AiAssist
295
325
  */
296
326
  swept?: boolean;
297
327
  }>): void;
328
+ /**
329
+ * Store (or clear, with `null`) one vendor's budget figures for the meter.
330
+ *
331
+ * Validation lives at the element boundary (`setVendorBudget` on the
332
+ * element), not here — the reducer is the storage layer and trusts its one
333
+ * caller, exactly as `setVendorBlocked` above trusts its.
334
+ *
335
+ * Idempotent by VALUE, not just by effect: re-feeding the figures a vendor
336
+ * already holds leaves the stored object untouched. The documented feed
337
+ * re-runs after every turn, so without this every turn would publish a new
338
+ * store reference for the same dollars and re-render a meter that has not
339
+ * changed.
340
+ */
341
+ setVendorBudget(state: import("immer").WritableDraft<AiAssistantSessionState>, action: PayloadAction<{
342
+ vendor: AIProviderType;
343
+ figures: VendorBudgetFigures | null;
344
+ }>): void;
298
345
  setInputValue(state: import("immer").WritableDraft<AiAssistantSessionState>, action: PayloadAction<string>): void;
299
346
  setLiveSubAgentTrace(state: import("immer").WritableDraft<AiAssistantSessionState>, action: PayloadAction<ChatMessage[]>): void;
300
347
  setLiveSubAgentName(state: import("immer").WritableDraft<AiAssistantSessionState>, action: PayloadAction<string | null>): void;
@@ -1 +1 @@
1
- {"version":3,"file":"ai-assistant-slice.d.ts","sourceRoot":"","sources":["../../../src/state/ai-assistant-slice.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"ai-assistant-slice.d.ts","sourceRoot":"","sources":["../../../src/state/ai-assistant-slice.ts"],"names":[],"mappings":"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"}
@@ -34,6 +34,10 @@ declare function makeStore(devTools: boolean | undefined): import("@genesislcap/
34
34
  reason?: string | null;
35
35
  swept?: boolean;
36
36
  }>): void;
37
+ setVendorBudget(state: import("immer").WritableDraft<AiAssistantSessionState>, action: import("@reduxjs/toolkit").PayloadAction<{
38
+ vendor: import("@genesislcap/foundation-ai").AIProviderType;
39
+ figures: import("./ai-assistant-slice").VendorBudgetFigures | null;
40
+ }>): void;
37
41
  setInputValue(state: import("immer").WritableDraft<AiAssistantSessionState>, action: import("@reduxjs/toolkit").PayloadAction<string>): void;
38
42
  setLiveSubAgentTrace(state: import("immer").WritableDraft<AiAssistantSessionState>, action: import("@reduxjs/toolkit").PayloadAction<import("@genesislcap/foundation-ai").ChatMessage[]>): void;
39
43
  setLiveSubAgentName(state: import("immer").WritableDraft<AiAssistantSessionState>, action: import("@reduxjs/toolkit").PayloadAction<string | null>): void;
@@ -1 +1 @@
1
- {"version":3,"file":"session-store.d.ts","sourceRoot":"","sources":["../../../src/state/session-store.ts"],"names":[],"mappings":"AACA,OAAO,EAGL,KAAK,uBAAuB,EAC7B,MAAM,sBAAsB,CAAC;AAE9B,KAAK,kBAAkB,GAAG,UAAU,CAAC,OAAO,SAAS,CAAC,CAAC;AAIvD,iBAAS,SAAS,CAAC,QAAQ,EAAE,OAAO,GAAG,SAAS;;;;;;;;;;;;;;;;;;;;;;;;;cA6C+wa,CAAC;;;;;;cAAggD,CAAC;aAAob,CAAC;;;;;;;;;;;wCAvCrve;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,OAAO,GAAG,kBAAkB,CAMnF;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAEpD;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAEpD;AAED;;;;GAIG;AACH,wBAAgB,qBAAqB,IAAI,IAAI,CAE5C;AAED,YAAY,EAAE,kBAAkB,EAAE,uBAAuB,EAAE,CAAC"}
1
+ {"version":3,"file":"session-store.d.ts","sourceRoot":"","sources":["../../../src/state/session-store.ts"],"names":[],"mappings":"AACA,OAAO,EAGL,KAAK,uBAAuB,EAC7B,MAAM,sBAAsB,CAAC;AAE9B,KAAK,kBAAkB,GAAG,UAAU,CAAC,OAAO,SAAS,CAAC,CAAC;AAIvD,iBAAS,SAAS,CAAC,QAAQ,EAAE,OAAO,GAAG,SAAS;;;;;;;;;;;;;;;;;;;;;;;;;cA6Cysd,CAAC;;;;;;cAAggD,CAAC;aAAob,CAAC;;;;;;;;;;;;;;;wCAvC/qhB;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,OAAO,GAAG,kBAAkB,CAMnF;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAEpD;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAEpD;AAED;;;;GAIG;AACH,wBAAgB,qBAAqB,IAAI,IAAI,CAE5C;AAED,YAAY,EAAE,kBAAkB,EAAE,uBAAuB,EAAE,CAAC"}
@@ -0,0 +1,24 @@
1
+ /**
2
+ * The ONE place this package renders a USD amount as text.
3
+ *
4
+ * Extracted (GENC-1464 budget meter) from the ad-hoc `.toFixed(2)` call sites
5
+ * that had grown in `formatBlockedReason` and the settings modal's cost blocks,
6
+ * so the blocked banner, the session-cost figures and the budget meter cannot
7
+ * drift to different precisions. Two decimals always — money columns jitter
8
+ * when "$5.1" sits above "$5.10".
9
+ *
10
+ * @internal
11
+ */
12
+ /** Decimal places for every USD figure this package renders. */
13
+ export declare const USD_DECIMALS = 2;
14
+ /**
15
+ * The bare amount, no currency sign — `(12.3) => "12.30"`.
16
+ *
17
+ * Exists for the settings modal's cost blocks, where the `$` is a separately
18
+ * styled `<span class="prefix">` and folding it into the text would change the
19
+ * DOM those styles target.
20
+ */
21
+ export declare const formatUsdAmount: (value: number) => string;
22
+ /** A signed USD figure — `(12.3) => "$12.30"`. */
23
+ export declare const formatUsd: (value: number) => string;
24
+ //# sourceMappingURL=format-usd.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"format-usd.d.ts","sourceRoot":"","sources":["../../../src/utils/format-usd.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,gEAAgE;AAChE,eAAO,MAAM,YAAY,IAAI,CAAC;AAE9B;;;;;;GAMG;AACH,eAAO,MAAM,eAAe,GAAI,OAAO,MAAM,KAAG,MAAqC,CAAC;AAEtF,kDAAkD;AAClD,eAAO,MAAM,SAAS,GAAI,OAAO,MAAM,KAAG,MAAsC,CAAC"}
@@ -274,7 +274,17 @@ export const settingsModalStyles = css `
274
274
  font-style: italic;
275
275
  }
276
276
 
277
- .settings-context-header {
277
+ /*
278
+ * The budget meter (GENC-1464) is the context indicator's money sibling and
279
+ * shares its row anatomy 1:1, so the budget classes JOIN these rules rather
280
+ * than restating them — one place to restyle the header/label/figures shape,
281
+ * and the two indicators cannot drift apart. Design-system tokens only, and
282
+ * unfallbacked: every token here ships a DS default, so a var(--x, y)
283
+ * fallback would be dead code that hides a broken token name (#2411/#2436
284
+ * review rule, pinned by settings-modal.styles.test.ts).
285
+ */
286
+ .settings-context-header,
287
+ .settings-budget-header {
278
288
  display: flex;
279
289
  justify-content: space-between;
280
290
  gap: calc(var(--design-unit) * 2px);
@@ -282,7 +292,8 @@ export const settingsModalStyles = css `
282
292
  font-size: 12px;
283
293
  }
284
294
 
285
- .settings-context-label {
295
+ .settings-context-label,
296
+ .settings-budget-label {
286
297
  color: var(--neutral-foreground-hint);
287
298
  font-size: 11px;
288
299
  font-weight: 600;
@@ -290,10 +301,29 @@ export const settingsModalStyles = css `
290
301
  letter-spacing: 0.06em;
291
302
  }
292
303
 
293
- .settings-context-tokens {
304
+ .settings-context-tokens,
305
+ .settings-budget-figures {
294
306
  color: var(--neutral-foreground-rest);
295
307
  }
296
308
 
309
+ /*
310
+ * ONE rhythm for every meter row. The outer wrapper holds the context
311
+ * indicator and the budget meter; the inner one holds the budget meter's
312
+ * vendor rows — and both gap by the same 2 units (half the section body's 4)
313
+ * so that visually identical meter rows are evenly spaced whatever mix of
314
+ * them is on screen. Without the wrapper the context row inherits the body's
315
+ * 4-unit gap while the vendor rows keep 2, and three identical-looking rows
316
+ * read as an uneven 4/2/2. The wrapper renders only when at least one meter
317
+ * does — see settingsUsageMetersTemplate for why an always-on one would cost
318
+ * a stray gap.
319
+ */
320
+ .settings-usage-meters,
321
+ .settings-budget-usage {
322
+ display: flex;
323
+ flex-direction: column;
324
+ gap: calc(var(--design-unit) * 2px);
325
+ }
326
+
297
327
  .settings-current-build,
298
328
  .settings-lifetime-usage {
299
329
  background: var(--neutral-layer-2);
@@ -0,0 +1,80 @@
1
+ var _a;
2
+ import { assert, createLogicSuite } from '@genesislcap/foundation-testing';
3
+ import { settingsModalStyles } from './settings-modal.styles';
4
+ // GENC-1464 budget meter — the styling invariants a reviewer would otherwise
5
+ // have to catch by eye, following main.styles.test.ts (see there for why CSS
6
+ // gets asserted from the generated text at all, and for the scoping lesson:
7
+ // these assertions are about the BUDGET rules only, not a global style policy).
8
+ //
9
+ // 1. Design-system tokens are consumed UNFALLBACKED — the #2411/#2436 review
10
+ // rule. Every token the meter reads ships a DS default, so a
11
+ // `var(--token, fallback)` is dead code at best and a silently-broken
12
+ // token name at worst.
13
+ // 2. The meter's row anatomy is SHARED with the context indicator via grouped
14
+ // selectors, not copied. The two indicators are siblings by design;
15
+ // copied declarations would let them drift apart one edit at a time, which
16
+ // is exactly what the grouping was chosen to make impossible.
17
+ const suite = createLogicSuite('settings-modal.styles budget-meter rules');
18
+ /**
19
+ * The generated stylesheet text, flattened recursively — `css` composes nested
20
+ * `ElementStyles`, and a shallow read can return almost nothing, making every
21
+ * negative assertion below pass vacuously. The first test guards that.
22
+ */
23
+ const flatten = (value) => {
24
+ if (typeof value === 'string')
25
+ return value;
26
+ if (Array.isArray(value))
27
+ return value.map(flatten).join('\n');
28
+ const nested = value === null || value === void 0 ? void 0 : value.styles;
29
+ return nested ? flatten(nested) : '';
30
+ };
31
+ /**
32
+ * Comments stripped before matching: the assertions below are about
33
+ * DECLARATIONS, and the sheet's own prose legitimately mentions the very
34
+ * pattern (`var(--x, y)`) the negative assertion hunts for.
35
+ */
36
+ const cssText = flatten(settingsModalStyles).replace(/\/\*[\s\S]*?\*\//g, '');
37
+ /** Every declaration block whose selector mentions `.settings-budget-`, joined. */
38
+ const budgetRules = ((_a = cssText.match(/[^{}]*\.settings-budget-[^{}]*\{[^}]*\}/g)) !== null && _a !== void 0 ? _a : []).join('\n');
39
+ suite('the budget rules were extracted as text (guards the assertions below)', () => {
40
+ assert.ok(cssText.includes('.settings-budget-usage'), 'the meter container rule is in the sheet');
41
+ assert.ok(budgetRules.length > 0, 'the extraction found the budget rules');
42
+ });
43
+ suite('the meter consumes design-system tokens without dead fallbacks', () => {
44
+ // One structural probe rather than a per-token list: ANY `var(--x,` inside a
45
+ // budget rule is a fallback, and the meter reads only tokens that ship DS
46
+ // defaults (--design-unit, --neutral-foreground-*), so there is no legitimate
47
+ // fallback for this scope. Scoped to the budget rules on purpose — see
48
+ // main.styles.test.ts for why a whole-sheet grep is the wrong test.
49
+ assert.not.ok(/var\(--[a-z-]+\s*,/.test(budgetRules), 'no var(--token, fallback) in the meter');
50
+ assert.ok(budgetRules.includes('var(--design-unit)'), 'and it does consume tokens');
51
+ });
52
+ suite('every meter row sits on one shared rhythm', () => {
53
+ // The 4/2/2 fix. `.settings-modal-section-body` gaps at 4 units and the
54
+ // budget meter at 2, so an unwrapped context indicator sat 4 units above the
55
+ // FIRST vendor row while the vendor rows sat 2 apart — three visually
56
+ // identical meter rows, unevenly spaced. Both meters now live in
57
+ // `.settings-usage-meters`, which must carry the SAME gap as the meter it
58
+ // wraps: asserted as ONE grouped rule, so the two cannot drift to different
59
+ // rhythms. (settings-modal.template.test.ts pins the other half — that the
60
+ // wrapper is conditional, so a host with no meters pays no stray gap for it.)
61
+ const shared = /\.settings-usage-meters\s*,\s*\.settings-budget-usage\s*\{([^}]*)\}/.exec(cssText);
62
+ assert.ok(shared, '.settings-usage-meters is grouped with .settings-budget-usage');
63
+ assert.ok(shared[1].includes('flex-direction: column'), 'the shared container is a column');
64
+ assert.ok(shared[1].includes('gap: calc(var(--design-unit) * 2px)'), 'gapped at the meter rhythm (2 units), not the section body 4');
65
+ });
66
+ suite("the meter shares the context indicator's row anatomy by grouped selector", () => {
67
+ // The invariant is the SHARING, not the shape: each budget class must sit in
68
+ // the same selector list as its context sibling, so restyling one restyles
69
+ // both and the two indicators cannot drift apart.
70
+ const sharedPairs = [
71
+ ['.settings-context-header', '.settings-budget-header'],
72
+ ['.settings-context-label', '.settings-budget-label'],
73
+ ['.settings-context-tokens', '.settings-budget-figures'],
74
+ ];
75
+ for (const [contextClass, budgetClass] of sharedPairs) {
76
+ const grouped = new RegExp(`${contextClass.replace(/\./g, '\\.')}\\s*,\\s*${budgetClass.replace(/\./g, '\\.')}\\s*\\{`);
77
+ assert.ok(grouped.test(cssText), `${budgetClass} is grouped with ${contextClass}`);
78
+ }
79
+ });
80
+ suite.run();
@@ -1,8 +1,8 @@
1
1
  import { html, ref, repeat, slotted, when } from '@genesislcap/web-core';
2
- import { ANIMATION_DEFS } from '../../main/main.types';
2
+ import { ANIMATION_DEFS, } from '../../main/main.types';
3
3
  import { formatCostSessionDate, } from '../../utils/cost-session-history';
4
+ import { formatUsd, formatUsdAmount } from '../../utils/format-usd';
4
5
  import { formatCompactTokenCountSafe } from '../../utils/sum-tokens';
5
- const SESSION_COST_DECIMALS = 2;
6
6
  /** Animation options grouped by {@link ANIMATION_DEFS} category for the settings list. */
7
7
  const animationCategoryGroups = (() => {
8
8
  var _a;
@@ -92,12 +92,11 @@ const chatBotToggleRow = (title, description, switchTag, part, checked, onChange
92
92
  ></${switchTag}>
93
93
  `);
94
94
  const platformContextUsageTemplate = (progressTag) => html `
95
- ${when((x) => {
96
- var _a;
97
- return ((_a = x.chatConfig.ui) === null || _a === void 0 ? void 0 : _a.showContextUsage) !== false &&
98
- x.contextTokens != null &&
99
- x.contextLimit != null;
100
- }, html `
95
+ ${when(
96
+ // The getter, not a copy of its expression: `settingsUsageMetersTemplate`
97
+ // gates its shared wrapper on this same getter, and the two must agree —
98
+ // a drifted copy would leave the wrapper rendering around nothing.
99
+ (x) => x.settingsContextUsageVisible, html `
101
100
  <div class="settings-context-usage" part="context-usage">
102
101
  <div class="settings-context-header">
103
102
  <span class="settings-context-label">Context window</span>
@@ -114,6 +113,59 @@ const platformContextUsageTemplate = (progressTag) => html `
114
113
  </div>
115
114
  `)}
116
115
  `;
116
+ /**
117
+ * Per-vendor AI budget meter (GENC-1464) — the money sibling of
118
+ * {@link platformContextUsageTemplate}, one row per vendor the host has fed
119
+ * figures for. The row text is unclamped (overshoot is real and shown); only
120
+ * the bar clamps — both computed in `settingsBudgetRows`, so the bindings here
121
+ * stay pure reads and the arithmetic is unit-testable.
122
+ */
123
+ const platformBudgetUsageTemplate = (progressTag) => html `
124
+ ${when((x) => x.settingsBudgetUsageVisible, html `
125
+ <div class="settings-budget-usage" part="budget-usage">
126
+ ${repeat((x) => x.settingsBudgetRows, html `
127
+ <div class="settings-budget-vendor">
128
+ <div class="settings-budget-header">
129
+ <span class="settings-budget-label">${(row) => row.label} budget</span>
130
+ <span class="settings-budget-figures">${(row) => row.figures}</span>
131
+ </div>
132
+ <${progressTag}
133
+ part="budget-progress"
134
+ value="${(row) => row.barValue}"
135
+ ></${progressTag}>
136
+ </div>
137
+ `)}
138
+ </div>
139
+ `)}
140
+ `;
141
+ /**
142
+ * The two usage meters in one shared, evenly-gapped column.
143
+ *
144
+ * Without it the context row and the FIRST budget row sit the section body's
145
+ * 4 units apart while the budget rows sit 2 apart — three visually identical
146
+ * meter rows reading as an uneven 4/2/2. The wrapper puts every meter row on
147
+ * the same 2-unit rhythm; see `.settings-usage-meters` in the styles.
148
+ *
149
+ * CONDITIONAL on purpose. An always-present wrapper is a permanent flex child
150
+ * of `.settings-modal-section-body`, so a host with neither meter would get a
151
+ * stray 4-unit gap after its model slot from a div that renders nothing. The
152
+ * gate is exactly the disjunction of the two children's own gates — both of
153
+ * which are `when`, which emplaces only comment markers when false, so a
154
+ * hidden meter contributes no flex child inside the wrapper either.
155
+ *
156
+ * Exported for `settings-modal.template.test.ts`, which renders it in isolation
157
+ * to pin both of those properties; `index.ts` does not re-export it, so it is
158
+ * not package API.
159
+ *
160
+ * @internal
161
+ */
162
+ export const settingsUsageMetersTemplate = (progressTag) => html `
163
+ ${when((x) => x.settingsContextUsageVisible || x.settingsBudgetUsageVisible, html `
164
+ <div class="settings-usage-meters">
165
+ ${platformContextUsageTemplate(progressTag)} ${platformBudgetUsageTemplate(progressTag)}
166
+ </div>
167
+ `)}
168
+ `;
117
169
  const chatBotSettingsTemplate = (switchTag, buttonTag) => html `
118
170
  ${when((x) => { var _a; return ((_a = x.chatConfig.ui) === null || _a === void 0 ? void 0 : _a.showToolCalls) != null; }, chatBotToggleRow('Show tool calls', "Display each tool the chatbot invokes — file reads, searches, edits and commands — inline as it works, so you can follow exactly what it's doing behind the scenes.", switchTag, 'toggle-tool-calls', (x) => x.showToolCalls, (x, c) => {
119
171
  x.showToolCalls = readSwitchChecked(c.event);
@@ -306,9 +358,7 @@ const lifetimeUsageTemplate = () => html `
306
358
  <div class="settings-build-stat-label">Total build cost</div>
307
359
  <div class="settings-build-stat-value">
308
360
  <span class="prefix">$</span>
309
- <span class="value">
310
- ${(x) => x.cumulativeCostUsd.toFixed(SESSION_COST_DECIMALS)}
311
- </span>
361
+ <span class="value">${(x) => formatUsdAmount(x.cumulativeCostUsd)}</span>
312
362
  </div>
313
363
  </div>
314
364
  <div
@@ -350,7 +400,7 @@ const sessionCostSummaryTemplate = () => html `
350
400
  </div>
351
401
  <div class="settings-build-stat-value">
352
402
  <span class="prefix">$</span>
353
- <span class="value">${(x) => x.sessionCostUsd.toFixed(SESSION_COST_DECIMALS)}</span>
403
+ <span class="value">${(x) => formatUsdAmount(x.sessionCostUsd)}</span>
354
404
  </div>
355
405
  </div>
356
406
  <div
@@ -394,7 +444,7 @@ const costSessionHistoryTemplate = (iconTag, buttonTag) => html `
394
444
  <div class="settings-cost-history-row-right">
395
445
  <div class="settings-cost-history-row-metrics">
396
446
  <div class="settings-cost-history-row-cost">
397
- $${(record) => record.usage.costUsd.toFixed(SESSION_COST_DECIMALS)}
447
+ ${(record) => formatUsd(record.usage.costUsd)}
398
448
  </div>
399
449
  </div>
400
450
  <${buttonTag}
@@ -461,7 +511,7 @@ export const SettingsModalTemplate = (designSystemPrefix) => {
461
511
  </h3>
462
512
  <div class="settings-modal-section-body">
463
513
  <slot name="settings-model" ${slotted('settingsModelSlotted')}></slot>
464
- ${platformContextUsageTemplate(progressTag)}
514
+ ${settingsUsageMetersTemplate(progressTag)}
465
515
  </div>
466
516
  </section>
467
517
  <!--
@@ -0,0 +1,91 @@
1
+ import { assert, createLogicSuite } from '@genesislcap/foundation-testing';
2
+ import { FoundationAiAssistant } from '../../main/main';
3
+ import { getSessionStore, clearAllSessionStores } from '../../state/session-store';
4
+ import { settingsUsageMetersTemplate } from './settings-modal.template';
5
+ // Hold a reference so the custom-element registration isn't tree-shaken.
6
+ FoundationAiAssistant;
7
+ // GENC-1464 — the shared meter column in the settings modal's "AI Model
8
+ // Settings" section.
9
+ //
10
+ // The section body is a 4-unit flex column and the budget meter is a 2-unit
11
+ // one, so an unwrapped context indicator sat 4 units above the first vendor row
12
+ // while the vendor rows sat 2 apart — three visually identical meter rows
13
+ // reading as an uneven 4/2/2. Both meters now share ONE 2-unit column
14
+ // (`.settings-usage-meters`, grouped with `.settings-budget-usage` in the
15
+ // styles — see settings-modal.styles.test.ts for the gap itself).
16
+ //
17
+ // The wrapper must also be ABSENT when neither meter shows: a permanent wrapper
18
+ // is a permanent flex child of the section body, so a host with no meters would
19
+ // pay a stray 4-unit gap after its model slot for a div that renders nothing.
20
+ // That is a structural claim about the template, so this suite renders the
21
+ // template rather than reading a getter — the getter cannot show whether the
22
+ // `when` was wired to it.
23
+ //
24
+ // The element is created but never CONNECTED (`document.createElement` upgrades
25
+ // only), for the reason budget-meter.test.ts gives: connecting subscribes to
26
+ // `agenticActivityBus` and leaves the runner's event loop open.
27
+ const Suite = createLogicSuite('settings-modal usage meters structure');
28
+ const template = settingsUsageMetersTemplate('test-progress');
29
+ let storeSeq = 0;
30
+ /** A fresh (unconnected) element wired to its own real session store. */
31
+ function element() {
32
+ const el = document.createElement('foundation-ai-assistant');
33
+ storeSeq += 1;
34
+ el._sessionRef = getSessionStore(`usage-meters-test-${storeSeq}`, false);
35
+ return el;
36
+ }
37
+ /** Renders the meters for `el` into a detached host and returns it. */
38
+ function render(el) {
39
+ const host = document.createElement('div');
40
+ template.render(el, host);
41
+ return host;
42
+ }
43
+ Suite.after(() => {
44
+ clearAllSessionStores();
45
+ });
46
+ Suite('both meters render inside one shared column', () => {
47
+ const el = element();
48
+ el.contextTokens = 1000;
49
+ el.contextLimit = 4000;
50
+ el.setVendorBudget('anthropic', { budgetUsd: 50, spentUsd: 45 });
51
+ const host = render(el);
52
+ const wrapper = host.querySelector('.settings-usage-meters');
53
+ assert.ok(wrapper, 'the shared column is rendered');
54
+ assert.ok(wrapper.querySelector('.settings-context-usage'), 'the context indicator is inside it');
55
+ assert.ok(wrapper.querySelector('.settings-budget-usage'), 'so is the budget meter');
56
+ // The rhythm the wrapper exists for: every meter row is a child of a 2-unit
57
+ // column, none of them of the section body's 4-unit one.
58
+ assert.is(host.querySelectorAll('.settings-usage-meters').length, 1, 'one column, not one per meter');
59
+ });
60
+ Suite('neither meter visible → no wrapper at all, not an empty one', () => {
61
+ // The stray-gap case. Nothing fed, no context figures: the section body must
62
+ // see no flex child from the meters.
63
+ const el = element();
64
+ const host = render(el);
65
+ assert.is(el.settingsContextUsageVisible, false, 'precondition: no context data');
66
+ assert.is(el.settingsBudgetUsageVisible, false, 'precondition: no budget data');
67
+ assert.is(host.querySelector('.settings-usage-meters'), null, 'no wrapper element');
68
+ assert.is(host.querySelectorAll('*').length, 0, 'and no element of any kind — comment markers only');
69
+ });
70
+ Suite('one meter alone still gets the shared column, with only its own child', () => {
71
+ // Both single-meter cases: the wrapper appears for either meter on its own,
72
+ // and the hidden sibling contributes NO element inside it (both gate with
73
+ // `when`, which emplaces comment markers, not empty divs — an empty div would
74
+ // be a second stray gap, inside the column this time).
75
+ const contextOnly = element();
76
+ contextOnly.contextTokens = 1000;
77
+ contextOnly.contextLimit = 4000;
78
+ const contextHost = render(contextOnly);
79
+ const contextWrapper = contextHost.querySelector('.settings-usage-meters');
80
+ assert.ok(contextWrapper, 'context alone still gets the column');
81
+ assert.is(contextWrapper.children.length, 1, 'the hidden budget meter adds no flex child');
82
+ assert.ok(contextWrapper.querySelector('.settings-context-usage'));
83
+ const budgetOnly = element();
84
+ budgetOnly.setVendorBudget('gemini', { budgetUsd: 50, spentUsd: 15 });
85
+ const budgetHost = render(budgetOnly);
86
+ const budgetWrapper = budgetHost.querySelector('.settings-usage-meters');
87
+ assert.ok(budgetWrapper, 'budget alone still gets the column');
88
+ assert.is(budgetWrapper.children.length, 1, 'the hidden context indicator adds no flex child');
89
+ assert.ok(budgetWrapper.querySelector('.settings-budget-usage'));
90
+ });
91
+ Suite.run();