@genesislcap/ai-assistant 15.10.6 → 15.11.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.
- package/dist/ai-assistant.api.json +91 -0
- package/dist/ai-assistant.d.ts +25 -0
- package/dist/custom-elements.json +1722 -1688
- package/dist/dts/components/settings-modal/settings-modal.styles.d.ts.map +1 -1
- package/dist/dts/components/settings-modal/settings-modal.template.d.ts +1 -1
- package/dist/dts/components/settings-modal/settings-modal.template.d.ts.map +1 -1
- package/dist/dts/main/main.d.ts +25 -0
- package/dist/dts/main/main.d.ts.map +1 -1
- package/dist/dts/react.d.ts +9 -8
- package/dist/esm/components/settings-modal/settings-modal.styles.js +22 -0
- package/dist/esm/components/settings-modal/settings-modal.template.js +25 -4
- package/dist/esm/components/settings-modal/settings-modal.template.test.js +1 -1
- package/dist/esm/main/blocked-state.test.js +1 -0
- package/dist/esm/main/budget-meter.test.js +42 -0
- package/dist/esm/main/main.js +49 -0
- package/dist/react.cjs +11 -6
- package/dist/react.mjs +10 -5
- package/package.json +17 -17
- package/src/components/settings-modal/settings-modal.styles.ts +22 -0
- package/src/components/settings-modal/settings-modal.template.test.ts +1 -1
- package/src/components/settings-modal/settings-modal.template.ts +27 -2
- package/src/main/blocked-state.test.ts +1 -0
- package/src/main/budget-meter.test.ts +50 -0
- package/src/main/main.ts +47 -0
|
@@ -217,6 +217,8 @@ const platformContextUsageTemplate = (
|
|
|
217
217
|
*/
|
|
218
218
|
const platformBudgetUsageTemplate = (
|
|
219
219
|
progressTag: string,
|
|
220
|
+
buttonTag: string,
|
|
221
|
+
iconTag: string,
|
|
220
222
|
): ViewTemplate<FoundationAiAssistant> => html<FoundationAiAssistant>`
|
|
221
223
|
${when(
|
|
222
224
|
(x) => x.settingsBudgetUsageVisible,
|
|
@@ -237,6 +239,26 @@ const platformBudgetUsageTemplate = (
|
|
|
237
239
|
</div>
|
|
238
240
|
`,
|
|
239
241
|
)}
|
|
242
|
+
<!--
|
|
243
|
+
Freshness footer (GENC-1464): the rows above update per turn and at
|
|
244
|
+
the wall, which reads as "stale" to anyone watching the modal — the
|
|
245
|
+
label says when the figures arrived, the button asks the host for new
|
|
246
|
+
ones (see FoundationAiAssistant.requestBudgetRefresh). Below the rows
|
|
247
|
+
and left-aligned, reading as a footnote to the figures it describes.
|
|
248
|
+
-->
|
|
249
|
+
<div class="settings-budget-toolbar" part="budget-toolbar">
|
|
250
|
+
<${buttonTag}
|
|
251
|
+
appearance="stealth"
|
|
252
|
+
class="settings-budget-refresh"
|
|
253
|
+
part="budget-refresh"
|
|
254
|
+
title="Refresh budget figures"
|
|
255
|
+
aria-label="Refresh budget figures"
|
|
256
|
+
@click=${(x: FoundationAiAssistant) => x.requestBudgetRefresh()}
|
|
257
|
+
>
|
|
258
|
+
<${iconTag} name="arrows-rotate"></${iconTag}>
|
|
259
|
+
</${buttonTag}>
|
|
260
|
+
<span class="settings-budget-updated">${(x) => x.settingsBudgetUpdatedLabel}</span>
|
|
261
|
+
</div>
|
|
240
262
|
</div>
|
|
241
263
|
`,
|
|
242
264
|
)}
|
|
@@ -265,12 +287,15 @@ const platformBudgetUsageTemplate = (
|
|
|
265
287
|
*/
|
|
266
288
|
export const settingsUsageMetersTemplate = (
|
|
267
289
|
progressTag: string,
|
|
290
|
+
buttonTag: string,
|
|
291
|
+
iconTag: string,
|
|
268
292
|
): ViewTemplate<FoundationAiAssistant> => html<FoundationAiAssistant>`
|
|
269
293
|
${when(
|
|
270
294
|
(x) => x.settingsContextUsageVisible || x.settingsBudgetUsageVisible,
|
|
271
295
|
html<FoundationAiAssistant>`
|
|
272
296
|
<div class="settings-usage-meters">
|
|
273
|
-
${platformContextUsageTemplate(progressTag)}
|
|
297
|
+
${platformContextUsageTemplate(progressTag)}
|
|
298
|
+
${platformBudgetUsageTemplate(progressTag, buttonTag, iconTag)}
|
|
274
299
|
</div>
|
|
275
300
|
`,
|
|
276
301
|
)}
|
|
@@ -735,7 +760,7 @@ export const SettingsModalTemplate = (
|
|
|
735
760
|
</h3>
|
|
736
761
|
<div class="settings-modal-section-body">
|
|
737
762
|
<slot name="settings-model" ${slotted('settingsModelSlotted')}></slot>
|
|
738
|
-
${settingsUsageMetersTemplate(progressTag)}
|
|
763
|
+
${settingsUsageMetersTemplate(progressTag, buttonTag, iconTag)}
|
|
739
764
|
</div>
|
|
740
765
|
</section>
|
|
741
766
|
<!--
|
|
@@ -325,6 +325,7 @@ Suite('a wall with figures snaps the vendor meter at latch time', () => {
|
|
|
325
325
|
reachable(el, 'anthropic', 'gemini');
|
|
326
326
|
latch(el, 'budget-exhausted', { vendorLabel: 'Anthropic', budgetUsd: 20, spentUsd: 20.1 });
|
|
327
327
|
assert.equal(storedBudgets(el)['anthropic'], { budgetUsd: 20, spentUsd: 20.1 });
|
|
328
|
+
assert.ok(typeof el.budgetMeterLastFedAt === 'number', 'the wall feed stamps freshness too');
|
|
328
329
|
|
|
329
330
|
// A later 402 for the same (already-walled) vendor carries fresher spend —
|
|
330
331
|
// the feed is deliberately outside the idempotence guard, like the sweep.
|
|
@@ -424,4 +424,54 @@ Suite('figures alone are enough to surface the AI Model Settings section', () =>
|
|
|
424
424
|
assert.is(el.settingsModelSectionVisible, false, 'and the config flag takes it back out');
|
|
425
425
|
});
|
|
426
426
|
|
|
427
|
+
// ── Freshness stamp + refresh request (GENC-1464 refresh affordance) ─────────
|
|
428
|
+
|
|
429
|
+
Suite('every feed stamps budgetMeterLastFedAt — identical figures included', () => {
|
|
430
|
+
const el = element();
|
|
431
|
+
assert.is(el.budgetMeterLastFedAt, null, 'null before any feed');
|
|
432
|
+
assert.is(el.settingsBudgetUpdatedLabel, undefined, 'no label before any feed');
|
|
433
|
+
|
|
434
|
+
el.setVendorBudget('anthropic', { budgetUsd: 50, spentUsd: 10 });
|
|
435
|
+
const first = el.budgetMeterLastFedAt;
|
|
436
|
+
assert.ok(typeof first === 'number', 'stamped on feed');
|
|
437
|
+
assert.ok(el.settingsBudgetUpdatedLabel!.startsWith('Updated '), 'label renders');
|
|
438
|
+
|
|
439
|
+
// The store is idempotent by value, but a refresh that CONFIRMS the same
|
|
440
|
+
// dollars is still a refresh — the stamp must move even when the rows don't.
|
|
441
|
+
el.budgetMeterLastFedAt = first! - 5000; // age it so a same-ms re-feed still differs
|
|
442
|
+
el.setVendorBudget('anthropic', { budgetUsd: 50, spentUsd: 10 });
|
|
443
|
+
assert.ok(el.budgetMeterLastFedAt! > first! - 5000, 'identical re-feed still stamps');
|
|
444
|
+
|
|
445
|
+
// Clearing figures is not a refresh — the label describes shown figures.
|
|
446
|
+
const before = el.budgetMeterLastFedAt;
|
|
447
|
+
el.setVendorBudget('anthropic', null);
|
|
448
|
+
assert.is(el.budgetMeterLastFedAt, before, 'a clear does not stamp');
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
Suite('requestBudgetRefresh emits budget-refresh-requested, and settings open asks too', () => {
|
|
452
|
+
const el = element();
|
|
453
|
+
let fired = 0;
|
|
454
|
+
el.addEventListener('budget-refresh-requested', () => {
|
|
455
|
+
fired += 1;
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
el.requestBudgetRefresh();
|
|
459
|
+
assert.is(fired, 1, 'the button path emits');
|
|
460
|
+
|
|
461
|
+
// The element cannot fetch — the event is the whole contract. Hosts without
|
|
462
|
+
// a listener simply keep the per-turn cadence, so firing is unconditional.
|
|
463
|
+
//
|
|
464
|
+
// The modal stub is load-bearing: openSettingsModal's `show` closure
|
|
465
|
+
// re-queues itself via DOM.queueUpdate until `settingsModal` exists, and on
|
|
466
|
+
// an unconnected element that is never — an infinite queue that keeps the
|
|
467
|
+
// test runner alive (this file's documented hang class). `open: true` makes
|
|
468
|
+
// the first queued run return immediately.
|
|
469
|
+
(el as unknown as { settingsModal: { open: boolean; show(): void } }).settingsModal = {
|
|
470
|
+
open: true,
|
|
471
|
+
show() {},
|
|
472
|
+
};
|
|
473
|
+
el.openSettingsModal();
|
|
474
|
+
assert.is(fired, 2, 'opening settings requests fresh figures');
|
|
475
|
+
});
|
|
476
|
+
|
|
427
477
|
Suite.run();
|
package/src/main/main.ts
CHANGED
|
@@ -1023,9 +1023,52 @@ export class FoundationAiAssistant extends GenesisElement {
|
|
|
1023
1023
|
figures:
|
|
1024
1024
|
figures == null ? null : { budgetUsd: figures.budgetUsd, spentUsd: figures.spentUsd },
|
|
1025
1025
|
});
|
|
1026
|
+
if (figures != null) this.budgetMeterLastFedAt = Date.now();
|
|
1026
1027
|
return true;
|
|
1027
1028
|
}
|
|
1028
1029
|
|
|
1030
|
+
/**
|
|
1031
|
+
* When budget figures last arrived — from the host's {@link FoundationAiAssistant.setVendorBudget}
|
|
1032
|
+
* or the wall latch's own feed — as epoch ms; `null` until the first feed.
|
|
1033
|
+
*
|
|
1034
|
+
* ELEMENT state, not slice state, on purpose: the reducer is idempotent by
|
|
1035
|
+
* value (an unchanged re-feed must not re-render the meter rows), but "when
|
|
1036
|
+
* was this fetched" must move on every feed, changed figures or not — a
|
|
1037
|
+
* refresh that confirms the same dollars is still a refresh. Only the
|
|
1038
|
+
* toolbar label observes this, so a stamp re-renders one span, not the rows.
|
|
1039
|
+
*/
|
|
1040
|
+
@observable budgetMeterLastFedAt: number | null = null;
|
|
1041
|
+
|
|
1042
|
+
/** "Updated HH:MM:SS" for the meter toolbar, or `undefined` before any feed. */
|
|
1043
|
+
@volatile
|
|
1044
|
+
get settingsBudgetUpdatedLabel(): string | undefined {
|
|
1045
|
+
if (this.budgetMeterLastFedAt == null) return undefined;
|
|
1046
|
+
return `Updated ${new Date(this.budgetMeterLastFedAt).toLocaleTimeString()}`;
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
/**
|
|
1050
|
+
* Ask the host to re-fetch budget figures now (GENC-1464 refresh affordance).
|
|
1051
|
+
*
|
|
1052
|
+
* The element cannot fetch — the meter is host-fed by design — so this emits
|
|
1053
|
+
* `budget-refresh-requested` and the host answers with the same fetch that
|
|
1054
|
+
* already feeds {@link FoundationAiAssistant.setVendorBudget} (Create's handler coalesces in-flight
|
|
1055
|
+
* fetches, so a spammed button costs one request). Fired by the meter's
|
|
1056
|
+
* refresh button and on settings-modal open, so the figures are fresh exactly
|
|
1057
|
+
* when someone is looking at them. Fires unconditionally: a host without a
|
|
1058
|
+
* listener simply keeps the per-turn cadence.
|
|
1059
|
+
*/
|
|
1060
|
+
requestBudgetRefresh(): void {
|
|
1061
|
+
// Plain dispatchEvent, not FAST's $emit: $emit silently no-ops while the
|
|
1062
|
+
// element is disconnected, and this must stay observable in headless/test
|
|
1063
|
+
// harnesses that never connect (and in any odd pop-out lifecycle moment).
|
|
1064
|
+
// bubbles + composed, matching this class's other events (SessionCleared
|
|
1065
|
+
// et al): the element lives inside host shadow roots (Create nests it in
|
|
1066
|
+
// gc-assistant's), and a delegation-based host must still see the request.
|
|
1067
|
+
this.dispatchEvent(
|
|
1068
|
+
new CustomEvent('budget-refresh-requested', { bubbles: true, composed: true }),
|
|
1069
|
+
);
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1029
1072
|
/** Whether this vendor's wall came from the sweep alone — see the slice's `sweptVendors`. */
|
|
1030
1073
|
private isVendorSwept(vendor: AIProviderType): boolean {
|
|
1031
1074
|
return (this._sessionRef?.store.aiAssistant.sweptVendors ?? []).includes(vendor);
|
|
@@ -1248,6 +1291,7 @@ export class FoundationAiAssistant extends GenesisElement {
|
|
|
1248
1291
|
vendor,
|
|
1249
1292
|
figures: { budgetUsd: budget.budgetUsd, spentUsd: budget.spentUsd },
|
|
1250
1293
|
});
|
|
1294
|
+
this.budgetMeterLastFedAt = Date.now();
|
|
1251
1295
|
}
|
|
1252
1296
|
if (!vendor || vendor === 'none') {
|
|
1253
1297
|
if (!this.blocked) {
|
|
@@ -3789,6 +3833,9 @@ export class FoundationAiAssistant extends GenesisElement {
|
|
|
3789
3833
|
|
|
3790
3834
|
openSettingsModal(): void {
|
|
3791
3835
|
this.logMeta('panel.toggled', { panel: 'settings', open: true });
|
|
3836
|
+
// The meter refreshes per turn, which a user staring at the modal cannot
|
|
3837
|
+
// see — ask the host for fresh figures at the moment they start looking.
|
|
3838
|
+
this.requestBudgetRefresh();
|
|
3792
3839
|
this.settingsModalTab = 'settings';
|
|
3793
3840
|
this.settingsOpen = true;
|
|
3794
3841
|
const show = (): void => {
|