@genesislcap/ai-assistant 14.458.3 → 14.460.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 (43) hide show
  1. package/dist/ai-assistant.api.json +38 -11
  2. package/dist/ai-assistant.d.ts +49 -5
  3. package/dist/dts/components/chat-driver/chat-driver.d.ts +3 -1
  4. package/dist/dts/components/chat-driver/chat-driver.d.ts.map +1 -1
  5. package/dist/dts/components/flowing-waves-indicator.d.ts +32 -0
  6. package/dist/dts/components/flowing-waves-indicator.d.ts.map +1 -0
  7. package/dist/dts/components/plasma-orb-indicator.d.ts +22 -0
  8. package/dist/dts/components/plasma-orb-indicator.d.ts.map +1 -0
  9. package/dist/dts/components/waves-indicator.d.ts +30 -0
  10. package/dist/dts/components/waves-indicator.d.ts.map +1 -0
  11. package/dist/dts/main/main.d.ts.map +1 -1
  12. package/dist/dts/main/main.styles.d.ts.map +1 -1
  13. package/dist/dts/main/main.template.d.ts.map +1 -1
  14. package/dist/dts/main/main.types.d.ts +44 -4
  15. package/dist/dts/main/main.types.d.ts.map +1 -1
  16. package/dist/dts/utils/animation-exclusivity.d.ts +23 -0
  17. package/dist/dts/utils/animation-exclusivity.d.ts.map +1 -0
  18. package/dist/dts/utils/animation-exclusivity.test.d.ts +2 -0
  19. package/dist/dts/utils/animation-exclusivity.test.d.ts.map +1 -0
  20. package/dist/esm/components/chat-driver/chat-driver.js +7 -2
  21. package/dist/esm/components/chat-driver/chat-driver.test.js +24 -0
  22. package/dist/esm/components/flowing-waves-indicator.js +222 -0
  23. package/dist/esm/components/plasma-orb-indicator.js +280 -0
  24. package/dist/esm/components/waves-indicator.js +189 -0
  25. package/dist/esm/main/main.js +20 -9
  26. package/dist/esm/main/main.styles.js +62 -7
  27. package/dist/esm/main/main.template.js +75 -21
  28. package/dist/esm/main/main.types.js +46 -3
  29. package/dist/esm/utils/animation-exclusivity.js +33 -0
  30. package/dist/esm/utils/animation-exclusivity.test.js +52 -0
  31. package/dist/tsconfig.tsbuildinfo +1 -1
  32. package/package.json +16 -16
  33. package/src/components/chat-driver/chat-driver.test.ts +36 -0
  34. package/src/components/chat-driver/chat-driver.ts +10 -2
  35. package/src/components/flowing-waves-indicator.ts +260 -0
  36. package/src/components/plasma-orb-indicator.ts +281 -0
  37. package/src/components/waves-indicator.ts +221 -0
  38. package/src/main/main.styles.ts +62 -7
  39. package/src/main/main.template.ts +88 -27
  40. package/src/main/main.ts +24 -8
  41. package/src/main/main.types.ts +56 -5
  42. package/src/utils/animation-exclusivity.test.ts +72 -0
  43. package/src/utils/animation-exclusivity.ts +40 -0
@@ -33,21 +33,25 @@ import { AiChatBubble } from '../components/chat-bubble/chat-bubble';
33
33
  import { ChatDriver } from '../components/chat-driver/chat-driver';
34
34
  import { AiChatInteractionWrapper } from '../components/chat-interaction-wrapper/chat-interaction-wrapper';
35
35
  import { AiChatMarkdown } from '../components/chat-markdown/chat-markdown';
36
+ import { AiFlowingWavesIndicator } from '../components/flowing-waves-indicator';
36
37
  import { AiHaloOverlay } from '../components/halo-overlay';
37
38
  import { OrchestratingDriver } from '../components/orchestrating-driver/orchestrating-driver';
39
+ import { AiPlasmaOrbIndicator } from '../components/plasma-orb-indicator';
40
+ import { AiWavesIndicator } from '../components/waves-indicator';
38
41
  import { recordMetaEvent, getMetaEvents, DEBUG_LOG_README, } from '../state/debug-event-log';
39
42
  import { getOrCreateDriver, getDriver, deleteDriver } from '../state/driver-registry';
40
43
  import { getSessionStore, hasSessionStore } from '../state/session-store';
41
44
  import { AI_COLOUR_AMBER, AI_COLOUR_CYAN, AI_COLOUR_PINK, AI_COLOUR_VIOLET, } from '../styles/ai-colours';
42
45
  import { ChatSuggestions } from '../suggestions/chat-suggestions';
43
46
  import { AnimatedPanelToggle } from '../utils/animated-panel-toggle';
47
+ import { resolveExclusiveLoadingStyle } from '../utils/animation-exclusivity';
44
48
  import { logger } from '../utils/logger';
45
49
  import { filterVisibleMessages, trailingInteractionRow } from '../utils/message-partition';
46
50
  import { sumCosts } from '../utils/sum-costs';
47
51
  import { expandToolTree } from '../utils/tool-fold';
48
52
  import { styles } from './main.styles';
49
53
  import { FoundationAiAssistantTemplate } from './main.template';
50
- import { ALL_ANIMATIONS } from './main.types';
54
+ import { DEFAULT_ANIMATIONS } from './main.types';
51
55
  /** Context window sizes (in tokens) for known models. */
52
56
  /**
53
57
  * Pin tint palette, cycled by agent position. Matches the four brand colours
@@ -90,7 +94,7 @@ const COMPOSER_MAX_HEIGHT_PX = 400;
90
94
  /** Keep at least this much of the message list visible while growing the composer. */
91
95
  const COMPOSER_MIN_MESSAGES_PX = 80;
92
96
  // Register supporting components when the main component module is imported.
93
- avoidTreeShaking(AiChatMarkdown, AiChatInteractionWrapper, AiHaloOverlay, AiChatBubble, AiActivityHalo, ChatSuggestions, AgentPicker);
97
+ avoidTreeShaking(AiChatMarkdown, AiChatInteractionWrapper, AiHaloOverlay, AiWavesIndicator, AiFlowingWavesIndicator, AiPlasmaOrbIndicator, AiChatBubble, AiActivityHalo, ChatSuggestions, AgentPicker);
94
98
  /**
95
99
  * Recursively strips non-serializable fields from an agent before storing in Redux:
96
100
  * - `toolHandlers` (functions),
@@ -538,7 +542,9 @@ let FoundationAiAssistant = FoundationAiAssistant_1 = class FoundationAiAssistan
538
542
  return;
539
543
  }
540
544
  const last = this.messages[this.messages.length - 1];
541
- if (last === null || last === void 0 ? void 0 : last.interaction) {
545
+ // Hide only while a pending interaction blocks on the user; a resolved
546
+ // interaction means the assistant is computing again (keep the halo).
547
+ if ((last === null || last === void 0 ? void 0 : last.interaction) && !last.interaction.resolved) {
542
548
  this.showHalo = 'no';
543
549
  }
544
550
  else if (this.showHalo !== 'orchestrating') {
@@ -911,8 +917,7 @@ let FoundationAiAssistant = FoundationAiAssistant_1 = class FoundationAiAssistan
911
917
  this.showToolCalls = ui.showToolCalls === true;
912
918
  this.showThinkingSteps = ui.showThinkingSteps === true;
913
919
  this.showAgentSwitchIndicator = ui.showAgentSwitchIndicator === true;
914
- this.enabledAnimations =
915
- (_c = (_b = ui.animations) === null || _b === void 0 ? void 0 : _b.enabled) !== null && _c !== void 0 ? _c : (ui.animations ? [...ALL_ANIMATIONS] : []);
920
+ this.enabledAnimations = resolveExclusiveLoadingStyle((_c = (_b = ui.animations) === null || _b === void 0 ? void 0 : _b.enabled) !== null && _c !== void 0 ? _c : (ui.animations ? [...DEFAULT_ANIMATIONS] : []));
916
921
  const defaultAgent = (_d = this.chatConfig.picker) === null || _d === void 0 ? void 0 : _d.defaultAgent;
917
922
  if (defaultAgent && ((_e = this.agents) !== null && _e !== void 0 ? _e : []).some((a) => a.name === defaultAgent)) {
918
923
  this.pinnedAgentName = defaultAgent;
@@ -1078,10 +1083,14 @@ let FoundationAiAssistant = FoundationAiAssistant_1 = class FoundationAiAssistan
1078
1083
  // waiting for the user, not computing.
1079
1084
  if (this.busy) {
1080
1085
  const last = this.messages[this.messages.length - 1];
1081
- if (last === null || last === void 0 ? void 0 : last.interaction) {
1086
+ // Only a *pending* interaction means the assistant is blocked waiting on
1087
+ // the user. Once it's resolved — or for any normal step — the assistant is
1088
+ // computing again, so the indicator should resume (e.g. while it works out
1089
+ // the next planning question after the user answers a widget).
1090
+ if ((last === null || last === void 0 ? void 0 : last.interaction) && !last.interaction.resolved) {
1082
1091
  this.stopLoadingTimer();
1083
1092
  }
1084
- else if ((last === null || last === void 0 ? void 0 : last.role) === 'assistant') {
1093
+ else {
1085
1094
  this.startLoadingTimer();
1086
1095
  }
1087
1096
  }
@@ -1334,7 +1343,9 @@ let FoundationAiAssistant = FoundationAiAssistant_1 = class FoundationAiAssistan
1334
1343
  this.showAgentSwitchIndicator = !this.showAgentSwitchIndicator;
1335
1344
  }
1336
1345
  setEnabledAnimations(animations) {
1337
- this.enabledAnimations = animations;
1346
+ // The dots and waves loading styles are mutually exclusive — enabling one
1347
+ // disables the other (see resolveExclusiveLoadingStyle).
1348
+ this.enabledAnimations = resolveExclusiveLoadingStyle(animations, this.enabledAnimations);
1338
1349
  }
1339
1350
  getDebugLog() {
1340
1351
  var _a, _b, _c, _d, _e, _f, _j, _k, _l, _m, _o, _p, _q;
@@ -1822,7 +1833,7 @@ let FoundationAiAssistant = FoundationAiAssistant_1 = class FoundationAiAssistan
1822
1833
  FoundationAiAssistant.SCROLL_BOTTOM_THRESHOLD_PX = 50;
1823
1834
  /** Context-usage percentage that fires the one-shot `context.threshold-crossed` event. */
1824
1835
  FoundationAiAssistant.CONTEXT_USAGE_WARN_PERCENT = 80;
1825
- FoundationAiAssistant.DEFAULT_LOADING_DELAY_S = 5;
1836
+ FoundationAiAssistant.DEFAULT_LOADING_DELAY_S = 0;
1826
1837
  FoundationAiAssistant.DEFAULT_SUGGESTION_COUNT = 3;
1827
1838
  FoundationAiAssistant.MS_PER_SECOND = 1000;
1828
1839
  __decorate([
@@ -95,14 +95,22 @@ export const styles = css `
95
95
  animation: settings-slide-out 0.2s ease-in forwards;
96
96
  }
97
97
 
98
- rapid-multiselect::part(root) {
99
- min-width: 80px;
100
- width: 300%;
98
+ /* Collapsed control footprint stays compact (100px)... */
99
+ rapid-categorized-multiselect::part(root) {
100
+ min-width: 0;
101
+ width: 100px;
101
102
  }
102
103
 
103
- rapid-multiselect::part(control),
104
- .settings-panel > [part='download-button'] {
105
- width: fit-content;
104
+ /* ...while the dropdown panel, being absolutely positioned, can be wider
105
+ (200px) without affecting the root's layout footprint. The control sits on
106
+ a different side of the settings panel depending on the container width
107
+ (see the layout rules below), so the dropdown must anchor to whichever side
108
+ keeps it inside the panel. Default (narrow) layout puts the control on the
109
+ LEFT, so the dropdown grows rightward. */
110
+ rapid-categorized-multiselect::part(options) {
111
+ width: 200px;
112
+ left: 0;
113
+ right: auto;
106
114
  }
107
115
 
108
116
  .settings-panel > [part='toggle-tool-calls'] {
@@ -121,6 +129,7 @@ export const styles = css `
121
129
  }
122
130
 
123
131
  .settings-panel > [part='download-button'] {
132
+ width: fit-content;
124
133
  grid-column: 2;
125
134
  grid-row: 2;
126
135
  }
@@ -170,6 +179,13 @@ export const styles = css `
170
179
  .settings-panel > .settings-animations {
171
180
  grid-row: 2;
172
181
  }
182
+
183
+ /* Control now sits on the RIGHT (justify-self: end / margin-left: auto in
184
+ the wider layouts), so the dropdown grows leftward to stay in the panel. */
185
+ rapid-categorized-multiselect::part(options) {
186
+ left: auto;
187
+ right: 0;
188
+ }
173
189
  }
174
190
 
175
191
  @container (min-width: 750px) {
@@ -411,6 +427,13 @@ export const styles = css `
411
427
  animation: slide-in-left 0.25s ease-out;
412
428
  }
413
429
 
430
+ /* A 'bubble' interaction reads like a normal assistant message: avatar to the
431
+ left, bubble to the right — not the full-width column the other interaction
432
+ presentations use. */
433
+ .message-row.interaction.present-bubble {
434
+ flex-direction: row;
435
+ }
436
+
414
437
  .avatar {
415
438
  position: relative;
416
439
  width: 32px;
@@ -519,7 +542,10 @@ export const styles = css `
519
542
  border: 1px solid color-mix(in srgb, var(--ai-function-color, #86efac) 20%, transparent);
520
543
  }
521
544
 
522
- .message.interaction {
545
+ /* 'label' (default) and 'bare' interactions render the widget chrome-free and
546
+ full-width — the widget owns its body. */
547
+ .message-row.interaction.present-label .message,
548
+ .message-row.interaction.present-bare .message {
523
549
  background: none;
524
550
  border: none;
525
551
  padding: 0;
@@ -527,6 +553,17 @@ export const styles = css `
527
553
  width: 100%;
528
554
  }
529
555
 
556
+ /* 'bubble' interactions reuse the standard assistant-message skin so the
557
+ widget sits inside a normal chat bubble. Padding, max-width and min-width
558
+ come from the base .message rule. */
559
+ .message-row.interaction.present-bubble .message {
560
+ background: linear-gradient(135deg, var(--neutral-layer-3) 0%, var(--neutral-layer-4) 100%);
561
+ color: var(--neutral-foreground-rest);
562
+ border-radius: 4px 18px 18px;
563
+ border: calc(var(--stroke-width) * 1px) solid var(--neutral-stroke-rest);
564
+ box-shadow: var(--ai-message-shadow, 0 2px 8px rgb(0 0 0 / 15%));
565
+ }
566
+
530
567
  .sender {
531
568
  font-weight: bold;
532
569
  font-size: 0.9em;
@@ -803,6 +840,24 @@ export const styles = css `
803
840
  }
804
841
  }
805
842
 
843
+ .thinking-waves,
844
+ .thinking-flowing-waves,
845
+ .thinking-plasma {
846
+ display: flex;
847
+ flex-direction: column;
848
+ align-items: center;
849
+ align-self: center;
850
+ gap: 6px;
851
+ padding: calc(var(--design-unit) * 2px) calc(var(--design-unit) * 3px);
852
+ }
853
+
854
+ .thinking-caption {
855
+ font-size: var(--type-ramp-minus-1-font-size, 12px);
856
+ line-height: var(--type-ramp-minus-1-line-height, 16px);
857
+ color: var(--neutral-foreground-rest);
858
+ opacity: 70%;
859
+ }
860
+
806
861
  .attachment-chips {
807
862
  display: flex;
808
863
  flex-wrap: wrap;
@@ -16,7 +16,7 @@
16
16
  */
17
17
  import { isChatToolCallUnknown } from '@genesislcap/foundation-ai';
18
18
  import { classNames, html, ref, repeat, when } from '@genesislcap/web-core';
19
- import { ANIMATION_DEFS } from './main.types';
19
+ import { ANIMATION_DEFS, LOADING_STYLE_ANIMATIONS } from './main.types';
20
20
  function unknownToolPayload(tc) {
21
21
  if (!isChatToolCallUnknown(tc))
22
22
  return '';
@@ -31,18 +31,16 @@ function unknownToolPayload(tc) {
31
31
  }
32
32
  return lines.join('\n');
33
33
  }
34
- const animationItemRenderer = (option) => html `
35
- <span part="option-label" title="${() => option.tooltip}">${() => option.label}</span>
36
- `;
37
34
  const HALO_SPEED_DEFAULT = 1.5;
38
35
  const HALO_SPEED_ORCHESTRATING = 0.4;
39
36
  const HALO_BORDER_SIZE_DEFAULT = 3;
40
37
  /** Decimal places shown for the running session cost (USD). 4 ≈ $0.0001 resolution. */
41
38
  const SESSION_COST_DECIMALS = 4;
42
39
  const animationOptions = Object.entries(ANIMATION_DEFS).map(([value, def]) => ({
40
+ type: def.category,
43
41
  value,
44
42
  label: def.label,
45
- tooltip: def.tooltip,
43
+ description: def.description,
46
44
  }));
47
45
  // Avatar markup is owned by the component (`assistantIconSafe` / `userIconSafe`),
48
46
  // which holds the sanitized SVG string for the default or any consumer override
@@ -102,6 +100,12 @@ const senderLabel = {
102
100
  interaction: 'Assistant',
103
101
  ai: 'Assistant',
104
102
  };
103
+ /**
104
+ * Resolves how the host frames an interaction widget, defaulting to `'label'`
105
+ * (the historical "Assistant" label, no bubble). Callers guard on
106
+ * `m.interaction` first; non-interaction messages never consult this.
107
+ */
108
+ const interactionPresentation = (m) => { var _a, _b; return (_b = (_a = m.interaction) === null || _a === void 0 ? void 0 : _a.presentation) !== null && _b !== void 0 ? _b : 'label'; };
105
109
  // ─── Sub-agent trace fragments ────────────────────────────────────────────────
106
110
  const subAgentAssistantTemplate = html `
107
111
  <div class="sub-agent-message sub-agent-assistant">${(m) => m.content}</div>
@@ -136,12 +140,61 @@ const liveSubAgentTraceTemplate = html `
136
140
  ${repeat((x) => x.liveSubAgentTrace.filter((m) => m.role !== 'user'), subAgentMessageRowTemplate)}
137
141
  </div>
138
142
  `;
143
+ // The interchangeable loading indicators. These MUST be stable module-level
144
+ // instances: the binding that selects between them reads `enabledAnimations`,
145
+ // which is backed by the redux store proxy and re-evaluates on every change to
146
+ // the aiAssistant slice (i.e. every new message, including hidden tool-call and
147
+ // thinking-step messages). Returning a fresh `html` instance from that binding
148
+ // would make FAST tear down and rebuild the indicator DOM each time, restarting
149
+ // the CSS animations and rAF loops. Stable references let FAST reuse the
150
+ // existing view so the animation runs uninterrupted.
151
+ const thinkingDotsTemplate = html `
152
+ <div class="thinking-dots">
153
+ <div class="dot dot-1"></div>
154
+ <div class="dot dot-2"></div>
155
+ <div class="dot dot-3"></div>
156
+ <div class="dot dot-4"></div>
157
+ </div>
158
+ `;
159
+ const thinkingWavesTemplate = html `
160
+ <div class="thinking-waves" part="thinking-waves">
161
+ <ai-waves-indicator></ai-waves-indicator>
162
+ <span class="thinking-caption">Thinking...</span>
163
+ </div>
164
+ `;
165
+ const thinkingFlowingWavesTemplate = html `
166
+ <div class="thinking-flowing-waves" part="thinking-flowing-waves">
167
+ <ai-flowing-waves-indicator></ai-flowing-waves-indicator>
168
+ <span class="thinking-caption">Thinking...</span>
169
+ </div>
170
+ `;
171
+ const thinkingPlasmaTemplate = html `
172
+ <div class="thinking-plasma" part="thinking-plasma">
173
+ <ai-plasma-orb-indicator></ai-plasma-orb-indicator>
174
+ <span class="thinking-caption">Thinking...</span>
175
+ </div>
176
+ `;
177
+ /**
178
+ * Picks the loading indicator for the currently enabled style, falling back to
179
+ * the dots (also the default when no animations config is supplied). Returns a
180
+ * stable template instance per style — see the note above.
181
+ */
182
+ const selectThinkingTemplate = (x) => {
183
+ const enabled = x.enabledAnimations;
184
+ if (enabled.includes('waves'))
185
+ return thinkingWavesTemplate;
186
+ if (enabled.includes('flowingWaves'))
187
+ return thinkingFlowingWavesTemplate;
188
+ if (enabled.includes('plasma'))
189
+ return thinkingPlasmaTemplate;
190
+ return thinkingDotsTemplate;
191
+ };
139
192
  // ─── Public factory ───────────────────────────────────────────────────────────
140
193
  /** @internal */
141
194
  export const FoundationAiAssistantTemplate = (designSystemPrefix) => {
142
195
  const buttonTag = `${designSystemPrefix}-button`;
143
196
  const switchTag = `${designSystemPrefix}-switch`;
144
- const multiselectTag = `${designSystemPrefix}-multiselect`;
197
+ const categorizedMultiselectTag = `${designSystemPrefix}-categorized-multiselect`;
145
198
  const textareaTag = `${designSystemPrefix}-text-area`;
146
199
  const iconTag = `${designSystemPrefix}-icon`;
147
200
  const progressTag = `${designSystemPrefix}-progress`;
@@ -171,7 +224,9 @@ ${(tc) => { var _a; return (((_a = tc.foldPath) === null || _a === void 0 ? void
171
224
  </div>
172
225
  `)}
173
226
  ${when((m) => m.role !== 'system-event', html `
174
- <div class="message-row ${(m) => messageType(m)}">
227
+ <div
228
+ class="message-row ${(m) => messageType(m)} ${(m) => m.interaction ? `present-${interactionPresentation(m)}` : ''}"
229
+ >
175
230
  <div class="avatar ${(m) => messageType(m)}">
176
231
  ${when(
177
232
  // Keyed on messageType (not raw role) so a synthetic-user message,
@@ -180,8 +235,12 @@ ${(tc) => { var _a; return (((_a = tc.foldPath) === null || _a === void 0 ? void
180
235
  (m) => messageType(m) === 'user', userAvatarTemplate)}${when((m) => messageType(m) !== 'user', assistantAvatarTemplate)}
181
236
  </div>
182
237
  <div class="message ${(m) => messageType(m)}">
183
- <div class="sender">
184
- ${(m, c) => {
238
+ ${when(
239
+ // A 'bare' interaction owns its full presentation — suppress the
240
+ // host sender label. Every other message keeps it.
241
+ (m) => !(m.interaction && interactionPresentation(m) === 'bare'), html `
242
+ <div class="sender">
243
+ ${(m, c) => {
185
244
  var _a;
186
245
  return messageType(m) === 'ai-function' &&
187
246
  m.agentName &&
@@ -189,7 +248,8 @@ ${(tc) => { var _a; return (((_a = tc.foldPath) === null || _a === void 0 ? void
189
248
  ? `Tool Call · ${(_a = m.agentLabel) !== null && _a !== void 0 ? _a : m.agentName}`
190
249
  : senderLabel[messageType(m)];
191
250
  }}
192
- </div>
251
+ </div>
252
+ `)}
193
253
  <div class="content">
194
254
  ${when((m) => m.content, html `
195
255
  <ai-chat-markdown :content="${(m) => m.content}"></ai-chat-markdown>
@@ -356,15 +416,13 @@ ${(tc) => { var _a; return (((_a = tc.foldPath) === null || _a === void 0 ? void
356
416
  ${when((x) => { var _a, _b; return ((_b = (_a = x.chatConfig.ui) === null || _a === void 0 ? void 0 : _a.animations) === null || _b === void 0 ? void 0 : _b.userConfigurable) === true; }, html `
357
417
  <div class="settings-animations">
358
418
  <span class="settings-label">Animations</span>
359
- <${multiselectTag}
419
+ <${categorizedMultiselectTag}
360
420
  part="toggle-animations"
361
421
  :selectedOptions=${(x) => x.enabledAnimations}
362
422
  :options=${() => animationOptions}
363
- :itemRenderer=${() => animationItemRenderer}
364
423
  @selectionChange=${(x, c) => x.setEnabledAnimations(c.event.detail)}
365
424
  search="false"
366
- all="false"
367
- ></${multiselectTag}>
425
+ ></${categorizedMultiselectTag}>
368
426
  </div>
369
427
  `)}
370
428
  </div>
@@ -453,7 +511,8 @@ ${(tc) => { var _a; return (((_a = tc.foldPath) === null || _a === void 0 ? void
453
511
  ${when((x) => {
454
512
  var _a;
455
513
  return x.showLoadingIndicator &&
456
- (((_a = x.chatConfig.ui) === null || _a === void 0 ? void 0 : _a.animations) == null || x.enabledAnimations.includes('loading'));
514
+ (((_a = x.chatConfig.ui) === null || _a === void 0 ? void 0 : _a.animations) == null ||
515
+ LOADING_STYLE_ANIMATIONS.some((s) => x.enabledAnimations.includes(s)));
457
516
  }, html `
458
517
  <div class="message-row ai" part="thinking">
459
518
  <div class="avatar">
@@ -465,12 +524,7 @@ ${(tc) => { var _a; return (((_a = tc.foldPath) === null || _a === void 0 ? void
465
524
  <span class="avatar-icon" :innerHTML="${() => x.assistantIconSafe}"></span>
466
525
  `}
467
526
  </div>
468
- <div class="thinking-dots">
469
- <div class="dot dot-1"></div>
470
- <div class="dot dot-2"></div>
471
- <div class="dot dot-3"></div>
472
- <div class="dot dot-4"></div>
473
- </div>
527
+ ${(x) => selectThinkingTemplate(x)}
474
528
  </div>
475
529
  `)}
476
530
  </div>
@@ -2,16 +2,39 @@
2
2
  * Registry of all available animations with their display metadata.
3
3
  * Adding an entry here automatically extends the {@link AiAssistantAnimation} type.
4
4
  *
5
+ * @remarks
6
+ * `loading` (dots), `waves`, `flowingWaves` and `plasma` are interchangeable
7
+ * styles of the same "is the assistant working" indicator and are therefore
8
+ * grouped under the same {@link AiAssistantAnimationDef.category}. They are
9
+ * mutually exclusive — see `LOADING_STYLE_ANIMATIONS`.
10
+ *
5
11
  * @beta
6
12
  */
7
13
  export const ANIMATION_DEFS = {
8
14
  loading: {
9
- label: 'Loading indicator',
10
- tooltip: 'Shows a pulsing animation while the assistant is generating a response.',
15
+ label: 'Dots',
16
+ description: 'Shows pulsing dots while the assistant is generating a response.',
17
+ category: 'Loading style',
18
+ },
19
+ waves: {
20
+ label: 'Waves',
21
+ description: 'Shows glowing sine waves inside a circle while the assistant is generating a response.',
22
+ category: 'Loading style',
23
+ },
24
+ flowingWaves: {
25
+ label: 'Flowing waves',
26
+ description: 'Shows coloured waves rising and settling from a line while the assistant is generating a response.',
27
+ category: 'Loading style',
28
+ },
29
+ plasma: {
30
+ label: 'Plasma orb',
31
+ description: 'Shows a glowing plasma sphere with drifting energy while the assistant is generating a response.',
32
+ category: 'Loading style',
11
33
  },
12
34
  halo: {
13
35
  label: 'Halo',
14
- tooltip: 'Displays a glowing halo around the assistant avatar while a response is streaming.',
36
+ description: 'Displays a glowing halo around the assistant avatar while a response is streaming.',
37
+ category: 'Effects',
15
38
  },
16
39
  };
17
40
  /**
@@ -20,3 +43,23 @@ export const ANIMATION_DEFS = {
20
43
  * @internal
21
44
  */
22
45
  export const ALL_ANIMATIONS = Object.keys(ANIMATION_DEFS);
46
+ /**
47
+ * The interchangeable "assistant is working" loading-indicator styles. At most
48
+ * one of these may be enabled at a time — enabling one disables the other.
49
+ *
50
+ * @internal
51
+ */
52
+ export const LOADING_STYLE_ANIMATIONS = [
53
+ 'loading',
54
+ 'waves',
55
+ 'flowingWaves',
56
+ 'plasma',
57
+ ];
58
+ /**
59
+ * Animations enabled by default when a consumer opts into the animations
60
+ * feature without specifying an explicit `enabled` list. Keeps the dots loading
61
+ * style (the long-standing default); the waves style is opt-in.
62
+ *
63
+ * @internal
64
+ */
65
+ export const DEFAULT_ANIMATIONS = ['loading', 'halo'];
@@ -0,0 +1,33 @@
1
+ import { LOADING_STYLE_ANIMATIONS } from '../main/main.types';
2
+ /**
3
+ * Enforces that at most one loading-indicator style (dots vs. waves) is enabled
4
+ * at a time. The two are alternative presentations of the same "assistant is
5
+ * working" state, so selecting one must deselect the other.
6
+ *
7
+ * The settings control is a multiselect (checkboxes), which permits selecting
8
+ * both; this resolver makes the loading-style group behave like a radio group
9
+ * by dropping the previously-selected style whenever a new one is added.
10
+ *
11
+ * @param next - The freshly-selected animation list (e.g. emitted by the
12
+ * multiselect, or read from consumer config).
13
+ * @param previous - The animation list in effect before this change. Used to
14
+ * work out which loading style was just added so the other can be dropped.
15
+ * Pass `[]` when resolving an initial/config value with no prior state.
16
+ * @returns `next` with at most one loading style retained. When both are
17
+ * present and neither is newly added (e.g. a misconfigured `enabled` list),
18
+ * the first entry of `LOADING_STYLE_ANIMATIONS` (dots) wins.
19
+ *
20
+ * @internal
21
+ */
22
+ export function resolveExclusiveLoadingStyle(next, previous = []) {
23
+ var _a;
24
+ const selectedStyles = LOADING_STYLE_ANIMATIONS.filter((style) => next.includes(style));
25
+ if (selectedStyles.length <= 1) {
26
+ return next;
27
+ }
28
+ // Both styles selected — keep whichever was just added (absent from
29
+ // `previous`); fall back to the first declared style on ambiguity.
30
+ const justAdded = (_a = selectedStyles.find((style) => !previous.includes(style))) !== null && _a !== void 0 ? _a : selectedStyles[0];
31
+ return next.filter((animation) => animation === justAdded ||
32
+ !LOADING_STYLE_ANIMATIONS.includes(animation));
33
+ }
@@ -0,0 +1,52 @@
1
+ import { assert, createLogicSuite } from '@genesislcap/foundation-testing';
2
+ import { resolveExclusiveLoadingStyle } from './animation-exclusivity';
3
+ const suite = createLogicSuite('resolveExclusiveLoadingStyle');
4
+ suite('leaves a selection with only the dots loading style untouched', () => {
5
+ const next = ['loading', 'halo'];
6
+ assert.equal(resolveExclusiveLoadingStyle(next, ['halo']), ['loading', 'halo']);
7
+ });
8
+ suite('leaves a selection with only the waves loading style untouched', () => {
9
+ const next = ['waves', 'halo'];
10
+ assert.equal(resolveExclusiveLoadingStyle(next, ['halo']), ['waves', 'halo']);
11
+ });
12
+ suite('leaves a selection with no loading style untouched', () => {
13
+ assert.equal(resolveExclusiveLoadingStyle(['halo'], []), ['halo']);
14
+ });
15
+ suite('drops dots when waves was just added on top of dots', () => {
16
+ // Previously dots was on; user ticks waves → waves wins, dots removed.
17
+ const result = resolveExclusiveLoadingStyle(['loading', 'waves', 'halo'], ['loading', 'halo']);
18
+ assert.equal(result, ['waves', 'halo']);
19
+ });
20
+ suite('drops waves when dots was just added on top of waves', () => {
21
+ // Previously waves was on; user ticks dots → dots wins, waves removed.
22
+ const result = resolveExclusiveLoadingStyle(['loading', 'waves', 'halo'], ['waves', 'halo']);
23
+ assert.equal(result, ['loading', 'halo']);
24
+ });
25
+ suite('preserves the order of the surviving selection', () => {
26
+ const result = resolveExclusiveLoadingStyle(['halo', 'loading', 'waves'], ['halo', 'loading']);
27
+ // waves was just added → loading dropped; halo and waves keep their order.
28
+ assert.equal(result, ['halo', 'waves']);
29
+ });
30
+ suite('falls back to dots when both are present with no prior state (e.g. bad config)', () => {
31
+ const result = resolveExclusiveLoadingStyle(['loading', 'waves', 'halo'], []);
32
+ assert.equal(result, ['loading', 'halo']);
33
+ });
34
+ suite('falls back to dots when both were already present (neither newly added)', () => {
35
+ const result = resolveExclusiveLoadingStyle(['loading', 'waves'], ['loading', 'waves', 'halo']);
36
+ assert.equal(result, ['loading']);
37
+ });
38
+ suite('handles an empty selection', () => {
39
+ assert.equal(resolveExclusiveLoadingStyle([], ['loading']), []);
40
+ });
41
+ suite('switching between any two loading styles keeps only the newly added one', () => {
42
+ // flowingWaves was on; user picks plasma → plasma wins, flowingWaves dropped.
43
+ assert.equal(resolveExclusiveLoadingStyle(['flowingWaves', 'plasma', 'halo'], ['flowingWaves', 'halo']), ['plasma', 'halo']);
44
+ // plasma was on; user picks the dots → dots win, plasma dropped.
45
+ assert.equal(resolveExclusiveLoadingStyle(['plasma', 'loading'], ['plasma']), ['loading']);
46
+ });
47
+ suite('drops all but the newly added style when several are somehow selected', () => {
48
+ const result = resolveExclusiveLoadingStyle(['loading', 'waves', 'flowingWaves', 'plasma'], ['loading', 'waves', 'plasma']);
49
+ // flowingWaves is the only one absent from `previous`, so it wins.
50
+ assert.equal(result, ['flowingWaves']);
51
+ });
52
+ suite.run();
@@ -1 +1 @@
1
- {"root":["../src/index.ts","../src/channel/ai-activity-bus.ts","../src/channel/ai-activity-channel.ts","../src/components/halo-overlay.ts","../src/components/activity-halo/activity-halo.ts","../src/components/agent-picker/agent-picker.constants.ts","../src/components/agent-picker/agent-picker.styles.ts","../src/components/agent-picker/agent-picker.template.ts","../src/components/agent-picker/agent-picker.ts","../src/components/agent-picker/index.ts","../src/components/ai-driver/ai-driver.ts","../src/components/ai-driver/index.ts","../src/components/chat-bubble/chat-bubble.styles.ts","../src/components/chat-bubble/chat-bubble.template.ts","../src/components/chat-bubble/chat-bubble.ts","../src/components/chat-bubble/index.ts","../src/components/chat-driver/align-event-globals.ts","../src/components/chat-driver/chat-driver.test.ts","../src/components/chat-driver/chat-driver.ts","../src/components/chat-driver/index.ts","../src/components/chat-interaction-wrapper/chat-interaction-wrapper.styles.ts","../src/components/chat-interaction-wrapper/chat-interaction-wrapper.template.ts","../src/components/chat-interaction-wrapper/chat-interaction-wrapper.test.ts","../src/components/chat-interaction-wrapper/chat-interaction-wrapper.ts","../src/components/chat-interaction-wrapper/index.ts","../src/components/chat-markdown/chat-markdown.ts","../src/components/chat-markdown/index.ts","../src/components/orchestrating-driver/index.ts","../src/components/orchestrating-driver/orchestrating-driver.ts","../src/components/popout-manager/index.ts","../src/components/popout-manager/popout-manager.ts","../src/config/config.ts","../src/config/define-stateful-agent.ts","../src/config/fallback-agents.ts","../src/config/index.ts","../src/config/validate-providers.test.ts","../src/config/validate-providers.ts","../src/main/index.ts","../src/main/main.styles.ts","../src/main/main.template.ts","../src/main/main.ts","../src/main/main.types.ts","../src/state/ai-assistant-slice.ts","../src/state/debug-event-log.ts","../src/state/driver-registry.ts","../src/state/session-store.ts","../src/styles/ai-colours.ts","../src/styles/index.ts","../src/styles/styles.ts","../src/suggestions/chat-suggestions.ts","../src/tags/index.ts","../src/types/ai-chat-widget.ts","../src/utils/animated-panel-toggle.ts","../src/utils/history-transform.ts","../src/utils/index.ts","../src/utils/logger.ts","../src/utils/message-partition.test.ts","../src/utils/message-partition.ts","../src/utils/sum-costs.test.ts","../src/utils/sum-costs.ts","../src/utils/tool-fold.ts"],"version":"5.9.2"}
1
+ {"root":["../src/index.ts","../src/channel/ai-activity-bus.ts","../src/channel/ai-activity-channel.ts","../src/components/flowing-waves-indicator.ts","../src/components/halo-overlay.ts","../src/components/plasma-orb-indicator.ts","../src/components/waves-indicator.ts","../src/components/activity-halo/activity-halo.ts","../src/components/agent-picker/agent-picker.constants.ts","../src/components/agent-picker/agent-picker.styles.ts","../src/components/agent-picker/agent-picker.template.ts","../src/components/agent-picker/agent-picker.ts","../src/components/agent-picker/index.ts","../src/components/ai-driver/ai-driver.ts","../src/components/ai-driver/index.ts","../src/components/chat-bubble/chat-bubble.styles.ts","../src/components/chat-bubble/chat-bubble.template.ts","../src/components/chat-bubble/chat-bubble.ts","../src/components/chat-bubble/index.ts","../src/components/chat-driver/align-event-globals.ts","../src/components/chat-driver/chat-driver.test.ts","../src/components/chat-driver/chat-driver.ts","../src/components/chat-driver/index.ts","../src/components/chat-interaction-wrapper/chat-interaction-wrapper.styles.ts","../src/components/chat-interaction-wrapper/chat-interaction-wrapper.template.ts","../src/components/chat-interaction-wrapper/chat-interaction-wrapper.test.ts","../src/components/chat-interaction-wrapper/chat-interaction-wrapper.ts","../src/components/chat-interaction-wrapper/index.ts","../src/components/chat-markdown/chat-markdown.ts","../src/components/chat-markdown/index.ts","../src/components/orchestrating-driver/index.ts","../src/components/orchestrating-driver/orchestrating-driver.ts","../src/components/popout-manager/index.ts","../src/components/popout-manager/popout-manager.ts","../src/config/config.ts","../src/config/define-stateful-agent.ts","../src/config/fallback-agents.ts","../src/config/index.ts","../src/config/validate-providers.test.ts","../src/config/validate-providers.ts","../src/main/index.ts","../src/main/main.styles.ts","../src/main/main.template.ts","../src/main/main.ts","../src/main/main.types.ts","../src/state/ai-assistant-slice.ts","../src/state/debug-event-log.ts","../src/state/driver-registry.ts","../src/state/session-store.ts","../src/styles/ai-colours.ts","../src/styles/index.ts","../src/styles/styles.ts","../src/suggestions/chat-suggestions.ts","../src/tags/index.ts","../src/types/ai-chat-widget.ts","../src/utils/animated-panel-toggle.ts","../src/utils/animation-exclusivity.test.ts","../src/utils/animation-exclusivity.ts","../src/utils/history-transform.ts","../src/utils/index.ts","../src/utils/logger.ts","../src/utils/message-partition.test.ts","../src/utils/message-partition.ts","../src/utils/sum-costs.test.ts","../src/utils/sum-costs.ts","../src/utils/tool-fold.ts"],"version":"5.9.2"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@genesislcap/ai-assistant",
3
3
  "description": "Genesis AI Assistant micro-frontend",
4
- "version": "14.458.3",
4
+ "version": "14.460.0",
5
5
  "license": "SEE LICENSE IN license.txt",
6
6
  "main": "dist/esm/index.js",
7
7
  "types": "dist/ai-assistant.d.ts",
@@ -64,24 +64,24 @@
64
64
  }
65
65
  },
66
66
  "devDependencies": {
67
- "@genesislcap/foundation-testing": "14.458.3",
68
- "@genesislcap/genx": "14.458.3",
69
- "@genesislcap/rollup-builder": "14.458.3",
70
- "@genesislcap/ts-builder": "14.458.3",
71
- "@genesislcap/uvu-playwright-builder": "14.458.3",
72
- "@genesislcap/vite-builder": "14.458.3",
73
- "@genesislcap/webpack-builder": "14.458.3",
67
+ "@genesislcap/foundation-testing": "14.460.0",
68
+ "@genesislcap/genx": "14.460.0",
69
+ "@genesislcap/rollup-builder": "14.460.0",
70
+ "@genesislcap/ts-builder": "14.460.0",
71
+ "@genesislcap/uvu-playwright-builder": "14.460.0",
72
+ "@genesislcap/vite-builder": "14.460.0",
73
+ "@genesislcap/webpack-builder": "14.460.0",
74
74
  "@types/dompurify": "^3.0.5",
75
75
  "@types/marked": "^5.0.2"
76
76
  },
77
77
  "dependencies": {
78
- "@genesislcap/foundation-ai": "14.458.3",
79
- "@genesislcap/foundation-logger": "14.458.3",
80
- "@genesislcap/foundation-redux": "14.458.3",
81
- "@genesislcap/foundation-ui": "14.458.3",
82
- "@genesislcap/foundation-utils": "14.458.3",
83
- "@genesislcap/rapid-design-system": "14.458.3",
84
- "@genesislcap/web-core": "14.458.3",
78
+ "@genesislcap/foundation-ai": "14.460.0",
79
+ "@genesislcap/foundation-logger": "14.460.0",
80
+ "@genesislcap/foundation-redux": "14.460.0",
81
+ "@genesislcap/foundation-ui": "14.460.0",
82
+ "@genesislcap/foundation-utils": "14.460.0",
83
+ "@genesislcap/rapid-design-system": "14.460.0",
84
+ "@genesislcap/web-core": "14.460.0",
85
85
  "dompurify": "^3.3.1",
86
86
  "marked": "^17.0.3"
87
87
  },
@@ -93,5 +93,5 @@
93
93
  "publishConfig": {
94
94
  "access": "public"
95
95
  },
96
- "gitHead": "b15ab1abb2bb8420f0c67e5c90472e74ff98d0ad"
96
+ "gitHead": "12d48ef6ec786b6fc0ad12bccb3682d0792e508c"
97
97
  }