@genesislcap/ai-assistant 14.432.2 → 14.433.1

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 (68) 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.constants.d.ts +3 -0
  5. package/dist/dts/components/agent-picker/agent-picker.constants.d.ts.map +1 -0
  6. package/dist/dts/components/agent-picker/agent-picker.d.ts +69 -0
  7. package/dist/dts/components/agent-picker/agent-picker.d.ts.map +1 -0
  8. package/dist/dts/components/agent-picker/agent-picker.styles.d.ts +2 -0
  9. package/dist/dts/components/agent-picker/agent-picker.styles.d.ts.map +1 -0
  10. package/dist/dts/components/agent-picker/agent-picker.template.d.ts +5 -0
  11. package/dist/dts/components/agent-picker/agent-picker.template.d.ts.map +1 -0
  12. package/dist/dts/components/agent-picker/index.d.ts +2 -0
  13. package/dist/dts/components/agent-picker/index.d.ts.map +1 -0
  14. package/dist/dts/components/chat-driver/chat-driver.d.ts +21 -0
  15. package/dist/dts/components/chat-driver/chat-driver.d.ts.map +1 -1
  16. package/dist/dts/components/orchestrating-driver/orchestrating-driver.d.ts +14 -0
  17. package/dist/dts/components/orchestrating-driver/orchestrating-driver.d.ts.map +1 -1
  18. package/dist/dts/config/config.d.ts +22 -12
  19. package/dist/dts/config/config.d.ts.map +1 -1
  20. package/dist/dts/index.d.ts +1 -0
  21. package/dist/dts/index.d.ts.map +1 -1
  22. package/dist/dts/main/main.d.ts +72 -4
  23. package/dist/dts/main/main.d.ts.map +1 -1
  24. package/dist/dts/main/main.styles.d.ts.map +1 -1
  25. package/dist/dts/main/main.template.d.ts.map +1 -1
  26. package/dist/dts/main/main.types.d.ts +1 -0
  27. package/dist/dts/main/main.types.d.ts.map +1 -1
  28. package/dist/dts/state/ai-assistant-slice.d.ts +39 -1
  29. package/dist/dts/state/ai-assistant-slice.d.ts.map +1 -1
  30. package/dist/dts/state/session-store.d.ts +6 -0
  31. package/dist/dts/state/session-store.d.ts.map +1 -1
  32. package/dist/dts/utils/animated-panel-toggle.d.ts +26 -0
  33. package/dist/dts/utils/animated-panel-toggle.d.ts.map +1 -0
  34. package/dist/dts/utils/index.d.ts +1 -0
  35. package/dist/dts/utils/index.d.ts.map +1 -1
  36. package/dist/esm/components/agent-picker/agent-picker.constants.js +2 -0
  37. package/dist/esm/components/agent-picker/agent-picker.js +157 -0
  38. package/dist/esm/components/agent-picker/agent-picker.styles.js +73 -0
  39. package/dist/esm/components/agent-picker/agent-picker.template.js +72 -0
  40. package/dist/esm/components/agent-picker/index.js +1 -0
  41. package/dist/esm/components/chat-driver/chat-driver.js +48 -6
  42. package/dist/esm/components/orchestrating-driver/orchestrating-driver.js +43 -6
  43. package/dist/esm/index.js +1 -0
  44. package/dist/esm/main/main.js +215 -21
  45. package/dist/esm/main/main.styles.js +59 -0
  46. package/dist/esm/main/main.template.js +66 -12
  47. package/dist/esm/state/ai-assistant-slice.js +15 -0
  48. package/dist/esm/utils/animated-panel-toggle.js +62 -0
  49. package/dist/esm/utils/index.js +1 -0
  50. package/dist/tsconfig.tsbuildinfo +1 -1
  51. package/docs/gemini-empty-response.md +110 -0
  52. package/package.json +16 -16
  53. package/src/components/agent-picker/agent-picker.constants.ts +2 -0
  54. package/src/components/agent-picker/agent-picker.styles.ts +74 -0
  55. package/src/components/agent-picker/agent-picker.template.ts +89 -0
  56. package/src/components/agent-picker/agent-picker.ts +148 -0
  57. package/src/components/agent-picker/index.ts +1 -0
  58. package/src/components/chat-driver/chat-driver.ts +65 -8
  59. package/src/components/orchestrating-driver/orchestrating-driver.ts +45 -6
  60. package/src/config/config.ts +28 -11
  61. package/src/index.ts +1 -0
  62. package/src/main/main.styles.ts +59 -0
  63. package/src/main/main.template.ts +79 -13
  64. package/src/main/main.ts +220 -19
  65. package/src/main/main.types.ts +2 -0
  66. package/src/state/ai-assistant-slice.ts +51 -1
  67. package/src/utils/animated-panel-toggle.ts +62 -0
  68. package/src/utils/index.ts +1 -0
@@ -189,6 +189,68 @@ ${(tc) => { var _a; return (((_a = tc.foldPath) === null || _a === void 0 ? void
189
189
  </div>
190
190
  </div>
191
191
  `)}
192
+ `;
193
+ // ── Agent picker ────────────────────────────────────────────────────────────
194
+ const agentToggleButtonTemplate = html `
195
+ <${buttonTag}
196
+ class="agent-toggle-button"
197
+ part="agent-toggle-button"
198
+ appearance="stealth"
199
+ title=${(x) => {
200
+ var _a;
201
+ return x.agentPickerOpen
202
+ ? 'Close agent picker'
203
+ : x.pinnedAgentName !== null
204
+ ? x.pinnedAgentHint
205
+ ? `${x.pinnedAgentName} — ${x.pinnedAgentHint}`
206
+ : ((_a = x.pinnedAgentName) !== null && _a !== void 0 ? _a : '')
207
+ : 'Attempts to route messages to the correct available agent. Click to manually pin an agent.';
208
+ }}
209
+ ?disabled=${(x) => x.state === 'loading'}
210
+ @click=${(x) => x.toggleAgentPicker()}
211
+ >
212
+ ${when((x) => x.agentPickerOpen, html `<${iconTag} name="chevron-down"></${iconTag}>`)}
213
+ ${when((x) => !x.agentPickerOpen && x.pinnedAgentName !== null, html `
214
+ <${iconTag}
215
+ name="thumbtack"
216
+ style="${(x) => (x.pinnedAgentColour ? `color: ${x.pinnedAgentColour}` : '')}"
217
+ ></${iconTag}>
218
+ `)}
219
+ ${when((x) => !x.agentPickerOpen && x.pinnedAgentName === null, html `
220
+ <span class="agent-toggle-label">Auto</span>
221
+ `)}
222
+ </${buttonTag}>
223
+ `;
224
+ const attachButtonTemplate = html `
225
+ <${buttonTag}
226
+ class="attach-button"
227
+ part="attach-button"
228
+ appearance="stealth"
229
+ title=${(x) => { var _a; return `Attach file (${(_a = x.chatConfig.ui) === null || _a === void 0 ? void 0 : _a.acceptedFiles})`; }}
230
+ ?disabled=${(x) => x.state === 'loading'}
231
+ @click=${(x) => x.triggerFileInput()}
232
+ ><${iconTag} name="paperclip"></${iconTag}></${buttonTag}>
233
+ `;
234
+ const inputLeftControlsTemplate = html `
235
+ <div class="input-left-controls" part="input-left-controls">
236
+ ${when((x) => x.agentPickerEnabled, agentToggleButtonTemplate)}
237
+ ${when((x) => { var _a; return !!((_a = x.chatConfig.ui) === null || _a === void 0 ? void 0 : _a.acceptedFiles); }, attachButtonTemplate)}
238
+ </div>
239
+ `;
240
+ const agentPickerPanelTemplate = html `
241
+ <div class="agent-picker-panel" part="agent-picker-panel">
242
+ <foundation-ai-agent-picker
243
+ part="agent-picker"
244
+ :designSystemPrefix="${(x) => x.designSystemPrefix}"
245
+ :mode="${(x) => x.agentPicker}"
246
+ :agents="${(x) => { var _a; return (_a = x.agents) !== null && _a !== void 0 ? _a : []; }}"
247
+ :pinnedAgentName="${(x) => x.pinnedAgentName}"
248
+ @agent-pinned="${(x, c) => {
249
+ x.pinnedAgentName = c.event.detail;
250
+ x.toggleAgentPicker();
251
+ }}"
252
+ ></foundation-ai-agent-picker>
253
+ </div>
192
254
  `;
193
255
  // ── Root template ───────────────────────────────────────────────────────────
194
256
  return html `
@@ -382,23 +444,15 @@ ${(tc) => { var _a; return (((_a = tc.foldPath) === null || _a === void 0 ? void
382
444
  @suggestion-clicked="${(x, c) => x.handleSuggestionClick(c.event.detail)}"
383
445
  ></chat-suggestions>
384
446
  `)}
385
- ${when((x) => { var _a; return !(x.state === 'loading' && ((_a = x.activeAgent) === null || _a === void 0 ? void 0 : _a.chatInputDuringExecution) === 'hidden'); }, html `
447
+ ${when((x) => x.agentPickerEnabled && x.agentPickerOpen, agentPickerPanelTemplate)}
448
+ ${when((x) => !(x.state === 'loading' && x.effectiveChatInputDuringExecution === 'hidden'), html `
386
449
  <div class="input-row" part="input-row">
387
- ${when((x) => { var _a; return !!((_a = x.chatConfig.ui) === null || _a === void 0 ? void 0 : _a.acceptedFiles); }, html `
388
- <${buttonTag}
389
- class="attach-button"
390
- part="attach-button"
391
- appearance="stealth"
392
- title=${(x) => { var _a; return `Attach file (${(_a = x.chatConfig.ui) === null || _a === void 0 ? void 0 : _a.acceptedFiles})`; }}
393
- ?disabled=${(x) => x.state === 'loading'}
394
- @click=${(x) => x.triggerFileInput()}
395
- ><${iconTag} name="paperclip"></${iconTag}></${buttonTag}>
396
- `)}
450
+ ${when((x) => { var _a; return x.agentPickerEnabled || !!((_a = x.chatConfig.ui) === null || _a === void 0 ? void 0 : _a.acceptedFiles); }, inputLeftControlsTemplate)}
397
451
  <${textareaTag}
398
452
  ${ref('chatInputEl')}
399
453
  class="chat-input"
400
454
  part="input"
401
- placeholder=${(x) => x.placeholder}
455
+ placeholder=${(x) => x.effectivePlaceholder}
402
456
  :value=${(x) => x.inputValue}
403
457
  ?disabled=${(x) => x.state === 'loading'}
404
458
  @input=${(x, c) => (x.inputValue = c.event.target.value)}
@@ -10,9 +10,12 @@ export const defaultSessionState = {
10
10
  contextTokens: undefined,
11
11
  contextLimit: undefined,
12
12
  activeAgent: undefined,
13
+ pinnedAgentName: null,
14
+ agentPickerOpen: false,
13
15
  inputValue: '',
14
16
  liveSubAgentTrace: [],
15
17
  liveSubAgentName: null,
18
+ subAgentInputOverrides: [],
16
19
  };
17
20
  export const aiAssistantSlice = createSlice({
18
21
  name: 'aiAssistant',
@@ -48,6 +51,12 @@ export const aiAssistantSlice = createSlice({
48
51
  setActiveAgent(state, action) {
49
52
  state.activeAgent = action.payload;
50
53
  },
54
+ setPinnedAgentName(state, action) {
55
+ state.pinnedAgentName = action.payload;
56
+ },
57
+ setAgentPickerOpen(state, action) {
58
+ state.agentPickerOpen = action.payload;
59
+ },
51
60
  setInputValue(state, action) {
52
61
  state.inputValue = action.payload;
53
62
  },
@@ -57,6 +66,12 @@ export const aiAssistantSlice = createSlice({
57
66
  setLiveSubAgentName(state, action) {
58
67
  state.liveSubAgentName = action.payload;
59
68
  },
69
+ addSubAgentInputOverride(state, action) {
70
+ state.subAgentInputOverrides.push(action.payload);
71
+ },
72
+ removeSubAgentInputOverride(state, action) {
73
+ state.subAgentInputOverrides = state.subAgentInputOverrides.filter((o) => o.id !== action.payload.id);
74
+ },
60
75
  },
61
76
  selectors: {},
62
77
  });
@@ -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
+ constructor(host, selector, closeDurationMs, getOpen, setOpen) {
11
+ this.host = host;
12
+ this.selector = selector;
13
+ this.closeDurationMs = closeDurationMs;
14
+ this.getOpen = getOpen;
15
+ this.setOpen = setOpen;
16
+ }
17
+ queryPanel() {
18
+ var _a;
19
+ return (_a = this.host.shadowRoot) === null || _a === void 0 ? void 0 : _a.querySelector(this.selector);
20
+ }
21
+ toggle() {
22
+ var _a;
23
+ if (this.getOpen()) {
24
+ // Capture the panel reference now so the timer callback strips
25
+ // `closing` from the same node, regardless of any later remounts.
26
+ // Leftover `closing` is what breaks the next open: with
27
+ // `animation-fill-mode: forwards` the slide-out keyframe pins the panel
28
+ // to opacity 0, so a remount looks like a no-op.
29
+ const panel = this.queryPanel();
30
+ panel === null || panel === void 0 ? void 0 : panel.classList.add('closing');
31
+ if (this.timer != null)
32
+ clearTimeout(this.timer);
33
+ this.timer = setTimeout(() => {
34
+ this.timer = undefined;
35
+ panel === null || panel === void 0 ? void 0 : panel.classList.remove('closing');
36
+ this.setOpen(false);
37
+ }, this.closeDurationMs);
38
+ }
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
+ (_a = this.queryPanel()) === null || _a === void 0 ? void 0 : _a.classList.remove('closing');
46
+ }
47
+ this.setOpen(true);
48
+ }
49
+ }
50
+ /**
51
+ * Synchronously finalise an in-flight close — call from
52
+ * `disconnectedCallback` so the closed state lands in the store before any
53
+ * session ref is cleared.
54
+ */
55
+ finalize() {
56
+ if (this.timer == null)
57
+ 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';
@@ -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/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/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.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/fallback-agents.ts","../src/config/index.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/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/history-transform.ts","../src/utils/index.ts","../src/utils/logger.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/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/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.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/fallback-agents.ts","../src/config/index.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/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/tool-fold.ts"],"version":"5.9.2"}
@@ -0,0 +1,110 @@
1
+ # Gemini 2.5 empty-response quirk — follow-up
2
+
3
+ > **Status:** known issue, mitigated at the consumer level (showcase). Pending a
4
+ > proper transport-level fix in a follow-up PR.
5
+
6
+ ## Symptom
7
+
8
+ On the first `generateContent` call against `gemini-2.5-flash` (and reportedly
9
+ also `gemini-2.5-pro`), the API occasionally returns a 200 response with **no
10
+ output** when:
11
+
12
+ - `contents` contains only user-role messages — i.e. no prior model turn, AND
13
+ - the request has a non-trivial `tools` array and/or a long `systemInstruction`.
14
+
15
+ The shape of the broken response:
16
+
17
+ ```json
18
+ {
19
+ "candidates": [{
20
+ "content": { "role": "model" },
21
+ "finishReason": "STOP",
22
+ "index": 0
23
+ }],
24
+ "usageMetadata": {
25
+ "promptTokenCount": 2373,
26
+ "totalTokenCount": 2373
27
+ }
28
+ }
29
+ ```
30
+
31
+ Note `totalTokenCount === promptTokenCount` — Gemini emitted **zero** output
32
+ tokens. `finishReason` is `STOP` (normal termination), not a safety block. The
33
+ `candidatesTokenCount` field is missing entirely.
34
+
35
+ In `ChatDriver` this hits the empty-response retry path. Three retries later we
36
+ give up and surface "Remote agent returned no response." The second user turn
37
+ always succeeds because there is now a prior model turn in history (even our
38
+ own "Remote agent returned no response" reply is enough of an anchor).
39
+
40
+ ## Known mitigations
41
+
42
+ Only one workaround has been reported as effective: **inject a prior model turn
43
+ into the request**. Confirmed ineffective in community reports:
44
+
45
+ - Lowering `temperature`.
46
+ - Setting `thinkingConfig: { thinkingBudget: 0 }`.
47
+
48
+ References:
49
+
50
+ - <https://discuss.ai.google.dev/t/finishreason-stop-but-parts-is-missing-inside-candidate/99331>
51
+ - <https://github.com/livekit/agents/issues/4706>
52
+ - <https://github.com/BerriAI/litellm/issues/24442>
53
+
54
+ ## Current mitigation (showcase only)
55
+
56
+ The showcase `genesis-assistant` configures a minimal `primerHistory` on the
57
+ trades agent — a benign `[user: "Hi", assistant: "Hi — how can I help…"]`
58
+ exchange. That single fake model turn is enough to satisfy Gemini.
59
+
60
+ We deliberately keep the primer **innocuous**: any text resembling a system
61
+ update or directive reinforcement (e.g. `[SYSTEM UPDATE]: Your core behavioral
62
+ directives are…`) is caught by Gemini's prompt-injection safety training and
63
+ rejected. Plain greeting language is fine.
64
+
65
+ ## Proposed follow-up: fix in `gemini-transport`
66
+
67
+ The right place to absorb this is `packages/foundation/foundation-ai/src/transports/gemini-transport.ts`
68
+ — specifically `toGeminiContents`. If the assembled `contents` array contains
69
+ **only** user-role entries (no model turn at all) at send time, prepend a
70
+ synthetic anchor pair:
71
+
72
+ ```ts
73
+ contents.unshift(
74
+ { role: 'user', parts: [{ text: 'Hi' }] },
75
+ { role: 'model', parts: [{ text: 'Hi! How can I help today?' }] },
76
+ );
77
+ ```
78
+
79
+ Properties of this approach:
80
+
81
+ - **Transport-scoped** — only applies when sending to Gemini; other transports
82
+ (Claude, OpenAI, etc.) are unaffected.
83
+ - **Transparent to the framework** — `ChatDriver` and the rest of the UI never
84
+ see the synthetic turns. They are not stored in `this.history`, not rendered,
85
+ not surfaced to consumers.
86
+ - **Self-cleaning** — once the conversation has any real model turn, the
87
+ injection no-ops.
88
+
89
+ ### Why we did **not** ship this now
90
+
91
+ A consumer-level fix is more visible and easier to reason about. Hiding the
92
+ workaround inside the transport risks masking related issues — e.g. if the
93
+ shape of system prompts or tool sets ever evolves such that Gemini fails
94
+ elsewhere, an always-on anchor would silently hide the regression. Surfacing
95
+ the workaround as an explicit `primerHistory` on each agent gives consumers a
96
+ chance to notice and remove it when it's no longer needed.
97
+
98
+ The transport-level fix is worth doing **once** the quirk is confirmed against
99
+ Google's official guidance or a forum/issue closeout — at which point we can
100
+ also remove the per-agent primers.
101
+
102
+ ## Acceptance criteria for the follow-up PR
103
+
104
+ - [ ] Add the anchor-injection logic in `gemini-transport.ts:toGeminiContents`.
105
+ - [ ] Unit test: contents containing only user messages get the anchor prepended.
106
+ - [ ] Unit test: contents containing any model message do **not** get the anchor.
107
+ - [ ] Remove the showcase `tradesPrimerHistory` and confirm "Book a trade" works
108
+ on the first turn.
109
+ - [ ] Add an integration note / TODO so the workaround can be ripped out when
110
+ Google ships a fix.
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.432.2",
4
+ "version": "14.433.1",
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.432.2",
68
- "@genesislcap/genx": "14.432.2",
69
- "@genesislcap/rollup-builder": "14.432.2",
70
- "@genesislcap/ts-builder": "14.432.2",
71
- "@genesislcap/uvu-playwright-builder": "14.432.2",
72
- "@genesislcap/vite-builder": "14.432.2",
73
- "@genesislcap/webpack-builder": "14.432.2",
67
+ "@genesislcap/foundation-testing": "14.433.1",
68
+ "@genesislcap/genx": "14.433.1",
69
+ "@genesislcap/rollup-builder": "14.433.1",
70
+ "@genesislcap/ts-builder": "14.433.1",
71
+ "@genesislcap/uvu-playwright-builder": "14.433.1",
72
+ "@genesislcap/vite-builder": "14.433.1",
73
+ "@genesislcap/webpack-builder": "14.433.1",
74
74
  "@types/dompurify": "^3.0.5",
75
75
  "@types/marked": "^5.0.2"
76
76
  },
77
77
  "dependencies": {
78
- "@genesislcap/foundation-ai": "14.432.2",
79
- "@genesislcap/foundation-logger": "14.432.2",
80
- "@genesislcap/foundation-redux": "14.432.2",
81
- "@genesislcap/foundation-ui": "14.432.2",
82
- "@genesislcap/foundation-utils": "14.432.2",
83
- "@genesislcap/rapid-design-system": "14.432.2",
84
- "@genesislcap/web-core": "14.432.2",
78
+ "@genesislcap/foundation-ai": "14.433.1",
79
+ "@genesislcap/foundation-logger": "14.433.1",
80
+ "@genesislcap/foundation-redux": "14.433.1",
81
+ "@genesislcap/foundation-ui": "14.433.1",
82
+ "@genesislcap/foundation-utils": "14.433.1",
83
+ "@genesislcap/rapid-design-system": "14.433.1",
84
+ "@genesislcap/web-core": "14.433.1",
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": "a93cc1972a01671997a744f7ce0477ca5809006d"
96
+ "gitHead": "320ccc8baed03cd24394a913aed3ae72e1d24991"
97
97
  }
@@ -0,0 +1,2 @@
1
+ /** Sentinel value used by the segmented control / select to represent "Auto". */
2
+ export const AGENT_PICKER_AUTO_VALUE = '__auto__';
@@ -0,0 +1,74 @@
1
+ import { css } from '@genesislcap/web-core';
2
+
3
+ export const styles = css`
4
+ :host {
5
+ display: block;
6
+ }
7
+
8
+ :host([hidden]) {
9
+ display: none;
10
+ }
11
+
12
+ .picker {
13
+ position: relative;
14
+ padding: 6px calc(var(--design-unit) * 3px);
15
+ background-color: var(--neutral-layer-2);
16
+ }
17
+
18
+ /* Clipping wrapper. The inner segmented-control is naturally inline-flex
19
+ and auto-sizes to its segments, so it would overflow the panel without
20
+ ever reporting scrollWidth > clientWidth. By giving the wrapper a
21
+ constrained width (flex+min-width:0) and overflow:hidden, the wrapper's
22
+ scrollWidth reflects the segments' natural width while clientWidth is
23
+ the available width — that's what we measure to decide on the fallback. */
24
+ .segmented-row {
25
+ display: flex;
26
+ flex: 1 1 auto;
27
+ min-width: 0;
28
+ overflow: hidden;
29
+ }
30
+
31
+ /* When the fallback select is showing, take the segmented row out of layout
32
+ but keep it in the DOM so the ResizeObserver can re-measure when the
33
+ container grows back and we can swap back to segmented-control. */
34
+ .picker[data-effective='select'] .segmented-row {
35
+ position: absolute;
36
+ inset: 6px calc(var(--design-unit) * 3px);
37
+ visibility: hidden;
38
+ pointer-events: none;
39
+ }
40
+
41
+ .segmented-control {
42
+ flex: 0 0 auto;
43
+ }
44
+
45
+ .select-fallback {
46
+ display: flex;
47
+ align-items: center;
48
+ gap: 6px;
49
+ }
50
+
51
+ .select-label {
52
+ font-family: var(--body-font);
53
+ font-size: 0.8em;
54
+ color: var(--neutral-foreground-hint, var(--neutral-foreground-rest));
55
+ }
56
+
57
+ .select-control {
58
+ flex: 1 1 auto;
59
+ min-width: 0;
60
+ padding: 4px 8px;
61
+ background: var(--neutral-layer-3);
62
+ color: var(--neutral-foreground-rest);
63
+ border: 1px solid var(--neutral-stroke-rest);
64
+ border-radius: 4px;
65
+ font-family: var(--body-font);
66
+ font-size: 0.85em;
67
+ cursor: pointer;
68
+ }
69
+
70
+ .select-control:focus-visible {
71
+ outline: 2px solid var(--accent-fill-rest);
72
+ outline-offset: 1px;
73
+ }
74
+ `;
@@ -0,0 +1,89 @@
1
+ import { html, ref, repeat, ViewTemplate, when } from '@genesislcap/web-core';
2
+ import type { AgentConfig } from '../../config/config';
3
+ import type { AgentPicker } from './agent-picker';
4
+ import { AGENT_PICKER_AUTO_VALUE } from './agent-picker.constants';
5
+
6
+ const AUTO_LABEL = 'Auto';
7
+ const AUTO_HINT = 'Let the orchestrator pick the right agent for each message.';
8
+
9
+ /** @internal */
10
+ export const AgentPickerTemplate = (designSystemPrefix: string): ViewTemplate<AgentPicker> => {
11
+ const segmentedControlTag = `${designSystemPrefix}-segmented-control`;
12
+ const segmentedItemTag = `${designSystemPrefix}-segmented-item`;
13
+
14
+ return html<AgentPicker>`
15
+ <div class="picker" data-effective="${(x) => x.effectiveMode}" part="picker">
16
+ ${when(
17
+ (x) => x.mode === 'segmented-control',
18
+ html<AgentPicker>`
19
+ <div class="segmented-row" ${ref('segmentedRowEl')}>
20
+ <${segmentedControlTag}
21
+ class="segmented-control"
22
+ appearance="primary"
23
+ aria-label="Active agent"
24
+ :value="${(x) => x.currentValue}"
25
+ @change="${(x, c) => {
26
+ const value = (c.event.currentTarget as HTMLElement & { value?: string }).value;
27
+ // The segmented-control re-emits 'change' on its initial value
28
+ // sync; ignore it so we don't close the panel on open.
29
+ if (typeof value !== 'string' || value === x.currentValue) return;
30
+ x.selectByValue(value);
31
+ }}"
32
+ >
33
+ <${segmentedItemTag}
34
+ value="${AGENT_PICKER_AUTO_VALUE}"
35
+ title="${AUTO_HINT}"
36
+ >${AUTO_LABEL}</${segmentedItemTag}>
37
+ ${repeat(
38
+ (x) => x.selectableAgents,
39
+ html<AgentConfig, AgentPicker>`
40
+ <${segmentedItemTag}
41
+ value="${(a) => a.name}"
42
+ title="${(a) => a.manualSelection?.hint ?? ''}"
43
+ >${(a) => a.name}</${segmentedItemTag}>
44
+ `,
45
+ )}
46
+ </${segmentedControlTag}>
47
+ </div>
48
+ `,
49
+ )}
50
+ ${when(
51
+ (x) => x.effectiveMode === 'select',
52
+ html<AgentPicker>`
53
+ <div class="select-fallback">
54
+ <span class="select-label">Agent:</span>
55
+ <select
56
+ class="select-control"
57
+ aria-label="Active agent"
58
+ @change="${(x, c) => {
59
+ const value = (c.event.target as HTMLSelectElement).value;
60
+ x.selectByValue(value);
61
+ }}"
62
+ >
63
+ <option
64
+ value="${AGENT_PICKER_AUTO_VALUE}"
65
+ title="${AUTO_HINT}"
66
+ ?selected="${(x) => x.pinnedAgentName === null}"
67
+ >
68
+ ${AUTO_LABEL}
69
+ </option>
70
+ ${repeat(
71
+ (x) => x.selectableAgents,
72
+ html<AgentConfig, AgentPicker>`
73
+ <option
74
+ value="${(a) => a.name}"
75
+ title="${(a) => a.manualSelection?.hint ?? ''}"
76
+ ?selected="${(a, c) => c.parent.pinnedAgentName === a.name}"
77
+ >
78
+ ${(a) =>
79
+ a.manualSelection?.hint ? `${a.name} — ${a.manualSelection.hint}` : a.name}
80
+ </option>
81
+ `,
82
+ )}
83
+ </select>
84
+ </div>
85
+ `,
86
+ )}
87
+ </div>
88
+ `;
89
+ };