@genesislcap/ai-assistant 14.432.2 → 14.433.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/api-extractor.json +8 -1
- package/dist/ai-assistant.api.json +1216 -141
- package/dist/ai-assistant.d.ts +216 -15
- package/dist/dts/components/agent-picker/agent-picker.d.ts +69 -0
- package/dist/dts/components/agent-picker/agent-picker.d.ts.map +1 -0
- package/dist/dts/components/agent-picker/agent-picker.styles.d.ts +2 -0
- package/dist/dts/components/agent-picker/agent-picker.styles.d.ts.map +1 -0
- package/dist/dts/components/agent-picker/agent-picker.template.d.ts +5 -0
- package/dist/dts/components/agent-picker/agent-picker.template.d.ts.map +1 -0
- package/dist/dts/components/agent-picker/index.d.ts +2 -0
- package/dist/dts/components/agent-picker/index.d.ts.map +1 -0
- package/dist/dts/components/chat-driver/chat-driver.d.ts +21 -0
- package/dist/dts/components/chat-driver/chat-driver.d.ts.map +1 -1
- package/dist/dts/components/orchestrating-driver/orchestrating-driver.d.ts +14 -0
- package/dist/dts/components/orchestrating-driver/orchestrating-driver.d.ts.map +1 -1
- package/dist/dts/config/config.d.ts +22 -12
- package/dist/dts/config/config.d.ts.map +1 -1
- package/dist/dts/index.d.ts +1 -0
- package/dist/dts/index.d.ts.map +1 -1
- package/dist/dts/main/main.d.ts +72 -4
- package/dist/dts/main/main.d.ts.map +1 -1
- package/dist/dts/main/main.styles.d.ts.map +1 -1
- package/dist/dts/main/main.template.d.ts.map +1 -1
- package/dist/dts/main/main.types.d.ts +1 -0
- package/dist/dts/main/main.types.d.ts.map +1 -1
- package/dist/dts/state/ai-assistant-slice.d.ts +39 -1
- package/dist/dts/state/ai-assistant-slice.d.ts.map +1 -1
- package/dist/dts/state/session-store.d.ts +6 -0
- package/dist/dts/state/session-store.d.ts.map +1 -1
- package/dist/dts/utils/animated-panel-toggle.d.ts +26 -0
- package/dist/dts/utils/animated-panel-toggle.d.ts.map +1 -0
- package/dist/dts/utils/index.d.ts +1 -0
- package/dist/dts/utils/index.d.ts.map +1 -1
- package/dist/esm/components/agent-picker/agent-picker.js +157 -0
- package/dist/esm/components/agent-picker/agent-picker.styles.js +73 -0
- package/dist/esm/components/agent-picker/agent-picker.template.js +72 -0
- package/dist/esm/components/agent-picker/index.js +1 -0
- package/dist/esm/components/chat-driver/chat-driver.js +48 -6
- package/dist/esm/components/orchestrating-driver/orchestrating-driver.js +43 -6
- package/dist/esm/index.js +1 -0
- package/dist/esm/main/main.js +215 -21
- package/dist/esm/main/main.styles.js +59 -0
- package/dist/esm/main/main.template.js +66 -12
- package/dist/esm/state/ai-assistant-slice.js +15 -0
- package/dist/esm/utils/animated-panel-toggle.js +62 -0
- package/dist/esm/utils/index.js +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/docs/gemini-empty-response.md +110 -0
- package/package.json +16 -16
- package/src/components/agent-picker/agent-picker.styles.ts +74 -0
- package/src/components/agent-picker/agent-picker.template.ts +88 -0
- package/src/components/agent-picker/agent-picker.ts +148 -0
- package/src/components/agent-picker/index.ts +1 -0
- package/src/components/chat-driver/chat-driver.ts +65 -8
- package/src/components/orchestrating-driver/orchestrating-driver.ts +45 -6
- package/src/config/config.ts +28 -11
- package/src/index.ts +1 -0
- package/src/main/main.styles.ts +59 -0
- package/src/main/main.template.ts +79 -13
- package/src/main/main.ts +220 -19
- package/src/main/main.types.ts +2 -0
- package/src/state/ai-assistant-slice.ts +51 -1
- package/src/utils/animated-panel-toggle.ts +62 -0
- package/src/utils/index.ts +1 -0
|
@@ -164,15 +164,39 @@ export class ChatDriver extends EventTarget {
|
|
|
164
164
|
isBusy() {
|
|
165
165
|
return this.busy;
|
|
166
166
|
}
|
|
167
|
+
/**
|
|
168
|
+
* Wire a parent driver as the host for this driver's interactions. When set,
|
|
169
|
+
* `requestInteraction` delegates upward so the widget renders in (and
|
|
170
|
+
* resolves through) the parent's history and pending map. Calls chain
|
|
171
|
+
* naturally: a grandchild → child → root.
|
|
172
|
+
*/
|
|
173
|
+
setHostInteractionRequester(fn) {
|
|
174
|
+
this.hostInteractionRequester = fn;
|
|
175
|
+
}
|
|
167
176
|
/**
|
|
168
177
|
* Request a custom UI interaction. Emits a new message with the interaction.
|
|
169
178
|
* Tool handlers can call this to pause execution until the user completes the UI interaction.
|
|
170
179
|
*
|
|
180
|
+
* If a host requester is wired (sub-agent case), the call delegates upward
|
|
181
|
+
* so the interaction lives on the parent — the main UI is only listening to
|
|
182
|
+
* the root driver. Only one interaction may be in flight at any time on a
|
|
183
|
+
* given root: concurrent calls (e.g. two parallel sub-agents both spawning a
|
|
184
|
+
* widget) throw. Parallel sub-agents are for parallel work, not for user
|
|
185
|
+
* interaction, which is inherently sequential.
|
|
186
|
+
*
|
|
171
187
|
* @param componentName - The custom element name to render.
|
|
172
188
|
* @param data - Data to pass to the component.
|
|
173
189
|
*/
|
|
174
190
|
requestInteraction(componentName, data) {
|
|
175
191
|
return __awaiter(this, void 0, void 0, function* () {
|
|
192
|
+
if (this.hostInteractionRequester) {
|
|
193
|
+
return this.hostInteractionRequester(componentName, data);
|
|
194
|
+
}
|
|
195
|
+
if (this.pendingInteractions.size > 0) {
|
|
196
|
+
throw new Error('requestInteraction: another user interaction is already in flight. ' +
|
|
197
|
+
'Only one interaction may be active at a time — sequence them in a single tool handler ' +
|
|
198
|
+
'rather than spawning widgets from parallel sub-agents or parallel tool calls.');
|
|
199
|
+
}
|
|
176
200
|
const interactionId = crypto.randomUUID();
|
|
177
201
|
return new Promise((resolve, reject) => {
|
|
178
202
|
this.pendingInteractions.set(interactionId, { resolve, reject });
|
|
@@ -297,19 +321,29 @@ export class ChatDriver extends EventTarget {
|
|
|
297
321
|
];
|
|
298
322
|
const child = new ChatDriver(this.aiProvider);
|
|
299
323
|
child.applyAgent(Object.assign(Object.assign({}, subConfig), { primerHistory: effectivePrimer }));
|
|
324
|
+
// Route interactions back through this driver so widgets render in the
|
|
325
|
+
// parent's (ultimately the root's) history and resolve via the same
|
|
326
|
+
// pending map the main UI is wired to. Recurses naturally for nested
|
|
327
|
+
// sub-agents.
|
|
328
|
+
child.setHostInteractionRequester((componentName, data) => this.requestInteraction(componentName, data));
|
|
300
329
|
const forwardTrace = (e) => {
|
|
301
330
|
this.dispatchEvent(new CustomEvent('sub-agent-history-updated', {
|
|
302
331
|
detail: { agentName: subConfig.name, history: e.detail },
|
|
303
332
|
}));
|
|
304
333
|
};
|
|
305
334
|
child.addEventListener('history-updated', forwardTrace);
|
|
306
|
-
|
|
335
|
+
// Unique per-invocation id so listeners can pair start/stop reliably even
|
|
336
|
+
// when the same sub-agent runs multiple times in parallel.
|
|
337
|
+
const invocationId = crypto.randomUUID();
|
|
338
|
+
const chatInputDuringExecution = options === null || options === void 0 ? void 0 : options.chatInputDuringExecution;
|
|
339
|
+
const lifecycleDetail = { name, invocationId, chatInputDuringExecution };
|
|
340
|
+
this.dispatchEvent(new CustomEvent('sub-agent-start', { detail: lifecycleDetail }));
|
|
307
341
|
try {
|
|
308
342
|
yield child.sendMessage(task !== null && task !== void 0 ? task : '');
|
|
309
343
|
}
|
|
310
344
|
finally {
|
|
311
345
|
child.removeEventListener('history-updated', forwardTrace);
|
|
312
|
-
this.dispatchEvent(new CustomEvent('sub-agent-stop', { detail:
|
|
346
|
+
this.dispatchEvent(new CustomEvent('sub-agent-stop', { detail: lifecycleDetail }));
|
|
313
347
|
}
|
|
314
348
|
const trace = child.getHistory();
|
|
315
349
|
const completion = child.getSubAgentCompletion();
|
|
@@ -514,16 +548,26 @@ export class ChatDriver extends EventTarget {
|
|
|
514
548
|
: emptyResponseAttempts > 0
|
|
515
549
|
? `${baseSystemPrompt !== null && baseSystemPrompt !== void 0 ? baseSystemPrompt : ''}\n\nIMPORTANT: You must respond to the user's message. Call the appropriate tool or provide a text response — do not return an empty response.`
|
|
516
550
|
: baseSystemPrompt;
|
|
551
|
+
// Capture the pending user input, then clear the slots BEFORE the chat
|
|
552
|
+
// call. `sendMessage` already appended the user message to `this.history`,
|
|
553
|
+
// so on retries (empty / malformed) we must rely on history alone —
|
|
554
|
+
// otherwise the message gets sent twice (once via history, once via
|
|
555
|
+
// `currentInput`), which Gemini answers with an empty response and then
|
|
556
|
+
// we retry forever.
|
|
557
|
+
const userInputForCall = currentInput;
|
|
558
|
+
const attachmentsForCall = currentAttachments;
|
|
559
|
+
currentInput = '';
|
|
560
|
+
currentAttachments = undefined;
|
|
517
561
|
const options = {
|
|
518
562
|
systemPrompt,
|
|
519
563
|
// Strip fold-only properties (foldEvent, foldPath) before sending to provider
|
|
520
564
|
tools: this.toolDefinitions.length ? this.toolDefinitions : undefined,
|
|
521
|
-
attachments:
|
|
565
|
+
attachments: attachmentsForCall,
|
|
522
566
|
};
|
|
523
567
|
let response;
|
|
524
568
|
try {
|
|
525
569
|
// eslint-disable-next-line no-await-in-loop
|
|
526
|
-
response = yield this.aiProvider.chat(historyForCall,
|
|
570
|
+
response = yield this.aiProvider.chat(historyForCall, userInputForCall, options);
|
|
527
571
|
}
|
|
528
572
|
catch (e) {
|
|
529
573
|
if (e instanceof MalformedFunctionCallError) {
|
|
@@ -542,7 +586,6 @@ export class ChatDriver extends EventTarget {
|
|
|
542
586
|
}
|
|
543
587
|
throw e;
|
|
544
588
|
}
|
|
545
|
-
currentAttachments = undefined;
|
|
546
589
|
const isThinkingStep = response.content && ((_b = response.toolCalls) === null || _b === void 0 ? void 0 : _b.length);
|
|
547
590
|
const isEmptyResponse = !((_c = response.content) === null || _c === void 0 ? void 0 : _c.trim()) && !((_d = response.toolCalls) === null || _d === void 0 ? void 0 : _d.length);
|
|
548
591
|
if (isEmptyResponse) {
|
|
@@ -739,7 +782,6 @@ export class ChatDriver extends EventTarget {
|
|
|
739
782
|
if (this.subAgentCompletion) {
|
|
740
783
|
return { reason: 'done' };
|
|
741
784
|
}
|
|
742
|
-
currentInput = '';
|
|
743
785
|
}
|
|
744
786
|
if (iterations >= this.maxToolIterations) {
|
|
745
787
|
logger.warn('ChatDriver: reached max tool iterations, stopping');
|
|
@@ -49,6 +49,7 @@ export class OrchestratingDriver extends EventTarget {
|
|
|
49
49
|
super();
|
|
50
50
|
this.aiProvider = aiProvider;
|
|
51
51
|
this.agents = agents;
|
|
52
|
+
this.pinnedAgentName = null;
|
|
52
53
|
this.maxHandoffs = (_a = options.maxHandoffs) !== null && _a !== void 0 ? _a : DEFAULT_MAX_HANDOFFS;
|
|
53
54
|
this.classifierHistoryLength =
|
|
54
55
|
(_b = options.classifierHistoryLength) !== null && _b !== void 0 ? _b : DEFAULT_CLASSIFIER_HISTORY_LENGTH;
|
|
@@ -69,6 +70,9 @@ export class OrchestratingDriver extends EventTarget {
|
|
|
69
70
|
this.chatDriver.addEventListener('sub-agent-history-updated', (e) => {
|
|
70
71
|
this.dispatchEvent(new CustomEvent('sub-agent-history-updated', { detail: e.detail }));
|
|
71
72
|
});
|
|
73
|
+
this.chatDriver.addEventListener('sub-agent-start', (e) => {
|
|
74
|
+
this.dispatchEvent(new CustomEvent('sub-agent-start', { detail: e.detail }));
|
|
75
|
+
});
|
|
72
76
|
this.chatDriver.addEventListener('sub-agent-stop', (e) => {
|
|
73
77
|
this.dispatchEvent(new CustomEvent('sub-agent-stop', { detail: e.detail }));
|
|
74
78
|
});
|
|
@@ -79,6 +83,15 @@ export class OrchestratingDriver extends EventTarget {
|
|
|
79
83
|
isBusy() {
|
|
80
84
|
return this.chatDriver.isBusy();
|
|
81
85
|
}
|
|
86
|
+
/**
|
|
87
|
+
* Pins routing to a specific agent by name. While pinned, the classifier is
|
|
88
|
+
* skipped and the continuation tool is hidden from the agent's tool list, so
|
|
89
|
+
* the agent cannot quietly hand back to the orchestrator. Pass `null` to
|
|
90
|
+
* return to automatic routing.
|
|
91
|
+
*/
|
|
92
|
+
setPinnedAgent(name) {
|
|
93
|
+
this.pinnedAgentName = name;
|
|
94
|
+
}
|
|
82
95
|
loadHistory(messages) {
|
|
83
96
|
this.chatDriver.loadHistory(messages);
|
|
84
97
|
}
|
|
@@ -87,7 +100,11 @@ export class OrchestratingDriver extends EventTarget {
|
|
|
87
100
|
}
|
|
88
101
|
getSuggestions(history, prompt, count, allAgentInfo) {
|
|
89
102
|
return __awaiter(this, void 0, void 0, function* () {
|
|
90
|
-
|
|
103
|
+
// When pinned to a specialist, scope suggestions to that agent so the
|
|
104
|
+
// proposed prompts reflect the active routing instead of the full panel.
|
|
105
|
+
const pinned = this.resolvePinnedAgent();
|
|
106
|
+
const candidates = pinned && isSpecialist(pinned) ? [pinned] : this.specialists;
|
|
107
|
+
const agentInfo = candidates.map((s) => {
|
|
91
108
|
var _a;
|
|
92
109
|
return ({
|
|
93
110
|
name: s.name,
|
|
@@ -106,8 +123,10 @@ export class OrchestratingDriver extends EventTarget {
|
|
|
106
123
|
this.dispatchEvent(new CustomEvent('history-updated', {
|
|
107
124
|
detail: [...history, { role: 'user', content: input, attachments }],
|
|
108
125
|
}));
|
|
109
|
-
this.
|
|
110
|
-
|
|
126
|
+
const pinned = this.resolvePinnedAgent();
|
|
127
|
+
if (!pinned)
|
|
128
|
+
this.dispatchEvent(new CustomEvent('orchestrating-start'));
|
|
129
|
+
let currentAgent = pinned !== null && pinned !== void 0 ? pinned : (yield this.classify(input, history, this.activeAgent));
|
|
111
130
|
let isHandoff = false;
|
|
112
131
|
let handoffs = 0;
|
|
113
132
|
let handoffSummary = '';
|
|
@@ -126,7 +145,9 @@ export class OrchestratingDriver extends EventTarget {
|
|
|
126
145
|
// eslint-disable-next-line no-await-in-loop
|
|
127
146
|
result = yield this.chatDriver.sendMessage(input, attachments);
|
|
128
147
|
}
|
|
129
|
-
|
|
148
|
+
// Pinned agents never hand off — the continuation tool is filtered out in
|
|
149
|
+
// applyAgent, but this guards against a model hallucinating a handoff result.
|
|
150
|
+
if (result.reason !== 'agent-handoff' || isFallback(currentAgent) || pinned) {
|
|
130
151
|
break;
|
|
131
152
|
}
|
|
132
153
|
handoffs += 1;
|
|
@@ -152,8 +173,9 @@ export class OrchestratingDriver extends EventTarget {
|
|
|
152
173
|
}
|
|
153
174
|
applyAgent(agent) {
|
|
154
175
|
var _a;
|
|
155
|
-
// Fallback agents are terminal
|
|
156
|
-
const
|
|
176
|
+
// Fallback and pinned agents are terminal — neither should hand off.
|
|
177
|
+
const isTerminal = isFallback(agent) || this.pinnedAgentName !== null;
|
|
178
|
+
const agentToApply = isTerminal
|
|
157
179
|
? agent
|
|
158
180
|
: Object.assign(Object.assign({}, agent), { toolDefinitions: [...((_a = agent.toolDefinitions) !== null && _a !== void 0 ? _a : []), REQUEST_CONTINUATION_DEFINITION] });
|
|
159
181
|
const previousAgent = this.activeAgent;
|
|
@@ -230,6 +252,21 @@ export class OrchestratingDriver extends EventTarget {
|
|
|
230
252
|
return this.specialists[0];
|
|
231
253
|
});
|
|
232
254
|
}
|
|
255
|
+
/**
|
|
256
|
+
* Returns the pinned agent if `pinnedAgentName` matches a known specialist or
|
|
257
|
+
* fallback. Logs and returns `undefined` if pinned to a name that no longer
|
|
258
|
+
* exists in the agents array — caller falls back to the classifier.
|
|
259
|
+
*/
|
|
260
|
+
resolvePinnedAgent() {
|
|
261
|
+
if (this.pinnedAgentName === null)
|
|
262
|
+
return undefined;
|
|
263
|
+
const match = this.agents.find((a) => a.name === this.pinnedAgentName);
|
|
264
|
+
if (!match) {
|
|
265
|
+
logger.warn(`OrchestratingDriver: pinned agent "${this.pinnedAgentName}" not found in agents array — falling back to classifier.`);
|
|
266
|
+
return undefined;
|
|
267
|
+
}
|
|
268
|
+
return match;
|
|
269
|
+
}
|
|
233
270
|
appendInlineMessage(content) {
|
|
234
271
|
const history = this.chatDriver.getHistory();
|
|
235
272
|
this.chatDriver.loadHistory([...history, { role: 'assistant', content }]);
|
package/dist/esm/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export * from './main/main';
|
|
2
2
|
export * from './main/main.types';
|
|
3
3
|
export * from './main/main.template';
|
|
4
|
+
export * from './components/agent-picker';
|
|
4
5
|
export * from './components/ai-driver';
|
|
5
6
|
export * from './components/chat-driver';
|
|
6
7
|
export * from './components/orchestrating-driver';
|
package/dist/esm/main/main.js
CHANGED
|
@@ -27,6 +27,7 @@ import { avoidTreeShaking } from '@genesislcap/foundation-utils';
|
|
|
27
27
|
import { customElement, html, GenesisElement, observable, volatile, attr, } from '@genesislcap/web-core';
|
|
28
28
|
import { agenticActivityBus } from '../channel/ai-activity-bus';
|
|
29
29
|
import { AiActivityHalo } from '../components/activity-halo/activity-halo';
|
|
30
|
+
import { AgentPicker } from '../components/agent-picker/agent-picker';
|
|
30
31
|
import { AiChatBubble } from '../components/chat-bubble/chat-bubble';
|
|
31
32
|
import { ChatDriver } from '../components/chat-driver/chat-driver';
|
|
32
33
|
import { AiChatInteractionWrapper } from '../components/chat-interaction-wrapper/chat-interaction-wrapper';
|
|
@@ -35,13 +36,30 @@ import { AiHaloOverlay } from '../components/halo-overlay';
|
|
|
35
36
|
import { OrchestratingDriver } from '../components/orchestrating-driver/orchestrating-driver';
|
|
36
37
|
import { getOrCreateDriver, deleteDriver } from '../state/driver-registry';
|
|
37
38
|
import { getSessionStore, hasSessionStore } from '../state/session-store';
|
|
39
|
+
import { AI_COLOUR_AMBER, AI_COLOUR_CYAN, AI_COLOUR_PINK, AI_COLOUR_VIOLET, } from '../styles/ai-colours';
|
|
38
40
|
import { ChatSuggestions } from '../suggestions/chat-suggestions';
|
|
41
|
+
import { AnimatedPanelToggle } from '../utils/animated-panel-toggle';
|
|
39
42
|
import { logger } from '../utils/logger';
|
|
40
43
|
import { expandToolTree } from '../utils/tool-fold';
|
|
41
44
|
import { styles } from './main.styles';
|
|
42
45
|
import { FoundationAiAssistantTemplate } from './main.template';
|
|
43
46
|
import { ALL_ANIMATIONS } from './main.types';
|
|
44
47
|
/** Context window sizes (in tokens) for known models. */
|
|
48
|
+
/**
|
|
49
|
+
* Pin tint palette, cycled by agent position. Matches the four brand colours
|
|
50
|
+
* used by the halo overlay and loading dots.
|
|
51
|
+
*/
|
|
52
|
+
const PIN_COLOURS = [AI_COLOUR_AMBER, AI_COLOUR_PINK, AI_COLOUR_CYAN, AI_COLOUR_VIOLET];
|
|
53
|
+
/**
|
|
54
|
+
* Duration of the agent-picker slide-out animation. Must stay in sync with the
|
|
55
|
+
* `agent-picker-slide-out` keyframe duration in `main.styles.ts`.
|
|
56
|
+
*/
|
|
57
|
+
const AGENT_PICKER_CLOSE_MS = 200;
|
|
58
|
+
/**
|
|
59
|
+
* Duration of the settings panel slide-out animation. Must stay in sync with
|
|
60
|
+
* the `settings-slide-out` keyframe duration in `main.styles.ts`.
|
|
61
|
+
*/
|
|
62
|
+
const SETTINGS_CLOSE_MS = 200;
|
|
45
63
|
const MODEL_CONTEXT_LIMITS = {
|
|
46
64
|
'gemini-2.5-flash': 1048576,
|
|
47
65
|
'gemini-2.5-flash-lite': 1048576,
|
|
@@ -50,7 +68,7 @@ const MODEL_CONTEXT_LIMITS = {
|
|
|
50
68
|
'gpt-4-turbo': 128000,
|
|
51
69
|
};
|
|
52
70
|
// Register supporting components when the main component module is imported.
|
|
53
|
-
avoidTreeShaking(AiChatMarkdown, AiChatInteractionWrapper, AiHaloOverlay, AiChatBubble, AiActivityHalo, ChatSuggestions);
|
|
71
|
+
avoidTreeShaking(AiChatMarkdown, AiChatInteractionWrapper, AiHaloOverlay, AiChatBubble, AiActivityHalo, ChatSuggestions, AgentPicker);
|
|
54
72
|
/** Recursively strips `toolHandlers` from an agent and all its sub-agents. */
|
|
55
73
|
function stripHandlers(agent) {
|
|
56
74
|
const { toolHandlers: _, subAgents } = agent, rest = __rest(agent, ["toolHandlers", "subAgents"]);
|
|
@@ -107,6 +125,20 @@ let FoundationAiAssistant = FoundationAiAssistant_1 = class FoundationAiAssistan
|
|
|
107
125
|
/** True when the user has intentionally scrolled away from the bottom — suppresses auto-scroll. */
|
|
108
126
|
this._userScrolledAway = false;
|
|
109
127
|
this.showHalo = 'no';
|
|
128
|
+
this._settingsToggle = new AnimatedPanelToggle(this, '.settings-panel', SETTINGS_CLOSE_MS, () => this.settingsOpen, (v) => {
|
|
129
|
+
this.settingsOpen = v;
|
|
130
|
+
});
|
|
131
|
+
this._agentPickerToggle = new AnimatedPanelToggle(this, '.agent-picker-panel', AGENT_PICKER_CLOSE_MS, () => this.agentPickerOpen, (v) => {
|
|
132
|
+
this.agentPickerOpen = v;
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Resolved agent picker mode from `chatConfig.picker.mode`. Defaults to
|
|
137
|
+
* `'disabled'`.
|
|
138
|
+
*/
|
|
139
|
+
get agentPicker() {
|
|
140
|
+
var _a, _b;
|
|
141
|
+
return (_b = (_a = this.chatConfig.picker) === null || _a === void 0 ? void 0 : _a.mode) !== null && _b !== void 0 ? _b : 'disabled';
|
|
110
142
|
}
|
|
111
143
|
get messages() {
|
|
112
144
|
var _a, _b;
|
|
@@ -208,6 +240,41 @@ let FoundationAiAssistant = FoundationAiAssistant_1 = class FoundationAiAssistan
|
|
|
208
240
|
var _a;
|
|
209
241
|
(_a = this._sessionRef) === null || _a === void 0 ? void 0 : _a.actions.aiAssistant.setEnabledAnimations(value);
|
|
210
242
|
}
|
|
243
|
+
/**
|
|
244
|
+
* Whether the agent picker slide-out panel is open. Lives on the session
|
|
245
|
+
* store so the bubble-dialog and popout-panel instances stay in sync.
|
|
246
|
+
*/
|
|
247
|
+
get agentPickerOpen() {
|
|
248
|
+
var _a, _b;
|
|
249
|
+
return (_b = (_a = this._sessionRef) === null || _a === void 0 ? void 0 : _a.store.aiAssistant.agentPickerOpen) !== null && _b !== void 0 ? _b : false;
|
|
250
|
+
}
|
|
251
|
+
set agentPickerOpen(value) {
|
|
252
|
+
var _a;
|
|
253
|
+
(_a = this._sessionRef) === null || _a === void 0 ? void 0 : _a.actions.aiAssistant.setAgentPickerOpen(value);
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Name of the agent the user has pinned via the agent picker. `null` means
|
|
257
|
+
* automatic routing (Auto). Persisted on the session store, so it survives
|
|
258
|
+
* pop-in/pop-out but resets on page refresh.
|
|
259
|
+
*/
|
|
260
|
+
get pinnedAgentName() {
|
|
261
|
+
var _a, _b;
|
|
262
|
+
return (_b = (_a = this._sessionRef) === null || _a === void 0 ? void 0 : _a.store.aiAssistant.pinnedAgentName) !== null && _b !== void 0 ? _b : null;
|
|
263
|
+
}
|
|
264
|
+
set pinnedAgentName(value) {
|
|
265
|
+
var _a, _b, _c;
|
|
266
|
+
const previous = (_b = (_a = this._sessionRef) === null || _a === void 0 ? void 0 : _a.store.aiAssistant.pinnedAgentName) !== null && _b !== void 0 ? _b : null;
|
|
267
|
+
(_c = this._sessionRef) === null || _c === void 0 ? void 0 : _c.actions.aiAssistant.setPinnedAgentName(value);
|
|
268
|
+
if (this.driver instanceof OrchestratingDriver) {
|
|
269
|
+
this.driver.setPinnedAgent(value);
|
|
270
|
+
}
|
|
271
|
+
// Suggestions are scoped to the active routing — resetting forces a fresh
|
|
272
|
+
// fetch when the user pins/unpins so the prompts match the new agent.
|
|
273
|
+
if (previous !== value) {
|
|
274
|
+
this.suggestionsState = { status: 'idle' };
|
|
275
|
+
this.fetchSuggestions();
|
|
276
|
+
}
|
|
277
|
+
}
|
|
211
278
|
get liveSubAgentTrace() {
|
|
212
279
|
var _a, _b;
|
|
213
280
|
return (_b = (_a = this._sessionRef) === null || _a === void 0 ? void 0 : _a.store.aiAssistant.liveSubAgentTrace) !== null && _b !== void 0 ? _b : [];
|
|
@@ -224,6 +291,29 @@ let FoundationAiAssistant = FoundationAiAssistant_1 = class FoundationAiAssistan
|
|
|
224
291
|
var _a;
|
|
225
292
|
(_a = this._sessionRef) === null || _a === void 0 ? void 0 : _a.actions.aiAssistant.setLiveSubAgentName(value);
|
|
226
293
|
}
|
|
294
|
+
/**
|
|
295
|
+
* In-flight per-call chat-input overrides pushed by `requestSubAgent`
|
|
296
|
+
* invocations. Empty means no override is active.
|
|
297
|
+
*/
|
|
298
|
+
get subAgentInputOverrides() {
|
|
299
|
+
var _a, _b;
|
|
300
|
+
return (_b = (_a = this._sessionRef) === null || _a === void 0 ? void 0 : _a.store.aiAssistant.subAgentInputOverrides) !== null && _b !== void 0 ? _b : [];
|
|
301
|
+
}
|
|
302
|
+
/**
|
|
303
|
+
* Resolves the effective chat-input behaviour while the assistant is
|
|
304
|
+
* executing. Sub-agent overrides take precedence over the agent-level
|
|
305
|
+
* config; among overrides the most restrictive wins (`'hidden'` >
|
|
306
|
+
* `'disabled'`). Falls back to the active agent's config, then `'disabled'`.
|
|
307
|
+
*/
|
|
308
|
+
get effectiveChatInputDuringExecution() {
|
|
309
|
+
var _a, _b;
|
|
310
|
+
const overrides = this.subAgentInputOverrides;
|
|
311
|
+
if (overrides.some((o) => o.mode === 'hidden'))
|
|
312
|
+
return 'hidden';
|
|
313
|
+
if (overrides.some((o) => o.mode === 'disabled'))
|
|
314
|
+
return 'disabled';
|
|
315
|
+
return (_b = (_a = this.activeAgent) === null || _a === void 0 ? void 0 : _a.chatInputDuringExecution) !== null && _b !== void 0 ? _b : 'disabled';
|
|
316
|
+
}
|
|
227
317
|
/** Most recent prompt token count from the AI provider, if available. */
|
|
228
318
|
get contextTokens() {
|
|
229
319
|
var _a;
|
|
@@ -418,18 +508,38 @@ let FoundationAiAssistant = FoundationAiAssistant_1 = class FoundationAiAssistan
|
|
|
418
508
|
// driver's own history array (which is still being mutated by the tool loop).
|
|
419
509
|
this.liveSubAgentTrace = structuredClone(history);
|
|
420
510
|
};
|
|
421
|
-
const
|
|
511
|
+
const onSubAgentStart = (e) => {
|
|
512
|
+
var _a;
|
|
513
|
+
const { invocationId, chatInputDuringExecution } = e.detail;
|
|
514
|
+
if (!chatInputDuringExecution)
|
|
515
|
+
return;
|
|
516
|
+
(_a = this._sessionRef) === null || _a === void 0 ? void 0 : _a.actions.aiAssistant.addSubAgentInputOverride({
|
|
517
|
+
id: invocationId,
|
|
518
|
+
mode: chatInputDuringExecution,
|
|
519
|
+
});
|
|
520
|
+
};
|
|
521
|
+
const onSubAgentStop = (e) => {
|
|
522
|
+
var _a;
|
|
422
523
|
this.liveSubAgentTrace = [];
|
|
423
524
|
this.liveSubAgentName = null;
|
|
525
|
+
const { invocationId } = e.detail;
|
|
526
|
+
if (invocationId) {
|
|
527
|
+
(_a = this._sessionRef) === null || _a === void 0 ? void 0 : _a.actions.aiAssistant.removeSubAgentInputOverride({ id: invocationId });
|
|
528
|
+
}
|
|
424
529
|
};
|
|
425
530
|
driver.addEventListener('sub-agent-history-updated', onSubAgentHistoryUpdated);
|
|
531
|
+
driver.addEventListener('sub-agent-start', onSubAgentStart);
|
|
426
532
|
driver.addEventListener('sub-agent-stop', onSubAgentStop);
|
|
427
533
|
const cleanups = [
|
|
428
534
|
() => driver.removeEventListener('history-updated', onHistoryUpdated),
|
|
429
535
|
() => driver.removeEventListener('sub-agent-history-updated', onSubAgentHistoryUpdated),
|
|
536
|
+
() => driver.removeEventListener('sub-agent-start', onSubAgentStart),
|
|
430
537
|
() => driver.removeEventListener('sub-agent-stop', onSubAgentStop),
|
|
431
538
|
];
|
|
432
539
|
if (driver instanceof OrchestratingDriver) {
|
|
540
|
+
// Restore any pinned agent from the session store onto the freshly built
|
|
541
|
+
// driver, so pop-in/pop-out and agents-array reassignment preserve the pin.
|
|
542
|
+
driver.setPinnedAgent(this.pinnedAgentName);
|
|
433
543
|
const onOrchStart = () => {
|
|
434
544
|
this.showHalo = 'orchestrating';
|
|
435
545
|
};
|
|
@@ -460,7 +570,7 @@ let FoundationAiAssistant = FoundationAiAssistant_1 = class FoundationAiAssistan
|
|
|
460
570
|
this.driverCleanup = undefined;
|
|
461
571
|
}
|
|
462
572
|
connectedCallback() {
|
|
463
|
-
var _a, _b, _c, _d, _e;
|
|
573
|
+
var _a, _b, _c, _d, _e, _f, _g;
|
|
464
574
|
// Initialise the store reference BEFORE super.connectedCallback() so that
|
|
465
575
|
// the first FAST render has access to the store. The store Proxy calls
|
|
466
576
|
// Observable.track(observableStore, sliceName) whenever a slice is read,
|
|
@@ -479,6 +589,10 @@ let FoundationAiAssistant = FoundationAiAssistant_1 = class FoundationAiAssistan
|
|
|
479
589
|
this.showAgentSwitchIndicator = ui.showAgentSwitchIndicator === true;
|
|
480
590
|
this.enabledAnimations =
|
|
481
591
|
(_c = (_b = ui.animations) === null || _b === void 0 ? void 0 : _b.enabled) !== null && _c !== void 0 ? _c : (ui.animations ? [...ALL_ANIMATIONS] : []);
|
|
592
|
+
const defaultAgent = (_d = this.chatConfig.picker) === null || _d === void 0 ? void 0 : _d.defaultAgent;
|
|
593
|
+
if (defaultAgent && ((_e = this.agents) !== null && _e !== void 0 ? _e : []).some((a) => a.name === defaultAgent)) {
|
|
594
|
+
this.pinnedAgentName = defaultAgent;
|
|
595
|
+
}
|
|
482
596
|
}
|
|
483
597
|
this.driver = getOrCreateDriver(key, () => this.createDriver());
|
|
484
598
|
this._driverAgentsKey = this.getAgentsKey(this.agents);
|
|
@@ -501,13 +615,13 @@ let FoundationAiAssistant = FoundationAiAssistant_1 = class FoundationAiAssistan
|
|
|
501
615
|
this.syncShowingSplash();
|
|
502
616
|
// Restore loading state if the driver is still executing mid-lifecycle.
|
|
503
617
|
// disconnectedCallback resets state to 'idle', so we must check real driver state here.
|
|
504
|
-
if ((
|
|
618
|
+
if ((_f = this.driver) === null || _f === void 0 ? void 0 : _f.isBusy()) {
|
|
505
619
|
this.state = 'loading';
|
|
506
620
|
this.startLoadingTimer();
|
|
507
621
|
// Subscribe once so that when the originating send() completes (possibly on a
|
|
508
622
|
// different element instance that has since disconnected), this element cleans up
|
|
509
623
|
// its own timer, syncs the halo, and triggers the post-response suggestion fetch.
|
|
510
|
-
this._executionCompletionUnsub = (
|
|
624
|
+
this._executionCompletionUnsub = (_g = this._sessionRef) === null || _g === void 0 ? void 0 : _g.subscribeKey((s) => s.aiAssistant.state, () => {
|
|
511
625
|
var _a, _b;
|
|
512
626
|
if (((_a = this._sessionRef) === null || _a === void 0 ? void 0 : _a.store.aiAssistant.state) === 'idle') {
|
|
513
627
|
this.stopLoadingTimer();
|
|
@@ -556,6 +670,11 @@ let FoundationAiAssistant = FoundationAiAssistant_1 = class FoundationAiAssistan
|
|
|
556
670
|
this._userScrolledAway = false;
|
|
557
671
|
document.removeEventListener('pointerdown', this._handleGlobalInteraction, true);
|
|
558
672
|
document.removeEventListener('focusin', this._handleGlobalInteraction, true);
|
|
673
|
+
// Finalise any in-flight panel-close animations before clearing
|
|
674
|
+
// _sessionRef, otherwise the closed state never makes it to the store and
|
|
675
|
+
// the panel re-mounts open.
|
|
676
|
+
this._agentPickerToggle.finalize();
|
|
677
|
+
this._settingsToggle.finalize();
|
|
559
678
|
// Clear local references only — driver and store stay in their registries.
|
|
560
679
|
this.driver = undefined;
|
|
561
680
|
this._sessionRef = undefined;
|
|
@@ -691,23 +810,79 @@ let FoundationAiAssistant = FoundationAiAssistant_1 = class FoundationAiAssistan
|
|
|
691
810
|
return parts.length ? parts.join('::') : undefined;
|
|
692
811
|
}
|
|
693
812
|
toggleSettings() {
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
813
|
+
this._settingsToggle.toggle();
|
|
814
|
+
}
|
|
815
|
+
toggleAgentPicker() {
|
|
816
|
+
this._agentPickerToggle.toggle();
|
|
817
|
+
}
|
|
818
|
+
/**
|
|
819
|
+
* Programmatically pin an agent by name. Returns `true` if the pin was
|
|
820
|
+
* applied, `false` if the agent isn't in the configured agents array or
|
|
821
|
+
* the call was suppressed by `force: false`.
|
|
822
|
+
*
|
|
823
|
+
* With `force: false`, the call is a no-op when a `picker.defaultAgent` is
|
|
824
|
+
* configured and the user has already moved away from it (either by picking
|
|
825
|
+
* another agent or by switching to Auto). This lets hosts seed an opinion
|
|
826
|
+
* without overriding an explicit user choice.
|
|
827
|
+
*
|
|
828
|
+
* @public
|
|
829
|
+
*/
|
|
830
|
+
setAgent(agentName, options) {
|
|
831
|
+
var _a, _b, _c;
|
|
832
|
+
if (!((_a = this.agents) !== null && _a !== void 0 ? _a : []).some((a) => a.name === agentName))
|
|
833
|
+
return false;
|
|
834
|
+
const force = (_b = options === null || options === void 0 ? void 0 : options.force) !== null && _b !== void 0 ? _b : true;
|
|
835
|
+
if (!force) {
|
|
836
|
+
const defaultAgent = (_c = this.chatConfig.picker) === null || _c === void 0 ? void 0 : _c.defaultAgent;
|
|
837
|
+
if (defaultAgent && this.pinnedAgentName !== defaultAgent)
|
|
838
|
+
return false;
|
|
710
839
|
}
|
|
840
|
+
this.pinnedAgentName = agentName;
|
|
841
|
+
return true;
|
|
842
|
+
}
|
|
843
|
+
/** Whether the picker toggle button should appear. Mirrors the picker's own visibility rule. */
|
|
844
|
+
get agentPickerEnabled() {
|
|
845
|
+
var _a, _b, _c;
|
|
846
|
+
if (this.agentPicker === 'disabled')
|
|
847
|
+
return false;
|
|
848
|
+
if (((_b = (_a = this.agents) === null || _a === void 0 ? void 0 : _a.length) !== null && _b !== void 0 ? _b : 0) <= 1)
|
|
849
|
+
return false;
|
|
850
|
+
return ((_c = this.agents) !== null && _c !== void 0 ? _c : []).some((a) => { var _a; return (_a = a.manualSelection) === null || _a === void 0 ? void 0 : _a.enabled; });
|
|
851
|
+
}
|
|
852
|
+
/** Hint text for the currently pinned agent, if any. Used in the toggle button tooltip. */
|
|
853
|
+
get pinnedAgentHint() {
|
|
854
|
+
var _a, _b, _c;
|
|
855
|
+
if (this.pinnedAgentName === null)
|
|
856
|
+
return undefined;
|
|
857
|
+
return (_c = (_b = (_a = this.agents) === null || _a === void 0 ? void 0 : _a.find((a) => a.name === this.pinnedAgentName)) === null || _b === void 0 ? void 0 : _b.manualSelection) === null || _c === void 0 ? void 0 : _c.hint;
|
|
858
|
+
}
|
|
859
|
+
/**
|
|
860
|
+
* Tint applied to the pin icon when an agent is pinned. Picked from the
|
|
861
|
+
* brand palette by agent position (modulo the palette length), so each agent
|
|
862
|
+
* gets a consistent colour across renders. `undefined` when nothing is
|
|
863
|
+
* pinned, the agent isn't in the current array, or the host has opted out
|
|
864
|
+
* via `chatConfig.picker.disablePinColours`.
|
|
865
|
+
*/
|
|
866
|
+
get pinnedAgentColour() {
|
|
867
|
+
var _a, _b, _c;
|
|
868
|
+
if (this.pinnedAgentName === null)
|
|
869
|
+
return undefined;
|
|
870
|
+
if ((_a = this.chatConfig.picker) === null || _a === void 0 ? void 0 : _a.disablePinColours)
|
|
871
|
+
return undefined;
|
|
872
|
+
const idx = (_c = (_b = this.agents) === null || _b === void 0 ? void 0 : _b.findIndex((a) => a.name === this.pinnedAgentName)) !== null && _c !== void 0 ? _c : -1;
|
|
873
|
+
if (idx < 0)
|
|
874
|
+
return undefined;
|
|
875
|
+
return PIN_COLOURS[idx % PIN_COLOURS.length];
|
|
876
|
+
}
|
|
877
|
+
/**
|
|
878
|
+
* Placeholder shown in the chat input. Substitutes the pinned agent's name
|
|
879
|
+
* when one is selected so the user has a clearer signal of where their
|
|
880
|
+
* message will go; otherwise falls back to the host-provided placeholder.
|
|
881
|
+
*/
|
|
882
|
+
get effectivePlaceholder() {
|
|
883
|
+
if (this.pinnedAgentName)
|
|
884
|
+
return `Message ${this.pinnedAgentName}...`;
|
|
885
|
+
return this.placeholder;
|
|
711
886
|
}
|
|
712
887
|
toggleShowToolCalls() {
|
|
713
888
|
this.showToolCalls = !this.showToolCalls;
|
|
@@ -903,6 +1078,10 @@ let FoundationAiAssistant = FoundationAiAssistant_1 = class FoundationAiAssistan
|
|
|
903
1078
|
this.attachmentErrors = [];
|
|
904
1079
|
this.suggestionsState = { status: 'idle' };
|
|
905
1080
|
this._userScrolledAway = false;
|
|
1081
|
+
// Close the picker on send — the toggle button is disabled during loading,
|
|
1082
|
+
// so without this an open panel would be stuck open with clickable segments.
|
|
1083
|
+
if (this.agentPickerOpen)
|
|
1084
|
+
this.toggleAgentPicker();
|
|
906
1085
|
this.state = 'loading';
|
|
907
1086
|
this.startLoadingTimer();
|
|
908
1087
|
const displayInput = (pendingAttachments === null || pendingAttachments === void 0 ? void 0 : pendingAttachments.length)
|
|
@@ -980,6 +1159,9 @@ __decorate([
|
|
|
980
1159
|
__decorate([
|
|
981
1160
|
attr({ attribute: 'debug-redux', mode: 'boolean' })
|
|
982
1161
|
], FoundationAiAssistant.prototype, "debugRedux", void 0);
|
|
1162
|
+
__decorate([
|
|
1163
|
+
volatile
|
|
1164
|
+
], FoundationAiAssistant.prototype, "effectiveChatInputDuringExecution", null);
|
|
983
1165
|
__decorate([
|
|
984
1166
|
observable
|
|
985
1167
|
], FoundationAiAssistant.prototype, "attachments", void 0);
|
|
@@ -1004,6 +1186,18 @@ __decorate([
|
|
|
1004
1186
|
__decorate([
|
|
1005
1187
|
volatile
|
|
1006
1188
|
], FoundationAiAssistant.prototype, "visibleMessages", null);
|
|
1189
|
+
__decorate([
|
|
1190
|
+
volatile
|
|
1191
|
+
], FoundationAiAssistant.prototype, "agentPickerEnabled", null);
|
|
1192
|
+
__decorate([
|
|
1193
|
+
volatile
|
|
1194
|
+
], FoundationAiAssistant.prototype, "pinnedAgentHint", null);
|
|
1195
|
+
__decorate([
|
|
1196
|
+
volatile
|
|
1197
|
+
], FoundationAiAssistant.prototype, "pinnedAgentColour", null);
|
|
1198
|
+
__decorate([
|
|
1199
|
+
volatile
|
|
1200
|
+
], FoundationAiAssistant.prototype, "effectivePlaceholder", null);
|
|
1007
1201
|
FoundationAiAssistant = FoundationAiAssistant_1 = __decorate([
|
|
1008
1202
|
customElement({
|
|
1009
1203
|
name: 'foundation-ai-assistant',
|
|
@@ -516,10 +516,69 @@ export const styles = css `
|
|
|
516
516
|
background-color: var(--neutral-layer-2);
|
|
517
517
|
}
|
|
518
518
|
|
|
519
|
+
.input-left-controls {
|
|
520
|
+
display: flex;
|
|
521
|
+
flex-direction: column;
|
|
522
|
+
align-items: stretch;
|
|
523
|
+
gap: calc(var(--design-unit) * 1px);
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
/* Lock the width so the column doesn't reflow when the button content
|
|
527
|
+
switches between the "Auto" label, the pin icon, and the chevron. */
|
|
528
|
+
.agent-toggle-button {
|
|
529
|
+
width: 56px;
|
|
530
|
+
min-width: 56px;
|
|
531
|
+
max-width: 56px;
|
|
532
|
+
box-sizing: border-box;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
.agent-toggle-label {
|
|
536
|
+
font-size: 0.75em;
|
|
537
|
+
font-weight: 600;
|
|
538
|
+
letter-spacing: 0.02em;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
@keyframes agent-picker-slide-in {
|
|
542
|
+
from {
|
|
543
|
+
opacity: 0%;
|
|
544
|
+
transform: translateY(8px);
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
to {
|
|
548
|
+
opacity: 100%;
|
|
549
|
+
transform: translateY(0);
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
@keyframes agent-picker-slide-out {
|
|
554
|
+
from {
|
|
555
|
+
opacity: 100%;
|
|
556
|
+
transform: translateY(0);
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
to {
|
|
560
|
+
opacity: 0%;
|
|
561
|
+
transform: translateY(8px);
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
.agent-picker-panel {
|
|
566
|
+
animation: agent-picker-slide-in 0.2s ease-out;
|
|
567
|
+
overflow: hidden;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
.agent-picker-panel.closing {
|
|
571
|
+
animation: agent-picker-slide-out 0.2s ease-in forwards;
|
|
572
|
+
}
|
|
573
|
+
|
|
519
574
|
.chat-input {
|
|
520
575
|
flex: 1;
|
|
521
576
|
resize: none;
|
|
522
577
|
max-height: 150px;
|
|
578
|
+
|
|
579
|
+
/* Match the height of the left-controls column when it has stacked buttons,
|
|
580
|
+
so the textarea fills the row instead of sitting flush to the send button. */
|
|
581
|
+
align-self: stretch;
|
|
523
582
|
}
|
|
524
583
|
|
|
525
584
|
.file-input {
|