@genesislcap/ai-assistant 14.419.2 → 14.421.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (105) hide show
  1. package/dist/ai-assistant.api.json +4061 -1416
  2. package/dist/ai-assistant.d.ts +594 -81
  3. package/dist/dts/channel/ai-activity-channel.d.ts +4 -22
  4. package/dist/dts/channel/ai-activity-channel.d.ts.map +1 -1
  5. package/dist/dts/components/ai-driver/ai-driver.d.ts +52 -0
  6. package/dist/dts/components/ai-driver/ai-driver.d.ts.map +1 -0
  7. package/dist/dts/components/ai-driver/index.d.ts +2 -0
  8. package/dist/dts/components/ai-driver/index.d.ts.map +1 -0
  9. package/dist/dts/components/chat-driver/chat-driver.d.ts +63 -8
  10. package/dist/dts/components/chat-driver/chat-driver.d.ts.map +1 -1
  11. package/dist/dts/components/chat-interaction-wrapper/chat-interaction-wrapper.d.ts +3 -3
  12. package/dist/dts/components/chat-interaction-wrapper/chat-interaction-wrapper.d.ts.map +1 -1
  13. package/dist/dts/components/chat-markdown/chat-markdown.d.ts +1 -1
  14. package/dist/dts/components/chat-markdown/chat-markdown.d.ts.map +1 -1
  15. package/dist/dts/components/halo-overlay.d.ts +13 -1
  16. package/dist/dts/components/halo-overlay.d.ts.map +1 -1
  17. package/dist/dts/components/orchestrating-driver/index.d.ts +2 -0
  18. package/dist/dts/components/orchestrating-driver/index.d.ts.map +1 -0
  19. package/dist/dts/components/orchestrating-driver/orchestrating-driver.d.ts +39 -0
  20. package/dist/dts/components/orchestrating-driver/orchestrating-driver.d.ts.map +1 -0
  21. package/dist/dts/components/popout-manager/index.d.ts +2 -0
  22. package/dist/dts/components/popout-manager/index.d.ts.map +1 -0
  23. package/dist/dts/components/popout-manager/popout-manager.d.ts +72 -0
  24. package/dist/dts/components/popout-manager/popout-manager.d.ts.map +1 -0
  25. package/dist/dts/config/config.d.ts +43 -15
  26. package/dist/dts/config/config.d.ts.map +1 -1
  27. package/dist/dts/config/fallback-agents.d.ts +20 -0
  28. package/dist/dts/config/fallback-agents.d.ts.map +1 -0
  29. package/dist/dts/config/index.d.ts +1 -0
  30. package/dist/dts/config/index.d.ts.map +1 -1
  31. package/dist/dts/index.d.ts +6 -0
  32. package/dist/dts/index.d.ts.map +1 -1
  33. package/dist/dts/main/main.d.ts +122 -21
  34. package/dist/dts/main/main.d.ts.map +1 -1
  35. package/dist/dts/main/main.styles.d.ts.map +1 -1
  36. package/dist/dts/main/main.template.d.ts.map +1 -1
  37. package/dist/dts/main/main.types.d.ts +16 -0
  38. package/dist/dts/main/main.types.d.ts.map +1 -1
  39. package/dist/dts/state/ai-assistant-slice.d.ts +38 -0
  40. package/dist/dts/state/ai-assistant-slice.d.ts.map +1 -0
  41. package/dist/dts/state/driver-registry.d.ts +22 -0
  42. package/dist/dts/state/driver-registry.d.ts.map +1 -0
  43. package/dist/dts/state/session-store.d.ts +37 -0
  44. package/dist/dts/state/session-store.d.ts.map +1 -0
  45. package/dist/dts/suggestions/chat-suggestions.d.ts +7 -0
  46. package/dist/dts/suggestions/chat-suggestions.d.ts.map +1 -0
  47. package/dist/dts/types/ai-chat-widget.d.ts +3 -2
  48. package/dist/dts/types/ai-chat-widget.d.ts.map +1 -1
  49. package/dist/dts/utils/index.d.ts +1 -0
  50. package/dist/dts/utils/index.d.ts.map +1 -1
  51. package/dist/dts/utils/tool-fold.d.ts +133 -0
  52. package/dist/dts/utils/tool-fold.d.ts.map +1 -0
  53. package/dist/esm/components/ai-driver/ai-driver.js +1 -0
  54. package/dist/esm/components/ai-driver/index.js +1 -0
  55. package/dist/esm/components/chat-driver/chat-driver.js +499 -67
  56. package/dist/esm/components/chat-interaction-wrapper/chat-interaction-wrapper.js +2 -2
  57. package/dist/esm/components/chat-markdown/chat-markdown.js +1 -1
  58. package/dist/esm/components/halo-overlay.js +53 -7
  59. package/dist/esm/components/orchestrating-driver/index.js +1 -0
  60. package/dist/esm/components/orchestrating-driver/orchestrating-driver.js +247 -0
  61. package/dist/esm/components/popout-manager/index.js +1 -0
  62. package/dist/esm/components/popout-manager/popout-manager.js +126 -0
  63. package/dist/esm/config/fallback-agents.js +26 -0
  64. package/dist/esm/config/index.js +1 -0
  65. package/dist/esm/index.js +6 -0
  66. package/dist/esm/main/main.js +546 -112
  67. package/dist/esm/main/main.styles.js +200 -4
  68. package/dist/esm/main/main.template.js +163 -63
  69. package/dist/esm/state/ai-assistant-slice.js +54 -0
  70. package/dist/esm/state/driver-registry.js +46 -0
  71. package/dist/esm/state/session-store.js +39 -0
  72. package/dist/esm/suggestions/chat-suggestions.js +147 -0
  73. package/dist/esm/utils/index.js +1 -0
  74. package/dist/esm/utils/tool-fold.js +92 -0
  75. package/dist/tsconfig.tsbuildinfo +1 -1
  76. package/docs/migration-FUI-2495.md +339 -0
  77. package/docs/sub_agent.md +310 -0
  78. package/package.json +16 -15
  79. package/src/channel/ai-activity-channel.ts +4 -20
  80. package/src/components/ai-driver/ai-driver.ts +69 -0
  81. package/src/components/ai-driver/index.ts +1 -0
  82. package/src/components/chat-driver/chat-driver.ts +600 -73
  83. package/src/components/chat-interaction-wrapper/chat-interaction-wrapper.ts +3 -3
  84. package/src/components/chat-markdown/chat-markdown.ts +1 -1
  85. package/src/components/halo-overlay.ts +45 -7
  86. package/src/components/orchestrating-driver/index.ts +1 -0
  87. package/src/components/orchestrating-driver/orchestrating-driver.ts +328 -0
  88. package/src/components/popout-manager/index.ts +1 -0
  89. package/src/components/popout-manager/popout-manager.ts +147 -0
  90. package/src/config/config.ts +45 -15
  91. package/src/config/fallback-agents.ts +29 -0
  92. package/src/config/index.ts +1 -0
  93. package/src/index.ts +6 -0
  94. package/src/main/main.styles.ts +200 -4
  95. package/src/main/main.template.ts +200 -80
  96. package/src/main/main.ts +567 -94
  97. package/src/main/main.types.ts +11 -0
  98. package/src/state/ai-assistant-slice.ts +80 -0
  99. package/src/state/driver-registry.ts +51 -0
  100. package/src/state/session-store.ts +56 -0
  101. package/src/suggestions/chat-suggestions.ts +158 -0
  102. package/src/types/ai-chat-widget.ts +4 -2
  103. package/src/utils/index.ts +1 -0
  104. package/src/utils/tool-fold.ts +181 -0
  105. package/docs/multi-agent-architecture.md +0 -198
package/src/main/main.ts CHANGED
@@ -1,3 +1,26 @@
1
+ // =============================================================================
2
+ // ARCHITECTURAL RULES — read before modifying this file
3
+ // =============================================================================
4
+ //
5
+ // 1. STORE INIT MUST PRECEDE super.connectedCallback()
6
+ // `_sessionRef` must be assigned before calling `super.connectedCallback()`.
7
+ // FAST triggers its first render inside `super.connectedCallback()`. The store
8
+ // Proxy calls Observable.track() when a slice is read, which wires up FAST's
9
+ // reactivity. If `_sessionRef` is null during that first render, no tracking
10
+ // is registered and subsequent Redux dispatches will not trigger re-renders.
11
+ // Do not reorder these lines.
12
+ //
13
+ // 2. MARK @volatile ON GETTERS WITH CONDITIONAL OBSERVABLE ACCESS
14
+ // FAST only tracks observables that are actually accessed during a getter's
15
+ // last evaluation. If a getter has branches that access different observables
16
+ // depending on runtime state (e.g. filtering over messages of varying types,
17
+ // or a ternary that reads one observable OR another), FAST may not know to
18
+ // re-evaluate when an unvisited branch's observable changes. Mark such
19
+ // getters @volatile so FAST always re-evaluates them when any dependency
20
+ // changes, rather than relying on cached dependency tracking.
21
+ //
22
+ // =============================================================================
23
+
1
24
  import { AIProvider } from '@genesislcap/foundation-ai';
2
25
  import type { ChatAttachment, ChatConfig, ChatMessage } from '@genesislcap/foundation-ai';
3
26
  import { avoidTreeShaking } from '@genesislcap/foundation-utils';
@@ -10,19 +33,38 @@ import {
10
33
  attr,
11
34
  } from '@genesislcap/web-core';
12
35
  import { agenticActivityBus } from '../channel/ai-activity-bus';
13
- import type { AiAssistantSerializedState } from '../channel/ai-activity-channel';
14
36
  import { AiActivityHalo } from '../components/activity-halo/activity-halo';
37
+ import type { AiDriver, AllAgentSummary } from '../components/ai-driver/ai-driver';
15
38
  import { AiChatBubble } from '../components/chat-bubble/chat-bubble';
16
39
  import { ChatDriver } from '../components/chat-driver/chat-driver';
17
40
  import { AiChatInteractionWrapper } from '../components/chat-interaction-wrapper/chat-interaction-wrapper';
18
41
  import { AiChatMarkdown } from '../components/chat-markdown/chat-markdown';
19
42
  import { AiHaloOverlay } from '../components/halo-overlay';
43
+ import { OrchestratingDriver } from '../components/orchestrating-driver/orchestrating-driver';
20
44
  import type { AgentConfig } from '../config/config';
45
+ import { getOrCreateDriver, deleteDriver } from '../state/driver-registry';
46
+ import { getSessionStore, hasSessionStore, type SessionStoreReturn } from '../state/session-store';
47
+ import { ChatSuggestions } from '../suggestions/chat-suggestions';
21
48
  import { logger } from '../utils/logger';
49
+ import { expandToolTree } from '../utils/tool-fold';
22
50
  import { styles } from './main.styles';
23
51
  import { FoundationAiAssistantTemplate } from './main.template';
24
52
  import { ALL_ANIMATIONS } from './main.types';
25
- import type { AiAssistantAnimation, AiAssistantState, PopoutMode } from './main.types';
53
+ import type {
54
+ AiAssistantAnimation,
55
+ AiAssistantState,
56
+ PopoutMode,
57
+ SuggestionsState,
58
+ } from './main.types';
59
+
60
+ /** Context window sizes (in tokens) for known models. */
61
+ const MODEL_CONTEXT_LIMITS: Record<string, number> = {
62
+ 'gemini-2.5-flash': 1_048_576,
63
+ 'gemini-2.5-flash-lite': 1_048_576,
64
+ 'gpt-4o': 128_000,
65
+ 'gpt-4o-mini': 128_000,
66
+ 'gpt-4-turbo': 128_000,
67
+ };
26
68
 
27
69
  // Register supporting components when the main component module is imported.
28
70
  avoidTreeShaking(
@@ -31,6 +73,7 @@ avoidTreeShaking(
31
73
  AiHaloOverlay,
32
74
  AiChatBubble,
33
75
  AiActivityHalo,
76
+ ChatSuggestions,
34
77
  );
35
78
 
36
79
  /**
@@ -38,7 +81,8 @@ avoidTreeShaking(
38
81
  *
39
82
  * @remarks
40
83
  * Inject an `AIProvider` through the DI container. Pass agent configuration via the `agents`
41
- * property. The component creates a `ChatDriver` to manage the conversation loop.
84
+ * property. The component creates a `ChatDriver` (single agent) or `OrchestratingDriver`
85
+ * (multiple agents) to manage the conversation loop.
42
86
  *
43
87
  * Popout/collapse coordination uses `agenticActivityBus` topics `chat-popout` and `chat-popin` — not DOM `CustomEvent`s on this element.
44
88
  *
@@ -57,7 +101,7 @@ export class FoundationAiAssistant extends GenesisElement {
57
101
  @AIProvider aiProvider!: AIProvider;
58
102
 
59
103
  @observable designSystemPrefix: string = 'rapid';
60
- @attr({ attribute: 'header-title' }) headerTitle?: string;
104
+ @attr({ attribute: 'header-title' }) headerTitle: string = 'Genesis Assistant';
61
105
  @attr({ attribute: 'image-src' }) imageSrc?: string;
62
106
  @attr() placeholder: string = 'Message assistant...';
63
107
  /**
@@ -74,46 +118,155 @@ export class FoundationAiAssistant extends GenesisElement {
74
118
  @observable agents?: AgentConfig[];
75
119
  @observable chatConfig: ChatConfig = {};
76
120
  @observable debugStateFactory?: () => unknown;
121
+ /** When set, enables Redux DevTools for this instance's session store. */
122
+ @attr({ attribute: 'debug-redux', mode: 'boolean' }) debugRedux = false;
77
123
 
78
- @observable messages: ChatMessage[] = [];
79
- @observable state: AiAssistantState = 'idle';
80
- @observable inputValue = '';
81
- @observable attachments: ChatAttachment[] = [];
82
- @observable attachmentErrors: string[] = [];
124
+ // ---- Store-backed state (getter reads from store, setter dispatches) ----
125
+
126
+ private _sessionRef?: SessionStoreReturn;
127
+
128
+ get messages(): ChatMessage[] {
129
+ return this._sessionRef?.store.aiAssistant.messages ?? [];
130
+ }
131
+ set messages(value: ChatMessage[]) {
132
+ this._sessionRef?.actions.aiAssistant.setMessages(value);
133
+ this.handleMessagesUpdate();
134
+ }
135
+
136
+ get state(): AiAssistantState {
137
+ return this._sessionRef?.store.aiAssistant.state ?? 'idle';
138
+ }
139
+ set state(value: AiAssistantState) {
140
+ this._sessionRef?.actions.aiAssistant.setState(value);
141
+ this.syncShowHalo();
142
+ }
143
+
144
+ get activeAgent(): Omit<AgentConfig, 'toolHandlers'> | undefined {
145
+ return this._sessionRef?.store.aiAssistant.activeAgent;
146
+ }
147
+ set activeAgent(value: AgentConfig | undefined) {
148
+ // Strip toolHandlers before storing — functions are non-serializable and Redux
149
+ // serializable-state middleware will warn. toolHandlers are never read back from
150
+ // the store; they are always sourced from this.agents when the driver is built.
151
+ if (value) {
152
+ const { toolHandlers: _, ...serializable } = value;
153
+ this._sessionRef?.actions.aiAssistant.setActiveAgent(serializable);
154
+ } else {
155
+ this._sessionRef?.actions.aiAssistant.setActiveAgent(undefined);
156
+ }
157
+ }
158
+
159
+ get suggestionsState(): SuggestionsState {
160
+ return this._sessionRef?.store.aiAssistant.suggestionsState ?? { status: 'idle' };
161
+ }
162
+ set suggestionsState(value: SuggestionsState) {
163
+ this._sessionRef?.actions.aiAssistant.setSuggestionsState(value);
164
+ }
83
165
 
84
166
  /** Current user-facing toggle state for tool call visibility. */
85
- @observable showToolCalls = false;
167
+ get showToolCalls(): boolean {
168
+ return this._sessionRef?.store.aiAssistant.showToolCalls ?? false;
169
+ }
170
+ set showToolCalls(value: boolean) {
171
+ this._sessionRef?.actions.aiAssistant.setShowToolCalls(value);
172
+ }
173
+
86
174
  /** Current user-facing toggle state for thinking step visibility. */
87
- @observable showThinkingSteps = false;
175
+ get showThinkingSteps(): boolean {
176
+ return this._sessionRef?.store.aiAssistant.showThinkingSteps ?? false;
177
+ }
178
+ set showThinkingSteps(value: boolean) {
179
+ this._sessionRef?.actions.aiAssistant.setShowThinkingSteps(value);
180
+ }
181
+
182
+ /** Current user-facing toggle state for agent switch indicator visibility. */
183
+ get showAgentSwitchIndicator(): boolean {
184
+ return this._sessionRef?.store.aiAssistant.showAgentSwitchIndicator ?? false;
185
+ }
186
+ set showAgentSwitchIndicator(value: boolean) {
187
+ this._sessionRef?.actions.aiAssistant.setShowAgentSwitchIndicator(value);
188
+ }
189
+
88
190
  /** Currently enabled animations. */
89
- @observable enabledAnimations: AiAssistantAnimation[] = [];
191
+ get enabledAnimations(): AiAssistantAnimation[] {
192
+ return this._sessionRef?.store.aiAssistant.enabledAnimations ?? [];
193
+ }
194
+ set enabledAnimations(value: AiAssistantAnimation[]) {
195
+ this._sessionRef?.actions.aiAssistant.setEnabledAnimations(value);
196
+ }
197
+
198
+ /** Most recent prompt token count from the AI provider, if available. */
199
+ get contextTokens(): number | undefined {
200
+ return this._sessionRef?.store.aiAssistant.contextTokens;
201
+ }
202
+ set contextTokens(value: number | undefined) {
203
+ this._sessionRef?.actions.aiAssistant.setContextTokens(value);
204
+ }
205
+
206
+ /** Context window size for the active model, if known. */
207
+ get contextLimit(): number | undefined {
208
+ return this._sessionRef?.store.aiAssistant.contextLimit;
209
+ }
210
+ set contextLimit(value: number | undefined) {
211
+ this._sessionRef?.actions.aiAssistant.setContextLimit(value);
212
+ }
213
+
214
+ // ---- Transient UI state (stays as @observable on the component) ----
215
+
216
+ private _suggestionsGeneration = 0;
217
+
218
+ get inputValue(): string {
219
+ return this._sessionRef?.store.aiAssistant.inputValue ?? '';
220
+ }
221
+ set inputValue(value: string) {
222
+ this._sessionRef?.actions.aiAssistant.setInputValue(value);
223
+ }
224
+ @observable attachments: ChatAttachment[] = [];
225
+ @observable attachmentErrors: string[] = [];
90
226
  /** Whether the loading spinner is currently visible. Controlled by the loading delay timer. */
91
227
  @observable showLoadingIndicator = false;
92
228
  /** Whether the settings panel is open. */
93
229
  @observable settingsOpen = false;
230
+ /** Whether the splash overlay is currently showing (no messages and showSplash is enabled). Reflected as a boolean attribute on the host. */
231
+ @observable showingSplash = false;
94
232
 
95
- private driver?: ChatDriver;
233
+ private driver?: AiDriver;
234
+ private driverCleanup?: () => void;
96
235
  private loadingTimer: ReturnType<typeof setTimeout> | undefined;
97
236
  private unsubBus?: () => void;
98
237
  private haloStartPublished = false;
99
-
238
+ /** Fingerprint of the agents array used to build the current driver. Used by agentsChanged to skip spurious rebuilds. */
239
+ private _driverAgentsKey?: string;
240
+ /** Bound to the messages container via the template ref — assigned by FAST before connectedCallback logic runs. */
241
+ messagesEl?: HTMLElement;
242
+ /** True when the user has intentionally scrolled away from the bottom — suppresses auto-scroll. */
243
+ private _userScrolledAway = false;
244
+ private _scrollListener?: () => void;
245
+ private static readonly SCROLL_BOTTOM_THRESHOLD_PX = 50;
100
246
  /**
101
- * Whether the halo animation should be shown.
102
- * True when the AI is actively computing (loading state, no pending interaction).
247
+ * Unsubscribes a one-shot state subscription created when this element connects
248
+ * mid-execution. The subscription watches for `state` becoming 'idle' so that
249
+ * this element can stop its loading timer and sync the halo even when the
250
+ * originating `send()` ran on a different element instance.
103
251
  */
104
- @observable showHalo: boolean = false;
252
+ private _executionCompletionUnsub?: () => void;
253
+
254
+ @observable showHalo: 'no' | 'orchestrating' | 'agent' = 'no';
105
255
 
106
256
  private syncShowHalo() {
107
257
  if (this.state !== 'loading') {
108
- this.showHalo = false;
258
+ this.showHalo = 'no';
109
259
  return;
110
260
  }
111
261
  const last = this.messages[this.messages.length - 1];
112
- this.showHalo = !last?.interaction;
262
+ if (last?.interaction) {
263
+ this.showHalo = 'no';
264
+ } else if (this.showHalo !== 'orchestrating') {
265
+ this.showHalo = 'agent';
266
+ }
113
267
  }
114
268
 
115
269
  /** True when there is a pending (unresolved) interaction — disables the popout button. */
116
- @volatile
117
270
  get hasActivePendingInteraction(): boolean {
118
271
  if (this.state !== 'loading') return false;
119
272
  const last = this.messages[this.messages.length - 1];
@@ -122,7 +275,7 @@ export class FoundationAiAssistant extends GenesisElement {
122
275
 
123
276
  showHaloChanged() {
124
277
  if (!this.enabledAnimations?.includes('halo')) return;
125
- if (!this.showHalo) {
278
+ if (this.showHalo === 'no') {
126
279
  agenticActivityBus.publish('halo-stop', undefined);
127
280
  this.haloStartPublished = false;
128
281
  }
@@ -145,10 +298,25 @@ export class FoundationAiAssistant extends GenesisElement {
145
298
  * Messages filtered by the current toggle state.
146
299
  * Tool-related messages (those with toolCalls or toolResult) are hidden when
147
300
  * `showToolCalls` is false.
301
+ *
302
+ * Marked `@volatile` because the filter branches conditionally access
303
+ * `showToolCalls`, `showThinkingSteps`, and `showAgentSwitchIndicator`
304
+ * depending on message content. Without it, FAST would only track the
305
+ * observables accessed on the last evaluation and miss toggle changes that
306
+ * happen to hit an untracked branch.
148
307
  */
149
308
  @volatile
150
309
  get visibleMessages(): ChatMessage[] {
310
+ const showAgentSwitchIndicator =
311
+ this.chatConfig.ui?.showAgentSwitchIndicator != null
312
+ ? this.showAgentSwitchIndicator
313
+ : this.showToolCalls;
314
+
151
315
  return this.messages.filter((m) => {
316
+ // Agent switch indicators are shown when the toggle is on (or showToolCalls implies it)
317
+ if (m.role === 'system-event') {
318
+ return showAgentSwitchIndicator;
319
+ }
152
320
  // Never show tool messages to the user
153
321
  if (m.role === 'tool') {
154
322
  return false;
@@ -166,41 +334,207 @@ export class FoundationAiAssistant extends GenesisElement {
166
334
  });
167
335
  }
168
336
 
169
- connectedCallback() {
170
- super.connectedCallback();
171
- const { showToolCalls, showThinkingSteps, animations } = this.chatConfig;
172
- this.showToolCalls = showToolCalls === true;
173
- this.showThinkingSteps = showThinkingSteps === true;
174
- this.enabledAnimations =
175
- (animations?.enabled as AiAssistantAnimation[]) ?? (animations ? [...ALL_ANIMATIONS] : []);
176
-
177
- // TODO: multi-agent orchestration classify intent and route to the correct agent
178
- if (this.agents && this.agents.length > 1) {
179
- logger.warn(
180
- 'FoundationAiAssistant: multiple agents configured but orchestration is not yet implemented. Only the first agent will be used.',
181
- );
337
+ agentsChanged(): void {
338
+ // Guard: driver doesn't exist yet during connectedCallback — createDriver/wireDriver handle that.
339
+ if (!this.driver) return;
340
+ const key = this.getStateKey();
341
+ if (!key) return;
342
+ // Skip rebuild if the agent configuration hasn't meaningfully changed.
343
+ // This prevents spurious driver teardowns when the collapse-mode element
344
+ // connects and its parent re-assigns the same agents array, which would
345
+ // destroy pending interactions in the existing driver.
346
+ const newKey = this.getAgentsKey(this.agents);
347
+ if (newKey === this._driverAgentsKey) return;
348
+ this._driverAgentsKey = newKey;
349
+ // Don't rebuild while the driver is busy — it was created with the correct agents
350
+ // and has live pending interactions. This prevents the collapse element (which
351
+ // initially has no agents, then gets them assigned by its wrapper) from tearing
352
+ // down a mid-flight driver and creating a fresh idle one.
353
+ if (this.driver.isBusy()) return;
354
+ const history = this.driver.getRawHistory?.() ?? [];
355
+ this.unwireDriver();
356
+ deleteDriver(key);
357
+ this.driver = getOrCreateDriver(key, () => this.createDriver());
358
+ this.wireDriver();
359
+ if (history.length) this.driver.loadHistory([...history]);
360
+ }
361
+
362
+ /** Returns a stable fingerprint for an agents array based on agent names and tool handler keys. */
363
+ private getAgentsKey(agents?: AgentConfig[]): string {
364
+ if (!agents?.length) return '';
365
+ return agents
366
+ .map((a) => {
367
+ const toolNames = Object.keys(a.toolHandlers ?? {})
368
+ .sort()
369
+ .join('+');
370
+ return `${a.name}[${toolNames}]`;
371
+ })
372
+ .join(',');
373
+ }
374
+
375
+ /**
376
+ * Pure factory — creates a ChatDriver or OrchestratingDriver based on the
377
+ * current agent configuration. Does not wire event listeners or register in
378
+ * the driver registry.
379
+ */
380
+ private createDriver(): AiDriver {
381
+ const agent = this.chatConfig.agent ?? {};
382
+ const { agents } = this;
383
+
384
+ if (agents && agents.length > 1) {
385
+ return new OrchestratingDriver(this.aiProvider, agents, {
386
+ maxHandoffs: agent.maxHandoffs,
387
+ classifierHistoryLength: agent.classifierHistoryLength,
388
+ classifierRetries: agent.classifierRetries,
389
+ maxToolIterations: agent.maxToolIterations,
390
+ maxFoldOperations: agent.maxFoldOperations,
391
+ });
182
392
  }
183
- const agent = this.agents?.[0];
184
- this.driver = new ChatDriver(
393
+
394
+ const singleAgent = agents?.[0];
395
+ this.activeAgent = singleAgent;
396
+ return new ChatDriver(
185
397
  this.aiProvider,
186
- agent?.toolHandlers ?? {},
187
- agent?.toolDefinitions ?? [],
188
- agent?.systemPrompt,
189
- agent?.primerHistory,
190
- this.chatConfig.maxToolIterations,
398
+ singleAgent?.toolHandlers ?? {},
399
+ singleAgent?.toolDefinitions ?? [],
400
+ singleAgent?.systemPrompt,
401
+ singleAgent?.primerHistory,
402
+ agent.maxToolIterations,
403
+ agent.maxFoldOperations,
191
404
  );
192
- this.driver.addEventListener('history-updated', (e: Event) => {
405
+ }
406
+
407
+ /**
408
+ * Attaches event listeners to the current driver. Stores a cleanup function
409
+ * so `unwireDriver` can remove them precisely. Safe to call multiple
410
+ * times — unwires any existing listeners first.
411
+ */
412
+ private wireDriver(): void {
413
+ const { driver } = this;
414
+ if (!driver) return;
415
+
416
+ // Idempotent — unwire previous listeners on this component before re-wiring.
417
+ this.unwireDriver();
418
+
419
+ const onHistoryUpdated = (e: Event) => {
193
420
  this.messages = [...(e as CustomEvent<ChatMessage[]>).detail];
194
- });
421
+ };
422
+ driver.addEventListener('history-updated', onHistoryUpdated);
423
+
424
+ const cleanups: (() => void)[] = [
425
+ () => driver.removeEventListener('history-updated', onHistoryUpdated),
426
+ ];
427
+
428
+ if (driver instanceof OrchestratingDriver) {
429
+ const onOrchStart = () => {
430
+ this.showHalo = 'orchestrating';
431
+ };
432
+ const onOrchStop = () => {
433
+ if (this.showHalo === 'orchestrating') this.showHalo = 'agent';
434
+ };
435
+ const onAgentChanged = (e: Event) => {
436
+ this.activeAgent = (e as CustomEvent<AgentConfig>).detail;
437
+ };
438
+ driver.addEventListener('orchestrating-start', onOrchStart);
439
+ driver.addEventListener('orchestrating-stop', onOrchStop);
440
+ driver.addEventListener('agent-changed', onAgentChanged);
441
+ cleanups.push(
442
+ () => driver.removeEventListener('orchestrating-start', onOrchStart),
443
+ () => driver.removeEventListener('orchestrating-stop', onOrchStop),
444
+ () => driver.removeEventListener('agent-changed', onAgentChanged),
445
+ );
446
+ }
447
+
448
+ this.driverCleanup = () => {
449
+ for (const fn of cleanups) fn();
450
+ };
451
+ }
452
+
453
+ /**
454
+ * Removes event listeners attached by `wireDriver`. Does not destroy
455
+ * the driver or remove it from the registry.
456
+ */
457
+ private unwireDriver(): void {
458
+ this.driverCleanup?.();
459
+ this.driverCleanup = undefined;
460
+ }
461
+
462
+ connectedCallback() {
463
+ // Initialise the store reference BEFORE super.connectedCallback() so that
464
+ // the first FAST render has access to the store. The store Proxy calls
465
+ // Observable.track(observableStore, sliceName) whenever a slice is read,
466
+ // and actions call Observable.notify — so FAST's reactivity wires up
467
+ // correctly only if _sessionRef is set during that first render pass.
468
+ const key = this.getStateKey()!;
469
+ const isNewStore = !hasSessionStore(key);
470
+ this._sessionRef = getSessionStore(key, this.debugRedux || undefined);
471
+
472
+ super.connectedCallback();
473
+
474
+ // Only apply chatConfig UI defaults to a freshly created store.
475
+ // Existing stores already hold the correct state from a prior session.
476
+ if (isNewStore) {
477
+ const ui = this.chatConfig.ui ?? {};
478
+ this.showToolCalls = ui.showToolCalls === true;
479
+ this.showThinkingSteps = ui.showThinkingSteps === true;
480
+ this.showAgentSwitchIndicator = ui.showAgentSwitchIndicator === true;
481
+ this.enabledAnimations =
482
+ (ui.animations?.enabled as AiAssistantAnimation[]) ??
483
+ (ui.animations ? [...ALL_ANIMATIONS] : []);
484
+ }
485
+
486
+ this.driver = getOrCreateDriver(key, () => this.createDriver());
487
+ this._driverAgentsKey = this.getAgentsKey(this.agents);
488
+ this.wireDriver();
195
489
 
196
- // When embedded in a bubble (expand mode), listen for chat-popin to restore state
197
- // when the layout panel collapses back.
198
490
  if (this.popoutMode === 'expand') {
199
- this.unsubBus = agenticActivityBus.subscribe('chat-popin', ({ state }) => {
200
- this.applyState(state);
491
+ // Dual-listener guard: when the assistant is popped out into the layout
492
+ // panel, a collapse-mode element connects and wires to the same shared
493
+ // driver. Unwire this (hidden) instance on popout; re-wire on popin.
494
+ const unsubPopout = agenticActivityBus.subscribe('chat-popout', () => {
495
+ this.unwireDriver();
496
+ });
497
+ const unsubPopin = agenticActivityBus.subscribe('chat-popin', () => {
498
+ this.wireDriver();
201
499
  });
500
+ this.unsubBus = () => {
501
+ unsubPopout();
502
+ unsubPopin();
503
+ };
202
504
  }
203
505
 
506
+ this.syncShowingSplash();
507
+ // Restore loading state if the driver is still executing mid-lifecycle.
508
+ // disconnectedCallback resets state to 'idle', so we must check real driver state here.
509
+ if (this.driver?.isBusy()) {
510
+ this.state = 'loading';
511
+ this.startLoadingTimer();
512
+ // Subscribe once so that when the originating send() completes (possibly on a
513
+ // different element instance that has since disconnected), this element cleans up
514
+ // its own timer, syncs the halo, and triggers the post-response suggestion fetch.
515
+ this._executionCompletionUnsub = this._sessionRef?.subscribeKey(
516
+ (s) => s.aiAssistant.state,
517
+ () => {
518
+ if (this._sessionRef?.store.aiAssistant.state === 'idle') {
519
+ this.stopLoadingTimer();
520
+ this.syncShowHalo();
521
+ this.fetchSuggestions();
522
+ this._executionCompletionUnsub?.();
523
+ this._executionCompletionUnsub = undefined;
524
+ }
525
+ },
526
+ );
527
+ }
528
+ this.fetchSuggestions();
529
+ void this.resolveContextLimit();
530
+ if (this.messagesEl) {
531
+ this._scrollListener = () => {
532
+ this._userScrolledAway =
533
+ this.messagesEl!.scrollTop + this.messagesEl!.clientHeight <
534
+ this.messagesEl!.scrollHeight - FoundationAiAssistant.SCROLL_BOTTOM_THRESHOLD_PX;
535
+ };
536
+ this.messagesEl.addEventListener('scroll', this._scrollListener);
537
+ }
204
538
  logger.debug('FoundationAiAssistant connected');
205
539
  }
206
540
 
@@ -208,16 +542,56 @@ export class FoundationAiAssistant extends GenesisElement {
208
542
  super.disconnectedCallback();
209
543
  this.stopLoadingTimer();
210
544
  this.state = 'idle';
545
+ this.unwireDriver();
211
546
  this.unsubBus?.();
212
547
  this.unsubBus = undefined;
548
+ this._executionCompletionUnsub?.();
549
+ this._executionCompletionUnsub = undefined;
550
+ if (this.messagesEl && this._scrollListener) {
551
+ this.messagesEl.removeEventListener('scroll', this._scrollListener);
552
+ }
553
+ this._scrollListener = undefined;
554
+ this._userScrolledAway = false;
555
+ // Clear local references only — driver and store stay in their registries.
213
556
  this.driver = undefined;
557
+ this._sessionRef = undefined;
214
558
  }
215
559
 
216
- stateChanged() {
217
- this.syncShowHalo();
560
+ private async resolveContextLimit(): Promise<void> {
561
+ try {
562
+ const status = await this.aiProvider.getStatus?.();
563
+ if (status?.model) {
564
+ this.contextLimit = MODEL_CONTEXT_LIMITS[status.model];
565
+ }
566
+ } catch {
567
+ // Non-fatal — context limit display simply won't show
568
+ }
569
+ }
570
+
571
+ chatConfigChanged() {
572
+ this.syncShowingSplash();
573
+ }
574
+
575
+ showingSplashChanged() {
576
+ if (this.showingSplash) {
577
+ this.setAttribute('showing-splash', '');
578
+ } else {
579
+ this.removeAttribute('showing-splash');
580
+ }
581
+ }
582
+
583
+ // Sets the observable which reflects as a boolean attribute on the host via showingSplashChanged.
584
+ // Must be an attribute on the host (not a shadow DOM class) so consumer CSS can target slotted
585
+ // light DOM content — shadow DOM classes are not visible to external stylesheets.
586
+ private syncShowingSplash() {
587
+ this.showingSplash = !!this.chatConfig.ui?.showSplash && this.messages.length === 0;
218
588
  }
219
589
 
220
- messagesChanged() {
590
+ /**
591
+ * Runs side effects that were previously in `messagesChanged()`.
592
+ * Called from the `messages` setter after dispatching to the store.
593
+ */
594
+ private handleMessagesUpdate(): void {
221
595
  // Reset the loading timer when an assistant message arrives mid-loop so each
222
596
  // individual step gets a fresh window before the spinner appears.
223
597
  // If the last message is a blocking interaction, stop the timer — the AI is
@@ -231,11 +605,16 @@ export class FoundationAiAssistant extends GenesisElement {
231
605
  }
232
606
  }
233
607
  this.syncShowHalo();
608
+ this.syncShowingSplash();
609
+ // Update context token count from the most recent message that carries usage data.
610
+ for (let i = this.messages.length - 1; i >= 0; i -= 1) {
611
+ if (this.messages[i].inputTokens != null) {
612
+ this.contextTokens = this.messages[i].inputTokens;
613
+ break;
614
+ }
615
+ }
234
616
  // Publish halo-start whenever a new toolCalls message arrives.
235
- // If we've already published one this turn, send halo-stop first so grids
236
- // not relevant to the new tools begin deactivating (fix #2).
237
- // Never publish with empty toolNames (fix #3).
238
- if (this.showHalo && this.enabledAnimations?.includes('halo')) {
617
+ if (this.showHalo !== 'no' && this.enabledAnimations?.includes('halo')) {
239
618
  const last = this.messages[this.messages.length - 1];
240
619
  if (last?.toolCalls?.length) {
241
620
  const toolNames = this.getActiveToolNames();
@@ -252,7 +631,6 @@ export class FoundationAiAssistant extends GenesisElement {
252
631
  }
253
632
 
254
633
  showLoadingIndicatorChanged() {
255
- // Scroll to bottom when the spinner appears so it is visible.
256
634
  if (this.showLoadingIndicator) {
257
635
  this.scrollToBottom();
258
636
  }
@@ -261,20 +639,21 @@ export class FoundationAiAssistant extends GenesisElement {
261
639
  // Double rAF: first frame lets the framework flush the DOM update,
262
640
  // second frame reads the correct scrollHeight after layout.
263
641
  private scrollToBottom() {
642
+ if (this._userScrolledAway) return;
264
643
  requestAnimationFrame(() => {
265
644
  requestAnimationFrame(() => {
266
- const el = this.shadowRoot?.querySelector('.messages');
267
- if (el) el.scrollTop = el.scrollHeight;
645
+ if (this.messagesEl) this.messagesEl.scrollTop = this.messagesEl.scrollHeight;
268
646
  });
269
647
  });
270
648
  }
271
649
 
272
650
  private static readonly DEFAULT_LOADING_DELAY_S = 5;
651
+ private static readonly DEFAULT_SUGGESTION_COUNT = 3;
273
652
  private static readonly MS_PER_SECOND = 1000;
274
653
 
275
654
  private startLoadingTimer() {
276
655
  this.clearLoadingTimer();
277
- const delay = this.chatConfig.loadingDelay ?? FoundationAiAssistant.DEFAULT_LOADING_DELAY_S;
656
+ const delay = this.chatConfig.ui?.loadingDelay ?? FoundationAiAssistant.DEFAULT_LOADING_DELAY_S;
278
657
  if (delay === 0) {
279
658
  this.showLoadingIndicator = true;
280
659
  } else {
@@ -299,32 +678,37 @@ export class FoundationAiAssistant extends GenesisElement {
299
678
  /** Called when the user clicks the popout button. */
300
679
  handlePopout(): void {
301
680
  if (this.popoutMode === 'expand') {
302
- agenticActivityBus.publish('chat-popout', { state: this.serializeState() });
681
+ agenticActivityBus.publish('chat-popout', undefined);
303
682
  } else if (this.popoutMode === 'collapse') {
304
- agenticActivityBus.publish('chat-popin', { state: this.serializeState() });
683
+ agenticActivityBus.publish('chat-popin', undefined);
305
684
  }
306
685
  }
307
686
 
308
- /** Applies a serialized state snapshot to this live instance (used on collapse path). */
309
- applyState(state: AiAssistantSerializedState): void {
310
- this.messages = [...state.messages];
311
- this.showToolCalls = state.showToolCalls;
312
- this.showThinkingSteps = state.showThinkingSteps;
313
- this.enabledAnimations = [...state.enabledAnimations];
314
- this.driver?.loadHistory(state.messages);
315
- }
316
-
317
- private serializeState(): AiAssistantSerializedState {
318
- return {
319
- messages: [...this.messages],
320
- showToolCalls: this.showToolCalls,
321
- showThinkingSteps: this.showThinkingSteps,
322
- enabledAnimations: [...this.enabledAnimations],
323
- };
687
+ /** Returns a cache key for this instance, or undefined if the component has no identity. */
688
+ private getStateKey(): string | undefined {
689
+ const parts = [this.id, this.headerTitle].filter(Boolean);
690
+ return parts.length ? parts.join('::') : undefined;
324
691
  }
325
692
 
326
693
  toggleSettings() {
327
- this.settingsOpen = !this.settingsOpen;
694
+ if (this.settingsOpen) {
695
+ const panel = this.shadowRoot?.querySelector('.settings-panel') as HTMLElement | null;
696
+ if (panel) {
697
+ panel.classList.add('closing');
698
+ panel.addEventListener(
699
+ 'animationend',
700
+ () => {
701
+ panel.classList.remove('closing');
702
+ this.settingsOpen = false;
703
+ },
704
+ { once: true },
705
+ );
706
+ } else {
707
+ this.settingsOpen = false;
708
+ }
709
+ } else {
710
+ this.settingsOpen = true;
711
+ }
328
712
  }
329
713
 
330
714
  toggleShowToolCalls() {
@@ -335,23 +719,41 @@ export class FoundationAiAssistant extends GenesisElement {
335
719
  this.showThinkingSteps = !this.showThinkingSteps;
336
720
  }
337
721
 
722
+ toggleShowAgentSwitchIndicator() {
723
+ this.showAgentSwitchIndicator = !this.showAgentSwitchIndicator;
724
+ }
725
+
338
726
  setEnabledAnimations(animations: AiAssistantAnimation[]) {
339
727
  this.enabledAnimations = animations;
340
728
  }
341
729
 
342
- downloadHistory() {
730
+ getDebugLog() {
731
+ const timestamp = new Date().toISOString().replace(/:/g, '-');
732
+ return {
733
+ messages: this.driver?.getRawHistory?.() ?? this.messages,
734
+ meta: {
735
+ timestamp,
736
+ host: window.location.host,
737
+ agentSummary: this.agents?.map((a) => ({
738
+ ...a,
739
+ toolDefinitions: expandToolTree(a.toolDefinitions ?? [], a.toolHandlers ?? {}),
740
+ toolHandlers: undefined,
741
+ })),
742
+ activeSystemPrompt: this.activeAgent?.systemPrompt,
743
+ activePrimerHistory: this.activeAgent?.primerHistory,
744
+ activeFoldStack:
745
+ this.driver instanceof ChatDriver ? this.driver.getActiveFoldNames() : undefined,
746
+ debug: this.debugStateFactory?.(),
747
+ },
748
+ };
749
+ }
750
+
751
+ downloadDebugLog() {
343
752
  const timestamp = new Date().toISOString().replace(/:/g, '-');
344
- // TODO: multi-agent orchestration use the active agent rather than always agents[0]
345
- const agent = this.agents?.[0];
346
- const agentName = (agent?.name ?? this.headerTitle ?? 'chat')
753
+ const agentName = (this.activeAgent?.name ?? this.headerTitle ?? 'chat')
347
754
  .toLowerCase()
348
755
  .replace(/\s+/g, '-');
349
- const payload = {
350
- messages: this.messages,
351
- systemPrompt: agent?.systemPrompt,
352
- primerHistory: agent?.primerHistory,
353
- debug: this.debugStateFactory?.(),
354
- };
756
+ const payload = this.getDebugLog();
355
757
  const blob = new Blob([JSON.stringify(payload, null, 2)], { type: 'application/json' });
356
758
  const url = URL.createObjectURL(blob);
357
759
  const a = document.createElement('a');
@@ -384,7 +786,7 @@ export class FoundationAiAssistant extends GenesisElement {
384
786
  }
385
787
 
386
788
  private isAcceptedFile(file: File): boolean {
387
- const accepted = this.chatConfig.acceptedFiles;
789
+ const accepted = this.chatConfig.ui?.acceptedFiles;
388
790
  if (!accepted) return false;
389
791
 
390
792
  const acceptedList = accepted.split(',').map((s) => s.trim().toLowerCase());
@@ -446,13 +848,81 @@ export class FoundationAiAssistant extends GenesisElement {
446
848
  this.send();
447
849
  }
448
850
 
851
+ handleSuggestionClick(suggestion: string) {
852
+ this.inputValue = suggestion;
853
+ this.send();
854
+ }
855
+
856
+ private async fetchSuggestions() {
857
+ const suggestionsConfig = this.chatConfig.suggestions;
858
+ if (!this.driver || !suggestionsConfig || suggestionsConfig.behavior === 'never') {
859
+ return;
860
+ }
861
+
862
+ if (suggestionsConfig.behavior === 'initial' && this.messages.length > 0) {
863
+ return;
864
+ }
865
+
866
+ // Skip if a tool flow is currently executing — suggestions would be generated
867
+ // from partial history and would be replaced by the post-response fetch anyway.
868
+ if (this.driver?.isBusy()) {
869
+ return;
870
+ }
871
+
872
+ // Skip if suggestions were already fetched or are in-flight — avoids redundant
873
+ // AI calls when the component reconnects during lifecycle events (pop-out/pop-in,
874
+ // docking) since suggestionsState is persisted in the store.
875
+ if (this.suggestionsState.status === 'loaded' || this.suggestionsState.status === 'loading') {
876
+ return;
877
+ }
878
+
879
+ this._suggestionsGeneration += 1;
880
+ const generation = this._suggestionsGeneration;
881
+ this.suggestionsState = { status: 'loading' };
882
+ // For single-agent (ChatDriver used directly), activeAgent is set but its name is never
883
+ // passed to the driver — build agentInfo here so getSuggestions has context.
884
+ // OrchestratingDriver ignores this parameter and builds its own from specialists.
885
+ const agentInfo: AllAgentSummary[] | undefined = this.activeAgent
886
+ ? [
887
+ {
888
+ name: this.activeAgent.name,
889
+ description: 'description' in this.activeAgent ? this.activeAgent.description : '',
890
+ tools: this.activeAgent.toolDefinitions ?? [],
891
+ },
892
+ ]
893
+ : undefined;
894
+ try {
895
+ const suggestions = await this.driver.getSuggestions(
896
+ this.messages,
897
+ suggestionsConfig.prompt || '',
898
+ suggestionsConfig.count || FoundationAiAssistant.DEFAULT_SUGGESTION_COUNT,
899
+ agentInfo,
900
+ );
901
+ if (generation !== this._suggestionsGeneration) return;
902
+ this.suggestionsState = { status: 'loaded', suggestions };
903
+ this.scrollToBottom();
904
+ } catch (e) {
905
+ if (generation !== this._suggestionsGeneration) return;
906
+ this.suggestionsState = { status: 'error', message: (e as Error).message };
907
+ logger.error('Failed to fetch suggestions:', e);
908
+ }
909
+ }
910
+
449
911
  private async send() {
450
912
  const input = this.inputValue.trim();
451
913
  if ((!input && !this.attachments.length) || this.state === 'loading') return;
914
+ // Capture the session ref before any await. If a lifecycle event occurs during
915
+ // execution, disconnectedCallback clears this._sessionRef — but the captured
916
+ // reference lets the finally block still write 'idle' back to the shared store,
917
+ // which in turn triggers the _executionCompletionUnsub subscription on whichever
918
+ // element is currently active (e.g. the collapse-mode element after docking).
919
+ const capturedSessionRef = this._sessionRef;
452
920
  const pendingAttachments = this.attachments.length ? [...this.attachments] : undefined;
453
921
  this.inputValue = '';
454
922
  this.attachments = [];
455
923
  this.attachmentErrors = [];
924
+ this.suggestionsState = { status: 'idle' };
925
+ this._userScrolledAway = false;
456
926
  this.state = 'loading';
457
927
  this.startLoadingTimer();
458
928
  const displayInput = pendingAttachments?.length
@@ -462,15 +932,17 @@ export class FoundationAiAssistant extends GenesisElement {
462
932
  await this.driver?.sendMessage(displayInput, pendingAttachments);
463
933
  } finally {
464
934
  this.stopLoadingTimer();
465
- this.state = 'idle';
935
+ // Write directly via captured ref — this._sessionRef may be null if the element
936
+ // disconnected mid-execution. The state setter also calls syncShowHalo(); replicate
937
+ // that here for the case where this element is still connected.
938
+ capturedSessionRef?.actions.aiAssistant.setState('idle');
939
+ this.syncShowHalo();
940
+ this.fetchSuggestions();
466
941
  this.restoreFocusIfAppropriate();
467
942
  }
468
943
  }
469
944
 
470
945
  private restoreFocusIfAppropriate() {
471
- // If focus is still within this component (document.activeElement === this, since shadow DOM
472
- // reports the host) or nothing specific has focus (body), return focus to the input.
473
- // If the user has navigated to another element in the app, leave them there.
474
946
  const active = document.activeElement;
475
947
  if (active !== document.body && active !== this) return;
476
948
  requestAnimationFrame(() => {
@@ -479,7 +951,7 @@ export class FoundationAiAssistant extends GenesisElement {
479
951
  }
480
952
 
481
953
  onChatHeaderMouseDown(e: MouseEvent) {
482
- if (this.popoutMode !== 'expand') return;
954
+ if (this.popoutMode === 'collapse') return;
483
955
  e.preventDefault();
484
956
  this.dispatchEvent(
485
957
  new CustomEvent('chat-header-mousedown', {
@@ -494,7 +966,8 @@ export class FoundationAiAssistant extends GenesisElement {
494
966
  const detail = (e as CustomEvent).detail;
495
967
  if (detail && detail.interactionId) {
496
968
  this.startLoadingTimer();
497
- this.driver?.resolveInteraction(detail.interactionId, detail);
969
+ const { interactionId, ...result } = detail;
970
+ this.driver?.resolveInteraction(interactionId, result);
498
971
  }
499
972
  }
500
973
  }