@genesislcap/ai-assistant 14.479.0 → 14.480.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 +546 -2
- package/dist/ai-assistant.d.ts +73 -1
- package/dist/dts/components/ai-driver/ai-driver.d.ts +8 -0
- package/dist/dts/components/ai-driver/ai-driver.d.ts.map +1 -1
- package/dist/dts/components/chat-driver/chat-driver.d.ts +32 -0
- package/dist/dts/components/chat-driver/chat-driver.d.ts.map +1 -1
- package/dist/dts/components/chat-driver/chat-driver.test.d.ts.map +1 -1
- package/dist/dts/components/chat-interaction-wrapper/chat-interaction-wrapper.d.ts +7 -0
- package/dist/dts/components/chat-interaction-wrapper/chat-interaction-wrapper.d.ts.map +1 -1
- package/dist/dts/components/flowing-waves-indicator.d.ts +1 -1
- package/dist/dts/components/flowing-waves-indicator.d.ts.map +1 -1
- package/dist/dts/components/halo-overlay.d.ts +1 -0
- package/dist/dts/components/halo-overlay.d.ts.map +1 -1
- package/dist/dts/components/orchestrating-driver/orchestrating-driver.d.ts +4 -0
- package/dist/dts/components/orchestrating-driver/orchestrating-driver.d.ts.map +1 -1
- 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.map +1 -1
- package/dist/dts/components/waves-indicator.d.ts +1 -1
- package/dist/dts/components/waves-indicator.d.ts.map +1 -1
- package/dist/dts/main/main.d.ts +29 -1
- package/dist/dts/main/main.d.ts.map +1 -1
- package/dist/dts/main/main.styles.d.ts.map +1 -1
- package/dist/dts/main/main.template.d.ts.map +1 -1
- package/dist/dts/state/ai-assistant-slice.d.ts +2 -0
- package/dist/dts/state/ai-assistant-slice.d.ts.map +1 -1
- package/dist/dts/state/session-store.d.ts +1 -0
- package/dist/dts/state/session-store.d.ts.map +1 -1
- package/dist/dts/utils/message-partition.d.ts +1 -0
- package/dist/dts/utils/message-partition.d.ts.map +1 -1
- package/dist/esm/components/chat-driver/chat-driver.js +88 -8
- package/dist/esm/components/chat-driver/chat-driver.test.js +104 -0
- package/dist/esm/components/chat-interaction-wrapper/chat-interaction-wrapper.js +48 -6
- package/dist/esm/components/chat-interaction-wrapper/chat-interaction-wrapper.test.js +62 -0
- package/dist/esm/components/flowing-waves-indicator.js +22 -11
- package/dist/esm/components/halo-overlay.js +30 -6
- package/dist/esm/components/orchestrating-driver/orchestrating-driver.js +14 -0
- package/dist/esm/components/settings-modal/settings-modal.styles.js +7 -0
- package/dist/esm/components/settings-modal/settings-modal.template.js +18 -8
- package/dist/esm/components/waves-indicator.js +22 -8
- package/dist/esm/main/main.js +73 -6
- package/dist/esm/main/main.styles.js +6 -0
- package/dist/esm/main/main.template.js +21 -2
- package/dist/esm/state/ai-assistant-slice.js +4 -0
- package/dist/esm/utils/message-partition.js +10 -1
- package/dist/esm/utils/message-partition.test.js +2 -0
- package/package.json +17 -17
- package/src/components/ai-driver/ai-driver.ts +17 -0
- package/src/components/chat-driver/chat-driver.test.ts +143 -0
- package/src/components/chat-driver/chat-driver.ts +105 -4
- package/src/components/chat-interaction-wrapper/chat-interaction-wrapper.test.ts +81 -0
- package/src/components/chat-interaction-wrapper/chat-interaction-wrapper.ts +51 -5
- package/src/components/flowing-waves-indicator.ts +26 -6
- package/src/components/halo-overlay.ts +31 -6
- package/src/components/orchestrating-driver/orchestrating-driver.ts +18 -0
- package/src/components/settings-modal/settings-modal.styles.ts +7 -0
- package/src/components/settings-modal/settings-modal.template.ts +36 -16
- package/src/components/waves-indicator.ts +25 -5
- package/src/main/main.styles.ts +6 -0
- package/src/main/main.template.ts +25 -2
- package/src/main/main.ts +65 -1
- package/src/state/ai-assistant-slice.ts +5 -0
- package/src/utils/message-partition.test.ts +2 -0
- package/src/utils/message-partition.ts +11 -1
|
@@ -12,6 +12,18 @@ const VIEWBOX = 120;
|
|
|
12
12
|
const CENTRE = VIEWBOX / 2;
|
|
13
13
|
/** Horizontal sampling step when tracing each wave path. Lower = smoother. */
|
|
14
14
|
const SAMPLE_STEP = 6;
|
|
15
|
+
/** Milliseconds in one second. */
|
|
16
|
+
const MS_PER_SECOND = 1000;
|
|
17
|
+
/** Baseline frame rate (fps) the wave speeds are tuned against. */
|
|
18
|
+
const BASELINE_FPS = 60;
|
|
19
|
+
/**
|
|
20
|
+
* Frame duration (ms) at the baseline rate. The paths are driven by elapsed
|
|
21
|
+
* wall-clock time divided by this — not by the rAF callback count — so they
|
|
22
|
+
* advance at a constant real-world rate regardless of the display's refresh
|
|
23
|
+
* rate or dropped frames. At the baseline rate the resulting frame index
|
|
24
|
+
* matches the old per-callback counter, preserving the current look.
|
|
25
|
+
*/
|
|
26
|
+
const FRAME_MS = MS_PER_SECOND / BASELINE_FPS;
|
|
15
27
|
const WAVES = [
|
|
16
28
|
{
|
|
17
29
|
colour: AI_COLOUR_AMBER,
|
|
@@ -75,10 +87,6 @@ let AiWavesIndicator = AiWavesIndicator_1 = class AiWavesIndicator extends Genes
|
|
|
75
87
|
super(...arguments);
|
|
76
88
|
/** Diameter of the circular window in px. Default: 56. */
|
|
77
89
|
this.size = WAVES_DEFAULT_SIZE;
|
|
78
|
-
// A rAF loop drives the wave paths for the same reason `<ai-halo-overlay>`
|
|
79
|
-
// hand-drives its rotation: a pure-CSS approach can't produce per-frame sine
|
|
80
|
-
// geometry, and SMIL/`<animate>` can't express the combined travel + slosh.
|
|
81
|
-
this.frame = 0;
|
|
82
90
|
}
|
|
83
91
|
sizeChanged() {
|
|
84
92
|
this.style.setProperty('--waves-size', `${this.size}px`);
|
|
@@ -96,8 +104,10 @@ let AiWavesIndicator = AiWavesIndicator_1 = class AiWavesIndicator extends Genes
|
|
|
96
104
|
cancelAnimationFrame(this.animFrame);
|
|
97
105
|
this.animFrame = undefined;
|
|
98
106
|
}
|
|
107
|
+
// Reset the clock so a later reconnect restarts the waves cleanly at frame 0.
|
|
108
|
+
this.startTime = undefined;
|
|
99
109
|
}
|
|
100
|
-
tick() {
|
|
110
|
+
tick(now) {
|
|
101
111
|
var _a, _b;
|
|
102
112
|
// Stop ticking once disconnected (belt-and-braces alongside the cancel in
|
|
103
113
|
// disconnectedCallback) so no frames are scheduled while detached.
|
|
@@ -110,11 +120,15 @@ let AiWavesIndicator = AiWavesIndicator_1 = class AiWavesIndicator extends Genes
|
|
|
110
120
|
if (paths === null || paths === void 0 ? void 0 : paths.length)
|
|
111
121
|
this.wavePaths = Array.from(paths);
|
|
112
122
|
}
|
|
123
|
+
// Anchor to the first real rAF timestamp; the initial synchronous call (no
|
|
124
|
+
// timestamp) renders frame 0. `frame` is then elapsed time in 60fps frames.
|
|
125
|
+
if (now !== undefined && this.startTime === undefined)
|
|
126
|
+
this.startTime = now;
|
|
127
|
+
const frame = now === undefined || this.startTime === undefined ? 0 : (now - this.startTime) / FRAME_MS;
|
|
113
128
|
(_b = this.wavePaths) === null || _b === void 0 ? void 0 : _b.forEach((path, i) => {
|
|
114
|
-
path.setAttribute('d', AiWavesIndicator_1.buildWavePath(WAVES[i],
|
|
129
|
+
path.setAttribute('d', AiWavesIndicator_1.buildWavePath(WAVES[i], frame));
|
|
115
130
|
});
|
|
116
|
-
this.
|
|
117
|
-
this.animFrame = requestAnimationFrame(() => this.tick());
|
|
131
|
+
this.animFrame = requestAnimationFrame((t) => this.tick(t));
|
|
118
132
|
}
|
|
119
133
|
/** Trace one wave's polyline `d` attribute for the given frame. */
|
|
120
134
|
static buildWavePath(cfg, frame) {
|
package/dist/esm/main/main.js
CHANGED
|
@@ -190,8 +190,20 @@ let FoundationAiAssistant = FoundationAiAssistant_1 = class FoundationAiAssistan
|
|
|
190
190
|
* to avoid stealing focus from elsewhere on the page.
|
|
191
191
|
*/
|
|
192
192
|
this.isEngaged = true;
|
|
193
|
+
/**
|
|
194
|
+
* GENC-1410 (Error 3): stable id for this assistant view. Reported to the shared driver so it
|
|
195
|
+
* can elect a single owner to render (and run) each interaction's widget when more than one view
|
|
196
|
+
* (e.g. a bubble and a docked panel) is wired to the same driver — so it fires once, not per view.
|
|
197
|
+
*/
|
|
198
|
+
this.hostId = crypto.randomUUID();
|
|
193
199
|
this._handleGlobalInteraction = (e) => {
|
|
200
|
+
var _a, _b;
|
|
194
201
|
const next = e.composedPath().includes(this);
|
|
202
|
+
// GENC-1410: sticky "last active host" — whenever the user interacts inside THIS view, claim
|
|
203
|
+
// ownership on the shared driver. Unlike `isEngaged` this does NOT clear when the user later
|
|
204
|
+
// clicks elsewhere in the layout; it only moves when they use a *different* assistant view.
|
|
205
|
+
if (next)
|
|
206
|
+
(_b = (_a = this.driver) === null || _a === void 0 ? void 0 : _a.setActiveHost) === null || _b === void 0 ? void 0 : _b.call(_a, this.hostId);
|
|
195
207
|
if (next === this.isEngaged)
|
|
196
208
|
return;
|
|
197
209
|
this.isEngaged = next;
|
|
@@ -368,6 +380,15 @@ let FoundationAiAssistant = FoundationAiAssistant_1 = class FoundationAiAssistan
|
|
|
368
380
|
var _a;
|
|
369
381
|
(_a = this._sessionRef) === null || _a === void 0 ? void 0 : _a.actions.aiAssistant.setShowThinkingSteps(value);
|
|
370
382
|
}
|
|
383
|
+
/** Current user-facing toggle state for narration (interstitial prose) visibility. */
|
|
384
|
+
get showNarration() {
|
|
385
|
+
var _a, _b;
|
|
386
|
+
return (_b = (_a = this._sessionRef) === null || _a === void 0 ? void 0 : _a.store.aiAssistant.showNarration) !== null && _b !== void 0 ? _b : false;
|
|
387
|
+
}
|
|
388
|
+
set showNarration(value) {
|
|
389
|
+
var _a;
|
|
390
|
+
(_a = this._sessionRef) === null || _a === void 0 ? void 0 : _a.actions.aiAssistant.setShowNarration(value);
|
|
391
|
+
}
|
|
371
392
|
/** Current user-facing toggle state for agent switch indicator visibility. */
|
|
372
393
|
get showAgentSwitchIndicator() {
|
|
373
394
|
var _a, _b;
|
|
@@ -687,7 +708,7 @@ let FoundationAiAssistant = FoundationAiAssistant_1 = class FoundationAiAssistan
|
|
|
687
708
|
this.showHalo = 'agent';
|
|
688
709
|
}
|
|
689
710
|
}
|
|
690
|
-
/** True when there is a pending (unresolved) interaction
|
|
711
|
+
/** True when there is a pending (unresolved) interaction. */
|
|
691
712
|
get hasActivePendingInteraction() {
|
|
692
713
|
if (!this.busy)
|
|
693
714
|
return false;
|
|
@@ -741,6 +762,7 @@ let FoundationAiAssistant = FoundationAiAssistant_1 = class FoundationAiAssistan
|
|
|
741
762
|
return filterVisibleMessages(listed, {
|
|
742
763
|
showToolCalls: this.showToolCalls,
|
|
743
764
|
showThinkingSteps: this.showThinkingSteps,
|
|
765
|
+
showNarration: this.showNarration,
|
|
744
766
|
showAgentSwitchIndicator,
|
|
745
767
|
});
|
|
746
768
|
}
|
|
@@ -916,11 +938,15 @@ let FoundationAiAssistant = FoundationAiAssistant_1 = class FoundationAiAssistan
|
|
|
916
938
|
* times — unwires any existing listeners first.
|
|
917
939
|
*/
|
|
918
940
|
wireDriver() {
|
|
941
|
+
var _a;
|
|
919
942
|
const { driver } = this;
|
|
920
943
|
if (!driver)
|
|
921
944
|
return;
|
|
922
945
|
// Idempotent — unwire previous listeners on this component before re-wiring.
|
|
923
946
|
this.unwireDriver();
|
|
947
|
+
// GENC-1410 (Error 3): announce this view to the (shared) driver so it can elect a single
|
|
948
|
+
// owner for side-effecting interactions when more than one view is wired to it.
|
|
949
|
+
(_a = driver.registerHost) === null || _a === void 0 ? void 0 : _a.call(driver, this.hostId);
|
|
924
950
|
const onHistoryUpdated = (e) => {
|
|
925
951
|
this.messages = [...e.detail];
|
|
926
952
|
};
|
|
@@ -1002,6 +1028,7 @@ let FoundationAiAssistant = FoundationAiAssistant_1 = class FoundationAiAssistan
|
|
|
1002
1028
|
driver.addEventListener('interaction-stop', onInteractionStop);
|
|
1003
1029
|
driver.addEventListener('provider-changed', onProviderChanged);
|
|
1004
1030
|
const cleanups = [
|
|
1031
|
+
() => { var _a; return (_a = driver.unregisterHost) === null || _a === void 0 ? void 0 : _a.call(driver, this.hostId); },
|
|
1005
1032
|
() => driver.removeEventListener('history-updated', onHistoryUpdated),
|
|
1006
1033
|
() => driver.removeEventListener('sub-agent-history-updated', onSubAgentHistoryUpdated),
|
|
1007
1034
|
() => driver.removeEventListener('sub-agent-start', onSubAgentStart),
|
|
@@ -1071,7 +1098,7 @@ let FoundationAiAssistant = FoundationAiAssistant_1 = class FoundationAiAssistan
|
|
|
1071
1098
|
this.driverCleanup = undefined;
|
|
1072
1099
|
}
|
|
1073
1100
|
connectedCallback() {
|
|
1074
|
-
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
|
|
1101
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m;
|
|
1075
1102
|
// Initialise the store reference BEFORE super.connectedCallback() so that
|
|
1076
1103
|
// the first FAST render has access to the store. The store Proxy calls
|
|
1077
1104
|
// Observable.track(observableStore, sliceName) whenever a slice is read,
|
|
@@ -1087,6 +1114,7 @@ let FoundationAiAssistant = FoundationAiAssistant_1 = class FoundationAiAssistan
|
|
|
1087
1114
|
const ui = (_a = this.chatConfig.ui) !== null && _a !== void 0 ? _a : {};
|
|
1088
1115
|
this.showToolCalls = ui.showToolCalls === true;
|
|
1089
1116
|
this.showThinkingSteps = ui.showThinkingSteps === true;
|
|
1117
|
+
this.showNarration = ui.showNarration === true;
|
|
1090
1118
|
this.showAgentSwitchIndicator = ui.showAgentSwitchIndicator === true;
|
|
1091
1119
|
this.enabledAnimations = resolveExclusiveLoadingStyle((_c = (_b = ui.animations) === null || _b === void 0 ? void 0 : _b.enabled) !== null && _c !== void 0 ? _c : (ui.animations ? [...DEFAULT_ANIMATIONS] : []));
|
|
1092
1120
|
const defaultAgent = (_d = this.chatConfig.picker) === null || _d === void 0 ? void 0 : _d.defaultAgent;
|
|
@@ -1109,9 +1137,15 @@ let FoundationAiAssistant = FoundationAiAssistant_1 = class FoundationAiAssistan
|
|
|
1109
1137
|
this._finalizeCostSessionOnPageHide = false;
|
|
1110
1138
|
});
|
|
1111
1139
|
const unsubPopin = agenticActivityBus.subscribe('chat-popin', () => {
|
|
1140
|
+
var _a, _b;
|
|
1112
1141
|
this.logMeta('driver.wired', { reason: 'popin' });
|
|
1113
1142
|
this.wireDriver();
|
|
1114
1143
|
this._finalizeCostSessionOnPageHide = true;
|
|
1144
|
+
// GENC-1410: popin makes THIS (bubble) view the visible one. Take interaction ownership on
|
|
1145
|
+
// the shared driver so it elects this view to render widgets — not the now-hidden docked
|
|
1146
|
+
// panel. Ownership transfers as part of the dock lifecycle, deterministically, rather than
|
|
1147
|
+
// being inferred from wherever the user last happened to click.
|
|
1148
|
+
(_b = (_a = this.driver) === null || _a === void 0 ? void 0 : _a.setActiveHost) === null || _b === void 0 ? void 0 : _b.call(_a, this.hostId);
|
|
1115
1149
|
});
|
|
1116
1150
|
this.unsubBus = () => {
|
|
1117
1151
|
unsubPopout();
|
|
@@ -1120,6 +1154,11 @@ let FoundationAiAssistant = FoundationAiAssistant_1 = class FoundationAiAssistan
|
|
|
1120
1154
|
}
|
|
1121
1155
|
else if (this.popoutMode === 'collapse') {
|
|
1122
1156
|
this._finalizeCostSessionOnPageHide = true;
|
|
1157
|
+
// GENC-1410: a collapse-mode view exists only while docked, and it connects (running this
|
|
1158
|
+
// setup) as part of `onDock` — the moment it becomes the visible view. Take interaction
|
|
1159
|
+
// ownership now so the shared driver renders widgets here, not in the hidden bubble. This is
|
|
1160
|
+
// the dock counterpart to the bubble's ownership claim on `chat-popin` above.
|
|
1161
|
+
(_g = (_f = this.driver) === null || _f === void 0 ? void 0 : _f.setActiveHost) === null || _g === void 0 ? void 0 : _g.call(_f, this.hostId);
|
|
1123
1162
|
const unsubPopout = agenticActivityBus.subscribe('chat-popout', () => {
|
|
1124
1163
|
this._finalizeCostSessionOnPageHide = true;
|
|
1125
1164
|
});
|
|
@@ -1134,13 +1173,13 @@ let FoundationAiAssistant = FoundationAiAssistant_1 = class FoundationAiAssistan
|
|
|
1134
1173
|
this.syncShowingSplash();
|
|
1135
1174
|
// Restore loading state if the driver is still executing mid-lifecycle.
|
|
1136
1175
|
// disconnectedCallback resets state to 'idle', so we must check real driver state here.
|
|
1137
|
-
if ((
|
|
1176
|
+
if ((_h = this.driver) === null || _h === void 0 ? void 0 : _h.isBusy()) {
|
|
1138
1177
|
this.state = 'loading';
|
|
1139
1178
|
this.startLoadingTimer();
|
|
1140
1179
|
// Subscribe once so that when the originating send() completes (possibly on a
|
|
1141
1180
|
// different element instance that has since disconnected), this element cleans up
|
|
1142
1181
|
// its own timer, syncs the halo, and triggers the post-response suggestion fetch.
|
|
1143
|
-
this._executionCompletionUnsub = (
|
|
1182
|
+
this._executionCompletionUnsub = (_j = this._sessionRef) === null || _j === void 0 ? void 0 : _j.subscribeKey((s) => s.aiAssistant.state, () => {
|
|
1144
1183
|
var _a, _b;
|
|
1145
1184
|
if (((_a = this._sessionRef) === null || _a === void 0 ? void 0 : _a.store.aiAssistant.state) === 'idle') {
|
|
1146
1185
|
this.stopLoadingTimer();
|
|
@@ -1163,7 +1202,7 @@ let FoundationAiAssistant = FoundationAiAssistant_1 = class FoundationAiAssistan
|
|
|
1163
1202
|
// `provider-changed`. Feature-detected: a no-op for immutable registries.
|
|
1164
1203
|
// Re-subscribed per connect (docking/popout remounts); balanced in
|
|
1165
1204
|
// disconnectedCallback.
|
|
1166
|
-
(
|
|
1205
|
+
(_k = this.unsubProviderRegistry) === null || _k === void 0 ? void 0 : _k.call(this);
|
|
1167
1206
|
this.unsubProviderRegistry = undefined;
|
|
1168
1207
|
if (isObservableAIProviderRegistry(this.providerRegistry)) {
|
|
1169
1208
|
this.unsubProviderRegistry = this.providerRegistry.subscribe(() => {
|
|
@@ -1199,7 +1238,7 @@ let FoundationAiAssistant = FoundationAiAssistant_1 = class FoundationAiAssistan
|
|
|
1199
1238
|
restoredMessages: this.messages.length,
|
|
1200
1239
|
driver: driverExisted ? 'reused' : 'created',
|
|
1201
1240
|
driverKind: this.driver instanceof OrchestratingDriver ? 'orchestrating' : 'chat',
|
|
1202
|
-
driverBusy: (
|
|
1241
|
+
driverBusy: (_m = (_l = this.driver) === null || _l === void 0 ? void 0 : _l.isBusy()) !== null && _m !== void 0 ? _m : false,
|
|
1203
1242
|
});
|
|
1204
1243
|
}
|
|
1205
1244
|
disconnectedCallback() {
|
|
@@ -1414,6 +1453,30 @@ let FoundationAiAssistant = FoundationAiAssistant_1 = class FoundationAiAssistan
|
|
|
1414
1453
|
this.showLoadingIndicator = false;
|
|
1415
1454
|
}
|
|
1416
1455
|
/** Called when the user clicks the popout button. */
|
|
1456
|
+
/**
|
|
1457
|
+
* GENC-1410 (Error 3): claim the right to run a side-effecting interaction's server work for
|
|
1458
|
+
* THIS view, via the shared driver's first-writer-wins latch. Returns `true` when this view may
|
|
1459
|
+
* run it (first/owning claimant, or a re-render of the same view), `false` when another view
|
|
1460
|
+
* already holds it. Fail-open when the driver has no latch. Called by `ai-chat-interaction-wrapper`.
|
|
1461
|
+
*/
|
|
1462
|
+
claimSideEffect(interactionId) {
|
|
1463
|
+
var _a, _b, _c;
|
|
1464
|
+
return (_c = (_b = (_a = this.driver) === null || _a === void 0 ? void 0 : _a.claimSideEffect) === null || _b === void 0 ? void 0 : _b.call(_a, interactionId, this.hostId)) !== null && _c !== void 0 ? _c : true;
|
|
1465
|
+
}
|
|
1466
|
+
/**
|
|
1467
|
+
* GENC-1410 (Error 3): mark THIS view as the interaction owner on the shared driver, so it (not a
|
|
1468
|
+
* sibling view sharing the same driver) is elected to render side-effecting widgets. The bubble
|
|
1469
|
+
* and docked panel normally claim this automatically on the pop-in / dock lifecycle, but that
|
|
1470
|
+
* lifecycle only runs once `popout-mode` is active. A host that establishes a view outside that
|
|
1471
|
+
* lifecycle — e.g. a page that restores a docked assistant from a persisted layout before the
|
|
1472
|
+
* pop-out affordance is enabled — calls this to seize ownership deterministically rather than
|
|
1473
|
+
* leaving it to the last-interaction / first-registered fallback. Idempotent; safe to call on
|
|
1474
|
+
* every layout (re)establish. No-op if the driver was never wired.
|
|
1475
|
+
*/
|
|
1476
|
+
claimInteractionOwnership() {
|
|
1477
|
+
var _a, _b;
|
|
1478
|
+
(_b = (_a = this.driver) === null || _a === void 0 ? void 0 : _a.setActiveHost) === null || _b === void 0 ? void 0 : _b.call(_a, this.hostId);
|
|
1479
|
+
}
|
|
1417
1480
|
handlePopout() {
|
|
1418
1481
|
if (this.popoutMode === 'expand') {
|
|
1419
1482
|
this.logMeta('assistant.popout');
|
|
@@ -1573,6 +1636,7 @@ let FoundationAiAssistant = FoundationAiAssistant_1 = class FoundationAiAssistan
|
|
|
1573
1636
|
!!this.headerTitle ||
|
|
1574
1637
|
(ui === null || ui === void 0 ? void 0 : ui.showToolCalls) != null ||
|
|
1575
1638
|
(ui === null || ui === void 0 ? void 0 : ui.showThinkingSteps) != null ||
|
|
1639
|
+
(ui === null || ui === void 0 ? void 0 : ui.showNarration) != null ||
|
|
1576
1640
|
(ui === null || ui === void 0 ? void 0 : ui.showAgentSwitchIndicator) != null ||
|
|
1577
1641
|
(ui === null || ui === void 0 ? void 0 : ui.allowDebugDownload) === true ||
|
|
1578
1642
|
(ui === null || ui === void 0 ? void 0 : ui.animations) != null ||
|
|
@@ -1754,6 +1818,9 @@ let FoundationAiAssistant = FoundationAiAssistant_1 = class FoundationAiAssistan
|
|
|
1754
1818
|
toggleShowThinkingSteps() {
|
|
1755
1819
|
this.showThinkingSteps = !this.showThinkingSteps;
|
|
1756
1820
|
}
|
|
1821
|
+
toggleShowNarration() {
|
|
1822
|
+
this.showNarration = !this.showNarration;
|
|
1823
|
+
}
|
|
1757
1824
|
toggleShowAgentSwitchIndicator() {
|
|
1758
1825
|
this.showAgentSwitchIndicator = !this.showAgentSwitchIndicator;
|
|
1759
1826
|
}
|
|
@@ -273,6 +273,12 @@ const baseStyles = css `
|
|
|
273
273
|
font-size: 0.95em;
|
|
274
274
|
}
|
|
275
275
|
|
|
276
|
+
/* Narration shares the Thinking bubble but is the assistant's commentary (labelled "Working"),
|
|
277
|
+
not its inner monologue — keep it upright so it reads as action, distinct from italic reasoning. */
|
|
278
|
+
.message.ai-thinking.narration {
|
|
279
|
+
font-style: normal;
|
|
280
|
+
}
|
|
281
|
+
|
|
276
282
|
.message.ai-function {
|
|
277
283
|
background: linear-gradient(135deg, rgb(15 32 39 / 90%) 0%, rgb(30 58 58 / 80%) 100%);
|
|
278
284
|
color: var(--ai-function-color, #86efac);
|
|
@@ -76,6 +76,12 @@ const messageType = (m) => {
|
|
|
76
76
|
// outcome; render it identically to a real user message.
|
|
77
77
|
case m.role === 'synthetic-user':
|
|
78
78
|
return 'user';
|
|
79
|
+
// Reasoning (chain-of-thought summary) and narration (interstitial prose the model emits
|
|
80
|
+
// alongside a tool call) both render as the "Thinking" bubble — visually set apart from a real
|
|
81
|
+
// final agent reply, even though they stay separate categories (own log entry + own toggle:
|
|
82
|
+
// `showThinkingSteps` vs `showNarration`). `thinking` is the legacy flag for pre-split sessions.
|
|
83
|
+
case m.category === 'reasoning':
|
|
84
|
+
case m.category === 'narration':
|
|
79
85
|
case !!m.thinking:
|
|
80
86
|
return 'ai-thinking';
|
|
81
87
|
case !!m.toolCalls:
|
|
@@ -93,6 +99,12 @@ const senderLabel = {
|
|
|
93
99
|
interaction: 'Assistant',
|
|
94
100
|
ai: 'Assistant',
|
|
95
101
|
};
|
|
102
|
+
/**
|
|
103
|
+
* Sender label for a message. Reasoning and narration share the "Thinking" bubble, so the label is
|
|
104
|
+
* the only thing that tells them apart: narration reads "Working" (the action it narrates alongside
|
|
105
|
+
* its tool call), reasoning stays "Thinking". Everything else uses the {@link messageType} map.
|
|
106
|
+
*/
|
|
107
|
+
const senderLabelFor = (m) => m.category === 'narration' ? 'Working' : senderLabel[messageType(m)];
|
|
96
108
|
/**
|
|
97
109
|
* Resolves how the host frames an interaction widget, defaulting to `'label'`
|
|
98
110
|
* (the historical "Assistant" label, no bubble). Callers guard on
|
|
@@ -224,7 +236,9 @@ ${(tc) => { var _a; return (((_a = tc.foldPath) === null || _a === void 0 ? void
|
|
|
224
236
|
// assistant one.
|
|
225
237
|
(m) => messageType(m) === 'user', userAvatarTemplate)}${when((m) => messageType(m) !== 'user', assistantAvatarTemplate)}
|
|
226
238
|
</div>
|
|
227
|
-
<div
|
|
239
|
+
<div
|
|
240
|
+
class="message ${(m) => messageType(m)} ${(m) => classNames(['narration', m.category === 'narration'])}"
|
|
241
|
+
>
|
|
228
242
|
${when(
|
|
229
243
|
// A 'bare' interaction owns its full presentation — suppress the
|
|
230
244
|
// host sender label. Every other message keeps it.
|
|
@@ -236,7 +250,7 @@ ${(tc) => { var _a; return (((_a = tc.foldPath) === null || _a === void 0 ? void
|
|
|
236
250
|
m.agentName &&
|
|
237
251
|
c.parent.showAgentSwitchIndicator
|
|
238
252
|
? `Tool Call · ${(_a = m.agentLabel) !== null && _a !== void 0 ? _a : m.agentName}`
|
|
239
|
-
:
|
|
253
|
+
: senderLabelFor(m);
|
|
240
254
|
}}
|
|
241
255
|
</div>
|
|
242
256
|
`)}
|
|
@@ -249,6 +263,11 @@ ${(tc) => { var _a; return (((_a = tc.foldPath) === null || _a === void 0 ? void
|
|
|
249
263
|
`)}
|
|
250
264
|
${when((m) => m.interaction, html `
|
|
251
265
|
<ai-chat-interaction-wrapper
|
|
266
|
+
${
|
|
267
|
+
/* GENC-1410: bind ownerHostId BEFORE componentName/interactionId — bindings
|
|
268
|
+
apply in template order and both trigger renderComponent, so the ownership
|
|
269
|
+
gate prop must be set by the time the first render runs. */ ''}
|
|
270
|
+
:ownerHostId=${(m) => { var _a; return (_a = m.interaction.ownerHostId) !== null && _a !== void 0 ? _a : ''; }}
|
|
252
271
|
:componentName=${(m) => m.interaction.componentName}
|
|
253
272
|
:data=${(m) => m.interaction.data}
|
|
254
273
|
:interactionId=${(m) => m.interaction.interactionId}
|
|
@@ -4,6 +4,7 @@ export const defaultSessionState = {
|
|
|
4
4
|
state: 'idle',
|
|
5
5
|
showToolCalls: false,
|
|
6
6
|
showThinkingSteps: false,
|
|
7
|
+
showNarration: false,
|
|
7
8
|
showAgentSwitchIndicator: false,
|
|
8
9
|
enabledAnimations: [],
|
|
9
10
|
suggestionsState: { status: 'idle' },
|
|
@@ -39,6 +40,9 @@ export const aiAssistantSlice = createSlice({
|
|
|
39
40
|
setShowThinkingSteps(state, action) {
|
|
40
41
|
state.showThinkingSteps = action.payload;
|
|
41
42
|
},
|
|
43
|
+
setShowNarration(state, action) {
|
|
44
|
+
state.showNarration = action.payload;
|
|
45
|
+
},
|
|
42
46
|
setShowAgentSwitchIndicator(state, action) {
|
|
43
47
|
state.showAgentSwitchIndicator = action.payload;
|
|
44
48
|
},
|
|
@@ -35,7 +35,16 @@ export function filterVisibleMessages(messages, toggles) {
|
|
|
35
35
|
if (m.role === 'tool') {
|
|
36
36
|
return false;
|
|
37
37
|
}
|
|
38
|
-
//
|
|
38
|
+
// Reasoning (chain-of-thought summary) follows the thinking toggle.
|
|
39
|
+
if (m.category === 'reasoning') {
|
|
40
|
+
return toggles.showThinkingSteps;
|
|
41
|
+
}
|
|
42
|
+
// Narration (interstitial prose alongside a tool call) follows its own toggle.
|
|
43
|
+
if (m.category === 'narration') {
|
|
44
|
+
return toggles.showNarration;
|
|
45
|
+
}
|
|
46
|
+
// Legacy: sessions persisted before the reasoning/narration split tagged reasoning-ish
|
|
47
|
+
// content with `thinking`; keep honouring the flag so their history still hides it.
|
|
39
48
|
if (m.thinking && !toggles.showThinkingSteps) {
|
|
40
49
|
return false;
|
|
41
50
|
}
|
|
@@ -5,11 +5,13 @@ const interaction = (interactionId, resolved = false) => (Object.assign({ intera
|
|
|
5
5
|
const ALL_ON = {
|
|
6
6
|
showToolCalls: true,
|
|
7
7
|
showThinkingSteps: true,
|
|
8
|
+
showNarration: true,
|
|
8
9
|
showAgentSwitchIndicator: true,
|
|
9
10
|
};
|
|
10
11
|
const ALL_OFF = {
|
|
11
12
|
showToolCalls: false,
|
|
12
13
|
showThinkingSteps: false,
|
|
14
|
+
showNarration: false,
|
|
13
15
|
showAgentSwitchIndicator: false,
|
|
14
16
|
};
|
|
15
17
|
const trailing = createLogicSuite('trailingInteractionRow');
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@genesislcap/ai-assistant",
|
|
3
3
|
"description": "Genesis AI Assistant micro-frontend",
|
|
4
|
-
"version": "14.
|
|
4
|
+
"version": "14.480.0",
|
|
5
5
|
"license": "SEE LICENSE IN license.txt",
|
|
6
6
|
"main": "dist/esm/index.js",
|
|
7
7
|
"types": "dist/ai-assistant.d.ts",
|
|
@@ -64,25 +64,25 @@
|
|
|
64
64
|
}
|
|
65
65
|
},
|
|
66
66
|
"devDependencies": {
|
|
67
|
-
"@genesislcap/foundation-testing": "14.
|
|
68
|
-
"@genesislcap/genx": "14.
|
|
69
|
-
"@genesislcap/rollup-builder": "14.
|
|
70
|
-
"@genesislcap/ts-builder": "14.
|
|
71
|
-
"@genesislcap/uvu-playwright-builder": "14.
|
|
72
|
-
"@genesislcap/vite-builder": "14.
|
|
73
|
-
"@genesislcap/webpack-builder": "14.
|
|
67
|
+
"@genesislcap/foundation-testing": "14.480.0",
|
|
68
|
+
"@genesislcap/genx": "14.480.0",
|
|
69
|
+
"@genesislcap/rollup-builder": "14.480.0",
|
|
70
|
+
"@genesislcap/ts-builder": "14.480.0",
|
|
71
|
+
"@genesislcap/uvu-playwright-builder": "14.480.0",
|
|
72
|
+
"@genesislcap/vite-builder": "14.480.0",
|
|
73
|
+
"@genesislcap/webpack-builder": "14.480.0",
|
|
74
74
|
"@types/dompurify": "^3.0.5",
|
|
75
75
|
"@types/marked": "^5.0.2"
|
|
76
76
|
},
|
|
77
77
|
"dependencies": {
|
|
78
|
-
"@genesislcap/foundation-ai": "14.
|
|
79
|
-
"@genesislcap/foundation-logger": "14.
|
|
80
|
-
"@genesislcap/foundation-notifications": "14.
|
|
81
|
-
"@genesislcap/foundation-redux": "14.
|
|
82
|
-
"@genesislcap/foundation-ui": "14.
|
|
83
|
-
"@genesislcap/foundation-utils": "14.
|
|
84
|
-
"@genesislcap/rapid-design-system": "14.
|
|
85
|
-
"@genesislcap/web-core": "14.
|
|
78
|
+
"@genesislcap/foundation-ai": "14.480.0",
|
|
79
|
+
"@genesislcap/foundation-logger": "14.480.0",
|
|
80
|
+
"@genesislcap/foundation-notifications": "14.480.0",
|
|
81
|
+
"@genesislcap/foundation-redux": "14.480.0",
|
|
82
|
+
"@genesislcap/foundation-ui": "14.480.0",
|
|
83
|
+
"@genesislcap/foundation-utils": "14.480.0",
|
|
84
|
+
"@genesislcap/rapid-design-system": "14.480.0",
|
|
85
|
+
"@genesislcap/web-core": "14.480.0",
|
|
86
86
|
"dompurify": "^3.3.1",
|
|
87
87
|
"marked": "^17.0.3"
|
|
88
88
|
},
|
|
@@ -94,5 +94,5 @@
|
|
|
94
94
|
"publishConfig": {
|
|
95
95
|
"access": "public"
|
|
96
96
|
},
|
|
97
|
-
"gitHead": "
|
|
97
|
+
"gitHead": "6d2bc60f9700ab9f5a6d8c1834baebbd66ae5e6b"
|
|
98
98
|
}
|
|
@@ -98,4 +98,21 @@ export interface AiDriver extends EventTarget {
|
|
|
98
98
|
* Optional; the host wires the stop button to it when present.
|
|
99
99
|
*/
|
|
100
100
|
cancel?(): void;
|
|
101
|
+
|
|
102
|
+
// --- GENC-1410 (Error 3): single-instance execution of interaction widgets ------------------
|
|
103
|
+
// When several assistant views share one driver, these coordinate which single view renders
|
|
104
|
+
// (and thus runs) each interaction's widget, so its side effect fires once. Optional — a driver
|
|
105
|
+
// that never hosts duplicate views can omit them; the host calls with `?.`.
|
|
106
|
+
|
|
107
|
+
/** Register an assistant view (host) wired to this driver. */
|
|
108
|
+
registerHost?(hostId: string): void;
|
|
109
|
+
|
|
110
|
+
/** Deregister a host on unwire/disconnect. */
|
|
111
|
+
unregisterHost?(hostId: string): void;
|
|
112
|
+
|
|
113
|
+
/** Record the host whose subtree the user most recently interacted in (owner election signal). */
|
|
114
|
+
setActiveHost?(hostId: string): void;
|
|
115
|
+
|
|
116
|
+
/** First-writer-wins latch: the first host to claim an interaction id runs its side effect. */
|
|
117
|
+
claimSideEffect?(interactionId: string, hostId: string): boolean;
|
|
101
118
|
}
|
|
@@ -14,6 +14,8 @@ import { assert, createLogicSuite } from '@genesislcap/foundation-testing';
|
|
|
14
14
|
import { agenticActivityBus } from '../../channel/ai-activity-bus';
|
|
15
15
|
import type { AgentConfig } from '../../config/config';
|
|
16
16
|
import { clearMetaEventRegistry, getMetaEvents } from '../../state/debug-event-log';
|
|
17
|
+
import { sumCosts } from '../../utils/sum-costs';
|
|
18
|
+
import { sumTokens } from '../../utils/sum-tokens';
|
|
17
19
|
import { createToolFold } from '../../utils/tool-fold';
|
|
18
20
|
// Side-effect import — MUST come before `./chat-driver` so the driver subclasses
|
|
19
21
|
// jsdom's EventTarget rather than Node's native one (see the file). None of the
|
|
@@ -1080,6 +1082,76 @@ subagent(
|
|
|
1080
1082
|
|
|
1081
1083
|
subagent.run();
|
|
1082
1084
|
|
|
1085
|
+
// ---------------------------------------------------------------------------
|
|
1086
|
+
// cost / token attribution on the thinking-step split (GENC-1410)
|
|
1087
|
+
// ---------------------------------------------------------------------------
|
|
1088
|
+
|
|
1089
|
+
const costAttribution = createLogicSuite('ChatDriver cost attribution (thinking-step split)');
|
|
1090
|
+
|
|
1091
|
+
costAttribution(
|
|
1092
|
+
'a text-plus-tool-call response contributes its cost and tokens exactly once',
|
|
1093
|
+
async () => {
|
|
1094
|
+
const config = agent({
|
|
1095
|
+
name: 'Worker',
|
|
1096
|
+
toolDefinitions: () => [def('do_thing')],
|
|
1097
|
+
toolHandlers: () => ({ do_thing: async () => 'ok' }),
|
|
1098
|
+
});
|
|
1099
|
+
// A single response that emits reasoning + interstitial narration + a tool call, carrying that
|
|
1100
|
+
// response's usage. The driver splits it into three messages — a `reasoning` bubble, a
|
|
1101
|
+
// `narration` bubble, and the tool-call message — but only ONE may carry the cost/tokens or the
|
|
1102
|
+
// session totals double-count this one response (GENC-1410).
|
|
1103
|
+
const provider = scriptedProvider([
|
|
1104
|
+
{
|
|
1105
|
+
role: 'assistant',
|
|
1106
|
+
content: 'Let me act.',
|
|
1107
|
+
reasoning: 'Let me think.',
|
|
1108
|
+
toolCalls: [{ id: 't1', name: 'do_thing', args: {} }],
|
|
1109
|
+
cost: 0.05,
|
|
1110
|
+
inputTokens: 1000,
|
|
1111
|
+
outputTokens: 50,
|
|
1112
|
+
},
|
|
1113
|
+
// Turn-ending reply (no tool calls, no usage) so the loop terminates cleanly.
|
|
1114
|
+
{ role: 'assistant', content: 'Done.' },
|
|
1115
|
+
]);
|
|
1116
|
+
const driver = makeDriver(config, provider);
|
|
1117
|
+
|
|
1118
|
+
await driver.sendMessage('go');
|
|
1119
|
+
|
|
1120
|
+
const history = driver.getHistory();
|
|
1121
|
+
const reasoning = history.find((m) => m.category === 'reasoning');
|
|
1122
|
+
const narration = history.find((m) => m.category === 'narration');
|
|
1123
|
+
const toolCallMsg = history.find((m) => m.toolCalls?.some((c) => c.name === 'do_thing'));
|
|
1124
|
+
|
|
1125
|
+
// The one response was split into reasoning + narration bubbles + the tool-call message.
|
|
1126
|
+
assert.ok(reasoning, 'a reasoning bubble is emitted for the chain-of-thought summary');
|
|
1127
|
+
assert.is(reasoning!.content, 'Let me think.');
|
|
1128
|
+
assert.ok(narration, 'a narration bubble is emitted for the interstitial prose');
|
|
1129
|
+
assert.is(narration!.content, 'Let me act.');
|
|
1130
|
+
assert.ok(toolCallMsg, 'the tool call is on its own message');
|
|
1131
|
+
assert.is(toolCallMsg!.content, '');
|
|
1132
|
+
|
|
1133
|
+
// Only the tool-call message carries the turn's usage; the display bubbles carry none.
|
|
1134
|
+
for (const m of [reasoning!, narration!]) {
|
|
1135
|
+
assert.is(m.cost, undefined, 'a display bubble must not carry cost');
|
|
1136
|
+
assert.is(m.inputTokens, undefined, 'nor inputTokens');
|
|
1137
|
+
assert.is(m.outputTokens, undefined, 'nor outputTokens');
|
|
1138
|
+
}
|
|
1139
|
+
assert.is(toolCallMsg!.cost, 0.05, 'the tool-call message carries the cost');
|
|
1140
|
+
assert.is(toolCallMsg!.inputTokens, 1000);
|
|
1141
|
+
assert.is(toolCallMsg!.outputTokens, 50);
|
|
1142
|
+
|
|
1143
|
+
// The reasoning/narration bubbles are UI-only; they must not carry the tool call either.
|
|
1144
|
+
assert.is(reasoning!.toolCalls, undefined, 'the reasoning bubble carries no tool call');
|
|
1145
|
+
assert.is(narration!.toolCalls, undefined, 'the narration bubble carries no tool call');
|
|
1146
|
+
|
|
1147
|
+
// The session totals count the one response once, not twice.
|
|
1148
|
+
assert.is(sumCosts(history), 0.05, 'cost is summed once');
|
|
1149
|
+
assert.is(sumTokens(history), 1050, 'tokens are summed once');
|
|
1150
|
+
},
|
|
1151
|
+
);
|
|
1152
|
+
|
|
1153
|
+
costAttribution.run();
|
|
1154
|
+
|
|
1083
1155
|
// ---------------------------------------------------------------------------
|
|
1084
1156
|
// per-agent / per-state temperature & tool-call mode (GENC-1321)
|
|
1085
1157
|
//
|
|
@@ -1342,6 +1414,77 @@ interactionPresentation('presentation is absent when the option is omitted', asy
|
|
|
1342
1414
|
|
|
1343
1415
|
interactionPresentation.run();
|
|
1344
1416
|
|
|
1417
|
+
// ---------------------------------------------------------------------------
|
|
1418
|
+
// single-instance interaction ownership (GENC-1410 Error 3) — when several views
|
|
1419
|
+
// share one driver, each interaction's widget is rendered/run by exactly one host.
|
|
1420
|
+
// The driver elects the owner (most-recently-active host, deterministic fallback),
|
|
1421
|
+
// stamps `ownerHostId` on every interaction, and de-dupes execution via a
|
|
1422
|
+
// first-writer claim.
|
|
1423
|
+
// ---------------------------------------------------------------------------
|
|
1424
|
+
|
|
1425
|
+
const ownedInteraction = createLogicSuite('ChatDriver single-instance interaction ownership');
|
|
1426
|
+
|
|
1427
|
+
ownedInteraction('stamps ownerHostId = most-recently-active host on the interaction', async () => {
|
|
1428
|
+
const driver = makeDriver(agent({ name: 'a' }), scriptedProvider([]));
|
|
1429
|
+
driver.registerHost('host-a');
|
|
1430
|
+
driver.registerHost('host-b');
|
|
1431
|
+
driver.setActiveHost('host-b');
|
|
1432
|
+
|
|
1433
|
+
const pending = driver.requestInteraction('w', {});
|
|
1434
|
+
const interaction = driver.getHistory().at(-1)!.interaction!;
|
|
1435
|
+
assert.is(interaction.ownerHostId, 'host-b', 'owner is the most-recently-active host');
|
|
1436
|
+
|
|
1437
|
+
driver.resolveInteraction(interaction.interactionId, { status: 'approved' });
|
|
1438
|
+
await pending;
|
|
1439
|
+
});
|
|
1440
|
+
|
|
1441
|
+
ownedInteraction(
|
|
1442
|
+
'owner falls back to the first-registered host when none is active yet',
|
|
1443
|
+
async () => {
|
|
1444
|
+
const driver = makeDriver(agent({ name: 'a' }), scriptedProvider([]));
|
|
1445
|
+
driver.registerHost('host-a');
|
|
1446
|
+
driver.registerHost('host-b');
|
|
1447
|
+
// No setActiveHost — startup, before the user has engaged a view.
|
|
1448
|
+
|
|
1449
|
+
const pending = driver.requestInteraction('w', {});
|
|
1450
|
+
const interaction = driver.getHistory().at(-1)!.interaction!;
|
|
1451
|
+
assert.is(interaction.ownerHostId, 'host-a', 'deterministic default = first registered');
|
|
1452
|
+
|
|
1453
|
+
driver.resolveInteraction(interaction.interactionId, { status: 'approved' });
|
|
1454
|
+
await pending;
|
|
1455
|
+
},
|
|
1456
|
+
);
|
|
1457
|
+
|
|
1458
|
+
ownedInteraction(
|
|
1459
|
+
'no host registered → no owner (widget renders everywhere, fail-open)',
|
|
1460
|
+
async () => {
|
|
1461
|
+
const driver = makeDriver(agent({ name: 'a' }), scriptedProvider([]));
|
|
1462
|
+
|
|
1463
|
+
const pending = driver.requestInteraction('w', {});
|
|
1464
|
+
const interaction = driver.getHistory().at(-1)!.interaction!;
|
|
1465
|
+
assert.ok(!('ownerHostId' in interaction), 'no owner stamped when no host is registered');
|
|
1466
|
+
|
|
1467
|
+
driver.resolveInteraction(interaction.interactionId, { status: 'approved' });
|
|
1468
|
+
await pending;
|
|
1469
|
+
},
|
|
1470
|
+
);
|
|
1471
|
+
|
|
1472
|
+
ownedInteraction(
|
|
1473
|
+
'claimSideEffect is first-writer-wins, and re-entrant for the same host',
|
|
1474
|
+
async () => {
|
|
1475
|
+
const driver = makeDriver(agent({ name: 'a' }), scriptedProvider([]));
|
|
1476
|
+
assert.is(driver.claimSideEffect('i1', 'host-a'), true, 'first host claims it');
|
|
1477
|
+
assert.is(driver.claimSideEffect('i1', 'host-b'), false, 'a different host is denied');
|
|
1478
|
+
assert.is(
|
|
1479
|
+
driver.claimSideEffect('i1', 'host-a'),
|
|
1480
|
+
true,
|
|
1481
|
+
're-claim by the owner (re-render) is fine',
|
|
1482
|
+
);
|
|
1483
|
+
},
|
|
1484
|
+
);
|
|
1485
|
+
|
|
1486
|
+
ownedInteraction.run();
|
|
1487
|
+
|
|
1345
1488
|
// ---------------------------------------------------------------------------
|
|
1346
1489
|
// interaction activity-bus signals (GENC-1346) — the driver brackets a parked
|
|
1347
1490
|
// widget interaction with `interaction-requested` / `interaction-resolved`, so
|