@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.
- package/dist/ai-assistant.api.json +4061 -1416
- package/dist/ai-assistant.d.ts +594 -81
- package/dist/dts/channel/ai-activity-channel.d.ts +4 -22
- package/dist/dts/channel/ai-activity-channel.d.ts.map +1 -1
- package/dist/dts/components/ai-driver/ai-driver.d.ts +52 -0
- package/dist/dts/components/ai-driver/ai-driver.d.ts.map +1 -0
- package/dist/dts/components/ai-driver/index.d.ts +2 -0
- package/dist/dts/components/ai-driver/index.d.ts.map +1 -0
- package/dist/dts/components/chat-driver/chat-driver.d.ts +63 -8
- package/dist/dts/components/chat-driver/chat-driver.d.ts.map +1 -1
- package/dist/dts/components/chat-interaction-wrapper/chat-interaction-wrapper.d.ts +3 -3
- package/dist/dts/components/chat-interaction-wrapper/chat-interaction-wrapper.d.ts.map +1 -1
- package/dist/dts/components/chat-markdown/chat-markdown.d.ts +1 -1
- package/dist/dts/components/chat-markdown/chat-markdown.d.ts.map +1 -1
- package/dist/dts/components/halo-overlay.d.ts +13 -1
- package/dist/dts/components/halo-overlay.d.ts.map +1 -1
- package/dist/dts/components/orchestrating-driver/index.d.ts +2 -0
- package/dist/dts/components/orchestrating-driver/index.d.ts.map +1 -0
- package/dist/dts/components/orchestrating-driver/orchestrating-driver.d.ts +39 -0
- package/dist/dts/components/orchestrating-driver/orchestrating-driver.d.ts.map +1 -0
- package/dist/dts/components/popout-manager/index.d.ts +2 -0
- package/dist/dts/components/popout-manager/index.d.ts.map +1 -0
- package/dist/dts/components/popout-manager/popout-manager.d.ts +72 -0
- package/dist/dts/components/popout-manager/popout-manager.d.ts.map +1 -0
- package/dist/dts/config/config.d.ts +43 -15
- package/dist/dts/config/config.d.ts.map +1 -1
- package/dist/dts/config/fallback-agents.d.ts +20 -0
- package/dist/dts/config/fallback-agents.d.ts.map +1 -0
- package/dist/dts/config/index.d.ts +1 -0
- package/dist/dts/config/index.d.ts.map +1 -1
- package/dist/dts/index.d.ts +6 -0
- package/dist/dts/index.d.ts.map +1 -1
- package/dist/dts/main/main.d.ts +122 -21
- 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/main/main.types.d.ts +16 -0
- package/dist/dts/main/main.types.d.ts.map +1 -1
- package/dist/dts/state/ai-assistant-slice.d.ts +38 -0
- package/dist/dts/state/ai-assistant-slice.d.ts.map +1 -0
- package/dist/dts/state/driver-registry.d.ts +22 -0
- package/dist/dts/state/driver-registry.d.ts.map +1 -0
- package/dist/dts/state/session-store.d.ts +37 -0
- package/dist/dts/state/session-store.d.ts.map +1 -0
- package/dist/dts/suggestions/chat-suggestions.d.ts +7 -0
- package/dist/dts/suggestions/chat-suggestions.d.ts.map +1 -0
- package/dist/dts/types/ai-chat-widget.d.ts +3 -2
- package/dist/dts/types/ai-chat-widget.d.ts.map +1 -1
- package/dist/dts/utils/index.d.ts +1 -0
- package/dist/dts/utils/index.d.ts.map +1 -1
- package/dist/dts/utils/tool-fold.d.ts +133 -0
- package/dist/dts/utils/tool-fold.d.ts.map +1 -0
- package/dist/esm/components/ai-driver/ai-driver.js +1 -0
- package/dist/esm/components/ai-driver/index.js +1 -0
- package/dist/esm/components/chat-driver/chat-driver.js +499 -67
- package/dist/esm/components/chat-interaction-wrapper/chat-interaction-wrapper.js +2 -2
- package/dist/esm/components/chat-markdown/chat-markdown.js +1 -1
- package/dist/esm/components/halo-overlay.js +53 -7
- package/dist/esm/components/orchestrating-driver/index.js +1 -0
- package/dist/esm/components/orchestrating-driver/orchestrating-driver.js +247 -0
- package/dist/esm/components/popout-manager/index.js +1 -0
- package/dist/esm/components/popout-manager/popout-manager.js +126 -0
- package/dist/esm/config/fallback-agents.js +26 -0
- package/dist/esm/config/index.js +1 -0
- package/dist/esm/index.js +6 -0
- package/dist/esm/main/main.js +546 -112
- package/dist/esm/main/main.styles.js +200 -4
- package/dist/esm/main/main.template.js +163 -63
- package/dist/esm/state/ai-assistant-slice.js +54 -0
- package/dist/esm/state/driver-registry.js +46 -0
- package/dist/esm/state/session-store.js +39 -0
- package/dist/esm/suggestions/chat-suggestions.js +147 -0
- package/dist/esm/utils/index.js +1 -0
- package/dist/esm/utils/tool-fold.js +92 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/docs/migration-FUI-2495.md +339 -0
- package/docs/sub_agent.md +310 -0
- package/package.json +16 -15
- package/src/channel/ai-activity-channel.ts +4 -20
- package/src/components/ai-driver/ai-driver.ts +69 -0
- package/src/components/ai-driver/index.ts +1 -0
- package/src/components/chat-driver/chat-driver.ts +600 -73
- package/src/components/chat-interaction-wrapper/chat-interaction-wrapper.ts +3 -3
- package/src/components/chat-markdown/chat-markdown.ts +1 -1
- package/src/components/halo-overlay.ts +45 -7
- package/src/components/orchestrating-driver/index.ts +1 -0
- package/src/components/orchestrating-driver/orchestrating-driver.ts +328 -0
- package/src/components/popout-manager/index.ts +1 -0
- package/src/components/popout-manager/popout-manager.ts +147 -0
- package/src/config/config.ts +45 -15
- package/src/config/fallback-agents.ts +29 -0
- package/src/config/index.ts +1 -0
- package/src/index.ts +6 -0
- package/src/main/main.styles.ts +200 -4
- package/src/main/main.template.ts +200 -80
- package/src/main/main.ts +567 -94
- package/src/main/main.types.ts +11 -0
- package/src/state/ai-assistant-slice.ts +80 -0
- package/src/state/driver-registry.ts +51 -0
- package/src/state/session-store.ts +56 -0
- package/src/suggestions/chat-suggestions.ts +158 -0
- package/src/types/ai-chat-widget.ts +4 -2
- package/src/utils/index.ts +1 -0
- package/src/utils/tool-fold.ts +181 -0
- 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 {
|
|
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`
|
|
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
|
|
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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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?:
|
|
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
|
-
*
|
|
102
|
-
*
|
|
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
|
-
|
|
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 =
|
|
258
|
+
this.showHalo = 'no';
|
|
109
259
|
return;
|
|
110
260
|
}
|
|
111
261
|
const last = this.messages[this.messages.length - 1];
|
|
112
|
-
|
|
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 (
|
|
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
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
//
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
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
|
-
|
|
184
|
-
|
|
393
|
+
|
|
394
|
+
const singleAgent = agents?.[0];
|
|
395
|
+
this.activeAgent = singleAgent;
|
|
396
|
+
return new ChatDriver(
|
|
185
397
|
this.aiProvider,
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
398
|
+
singleAgent?.toolHandlers ?? {},
|
|
399
|
+
singleAgent?.toolDefinitions ?? [],
|
|
400
|
+
singleAgent?.systemPrompt,
|
|
401
|
+
singleAgent?.primerHistory,
|
|
402
|
+
agent.maxToolIterations,
|
|
403
|
+
agent.maxFoldOperations,
|
|
191
404
|
);
|
|
192
|
-
|
|
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
|
-
|
|
200
|
-
|
|
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
|
-
|
|
217
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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',
|
|
681
|
+
agenticActivityBus.publish('chat-popout', undefined);
|
|
303
682
|
} else if (this.popoutMode === 'collapse') {
|
|
304
|
-
agenticActivityBus.publish('chat-popin',
|
|
683
|
+
agenticActivityBus.publish('chat-popin', undefined);
|
|
305
684
|
}
|
|
306
685
|
}
|
|
307
686
|
|
|
308
|
-
/**
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|
-
|
|
969
|
+
const { interactionId, ...result } = detail;
|
|
970
|
+
this.driver?.resolveInteraction(interactionId, result);
|
|
498
971
|
}
|
|
499
972
|
}
|
|
500
973
|
}
|