@corenel/web-ui 0.1.0 → 0.2.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 (51) hide show
  1. package/dist/AskUser.d.ts.map +1 -1
  2. package/dist/AskUser.js +48 -12
  3. package/dist/AskUser.js.map +1 -1
  4. package/dist/Composer.d.ts +7 -1
  5. package/dist/Composer.d.ts.map +1 -1
  6. package/dist/Composer.js +20 -6
  7. package/dist/Composer.js.map +1 -1
  8. package/dist/ContextBar.d.ts +9 -1
  9. package/dist/ContextBar.d.ts.map +1 -1
  10. package/dist/ContextBar.js +3 -3
  11. package/dist/ContextBar.js.map +1 -1
  12. package/dist/ContextManager.d.ts +78 -2
  13. package/dist/ContextManager.d.ts.map +1 -1
  14. package/dist/ContextManager.js +175 -51
  15. package/dist/ContextManager.js.map +1 -1
  16. package/dist/Lightbox.d.ts.map +1 -1
  17. package/dist/Lightbox.js +2 -10
  18. package/dist/Lightbox.js.map +1 -1
  19. package/dist/RichContent.d.ts +4 -2
  20. package/dist/RichContent.d.ts.map +1 -1
  21. package/dist/RichContent.js +12 -7
  22. package/dist/RichContent.js.map +1 -1
  23. package/dist/Transcript.d.ts +2 -1
  24. package/dist/Transcript.d.ts.map +1 -1
  25. package/dist/Transcript.js +6 -6
  26. package/dist/Transcript.js.map +1 -1
  27. package/dist/agentChat/AgentChat.d.ts +122 -0
  28. package/dist/agentChat/AgentChat.d.ts.map +1 -0
  29. package/dist/agentChat/AgentChat.js +527 -0
  30. package/dist/agentChat/AgentChat.js.map +1 -0
  31. package/dist/agentChat/OptionsDrawer.d.ts +81 -0
  32. package/dist/agentChat/OptionsDrawer.d.ts.map +1 -0
  33. package/dist/agentChat/OptionsDrawer.js +321 -0
  34. package/dist/agentChat/OptionsDrawer.js.map +1 -0
  35. package/dist/agentChat/SkillsSection.d.ts +3 -0
  36. package/dist/agentChat/SkillsSection.d.ts.map +1 -0
  37. package/dist/agentChat/SkillsSection.js +86 -0
  38. package/dist/agentChat/SkillsSection.js.map +1 -0
  39. package/dist/agentChat/controller.d.ts +93 -0
  40. package/dist/agentChat/controller.d.ts.map +1 -0
  41. package/dist/agentChat/controller.js +25 -0
  42. package/dist/agentChat/controller.js.map +1 -0
  43. package/dist/agentChat/skillRunner.d.ts +14 -0
  44. package/dist/agentChat/skillRunner.d.ts.map +1 -0
  45. package/dist/agentChat/skillRunner.js +4 -0
  46. package/dist/agentChat/skillRunner.js.map +1 -0
  47. package/dist/toolRender.d.ts +8 -0
  48. package/dist/toolRender.d.ts.map +1 -1
  49. package/dist/toolRender.js +7 -1
  50. package/dist/toolRender.js.map +1 -1
  51. package/package.json +16 -21
@@ -0,0 +1,527 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ /* AgentChat — the one chat column, extracted verbatim from the editor
3
+ * AgentPanel and re-based on the structural AgentSurfaceController so every
4
+ * agent surface (editor, /agent, /craft) renders the same header / transcript /
5
+ * live tools / permission card / composer with host differences expressed as
6
+ * props. Engine calls go through `controller` + the CONTROLLED `sessionId`;
7
+ * persona/policy selection and the system-parts toggles are owned here (harness
8
+ * get/set); everything editor-specific (context source, tips lifecycle, budget
9
+ * form, host slash commands) comes in via props. */
10
+ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
11
+ import { Transcript, ToolLine, DiffProposal } from '../Transcript';
12
+ import { ToolResultCard } from '../toolRender';
13
+ import { Markdown } from '../RichContent';
14
+ import { Composer } from '../Composer';
15
+ import { AskUser } from '../AskUser';
16
+ import { Lightbox } from '../Lightbox';
17
+ import { ContextBar } from '../ContextBar';
18
+ import { useTranscriptView } from '../transcriptView';
19
+ import { OptionsDrawer, useMcpTools } from './OptionsDrawer';
20
+ import { useControllerSession, useControllerGlobal } from './controller';
21
+ import { getSelectedPolicyName, setSelectedPolicyName } from '@corenel/harness/guardrail';
22
+ import { loadPersonas, getSelectedPersona, setSelectedPersona } from '@corenel/harness/prompts/config';
23
+ import { getSystemParts, setSystemParts } from '@corenel/harness/systemParts';
24
+ import { listSkills, subscribeSkills } from '@corenel/harness/skills';
25
+ import { allTools } from '@corenel/harness/tools/builtins';
26
+ import { useContextDocs, addDoc, removeDoc, classifyUpload, looksBinary, ContextDocError, UPLOAD_ACCEPT } from '@corenel/tools-web/contextDocs';
27
+ import { fmtTok } from '@corenel/ui-kit/usage';
28
+ import { useBrand } from '@corenel/ui-kit/host';
29
+ import { IcPlus, IcZap, IcTrash, IcChevron, IcX, IcRefresh, IcKey, IcCheck, IcSliders, IcUser, IcFile, IcCode, IcHelp, IcEdit, Spinner, LiveCursor } from '@corenel/ui-kit/icons';
30
+ const NOOP = () => { };
31
+ export function AgentChat(props) {
32
+ const { controller, sessionId, onSessionChange, sessions, contextWindow, systemSource, offloaded, usageSurface, inPrice, modes, currentSrc, onApplyEdit, renderFence, emptyState, headerLead, headerExtras, title, features, embedded = false, onClose, chips, onRemoveChip, renderContextMenu, capabilities, slashExtra, onSlashAction, interceptSend, sendOptsExtra, onAfterSend, tip, budgetSection, connections, toolBadge, policyEditor, settingsExtras, LiveTimeline, fixedIdentity, } = props;
33
+ const { name: brandName } = useBrand(); // host-injected product name (CorenelHost seam)
34
+ // The engine owns the conversation, run loop, worker, and persistence — this
35
+ // column is a view over ONE controller session. The active session id is
36
+ // CONTROLLED by the host (sessionId/onSessionChange).
37
+ const session = useControllerSession(controller, sessionId ?? '');
38
+ const globalTick = useControllerGlobal(controller); // cross-session changes (titles, run badges)
39
+ const { turns, input, mode, running, live, recovery, error, suggestions, attachments, pendingPerm, pendingAsk, policyNotes, degradedModel, recEvents } = session;
40
+ // Thin engine-routed setters so the render below reads like the old local-state
41
+ // version (same call sites, engine-backed).
42
+ const setInput = useCallback((v) => { if (sessionId)
43
+ controller.setInput(sessionId, v); }, [controller, sessionId]);
44
+ const setMode = useCallback((m) => { if (sessionId)
45
+ controller.setMode(sessionId, m); }, [controller, sessionId]);
46
+ const setAttachments = useCallback((fn) => { if (sessionId)
47
+ controller.setAttachments(sessionId, fn); }, [controller, sessionId]);
48
+ const toggleInclude = useCallback((turnId) => { if (sessionId)
49
+ controller.toggleInclude(sessionId, turnId); }, [controller, sessionId]);
50
+ // Which composed parts of the system prompt to include (persona/mode/...). A ref
51
+ // mirror keeps composeSystem reading the latest even if toggled just before send.
52
+ const [parts, setParts] = useState(getSystemParts);
53
+ const partsRef = useRef(parts);
54
+ partsRef.current = parts;
55
+ const togglePart = (k) => setParts((p) => { const next = { ...p, [k]: !p[k] }; setSystemParts(next); return next; });
56
+ // Per-run budget override: AgentChat only stores + forwards it (send opts); the
57
+ // HOST's budgetSection form computes the effective value and merges changes.
58
+ const [override, setOverride] = useState({});
59
+ const [sessionList, setSessionList] = useState([]);
60
+ const [menuOpen, setMenuOpen] = useState(false);
61
+ // The right-side options drawer: persona, display, policy, and capabilities —
62
+ // the extracted OptionsDrawer (Task 3) renders it; AgentChat owns only the
63
+ // open flag and the CONTROLLED persona/policy selections.
64
+ const [drawerOpen, setDrawerOpen] = useState(false);
65
+ // True while the injected live scrubber is scrubbed back: hide the live transcript
66
+ // so the scrubber's in-flow folded view reclaims its space.
67
+ const [timelineScrubbed, setTimelineScrubbed] = useState(false);
68
+ const [transcriptView] = useTranscriptView();
69
+ // Installed skills, for the "/" command menu (the drawer's SkillsSection loads its
70
+ // own copy). Subscribe so the menu re-renders when a skill is installed/removed;
71
+ // the same signal bumps registryTick so the drawer's tool groups recompute.
72
+ const [skills, setSkills] = useState([]);
73
+ const [registryTick, setRegistryTick] = useState(0);
74
+ const skillsFn = capabilities?.skills ?? listSkills;
75
+ // Fingerprint of the last-loaded skills list (names+enabled), so the reload
76
+ // below only bumps registryTick when the registry actually changed. Keying the
77
+ // effect on the OBJECT identity of `capabilities` (rather than its getters) and
78
+ // bumping the tick unconditionally would loop with a host that passes an inline
79
+ // (unmemoized) `capabilities={{...}}` prop: effect -> setRegistryTick ->
80
+ // re-render -> new object identity -> effect again, forever.
81
+ const skillsFpRef = useRef('');
82
+ useEffect(() => {
83
+ const reload = () => {
84
+ skillsFn().then((list) => {
85
+ setSkills(list);
86
+ const fp = list.map((s) => `${s.name}:${s.enabled}`).join('|');
87
+ if (fp !== skillsFpRef.current) {
88
+ skillsFpRef.current = fp;
89
+ setRegistryTick((t) => t + 1);
90
+ }
91
+ }).catch(() => setSkills([]));
92
+ };
93
+ reload();
94
+ return subscribeSkills(reload);
95
+ // eslint-disable-next-line react-hooks/exhaustive-deps
96
+ }, [capabilities?.skills, capabilities?.tools]);
97
+ // Discovered MCP tools for the "/" command menu — mount-load from cache, then a
98
+ // forced refresh each time the drawer opens (mirrors the old AgentPanel's
99
+ // mount-load + drawer-open refresh of its shared list), plus after a
100
+ // drawer-driven MCP mutation (onConnectionsChanged, Fix 4) so the slash menu
101
+ // doesn't go stale between drawer opens.
102
+ const [mcpKey, setMcpKey] = useState(0);
103
+ useEffect(() => { if (drawerOpen)
104
+ setMcpKey((k) => k + 1); }, [drawerOpen]);
105
+ const mcpTools = useMcpTools(connections, mcpKey);
106
+ const [ctxMenuOpen, setCtxMenuOpen] = useState(false);
107
+ const fileInputRef = useRef(null);
108
+ const [uploadNotice, setUploadNotice] = useState(null);
109
+ const contextDocs = useContextDocs(sessionId ?? '');
110
+ const readAsDataUrl = (f) => new Promise((res) => {
111
+ const r = new FileReader();
112
+ r.onload = () => res(typeof r.result === 'string' ? r.result : '');
113
+ r.onerror = () => res('');
114
+ r.readAsDataURL(f);
115
+ });
116
+ // Route uploads: images -> one-shot attachments (as paste does), text-like ->
117
+ // persistent context docs, everything else -> a notice. Cap breaches surface
118
+ // the store's user-facing ContextDocError message verbatim.
119
+ const processFiles = async (files) => {
120
+ setUploadNotice(null);
121
+ const notices = [];
122
+ for (const f of files) {
123
+ const kind = classifyUpload(f.name, f.type);
124
+ if (kind === 'image') {
125
+ // Images are one-shot attachments (composer state), so they route
126
+ // even without a bound session.
127
+ const url = await readAsDataUrl(f);
128
+ if (url)
129
+ setAttachments((a) => [...a, url]);
130
+ continue;
131
+ }
132
+ if (!sessionId) {
133
+ notices.push('Start a chat before attaching documents.');
134
+ continue;
135
+ }
136
+ if (kind === 'unsupported') {
137
+ notices.push(`"${f.name}" is not supported yet - PDF and Office ingestion are coming later. Upload plain-text files (md, txt, csv, json, code).`);
138
+ continue;
139
+ }
140
+ let text;
141
+ try {
142
+ text = await f.text();
143
+ }
144
+ catch {
145
+ notices.push(`"${f.name}" could not be read.`);
146
+ continue;
147
+ }
148
+ if (looksBinary(text)) {
149
+ notices.push(`"${f.name}" looks like a binary file - only plain-text documents are supported.`);
150
+ continue;
151
+ }
152
+ try {
153
+ addDoc(sessionId, { name: f.name, mime: f.type, text });
154
+ }
155
+ catch (e) {
156
+ if (e instanceof ContextDocError)
157
+ notices.push(e.message);
158
+ else
159
+ throw e;
160
+ }
161
+ }
162
+ setUploadNotice(notices.length ? notices.join('\n') : null);
163
+ };
164
+ // A rejection notice is about THIS session's upload attempt; don't let it
165
+ // linger onto another chat.
166
+ useEffect(() => { setUploadNotice(null); }, [sessionId]);
167
+ const [selPolicy, setSelPolicy] = useState(getSelectedPolicyName);
168
+ const selectPolicy = useCallback((name) => { setSelPolicy(name); setSelectedPolicyName(name); }, []);
169
+ const policyName = fixedIdentity?.policyName ?? selPolicy;
170
+ const [selPersona, setSelPersona] = useState(getSelectedPersona);
171
+ const pickPersona = useCallback((id) => { setSelPersona(id); setSelectedPersona(id); }, []);
172
+ const personaName = fixedIdentity ? (fixedIdentity.persona ?? '') : selPersona;
173
+ // Persona list, only for the options-button tooltip label (the drawer loads its
174
+ // own copy for the picker).
175
+ const [personas, setPersonas] = useState([]);
176
+ useEffect(() => { loadPersonas().then(setPersonas).catch(() => setPersonas([])); }, []);
177
+ const personaLabel = personas.find((p) => p.id === personaName)?.name || personaName;
178
+ const bodyRef = useRef(null);
179
+ const [lightboxSrc, setLightboxSrc] = useState(null);
180
+ // Stick-to-bottom: autoscroll only while the user is pinned at the bottom.
181
+ // Scrolling up to read history must never be interrupted by streaming deltas;
182
+ // scrolling back down re-engages the pin. 48px of slop absorbs sub-pixel
183
+ // rounding and momentum overshoot.
184
+ const pinnedRef = useRef(true);
185
+ const onBodyScroll = () => {
186
+ const el = bodyRef.current;
187
+ if (el)
188
+ pinnedRef.current = el.scrollHeight - el.scrollTop - el.clientHeight <= 48;
189
+ };
190
+ // auto-scroll the transcript to the bottom as content streams in — unless the
191
+ // user has scrolled up to read history
192
+ useEffect(() => {
193
+ if (pinnedRef.current)
194
+ bodyRef.current?.scrollTo({ top: bodyRef.current.scrollHeight });
195
+ }, [turns, live]);
196
+ // A session switch is a fresh view: re-pin and land on the latest message.
197
+ useEffect(() => {
198
+ pinnedRef.current = true;
199
+ bodyRef.current?.scrollTo({ top: bodyRef.current.scrollHeight });
200
+ }, [sessionId]);
201
+ // Keep the switcher in sync with the store — refreshed on every engine global
202
+ // change (new/deleted session, run start/stop, first-message auto-titling).
203
+ const refreshSessions = useCallback(() => { sessions.list().then(setSessionList).catch(() => { }); }, [sessions]);
204
+ useEffect(() => { refreshSessions(); }, [refreshSessions, globalTick]);
205
+ // Elapsed seconds while a run is in flight — drives the persistent header
206
+ // indicator so "the model is working" stays visible through streaming + tools
207
+ // (the inline "thinking…" only shows before the first token).
208
+ const [runElapsed, setRunElapsed] = useState(0);
209
+ useEffect(() => {
210
+ if (!running) {
211
+ setRunElapsed(0);
212
+ return;
213
+ }
214
+ const start = Date.now();
215
+ setRunElapsed(0);
216
+ const t = setInterval(() => setRunElapsed(Math.floor((Date.now() - start) / 1000)), 1000);
217
+ return () => clearInterval(t);
218
+ }, [running]);
219
+ const newSession = async () => {
220
+ const id = await controller.newSession();
221
+ onSessionChange(id);
222
+ refreshSessions();
223
+ setMenuOpen(false);
224
+ };
225
+ const switchSession = (id) => {
226
+ onSessionChange(id);
227
+ void controller.load(id);
228
+ setMenuOpen(false);
229
+ };
230
+ const deleteSession = async (id) => {
231
+ await controller.deleteSession(id);
232
+ // Land on (or create) another session so the composer stays bound — the
233
+ // engine keys all input/run state by session id. openInitial() can reject
234
+ // (e.g. nothing to reopen); fall back to a fresh session so the composer
235
+ // still lands on a live one. Only if both fail is the current id left as-is.
236
+ if (id === sessionId) {
237
+ const next = await controller.openInitial().catch(() => null) ?? await controller.newSession().catch(() => null);
238
+ if (next)
239
+ onSessionChange(next);
240
+ }
241
+ refreshSessions();
242
+ };
243
+ // Inline rename — double-click the header title or the menu's pencil. Committing
244
+ // locks the title so auto-titling from the first message stops overwriting it.
245
+ const [renaming, setRenaming] = useState(null);
246
+ const commitRename = async () => {
247
+ if (!renaming)
248
+ return;
249
+ const { id, title: next } = renaming;
250
+ setRenaming(null);
251
+ const t = next.trim();
252
+ const cur = sessionList.find((s) => s.id === id)?.title || '';
253
+ if (t && t !== cur) {
254
+ await sessions.rename(id, t);
255
+ refreshSessions();
256
+ }
257
+ };
258
+ const startRename = (id) => {
259
+ setMenuOpen(false);
260
+ setRenaming({ id, title: sessionList.find((s) => s.id === id)?.title || '' });
261
+ };
262
+ const renameKey = (e) => {
263
+ if (e.key === 'Enter') {
264
+ e.preventDefault();
265
+ void commitRename();
266
+ }
267
+ else if (e.key === 'Escape') {
268
+ e.preventDefault();
269
+ setRenaming(null);
270
+ }
271
+ };
272
+ const resolvePerm = (allow, always = false) => {
273
+ if (sessionId)
274
+ controller.resolvePerm(sessionId, allow, always);
275
+ };
276
+ // The "/" command menu = the generic commands, the host's extras, then every
277
+ // available tool, skill, and MCP tool as an `insert` command that prefills an
278
+ // editable request. Rebuilt when the registries change. `context` appears only
279
+ // when the host supplies an add-context popup to open.
280
+ const slashCommands = useMemo(() => {
281
+ // Order matches the old AgentPanel: Session, Workspace(+host extras), Prompt,
282
+ // Help, then Tools/Skills/MCP inserts. `help` is generic but lands AFTER the
283
+ // host's slashExtra merge so a host's Workspace/Prompt entries stay ahead of it.
284
+ const beforeExtra = [
285
+ { name: 'new', hint: 'start a fresh chat', section: 'Session', icon: _jsx(IcPlus, { size: 13 }), kind: 'action' },
286
+ { name: 'clear', hint: 'clear this conversation', section: 'Session', icon: _jsx(IcRefresh, { size: 13 }), kind: 'action' },
287
+ ...(renderContextMenu ? [{ name: 'context', hint: 'attach files or selection', section: 'Workspace', icon: _jsx(IcFile, { size: 13 }), kind: 'action' }] : []),
288
+ { name: 'persona', hint: 'switch the agent persona', section: 'Workspace', icon: _jsx(IcUser, { size: 13 }), kind: 'action' },
289
+ ];
290
+ const help = { name: 'help', hint: 'what the agent can do', section: 'Help', icon: _jsx(IcHelp, { size: 13 }), kind: 'action' };
291
+ // Dedupe slashExtra against the generic set — first definition wins (generic
292
+ // first), so a colliding host entry is dropped rather than doubled in the menu.
293
+ const genericNames = new Set([...beforeExtra, help].map((c) => c.name));
294
+ const dedupedExtra = (slashExtra ?? []).filter((c) => !genericNames.has(c.name));
295
+ const staticCmds = [...beforeExtra, ...dedupedExtra, help];
296
+ const staticNames = new Set(staticCmds.map((c) => c.name));
297
+ const trunc = (s) => {
298
+ const one = (s ?? '').replace(/\s+/g, ' ').trim();
299
+ return one.length > 72 ? one.slice(0, 71) + '…' : one;
300
+ };
301
+ const toolCmds = (capabilities?.tools ?? allTools)()
302
+ .filter((t) => !staticNames.has(t.name))
303
+ .map((t) => ({ name: t.name, hint: trunc(t.description), section: 'Tools', icon: _jsx(IcSliders, { size: 13 }), kind: 'insert', template: `Use the ${t.name} tool to ` }));
304
+ const skillCmds = skills
305
+ .filter((s) => s.enabled && !staticNames.has(s.name))
306
+ .map((s) => ({ name: s.name, hint: trunc(s.description), section: 'Skills', icon: _jsx(IcZap, { size: 13 }), kind: 'insert', template: `Use the ${s.name} skill to ` }));
307
+ const mcpCmds = mcpTools
308
+ .filter((t) => !!t.name && !t.error && !staticNames.has(t.name))
309
+ .map((t) => ({ name: t.name, hint: trunc(t.description || t.serverLabel), section: 'MCP', icon: _jsx(IcCode, { size: 13 }), kind: 'insert', template: `Use the ${t.name} MCP tool (${t.serverLabel}) to ` }));
310
+ return [...staticCmds, ...toolCmds, ...skillCmds, ...mcpCmds];
311
+ // eslint-disable-next-line react-hooks/exhaustive-deps
312
+ }, [renderContextMenu, slashExtra, capabilities, skills, mcpTools, registryTick]);
313
+ const slashMap = useMemo(() => Object.fromEntries(slashCommands.map((c) => [c.name, c])), [slashCommands]);
314
+ // Per-send options for the engine. Rebuilt per call so persona/policy/budget are
315
+ // read fresh; `partsOverride` reads through the ref so a toggle right before send
316
+ // still lands. Host extras (editorFile, onEditReview, ...) spread on top.
317
+ const buildSendOpts = () => {
318
+ const base = {
319
+ persona: personaName,
320
+ policyName,
321
+ constraints: Object.keys(override).length ? override : undefined,
322
+ partsOverride: partsRef.current,
323
+ // AgentChat renders the policyNotes banner — skip the duplicate transcript
324
+ // note() turn for budget warn/stop (see BaseSendOpts.budgetNotices).
325
+ budgetNotices: 'notes',
326
+ };
327
+ const extra = sendOptsExtra?.();
328
+ return (extra ? { ...base, ...extra } : base);
329
+ };
330
+ /** Resolve the session to run against — the active one, or a fresh one when
331
+ * none is active yet (mirrors the old runSend's auto-create). */
332
+ const ensureSession = async () => {
333
+ if (sessionId)
334
+ return sessionId;
335
+ const id = await controller.newSession();
336
+ onSessionChange(id);
337
+ return id;
338
+ };
339
+ const sendMessage = async (text, images) => {
340
+ if ((!text && images.length === 0) || running)
341
+ return;
342
+ // Sending your own message always snaps the view to the bottom.
343
+ pinnedRef.current = true;
344
+ const sid = await ensureSession();
345
+ await controller.send(sid, text, images, buildSendOpts());
346
+ onAfterSend?.(sid);
347
+ };
348
+ /** The full send pipeline minus slash dispatch — the `api.sendText` handed to
349
+ * host slash actions (the editor's /init sends a composed prompt through it). */
350
+ const sendText = (text) => {
351
+ void (async () => {
352
+ if (!text || running)
353
+ return;
354
+ if (interceptSend && (await interceptSend(text)))
355
+ return;
356
+ await sendMessage(text, []);
357
+ })();
358
+ };
359
+ // /help renders from the CURRENT resolved slash set, not a static blurb — so
360
+ // every surface (editor, /agent, /craft) lists exactly its own commands. Tools/
361
+ // Skills/MCP are excluded (those are insert-a-request shortcuts, not "commands"
362
+ // in the help sense); only the Session/Workspace/Prompt/Help-style entries
363
+ // (the generic set + a host's slashExtra) are listed.
364
+ const buildHelpText = () => {
365
+ const cmds = slashCommands.filter((c) => c.section !== 'Tools' && c.section !== 'Skills' && c.section !== 'MCP');
366
+ const lines = cmds.map((c) => `- \`/${c.name}\` — ${c.hint}`).join('\n');
367
+ return `**${brandName}** — type \`/\` for commands:\n\n${lines}\n\nPick a mode (auto/edit/plan/brainstorm) by the composer.`;
368
+ };
369
+ const runSlash = (name) => {
370
+ const def = slashMap[name];
371
+ if (!def)
372
+ return 'none';
373
+ if (def.kind === 'insert') {
374
+ setInput(def.template ?? '');
375
+ return 'prefilled';
376
+ }
377
+ if (onSlashAction?.(name, { sendText }))
378
+ return 'handled';
379
+ switch (name) {
380
+ case 'new':
381
+ void newSession();
382
+ break;
383
+ case 'clear':
384
+ if (sessionId)
385
+ controller.clear(sessionId);
386
+ break;
387
+ case 'context':
388
+ setCtxMenuOpen(true);
389
+ break;
390
+ case 'persona':
391
+ setDrawerOpen(true);
392
+ break;
393
+ case 'help':
394
+ if (sessionId)
395
+ controller.note(sessionId, buildHelpText());
396
+ break;
397
+ default: return 'none';
398
+ }
399
+ return 'handled';
400
+ };
401
+ const send = async () => {
402
+ const text = input.trim();
403
+ const images = attachments;
404
+ if ((!text && images.length === 0) || running)
405
+ return;
406
+ // Host commands are first-chance (the editor's /strategy + /model) — BEFORE
407
+ // slash dispatch, matching the old AgentPanel where those intercepts ran
408
+ // ahead of runSlash so '/strategy <goal>' isn't swallowed by a menu prefill.
409
+ if (interceptSend && (await interceptSend(text)))
410
+ return;
411
+ if (text.startsWith('/') && images.length === 0) {
412
+ const r = runSlash(text.slice(1).split(' ')[0]);
413
+ if (r === 'handled') {
414
+ setInput('');
415
+ return;
416
+ }
417
+ if (r === 'prefilled')
418
+ return; // template is in the composer — review, then send
419
+ }
420
+ // The engine guards + clears the composer itself: a feature-guard denial
421
+ // surfaces in session.error with the draft and pasted images intact.
422
+ await sendMessage(text, images);
423
+ };
424
+ // Re-run a prior user turn (the engine drops it + everything after, then re-sends).
425
+ const onRetry = (turnId) => {
426
+ if (!sessionId || running)
427
+ return;
428
+ void controller.retry(sessionId, turnId, buildSendOpts());
429
+ };
430
+ // The drawer acts on THIS engine session (include-toggles + offload Restore).
431
+ // `conversation` is a required ContextBar/ContextManagerBody prop, so this
432
+ // always returns an object; without an active session yet, the actions are
433
+ // no-ops (there is nothing to toggle/restore). Memoized on the turns identity
434
+ // so the projection memos downstream don't recompute per render.
435
+ const conversation = useMemo(() => ({
436
+ turns: session.turns,
437
+ toggleInclude: (turnId) => { if (sessionId)
438
+ controller.toggleInclude(sessionId, turnId); },
439
+ restoreToolResult: (turnId, blobId, content) => (sessionId ? controller.restoreToolResult(sessionId, turnId, blobId, content) : false),
440
+ // eslint-disable-next-line react-hooks/exhaustive-deps
441
+ }), [sessionId, session.turns]);
442
+ const menuEnabled = features?.sessionMenu !== false;
443
+ const sessionTitle = sessionList.find((s) => s.id === sessionId)?.title || (title ?? brandName);
444
+ const modeEntries = useMemo(() => modes.map((m) => ({ id: m.id, label: m.label, description: m.description ?? '', accent: m.accent })), [modes]);
445
+ return (_jsxs("div", { style: S.panel, children: [_jsxs("header", { style: S.head, children: [headerLead, !menuEnabled ? (_jsx("span", { style: S.sessionName, children: title ?? brandName })) : renaming && renaming.id === sessionId ? (_jsx("input", { autoFocus: true, style: S.renameInput, value: renaming.title, onChange: (e) => setRenaming({ id: renaming.id, title: e.target.value }), onKeyDown: renameKey, onBlur: commitRename, placeholder: "Session name", "aria-label": "Rename session" })) : (_jsxs("div", { style: S.sessionBtn, onClick: () => { setMenuOpen((o) => !o); }, onDoubleClick: (e) => { if (sessionId) {
446
+ e.stopPropagation();
447
+ startRename(sessionId);
448
+ } }, title: sessionId ? 'Double-click to rename' : undefined, children: [_jsx(IcZap, { size: 14 }), _jsx("span", { style: S.sessionName, children: sessionTitle }), _jsx(IcChevron, { size: 12 })] })), running && (_jsxs("span", { style: S.runChip, title: "The model is working \u2014 press Stop to cancel", children: [_jsx(Spinner, { size: 12 }), _jsx("span", { children: runElapsed > 0 ? `${runElapsed}s` : 'working…' })] })), _jsx("div", { style: { flex: 1, minWidth: 8 } }), headerExtras, _jsx("button", { style: S.iconBtn, onClick: newSession, title: "New chat", "aria-label": "New chat", children: _jsx(IcPlus, { size: 16 }) }), _jsx("button", { style: { ...S.iconBtn, ...(drawerOpen ? S.iconBtnActive : null) }, onClick: () => setDrawerOpen((o) => !o), title: fixedIdentity ? 'Display & capabilities' : `Persona, display & capabilities — ${personaLabel}`, "aria-label": "Panel options", children: _jsx(IcSliders, { size: 16 }) }), !embedded && onClose && _jsx("button", { style: S.iconBtn, onClick: onClose, title: "Close (\u2318K to reopen)", "aria-label": "Close", children: _jsx(IcX, { size: 16 }) }), menuEnabled && menuOpen && (_jsxs("div", { style: S.menu, onMouseLeave: () => setMenuOpen(false), children: [_jsxs("button", { style: S.menuItem, onClick: newSession, children: [_jsx(IcPlus, { size: 13 }), " New chat"] }), _jsx("div", { style: S.menuLabel, children: "Recent" }), sessionList.length === 0 && _jsx("div", { style: S.menuEmpty, children: "No saved chats" }), sessionList.slice(0, 12).map((s) => (_jsx("div", { style: S.menuRow, children: renaming && renaming.id === s.id ? (_jsx("input", { autoFocus: true, style: S.renameInput, value: renaming.title, onChange: (e) => setRenaming({ id: s.id, title: e.target.value }), onKeyDown: renameKey, onBlur: commitRename, placeholder: "Session name", "aria-label": "Rename session", onClick: (e) => e.stopPropagation() })) : (_jsxs(_Fragment, { children: [_jsx("button", { style: menuSession(s.id === sessionId), onClick: () => switchSession(s.id), children: s.title || 'Untitled' }), _jsx("button", { style: S.menuDel, onClick: () => startRename(s.id), title: "Rename", children: _jsx(IcEdit, { size: 12 }) }), _jsx("button", { style: S.menuDel, onClick: () => deleteSession(s.id), title: "Delete", children: _jsx(IcTrash, { size: 12 }) })] })) }, s.id)))] }))] }), _jsx(OptionsDrawer, { open: drawerOpen, onClose: () => setDrawerOpen(false), personaName: personaName, onPersona: pickPersona, policyName: policyName, onPolicy: selectPolicy, hideIdentity: !!fixedIdentity, budgetSection: budgetSection?.({ override, setOverride }), settingsExtras: settingsExtras, connections: connections, toolBadge: toolBadge, policyEditor: policyEditor, registryTick: registryTick, capabilities: capabilities, onConnectionsChanged: () => setMcpKey((k) => k + 1) }), _jsx(ContextBar, { contextWindow: contextWindow, parts: parts, onToggle: togglePart, systemSource: typeof systemSource === 'function' ? systemSource(parts) : systemSource, offloaded: offloaded, conversation: conversation, usageSurface: usageSurface }), _jsxs("div", { style: S.chatRegion, children: [_jsxs("div", { style: { ...S.body, ...(timelineScrubbed ? { display: 'none' } : null) }, ref: bodyRef, "data-testid": "chat-body", onScroll: onBodyScroll, children: [turns.length === 0 && !live && emptyState?.({ setInput }), _jsxs("div", { style: S.transcriptWrap, children: [_jsx(Transcript, { turns: turns, currentSrc: currentSrc ?? '', view: transcriptView, onApplyEdit: onApplyEdit ?? NOOP, onToggleInclude: toggleInclude, onRetry: onRetry, onImageClick: setLightboxSrc, renderFence: renderFence }), live && (_jsxs("div", { style: S.live, children: [recovery && _jsxs("div", { style: S.recovery, children: [_jsx(Spinner, { size: 13, title: "Recovering" }), " ", recovery] }), live.tools.map((t) => t.name === 'propose_edit' ? (
449
+ // propose_edit BLOCKS its run on the review, so the decidable card
450
+ // must render here, mid-run — not only in the settled transcript.
451
+ _jsx(DiffProposal, { call: { id: t.id, type: 'function', function: { name: 'propose_edit', arguments: JSON.stringify(t.args || {}) } }, currentSrc: currentSrc ?? '', onApplyEdit: onApplyEdit ?? NOOP, view: transcriptView }, t.id)) : t.done ? (
452
+ // A tool that finished mid-run gets the SAME expandable card as the
453
+ // settled transcript, so its args/result are viewable before the
454
+ // turn ends (not just a bare summary line).
455
+ _jsx(ToolResultCard, { inv: { name: t.name, args: t.args || {}, result: t.result ?? '', isError: t.isError, running: false, ms: t.ms } }, t.id)) : (_jsx("div", { style: S.liveToolRow, children: _jsx(ToolLine, { name: t.name, args: t.args || {}, result: t.result, isError: t.isError, running: true, ms: t.ms }) }, t.id))), live.text ? _jsxs(_Fragment, { children: [_jsx(Markdown, { text: live.text }), running && _jsx(LiveCursor, {})] }) : (!recovery && live.tools.length === 0 && (_jsxs("div", { style: S.thinkingWrap, children: [_jsxs("div", { style: S.thinking, children: [_jsx(Spinner, { size: 14, title: "Thinking" }), " thinking\u2026"] }), tip && _jsx("div", { style: S.tipGhost, children: tip })] })))] }))] }), pendingPerm && (_jsxs("div", { style: S.perm, children: [_jsxs("div", { style: S.permHead, children: [_jsx(IcKey, { size: 14 }), " ", pendingPerm.tool === 'budget' ? 'Budget limit reached' : 'Permission needed'] }), _jsx("div", { style: S.permBody, children: pendingPerm.tool === 'budget' ? pendingPerm.summary : _jsxs(_Fragment, { children: ["The agent wants to use ", _jsx("strong", { children: pendingPerm.tool }), ". ", pendingPerm.summary] }) }), _jsx("div", { style: S.permActions, children: pendingPerm.tool === 'budget' ? (_jsxs(_Fragment, { children: [_jsx("button", { style: S.permDeny, onClick: () => resolvePerm(false), children: "Stop" }), _jsxs("button", { style: S.permAllow, onClick: () => resolvePerm(true), children: [_jsx(IcCheck, { size: 12 }), " Continue"] })] })) : (_jsxs(_Fragment, { children: [_jsx("button", { style: S.permDeny, onClick: () => resolvePerm(false), children: "Deny" }), _jsx("button", { style: S.permAllowSession, onClick: () => resolvePerm(true, true), children: "Allow for session" }), _jsxs("button", { style: S.permAllow, onClick: () => resolvePerm(true), children: [_jsx(IcCheck, { size: 12 }), " Allow once"] })] })) })] })), policyNotes.map((n, i) => (_jsxs("div", { style: n.action === 'stop' ? S.policyStop : S.policyWarn, children: [_jsx(IcKey, { size: 12 }), " Policy (", policyName, "): ", n.message] }, i))), error && _jsx("div", { style: S.error, children: error }), !running && !error && suggestions.length > 0 && (_jsx("div", { style: S.followRow, children: suggestions.map((sg) => (_jsx("button", { style: S.followChip, onClick: () => setInput(sg), title: "Use this follow-up", children: sg }, sg))) }))] }), LiveTimeline && _jsx(LiveTimeline, { events: recEvents, running: running, onScrubbedChange: setTimelineScrubbed })] }), ctxMenuOpen && (_jsxs("div", { style: S.ctxMenu, onMouseLeave: () => setCtxMenuOpen(false), children: [renderContextMenu?.(() => setCtxMenuOpen(false)), _jsxs("button", { style: S.menuItem, disabled: !sessionId, onClick: () => fileInputRef.current?.click(), children: [_jsx(IcFile, { size: 13 }), " Upload document\u2026"] })] })), _jsx("input", { ref: fileInputRef, type: "file", multiple: true, accept: UPLOAD_ACCEPT, style: { display: 'none' }, onChange: (e) => {
456
+ const files = Array.from(e.target.files ?? []);
457
+ e.target.value = '';
458
+ setCtxMenuOpen(false);
459
+ void processFiles(files);
460
+ } }), uploadNotice && (_jsxs("div", { style: S.uploadNotice, children: [_jsx("span", { style: { flex: 1, whiteSpace: 'pre-line' }, children: uploadNotice }), _jsx("button", { style: S.uploadNoticeX, onClick: () => setUploadNotice(null), "aria-label": "Dismiss", children: _jsx(IcX, { size: 11 }) })] })), pendingAsk && (_jsx(AskUser, { questions: pendingAsk.questions, onSubmit: (answers) => { if (sessionId)
461
+ controller.resolveAsk(sessionId, answers); }, onCancel: () => { if (sessionId)
462
+ controller.resolveAsk(sessionId, null); } })), degradedModel && (_jsx("div", { style: S.budgetWrap, children: _jsxs("span", { style: S.budgetDegraded, children: ["Degraded to ", degradedModel, " (budget)"] }) })), _jsx(Composer, { value: input, onChange: setInput, onSend: send, onStop: () => { if (sessionId)
463
+ controller.stop(sessionId); }, running: running, mode: mode, onModeChange: setMode, modes: modeEntries, chips: [
464
+ ...(chips ?? []),
465
+ ...contextDocs.map((d) => ({ id: `doc:${d.id}`, label: `${d.name} · ${fmtTok(d.tokens)} tok`, removable: true })),
466
+ ], onRemoveChip: (cid) => {
467
+ if (cid.startsWith('doc:')) {
468
+ if (sessionId)
469
+ removeDoc(sessionId, cid.slice(4));
470
+ }
471
+ else
472
+ onRemoveChip?.(cid);
473
+ }, onAddContext: () => setCtxMenuOpen((o) => !o), slashCommands: slashCommands, onPickSlash: (name) => { if (runSlash(name) !== 'prefilled')
474
+ setInput(''); }, attachments: attachments, onAttach: (urls) => setAttachments((a) => [...a, ...urls]), onRemoveAttachment: (i) => setAttachments((a) => a.filter((_, j) => j !== i)), onImageClick: setLightboxSrc, inPrice: inPrice, onDropFiles: (files) => void processFiles(files) }), lightboxSrc && _jsx(Lightbox, { src: lightboxSrc, onClose: () => setLightboxSrc(null) })] }));
475
+ }
476
+ // Session-row style is keyed by "active" — kept out of S (which is a flat style
477
+ // map) and typed as its own helper.
478
+ const menuSession = (active) => ({ flex: 1, textAlign: 'left', padding: '7px 9px', border: 0, borderRadius: 'var(--radius-sm)', background: active ? 'var(--accent-soft)' : 'transparent', color: active ? 'var(--accent)' : 'var(--text-secondary)', fontSize: 13, cursor: 'pointer', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' });
479
+ const S = {
480
+ panel: { position: 'relative', display: 'flex', flexDirection: 'column', flex: 1, width: '100%', minWidth: 0, height: '100%', background: 'var(--surface-1)', overflow: 'hidden' },
481
+ head: { position: 'relative', display: 'flex', alignItems: 'center', flexWrap: 'wrap', gap: 6, rowGap: 6, minHeight: 48, padding: '7px 10px', borderBottom: '1px solid var(--border-subtle)', flexShrink: 0 },
482
+ sessionBtn: { display: 'inline-flex', alignItems: 'center', gap: 7, height: 30, padding: '0 10px', borderRadius: 'var(--radius-sm)', background: 'var(--surface-2)', border: '1px solid var(--border-subtle)', color: 'var(--text-secondary)', cursor: 'pointer', maxWidth: 220, minWidth: 0, flexShrink: 1 },
483
+ runChip: { display: 'inline-flex', alignItems: 'center', gap: 6, marginLeft: 8, height: 22, padding: '0 9px', borderRadius: 'var(--radius-pill, 999px)', background: 'var(--accent-soft)', border: '1px solid var(--border-accent)', color: 'var(--accent)', fontSize: 11.5, fontWeight: 600, fontVariantNumeric: 'tabular-nums', whiteSpace: 'nowrap' },
484
+ sessionName: { fontSize: 13, fontWeight: 600, color: 'var(--text-primary)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' },
485
+ iconBtn: { display: 'inline-flex', alignItems: 'center', justifyContent: 'center', width: 30, height: 30, border: 0, borderRadius: 'var(--radius-sm)', background: 'transparent', color: 'var(--text-tertiary)', cursor: 'pointer' },
486
+ iconBtnActive: { background: 'var(--accent-soft)', color: 'var(--accent)' },
487
+ menu: { position: 'absolute', top: 'calc(100% + 4px)', left: 10, width: 280, maxWidth: 'calc(100vw - 20px)', background: 'var(--surface-1)', border: '1px solid var(--border-default)', borderRadius: 'var(--radius-lg)', boxShadow: 'var(--shadow-4)', padding: 6, zIndex: 200 },
488
+ menuItem: { display: 'flex', alignItems: 'center', gap: 8, width: '100%', padding: '8px 9px', border: 0, borderRadius: 'var(--radius-sm)', background: 'transparent', color: 'var(--text-primary)', cursor: 'pointer', fontSize: 13, fontWeight: 500 },
489
+ menuLabel: { fontSize: 'var(--fs-eyebrow)', fontWeight: 600, textTransform: 'uppercase', letterSpacing: '0.08em', color: 'var(--text-tertiary)', padding: '8px 9px 4px' },
490
+ menuEmpty: { padding: '4px 9px 8px', fontSize: 12.5, color: 'var(--text-disabled)' },
491
+ menuRow: { display: 'flex', alignItems: 'center', gap: 4 },
492
+ menuDel: { display: 'inline-flex', width: 26, height: 26, alignItems: 'center', justifyContent: 'center', border: 0, borderRadius: 'var(--radius-sm)', background: 'transparent', color: 'var(--text-disabled)', cursor: 'pointer' },
493
+ renameInput: { flex: 1, minWidth: 0, maxWidth: 240, height: 28, padding: '0 9px', boxSizing: 'border-box', borderRadius: 'var(--radius-sm)', background: 'var(--surface-1)', border: '1px solid var(--border-accent)', color: 'var(--text-primary)', fontSize: 13, fontWeight: 600, fontFamily: 'inherit', outline: 'none' },
494
+ chatRegion: { position: 'relative', flex: 1, minHeight: 0, display: 'flex', flexDirection: 'column' },
495
+ body: { flex: 1, minHeight: 0, overflowY: 'auto', padding: 14, display: 'flex', flexDirection: 'column', gap: 16 },
496
+ // Wraps the settled Transcript + live-run block so a host-injected LiveTimeline
497
+ // (position: absolute) can overlay them while scrubbed back. `body` already
498
+ // owns the scroll (overflowY: auto) — this is just the positioning context.
499
+ transcriptWrap: { position: 'relative', display: 'flex', flexDirection: 'column', gap: 16 },
500
+ live: { display: 'flex', flexDirection: 'column', gap: 8 },
501
+ liveToolRow: { display: 'flex', alignItems: 'center', padding: '6px 10px', border: '1px solid var(--border-subtle)', borderRadius: 'var(--radius-md)', background: 'var(--surface-2)' },
502
+ thinkingWrap: { display: 'flex', flexDirection: 'column', gap: 6 },
503
+ thinking: { display: 'inline-flex', alignItems: 'center', gap: 6, color: 'var(--text-tertiary)', fontSize: 13 },
504
+ recovery: { display: 'inline-flex', alignItems: 'center', gap: 7, color: 'var(--warning)', fontSize: 13, marginBottom: 6 },
505
+ tipGhost: { fontSize: 12, color: 'var(--text-disabled)', fontStyle: 'italic', opacity: 0.7, paddingLeft: 20, maxWidth: 320, lineHeight: 1.4 },
506
+ error: { fontSize: 12.5, color: 'var(--danger)', background: 'var(--danger-soft)', border: '1px solid var(--danger)', borderRadius: 'var(--radius-sm)', padding: '8px 10px' },
507
+ // Follow-up chips under the latest reply — click to prefill the composer.
508
+ followRow: { display: 'flex', flexWrap: 'wrap', gap: 6, marginTop: 4 },
509
+ followChip: { display: 'inline-flex', alignItems: 'center', maxWidth: '100%', padding: '5px 10px', border: '1px solid var(--border-subtle)', borderRadius: 'var(--radius-pill)', background: 'transparent', color: 'var(--text-secondary)', fontSize: 12.5, lineHeight: 1.3, textAlign: 'left', cursor: 'pointer' },
510
+ policyWarn: { display: 'flex', alignItems: 'center', gap: 7, fontSize: 12.5, color: 'var(--warning)', background: 'var(--warning-soft)', border: '1px solid var(--warning)', borderRadius: 'var(--radius-sm)', padding: '8px 10px' },
511
+ policyStop: { display: 'flex', alignItems: 'center', gap: 7, fontSize: 12.5, color: 'var(--danger)', background: 'var(--danger-soft)', border: '1px solid var(--danger)', borderRadius: 'var(--radius-sm)', padding: '8px 10px' },
512
+ ctxMenu: { margin: '0 10px', padding: 8, background: 'var(--surface-2)', border: '1px solid var(--border-default)', borderRadius: 'var(--radius-md)', display: 'flex', flexDirection: 'column', gap: 4 },
513
+ perm: { border: '1px solid var(--warning)', borderRadius: 'var(--radius-md)', background: 'var(--warning-soft)', padding: 12, display: 'flex', flexDirection: 'column', gap: 8 },
514
+ permHead: { display: 'inline-flex', alignItems: 'center', gap: 7, fontSize: 13, fontWeight: 600, color: 'var(--text-primary)' },
515
+ permBody: { fontSize: 13, lineHeight: 1.5, color: 'var(--text-secondary)' },
516
+ permActions: { display: 'flex', gap: 8, flexWrap: 'wrap' },
517
+ permDeny: { height: 30, padding: '0 12px', border: '1px solid var(--border-default)', borderRadius: 'var(--radius-sm)', background: 'var(--surface-1)', color: 'var(--text-secondary)', fontSize: 12.5, fontWeight: 600, cursor: 'pointer' },
518
+ permAllowSession: { height: 30, padding: '0 12px', border: '1px solid var(--border-default)', borderRadius: 'var(--radius-sm)', background: 'var(--surface-1)', color: 'var(--text-secondary)', fontSize: 12.5, fontWeight: 600, cursor: 'pointer' },
519
+ permAllow: { display: 'inline-flex', alignItems: 'center', gap: 5, height: 30, padding: '0 14px', border: 0, borderRadius: 'var(--radius-sm)', background: 'var(--grad-brand)', color: '#fff', fontSize: 12.5, fontWeight: 600, cursor: 'pointer' },
520
+ // Per-run budget control — the run chip's soft note when a degrade breach
521
+ // switched the in-flight model.
522
+ budgetWrap: { position: 'relative', display: 'flex', alignItems: 'center', gap: 8, padding: '0 10px 6px', flexShrink: 0 },
523
+ budgetDegraded: { fontSize: 11, color: 'var(--warning)' },
524
+ uploadNotice: { display: 'flex', alignItems: 'center', gap: 8, margin: '0 10px 6px', padding: '7px 10px', border: '1px solid var(--warning)', borderRadius: 'var(--radius-sm)', background: 'var(--warning-soft)', color: 'var(--warning)', fontSize: 12.5 },
525
+ uploadNoticeX: { display: 'inline-flex', alignItems: 'center', justifyContent: 'center', width: 20, height: 20, border: 0, borderRadius: 'var(--radius-sm)', background: 'transparent', color: 'inherit', cursor: 'pointer', flexShrink: 0 },
526
+ };
527
+ //# sourceMappingURL=AgentChat.js.map