@genesislcap/ai-assistant 14.432.2 → 14.433.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/api-extractor.json +8 -1
- package/dist/ai-assistant.api.json +1216 -141
- package/dist/ai-assistant.d.ts +216 -15
- package/dist/dts/components/agent-picker/agent-picker.d.ts +69 -0
- package/dist/dts/components/agent-picker/agent-picker.d.ts.map +1 -0
- package/dist/dts/components/agent-picker/agent-picker.styles.d.ts +2 -0
- package/dist/dts/components/agent-picker/agent-picker.styles.d.ts.map +1 -0
- package/dist/dts/components/agent-picker/agent-picker.template.d.ts +5 -0
- package/dist/dts/components/agent-picker/agent-picker.template.d.ts.map +1 -0
- package/dist/dts/components/agent-picker/index.d.ts +2 -0
- package/dist/dts/components/agent-picker/index.d.ts.map +1 -0
- package/dist/dts/components/chat-driver/chat-driver.d.ts +21 -0
- package/dist/dts/components/chat-driver/chat-driver.d.ts.map +1 -1
- package/dist/dts/components/orchestrating-driver/orchestrating-driver.d.ts +14 -0
- package/dist/dts/components/orchestrating-driver/orchestrating-driver.d.ts.map +1 -1
- package/dist/dts/config/config.d.ts +22 -12
- package/dist/dts/config/config.d.ts.map +1 -1
- package/dist/dts/index.d.ts +1 -0
- package/dist/dts/index.d.ts.map +1 -1
- package/dist/dts/main/main.d.ts +72 -4
- 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 +1 -0
- package/dist/dts/main/main.types.d.ts.map +1 -1
- package/dist/dts/state/ai-assistant-slice.d.ts +39 -1
- package/dist/dts/state/ai-assistant-slice.d.ts.map +1 -1
- package/dist/dts/state/session-store.d.ts +6 -0
- package/dist/dts/state/session-store.d.ts.map +1 -1
- package/dist/dts/utils/animated-panel-toggle.d.ts +26 -0
- package/dist/dts/utils/animated-panel-toggle.d.ts.map +1 -0
- package/dist/dts/utils/index.d.ts +1 -0
- package/dist/dts/utils/index.d.ts.map +1 -1
- package/dist/esm/components/agent-picker/agent-picker.js +157 -0
- package/dist/esm/components/agent-picker/agent-picker.styles.js +73 -0
- package/dist/esm/components/agent-picker/agent-picker.template.js +72 -0
- package/dist/esm/components/agent-picker/index.js +1 -0
- package/dist/esm/components/chat-driver/chat-driver.js +48 -6
- package/dist/esm/components/orchestrating-driver/orchestrating-driver.js +43 -6
- package/dist/esm/index.js +1 -0
- package/dist/esm/main/main.js +215 -21
- package/dist/esm/main/main.styles.js +59 -0
- package/dist/esm/main/main.template.js +66 -12
- package/dist/esm/state/ai-assistant-slice.js +15 -0
- package/dist/esm/utils/animated-panel-toggle.js +62 -0
- package/dist/esm/utils/index.js +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/docs/gemini-empty-response.md +110 -0
- package/package.json +16 -16
- package/src/components/agent-picker/agent-picker.styles.ts +74 -0
- package/src/components/agent-picker/agent-picker.template.ts +88 -0
- package/src/components/agent-picker/agent-picker.ts +148 -0
- package/src/components/agent-picker/index.ts +1 -0
- package/src/components/chat-driver/chat-driver.ts +65 -8
- package/src/components/orchestrating-driver/orchestrating-driver.ts +45 -6
- package/src/config/config.ts +28 -11
- package/src/index.ts +1 -0
- package/src/main/main.styles.ts +59 -0
- package/src/main/main.template.ts +79 -13
- package/src/main/main.ts +220 -19
- package/src/main/main.types.ts +2 -0
- package/src/state/ai-assistant-slice.ts +51 -1
- package/src/utils/animated-panel-toggle.ts +62 -0
- package/src/utils/index.ts +1 -0
package/src/main/main.ts
CHANGED
|
@@ -22,7 +22,12 @@
|
|
|
22
22
|
// =============================================================================
|
|
23
23
|
|
|
24
24
|
import { AIProvider } from '@genesislcap/foundation-ai';
|
|
25
|
-
import type {
|
|
25
|
+
import type {
|
|
26
|
+
ChatAttachment,
|
|
27
|
+
ChatConfig,
|
|
28
|
+
ChatInputDuringExecutionMode,
|
|
29
|
+
ChatMessage,
|
|
30
|
+
} from '@genesislcap/foundation-ai';
|
|
26
31
|
import { avoidTreeShaking } from '@genesislcap/foundation-utils';
|
|
27
32
|
import {
|
|
28
33
|
customElement,
|
|
@@ -34,6 +39,7 @@ import {
|
|
|
34
39
|
} from '@genesislcap/web-core';
|
|
35
40
|
import { agenticActivityBus } from '../channel/ai-activity-bus';
|
|
36
41
|
import { AiActivityHalo } from '../components/activity-halo/activity-halo';
|
|
42
|
+
import { AgentPicker } from '../components/agent-picker/agent-picker';
|
|
37
43
|
import type { AiDriver, AllAgentSummary } from '../components/ai-driver/ai-driver';
|
|
38
44
|
import { AiChatBubble } from '../components/chat-bubble/chat-bubble';
|
|
39
45
|
import { ChatDriver } from '../components/chat-driver/chat-driver';
|
|
@@ -44,13 +50,21 @@ import { OrchestratingDriver } from '../components/orchestrating-driver/orchestr
|
|
|
44
50
|
import type { AgentConfig } from '../config/config';
|
|
45
51
|
import { getOrCreateDriver, deleteDriver } from '../state/driver-registry';
|
|
46
52
|
import { getSessionStore, hasSessionStore, type SessionStoreReturn } from '../state/session-store';
|
|
53
|
+
import {
|
|
54
|
+
AI_COLOUR_AMBER,
|
|
55
|
+
AI_COLOUR_CYAN,
|
|
56
|
+
AI_COLOUR_PINK,
|
|
57
|
+
AI_COLOUR_VIOLET,
|
|
58
|
+
} from '../styles/ai-colours';
|
|
47
59
|
import { ChatSuggestions } from '../suggestions/chat-suggestions';
|
|
60
|
+
import { AnimatedPanelToggle } from '../utils/animated-panel-toggle';
|
|
48
61
|
import { logger } from '../utils/logger';
|
|
49
62
|
import { expandToolTree } from '../utils/tool-fold';
|
|
50
63
|
import { styles } from './main.styles';
|
|
51
64
|
import { FoundationAiAssistantTemplate } from './main.template';
|
|
52
65
|
import { ALL_ANIMATIONS } from './main.types';
|
|
53
66
|
import type {
|
|
67
|
+
AgentPickerMode,
|
|
54
68
|
AiAssistantAnimation,
|
|
55
69
|
AiAssistantState,
|
|
56
70
|
PopoutMode,
|
|
@@ -58,6 +72,24 @@ import type {
|
|
|
58
72
|
} from './main.types';
|
|
59
73
|
|
|
60
74
|
/** Context window sizes (in tokens) for known models. */
|
|
75
|
+
/**
|
|
76
|
+
* Pin tint palette, cycled by agent position. Matches the four brand colours
|
|
77
|
+
* used by the halo overlay and loading dots.
|
|
78
|
+
*/
|
|
79
|
+
const PIN_COLOURS = [AI_COLOUR_AMBER, AI_COLOUR_PINK, AI_COLOUR_CYAN, AI_COLOUR_VIOLET];
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Duration of the agent-picker slide-out animation. Must stay in sync with the
|
|
83
|
+
* `agent-picker-slide-out` keyframe duration in `main.styles.ts`.
|
|
84
|
+
*/
|
|
85
|
+
const AGENT_PICKER_CLOSE_MS = 200;
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Duration of the settings panel slide-out animation. Must stay in sync with
|
|
89
|
+
* the `settings-slide-out` keyframe duration in `main.styles.ts`.
|
|
90
|
+
*/
|
|
91
|
+
const SETTINGS_CLOSE_MS = 200;
|
|
92
|
+
|
|
61
93
|
const MODEL_CONTEXT_LIMITS: Record<string, number> = {
|
|
62
94
|
'gemini-2.5-flash': 1_048_576,
|
|
63
95
|
'gemini-2.5-flash-lite': 1_048_576,
|
|
@@ -74,6 +106,7 @@ avoidTreeShaking(
|
|
|
74
106
|
AiChatBubble,
|
|
75
107
|
AiActivityHalo,
|
|
76
108
|
ChatSuggestions,
|
|
109
|
+
AgentPicker,
|
|
77
110
|
);
|
|
78
111
|
|
|
79
112
|
/** Recursively strips `toolHandlers` from an agent and all its sub-agents. */
|
|
@@ -125,6 +158,14 @@ export class FoundationAiAssistant extends GenesisElement {
|
|
|
125
158
|
*/
|
|
126
159
|
@observable agents?: AgentConfig[];
|
|
127
160
|
@observable chatConfig: ChatConfig = {};
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Resolved agent picker mode from `chatConfig.picker.mode`. Defaults to
|
|
164
|
+
* `'disabled'`.
|
|
165
|
+
*/
|
|
166
|
+
get agentPicker(): AgentPickerMode {
|
|
167
|
+
return this.chatConfig.picker?.mode ?? 'disabled';
|
|
168
|
+
}
|
|
128
169
|
@observable debugStateFactory?: () => unknown;
|
|
129
170
|
/** When set, enables Redux DevTools for this instance's session store. */
|
|
130
171
|
@attr({ attribute: 'debug-redux', mode: 'boolean' }) debugRedux = false;
|
|
@@ -223,6 +264,39 @@ export class FoundationAiAssistant extends GenesisElement {
|
|
|
223
264
|
this._sessionRef?.actions.aiAssistant.setEnabledAnimations(value);
|
|
224
265
|
}
|
|
225
266
|
|
|
267
|
+
/**
|
|
268
|
+
* Whether the agent picker slide-out panel is open. Lives on the session
|
|
269
|
+
* store so the bubble-dialog and popout-panel instances stay in sync.
|
|
270
|
+
*/
|
|
271
|
+
get agentPickerOpen(): boolean {
|
|
272
|
+
return this._sessionRef?.store.aiAssistant.agentPickerOpen ?? false;
|
|
273
|
+
}
|
|
274
|
+
set agentPickerOpen(value: boolean) {
|
|
275
|
+
this._sessionRef?.actions.aiAssistant.setAgentPickerOpen(value);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Name of the agent the user has pinned via the agent picker. `null` means
|
|
280
|
+
* automatic routing (Auto). Persisted on the session store, so it survives
|
|
281
|
+
* pop-in/pop-out but resets on page refresh.
|
|
282
|
+
*/
|
|
283
|
+
get pinnedAgentName(): string | null {
|
|
284
|
+
return this._sessionRef?.store.aiAssistant.pinnedAgentName ?? null;
|
|
285
|
+
}
|
|
286
|
+
set pinnedAgentName(value: string | null) {
|
|
287
|
+
const previous = this._sessionRef?.store.aiAssistant.pinnedAgentName ?? null;
|
|
288
|
+
this._sessionRef?.actions.aiAssistant.setPinnedAgentName(value);
|
|
289
|
+
if (this.driver instanceof OrchestratingDriver) {
|
|
290
|
+
this.driver.setPinnedAgent(value);
|
|
291
|
+
}
|
|
292
|
+
// Suggestions are scoped to the active routing — resetting forces a fresh
|
|
293
|
+
// fetch when the user pins/unpins so the prompts match the new agent.
|
|
294
|
+
if (previous !== value) {
|
|
295
|
+
this.suggestionsState = { status: 'idle' };
|
|
296
|
+
this.fetchSuggestions();
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
226
300
|
get liveSubAgentTrace(): ChatMessage[] {
|
|
227
301
|
return this._sessionRef?.store.aiAssistant.liveSubAgentTrace ?? [];
|
|
228
302
|
}
|
|
@@ -237,6 +311,28 @@ export class FoundationAiAssistant extends GenesisElement {
|
|
|
237
311
|
this._sessionRef?.actions.aiAssistant.setLiveSubAgentName(value);
|
|
238
312
|
}
|
|
239
313
|
|
|
314
|
+
/**
|
|
315
|
+
* In-flight per-call chat-input overrides pushed by `requestSubAgent`
|
|
316
|
+
* invocations. Empty means no override is active.
|
|
317
|
+
*/
|
|
318
|
+
get subAgentInputOverrides() {
|
|
319
|
+
return this._sessionRef?.store.aiAssistant.subAgentInputOverrides ?? [];
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Resolves the effective chat-input behaviour while the assistant is
|
|
324
|
+
* executing. Sub-agent overrides take precedence over the agent-level
|
|
325
|
+
* config; among overrides the most restrictive wins (`'hidden'` >
|
|
326
|
+
* `'disabled'`). Falls back to the active agent's config, then `'disabled'`.
|
|
327
|
+
*/
|
|
328
|
+
@volatile
|
|
329
|
+
get effectiveChatInputDuringExecution(): ChatInputDuringExecutionMode {
|
|
330
|
+
const overrides = this.subAgentInputOverrides;
|
|
331
|
+
if (overrides.some((o) => o.mode === 'hidden')) return 'hidden';
|
|
332
|
+
if (overrides.some((o) => o.mode === 'disabled')) return 'disabled';
|
|
333
|
+
return this.activeAgent?.chatInputDuringExecution ?? 'disabled';
|
|
334
|
+
}
|
|
335
|
+
|
|
240
336
|
/** Most recent prompt token count from the AI provider, if available. */
|
|
241
337
|
get contextTokens(): number | undefined {
|
|
242
338
|
return this._sessionRef?.store.aiAssistant.contextTokens;
|
|
@@ -484,20 +580,40 @@ export class FoundationAiAssistant extends GenesisElement {
|
|
|
484
580
|
// driver's own history array (which is still being mutated by the tool loop).
|
|
485
581
|
this.liveSubAgentTrace = structuredClone(history);
|
|
486
582
|
};
|
|
487
|
-
const
|
|
583
|
+
const onSubAgentStart = (e: Event) => {
|
|
584
|
+
const { invocationId, chatInputDuringExecution } = (e as CustomEvent).detail as {
|
|
585
|
+
invocationId: string;
|
|
586
|
+
chatInputDuringExecution?: ChatInputDuringExecutionMode;
|
|
587
|
+
};
|
|
588
|
+
if (!chatInputDuringExecution) return;
|
|
589
|
+
this._sessionRef?.actions.aiAssistant.addSubAgentInputOverride({
|
|
590
|
+
id: invocationId,
|
|
591
|
+
mode: chatInputDuringExecution,
|
|
592
|
+
});
|
|
593
|
+
};
|
|
594
|
+
const onSubAgentStop = (e: Event) => {
|
|
488
595
|
this.liveSubAgentTrace = [];
|
|
489
596
|
this.liveSubAgentName = null;
|
|
597
|
+
const { invocationId } = (e as CustomEvent).detail as { invocationId?: string };
|
|
598
|
+
if (invocationId) {
|
|
599
|
+
this._sessionRef?.actions.aiAssistant.removeSubAgentInputOverride({ id: invocationId });
|
|
600
|
+
}
|
|
490
601
|
};
|
|
491
602
|
driver.addEventListener('sub-agent-history-updated', onSubAgentHistoryUpdated);
|
|
603
|
+
driver.addEventListener('sub-agent-start', onSubAgentStart);
|
|
492
604
|
driver.addEventListener('sub-agent-stop', onSubAgentStop);
|
|
493
605
|
|
|
494
606
|
const cleanups: (() => void)[] = [
|
|
495
607
|
() => driver.removeEventListener('history-updated', onHistoryUpdated),
|
|
496
608
|
() => driver.removeEventListener('sub-agent-history-updated', onSubAgentHistoryUpdated),
|
|
609
|
+
() => driver.removeEventListener('sub-agent-start', onSubAgentStart),
|
|
497
610
|
() => driver.removeEventListener('sub-agent-stop', onSubAgentStop),
|
|
498
611
|
];
|
|
499
612
|
|
|
500
613
|
if (driver instanceof OrchestratingDriver) {
|
|
614
|
+
// Restore any pinned agent from the session store onto the freshly built
|
|
615
|
+
// driver, so pop-in/pop-out and agents-array reassignment preserve the pin.
|
|
616
|
+
driver.setPinnedAgent(this.pinnedAgentName);
|
|
501
617
|
const onOrchStart = () => {
|
|
502
618
|
this.showHalo = 'orchestrating';
|
|
503
619
|
};
|
|
@@ -553,6 +669,11 @@ export class FoundationAiAssistant extends GenesisElement {
|
|
|
553
669
|
this.enabledAnimations =
|
|
554
670
|
(ui.animations?.enabled as AiAssistantAnimation[]) ??
|
|
555
671
|
(ui.animations ? [...ALL_ANIMATIONS] : []);
|
|
672
|
+
|
|
673
|
+
const defaultAgent = this.chatConfig.picker?.defaultAgent;
|
|
674
|
+
if (defaultAgent && (this.agents ?? []).some((a) => a.name === defaultAgent)) {
|
|
675
|
+
this.pinnedAgentName = defaultAgent;
|
|
676
|
+
}
|
|
556
677
|
}
|
|
557
678
|
|
|
558
679
|
this.driver = getOrCreateDriver(key, () => this.createDriver());
|
|
@@ -634,6 +755,11 @@ export class FoundationAiAssistant extends GenesisElement {
|
|
|
634
755
|
this._userScrolledAway = false;
|
|
635
756
|
document.removeEventListener('pointerdown', this._handleGlobalInteraction, true);
|
|
636
757
|
document.removeEventListener('focusin', this._handleGlobalInteraction, true);
|
|
758
|
+
// Finalise any in-flight panel-close animations before clearing
|
|
759
|
+
// _sessionRef, otherwise the closed state never makes it to the store and
|
|
760
|
+
// the panel re-mounts open.
|
|
761
|
+
this._agentPickerToggle.finalize();
|
|
762
|
+
this._settingsToggle.finalize();
|
|
637
763
|
// Clear local references only — driver and store stay in their registries.
|
|
638
764
|
this.driver = undefined;
|
|
639
765
|
this._sessionRef = undefined;
|
|
@@ -772,25 +898,97 @@ export class FoundationAiAssistant extends GenesisElement {
|
|
|
772
898
|
return parts.length ? parts.join('::') : undefined;
|
|
773
899
|
}
|
|
774
900
|
|
|
901
|
+
private readonly _settingsToggle = new AnimatedPanelToggle(
|
|
902
|
+
this,
|
|
903
|
+
'.settings-panel',
|
|
904
|
+
SETTINGS_CLOSE_MS,
|
|
905
|
+
() => this.settingsOpen,
|
|
906
|
+
(v) => {
|
|
907
|
+
this.settingsOpen = v;
|
|
908
|
+
},
|
|
909
|
+
);
|
|
910
|
+
|
|
775
911
|
toggleSettings() {
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
912
|
+
this._settingsToggle.toggle();
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
private readonly _agentPickerToggle = new AnimatedPanelToggle(
|
|
916
|
+
this,
|
|
917
|
+
'.agent-picker-panel',
|
|
918
|
+
AGENT_PICKER_CLOSE_MS,
|
|
919
|
+
() => this.agentPickerOpen,
|
|
920
|
+
(v) => {
|
|
921
|
+
this.agentPickerOpen = v;
|
|
922
|
+
},
|
|
923
|
+
);
|
|
924
|
+
|
|
925
|
+
toggleAgentPicker() {
|
|
926
|
+
this._agentPickerToggle.toggle();
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
/**
|
|
930
|
+
* Programmatically pin an agent by name. Returns `true` if the pin was
|
|
931
|
+
* applied, `false` if the agent isn't in the configured agents array or
|
|
932
|
+
* the call was suppressed by `force: false`.
|
|
933
|
+
*
|
|
934
|
+
* With `force: false`, the call is a no-op when a `picker.defaultAgent` is
|
|
935
|
+
* configured and the user has already moved away from it (either by picking
|
|
936
|
+
* another agent or by switching to Auto). This lets hosts seed an opinion
|
|
937
|
+
* without overriding an explicit user choice.
|
|
938
|
+
*
|
|
939
|
+
* @public
|
|
940
|
+
*/
|
|
941
|
+
setAgent(agentName: string, options?: { force?: boolean }): boolean {
|
|
942
|
+
if (!(this.agents ?? []).some((a) => a.name === agentName)) return false;
|
|
943
|
+
const force = options?.force ?? true;
|
|
944
|
+
if (!force) {
|
|
945
|
+
const defaultAgent = this.chatConfig.picker?.defaultAgent;
|
|
946
|
+
if (defaultAgent && this.pinnedAgentName !== defaultAgent) return false;
|
|
793
947
|
}
|
|
948
|
+
this.pinnedAgentName = agentName;
|
|
949
|
+
return true;
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
/** Whether the picker toggle button should appear. Mirrors the picker's own visibility rule. */
|
|
953
|
+
@volatile
|
|
954
|
+
get agentPickerEnabled(): boolean {
|
|
955
|
+
if (this.agentPicker === 'disabled') return false;
|
|
956
|
+
if ((this.agents?.length ?? 0) <= 1) return false;
|
|
957
|
+
return (this.agents ?? []).some((a) => a.manualSelection?.enabled);
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
/** Hint text for the currently pinned agent, if any. Used in the toggle button tooltip. */
|
|
961
|
+
@volatile
|
|
962
|
+
get pinnedAgentHint(): string | undefined {
|
|
963
|
+
if (this.pinnedAgentName === null) return undefined;
|
|
964
|
+
return this.agents?.find((a) => a.name === this.pinnedAgentName)?.manualSelection?.hint;
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
/**
|
|
968
|
+
* Tint applied to the pin icon when an agent is pinned. Picked from the
|
|
969
|
+
* brand palette by agent position (modulo the palette length), so each agent
|
|
970
|
+
* gets a consistent colour across renders. `undefined` when nothing is
|
|
971
|
+
* pinned, the agent isn't in the current array, or the host has opted out
|
|
972
|
+
* via `chatConfig.picker.disablePinColours`.
|
|
973
|
+
*/
|
|
974
|
+
@volatile
|
|
975
|
+
get pinnedAgentColour(): string | undefined {
|
|
976
|
+
if (this.pinnedAgentName === null) return undefined;
|
|
977
|
+
if (this.chatConfig.picker?.disablePinColours) return undefined;
|
|
978
|
+
const idx = this.agents?.findIndex((a) => a.name === this.pinnedAgentName) ?? -1;
|
|
979
|
+
if (idx < 0) return undefined;
|
|
980
|
+
return PIN_COLOURS[idx % PIN_COLOURS.length];
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
/**
|
|
984
|
+
* Placeholder shown in the chat input. Substitutes the pinned agent's name
|
|
985
|
+
* when one is selected so the user has a clearer signal of where their
|
|
986
|
+
* message will go; otherwise falls back to the host-provided placeholder.
|
|
987
|
+
*/
|
|
988
|
+
@volatile
|
|
989
|
+
get effectivePlaceholder(): string {
|
|
990
|
+
if (this.pinnedAgentName) return `Message ${this.pinnedAgentName}...`;
|
|
991
|
+
return this.placeholder;
|
|
794
992
|
}
|
|
795
993
|
|
|
796
994
|
toggleShowToolCalls() {
|
|
@@ -1005,6 +1203,9 @@ export class FoundationAiAssistant extends GenesisElement {
|
|
|
1005
1203
|
this.attachmentErrors = [];
|
|
1006
1204
|
this.suggestionsState = { status: 'idle' };
|
|
1007
1205
|
this._userScrolledAway = false;
|
|
1206
|
+
// Close the picker on send — the toggle button is disabled during loading,
|
|
1207
|
+
// so without this an open panel would be stuck open with clickable segments.
|
|
1208
|
+
if (this.agentPickerOpen) this.toggleAgentPicker();
|
|
1008
1209
|
this.state = 'loading';
|
|
1009
1210
|
this.startLoadingTimer();
|
|
1010
1211
|
const displayInput = pendingAttachments?.length
|
package/src/main/main.types.ts
CHANGED
|
@@ -1,9 +1,24 @@
|
|
|
1
|
-
import type { ChatMessage } from '@genesislcap/foundation-ai';
|
|
1
|
+
import type { ChatInputDuringExecutionMode, ChatMessage } from '@genesislcap/foundation-ai';
|
|
2
2
|
import { createSlice } from '@genesislcap/foundation-redux';
|
|
3
3
|
import type { PayloadAction } from '@genesislcap/foundation-redux';
|
|
4
4
|
import type { AgentConfig } from '../config/config';
|
|
5
5
|
import type { AiAssistantAnimation, AiAssistantState, SuggestionsState } from '../main/main.types';
|
|
6
6
|
|
|
7
|
+
/**
|
|
8
|
+
* A single in-flight per-call chat-input override pushed by a `requestSubAgent`
|
|
9
|
+
* invocation. Tracked as an array (not a counter) so the slice can survive
|
|
10
|
+
* pop-in/pop-out and so a listener that connects mid-execution can compute the
|
|
11
|
+
* effective mode without having seen the start event.
|
|
12
|
+
*
|
|
13
|
+
* @internal
|
|
14
|
+
*/
|
|
15
|
+
export interface SubAgentInputOverride {
|
|
16
|
+
/** Unique per-invocation id, paired with the start/stop events. */
|
|
17
|
+
id: string;
|
|
18
|
+
/** The mode requested for this invocation. */
|
|
19
|
+
mode: ChatInputDuringExecutionMode;
|
|
20
|
+
}
|
|
21
|
+
|
|
7
22
|
/**
|
|
8
23
|
* Shape of a single AI assistant session's serializable state.
|
|
9
24
|
*
|
|
@@ -20,12 +35,30 @@ export interface AiAssistantSessionState {
|
|
|
20
35
|
contextTokens: number | undefined;
|
|
21
36
|
contextLimit: number | undefined;
|
|
22
37
|
activeAgent: Omit<AgentConfig, 'toolHandlers'> | undefined;
|
|
38
|
+
/**
|
|
39
|
+
* Name of the agent the user has pinned via the picker. `null` means the
|
|
40
|
+
* orchestrator chooses each turn (Auto). Survives lifecycle events but
|
|
41
|
+
* resets on page refresh.
|
|
42
|
+
*/
|
|
43
|
+
pinnedAgentName: string | null;
|
|
44
|
+
/**
|
|
45
|
+
* Whether the agent picker slide-out panel is currently open. Lives in the
|
|
46
|
+
* slice so the bubble-dialog and popout-panel instances stay in sync as the
|
|
47
|
+
* user pops in and out.
|
|
48
|
+
*/
|
|
49
|
+
agentPickerOpen: boolean;
|
|
23
50
|
/** Draft text in the input box — preserved across pop-in/pop-out cycles. */
|
|
24
51
|
inputValue: string;
|
|
25
52
|
/** Live trace from a currently-executing sub-agent. Cleared on sub-agent-stop. */
|
|
26
53
|
liveSubAgentTrace: ChatMessage[];
|
|
27
54
|
/** Name of the currently-executing sub-agent, or null when idle. */
|
|
28
55
|
liveSubAgentName: string | null;
|
|
56
|
+
/**
|
|
57
|
+
* In-flight per-call chat-input overrides pushed by `requestSubAgent` calls.
|
|
58
|
+
* The most restrictive entry (`'hidden'` > `'disabled'`) wins; an empty list
|
|
59
|
+
* means the agent-level `chatInputDuringExecution` applies.
|
|
60
|
+
*/
|
|
61
|
+
subAgentInputOverrides: SubAgentInputOverride[];
|
|
29
62
|
}
|
|
30
63
|
|
|
31
64
|
export const defaultSessionState: AiAssistantSessionState = {
|
|
@@ -39,9 +72,12 @@ export const defaultSessionState: AiAssistantSessionState = {
|
|
|
39
72
|
contextTokens: undefined,
|
|
40
73
|
contextLimit: undefined,
|
|
41
74
|
activeAgent: undefined,
|
|
75
|
+
pinnedAgentName: null,
|
|
76
|
+
agentPickerOpen: false,
|
|
42
77
|
inputValue: '',
|
|
43
78
|
liveSubAgentTrace: [],
|
|
44
79
|
liveSubAgentName: null,
|
|
80
|
+
subAgentInputOverrides: [],
|
|
45
81
|
};
|
|
46
82
|
|
|
47
83
|
export const aiAssistantSlice = createSlice({
|
|
@@ -78,6 +114,12 @@ export const aiAssistantSlice = createSlice({
|
|
|
78
114
|
setActiveAgent(state, action: PayloadAction<Omit<AgentConfig, 'toolHandlers'> | undefined>) {
|
|
79
115
|
state.activeAgent = action.payload;
|
|
80
116
|
},
|
|
117
|
+
setPinnedAgentName(state, action: PayloadAction<string | null>) {
|
|
118
|
+
state.pinnedAgentName = action.payload;
|
|
119
|
+
},
|
|
120
|
+
setAgentPickerOpen(state, action: PayloadAction<boolean>) {
|
|
121
|
+
state.agentPickerOpen = action.payload;
|
|
122
|
+
},
|
|
81
123
|
setInputValue(state, action: PayloadAction<string>) {
|
|
82
124
|
state.inputValue = action.payload;
|
|
83
125
|
},
|
|
@@ -87,6 +129,14 @@ export const aiAssistantSlice = createSlice({
|
|
|
87
129
|
setLiveSubAgentName(state, action: PayloadAction<string | null>) {
|
|
88
130
|
state.liveSubAgentName = action.payload;
|
|
89
131
|
},
|
|
132
|
+
addSubAgentInputOverride(state, action: PayloadAction<SubAgentInputOverride>) {
|
|
133
|
+
state.subAgentInputOverrides.push(action.payload);
|
|
134
|
+
},
|
|
135
|
+
removeSubAgentInputOverride(state, action: PayloadAction<{ id: string }>) {
|
|
136
|
+
state.subAgentInputOverrides = state.subAgentInputOverrides.filter(
|
|
137
|
+
(o) => o.id !== action.payload.id,
|
|
138
|
+
);
|
|
139
|
+
},
|
|
90
140
|
},
|
|
91
141
|
selectors: {},
|
|
92
142
|
});
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Drives the open/close lifecycle of a slide-out panel inside a component's
|
|
3
|
+
* shadow root, with a tracked close timer instead of an `animationend`
|
|
4
|
+
* listener so the panel cannot wedge open if it's unmounted mid-animation
|
|
5
|
+
* (pop-in/pop-out, dock change, host disconnect).
|
|
6
|
+
*
|
|
7
|
+
* @internal
|
|
8
|
+
*/
|
|
9
|
+
export class AnimatedPanelToggle {
|
|
10
|
+
private timer?: ReturnType<typeof setTimeout>;
|
|
11
|
+
|
|
12
|
+
constructor(
|
|
13
|
+
private readonly host: HTMLElement,
|
|
14
|
+
private readonly selector: string,
|
|
15
|
+
private readonly closeDurationMs: number,
|
|
16
|
+
private readonly getOpen: () => boolean,
|
|
17
|
+
private readonly setOpen: (v: boolean) => void,
|
|
18
|
+
) {}
|
|
19
|
+
|
|
20
|
+
private queryPanel(): HTMLElement | null {
|
|
21
|
+
return this.host.shadowRoot?.querySelector(this.selector) as HTMLElement | null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
toggle(): void {
|
|
25
|
+
if (this.getOpen()) {
|
|
26
|
+
// Capture the panel reference now so the timer callback strips
|
|
27
|
+
// `closing` from the same node, regardless of any later remounts.
|
|
28
|
+
// Leftover `closing` is what breaks the next open: with
|
|
29
|
+
// `animation-fill-mode: forwards` the slide-out keyframe pins the panel
|
|
30
|
+
// to opacity 0, so a remount looks like a no-op.
|
|
31
|
+
const panel = this.queryPanel();
|
|
32
|
+
panel?.classList.add('closing');
|
|
33
|
+
if (this.timer != null) clearTimeout(this.timer);
|
|
34
|
+
this.timer = setTimeout(() => {
|
|
35
|
+
this.timer = undefined;
|
|
36
|
+
panel?.classList.remove('closing');
|
|
37
|
+
this.setOpen(false);
|
|
38
|
+
}, this.closeDurationMs);
|
|
39
|
+
} else {
|
|
40
|
+
// Re-opened mid-close: cancel the pending flip and strip the closing
|
|
41
|
+
// class so the panel snaps back to the open state.
|
|
42
|
+
if (this.timer != null) {
|
|
43
|
+
clearTimeout(this.timer);
|
|
44
|
+
this.timer = undefined;
|
|
45
|
+
this.queryPanel()?.classList.remove('closing');
|
|
46
|
+
}
|
|
47
|
+
this.setOpen(true);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Synchronously finalise an in-flight close — call from
|
|
53
|
+
* `disconnectedCallback` so the closed state lands in the store before any
|
|
54
|
+
* session ref is cleared.
|
|
55
|
+
*/
|
|
56
|
+
finalize(): void {
|
|
57
|
+
if (this.timer == null) return;
|
|
58
|
+
clearTimeout(this.timer);
|
|
59
|
+
this.timer = undefined;
|
|
60
|
+
this.setOpen(false);
|
|
61
|
+
}
|
|
62
|
+
}
|
package/src/utils/index.ts
CHANGED