@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.
Files changed (64) hide show
  1. package/api-extractor.json +8 -1
  2. package/dist/ai-assistant.api.json +1216 -141
  3. package/dist/ai-assistant.d.ts +216 -15
  4. package/dist/dts/components/agent-picker/agent-picker.d.ts +69 -0
  5. package/dist/dts/components/agent-picker/agent-picker.d.ts.map +1 -0
  6. package/dist/dts/components/agent-picker/agent-picker.styles.d.ts +2 -0
  7. package/dist/dts/components/agent-picker/agent-picker.styles.d.ts.map +1 -0
  8. package/dist/dts/components/agent-picker/agent-picker.template.d.ts +5 -0
  9. package/dist/dts/components/agent-picker/agent-picker.template.d.ts.map +1 -0
  10. package/dist/dts/components/agent-picker/index.d.ts +2 -0
  11. package/dist/dts/components/agent-picker/index.d.ts.map +1 -0
  12. package/dist/dts/components/chat-driver/chat-driver.d.ts +21 -0
  13. package/dist/dts/components/chat-driver/chat-driver.d.ts.map +1 -1
  14. package/dist/dts/components/orchestrating-driver/orchestrating-driver.d.ts +14 -0
  15. package/dist/dts/components/orchestrating-driver/orchestrating-driver.d.ts.map +1 -1
  16. package/dist/dts/config/config.d.ts +22 -12
  17. package/dist/dts/config/config.d.ts.map +1 -1
  18. package/dist/dts/index.d.ts +1 -0
  19. package/dist/dts/index.d.ts.map +1 -1
  20. package/dist/dts/main/main.d.ts +72 -4
  21. package/dist/dts/main/main.d.ts.map +1 -1
  22. package/dist/dts/main/main.styles.d.ts.map +1 -1
  23. package/dist/dts/main/main.template.d.ts.map +1 -1
  24. package/dist/dts/main/main.types.d.ts +1 -0
  25. package/dist/dts/main/main.types.d.ts.map +1 -1
  26. package/dist/dts/state/ai-assistant-slice.d.ts +39 -1
  27. package/dist/dts/state/ai-assistant-slice.d.ts.map +1 -1
  28. package/dist/dts/state/session-store.d.ts +6 -0
  29. package/dist/dts/state/session-store.d.ts.map +1 -1
  30. package/dist/dts/utils/animated-panel-toggle.d.ts +26 -0
  31. package/dist/dts/utils/animated-panel-toggle.d.ts.map +1 -0
  32. package/dist/dts/utils/index.d.ts +1 -0
  33. package/dist/dts/utils/index.d.ts.map +1 -1
  34. package/dist/esm/components/agent-picker/agent-picker.js +157 -0
  35. package/dist/esm/components/agent-picker/agent-picker.styles.js +73 -0
  36. package/dist/esm/components/agent-picker/agent-picker.template.js +72 -0
  37. package/dist/esm/components/agent-picker/index.js +1 -0
  38. package/dist/esm/components/chat-driver/chat-driver.js +48 -6
  39. package/dist/esm/components/orchestrating-driver/orchestrating-driver.js +43 -6
  40. package/dist/esm/index.js +1 -0
  41. package/dist/esm/main/main.js +215 -21
  42. package/dist/esm/main/main.styles.js +59 -0
  43. package/dist/esm/main/main.template.js +66 -12
  44. package/dist/esm/state/ai-assistant-slice.js +15 -0
  45. package/dist/esm/utils/animated-panel-toggle.js +62 -0
  46. package/dist/esm/utils/index.js +1 -0
  47. package/dist/tsconfig.tsbuildinfo +1 -1
  48. package/docs/gemini-empty-response.md +110 -0
  49. package/package.json +16 -16
  50. package/src/components/agent-picker/agent-picker.styles.ts +74 -0
  51. package/src/components/agent-picker/agent-picker.template.ts +88 -0
  52. package/src/components/agent-picker/agent-picker.ts +148 -0
  53. package/src/components/agent-picker/index.ts +1 -0
  54. package/src/components/chat-driver/chat-driver.ts +65 -8
  55. package/src/components/orchestrating-driver/orchestrating-driver.ts +45 -6
  56. package/src/config/config.ts +28 -11
  57. package/src/index.ts +1 -0
  58. package/src/main/main.styles.ts +59 -0
  59. package/src/main/main.template.ts +79 -13
  60. package/src/main/main.ts +220 -19
  61. package/src/main/main.types.ts +2 -0
  62. package/src/state/ai-assistant-slice.ts +51 -1
  63. package/src/utils/animated-panel-toggle.ts +62 -0
  64. 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 { ChatAttachment, ChatConfig, ChatMessage } from '@genesislcap/foundation-ai';
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 onSubAgentStop = () => {
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
- if (this.settingsOpen) {
777
- const panel = this.shadowRoot?.querySelector('.settings-panel') as HTMLElement | null;
778
- if (panel) {
779
- panel.classList.add('closing');
780
- panel.addEventListener(
781
- 'animationend',
782
- () => {
783
- panel.classList.remove('closing');
784
- this.settingsOpen = false;
785
- },
786
- { once: true },
787
- );
788
- } else {
789
- this.settingsOpen = false;
790
- }
791
- } else {
792
- this.settingsOpen = true;
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
@@ -14,6 +14,8 @@ export type AiAssistantState = 'idle' | 'loading' | 'error';
14
14
  */
15
15
  export type PopoutMode = 'expand' | 'collapse';
16
16
 
17
+ export type { AgentPickerMode } from '@genesislcap/foundation-ai';
18
+
17
19
  /**
18
20
  * State of the chat suggestions feature.
19
21
  *
@@ -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
+ }
@@ -1,2 +1,3 @@
1
+ export * from './animated-panel-toggle';
1
2
  export * from './logger';
2
3
  export * from './tool-fold';