@corenel/web-ui 0.10.0 → 0.11.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/Composer.d.ts +30 -9
- package/dist/Composer.d.ts.map +1 -1
- package/dist/Composer.js +288 -230
- package/dist/Composer.js.map +1 -1
- package/dist/ContextBar.d.ts +4 -1
- package/dist/ContextBar.d.ts.map +1 -1
- package/dist/ContextBar.js +15 -2
- package/dist/ContextBar.js.map +1 -1
- package/dist/DictateButton.d.ts +26 -1
- package/dist/DictateButton.d.ts.map +1 -1
- package/dist/DictateButton.js +26 -10
- package/dist/DictateButton.js.map +1 -1
- package/dist/DockStatus.d.ts +11 -0
- package/dist/DockStatus.d.ts.map +1 -0
- package/dist/DockStatus.js +41 -0
- package/dist/DockStatus.js.map +1 -0
- package/dist/StarterCards.d.ts +20 -0
- package/dist/StarterCards.d.ts.map +1 -0
- package/dist/StarterCards.js +30 -0
- package/dist/StarterCards.js.map +1 -0
- package/dist/Transcript.d.ts +9 -1
- package/dist/Transcript.d.ts.map +1 -1
- package/dist/Transcript.js +11 -2
- package/dist/Transcript.js.map +1 -1
- package/dist/agentChat/AgentChat.d.ts +43 -7
- package/dist/agentChat/AgentChat.d.ts.map +1 -1
- package/dist/agentChat/AgentChat.js +220 -93
- package/dist/agentChat/AgentChat.js.map +1 -1
- package/dist/agentChat/OptionsDrawer.d.ts.map +1 -1
- package/dist/agentChat/OptionsDrawer.js +5 -2
- package/dist/agentChat/OptionsDrawer.js.map +1 -1
- package/dist/agentChat/controller.d.ts +3 -0
- package/dist/agentChat/controller.d.ts.map +1 -1
- package/dist/agentChat/controller.js.map +1 -1
- package/dist/credentialLines.d.ts +10 -0
- package/dist/credentialLines.d.ts.map +1 -0
- package/dist/credentialLines.js +46 -0
- package/dist/credentialLines.js.map +1 -0
- package/dist/sendLater/SendLater.d.ts +8 -1
- package/dist/sendLater/SendLater.d.ts.map +1 -1
- package/dist/sendLater/SendLater.js +16 -8
- package/dist/sendLater/SendLater.js.map +1 -1
- package/dist/toolRender.d.ts.map +1 -1
- package/dist/toolRender.js +7 -2
- package/dist/toolRender.js.map +1 -1
- package/package.json +5 -5
|
@@ -37,9 +37,12 @@ import { isDeferredTool } from '@corenel/harness/tools/toolSearch';
|
|
|
37
37
|
import { addLoadedTools } from '@corenel/tools-web/sessionLoadedTools';
|
|
38
38
|
import { useContextDocs, addDoc, removeDoc, classifyUpload, looksBinary, ContextDocError, UPLOAD_ACCEPT } from '@corenel/tools-web/contextDocs';
|
|
39
39
|
import { DictateButton } from '../DictateButton';
|
|
40
|
+
import { resolveDictation } from '@corenel/tools-web/speech/route';
|
|
40
41
|
import { browserModelHost } from '@corenel/tools-web/speech/model';
|
|
41
42
|
import { appendDictated } from './appendDictated';
|
|
42
43
|
import { TodoBar } from '../TodoBar';
|
|
44
|
+
import { DockStatus } from '../DockStatus';
|
|
45
|
+
import { useTodos } from '@corenel/harness/todos';
|
|
43
46
|
import { CommandTray, RunningCommand } from '../CommandTray';
|
|
44
47
|
import { trayCommand, transcriptTurns } from '../commandTurn';
|
|
45
48
|
import { composerHistory } from '../composerHistory';
|
|
@@ -48,7 +51,7 @@ import { fmtTok } from '@corenel/ui-kit/usage';
|
|
|
48
51
|
import { isCheckpointTool } from '@corenel/protocol';
|
|
49
52
|
import { PlanApprovalCard, SUBMIT_PLAN, planTextOf } from './PlanReview';
|
|
50
53
|
import { useBrand } from '@corenel/ui-kit/host';
|
|
51
|
-
import { IcPlus, IcZap, IcTrash, IcChevron, IcX, IcRefresh, IcKey, IcCheck, IcSliders, IcUser, IcFile, IcHelp, IcEdit, IcLayout, Spinner, LiveCursor } from '@corenel/ui-kit/icons';
|
|
54
|
+
import { IcPlus, IcZap, IcTrash, IcChevron, IcX, IcRefresh, IcKey, IcCheck, IcSliders, IcUser, IcFile, IcHelp, IcEdit, IcLayout, IcCode, IcMic, IcClock, Spinner, LiveCursor } from '@corenel/ui-kit/icons';
|
|
52
55
|
/* No NOOP default for onApplyEdit, deliberately. Transcript treats the prop's
|
|
53
56
|
* ABSENCE as "this surface has nothing to apply an edit to" and renders the
|
|
54
57
|
* review row as a statement instead of dead buttons. Substituting a no-op here
|
|
@@ -79,7 +82,7 @@ const PS = {
|
|
|
79
82
|
textDone: { fontSize: 13, color: 'var(--text-tertiary)', textDecoration: 'line-through' },
|
|
80
83
|
};
|
|
81
84
|
export function AgentChat(props) {
|
|
82
|
-
const { controller, sessionId, onSessionChange, sessions, aboveComposer, contextWindow, systemSource, offloaded, inPrice, modes, currentSrc, onApplyEdit, renderFence, renderToolCard, emptyState, headerLead, headerExtras,
|
|
85
|
+
const { controller, sessionId, onSessionChange, sessions, aboveComposer, contextWindow, systemSource, offloaded, inPrice, modes, currentSrc, onApplyEdit, renderFence, renderToolCard, emptyState, headerLead, headerExtras, composerToggles, dockStatus, title, features, embedded = false, onClose, chips, onRemoveChip, renderContextMenu, capabilities, slashExtra, onSlashAction, interceptSend, onUploadFile, sendOptsExtra, onAfterSend, tip, budgetSection, toolBadge, policyEditor, settingsExtras, LiveTimeline, fixedIdentity, readOnly, planAgents, contentMaxWidth, startLayout = 'docked', startFooter, } = props;
|
|
83
86
|
const { name: brandName } = useBrand(); // host-injected product name (CorenelHost seam)
|
|
84
87
|
// The engine owns the conversation, run loop, worker, and persistence — this
|
|
85
88
|
// column is a view over ONE controller session. The active session id is
|
|
@@ -157,7 +160,15 @@ export function AgentChat(props) {
|
|
|
157
160
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
158
161
|
}, [capabilities?.skills, capabilities?.tools]);
|
|
159
162
|
const [ctxMenuOpen, setCtxMenuOpen] = useState(false);
|
|
163
|
+
/* The mic and send-later live in the "+" menu; these drive them from there.
|
|
164
|
+
* The mic's own Stop shows in the composer whenever it is busy. */
|
|
165
|
+
const dictControl = useRef(null);
|
|
166
|
+
const [laterOpen, setLaterOpen] = useState(false);
|
|
167
|
+
const todosNow = useTodos(sessionId ?? null);
|
|
168
|
+
const [dockEmpty, setDockEmpty] = useState(true);
|
|
169
|
+
const onDockEmpty = useCallback((e) => setDockEmpty(e), []);
|
|
160
170
|
const fileInputRef = useRef(null);
|
|
171
|
+
const panelRef = useRef(null);
|
|
161
172
|
const [uploadNotice, setUploadNotice] = useState(null);
|
|
162
173
|
/* THE DRAFT AS IT STOOD WHEN THE MIC OPENED, and the whole trick behind live
|
|
163
174
|
* dictation writing into the composer.
|
|
@@ -380,7 +391,8 @@ export function AgentChat(props) {
|
|
|
380
391
|
return () => clearInterval(t);
|
|
381
392
|
}, [running]);
|
|
382
393
|
const newSession = async () => {
|
|
383
|
-
|
|
394
|
+
// A draft: nothing is saved, or listed, until the first send.
|
|
395
|
+
const id = await (controller.newDraft ? controller.newDraft() : controller.newSession());
|
|
384
396
|
onSessionChange(id);
|
|
385
397
|
refreshSessions();
|
|
386
398
|
setMenuOpen(false);
|
|
@@ -525,7 +537,7 @@ export function AgentChat(props) {
|
|
|
525
537
|
void (async () => {
|
|
526
538
|
if (!text || running)
|
|
527
539
|
return;
|
|
528
|
-
if (interceptSend && (await interceptSend(text)))
|
|
540
|
+
if (interceptSend && (await interceptSend(text, [])))
|
|
529
541
|
return;
|
|
530
542
|
await sendMessage(text, []);
|
|
531
543
|
})();
|
|
@@ -633,12 +645,19 @@ export function AgentChat(props) {
|
|
|
633
645
|
if (interceptSend) {
|
|
634
646
|
const prior = input;
|
|
635
647
|
setInput('');
|
|
636
|
-
|
|
648
|
+
// The attachments go with the text, both ways: cleared now, restored if
|
|
649
|
+
// declined. Left behind, an image-mode send's screenshot sat in the
|
|
650
|
+
// composer after the image it was attached to had been made.
|
|
651
|
+
if (images.length)
|
|
652
|
+
setAttachments(() => []);
|
|
653
|
+
if (await interceptSend(text, images))
|
|
637
654
|
return;
|
|
638
655
|
// Declined: it was never ours to take. Restoring matters most for the
|
|
639
656
|
// slash path below, where a 'prefilled' template is left in the composer
|
|
640
657
|
// for review and an optimistic clear would have wiped it.
|
|
641
658
|
setInput(prior);
|
|
659
|
+
if (images.length)
|
|
660
|
+
setAttachments(() => images);
|
|
642
661
|
}
|
|
643
662
|
if (text.startsWith('/') && images.length === 0) {
|
|
644
663
|
const [cmd, ...rest] = text.slice(1).split(' ');
|
|
@@ -852,6 +871,11 @@ export function AgentChat(props) {
|
|
|
852
871
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
853
872
|
}), [sessionId, session.turns]);
|
|
854
873
|
const menuEnabled = features?.sessionMenu !== false && !readOnly;
|
|
874
|
+
// Full-bleed surface, capped content: the panel and skin paint fill the stage
|
|
875
|
+
// while the header row, body content and dock share one centred column.
|
|
876
|
+
const cap = contentMaxWidth
|
|
877
|
+
? { width: '100%', maxWidth: contentMaxWidth, marginInline: 'auto', boxSizing: 'border-box' }
|
|
878
|
+
: null;
|
|
855
879
|
// Defaults true: every existing surface runs our loop, and only a proxied
|
|
856
880
|
// engine opts out.
|
|
857
881
|
const ownLoop = features?.ownLoop !== false;
|
|
@@ -860,9 +884,10 @@ export function AgentChat(props) {
|
|
|
860
884
|
// A host card per live tool, built from the same call shape the settled
|
|
861
885
|
// transcript hands renderToolCard, so a card does not change on settle. A
|
|
862
886
|
// non-null card wins in every skin and effect: never folded, never a board row.
|
|
887
|
+
// The result goes along once the tool is done, as the settled transcript does.
|
|
863
888
|
const liveCards = new Map();
|
|
864
889
|
for (const t of live?.tools ?? []) {
|
|
865
|
-
const card = renderToolCard?.({ id: t.id, type: 'function', function: { name: t.name, arguments: JSON.stringify(t.args || {}) } });
|
|
890
|
+
const card = renderToolCard?.({ id: t.id, type: 'function', function: { name: t.name, arguments: JSON.stringify(t.args || {}) } }, t.done && t.result !== undefined ? { text: t.result, isError: !!t.isError } : undefined);
|
|
866
891
|
if (card)
|
|
867
892
|
liveCards.set(t.id, card);
|
|
868
893
|
}
|
|
@@ -877,103 +902,205 @@ export function AgentChat(props) {
|
|
|
877
902
|
const skinReach = SKIN_REACH[transcriptSkin];
|
|
878
903
|
const headerOutside = skinReach.chat && !skinReach.header;
|
|
879
904
|
const chatWords = skinReach.chat ? CHAT_WORDS[transcriptSkin] ?? {} : {};
|
|
880
|
-
const
|
|
905
|
+
const HeadRow = cap ? 'div' : 'header';
|
|
906
|
+
const headerRow = (_jsxs(HeadRow, { style: cap ? { ...S.head, ...cap } : S.head, "data-chat-part": "header", "data-chat-running": running ? '' : undefined, ...(cap ? { 'data-content-cap': '' } : null), 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) {
|
|
881
907
|
e.stopPropagation();
|
|
882
908
|
startRename(sessionId);
|
|
883
|
-
} }, title: sessionId ? 'Double-click to rename' : undefined, children: [_jsx(IcZap, { size: 14 }), _jsx("span", { style: S.sessionName, children: sessionTitle }), _jsx(IcChevron, { size: 12 })] })),
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
909
|
+
} }, title: sessionId ? 'Double-click to rename' : undefined, children: [_jsx(IcZap, { size: 14 }), _jsx("span", { style: S.sessionName, children: sessionTitle }), _jsx(IcChevron, { size: 12 })] })), _jsx("div", { style: { flex: 1, minWidth: 8 } }), headerExtras, ownLoop && !readOnly && _jsx(ContextBar, { variant: "inline", contextWindow: contextWindow, systemSource: resolveSystemSource(), offloaded: offloaded, conversation: conversation, sessionId: sessionId ?? '' }), !readOnly && _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)))] }))] }));
|
|
910
|
+
const isPhone = useMatchMedia('(max-width: 640px)');
|
|
911
|
+
const switchRef = useRef(switchSession);
|
|
912
|
+
switchRef.current = switchSession;
|
|
913
|
+
const lastSession = useMemo(() => {
|
|
914
|
+
const best = sessionList
|
|
915
|
+
.filter((s) => s.id !== sessionId && typeof s.updatedAt === 'number' && (s.messages === undefined || s.messages.length > 0))
|
|
916
|
+
.sort((a, b) => (b.updatedAt ?? 0) - (a.updatedAt ?? 0))[0];
|
|
917
|
+
return best ? { title: best.title || 'Untitled', updatedAt: best.updatedAt ?? 0, open: () => switchRef.current(best.id) } : undefined;
|
|
918
|
+
}, [sessionList, sessionId]);
|
|
919
|
+
const showEmpty = turns.length === 0 && !live && loaded !== false && !readOnly;
|
|
920
|
+
const centered = startLayout === 'centered' && showEmpty && !isPhone;
|
|
921
|
+
const wasCentered = useRef(centered);
|
|
922
|
+
/* Moving the dock between the centred start and the panel foot REMOUNTS the
|
|
923
|
+
* composer (a new parent), so whatever focus it held is gone by the time any
|
|
924
|
+
* effect runs. Whether it had focus is read here, during the render that
|
|
925
|
+
* flips `centered` and before React touches the DOM; the old textarea is
|
|
926
|
+
* still the one in the document. */
|
|
927
|
+
const composerHadFocus = useRef(false);
|
|
928
|
+
if (centered !== wasCentered.current && typeof document !== 'undefined') {
|
|
929
|
+
const old = panelRef.current?.querySelector('[data-chat-part="composer"] textarea');
|
|
930
|
+
composerHadFocus.current = !!old && old === document.activeElement;
|
|
931
|
+
}
|
|
932
|
+
useEffect(() => {
|
|
933
|
+
const focusComposer = () => requestAnimationFrame(() => panelRef.current?.querySelector('[data-chat-part="composer"] textarea')?.focus());
|
|
934
|
+
// Centred -> docked once the chat has turns: the first send, and also a
|
|
935
|
+
// session switch into a chat that already has turns. Both put the caret in
|
|
936
|
+
// the box, intentionally -- the next thing you do is type.
|
|
937
|
+
if (wasCentered.current && !centered && (turns.length > 0 || live))
|
|
938
|
+
focusComposer();
|
|
939
|
+
// Docked -> centred (a new or emptied chat): keep the caret only if it was
|
|
940
|
+
// already in the composer; never steal focus from elsewhere on the page.
|
|
941
|
+
else if (!wasCentered.current && centered && composerHadFocus.current)
|
|
942
|
+
focusComposer();
|
|
943
|
+
wasCentered.current = centered;
|
|
944
|
+
composerHadFocus.current = false;
|
|
945
|
+
// turns/live are read only on the centred -> docked transition itself.
|
|
946
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
947
|
+
}, [centered]);
|
|
948
|
+
/* Escape closes the "+" menu and hands focus back to the "+" button that
|
|
949
|
+
* opened it, the way any menu behaves. Listening only while it is open. */
|
|
950
|
+
useEffect(() => {
|
|
951
|
+
if (!ctxMenuOpen)
|
|
952
|
+
return;
|
|
953
|
+
const onKey = (e) => {
|
|
954
|
+
if (e.key !== 'Escape')
|
|
955
|
+
return;
|
|
956
|
+
setCtxMenuOpen(false);
|
|
957
|
+
panelRef.current?.querySelector('[data-testid="composer-add"]')?.focus();
|
|
958
|
+
};
|
|
959
|
+
document.addEventListener('keydown', onKey);
|
|
960
|
+
return () => document.removeEventListener('keydown', onKey);
|
|
961
|
+
}, [ctxMenuOpen]);
|
|
962
|
+
const headerEl = cap ? _jsx("header", { style: S.headOuter, children: headerRow }) : headerRow;
|
|
963
|
+
const composerEl = (_jsx(Composer, { value: input, onChange: setInput, docked: !centered,
|
|
964
|
+
// The mic goes in the bottom row's FIXED first position, not the chip
|
|
965
|
+
// row. It was in the chip row first, and TodoBar shoved it sideways
|
|
966
|
+
// every time the agent's current task changed -- a control that moves
|
|
967
|
+
// while the agent works is a control people misclick, and this one's
|
|
968
|
+
// worst misclick is "I thought I stopped recording". It stays in the
|
|
969
|
+
// composer either way, which is what keeps it reachable on a phone
|
|
970
|
+
// (the chat header is not -- see composerSlot.test.ts).
|
|
971
|
+
//
|
|
972
|
+
// Rendered here rather than passed in by each host: dictation belongs to
|
|
973
|
+
// the agent chat, not to any one surface, and four separate wirings
|
|
974
|
+
// would mean four chances to ship a surface that is silently mute.
|
|
975
|
+
dictating: dictating, leadSlot: _jsx(DictateButton, { host: browserModelHost(),
|
|
976
|
+
// Started from the "+" menu; shows itself (Stop, progress, consent,
|
|
977
|
+
// errors) only while busy, always in this same first slot.
|
|
978
|
+
hideWhenIdle: true, controlRef: dictControl,
|
|
979
|
+
// Browser or paired sidecar, chosen when a dictation starts.
|
|
980
|
+
backend: () => { const r = resolveDictation(); return { host: r.host, createDictation: r.createDictation, supported: r.where !== 'none' }; },
|
|
981
|
+
// Capture the draft the moment the mic opens; every guess below joins
|
|
982
|
+
// onto THIS, never onto the previous guess.
|
|
983
|
+
onListeningChange: (on) => { if (on)
|
|
984
|
+
dictationBase.current = inputRef.current; setDictating(on); },
|
|
985
|
+
/* Live guesses go straight into the draft -- that is the feature.
|
|
986
|
+
EMPTY IS IGNORED, and that is load-bearing: DictateButton clears the
|
|
987
|
+
guess with onInterim('') AFTER stop() resolves, which is after the
|
|
988
|
+
final transcript has already landed. Honouring it would recompute
|
|
989
|
+
`base + ''` and wipe the committed text a beat after showing it. */
|
|
990
|
+
onInterim: (t) => { if (t)
|
|
991
|
+
setInput(appendDictated(dictationBase.current, t)); }, onFinal: (t) => setInput(appendDictated(dictationBase.current, t)) }), onSend: send, onStop: () => { stoppedRef.current = true; if (sessionId)
|
|
992
|
+
controller.stop(sessionId); }, running: running, mode: mode, onModeChange: setMode, modes: modeEntries, chips: [
|
|
993
|
+
...(chips ?? []),
|
|
994
|
+
...contextDocs.map((d) => ({ id: `doc:${d.id}`, label: `${d.name} · ${fmtTok(d.tokens)} tok`, removable: true })),
|
|
995
|
+
], onRemoveChip: (cid) => {
|
|
996
|
+
if (cid.startsWith('doc:')) {
|
|
997
|
+
if (sessionId)
|
|
998
|
+
removeDoc(sessionId, cid.slice(4));
|
|
999
|
+
}
|
|
1000
|
+
else
|
|
1001
|
+
onRemoveChip?.(cid);
|
|
1002
|
+
}, onAddContext: () => setCtxMenuOpen((o) => !o),
|
|
1003
|
+
// The agent's plan, above the composer. prompd.app also has a dock panel
|
|
1004
|
+
// for it; on corenel.ai this is the ONLY place it is visible. Renders
|
|
1005
|
+
// nothing when there are no tasks.
|
|
1006
|
+
// The host's own controls sit BEFORE the task list, so their position
|
|
1007
|
+
// stays put: TodoBar renders nothing when there are no tasks, and a
|
|
1008
|
+
// control that moves as the agent starts and finishes work is a
|
|
1009
|
+
// control people misclick.
|
|
1010
|
+
toggleChips: (composerToggles ?? []).filter((t) => t.on).map((t) => ({
|
|
1011
|
+
id: t.id, label: t.label, icon: t.icon, ...(t.title ? { title: t.title } : {}), onRemove: t.onToggle,
|
|
1012
|
+
})), status: (running || todosNow.length > 0 || degradedModel || dockStatus != null) ? (_jsx(DockStatus, { running: running, elapsed: runElapsed, todos: _jsx(TodoBar, { sessionId: sessionId }), hasTodos: todosNow.length > 0, degradedModel: degradedModel, host: dockStatus, onEmptyChange: onDockEmpty })) : undefined, statusHidden: dockEmpty && !running && todosNow.length === 0 && !degradedModel && pending.length === 0, slashCommands: slashCommands, onPickSlash: (name) => { if (runSlash(name) !== 'prefilled')
|
|
1013
|
+
setInput(''); }, attachments: attachments, onAttach: (urls) => setAttachments((a) => [...a, ...urls]), onRemoveAttachment: (i) => setAttachments((a) => a.filter((_, j) => j !== i)), pending: pending, onQueue: queueCurrent, onInject: injectPending, onRemovePending: (id) => { if (sessionId)
|
|
1014
|
+
removePending(sessionId, id); }, onEditPending: (id, text) => { if (sessionId)
|
|
1015
|
+
editPending(sessionId, id, text); }, onImageClick: setLightboxSrc, inPrice: inPrice, onDropFiles: (files) => void processFiles(files), history: history, sendLater: (_jsx(SendLater, { collapsed: true, open: laterOpen, onOpenChange: setLaterOpen, text: input, onQueued: () => setInput(''), onSendNow: (t) => { void sendMessage(t, []); }, items: laterItems, now: laterNow })) }));
|
|
1016
|
+
/* Everything that sits directly above the dock: notices, a pending question,
|
|
1017
|
+
* the host's aboveComposer, a command in flight and the console tray. It
|
|
1018
|
+
* travels WITH the dock -- into the centred start when the chat is empty,
|
|
1019
|
+
* at the panel foot otherwise -- so it is never stranded far from the box
|
|
1020
|
+
* it belongs to. The read-only bar is not part of it (centred never happens
|
|
1021
|
+
* in read-only). */
|
|
1022
|
+
const noticesEl = (_jsxs(_Fragment, { children: [saveBlocked === 'load-failed' && (_jsxs("div", { style: S.uploadNotice, role: "alert", "data-chat-part": "unsaved", children: [_jsx("span", { style: { flex: 1 }, children: "New messages in this chat are not being saved." }), controller.retryLoad && (_jsx("button", { type: "button", style: S.unsavedRetry, disabled: retrying, onClick: () => { void retryLoad(); }, children: retrying ? 'Retrying...' : 'Retry' }))] })), mergedNotice && (_jsxs("div", { style: S.uploadNotice, role: "status", "data-chat-part": "merged", children: [_jsx("span", { style: { flex: 1 }, children: "Another tab changed this chat; its turns were merged in." }), _jsx("button", { type: "button", style: S.uploadNoticeX, onClick: () => setMergedNotice(false), "aria-label": "Dismiss", children: _jsx(IcX, { size: 11 }) })] })), 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)
|
|
1023
|
+
controller.resolveAsk(sessionId, answers); }, onCancel: () => { if (sessionId)
|
|
1024
|
+
controller.resolveAsk(sessionId, null); } })), readOnly ? null : (_jsxs(_Fragment, { children: [aboveComposer, pendingCommand && _jsx(RunningCommand, { command: pendingCommand.command, startedAt: pendingCommand.startedAt }), _jsx(CommandTray, { command: tray, onKeep: (turnId) => { if (sessionId)
|
|
1025
|
+
controller.toggleInclude(sessionId, turnId); }, onDismiss: (turnId) => setDismissed((d) => new Set(d).add(turnId)) })] }))] }));
|
|
1026
|
+
const dockEl = (_jsxs("div", { style: S.dockWrap, children: [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"] }), slashCommands.length > 0 && (_jsxs("button", { style: S.menuItem, onClick: () => {
|
|
1027
|
+
setCtxMenuOpen(false);
|
|
1028
|
+
setInput('/');
|
|
1029
|
+
requestAnimationFrame(() => panelRef.current?.querySelector('[data-chat-part="composer"] textarea')?.focus());
|
|
1030
|
+
}, children: [_jsx(IcCode, { size: 13 }), " Commands"] })), _jsx("div", { style: S.menuLabel, children: "Tools" }), (() => {
|
|
1031
|
+
const route = resolveDictation();
|
|
1032
|
+
const label = dictating ? 'Stop dictating'
|
|
1033
|
+
: route.where === 'sidecar' ? (route.tunnel ? 'Dictate (on your sidecar, via tunnel)' : 'Dictate (on your sidecar)')
|
|
1034
|
+
: 'Dictate';
|
|
1035
|
+
const unsupported = dictControl.current?.supported === false;
|
|
1036
|
+
return (_jsxs("button", { style: S.menuItem, disabled: !dictating && (route.where === 'none' || unsupported), title: dictating ? undefined : route.where === 'none' ? route.reason
|
|
1037
|
+
: unsupported ? 'Voice input needs a microphone and a browser that supports one.' : undefined, onClick: () => { setCtxMenuOpen(false); void dictControl.current?.toggle(); }, children: [_jsx(IcMic, { size: 13 }), " ", label] }));
|
|
1038
|
+
})(), _jsxs("button", { style: S.menuItem, disabled: running, title: running ? 'Available when this run finishes' : undefined, onClick: () => { setCtxMenuOpen(false); setLaterOpen(true); }, children: [_jsx(IcClock, { size: 13 }), " Send later\u2026"] }), (composerToggles ?? []).map((t) => (_jsxs("button", { role: "menuitemcheckbox", "aria-checked": t.on, style: S.menuItem, title: t.title, onClick: () => { setCtxMenuOpen(false); t.onToggle(); }, children: [t.icon, " ", _jsx("span", { style: { flex: 1, textAlign: 'left' }, children: t.label }), t.on && _jsx(IcCheck, { size: 13 })] }, t.id)))] })), composerEl] }));
|
|
1039
|
+
return (_jsxs("div", { style: S.panel, ref: panelRef, children: [headerOutside ? headerEl : null, _jsxs(ChatSkinRoot, { skin: transcriptSkin, style: S.skinBody, children: [headerOutside ? null : headerEl, ownLoop && _jsx(OptionsDrawer, { open: drawerOpen, onClose: () => setDrawerOpen(false), personaName: personaName, onPersona: pickPersona, policyName: policyName, onPolicy: selectPolicy, hideIdentity: !!fixedIdentity, budgetSection: budgetSection?.({ override, setOverride }), settingsExtras: settingsExtras, toolBadge: toolBadge, policyEditor: policyEditor, registryTick: registryTick, capabilities: capabilities, sessionId: sessionId ?? '' }), _jsxs("div", { style: S.chatRegion, children: [_jsx("div", { style: { ...S.body, ...(timelineScrubbed ? { display: 'none' } : null) }, ref: bodyRef, "data-testid": "chat-body", onScroll: onBodyScroll, children: _jsxs(CapBox, { cap: cap, extra: S.bodyInner, children: [turns.length === 0 && !live && (loaded !== false
|
|
1040
|
+
? (readOnly ? null : (centered ? (_jsxs("div", { style: S.centerStart, "data-testid": "center-start", children: [emptyState?.({ setInput, lastSession }), noticesEl, dockEl, startFooter?.({ setInput, lastSession })] })) : (_jsxs(_Fragment, { children: [emptyState?.({ setInput, lastSession }), startFooter?.({ setInput, lastSession })] }))))
|
|
1041
|
+
: _jsxs("div", { style: S.loadingTranscript, children: [_jsx(Spinner, { size: 14, title: "Loading conversation" }), " Loading conversation\u2026"] })), _jsxs("div", { style: S.transcriptWrap, children: [_jsx(Transcript, { turns: shownTurns, currentSrc: currentSrc ?? '', view: transcriptView, skin: transcriptSkin, handoff: handoff ?? undefined, onHandoffDone: () => setHandoff(null), onApplyEdit: onApplyEdit, onToggleInclude: readOnly ? undefined : toggleInclude, onRetry: readOnly ? undefined : onRetry, onImageClick: setLightboxSrc, renderFence: renderFence, renderToolCard: renderToolCard, footer: live ? (_jsxs("div", { style: fx === 'matrix' ? { ...S.live, position: 'relative', isolation: 'isolate' } : S.live, children: [fx === 'matrix' && _jsx(MatrixRain, { active: running }), recovery && _jsxs("div", { style: S.recovery, children: [_jsx(Spinner, { size: 13, title: "Recovering" }), " ", recovery] }), fx === 'splitflap' && live.tools.filter((t) => t.name !== 'propose_edit' && !liveCards.has(t.id)).map((t) => (_jsx("div", { "data-fx-id": t.id, children: _jsx(FlapRow, { status: t.done ? (t.isError ? 'failed' : 'done') : 'running', name: t.name.replace(/_/g, ' '), detail: Object.values(t.args ?? {}).find((v) => typeof v === 'string') ?? '' }) }, t.id))), fx !== 'splitflap' && transcriptSkin === 'minimal' && (_jsx(ToolFold, { invs: live.tools.filter((t) => t.name !== 'propose_edit' && !liveCards.has(t.id)).map((t) => ({
|
|
1042
|
+
name: t.name, args: t.args || {}, result: t.result ?? '', isError: t.isError, running: !t.done, ms: t.ms,
|
|
1043
|
+
})), view: transcriptView })), live.tools.filter((t) => (fx !== 'splitflap' && transcriptSkin !== 'minimal') || t.name === 'propose_edit' || liveCards.has(t.id)).map((t) => liveCards.has(t.id) ? (
|
|
1044
|
+
// A host card replaces the default card mid-run too.
|
|
1045
|
+
_jsx(React.Fragment, { children: liveCards.get(t.id) }, t.id)) : t.name === 'propose_edit' ? (
|
|
1046
|
+
// propose_edit BLOCKS its run on the review, so the decidable card
|
|
1047
|
+
// must render here, mid-run — not only in the settled transcript.
|
|
1048
|
+
_jsx(DiffProposal, { call: { id: t.id, type: 'function', function: { name: 'propose_edit', arguments: JSON.stringify(t.args || {}) } }, currentSrc: currentSrc ?? '', onApplyEdit: onApplyEdit, view: transcriptView }, t.id)) : t.done ? (
|
|
1049
|
+
// A tool that finished mid-run gets the SAME expandable card as the
|
|
1050
|
+
// settled transcript, so its args/result are viewable before the
|
|
1051
|
+
// turn ends (not just a bare summary line).
|
|
1052
|
+
// data-fx-id stays the same across running -> done, so an effect
|
|
1053
|
+
// treats the swap as the same tool line (one rock, one animation).
|
|
1054
|
+
_jsx("div", { "data-fx-id": t.id, children: _jsx(ToolResultCard, { inv: { name: t.name, args: t.args || {}, result: t.result ?? '', isError: t.isError, running: false, ms: t.ms } }) }, t.id)) : (
|
|
1055
|
+
// The name animates once, here, when the tool appears.
|
|
1056
|
+
_jsx("div", { "data-fx-id": t.id, style: S.liveToolRow, children: _jsx(ToolLine, { name: t.name, args: t.args || {}, result: t.result, isError: t.isError, running: true, ms: t.ms, nameFx: fx }) }, t.id))), live.text && fx === 'netsplit' && !hasAnswer && _jsx(NetsplitJoin, {}), live.text ? _jsxs(_Fragment, { children: [_jsx("div", { "data-tx-part": "assistant", children: _jsx(LiveAnswer, { text: live.text, effect: fx, complete: !running, onProgress: (n) => { liveShownRef.current = n; } }) }), 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 })] })))] })) : undefined }), plan && plan.length > 0 && _jsx(PlanCard, { entries: plan })] }), pendingPerm && pendingPerm.tool === SUBMIT_PLAN && (_jsx(PlanApprovalCard, { agent: pendingPerm.agent ?? 'This agent', plan: planTextOf(undefined, pendingPerm.summary), ...(planAgents ? { agents: planAgents } : {}), onApprove: () => resolvePerm(true), onDeny: (note) => resolvePerm(false, false, note) })), pendingPerm && pendingPerm.tool !== SUBMIT_PLAN && (_jsxs("div", { style: S.perm, "data-chat-part": "perm", ...((pendingPerm.tool === 'budget' ? chatWords.budgetTitle : chatWords.permTitle)
|
|
1057
|
+
? { role: 'group', 'aria-label': pendingPerm.tool === 'budget' ? 'Budget limit reached' : 'Permission needed' }
|
|
1058
|
+
: {}), children: [_jsx("div", { style: S.permHead, "data-chat-part": "perm-head", children: pendingPerm.tool === 'budget'
|
|
1059
|
+
? (chatWords.budgetTitle ?? _jsxs(_Fragment, { children: [_jsx(IcKey, { size: 14 }), " Budget limit reached"] }))
|
|
1060
|
+
: (chatWords.permTitle?.(pendingPerm.tool) ?? _jsxs(_Fragment, { children: [_jsx(IcKey, { size: 14 }), " Permission needed"] })) }), _jsx("div", { style: S.permBody, children: pendingPerm.tool === 'budget'
|
|
1061
|
+
? pendingPerm.summary
|
|
1062
|
+
: (_jsxs(_Fragment, { children: [pendingPerm.requester === 'you'
|
|
1063
|
+
? 'Your command needs'
|
|
1064
|
+
: pendingPerm.agent
|
|
1065
|
+
? _jsxs(_Fragment, { children: [_jsx("strong", { children: pendingPerm.agent }), " (called by ", pendingPerm.caller ?? 'this agent', ") wants to use"] })
|
|
1066
|
+
: '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, "data-chat-part": "perm-deny", ...plainName(chatWords.budgetStop, 'Stop'), onClick: () => resolvePerm(false), children: chatWords.budgetStop ?? 'Stop' }), _jsxs("button", { style: S.permAllow, "data-chat-part": "perm-go", ...plainName(chatWords.budgetContinue, 'Continue'), onClick: () => resolvePerm(true), children: [_jsx(IcCheck, { size: 12 }), " ", chatWords.budgetContinue ?? 'Continue'] })] })) : (_jsxs(_Fragment, { children: [_jsx("button", { style: S.permDeny, "data-chat-part": "perm-deny", ...plainName(chatWords.deny, 'Deny'), onClick: () => resolvePerm(false), children: chatWords.deny ?? 'Deny' }), !isCheckpointTool(pendingPerm.tool) && (_jsx("button", { style: S.permAllowSession, "data-chat-part": "perm-session", ...plainName(chatWords.allowSession, 'Allow for session'), onClick: () => resolvePerm(true, true), children: chatWords.allowSession ?? 'Allow for session' })), _jsxs("button", { style: S.permAllow, "data-chat-part": "perm-go", ...plainName(chatWords.allowOnce, 'Allow once'), onClick: () => resolvePerm(true), children: [_jsx(IcCheck, { size: 12 }), " ", chatWords.allowOnce ?? 'Allow once'] })] })) })] })), approvalSuggestion && !pendingPerm && (_jsxs("div", { style: S.approvalSuggest, role: "status", "aria-live": "polite", "data-chat-part": "suggest", children: [_jsxs("div", { children: ["You've allowed ", _jsx("strong", { children: approvalSuggestion.tool }), " ", approvalSuggestion.count, " times", approvalSuggestion.scope === 'workspace' ? ' in this workspace' : '', ". Always allow it?"] }), _jsxs("div", { style: S.permActions, children: [approvalSuggestion.scopeChoice ? (_jsxs(_Fragment, { children: [_jsx("button", { style: S.permAllowSession, onClick: () => respondSuggestion('workspace'), children: "This workspace" }), _jsx("button", { style: S.permAllowSession, onClick: () => respondSuggestion('global'), children: "Everywhere" })] })) : (_jsx("button", { style: S.permAllowSession, onClick: () => respondSuggestion('global'), children: "Always allow" })), _jsx("button", { style: S.permDeny, onClick: () => respondSuggestion('not-now'), children: "Not now" }), _jsx("button", { style: S.permDeny, onClick: () => respondSuggestion('never'), children: "Never" })] })] })), 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 }), !readOnly && !running && !error && suggestions.length > 0 && (_jsxs("div", { style: S.followRow, "data-chat-part": "followups", children: [chatWords.followups && _jsx("span", { style: S.followLabel, children: chatWords.followups }), 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 })] }), _jsx("input", { ref: fileInputRef, type: "file", multiple: true, accept: UPLOAD_ACCEPT, style: { display: 'none' }, onChange: (e) => {
|
|
912
1067
|
const files = Array.from(e.target.files ?? []);
|
|
913
1068
|
e.target.value = '';
|
|
914
1069
|
setCtxMenuOpen(false);
|
|
915
1070
|
void processFiles(files);
|
|
916
|
-
} }),
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
dictating: dictating, leadSlot: _jsx(DictateButton, { host: browserModelHost(),
|
|
935
|
-
// Capture the draft the moment the mic opens; every guess below joins
|
|
936
|
-
// onto THIS, never onto the previous guess.
|
|
937
|
-
onListeningChange: (on) => { if (on)
|
|
938
|
-
dictationBase.current = inputRef.current; setDictating(on); },
|
|
939
|
-
/* Live guesses go straight into the draft -- that is the feature.
|
|
940
|
-
EMPTY IS IGNORED, and that is load-bearing: DictateButton clears the
|
|
941
|
-
guess with onInterim('') AFTER stop() resolves, which is after the
|
|
942
|
-
final transcript has already landed. Honouring it would recompute
|
|
943
|
-
`base + ''` and wipe the committed text a beat after showing it. */
|
|
944
|
-
onInterim: (t) => { if (t)
|
|
945
|
-
setInput(appendDictated(dictationBase.current, t)); }, onFinal: (t) => setInput(appendDictated(dictationBase.current, t)) }), onSend: send, onStop: () => { stoppedRef.current = true; if (sessionId)
|
|
946
|
-
controller.stop(sessionId); }, running: running, mode: mode, onModeChange: setMode, modes: modeEntries, chips: [
|
|
947
|
-
...(chips ?? []),
|
|
948
|
-
...contextDocs.map((d) => ({ id: `doc:${d.id}`, label: `${d.name} · ${fmtTok(d.tokens)} tok`, removable: true })),
|
|
949
|
-
], onRemoveChip: (cid) => {
|
|
950
|
-
if (cid.startsWith('doc:')) {
|
|
951
|
-
if (sessionId)
|
|
952
|
-
removeDoc(sessionId, cid.slice(4));
|
|
953
|
-
}
|
|
954
|
-
else
|
|
955
|
-
onRemoveChip?.(cid);
|
|
956
|
-
}, onAddContext: () => setCtxMenuOpen((o) => !o),
|
|
957
|
-
// The agent's plan, above the composer. prompd.app also has a dock panel
|
|
958
|
-
// for it; on corenel.ai this is the ONLY place it is visible. Renders
|
|
959
|
-
// nothing when there are no tasks.
|
|
960
|
-
// The host's own controls sit BEFORE the task list, so their position
|
|
961
|
-
// stays put: TodoBar renders nothing when there are no tasks, and a
|
|
962
|
-
// control that moves as the agent starts and finishes work is a
|
|
963
|
-
// control people misclick.
|
|
964
|
-
rightSlot: _jsxs(_Fragment, { children: [composerRightSlot, _jsx(TodoBar, { sessionId: sessionId })] }), slashCommands: slashCommands, onPickSlash: (name) => { if (runSlash(name) !== 'prefilled')
|
|
965
|
-
setInput(''); }, attachments: attachments, onAttach: (urls) => setAttachments((a) => [...a, ...urls]), onRemoveAttachment: (i) => setAttachments((a) => a.filter((_, j) => j !== i)), pending: pending, onQueue: queueCurrent, onInject: injectPending, onRemovePending: (id) => { if (sessionId)
|
|
966
|
-
removePending(sessionId, id); }, onEditPending: (id, text) => { if (sessionId)
|
|
967
|
-
editPending(sessionId, id, text); }, 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) })] }));
|
|
1071
|
+
} }), _jsxs(CapBox, { cap: cap, children: [centered ? null : noticesEl, readOnly ? (
|
|
1072
|
+
/* WATCH MODE: the whole input region is replaced, not disabled piecemeal.
|
|
1073
|
+
A disabled box with live chips around it still reads as "type here". */
|
|
1074
|
+
_jsxs("div", { style: S.readOnlyBar, role: "region", "aria-label": "Watching this run", children: [_jsx("div", { style: S.readOnlyBanner, children: readOnly.banner }), readOnly.action] })) : (centered ? null : dockEl)] })] }), lightboxSrc && _jsx(Lightbox, { src: lightboxSrc, onClose: () => setLightboxSrc(null) })] }));
|
|
1075
|
+
}
|
|
1076
|
+
function useMatchMedia(q) {
|
|
1077
|
+
const get = () => typeof matchMedia === 'function' && matchMedia(q).matches;
|
|
1078
|
+
const [m, setM] = useState(get);
|
|
1079
|
+
useEffect(() => {
|
|
1080
|
+
if (typeof matchMedia !== 'function')
|
|
1081
|
+
return;
|
|
1082
|
+
const mq = matchMedia(q);
|
|
1083
|
+
const on = () => setM(mq.matches);
|
|
1084
|
+
on();
|
|
1085
|
+
mq.addEventListener?.('change', on);
|
|
1086
|
+
return () => mq.removeEventListener?.('change', on);
|
|
1087
|
+
}, [q]);
|
|
1088
|
+
return m;
|
|
968
1089
|
}
|
|
969
1090
|
// Session-row style is keyed by "active" — kept out of S (which is a flat style
|
|
970
1091
|
// map) and typed as its own helper.
|
|
1092
|
+
function CapBox({ cap, extra, children }) {
|
|
1093
|
+
return cap ? _jsx("div", { "data-content-cap": "", style: { ...cap, ...extra }, children: children }) : _jsx(_Fragment, { children: children });
|
|
1094
|
+
}
|
|
971
1095
|
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' });
|
|
972
1096
|
const S = {
|
|
973
1097
|
panel: { position: 'relative', display: 'flex', flexDirection: 'column', flex: 1, width: '100%', minWidth: 0, height: '100%', background: 'var(--surface-1)', overflow: 'hidden' },
|
|
974
|
-
|
|
1098
|
+
// Full-width shell for a capped header row; the rule stays on the capped row so it follows the column.
|
|
1099
|
+
headOuter: { position: 'relative', flexShrink: 0, borderBottom: 'none' },
|
|
1100
|
+
// Same flow as S.body's own, for the capped inner wrapper.
|
|
1101
|
+
bodyInner: { display: 'flex', flexDirection: 'column', gap: 16, flex: '1 0 auto' },
|
|
1102
|
+
head: { position: 'relative', display: 'flex', alignItems: 'center', flexWrap: 'wrap', gap: 6, rowGap: 6, minHeight: 40, padding: '4px 10px', borderBottom: '1px solid var(--border-subtle)', flexShrink: 0 },
|
|
975
1103
|
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 },
|
|
976
|
-
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' },
|
|
977
1104
|
sessionName: { fontSize: 13, fontWeight: 600, color: 'var(--text-primary)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' },
|
|
978
1105
|
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' },
|
|
979
1106
|
iconBtnActive: { background: 'var(--accent-soft)', color: 'var(--accent)' },
|
|
@@ -1009,7 +1136,9 @@ const S = {
|
|
|
1009
1136
|
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' },
|
|
1010
1137
|
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' },
|
|
1011
1138
|
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' },
|
|
1012
|
-
|
|
1139
|
+
dockWrap: { position: 'relative' },
|
|
1140
|
+
centerStart: { margin: 'auto 0', display: 'flex', flexDirection: 'column', gap: 20, padding: '24px 0' },
|
|
1141
|
+
ctxMenu: { position: 'absolute', left: 10, bottom: 'calc(100% - 4px)', zIndex: 200, minWidth: 240, boxShadow: 'var(--shadow-3)', padding: 8, background: 'var(--surface-2)', border: '1px solid var(--border-default)', borderRadius: 'var(--radius-md)', display: 'flex', flexDirection: 'column', gap: 4 },
|
|
1013
1142
|
perm: { border: '1px solid var(--warning)', borderRadius: 'var(--radius-md)', background: 'var(--warning-soft)', padding: 12, display: 'flex', flexDirection: 'column', gap: 8 },
|
|
1014
1143
|
permHead: { display: 'inline-flex', alignItems: 'center', gap: 7, fontSize: 13, fontWeight: 600, color: 'var(--text-primary)' },
|
|
1015
1144
|
// pre-wrap: a submit_plan summary is the whole multi-line plan being approved.
|
|
@@ -1023,8 +1152,6 @@ const S = {
|
|
|
1023
1152
|
approvalSuggest: { border: '1px solid var(--border-subtle)', borderRadius: 'var(--radius-md)', background: 'var(--surface-2)', padding: 12, display: 'flex', flexDirection: 'column', gap: 8 },
|
|
1024
1153
|
// Per-run budget control — the run chip's soft note when a degrade breach
|
|
1025
1154
|
// switched the in-flight model.
|
|
1026
|
-
budgetWrap: { position: 'relative', display: 'flex', alignItems: 'center', gap: 8, padding: '0 10px 6px', flexShrink: 0 },
|
|
1027
|
-
budgetDegraded: { fontSize: 11, color: 'var(--warning)' },
|
|
1028
1155
|
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 },
|
|
1029
1156
|
// 44px tall: the one way out of an unsaved chat has to be easy to hit on a phone.
|
|
1030
1157
|
unsavedRetry: { minHeight: 44, padding: '0 14px', border: '1px solid var(--warning)', borderRadius: 'var(--radius-sm)', background: 'transparent', color: 'inherit', fontSize: 12.5, fontWeight: 600, cursor: 'pointer', flexShrink: 0 },
|