@genesislcap/ai-assistant 15.4.1 → 15.6.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 (98) hide show
  1. package/dist/ai-assistant.api.json +571 -82
  2. package/dist/ai-assistant.d.ts +336 -36
  3. package/dist/chat-driver.cjs +104 -26
  4. package/dist/chat-driver.cjs.map +3 -3
  5. package/dist/chat-driver.mjs +104 -26
  6. package/dist/chat-driver.mjs.map +3 -3
  7. package/dist/custom-elements.json +314 -36
  8. package/dist/dts/components/chat-driver/chat-driver.d.ts +12 -0
  9. package/dist/dts/components/chat-driver/chat-driver.d.ts.map +1 -1
  10. package/dist/dts/components/settings-modal/settings-modal.styles.d.ts.map +1 -1
  11. package/dist/dts/components/settings-modal/settings-modal.template.d.ts +9 -2
  12. package/dist/dts/components/settings-modal/settings-modal.template.d.ts.map +1 -1
  13. package/dist/dts/index.d.ts +1 -0
  14. package/dist/dts/index.d.ts.map +1 -1
  15. package/dist/dts/main/cost-session-banking.test.d.ts +2 -0
  16. package/dist/dts/main/cost-session-banking.test.d.ts.map +1 -0
  17. package/dist/dts/main/main.d.ts +184 -24
  18. package/dist/dts/main/main.d.ts.map +1 -1
  19. package/dist/dts/provider/assistant-app-settings.d.ts +30 -5
  20. package/dist/dts/provider/assistant-app-settings.d.ts.map +1 -1
  21. package/dist/dts/state/ai-assistant-slice.d.ts +11 -8
  22. package/dist/dts/state/ai-assistant-slice.d.ts.map +1 -1
  23. package/dist/dts/state/persistence/session-persistence-provider.d.ts +23 -0
  24. package/dist/dts/state/persistence/session-persistence-provider.d.ts.map +1 -1
  25. package/dist/dts/state/persistence/session-snapshot.d.ts.map +1 -1
  26. package/dist/dts/state/session-store.d.ts +1 -2
  27. package/dist/dts/state/session-store.d.ts.map +1 -1
  28. package/dist/dts/styles/settings-section.d.ts +29 -0
  29. package/dist/dts/styles/settings-section.d.ts.map +1 -0
  30. package/dist/dts/utils/condense-history.d.ts +14 -0
  31. package/dist/dts/utils/condense-history.d.ts.map +1 -1
  32. package/dist/dts/utils/cost-session-history.d.ts +103 -12
  33. package/dist/dts/utils/cost-session-history.d.ts.map +1 -1
  34. package/dist/dts/utils/resolve-cost-history-config.d.ts +9 -3
  35. package/dist/dts/utils/resolve-cost-history-config.d.ts.map +1 -1
  36. package/dist/dts/utils/sum-costs.d.ts.map +1 -1
  37. package/dist/dts/utils/sum-tokens.d.ts +8 -8
  38. package/dist/dts/utils/sum-tokens.d.ts.map +1 -1
  39. package/dist/dts/utils/sum-usage.d.ts +59 -0
  40. package/dist/dts/utils/sum-usage.d.ts.map +1 -0
  41. package/dist/dts/utils/sum-usage.test.d.ts +2 -0
  42. package/dist/dts/utils/sum-usage.test.d.ts.map +1 -0
  43. package/dist/esm/components/chat-driver/chat-driver.js +9 -1
  44. package/dist/esm/components/settings-modal/settings-modal.styles.js +237 -18
  45. package/dist/esm/components/settings-modal/settings-modal.template.js +229 -73
  46. package/dist/esm/index.js +1 -0
  47. package/dist/esm/main/cost-session-banking.test.js +308 -0
  48. package/dist/esm/main/main.js +424 -71
  49. package/dist/esm/state/ai-assistant-slice.js +11 -8
  50. package/dist/esm/state/ai-assistant-slice.test.js +12 -5
  51. package/dist/esm/state/debug-event-log.js +2 -2
  52. package/dist/esm/state/persistence/session-persistence.integration.test.js +5 -1
  53. package/dist/esm/state/persistence/session-persister.js +2 -2
  54. package/dist/esm/state/persistence/session-persister.test.js +10 -1
  55. package/dist/esm/state/persistence/session-snapshot.js +6 -2
  56. package/dist/esm/state/persistence/session-snapshot.test.js +4 -1
  57. package/dist/esm/state/persistence/stateful-restore.e2e.test.js +10 -1
  58. package/dist/esm/styles/settings-section.js +39 -0
  59. package/dist/esm/utils/condense-history.js +18 -5
  60. package/dist/esm/utils/condense-history.test.js +67 -0
  61. package/dist/esm/utils/cost-session-history.js +92 -15
  62. package/dist/esm/utils/cost-session-history.test.js +155 -13
  63. package/dist/esm/utils/resolve-cost-history-config.js +2 -1
  64. package/dist/esm/utils/sum-costs.js +2 -13
  65. package/dist/esm/utils/sum-tokens.js +10 -27
  66. package/dist/esm/utils/sum-tokens.test.js +1 -5
  67. package/dist/esm/utils/sum-usage.js +123 -0
  68. package/dist/esm/utils/sum-usage.test.js +120 -0
  69. package/dist/tsconfig.tsbuildinfo +1 -1
  70. package/package.json +17 -17
  71. package/src/components/chat-driver/chat-driver.ts +21 -0
  72. package/src/components/settings-modal/settings-modal.styles.ts +237 -18
  73. package/src/components/settings-modal/settings-modal.template.ts +270 -81
  74. package/src/index.ts +1 -0
  75. package/src/main/cost-session-banking.test.ts +407 -0
  76. package/src/main/main.ts +433 -68
  77. package/src/provider/assistant-app-settings.ts +31 -5
  78. package/src/state/ai-assistant-slice.test.ts +12 -5
  79. package/src/state/ai-assistant-slice.ts +21 -13
  80. package/src/state/debug-event-log.ts +2 -2
  81. package/src/state/persistence/session-persistence-provider.ts +24 -0
  82. package/src/state/persistence/session-persistence.integration.test.ts +8 -1
  83. package/src/state/persistence/session-persister.test.ts +10 -1
  84. package/src/state/persistence/session-persister.ts +2 -2
  85. package/src/state/persistence/session-snapshot.test.ts +4 -1
  86. package/src/state/persistence/session-snapshot.ts +5 -1
  87. package/src/state/persistence/stateful-restore.e2e.test.ts +9 -1
  88. package/src/styles/settings-section.ts +40 -0
  89. package/src/utils/condense-history.test.ts +103 -0
  90. package/src/utils/condense-history.ts +33 -3
  91. package/src/utils/cost-session-history.test.ts +187 -16
  92. package/src/utils/cost-session-history.ts +142 -23
  93. package/src/utils/resolve-cost-history-config.ts +10 -3
  94. package/src/utils/sum-costs.ts +2 -9
  95. package/src/utils/sum-tokens.test.ts +1 -11
  96. package/src/utils/sum-tokens.ts +10 -26
  97. package/src/utils/sum-usage.test.ts +140 -0
  98. package/src/utils/sum-usage.ts +130 -0
@@ -1,4 +1,4 @@
1
- import type { AIProviderRegistryStatusEntry } from '@genesislcap/foundation-ai';
1
+ import type { AggregateUsage } from '@genesislcap/foundation-ai';
2
2
  import { html, ref, repeat, slotted, when, type ViewTemplate } from '@genesislcap/web-core';
3
3
  import type { FoundationAiAssistant } from '../../main/main';
4
4
  import { ANIMATION_DEFS, type AiAssistantAnimation } from '../../main/main.types';
@@ -8,7 +8,7 @@ import {
8
8
  type CostSessionModelEntry,
9
9
  type CostSessionRecord,
10
10
  } from '../../utils/cost-session-history';
11
- import { formatCompactTokenCountPlain, formatCompactTokenCountSafe } from '../../utils/sum-tokens';
11
+ import { formatCompactTokenCountSafe } from '../../utils/sum-tokens';
12
12
 
13
13
  const SESSION_COST_DECIMALS = 2;
14
14
 
@@ -114,18 +114,41 @@ const animationsPickerTemplate = (
114
114
  </div>
115
115
  `;
116
116
 
117
- /** Settings vs Cost tab identifiers for the modal tab bar. */
117
+ /**
118
+ * Tab identifiers for the modal tab bar.
119
+ *
120
+ * The values are deliberately NOT renamed to match the visible labels ("Configuration" and
121
+ * "Usage"): they are the public API that hosts read and write via `settingsModalTab`, and are
122
+ * matched by the `settings-modal-tab-*` shadow parts. Renaming a label is cosmetic; renaming
123
+ * these would break consumers.
124
+ */
118
125
  export type SettingsModalTab = 'settings' | 'cost';
119
126
 
127
+ /**
128
+ * How a settings row places its control.
129
+ *
130
+ * `row` top-aligns it, which is what a switch wants — it then sits level with the title rather
131
+ * than floating beside a two-line description. `row-centered` is for a taller control such as a
132
+ * button, which top-aligned reads as hanging above the text it belongs to. `column` stacks the
133
+ * control under the text, for controls that need the full width.
134
+ */
135
+ type SettingsRowLayout = 'row' | 'row-centered' | 'column';
136
+
137
+ const SETTINGS_ROW_LAYOUT_CLASS: Record<SettingsRowLayout, string> = {
138
+ row: '',
139
+ 'row-centered': 'is-centered',
140
+ column: 'is-stacked',
141
+ };
142
+
120
143
  /** Shared label + control row used for switches, buttons, and multiselects. */
121
144
  const settingsToggleRow = (
122
145
  title: string,
123
146
  description: string,
124
147
  part: string,
125
148
  control: ViewTemplate<FoundationAiAssistant>,
126
- layout: 'row' | 'column' = 'row',
149
+ layout: SettingsRowLayout = 'row',
127
150
  ) => html<FoundationAiAssistant>`
128
- <div class="settings-toggle-row ${layout === 'column' ? 'is-stacked' : ''}" part="${part}-row">
151
+ <div class="settings-toggle-row ${SETTINGS_ROW_LAYOUT_CLASS[layout]}" part="${part}-row">
129
152
  <div class="settings-toggle-text">
130
153
  <div class="settings-toggle-title">${title}</div>
131
154
  <div class="settings-toggle-desc">${description}</div>
@@ -265,7 +288,34 @@ const chatBotSettingsTemplate = (
265
288
 
266
289
  const appSettingsTemplate = (
267
290
  switchTag: string,
291
+ iconTag: string,
268
292
  ): ViewTemplate<FoundationAiAssistant> => html<FoundationAiAssistant>`
293
+ <!--
294
+ Slot FIRST, ahead of the provider toggles. The assistant renders no heading for this
295
+ section, so whatever the host slots in has to be able to title it — and a heading below the
296
+ rows it names would title nothing. Anything the host wants under the toggles it can render
297
+ below them within its own slotted block.
298
+ -->
299
+ <slot name="settings-app" ${slotted('settingsAppSlotted')}></slot>
300
+ <!--
301
+ Sub-heading for the toggle group, rendered only when the host asks for one — both the text
302
+ and the icon come from its provider. It exists so a host whose slotted content covers other
303
+ ground can still mark these rows off from it.
304
+ -->
305
+ ${when(
306
+ (x) => !!x.appSettingsTogglesHeading,
307
+ html<FoundationAiAssistant>`
308
+ <h4 class="ai-settings-section-title settings-app-toggles-title" part="app-toggles-title">
309
+ ${when(
310
+ (x) => !!x.appSettingsTogglesHeading?.icon,
311
+ html<FoundationAiAssistant>`
312
+ <${iconTag} name="${(x) => x.appSettingsTogglesHeading?.icon}"></${iconTag}>
313
+ `,
314
+ )}
315
+ ${(x) => x.appSettingsTogglesHeading?.title}
316
+ </h4>
317
+ `,
318
+ )}
269
319
  ${repeat(
270
320
  (x) => x.visibleAppSettingsToggles,
271
321
  html<AssistantAppSettingsToggle, FoundationAiAssistant>`
@@ -298,9 +348,142 @@ const appSettingsTemplate = (
298
348
  </div>
299
349
  `,
300
350
  )}
301
- <slot name="settings-app" ${slotted('settingsAppSlotted')}></slot>
302
351
  `;
303
352
 
353
+ /**
354
+ * One model chip. Shared by the live session and the history rows so the two can't drift — both
355
+ * already reduce to a distinct-model list, the live one via `uniqueProviderModels` and a row via
356
+ * `collectSessionModels`.
357
+ */
358
+ const modelTagTemplate = html<CostSessionModelEntry>`
359
+ <span class="settings-model-tag vendor-${(entry) => entry.provider || 'generic'}">
360
+ ${(entry) => entry.model}
361
+ </span>
362
+ `;
363
+
364
+ /**
365
+ * The four billing buckets for one `AggregateUsage`, wherever it comes from — the lifetime
366
+ * total, the live session, or a history row.
367
+ *
368
+ * Why four numbers instead of one: each bucket bills at a different rate, so a single token
369
+ * total can't explain a session that processed millions of tokens for pennies. Uncached input,
370
+ * cache read and cache write partition the prompt (they sum to it, never add on top), and
371
+ * output is what the model generated.
372
+ *
373
+ * Ordered input, output, cache read, cache write to match how Claude Code reports usage, so the
374
+ * two can be read side by side — comparing against it is what this breakdown exists for. Note
375
+ * this puts output between the three buckets that partition the prompt, so their relationship is
376
+ * carried by the tooltips rather than by adjacency.
377
+ *
378
+ * `partPrefix` keeps the shadow-part names caller-specific (`session-tokens-input`,
379
+ * `lifetime-tokens-input`, …) so a host can style one context without hitting the others.
380
+ */
381
+ const tokenBreakdownTemplate = <T>(
382
+ usageOf: (x: T) => AggregateUsage,
383
+ partPrefix: string,
384
+ ): ViewTemplate<T> => html<T>`
385
+ <div class="settings-token-breakdown" part="${partPrefix}-token-breakdown">
386
+ <!--
387
+ Titles the box. Nothing in it otherwise said "tokens": the four bucket names are the
388
+ token TYPES, not the unit, so the panel read as four unexplained numbers unless the
389
+ reader found the per-bucket tooltips.
390
+ -->
391
+ <div class="settings-token-breakdown-label">Tokens</div>
392
+ <div class="settings-token-buckets">
393
+ <div class="settings-token-bucket" part="${partPrefix}-tokens-input">
394
+ <span
395
+ class="settings-token-bucket-label"
396
+ title="Prompt tokens billed at the full uncached rate"
397
+ >
398
+ Input
399
+ </span>
400
+ <span
401
+ class="settings-token-bucket-value"
402
+ :innerHTML="${(x: T) => formatCompactTokenCountSafe(usageOf(x).uncachedInputTokens)}"
403
+ ></span>
404
+ </div>
405
+ <div class="settings-token-bucket" part="${partPrefix}-tokens-output">
406
+ <span
407
+ class="settings-token-bucket-label"
408
+ title="Tokens the model generated, including reasoning tokens billed as output"
409
+ >
410
+ Output
411
+ </span>
412
+ <span
413
+ class="settings-token-bucket-value"
414
+ :innerHTML="${(x: T) => formatCompactTokenCountSafe(usageOf(x).outputTokens)}"
415
+ ></span>
416
+ </div>
417
+ <div class="settings-token-bucket is-discounted" part="${partPrefix}-tokens-cache-read">
418
+ <span
419
+ class="settings-token-bucket-label"
420
+ title="Prompt tokens served from the provider cache — billed at a fraction of the uncached rate"
421
+ >
422
+ Cache read
423
+ </span>
424
+ <span
425
+ class="settings-token-bucket-value"
426
+ :innerHTML="${(x: T) => formatCompactTokenCountSafe(usageOf(x).cacheReadTokens)}"
427
+ ></span>
428
+ </div>
429
+ <div class="settings-token-bucket" part="${partPrefix}-tokens-cache-write">
430
+ <span
431
+ class="settings-token-bucket-label"
432
+ title="Prompt tokens written to the provider cache — billed at a premium over the uncached rate"
433
+ >
434
+ Cache write
435
+ </span>
436
+ <span
437
+ class="settings-token-bucket-value"
438
+ :innerHTML="${(x: T) => formatCompactTokenCountSafe(usageOf(x).cacheWriteTokens)}"
439
+ ></span>
440
+ </div>
441
+ </div>
442
+ </div>
443
+ `;
444
+
445
+ /**
446
+ * Lifetime usage across every project in the history scope.
447
+ *
448
+ * Sits above the live session block because it is the wider figure. It used to live inside
449
+ * that block as a lone "Total build cost", where the live badge and project name directly
450
+ * above it implied the total belonged to the open project.
451
+ */
452
+ const lifetimeUsageTemplate =
453
+ (): ViewTemplate<FoundationAiAssistant> => html<FoundationAiAssistant>`
454
+ ${when(
455
+ (x) => x.settingsCostLifetimeVisible,
456
+ html<FoundationAiAssistant>`
457
+ <div class="settings-lifetime-usage" part="lifetime-usage">
458
+ <div class="settings-lifetime-usage-top">
459
+ <span class="settings-lifetime-label">Lifetime usage</span>
460
+ </div>
461
+ <div class="settings-usage-row">
462
+ <div
463
+ class="settings-build-stat"
464
+ part="lifetime-cost"
465
+ ?hidden=${(x) => !x.resolvedCostHistory.showCost}
466
+ >
467
+ <div class="settings-build-stat-label">Total build cost</div>
468
+ <div class="settings-build-stat-value">
469
+ <span class="prefix">$</span>
470
+ <span class="value">
471
+ ${(x) => x.cumulativeCostUsd.toFixed(SESSION_COST_DECIMALS)}
472
+ </span>
473
+ </div>
474
+ </div>
475
+ <div
476
+ class="settings-usage-row-tokens"
477
+ ?hidden=${(x) => !x.resolvedCostHistory.showTokens}
478
+ >
479
+ ${tokenBreakdownTemplate<FoundationAiAssistant>((x) => x.cumulativeUsage, 'lifetime')}
480
+ </div>
481
+ </div>
482
+ </div>
483
+ `,
484
+ )}
485
+ `;
486
+
304
487
  const sessionCostSummaryTemplate =
305
488
  (): ViewTemplate<FoundationAiAssistant> => html<FoundationAiAssistant>`
306
489
  ${when(
@@ -319,58 +502,38 @@ const sessionCostSummaryTemplate =
319
502
  `,
320
503
  )}
321
504
  </div>
322
- <div class="settings-current-build-stats">
505
+ <div class="settings-usage-row">
323
506
  <div
324
507
  class="settings-build-stat"
325
508
  part="session-cost"
326
509
  ?hidden=${(x) => !x.resolvedCostHistory.showCost}
327
510
  >
328
- <div class="settings-build-stat-label">Current session cost</div>
511
+ <!--
512
+ Label states the scope rather than implying one. These figures are whatever
513
+ the open transcript proves, so they are the project's whole history exactly
514
+ when that transcript is restored on load, and only this page-load's when it
515
+ isn't. Same number, two meanings — so the caller can't be left guessing.
516
+ -->
517
+ <div class="settings-build-stat-label">
518
+ ${(x) => (x.liveUsageIsProjectLifetime ? 'Project cost' : 'Session cost')}
519
+ </div>
329
520
  <div class="settings-build-stat-value">
330
521
  <span class="prefix">$</span>
331
522
  <span class="value">${(x) => x.sessionCostUsd.toFixed(SESSION_COST_DECIMALS)}</span>
332
523
  </div>
333
524
  </div>
334
525
  <div
335
- class="settings-build-stat"
336
- part="session-tokens"
526
+ class="settings-usage-row-tokens"
337
527
  ?hidden=${(x) => !x.resolvedCostHistory.showTokens}
338
528
  >
339
- <div class="settings-build-stat-label">Tokens consumed</div>
340
- <div
341
- class="settings-build-stat-value"
342
- :innerHTML="${(x) => formatCompactTokenCountSafe(x.sessionTokensConsumed)}"
343
- ></div>
344
- </div>
345
- <div
346
- class="settings-build-stat"
347
- part="session-cumulative-cost"
348
- ?hidden=${(x) => !x.resolvedCostHistory.showCost}
349
- >
350
- <div class="settings-build-stat-label">Total build cost</div>
351
- <div class="settings-build-stat-value">
352
- <span class="prefix">$</span>
353
- <span class="value">
354
- ${(x) => x.cumulativeCostUsd.toFixed(SESSION_COST_DECIMALS)}
355
- </span>
356
- </div>
529
+ ${tokenBreakdownTemplate<FoundationAiAssistant>((x) => x.sessionUsage, 'session')}
357
530
  </div>
358
531
  </div>
359
532
  ${when(
360
- (x) => x.chatConfig.ui?.showActiveModel !== false && x.providerStatuses.length > 0,
533
+ (x) => x.chatConfig.ui?.showActiveModel !== false && x.uniqueProviderModels.length > 0,
361
534
  html<FoundationAiAssistant>`
362
535
  <div class="settings-model-tags" part="session-model-tags">
363
- ${repeat(
364
- (x) => x.providerStatuses,
365
- html<AIProviderRegistryStatusEntry>`
366
- <span
367
- class="settings-model-tag vendor-${(entry) =>
368
- entry.status?.provider || 'generic'}"
369
- >
370
- ${(entry) => entry.status?.model ?? entry.name}
371
- </span>
372
- `,
373
- )}
536
+ ${repeat((x) => x.uniqueProviderModels, modelTagTemplate)}
374
537
  </div>
375
538
  `,
376
539
  )}
@@ -384,7 +547,7 @@ const costSessionHistoryTemplate = (
384
547
  buttonTag: string,
385
548
  ): ViewTemplate<FoundationAiAssistant> => html<FoundationAiAssistant>`
386
549
  ${repeat(
387
- (x) => x.costSessionHistory,
550
+ (x) => x.sortedCostSessionHistory,
388
551
  html<CostSessionRecord>`
389
552
  <div class="settings-cost-history-row" part="cost-history-row">
390
553
  <div class="settings-cost-history-row-content">
@@ -397,29 +560,46 @@ const costSessionHistoryTemplate = (
397
560
  ${(record) => record.title}
398
561
  </div>
399
562
  <div class="settings-cost-history-row-date">
400
- ${(record) => formatCostSessionDate(record.endedAt)}
563
+ ${(record) => formatCostSessionDate(record.updatedAt)}
401
564
  </div>
402
565
  </div>
566
+ <!--
567
+ Cost and the delete control live together so they centre against each other.
568
+ The button used to sit outside the row body, held in place by a negative margin
569
+ tuned for when this column had a second line (a merged token total); that line
570
+ is gone, and a nudge measured against a removed sibling is exactly the kind that
571
+ rots silently.
572
+ -->
403
573
  <div class="settings-cost-history-row-right">
404
574
  <div class="settings-cost-history-row-metrics">
405
575
  <div class="settings-cost-history-row-cost">
406
- $${(record) => record.costUsd.toFixed(SESSION_COST_DECIMALS)}
407
- </div>
408
- <div class="settings-cost-history-row-tokens">
409
- ${(record) => formatCompactTokenCountPlain(record.tokensConsumed)} tokens
576
+ $${(record) => record.usage.costUsd.toFixed(SESSION_COST_DECIMALS)}
410
577
  </div>
411
578
  </div>
579
+ <${buttonTag}
580
+ class="settings-cost-history-delete"
581
+ part="cost-history-delete"
582
+ appearance="stealth"
583
+ aria-label="Remove from build history"
584
+ @click=${(record, c) => c.parent.showConfirmationDialog('Are you sure you want to remove this build from history?', c.parent.settingsModal, () => c.parent.removeCostSessionFromHistory(record.id))}
585
+ >
586
+ <${iconTag} name="trash"></${iconTag}>
587
+ </${buttonTag}>
412
588
  </div>
413
589
  </div>
414
- <${buttonTag}
415
- class="settings-cost-history-delete"
416
- part="cost-history-delete"
417
- appearance="stealth"
418
- aria-label="Remove from build history"
419
- @click=${(record, c) => c.parent.showConfirmationDialog('Are you sure you want to remove this build from history?', c.parent.settingsModal, () => c.parent.removeCostSessionFromHistory(record.id))}
420
- >
421
- <${iconTag} name="trash"></${iconTag}>
422
- </${buttonTag}>
590
+ </div>
591
+ <!--
592
+ The same four buckets as the live block, on its own line below the row rather than
593
+ beside the cost — the row's own width is spoken for by title, cost and delete.
594
+ Replaces a merged total that could not explain a huge token count at a small cost
595
+ without hovering for a tooltip.
596
+ -->
597
+ <div
598
+ class="settings-cost-history-row-content settings-cost-history-row-breakdown"
599
+ ?hidden=${(record, c) =>
600
+ !(c.parent as FoundationAiAssistant).resolvedCostHistory.showTokens}
601
+ >
602
+ ${tokenBreakdownTemplate<CostSessionRecord>((record) => record.usage, 'cost-history-row')}
423
603
  </div>
424
604
  ${when(
425
605
  (record) => record.models.length > 0,
@@ -427,14 +607,7 @@ const costSessionHistoryTemplate = (
427
607
  <div
428
608
  class="settings-cost-history-row-content settings-model-tags settings-cost-history-model-tags"
429
609
  >
430
- ${repeat(
431
- (record) => record.models,
432
- html<CostSessionModelEntry>`
433
- <span class="settings-model-tag vendor-${(model) => model.provider || 'generic'}">
434
- ${(model) => model.model}
435
- </span>
436
- `,
437
- )}
610
+ ${repeat((record) => record.models, modelTagTemplate)}
438
611
  </div>
439
612
  `,
440
613
  )}
@@ -444,7 +617,7 @@ const costSessionHistoryTemplate = (
444
617
  `;
445
618
 
446
619
  /**
447
- * Modal shell for assistant settings — Settings and Cost tabs, section slots for
620
+ * Modal shell for assistant settings — Configuration and Usage tabs, section slots for
448
621
  * app-specific UI, and built-in chat-bot toggles.
449
622
  */
450
623
  export const SettingsModalTemplate = (
@@ -471,7 +644,7 @@ export const SettingsModalTemplate = (
471
644
  part="settings-section-model"
472
645
  ?hidden=${(x) => !x.settingsModelSectionVisible}
473
646
  >
474
- <h3 class="settings-modal-section-title">
647
+ <h3 class="ai-settings-section-title">
475
648
  <${iconTag} name="microchip"></${iconTag}> AI Model Settings
476
649
  </h3>
477
650
  <div class="settings-modal-section-body">
@@ -479,18 +652,11 @@ export const SettingsModalTemplate = (
479
652
  ${platformContextUsageTemplate(progressTag)}
480
653
  </div>
481
654
  </section>
482
- <section
483
- class="settings-modal-section"
484
- part="settings-section-app"
485
- ?hidden=${(x) => !x.settingsAppSectionVisible}
486
- >
487
- <h3 class="settings-modal-section-title">
488
- <${iconTag} name="home"></${iconTag}> ${(x) => x.appSettingsSectionTitle}
489
- </h3>
490
- <div class="settings-modal-section-body">
491
- ${appSettingsTemplate(switchTag)}
492
- </div>
493
- </section>
655
+ <!--
656
+ Above the host section, deliberately. This is the assistant's own control, so it belongs
657
+ with the assistant's other sections rather than trailing the host's — where, now that the
658
+ host section carries no heading of its own, it read as one more of the host's settings.
659
+ -->
494
660
  ${when(
495
661
  (x) => x.chatConfig.ui?.allowDebugDownload === true,
496
662
  html<FoundationAiAssistant>`
@@ -520,13 +686,29 @@ export const SettingsModalTemplate = (
520
686
  ${(x) => (x.downloadingDebugLog ? 'Downloading…' : 'Download')}
521
687
  </${buttonTag}>
522
688
  `,
689
+ 'row-centered',
523
690
  )}
524
691
  </div>
525
692
  </section>
526
693
  `,
527
694
  )}
695
+ <!--
696
+ No heading here, by design. This section belongs to the host: only it knows what its
697
+ settings are called, so the assistant supplies neither text nor icon and the host titles
698
+ the section from the slot. See assistantSettingsSectionTitleStyles for the heading style
699
+ it can apply to match the sections above and below.
700
+ -->
701
+ <section
702
+ class="settings-modal-section"
703
+ part="settings-section-app"
704
+ ?hidden=${(x) => !x.settingsAppSectionVisible}
705
+ >
706
+ <div class="settings-modal-section-body">
707
+ ${appSettingsTemplate(switchTag, iconTag)}
708
+ </div>
709
+ </section>
528
710
  <section class="settings-modal-section" part="settings-section-chat">
529
- <h3 class="settings-modal-section-title">
711
+ <h3 class="ai-settings-section-title">
530
712
  <${iconTag} name="comment"></${iconTag}> AI Chat Bot Settings
531
713
  </h3>
532
714
  <p class="settings-modal-section-note">These options only control what you see in the chat window and do not control model behaviour.</p>
@@ -545,6 +727,7 @@ export const SettingsModalTemplate = (
545
727
  part="cost-tab-panel"
546
728
  >
547
729
  <div class="settings-modal-body" part="settings-modal-body">
730
+ ${lifetimeUsageTemplate()}
548
731
  ${sessionCostSummaryTemplate()}
549
732
  <div
550
733
  class="settings-cost-current"
@@ -595,7 +778,7 @@ export const SettingsModalTemplate = (
595
778
  appearance="secondary"
596
779
  class="settings-modal-tab"
597
780
  part="settings-modal-tab-settings"
598
- ><${iconTag} name="cog"></${iconTag}> AI Assistant</${tabTag}>${when(
781
+ ><${iconTag} name="sliders"></${iconTag}> Configuration</${tabTag}>${when(
599
782
  (x) => x.settingsCostTabVisible,
600
783
  html<FoundationAiAssistant>`<${tabTag}
601
784
  slot="tab"
@@ -603,7 +786,7 @@ export const SettingsModalTemplate = (
603
786
  appearance="secondary"
604
787
  class="settings-modal-tab"
605
788
  part="settings-modal-tab-cost"
606
- ><${iconTag} name="dollar-sign"></${iconTag}> Cost</${tabTag}>`,
789
+ ><${iconTag} name="chart-simple"></${iconTag}> Usage</${tabTag}>`,
607
790
  )}${settingsTabPanel}${when((x) => x.settingsCostTabVisible, costTabPanel)}</${tabsTag}>
608
791
  </div>
609
792
  <div slot="bottom" class="settings-modal-footer" part="settings-modal-footer">
@@ -612,7 +795,13 @@ export const SettingsModalTemplate = (
612
795
  (x) => x.settingsModalTab === 'cost',
613
796
  html<FoundationAiAssistant>`
614
797
  <span class="settings-disclaimer">
615
- Costs are estimated from token usage at list price.
798
+ ${when(
799
+ (x) => !!x.resolvedCostHistory.summaryHint,
800
+ html<FoundationAiAssistant>`
801
+ ${(x) => x.resolvedCostHistory.summaryHint}
802
+ `,
803
+ )}Costs
804
+ are estimated from token usage at list price.
616
805
  </span>
617
806
  `,
618
807
  )}
package/src/index.ts CHANGED
@@ -16,6 +16,7 @@ export * from './state/persistence';
16
16
  export * from './provider/ai-provider-switcher';
17
17
  export * from './provider/assistant-app-settings';
18
18
  export * from './utils/tool-fold';
19
+ export { assistantSettingsSectionTitleStyles } from './styles/settings-section';
19
20
  export type { TurnFailureReason } from './state/debug-event-log';
20
21
  export type { TimelineMessage } from './utils/flatten-sub-agent-messages';
21
22
  export type { CostSessionModelEntry, CostSessionRecord } from './utils/cost-session-history';