@genesislcap/ai-assistant 15.10.6 → 15.12.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 +226 -0
- package/dist/ai-assistant.d.ts +100 -3
- package/dist/chat-driver.cjs +540 -117
- package/dist/chat-driver.cjs.map +4 -4
- package/dist/chat-driver.mjs +524 -116
- package/dist/chat-driver.mjs.map +4 -4
- package/dist/custom-elements.json +439 -66
- package/dist/dts/chat-driver-node.d.ts +5 -2
- package/dist/dts/chat-driver-node.d.ts.map +1 -1
- package/dist/dts/components/chat-driver/chat-driver.d.ts +20 -3
- package/dist/dts/components/chat-driver/chat-driver.d.ts.map +1 -1
- package/dist/dts/components/chat-driver/chat-driver.thinking-policy.test.d.ts +2 -0
- package/dist/dts/components/chat-driver/chat-driver.thinking-policy.test.d.ts.map +1 -0
- package/dist/dts/components/chat-driver/chat-driver.trace-capture.test.d.ts +2 -0
- package/dist/dts/components/chat-driver/chat-driver.trace-capture.test.d.ts.map +1 -0
- 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/config/config.d.ts +39 -2
- package/dist/dts/config/config.d.ts.map +1 -1
- package/dist/dts/config/define-stateful-agent.d.ts +15 -1
- package/dist/dts/config/define-stateful-agent.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/main/main.template.d.ts.map +1 -1
- package/dist/dts/react.d.ts +9 -8
- package/dist/dts/utils/strip-agent-handlers.d.ts +1 -1
- package/dist/dts/utils/sum-usage.d.ts +37 -4
- package/dist/dts/utils/sum-usage.d.ts.map +1 -1
- package/dist/dts/utils/usage-rows.d.ts +102 -0
- package/dist/dts/utils/usage-rows.d.ts.map +1 -0
- package/dist/dts/utils/usage-rows.test.d.ts +2 -0
- package/dist/dts/utils/usage-rows.test.d.ts.map +1 -0
- package/dist/esm/chat-driver-node.js +36 -1
- package/dist/esm/components/chat-driver/chat-driver.js +73 -10
- package/dist/esm/components/chat-driver/chat-driver.thinking-policy.test.js +137 -0
- package/dist/esm/components/chat-driver/chat-driver.trace-capture.test.js +200 -0
- 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/config/define-stateful-agent.js +11 -0
- 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/esm/main/main.template.js +20 -1
- package/dist/esm/utils/strip-agent-handlers.js +1 -1
- package/dist/esm/utils/sum-usage.js +37 -4
- package/dist/esm/utils/usage-rows.js +90 -0
- package/dist/esm/utils/usage-rows.test.js +189 -0
- package/dist/react.cjs +11 -6
- package/dist/react.mjs +10 -5
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +17 -17
- package/src/chat-driver-node.ts +58 -0
- package/src/components/chat-driver/chat-driver.thinking-policy.test.ts +185 -0
- package/src/components/chat-driver/chat-driver.trace-capture.test.ts +251 -0
- package/src/components/chat-driver/chat-driver.ts +90 -10
- 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/config/config.ts +50 -1
- package/src/config/define-stateful-agent.ts +37 -0
- package/src/main/blocked-state.test.ts +1 -0
- package/src/main/budget-meter.test.ts +50 -0
- package/src/main/main.template.ts +19 -1
- package/src/main/main.ts +47 -0
- package/src/utils/strip-agent-handlers.ts +1 -1
- package/src/utils/sum-usage.ts +37 -4
- package/src/utils/usage-rows.test.ts +237 -0
- package/src/utils/usage-rows.ts +187 -0
|
@@ -199,6 +199,16 @@ export function defineStatefulAgent(opts) {
|
|
|
199
199
|
return opts.cachePolicy(Object.assign(Object.assign({}, ctx), { state }));
|
|
200
200
|
})
|
|
201
201
|
: opts.cachePolicy;
|
|
202
|
+
// Unlike the resolvers above, a pre-init call returns `undefined` rather than throwing: that is
|
|
203
|
+
// this option's "model default" state, so degrading to it costs a turn its thinking preference
|
|
204
|
+
// instead of breaking the turn outright.
|
|
205
|
+
const wrappedThinkingPolicy = typeof opts.thinkingPolicy === 'function'
|
|
206
|
+
? (ctx) => __awaiter(this, void 0, void 0, function* () {
|
|
207
|
+
if (!state)
|
|
208
|
+
return undefined;
|
|
209
|
+
return opts.thinkingPolicy(Object.assign(Object.assign({}, ctx), { state }));
|
|
210
|
+
})
|
|
211
|
+
: opts.thinkingPolicy;
|
|
202
212
|
const wrappedTailContext = typeof opts.tailContext === 'function'
|
|
203
213
|
? (ctx) => __awaiter(this, void 0, void 0, function* () {
|
|
204
214
|
if (!state) {
|
|
@@ -249,6 +259,7 @@ export function defineStatefulAgent(opts) {
|
|
|
249
259
|
temperature: wrappedTemperature,
|
|
250
260
|
toolChoice: wrappedToolChoice,
|
|
251
261
|
cachePolicy: wrappedCachePolicy,
|
|
262
|
+
thinkingPolicy: wrappedThinkingPolicy,
|
|
252
263
|
tailContext: wrappedTailContext,
|
|
253
264
|
onUnresolvedTool: wrappedOnUnresolvedTool,
|
|
254
265
|
resumable: wrappedResumable,
|
|
@@ -219,6 +219,7 @@ Suite('a wall with figures snaps the vendor meter at latch time', () => {
|
|
|
219
219
|
reachable(el, 'anthropic', 'gemini');
|
|
220
220
|
latch(el, 'budget-exhausted', { vendorLabel: 'Anthropic', budgetUsd: 20, spentUsd: 20.1 });
|
|
221
221
|
assert.equal(storedBudgets(el)['anthropic'], { budgetUsd: 20, spentUsd: 20.1 });
|
|
222
|
+
assert.ok(typeof el.budgetMeterLastFedAt === 'number', 'the wall feed stamps freshness too');
|
|
222
223
|
// A later 402 for the same (already-walled) vendor carries fresher spend —
|
|
223
224
|
// the feed is deliberately outside the idempotence guard, like the sweep.
|
|
224
225
|
latch(el, 'budget-exhausted', { vendorLabel: 'Anthropic', budgetUsd: 20, spentUsd: 21.5 });
|
|
@@ -314,4 +314,46 @@ Suite('figures alone are enough to surface the AI Model Settings section', () =>
|
|
|
314
314
|
el.chatConfig = { ui: { showBudgetUsage: false } };
|
|
315
315
|
assert.is(el.settingsModelSectionVisible, false, 'and the config flag takes it back out');
|
|
316
316
|
});
|
|
317
|
+
// ── Freshness stamp + refresh request (GENC-1464 refresh affordance) ─────────
|
|
318
|
+
Suite('every feed stamps budgetMeterLastFedAt — identical figures included', () => {
|
|
319
|
+
const el = element();
|
|
320
|
+
assert.is(el.budgetMeterLastFedAt, null, 'null before any feed');
|
|
321
|
+
assert.is(el.settingsBudgetUpdatedLabel, undefined, 'no label before any feed');
|
|
322
|
+
el.setVendorBudget('anthropic', { budgetUsd: 50, spentUsd: 10 });
|
|
323
|
+
const first = el.budgetMeterLastFedAt;
|
|
324
|
+
assert.ok(typeof first === 'number', 'stamped on feed');
|
|
325
|
+
assert.ok(el.settingsBudgetUpdatedLabel.startsWith('Updated '), 'label renders');
|
|
326
|
+
// The store is idempotent by value, but a refresh that CONFIRMS the same
|
|
327
|
+
// dollars is still a refresh — the stamp must move even when the rows don't.
|
|
328
|
+
el.budgetMeterLastFedAt = first - 5000; // age it so a same-ms re-feed still differs
|
|
329
|
+
el.setVendorBudget('anthropic', { budgetUsd: 50, spentUsd: 10 });
|
|
330
|
+
assert.ok(el.budgetMeterLastFedAt > first - 5000, 'identical re-feed still stamps');
|
|
331
|
+
// Clearing figures is not a refresh — the label describes shown figures.
|
|
332
|
+
const before = el.budgetMeterLastFedAt;
|
|
333
|
+
el.setVendorBudget('anthropic', null);
|
|
334
|
+
assert.is(el.budgetMeterLastFedAt, before, 'a clear does not stamp');
|
|
335
|
+
});
|
|
336
|
+
Suite('requestBudgetRefresh emits budget-refresh-requested, and settings open asks too', () => {
|
|
337
|
+
const el = element();
|
|
338
|
+
let fired = 0;
|
|
339
|
+
el.addEventListener('budget-refresh-requested', () => {
|
|
340
|
+
fired += 1;
|
|
341
|
+
});
|
|
342
|
+
el.requestBudgetRefresh();
|
|
343
|
+
assert.is(fired, 1, 'the button path emits');
|
|
344
|
+
// The element cannot fetch — the event is the whole contract. Hosts without
|
|
345
|
+
// a listener simply keep the per-turn cadence, so firing is unconditional.
|
|
346
|
+
//
|
|
347
|
+
// The modal stub is load-bearing: openSettingsModal's `show` closure
|
|
348
|
+
// re-queues itself via DOM.queueUpdate until `settingsModal` exists, and on
|
|
349
|
+
// an unconnected element that is never — an infinite queue that keeps the
|
|
350
|
+
// test runner alive (this file's documented hang class). `open: true` makes
|
|
351
|
+
// the first queued run return immediately.
|
|
352
|
+
el.settingsModal = {
|
|
353
|
+
open: true,
|
|
354
|
+
show() { },
|
|
355
|
+
};
|
|
356
|
+
el.openSettingsModal();
|
|
357
|
+
assert.is(fired, 2, 'opening settings requests fresh figures');
|
|
358
|
+
});
|
|
317
359
|
Suite.run();
|
package/dist/esm/main/main.js
CHANGED
|
@@ -265,6 +265,17 @@ let FoundationAiAssistant = FoundationAiAssistant_1 = class FoundationAiAssistan
|
|
|
265
265
|
* repeat the same warning on every send for the life of the page.
|
|
266
266
|
*/
|
|
267
267
|
this.vendorBudgetWarningIssued = false;
|
|
268
|
+
/**
|
|
269
|
+
* When budget figures last arrived — from the host's {@link FoundationAiAssistant.setVendorBudget}
|
|
270
|
+
* or the wall latch's own feed — as epoch ms; `null` until the first feed.
|
|
271
|
+
*
|
|
272
|
+
* ELEMENT state, not slice state, on purpose: the reducer is idempotent by
|
|
273
|
+
* value (an unchanged re-feed must not re-render the meter rows), but "when
|
|
274
|
+
* was this fetched" must move on every feed, changed figures or not — a
|
|
275
|
+
* refresh that confirms the same dollars is still a refresh. Only the
|
|
276
|
+
* toolbar label observes this, so a stamp re-renders one span, not the rows.
|
|
277
|
+
*/
|
|
278
|
+
this.budgetMeterLastFedAt = null;
|
|
268
279
|
// ---- Transient UI state (stays as @observable on the component) ----
|
|
269
280
|
this._suggestionsGeneration = 0;
|
|
270
281
|
this.attachments = [];
|
|
@@ -982,8 +993,36 @@ let FoundationAiAssistant = FoundationAiAssistant_1 = class FoundationAiAssistan
|
|
|
982
993
|
// must not adopt (and potentially freeze) state the host still owns.
|
|
983
994
|
figures: figures == null ? null : { budgetUsd: figures.budgetUsd, spentUsd: figures.spentUsd },
|
|
984
995
|
});
|
|
996
|
+
if (figures != null)
|
|
997
|
+
this.budgetMeterLastFedAt = Date.now();
|
|
985
998
|
return true;
|
|
986
999
|
}
|
|
1000
|
+
/** "Updated HH:MM:SS" for the meter toolbar, or `undefined` before any feed. */
|
|
1001
|
+
get settingsBudgetUpdatedLabel() {
|
|
1002
|
+
if (this.budgetMeterLastFedAt == null)
|
|
1003
|
+
return undefined;
|
|
1004
|
+
return `Updated ${new Date(this.budgetMeterLastFedAt).toLocaleTimeString()}`;
|
|
1005
|
+
}
|
|
1006
|
+
/**
|
|
1007
|
+
* Ask the host to re-fetch budget figures now (GENC-1464 refresh affordance).
|
|
1008
|
+
*
|
|
1009
|
+
* The element cannot fetch — the meter is host-fed by design — so this emits
|
|
1010
|
+
* `budget-refresh-requested` and the host answers with the same fetch that
|
|
1011
|
+
* already feeds {@link FoundationAiAssistant.setVendorBudget} (Create's handler coalesces in-flight
|
|
1012
|
+
* fetches, so a spammed button costs one request). Fired by the meter's
|
|
1013
|
+
* refresh button and on settings-modal open, so the figures are fresh exactly
|
|
1014
|
+
* when someone is looking at them. Fires unconditionally: a host without a
|
|
1015
|
+
* listener simply keeps the per-turn cadence.
|
|
1016
|
+
*/
|
|
1017
|
+
requestBudgetRefresh() {
|
|
1018
|
+
// Plain dispatchEvent, not FAST's $emit: $emit silently no-ops while the
|
|
1019
|
+
// element is disconnected, and this must stay observable in headless/test
|
|
1020
|
+
// harnesses that never connect (and in any odd pop-out lifecycle moment).
|
|
1021
|
+
// bubbles + composed, matching this class's other events (SessionCleared
|
|
1022
|
+
// et al): the element lives inside host shadow roots (Create nests it in
|
|
1023
|
+
// gc-assistant's), and a delegation-based host must still see the request.
|
|
1024
|
+
this.dispatchEvent(new CustomEvent('budget-refresh-requested', { bubbles: true, composed: true }));
|
|
1025
|
+
}
|
|
987
1026
|
/** Whether this vendor's wall came from the sweep alone — see the slice's `sweptVendors`. */
|
|
988
1027
|
isVendorSwept(vendor) {
|
|
989
1028
|
var _a, _b;
|
|
@@ -1201,6 +1240,7 @@ let FoundationAiAssistant = FoundationAiAssistant_1 = class FoundationAiAssistan
|
|
|
1201
1240
|
vendor,
|
|
1202
1241
|
figures: { budgetUsd: budget.budgetUsd, spentUsd: budget.spentUsd },
|
|
1203
1242
|
});
|
|
1243
|
+
this.budgetMeterLastFedAt = Date.now();
|
|
1204
1244
|
}
|
|
1205
1245
|
if (!vendor || vendor === 'none') {
|
|
1206
1246
|
if (!this.blocked) {
|
|
@@ -3459,6 +3499,9 @@ let FoundationAiAssistant = FoundationAiAssistant_1 = class FoundationAiAssistan
|
|
|
3459
3499
|
}
|
|
3460
3500
|
openSettingsModal() {
|
|
3461
3501
|
this.logMeta('panel.toggled', { panel: 'settings', open: true });
|
|
3502
|
+
// The meter refreshes per turn, which a user staring at the modal cannot
|
|
3503
|
+
// see — ask the host for fresh figures at the moment they start looking.
|
|
3504
|
+
this.requestBudgetRefresh();
|
|
3462
3505
|
this.settingsModalTab = 'settings';
|
|
3463
3506
|
this.settingsOpen = true;
|
|
3464
3507
|
const show = () => {
|
|
@@ -4375,6 +4418,12 @@ __decorate([
|
|
|
4375
4418
|
__decorate([
|
|
4376
4419
|
volatile
|
|
4377
4420
|
], FoundationAiAssistant.prototype, "reachableVendors", null);
|
|
4421
|
+
__decorate([
|
|
4422
|
+
observable
|
|
4423
|
+
], FoundationAiAssistant.prototype, "budgetMeterLastFedAt", void 0);
|
|
4424
|
+
__decorate([
|
|
4425
|
+
volatile
|
|
4426
|
+
], FoundationAiAssistant.prototype, "settingsBudgetUpdatedLabel", null);
|
|
4378
4427
|
__decorate([
|
|
4379
4428
|
volatile
|
|
4380
4429
|
], FoundationAiAssistant.prototype, "relevantBlockedVendors", null);
|
|
@@ -144,11 +144,30 @@ const subAgentToolResultTemplate = html `
|
|
|
144
144
|
const subAgentMessageRowTemplate = html `
|
|
145
145
|
${when((m) => { var _a; return m.role === 'assistant' && !((_a = m.toolCalls) === null || _a === void 0 ? void 0 : _a.length) && !!m.content; }, subAgentAssistantTemplate)}${when((m) => { var _a; return !!((_a = m.toolCalls) === null || _a === void 0 ? void 0 : _a.length); }, subAgentToolCallTemplate)}${when((m) => { var _a; return m.role === 'tool' && !!((_a = m.toolResult) === null || _a === void 0 ? void 0 : _a.content); }, subAgentToolResultTemplate)}
|
|
146
146
|
`;
|
|
147
|
+
/**
|
|
148
|
+
* Label for a trace block: the distinct agents that appear in it.
|
|
149
|
+
*
|
|
150
|
+
* One tool call can invoke several sub-agents — a scheduler dispatching a dependency
|
|
151
|
+
* graph, or a retry — and their conversations arrive concatenated in one trace. Naming
|
|
152
|
+
* only `[0]` would then label a block "gen_A trace" while `gen_B`'s turns sit inside
|
|
153
|
+
* it. Two agents read as "gen_A, gen_B"; beyond that it degrades to a count rather
|
|
154
|
+
* than growing an unbounded summary line.
|
|
155
|
+
*/
|
|
156
|
+
const subAgentTraceLabel = (trace) => {
|
|
157
|
+
const names = [...new Set(trace.map((m) => m.agentName).filter(Boolean))];
|
|
158
|
+
if (names.length === 0)
|
|
159
|
+
return 'Sub-agent trace';
|
|
160
|
+
if (names.length <= 2)
|
|
161
|
+
return `${names.join(', ')} trace`;
|
|
162
|
+
// Owns the whole string rather than returning a fragment the template suffixes, so the noun
|
|
163
|
+
// agrees with the count — a shared ` trace` suffix rendered "3 sub-agents trace".
|
|
164
|
+
return `${names.length} sub-agent traces`;
|
|
165
|
+
};
|
|
147
166
|
/** Collapsed <details> trace shown inside a tool-call card once the sub-agent finishes. */
|
|
148
167
|
const subAgentTraceTemplate = html `
|
|
149
168
|
<details class="sub-agent-trace">
|
|
150
169
|
<summary class="sub-agent-trace-summary">
|
|
151
|
-
${(tc) =>
|
|
170
|
+
${(tc) => subAgentTraceLabel(tc.subAgentTrace)}
|
|
152
171
|
</summary>
|
|
153
172
|
${repeat((tc) => tc.subAgentTrace.filter((m) => m.role !== 'user'), subAgentMessageRowTemplate)}
|
|
154
173
|
</details>
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* `onDeactivate`, `getDebugSnapshot`, `onUnresolvedTool`) and the function
|
|
7
7
|
* form of the per-turn resolvers (`systemPrompt`, `toolDefinitions`,
|
|
8
8
|
* `displayName`, `provider`, `temperature`, `toolChoice`, `cachePolicy`,
|
|
9
|
-
* `toolHandlers`).
|
|
9
|
+
* `thinkingPolicy`, `toolHandlers`).
|
|
10
10
|
* 2. **Object "handler bags" whose *values* are functions** — `toolHandlers` in
|
|
11
11
|
* its object form is `{ name: handler }`, so `typeof` is `'object'`, not
|
|
12
12
|
* `'function'`. A by-value check on the field alone misses it, leaking a live
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Zeroed {@link AggregateUsage}. A factory, not a shared const, because callers
|
|
3
3
|
* accumulate into the returned object.
|
|
4
4
|
*
|
|
5
|
-
* @
|
|
5
|
+
* @beta
|
|
6
6
|
*/
|
|
7
7
|
export function emptyUsage() {
|
|
8
8
|
return {
|
|
@@ -23,7 +23,7 @@ export function emptyUsage() {
|
|
|
23
23
|
* NOT the count of distinct tokens in the conversation, so it grows
|
|
24
24
|
* super-linearly with turn count and is expected to dwarf the context size.
|
|
25
25
|
*
|
|
26
|
-
* @
|
|
26
|
+
* @beta
|
|
27
27
|
*/
|
|
28
28
|
export function totalTokens(usage) {
|
|
29
29
|
return (usage.uncachedInputTokens + usage.cacheReadTokens + usage.cacheWriteTokens + usage.outputTokens);
|
|
@@ -35,7 +35,7 @@ export function totalTokens(usage) {
|
|
|
35
35
|
* proves. Pure rather than in-place because both operands are live state a caller
|
|
36
36
|
* must not mutate.
|
|
37
37
|
*
|
|
38
|
-
* @
|
|
38
|
+
* @beta
|
|
39
39
|
*/
|
|
40
40
|
export function addUsage(a, b) {
|
|
41
41
|
return {
|
|
@@ -70,7 +70,40 @@ export function addUsage(a, b) {
|
|
|
70
70
|
* Returns a zeroed total when no message carries usage — providers that report
|
|
71
71
|
* none (e.g. Chrome built-in) are skipped silently rather than guessed at.
|
|
72
72
|
*
|
|
73
|
-
* @
|
|
73
|
+
* @remarks
|
|
74
|
+
* This sums the WHOLE array, which is the right answer only when the array is the
|
|
75
|
+
* whole of the work being billed. A driver whose conversation **continues across
|
|
76
|
+
* rounds** opens its history with turns an earlier round already paid for, so
|
|
77
|
+
* summing it again bills them twice — and the session total then grows
|
|
78
|
+
* quadratically in the number of rounds while every individual figure in it stays
|
|
79
|
+
* correct. Nothing about the result looks wrong, which is why it is called out
|
|
80
|
+
* here rather than left to the caller to notice.
|
|
81
|
+
*
|
|
82
|
+
* Do NOT solve this with a "sum from this message onwards" marker. Compaction
|
|
83
|
+
* *replaces* the messages it summarises — and compaction is one of this function's
|
|
84
|
+
* own call sites — so the marker message can be absent from history by the time
|
|
85
|
+
* the sum runs, leaving the caller to either fail or silently sum everything,
|
|
86
|
+
* which is the double-count the marker was meant to prevent.
|
|
87
|
+
*
|
|
88
|
+
* Instead hold a **banked baseline**: spend that predates the current transcript,
|
|
89
|
+
* carried as a value, with `usage = banked + live` composed via {@link addUsage}.
|
|
90
|
+
* `resolveBankedUsage` in `utils/cost-session-history.ts` is the worked form, and
|
|
91
|
+
* the shape of its signature is the load-bearing part. It separates three cases:
|
|
92
|
+
*
|
|
93
|
+
* 1. no prior record — a genuine first round, so nothing is banked;
|
|
94
|
+
* 2. prior record, and the live transcript already accounts for it (a restored
|
|
95
|
+
* session) — carry the prior record's own banked figure forward;
|
|
96
|
+
* 3. prior record, and the transcript starts empty — bank the prior record's
|
|
97
|
+
* whole total, because nothing in the transcript can re-derive it.
|
|
98
|
+
*
|
|
99
|
+
* Case 1 and case 3 take the same arithmetic and mean opposite things, so the
|
|
100
|
+
* prior record must be a **required** input with no zero-defaulting path: a
|
|
101
|
+
* caller who carries a transcript and forgets its baseline reports the entire
|
|
102
|
+
* prior conversation as this round's spend. Distinguishing 2 from 3 is a separate
|
|
103
|
+
* decision, and getting it wrong is destructive in the other direction — it
|
|
104
|
+
* rewrites a total down to the current page-load and drops earlier spend.
|
|
105
|
+
*
|
|
106
|
+
* @beta
|
|
74
107
|
*/
|
|
75
108
|
export function sumUsage(messages) {
|
|
76
109
|
const total = emptyUsage();
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { vendorOfModel } from '@genesislcap/foundation-ai';
|
|
2
|
+
/** Whether a message carries anything a ledger would record. */
|
|
3
|
+
function hasUsage(m) {
|
|
4
|
+
return (m.cost != null ||
|
|
5
|
+
m.externalCostUsd != null ||
|
|
6
|
+
m.inputTokens != null ||
|
|
7
|
+
m.outputTokens != null ||
|
|
8
|
+
m.cacheReadTokens != null ||
|
|
9
|
+
m.cacheWriteTokens != null);
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Project a transcript into one row per billable unit of spend.
|
|
13
|
+
*
|
|
14
|
+
* @remarks
|
|
15
|
+
* Reads the messages and returns a table; it stores nothing, mutates nothing, and
|
|
16
|
+
* adds nothing to history. Use it where an **aggregate is not enough** — writing
|
|
17
|
+
* usage rows for per-project attribution, a cost dashboard, or auditing which turns
|
|
18
|
+
* came back unpriced. For a single total, `sumUsage` is the answer and this is the
|
|
19
|
+
* wrong tool.
|
|
20
|
+
*
|
|
21
|
+
* Walks the same shape `sumUsage` does, and is guaranteed to agree with it: summing
|
|
22
|
+
* `costUsd` and `externalCostUsd` across these rows equals `sumUsage(...).costUsd`
|
|
23
|
+
* over the same input, and the four token buckets reconcile likewise. That property
|
|
24
|
+
* is the point of the function — a per-call view that quietly disagrees with the
|
|
25
|
+
* total is worse than no per-call view, because both look right in isolation.
|
|
26
|
+
*
|
|
27
|
+
* Covers the two places spend hides:
|
|
28
|
+
*
|
|
29
|
+
* - **Sub-agent conversations.** A delegating tool call carries the child's entire
|
|
30
|
+
* conversation on `toolCall.subAgentTrace`, so a walk of top-level messages alone
|
|
31
|
+
* misses everything a delegating agent spent. A consumer that reimplemented this
|
|
32
|
+
* and omitted the branch reported $0.106 on a run that cost $0.234.
|
|
33
|
+
* - **Compactions.** A compaction deletes the messages it summarises, banking their
|
|
34
|
+
* spend on the summary. Those turns produce a single `source: 'compaction'` row.
|
|
35
|
+
*
|
|
36
|
+
* Messages carrying no usage at all (a user turn, a narration) produce no row.
|
|
37
|
+
*
|
|
38
|
+
* **Do not pass the result to `sumUsage`** — the types prevent it, and the reason is
|
|
39
|
+
* that both recurse, so a pre-flattened list would be counted twice. Both functions
|
|
40
|
+
* take raw history.
|
|
41
|
+
*
|
|
42
|
+
* @beta
|
|
43
|
+
*/
|
|
44
|
+
export function usageRows(messages) {
|
|
45
|
+
return collectRows(messages, 0, undefined);
|
|
46
|
+
}
|
|
47
|
+
/** Map an already-bucketed {@link AggregateUsage} onto a row's token fields. */
|
|
48
|
+
function bucketsOf(usage) {
|
|
49
|
+
return {
|
|
50
|
+
uncachedInputTokens: usage.uncachedInputTokens,
|
|
51
|
+
cacheReadTokens: usage.cacheReadTokens,
|
|
52
|
+
cacheWriteTokens: usage.cacheWriteTokens,
|
|
53
|
+
outputTokens: usage.outputTokens,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
function collectRows(messages, depth, subAgentOf) {
|
|
57
|
+
var _a, _b, _c, _d, _e, _f, _g;
|
|
58
|
+
const rows = [];
|
|
59
|
+
for (const m of messages) {
|
|
60
|
+
if (hasUsage(m)) {
|
|
61
|
+
const cacheReadTokens = (_a = m.cacheReadTokens) !== null && _a !== void 0 ? _a : 0;
|
|
62
|
+
const cacheWriteTokens = (_b = m.cacheWriteTokens) !== null && _b !== void 0 ? _b : 0;
|
|
63
|
+
rows.push(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({ source: 'request', model: m.model,
|
|
64
|
+
// The driver stamps `ChatMessage.provider` from the resolved provider's own
|
|
65
|
+
// status, so it is AUTHORITATIVE and covers vendors the model allowlists do
|
|
66
|
+
// not — a server-proxied `gpt-5` turn is `provider: 'openai'` on the message
|
|
67
|
+
// and unknown to `vendorOfModel`. Prefer it; fall back to resolving the model
|
|
68
|
+
// id only for messages persisted before the field existed. Still left
|
|
69
|
+
// undefined when neither knows, rather than guessed at.
|
|
70
|
+
provider: (_c = m.provider) !== null && _c !== void 0 ? _c : (m.model ? vendorOfModel(m.model) : undefined) }, (m.cost != null && { costUsd: m.cost })), (m.externalCostUsd != null && { externalCostUsd: m.externalCostUsd })), {
|
|
71
|
+
// Uncached input is the REMAINDER, matching `sumUsage`: `inputTokens` is the
|
|
72
|
+
// whole prompt and the cache fields break it down. Clamped for the same
|
|
73
|
+
// reason — hand-edited history must not drive a total negative.
|
|
74
|
+
uncachedInputTokens: Math.max(0, ((_d = m.inputTokens) !== null && _d !== void 0 ? _d : 0) - cacheReadTokens - cacheWriteTokens), cacheReadTokens,
|
|
75
|
+
cacheWriteTokens, outputTokens: (_e = m.outputTokens) !== null && _e !== void 0 ? _e : 0 }), (m.agentName != null && { agentName: m.agentName })), { subAgentDepth: depth }), (subAgentOf != null && { subAgentOf })));
|
|
76
|
+
}
|
|
77
|
+
// Spend the compaction banked on this summary's behalf. Its buckets are already
|
|
78
|
+
// disjoint (it is an `AggregateUsage`), so they map across without the
|
|
79
|
+
// subtraction a per-message record needs.
|
|
80
|
+
const rolled = (_f = m.compaction) === null || _f === void 0 ? void 0 : _f.rolledUpUsage;
|
|
81
|
+
if (rolled) {
|
|
82
|
+
rows.push(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({ source: 'compaction' }, (rolled.costUsd != null && { costUsd: rolled.costUsd })), bucketsOf(rolled)), (m.agentName != null && { agentName: m.agentName })), { subAgentDepth: depth }), (subAgentOf != null && { subAgentOf })));
|
|
83
|
+
}
|
|
84
|
+
for (const tc of (_g = m.toolCalls) !== null && _g !== void 0 ? _g : []) {
|
|
85
|
+
if (tc.subAgentTrace)
|
|
86
|
+
rows.push(...collectRows(tc.subAgentTrace, depth + 1, tc.id));
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return rows;
|
|
90
|
+
}
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { assert, createLogicSuite } from '@genesislcap/foundation-testing';
|
|
2
|
+
import { sumUsage } from './sum-usage';
|
|
3
|
+
import { usageRows } from './usage-rows';
|
|
4
|
+
const msg = (over) => (Object.assign({ role: 'assistant', content: '' }, over));
|
|
5
|
+
const rolled = (over) => (Object.assign({ costUsd: 0, uncachedInputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0, outputTokens: 0 }, over));
|
|
6
|
+
/** Every place spend can hide, in one transcript: a delegation and a compaction. */
|
|
7
|
+
const richHistory = () => [
|
|
8
|
+
msg({
|
|
9
|
+
role: 'compacted-summary',
|
|
10
|
+
content: 'summary',
|
|
11
|
+
compaction: {
|
|
12
|
+
compactedCount: 4,
|
|
13
|
+
rolledUpUsage: rolled({ costUsd: 0.05, uncachedInputTokens: 400, outputTokens: 60 }),
|
|
14
|
+
// The summariser's model — NOT what the banked spend ran on.
|
|
15
|
+
model: 'claude-haiku-4-5-20251001',
|
|
16
|
+
},
|
|
17
|
+
}),
|
|
18
|
+
msg({ role: 'user', content: 'go' }),
|
|
19
|
+
msg({
|
|
20
|
+
model: 'claude-sonnet-5',
|
|
21
|
+
agentName: 'boss',
|
|
22
|
+
cost: 0.01,
|
|
23
|
+
inputTokens: 1000,
|
|
24
|
+
cacheReadTokens: 900,
|
|
25
|
+
outputTokens: 50,
|
|
26
|
+
toolCalls: [
|
|
27
|
+
{
|
|
28
|
+
id: 'tc1',
|
|
29
|
+
name: 'delegate',
|
|
30
|
+
args: {},
|
|
31
|
+
subAgentTrace: [
|
|
32
|
+
msg({
|
|
33
|
+
model: 'gemini-2.5-flash',
|
|
34
|
+
agentName: 'worker',
|
|
35
|
+
cost: 0.12,
|
|
36
|
+
inputTokens: 500,
|
|
37
|
+
outputTokens: 200,
|
|
38
|
+
}),
|
|
39
|
+
],
|
|
40
|
+
},
|
|
41
|
+
],
|
|
42
|
+
}),
|
|
43
|
+
];
|
|
44
|
+
const suite = createLogicSuite('usageRows');
|
|
45
|
+
suite('reconciles exactly with sumUsage over the same input', () => {
|
|
46
|
+
// THE guarantee. A per-call view that disagrees with the total is worse than none,
|
|
47
|
+
// because both look right in isolation — which is how the consumer's own copy
|
|
48
|
+
// under-reported for a month.
|
|
49
|
+
const history = richHistory();
|
|
50
|
+
const rows = usageRows(history);
|
|
51
|
+
const total = sumUsage(history);
|
|
52
|
+
const rowCost = rows.reduce((n, r) => { var _a, _b; return n + ((_a = r.costUsd) !== null && _a !== void 0 ? _a : 0) + ((_b = r.externalCostUsd) !== null && _b !== void 0 ? _b : 0); }, 0);
|
|
53
|
+
assert.ok(Math.abs(rowCost - total.costUsd) < 1e-12, `${rowCost} vs ${total.costUsd}`);
|
|
54
|
+
const bucket = (k) => rows.reduce((n, r) => n + r[k], 0);
|
|
55
|
+
assert.is(bucket('uncachedInputTokens'), total.uncachedInputTokens, 'uncached input');
|
|
56
|
+
assert.is(bucket('cacheReadTokens'), total.cacheReadTokens, 'cache read');
|
|
57
|
+
assert.is(bucket('cacheWriteTokens'), total.cacheWriteTokens, 'cache write');
|
|
58
|
+
assert.is(bucket('outputTokens'), total.outputTokens, 'output');
|
|
59
|
+
});
|
|
60
|
+
suite('emits a compaction row so banked spend is not lost from a per-call view', () => {
|
|
61
|
+
// The shortfall this function exists to close: the summary message carries no
|
|
62
|
+
// `cost` and no token fields of its own, so a naive per-message walk drops the
|
|
63
|
+
// spend of every turn the compaction deleted.
|
|
64
|
+
const rows = usageRows(richHistory());
|
|
65
|
+
const compactionRows = rows.filter((r) => r.source === 'compaction');
|
|
66
|
+
assert.is(compactionRows.length, 1);
|
|
67
|
+
assert.is(compactionRows[0].costUsd, 0.05);
|
|
68
|
+
assert.is(compactionRows[0].uncachedInputTokens, 400);
|
|
69
|
+
assert.is(compactionRows[0].model, undefined, 'no model — the banked spend may span several, and compaction.model is the summariser');
|
|
70
|
+
});
|
|
71
|
+
suite('recurses into sub-agent traces, tagging depth and the spawning call', () => {
|
|
72
|
+
const rows = usageRows(richHistory());
|
|
73
|
+
const child = rows.find((r) => r.model === 'gemini-2.5-flash');
|
|
74
|
+
assert.ok(child, 'the sub-agent turn produced a row');
|
|
75
|
+
assert.is(child.subAgentDepth, 1);
|
|
76
|
+
assert.is(child.subAgentOf, 'tc1');
|
|
77
|
+
assert.is(child.agentName, 'worker');
|
|
78
|
+
const parent = rows.find((r) => r.model === 'claude-sonnet-5');
|
|
79
|
+
assert.is(parent.subAgentDepth, 0);
|
|
80
|
+
assert.is(parent.subAgentOf, undefined);
|
|
81
|
+
});
|
|
82
|
+
suite('prefers the provider the driver stamped over re-deriving it from the model', () => {
|
|
83
|
+
// `ChatMessage.provider` comes from the resolved provider's own status, so it is
|
|
84
|
+
// authoritative and knows vendors the model allowlists do not. Re-deriving from the
|
|
85
|
+
// model id alone drops attribution for a server-proxied turn — exactly the rows a
|
|
86
|
+
// usage ledger cares most about getting right. PR review.
|
|
87
|
+
const rows = usageRows([
|
|
88
|
+
msg({ model: 'gpt-5', provider: 'openai', cost: 1, inputTokens: 1 }),
|
|
89
|
+
// Stamped provider disagrees with what the model id would derive: the stamp still wins.
|
|
90
|
+
msg({
|
|
91
|
+
model: 'claude-sonnet-5',
|
|
92
|
+
provider: 'chrome',
|
|
93
|
+
cost: 1,
|
|
94
|
+
inputTokens: 1,
|
|
95
|
+
}),
|
|
96
|
+
]);
|
|
97
|
+
assert.equal(rows.map((r) => r.provider), ['openai', 'chrome']);
|
|
98
|
+
});
|
|
99
|
+
suite('resolves the provider from the model, and leaves it unset when unknown', () => {
|
|
100
|
+
const rows = usageRows([
|
|
101
|
+
msg({ model: 'claude-sonnet-5', cost: 1, inputTokens: 1 }),
|
|
102
|
+
msg({ model: 'gemini-2.5-flash', cost: 1, inputTokens: 1 }),
|
|
103
|
+
msg({ model: 'gpt-5', cost: 1, inputTokens: 1 }),
|
|
104
|
+
msg({ cost: 1, inputTokens: 1 }),
|
|
105
|
+
]);
|
|
106
|
+
assert.equal(rows.map((r) => r.provider), ['anthropic', 'gemini', undefined, undefined], 'an unrecognised or absent model leaves provider unset rather than guessing');
|
|
107
|
+
});
|
|
108
|
+
suite('splits the prompt into buckets the same way sumUsage does', () => {
|
|
109
|
+
const [row] = usageRows([
|
|
110
|
+
msg({
|
|
111
|
+
model: 'claude-sonnet-5',
|
|
112
|
+
inputTokens: 1000,
|
|
113
|
+
cacheReadTokens: 600,
|
|
114
|
+
cacheWriteTokens: 300,
|
|
115
|
+
}),
|
|
116
|
+
]);
|
|
117
|
+
assert.is(row.uncachedInputTokens, 100, 'the remainder, not an addition');
|
|
118
|
+
assert.is(row.cacheReadTokens, 600);
|
|
119
|
+
assert.is(row.cacheWriteTokens, 300);
|
|
120
|
+
});
|
|
121
|
+
suite('clamps a prompt whose cache buckets exceed the total', () => {
|
|
122
|
+
const [row] = usageRows([msg({ inputTokens: 100, cacheReadTokens: 900 })]);
|
|
123
|
+
assert.is(row.uncachedInputTokens, 0, 'never negative');
|
|
124
|
+
});
|
|
125
|
+
suite('leaves costUsd undefined when nothing was reported, rather than zero', () => {
|
|
126
|
+
// What the "carried usage but no cost" audit keys off. A zero here would read as a
|
|
127
|
+
// free call and the alarm would never fire.
|
|
128
|
+
const [row] = usageRows([msg({ model: 'claude-sonnet-5', inputTokens: 500, outputTokens: 10 })]);
|
|
129
|
+
assert.is(row.costUsd, undefined);
|
|
130
|
+
assert.is(row.source, 'request');
|
|
131
|
+
});
|
|
132
|
+
suite('records external (non-LLM) cost and counts it in the reconciliation', () => {
|
|
133
|
+
const history = [msg({ externalCostUsd: 0.4 }), msg({ model: 'claude-sonnet-5', cost: 0.1 })];
|
|
134
|
+
const rows = usageRows(history);
|
|
135
|
+
assert.is(rows.find((r) => r.externalCostUsd != null).externalCostUsd, 0.4);
|
|
136
|
+
const rowCost = rows.reduce((n, r) => { var _a, _b; return n + ((_a = r.costUsd) !== null && _a !== void 0 ? _a : 0) + ((_b = r.externalCostUsd) !== null && _b !== void 0 ? _b : 0); }, 0);
|
|
137
|
+
assert.ok(Math.abs(rowCost - sumUsage(history).costUsd) < 1e-12);
|
|
138
|
+
});
|
|
139
|
+
suite('produces no row for a message carrying no usage', () => {
|
|
140
|
+
assert.equal(usageRows([
|
|
141
|
+
msg({ role: 'user', content: 'hello' }),
|
|
142
|
+
msg({ content: 'thinking out loud', category: 'reasoning' }),
|
|
143
|
+
]), []);
|
|
144
|
+
});
|
|
145
|
+
suite('is empty for an empty transcript', () => {
|
|
146
|
+
assert.equal(usageRows([]), []);
|
|
147
|
+
});
|
|
148
|
+
suite('reconciles on a nested delegation two levels deep', () => {
|
|
149
|
+
const history = [
|
|
150
|
+
msg({
|
|
151
|
+
model: 'claude-sonnet-5',
|
|
152
|
+
cost: 0.01,
|
|
153
|
+
inputTokens: 100,
|
|
154
|
+
toolCalls: [
|
|
155
|
+
{
|
|
156
|
+
id: 'a',
|
|
157
|
+
name: 'delegate',
|
|
158
|
+
args: {},
|
|
159
|
+
subAgentTrace: [
|
|
160
|
+
msg({
|
|
161
|
+
model: 'claude-sonnet-5',
|
|
162
|
+
cost: 0.02,
|
|
163
|
+
inputTokens: 200,
|
|
164
|
+
toolCalls: [
|
|
165
|
+
{
|
|
166
|
+
id: 'b',
|
|
167
|
+
name: 'delegate',
|
|
168
|
+
args: {},
|
|
169
|
+
subAgentTrace: [msg({ model: 'gemini-2.5-flash', cost: 0.04, inputTokens: 400 })],
|
|
170
|
+
},
|
|
171
|
+
],
|
|
172
|
+
}),
|
|
173
|
+
],
|
|
174
|
+
},
|
|
175
|
+
],
|
|
176
|
+
}),
|
|
177
|
+
];
|
|
178
|
+
const rows = usageRows(history);
|
|
179
|
+
assert.equal(rows.map((r) => r.subAgentDepth), [0, 1, 2]);
|
|
180
|
+
const rowCost = rows.reduce((n, r) => { var _a; return n + ((_a = r.costUsd) !== null && _a !== void 0 ? _a : 0); }, 0);
|
|
181
|
+
assert.ok(Math.abs(rowCost - sumUsage(history).costUsd) < 1e-12, 'still reconciles');
|
|
182
|
+
});
|
|
183
|
+
suite('does not mutate the transcript', () => {
|
|
184
|
+
const history = richHistory();
|
|
185
|
+
const before = JSON.stringify(history);
|
|
186
|
+
usageRows(history);
|
|
187
|
+
assert.is(JSON.stringify(history), before, 'a projection, not a transform');
|
|
188
|
+
});
|
|
189
|
+
suite.run();
|
package/dist/react.cjs
CHANGED
|
@@ -49,8 +49,10 @@ const AiWavesIndicator = React.forwardRef(function AiWavesIndicator(props, ref)
|
|
|
49
49
|
});
|
|
50
50
|
|
|
51
51
|
const FoundationAiAssistant = React.forwardRef(function FoundationAiAssistant(props, ref) {
|
|
52
|
-
const { onChatHeaderMousedown, onSessionCleared, children, ...rest } = props;
|
|
52
|
+
const { onBudgetRefreshRequested, onChatHeaderMousedown, onSessionCleared, children, ...rest } = props;
|
|
53
53
|
const _innerRef = React.useRef(null);
|
|
54
|
+
const _onBudgetRefreshRequestedRef = React.useRef(onBudgetRefreshRequested);
|
|
55
|
+
_onBudgetRefreshRequestedRef.current = onBudgetRefreshRequested;
|
|
54
56
|
const _onChatHeaderMousedownRef = React.useRef(onChatHeaderMousedown);
|
|
55
57
|
_onChatHeaderMousedownRef.current = onChatHeaderMousedown;
|
|
56
58
|
const _onSessionClearedRef = React.useRef(onSessionCleared);
|
|
@@ -58,11 +60,14 @@ const FoundationAiAssistant = React.forwardRef(function FoundationAiAssistant(pr
|
|
|
58
60
|
React.useLayoutEffect(() => {
|
|
59
61
|
const el = _innerRef.current;
|
|
60
62
|
if (!el) return;
|
|
63
|
+
const _onBudgetRefreshRequestedFn = (e) => _onBudgetRefreshRequestedRef.current?.(e);
|
|
64
|
+
el.addEventListener('budget-refresh-requested', _onBudgetRefreshRequestedFn);
|
|
61
65
|
const _onChatHeaderMousedownFn = (e) => _onChatHeaderMousedownRef.current?.(e);
|
|
62
66
|
el.addEventListener('chat-header-mousedown', _onChatHeaderMousedownFn);
|
|
63
67
|
const _onSessionClearedFn = (e) => _onSessionClearedRef.current?.(e);
|
|
64
68
|
el.addEventListener('session-cleared', _onSessionClearedFn);
|
|
65
69
|
return () => {
|
|
70
|
+
el.removeEventListener('budget-refresh-requested', _onBudgetRefreshRequestedFn);
|
|
66
71
|
el.removeEventListener('chat-header-mousedown', _onChatHeaderMousedownFn);
|
|
67
72
|
el.removeEventListener('session-cleared', _onSessionClearedFn);
|
|
68
73
|
};
|
|
@@ -124,14 +129,14 @@ const AiChatInteractionWrapper = React.forwardRef(function AiChatInteractionWrap
|
|
|
124
129
|
return React.createElement(customElements.getName(AiChatInteractionWrapperWC) ?? 'ai-chat-interaction-wrapper', { ...rest, ref: _mergeRefs(_innerRef, ref) }, children);
|
|
125
130
|
});
|
|
126
131
|
|
|
127
|
-
const
|
|
132
|
+
const AiChatMarkdown = React.forwardRef(function AiChatMarkdown(props, ref) {
|
|
128
133
|
const { children, ...rest } = props;
|
|
129
|
-
return React.createElement(customElements.getName(
|
|
134
|
+
return React.createElement(customElements.getName(AiChatMarkdownWC) ?? 'ai-chat-markdown', { ...rest, ref }, children);
|
|
130
135
|
});
|
|
131
136
|
|
|
132
|
-
const
|
|
137
|
+
const FoundationAiPopoutManager = React.forwardRef(function FoundationAiPopoutManager(props, ref) {
|
|
133
138
|
const { children, ...rest } = props;
|
|
134
|
-
return React.createElement(customElements.getName(
|
|
139
|
+
return React.createElement(customElements.getName(FoundationAiPopoutManagerWC) ?? 'foundation-ai-popout-manager', { ...rest, ref }, children);
|
|
135
140
|
});
|
|
136
141
|
|
|
137
142
|
module.exports = {
|
|
@@ -145,6 +150,6 @@ module.exports = {
|
|
|
145
150
|
AgentPicker,
|
|
146
151
|
AiChatBubble,
|
|
147
152
|
AiChatInteractionWrapper,
|
|
148
|
-
FoundationAiPopoutManager,
|
|
149
153
|
AiChatMarkdown,
|
|
154
|
+
FoundationAiPopoutManager,
|
|
150
155
|
};
|
package/dist/react.mjs
CHANGED
|
@@ -47,8 +47,10 @@ export const AiWavesIndicator = React.forwardRef(function AiWavesIndicator(props
|
|
|
47
47
|
});
|
|
48
48
|
|
|
49
49
|
export const FoundationAiAssistant = React.forwardRef(function FoundationAiAssistant(props, ref) {
|
|
50
|
-
const { onChatHeaderMousedown, onSessionCleared, children, ...rest } = props;
|
|
50
|
+
const { onBudgetRefreshRequested, onChatHeaderMousedown, onSessionCleared, children, ...rest } = props;
|
|
51
51
|
const _innerRef = React.useRef(null);
|
|
52
|
+
const _onBudgetRefreshRequestedRef = React.useRef(onBudgetRefreshRequested);
|
|
53
|
+
_onBudgetRefreshRequestedRef.current = onBudgetRefreshRequested;
|
|
52
54
|
const _onChatHeaderMousedownRef = React.useRef(onChatHeaderMousedown);
|
|
53
55
|
_onChatHeaderMousedownRef.current = onChatHeaderMousedown;
|
|
54
56
|
const _onSessionClearedRef = React.useRef(onSessionCleared);
|
|
@@ -56,11 +58,14 @@ export const FoundationAiAssistant = React.forwardRef(function FoundationAiAssis
|
|
|
56
58
|
React.useLayoutEffect(() => {
|
|
57
59
|
const el = _innerRef.current;
|
|
58
60
|
if (!el) return;
|
|
61
|
+
const _onBudgetRefreshRequestedFn = (e) => _onBudgetRefreshRequestedRef.current?.(e);
|
|
62
|
+
el.addEventListener('budget-refresh-requested', _onBudgetRefreshRequestedFn);
|
|
59
63
|
const _onChatHeaderMousedownFn = (e) => _onChatHeaderMousedownRef.current?.(e);
|
|
60
64
|
el.addEventListener('chat-header-mousedown', _onChatHeaderMousedownFn);
|
|
61
65
|
const _onSessionClearedFn = (e) => _onSessionClearedRef.current?.(e);
|
|
62
66
|
el.addEventListener('session-cleared', _onSessionClearedFn);
|
|
63
67
|
return () => {
|
|
68
|
+
el.removeEventListener('budget-refresh-requested', _onBudgetRefreshRequestedFn);
|
|
64
69
|
el.removeEventListener('chat-header-mousedown', _onChatHeaderMousedownFn);
|
|
65
70
|
el.removeEventListener('session-cleared', _onSessionClearedFn);
|
|
66
71
|
};
|
|
@@ -122,12 +127,12 @@ export const AiChatInteractionWrapper = React.forwardRef(function AiChatInteract
|
|
|
122
127
|
return React.createElement(customElements.getName(AiChatInteractionWrapperWC) ?? 'ai-chat-interaction-wrapper', { ...rest, ref: _mergeRefs(_innerRef, ref) }, children);
|
|
123
128
|
});
|
|
124
129
|
|
|
125
|
-
export const
|
|
130
|
+
export const AiChatMarkdown = React.forwardRef(function AiChatMarkdown(props, ref) {
|
|
126
131
|
const { children, ...rest } = props;
|
|
127
|
-
return React.createElement(customElements.getName(
|
|
132
|
+
return React.createElement(customElements.getName(AiChatMarkdownWC) ?? 'ai-chat-markdown', { ...rest, ref }, children);
|
|
128
133
|
});
|
|
129
134
|
|
|
130
|
-
export const
|
|
135
|
+
export const FoundationAiPopoutManager = React.forwardRef(function FoundationAiPopoutManager(props, ref) {
|
|
131
136
|
const { children, ...rest } = props;
|
|
132
|
-
return React.createElement(customElements.getName(
|
|
137
|
+
return React.createElement(customElements.getName(FoundationAiPopoutManagerWC) ?? 'foundation-ai-popout-manager', { ...rest, ref }, children);
|
|
133
138
|
});
|