@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.
- package/dist/AskUser.d.ts.map +1 -1
- package/dist/AskUser.js +49 -13
- package/dist/AskUser.js.map +1 -1
- package/dist/CommandTray.d.ts +21 -0
- package/dist/CommandTray.d.ts.map +1 -0
- package/dist/CommandTray.js +102 -0
- package/dist/CommandTray.js.map +1 -0
- package/dist/Composer.d.ts +17 -1
- package/dist/Composer.d.ts.map +1 -1
- package/dist/Composer.js +93 -6
- package/dist/Composer.js.map +1 -1
- package/dist/ContextBar.d.ts +9 -1
- package/dist/ContextBar.d.ts.map +1 -1
- package/dist/ContextBar.js +9 -3
- package/dist/ContextBar.js.map +1 -1
- package/dist/ContextManager.d.ts +78 -2
- package/dist/ContextManager.d.ts.map +1 -1
- package/dist/ContextManager.js +175 -51
- package/dist/ContextManager.js.map +1 -1
- package/dist/Lightbox.d.ts.map +1 -1
- package/dist/Lightbox.js +2 -10
- package/dist/Lightbox.js.map +1 -1
- package/dist/ProcessDock.d.ts +45 -0
- package/dist/ProcessDock.d.ts.map +1 -0
- package/dist/ProcessDock.js +150 -0
- package/dist/ProcessDock.js.map +1 -0
- package/dist/RichContent.d.ts +4 -2
- package/dist/RichContent.d.ts.map +1 -1
- package/dist/RichContent.js +12 -7
- package/dist/RichContent.js.map +1 -1
- package/dist/TodoBar.d.ts +3 -0
- package/dist/TodoBar.d.ts.map +1 -0
- package/dist/TodoBar.js +163 -0
- package/dist/TodoBar.js.map +1 -0
- package/dist/Transcript.d.ts +2 -1
- package/dist/Transcript.d.ts.map +1 -1
- package/dist/Transcript.js +7 -7
- package/dist/Transcript.js.map +1 -1
- package/dist/agentChat/AgentChat.d.ts +132 -0
- package/dist/agentChat/AgentChat.d.ts.map +1 -0
- package/dist/agentChat/AgentChat.js +588 -0
- package/dist/agentChat/AgentChat.js.map +1 -0
- package/dist/agentChat/OptionsDrawer.d.ts +81 -0
- package/dist/agentChat/OptionsDrawer.d.ts.map +1 -0
- package/dist/agentChat/OptionsDrawer.js +321 -0
- package/dist/agentChat/OptionsDrawer.js.map +1 -0
- package/dist/agentChat/SkillsSection.d.ts +3 -0
- package/dist/agentChat/SkillsSection.d.ts.map +1 -0
- package/dist/agentChat/SkillsSection.js +86 -0
- package/dist/agentChat/SkillsSection.js.map +1 -0
- package/dist/agentChat/controller.d.ts +116 -0
- package/dist/agentChat/controller.d.ts.map +1 -0
- package/dist/agentChat/controller.js +25 -0
- package/dist/agentChat/controller.js.map +1 -0
- package/dist/agentChat/skillRunner.d.ts +14 -0
- package/dist/agentChat/skillRunner.d.ts.map +1 -0
- package/dist/agentChat/skillRunner.js +4 -0
- package/dist/agentChat/skillRunner.js.map +1 -0
- package/dist/commandTurn.d.ts +47 -0
- package/dist/commandTurn.d.ts.map +1 -0
- package/dist/commandTurn.js +64 -0
- package/dist/commandTurn.js.map +1 -0
- package/dist/composerHistory.d.ts +10 -0
- package/dist/composerHistory.d.ts.map +1 -0
- package/dist/composerHistory.js +44 -0
- package/dist/composerHistory.js.map +1 -0
- package/dist/sendLater/SendLater.d.ts +31 -0
- package/dist/sendLater/SendLater.d.ts.map +1 -0
- package/dist/sendLater/SendLater.js +145 -0
- package/dist/sendLater/SendLater.js.map +1 -0
- package/dist/sendLater/queue.d.ts +69 -0
- package/dist/sendLater/queue.d.ts.map +1 -0
- package/dist/sendLater/queue.js +216 -0
- package/dist/sendLater/queue.js.map +1 -0
- package/dist/toolRender.d.ts +8 -0
- package/dist/toolRender.d.ts.map +1 -1
- package/dist/toolRender.js +7 -1
- package/dist/toolRender.js.map +1 -1
- 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
|