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