@corenel/web-ui 0.8.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/dist/AskUser.d.ts.map +1 -1
  2. package/dist/AskUser.js +3 -1
  3. package/dist/AskUser.js.map +1 -1
  4. package/dist/CommandTray.d.ts.map +1 -1
  5. package/dist/CommandTray.js +2 -1
  6. package/dist/CommandTray.js.map +1 -1
  7. package/dist/Composer.d.ts.map +1 -1
  8. package/dist/Composer.js +14 -6
  9. package/dist/Composer.js.map +1 -1
  10. package/dist/ContextBar.d.ts.map +1 -1
  11. package/dist/ContextBar.js +24 -1
  12. package/dist/ContextBar.js.map +1 -1
  13. package/dist/ContextManager.d.ts +10 -1
  14. package/dist/ContextManager.d.ts.map +1 -1
  15. package/dist/ContextManager.js +63 -26
  16. package/dist/ContextManager.js.map +1 -1
  17. package/dist/ContextStrip.d.ts +5 -1
  18. package/dist/ContextStrip.d.ts.map +1 -1
  19. package/dist/ContextStrip.js +4 -1
  20. package/dist/ContextStrip.js.map +1 -1
  21. package/dist/ExclusionsModal.d.ts +3 -0
  22. package/dist/ExclusionsModal.d.ts.map +1 -1
  23. package/dist/ExclusionsModal.js +28 -7
  24. package/dist/ExclusionsModal.js.map +1 -1
  25. package/dist/RichContent.d.ts.map +1 -1
  26. package/dist/RichContent.js +3 -1
  27. package/dist/RichContent.js.map +1 -1
  28. package/dist/SummaryTranscript.d.ts.map +1 -1
  29. package/dist/SummaryTranscript.js +4 -3
  30. package/dist/SummaryTranscript.js.map +1 -1
  31. package/dist/TodoBar.d.ts.map +1 -1
  32. package/dist/TodoBar.js +12 -6
  33. package/dist/TodoBar.js.map +1 -1
  34. package/dist/ToolFold.d.ts.map +1 -1
  35. package/dist/ToolFold.js +2 -1
  36. package/dist/ToolFold.js.map +1 -1
  37. package/dist/Transcript.d.ts.map +1 -1
  38. package/dist/Transcript.js +30 -20
  39. package/dist/Transcript.js.map +1 -1
  40. package/dist/agentChat/AgentChat.d.ts +8 -2
  41. package/dist/agentChat/AgentChat.d.ts.map +1 -1
  42. package/dist/agentChat/AgentChat.js +149 -90
  43. package/dist/agentChat/AgentChat.js.map +1 -1
  44. package/dist/agentChat/OptionsDrawer.js +1 -1
  45. package/dist/agentChat/OptionsDrawer.js.map +1 -1
  46. package/dist/agentChat/PlanReview.d.ts +37 -5
  47. package/dist/agentChat/PlanReview.d.ts.map +1 -1
  48. package/dist/agentChat/PlanReview.js +192 -24
  49. package/dist/agentChat/PlanReview.js.map +1 -1
  50. package/dist/agentChat/controller.d.ts +16 -1
  51. package/dist/agentChat/controller.d.ts.map +1 -1
  52. package/dist/agentChat/controller.js.map +1 -1
  53. package/dist/chatSkin.d.ts +20 -0
  54. package/dist/chatSkin.d.ts.map +1 -0
  55. package/dist/chatSkin.js +220 -0
  56. package/dist/chatSkin.js.map +1 -0
  57. package/dist/chatSkinContext.d.ts +55 -0
  58. package/dist/chatSkinContext.d.ts.map +1 -0
  59. package/dist/chatSkinContext.js +16 -0
  60. package/dist/chatSkinContext.js.map +1 -0
  61. package/dist/chatSkinCss.d.ts +3 -0
  62. package/dist/chatSkinCss.d.ts.map +1 -0
  63. package/dist/chatSkinCss.js +202 -0
  64. package/dist/chatSkinCss.js.map +1 -0
  65. package/dist/commandTurn.d.ts +4 -1
  66. package/dist/commandTurn.d.ts.map +1 -1
  67. package/dist/commandTurn.js +13 -1
  68. package/dist/commandTurn.js.map +1 -1
  69. package/dist/contextCopy.d.ts +13 -0
  70. package/dist/contextCopy.d.ts.map +1 -0
  71. package/dist/contextCopy.js +20 -0
  72. package/dist/contextCopy.js.map +1 -0
  73. package/dist/toolRender.d.ts.map +1 -1
  74. package/dist/toolRender.js +7 -6
  75. package/dist/toolRender.js.map +1 -1
  76. package/dist/transcriptSkin.d.ts +23 -1
  77. package/dist/transcriptSkin.d.ts.map +1 -1
  78. package/dist/transcriptSkin.js +134 -9
  79. package/dist/transcriptSkin.js.map +1 -1
  80. package/dist/transcriptSkinCss.d.ts.map +1 -1
  81. package/dist/transcriptSkinCss.js +17 -10
  82. package/dist/transcriptSkinCss.js.map +1 -1
  83. package/package.json +5 -5
@@ -16,7 +16,9 @@ import { AskUser } from '../AskUser';
16
16
  import { Lightbox } from '../Lightbox';
17
17
  import { ContextBar } from '../ContextBar';
18
18
  import { useTranscriptView } from '../transcriptView';
19
- import { useTranscriptSkin, skinCommand } from '../transcriptSkin';
19
+ import { useTranscriptSkin, skinCommand, SKIN_REACH } from '../transcriptSkin';
20
+ import { ChatSkinRoot, CHAT_WORDS } from '../chatSkin';
21
+ import { plainName } from '../chatSkinContext';
20
22
  import { ToolFold } from '../ToolFold';
21
23
  import { useArrivalEffect, effectCommand, prefersReducedMotion, resolveArrivalEffect } from '../arrivalEffect';
22
24
  import { LiveAnswer } from '../fx/LiveAnswer';
@@ -77,14 +79,14 @@ const PS = {
77
79
  textDone: { fontSize: 13, color: 'var(--text-tertiary)', textDecoration: 'line-through' },
78
80
  };
79
81
  export function AgentChat(props) {
80
- const { controller, sessionId, onSessionChange, sessions, aboveComposer, contextWindow, systemSource, offloaded, inPrice, modes, currentSrc, onApplyEdit, renderFence, renderToolCard, emptyState, headerLead, headerExtras, composerRightSlot, title, features, embedded = false, onClose, chips, onRemoveChip, renderContextMenu, capabilities, slashExtra, onSlashAction, interceptSend, onUploadFile, sendOptsExtra, onAfterSend, tip, budgetSection, toolBadge, policyEditor, settingsExtras, LiveTimeline, fixedIdentity, readOnly, } = props;
82
+ const { controller, sessionId, onSessionChange, sessions, aboveComposer, contextWindow, systemSource, offloaded, inPrice, modes, currentSrc, onApplyEdit, renderFence, renderToolCard, emptyState, headerLead, headerExtras, composerRightSlot, title, features, embedded = false, onClose, chips, onRemoveChip, renderContextMenu, capabilities, slashExtra, onSlashAction, interceptSend, onUploadFile, sendOptsExtra, onAfterSend, tip, budgetSection, toolBadge, policyEditor, settingsExtras, LiveTimeline, fixedIdentity, readOnly, planAgents, } = props;
81
83
  const { name: brandName } = useBrand(); // host-injected product name (CorenelHost seam)
82
84
  // The engine owns the conversation, run loop, worker, and persistence — this
83
85
  // column is a view over ONE controller session. The active session id is
84
86
  // CONTROLLED by the host (sessionId/onSessionChange).
85
87
  const session = useControllerSession(controller, sessionId ?? '');
86
88
  const globalTick = useControllerGlobal(controller); // cross-session changes (titles, run badges)
87
- const { turns, input, mode, running, live, recovery, error, suggestions, attachments, pendingPerm, pendingAsk, approvalSuggestion, policyNotes, degradedModel, recEvents, plan, pendingCommand, loaded } = session;
89
+ const { turns, input, mode, running, live, recovery, error, suggestions, attachments, pendingPerm, pendingAsk, approvalSuggestion, policyNotes, degradedModel, recEvents, plan, pendingCommand, loaded, saveBlocked, mergeTick } = session;
88
90
  // Thin engine-routed setters so the render below reads like the old local-state
89
91
  // version (same call sites, engine-backed).
90
92
  const setInput = useCallback((v) => { if (sessionId)
@@ -253,6 +255,41 @@ export function AgentChat(props) {
253
255
  // A rejection notice is about THIS session's upload attempt; don't let it
254
256
  // linger onto another chat.
255
257
  useEffect(() => { setUploadNotice(null); }, [sessionId]);
258
+ /* MERGED IN. A save found another writer (a second tab, the daemon) had
259
+ * changed this chat and merged its turns in, so the transcript just grew
260
+ * above where the person was reading. Shown when the tick goes UP while this
261
+ * chat is on screen -- never for a merge from before it was opened -- and
262
+ * dismissible, like the upload notice beside it. */
263
+ const [mergedNotice, setMergedNotice] = useState(false);
264
+ const mergeSeen = useRef(null);
265
+ useEffect(() => {
266
+ const tick = mergeTick ?? 0;
267
+ const prev = mergeSeen.current;
268
+ mergeSeen.current = { id: sessionId ?? '', tick };
269
+ if (prev && prev.id === (sessionId ?? '')) {
270
+ if (tick > prev.tick)
271
+ setMergedNotice(true);
272
+ }
273
+ else {
274
+ setMergedNotice(false);
275
+ }
276
+ }, [sessionId, mergeTick]);
277
+ /* The unsaved-chat notice's Retry. Pending while the read and save run, so
278
+ * a second tap cannot start a second retry; a rejection is swallowed here
279
+ * because the notice itself is the report -- it stays until a save lands. */
280
+ const [retrying, setRetrying] = useState(false);
281
+ const retryLoad = useCallback(async () => {
282
+ if (!sessionId || !controller.retryLoad)
283
+ return;
284
+ setRetrying(true);
285
+ try {
286
+ await controller.retryLoad(sessionId);
287
+ }
288
+ catch { /* the notice stays */ }
289
+ finally {
290
+ setRetrying(false);
291
+ }
292
+ }, [controller, sessionId]);
256
293
  const [selPolicy, setSelPolicy] = useState(getSelectedPolicyName);
257
294
  const selectPolicy = useCallback((name) => { setSelPolicy(name); setSelectedPolicyName(name); }, []);
258
295
  const policyName = fixedIdentity?.policyName ?? selPolicy;
@@ -395,9 +432,9 @@ export function AgentChat(props) {
395
432
  setRenaming(null);
396
433
  }
397
434
  };
398
- const resolvePerm = (allow, always = false) => {
435
+ const resolvePerm = (allow, always = false, note) => {
399
436
  if (sessionId)
400
- controller.resolvePerm(sessionId, allow, always);
437
+ controller.resolvePerm(sessionId, allow, always, note);
401
438
  };
402
439
  const respondSuggestion = (c) => {
403
440
  if (sessionId)
@@ -414,7 +451,7 @@ export function AgentChat(props) {
414
451
  const beforeExtra = [
415
452
  { name: 'new', hint: 'start a fresh chat', section: 'Session', icon: _jsx(IcPlus, { size: 13 }), kind: 'action' },
416
453
  { name: 'clear', hint: 'clear this conversation', section: 'Session', icon: _jsx(IcRefresh, { size: 13 }), kind: 'action' },
417
- { name: 'skin', hint: 'Change how the transcript looks', section: 'Session', icon: _jsx(IcLayout, { size: 13 }), kind: 'action' },
454
+ { name: 'skin', hint: 'Change how the chat looks', section: 'Session', icon: _jsx(IcLayout, { size: 13 }), kind: 'action' },
418
455
  { name: 'effect', hint: 'Change how new text arrives', section: 'Session', icon: _jsx(IcZap, { size: 13 }), kind: 'action' },
419
456
  ...(renderContextMenu ? [{ name: 'context', hint: 'attach files or selection', section: 'Workspace', icon: _jsx(IcFile, { size: 13 }), kind: 'action' }] : []),
420
457
  { name: 'persona', hint: 'switch the agent persona', section: 'Workspace', icon: _jsx(IcUser, { size: 13 }), kind: 'action' },
@@ -525,7 +562,7 @@ export function AgentChat(props) {
525
562
  setInput(def.template ?? '');
526
563
  return 'prefilled';
527
564
  }
528
- if (onSlashAction?.(name, { sendText }))
565
+ if (onSlashAction?.(name, { sendText }, arg))
529
566
  return 'handled';
530
567
  switch (name) {
531
568
  case 'new':
@@ -829,89 +866,105 @@ export function AgentChat(props) {
829
866
  if (card)
830
867
  liveCards.set(t.id, card);
831
868
  }
832
- 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) {
833
- e.stopPropagation();
834
- startRename(sessionId);
835
- } }, 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, !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)))] }))] }), 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 ?? '' }), ownLoop && !readOnly && _jsx(ContextBar, { contextWindow: contextWindow, systemSource: resolveSystemSource(), offloaded: offloaded, conversation: conversation, sessionId: sessionId ?? '' }), _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 && (loaded !== false
836
- ? (readOnly ? null : emptyState?.({ setInput }))
837
- : _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) => ({
838
- name: t.name, args: t.args || {}, result: t.result ?? '', isError: t.isError, running: !t.done, ms: t.ms,
839
- })), view: transcriptView })), live.tools.filter((t) => (fx !== 'splitflap' && transcriptSkin !== 'minimal') || t.name === 'propose_edit' || liveCards.has(t.id)).map((t) => liveCards.has(t.id) ? (
840
- // A host card replaces the default card mid-run too.
841
- _jsx(React.Fragment, { children: liveCards.get(t.id) }, t.id)) : t.name === 'propose_edit' ? (
842
- // propose_edit BLOCKS its run on the review, so the decidable card
843
- // must render here, mid-run — not only in the settled transcript.
844
- _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 ? (
845
- // A tool that finished mid-run gets the SAME expandable card as the
846
- // settled transcript, so its args/result are viewable before the
847
- // turn ends (not just a bare summary line).
848
- // data-fx-id stays the same across running -> done, so an effect
849
- // treats the swap as the same tool line (one rock, one animation).
850
- _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)) : (
851
- // The name animates once, here, when the tool appears.
852
- _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), onApprove: () => resolvePerm(true), onDeny: () => resolvePerm(false) })), pendingPerm && pendingPerm.tool !== SUBMIT_PLAN && (_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'
853
- ? pendingPerm.summary
854
- : (_jsxs(_Fragment, { children: [pendingPerm.requester === 'you'
855
- ? 'Your command needs'
856
- : pendingPerm.agent
857
- ? _jsxs(_Fragment, { children: [_jsx("strong", { children: pendingPerm.agent }), " (called by ", pendingPerm.caller ?? 'this agent', ") wants to use"] })
858
- : '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" }), !isCheckpointTool(pendingPerm.tool) && (_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"] })] })) })] })), approvalSuggestion && !pendingPerm && (_jsxs("div", { style: S.approvalSuggest, role: "status", "aria-live": "polite", 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 && (_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) => {
859
- const files = Array.from(e.target.files ?? []);
860
- e.target.value = '';
861
- setCtxMenuOpen(false);
862
- void processFiles(files);
863
- } }), 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)
864
- controller.resolveAsk(sessionId, answers); }, onCancel: () => { if (sessionId)
865
- controller.resolveAsk(sessionId, null); } })), degradedModel && (_jsx("div", { style: S.budgetWrap, children: _jsxs("span", { style: S.budgetDegraded, children: ["Degraded to ", degradedModel, " (budget)"] }) })), readOnly ? (
866
- /* WATCH MODE: the whole input region is replaced, not disabled piecemeal.
867
- A disabled box with live chips around it still reads as "type here". */
868
- _jsxs("div", { style: S.readOnlyBar, role: "region", "aria-label": "Watching this run", children: [_jsx("div", { style: S.readOnlyBanner, children: readOnly.banner }), readOnly.action] })) : (_jsxs(_Fragment, { children: [aboveComposer, pendingCommand && _jsx(RunningCommand, { command: pendingCommand.command, startedAt: pendingCommand.startedAt }), _jsx(CommandTray, { command: tray, onKeep: (turnId) => { if (sessionId)
869
- controller.toggleInclude(sessionId, turnId); }, onDismiss: (turnId) => setDismissed((d) => new Set(d).add(turnId)) }), _jsx(Composer, { value: input, onChange: setInput,
870
- // The mic goes in the bottom row's FIXED first position, not the chip
871
- // row. It was in the chip row first, and TodoBar shoved it sideways
872
- // every time the agent's current task changed -- a control that moves
873
- // while the agent works is a control people misclick, and this one's
874
- // worst misclick is "I thought I stopped recording". It stays in the
875
- // composer either way, which is what keeps it reachable on a phone
876
- // (the chat header is not -- see composerSlot.test.ts).
877
- //
878
- // Rendered here rather than passed in by each host: dictation belongs to
879
- // the agent chat, not to any one surface, and four separate wirings
880
- // would mean four chances to ship a surface that is silently mute.
881
- dictating: dictating, leadSlot: _jsx(DictateButton, { host: browserModelHost(),
882
- // Capture the draft the moment the mic opens; every guess below joins
883
- // onto THIS, never onto the previous guess.
884
- onListeningChange: (on) => { if (on)
885
- dictationBase.current = inputRef.current; setDictating(on); },
886
- /* Live guesses go straight into the draft -- that is the feature.
887
- EMPTY IS IGNORED, and that is load-bearing: DictateButton clears the
888
- guess with onInterim('') AFTER stop() resolves, which is after the
889
- final transcript has already landed. Honouring it would recompute
890
- `base + ''` and wipe the committed text a beat after showing it. */
891
- onInterim: (t) => { if (t)
892
- setInput(appendDictated(dictationBase.current, t)); }, onFinal: (t) => setInput(appendDictated(dictationBase.current, t)) }), onSend: send, onStop: () => { stoppedRef.current = true; if (sessionId)
893
- controller.stop(sessionId); }, running: running, mode: mode, onModeChange: setMode, modes: modeEntries, chips: [
894
- ...(chips ?? []),
895
- ...contextDocs.map((d) => ({ id: `doc:${d.id}`, label: `${d.name} · ${fmtTok(d.tokens)} tok`, removable: true })),
896
- ], onRemoveChip: (cid) => {
897
- if (cid.startsWith('doc:')) {
898
- if (sessionId)
899
- removeDoc(sessionId, cid.slice(4));
900
- }
901
- else
902
- onRemoveChip?.(cid);
903
- }, onAddContext: () => setCtxMenuOpen((o) => !o),
904
- // The agent's plan, above the composer. prompd.app also has a dock panel
905
- // for it; on corenel.ai this is the ONLY place it is visible. Renders
906
- // nothing when there are no tasks.
907
- // The host's own controls sit BEFORE the task list, so their position
908
- // stays put: TodoBar renders nothing when there are no tasks, and a
909
- // control that moves as the agent starts and finishes work is a
910
- // control people misclick.
911
- rightSlot: _jsxs(_Fragment, { children: [composerRightSlot, _jsx(TodoBar, { sessionId: sessionId })] }), slashCommands: slashCommands, onPickSlash: (name) => { if (runSlash(name) !== 'prefilled')
912
- 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)
913
- removePending(sessionId, id); }, onEditPending: (id, text) => { if (sessionId)
914
- 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) })] }));
869
+ /* The skin reaches past the transcript only for skins whose SKIN_REACH says
870
+ * so. The header and the options drawer sit inside the root, in their
871
+ * original order, for every skin: an unskinned chat's DOM (and so its tab
872
+ * order) is as before, a skin change restyles in place rather than
873
+ * remounting, and a drawer opened from a skinned header looks like it. The
874
+ * one exception is a chat-reach skin that leaves its header out: the header
875
+ * then renders above the root. The lightbox, a neutral image viewer, stays
876
+ * outside and keeps the app theme. */
877
+ const skinReach = SKIN_REACH[transcriptSkin];
878
+ const headerOutside = skinReach.chat && !skinReach.header;
879
+ const chatWords = skinReach.chat ? CHAT_WORDS[transcriptSkin] ?? {} : {};
880
+ const headerEl = (_jsxs("header", { style: S.head, "data-chat-part": "header", "data-chat-running": running ? '' : undefined, 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
+ e.stopPropagation();
882
+ 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 })] })), 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, !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)))] }))] }));
884
+ return (_jsxs("div", { style: S.panel, 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 ?? '' }), ownLoop && !readOnly && _jsx(ContextBar, { contextWindow: contextWindow, systemSource: resolveSystemSource(), offloaded: offloaded, conversation: conversation, sessionId: sessionId ?? '' }), _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 && (loaded !== false
885
+ ? (readOnly ? null : emptyState?.({ setInput }))
886
+ : _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) => ({
887
+ name: t.name, args: t.args || {}, result: t.result ?? '', isError: t.isError, running: !t.done, ms: t.ms,
888
+ })), view: transcriptView })), live.tools.filter((t) => (fx !== 'splitflap' && transcriptSkin !== 'minimal') || t.name === 'propose_edit' || liveCards.has(t.id)).map((t) => liveCards.has(t.id) ? (
889
+ // A host card replaces the default card mid-run too.
890
+ _jsx(React.Fragment, { children: liveCards.get(t.id) }, t.id)) : t.name === 'propose_edit' ? (
891
+ // propose_edit BLOCKS its run on the review, so the decidable card
892
+ // must render here, mid-run — not only in the settled transcript.
893
+ _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 ? (
894
+ // A tool that finished mid-run gets the SAME expandable card as the
895
+ // settled transcript, so its args/result are viewable before the
896
+ // turn ends (not just a bare summary line).
897
+ // data-fx-id stays the same across running -> done, so an effect
898
+ // treats the swap as the same tool line (one rock, one animation).
899
+ _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)) : (
900
+ // The name animates once, here, when the tool appears.
901
+ _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)
902
+ ? { role: 'group', 'aria-label': pendingPerm.tool === 'budget' ? 'Budget limit reached' : 'Permission needed' }
903
+ : {}), children: [_jsx("div", { style: S.permHead, "data-chat-part": "perm-head", children: pendingPerm.tool === 'budget'
904
+ ? (chatWords.budgetTitle ?? _jsxs(_Fragment, { children: [_jsx(IcKey, { size: 14 }), " Budget limit reached"] }))
905
+ : (chatWords.permTitle?.(pendingPerm.tool) ?? _jsxs(_Fragment, { children: [_jsx(IcKey, { size: 14 }), " Permission needed"] })) }), _jsx("div", { style: S.permBody, children: pendingPerm.tool === 'budget'
906
+ ? pendingPerm.summary
907
+ : (_jsxs(_Fragment, { children: [pendingPerm.requester === 'you'
908
+ ? 'Your command needs'
909
+ : pendingPerm.agent
910
+ ? _jsxs(_Fragment, { children: [_jsx("strong", { children: pendingPerm.agent }), " (called by ", pendingPerm.caller ?? 'this agent', ") wants to use"] })
911
+ : '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 })] }), 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) => {
912
+ const files = Array.from(e.target.files ?? []);
913
+ e.target.value = '';
914
+ setCtxMenuOpen(false);
915
+ void processFiles(files);
916
+ } }), 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)
917
+ controller.resolveAsk(sessionId, answers); }, onCancel: () => { if (sessionId)
918
+ controller.resolveAsk(sessionId, null); } })), degradedModel && (_jsx("div", { style: S.budgetWrap, children: _jsxs("span", { style: S.budgetDegraded, children: ["Degraded to ", degradedModel, " (budget)"] }) })), readOnly ? (
919
+ /* WATCH MODE: the whole input region is replaced, not disabled piecemeal.
920
+ A disabled box with live chips around it still reads as "type here". */
921
+ _jsxs("div", { style: S.readOnlyBar, role: "region", "aria-label": "Watching this run", children: [_jsx("div", { style: S.readOnlyBanner, children: readOnly.banner }), readOnly.action] })) : (_jsxs(_Fragment, { children: [aboveComposer, pendingCommand && _jsx(RunningCommand, { command: pendingCommand.command, startedAt: pendingCommand.startedAt }), _jsx(CommandTray, { command: tray, onKeep: (turnId) => { if (sessionId)
922
+ controller.toggleInclude(sessionId, turnId); }, onDismiss: (turnId) => setDismissed((d) => new Set(d).add(turnId)) }), _jsx(Composer, { value: input, onChange: setInput,
923
+ // The mic goes in the bottom row's FIXED first position, not the chip
924
+ // row. It was in the chip row first, and TodoBar shoved it sideways
925
+ // every time the agent's current task changed -- a control that moves
926
+ // while the agent works is a control people misclick, and this one's
927
+ // worst misclick is "I thought I stopped recording". It stays in the
928
+ // composer either way, which is what keeps it reachable on a phone
929
+ // (the chat header is not -- see composerSlot.test.ts).
930
+ //
931
+ // Rendered here rather than passed in by each host: dictation belongs to
932
+ // the agent chat, not to any one surface, and four separate wirings
933
+ // would mean four chances to ship a surface that is silently mute.
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) })] }));
915
968
  }
916
969
  // Session-row style is keyed by "active" — kept out of S (which is a flat style
917
970
  // map) and typed as its own helper.
@@ -931,6 +984,9 @@ const S = {
931
984
  menuRow: { display: 'flex', alignItems: 'center', gap: 4 },
932
985
  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' },
933
986
  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' },
987
+ // Holds everything the chat skin can reach. Same flex role the panel's own
988
+ // children had, so an unskinned chat lays out exactly as before.
989
+ skinBody: { position: 'relative', flex: 1, minHeight: 0, display: 'flex', flexDirection: 'column' },
934
990
  chatRegion: { position: 'relative', flex: 1, minHeight: 0, display: 'flex', flexDirection: 'column' },
935
991
  body: { flex: 1, minHeight: 0, overflowY: 'auto', padding: 14, display: 'flex', flexDirection: 'column', gap: 16 },
936
992
  // Wraps the settled Transcript + live-run block so a host-injected LiveTimeline
@@ -949,6 +1005,7 @@ const S = {
949
1005
  error: { fontSize: 12.5, color: 'var(--danger)', background: 'var(--danger-soft)', border: '1px solid var(--danger)', borderRadius: 'var(--radius-sm)', padding: '8px 10px' },
950
1006
  // Follow-up chips under the latest reply — click to prefill the composer.
951
1007
  followRow: { display: 'flex', flexWrap: 'wrap', gap: 6, marginTop: 4 },
1008
+ followLabel: { alignSelf: 'center', fontSize: 10.5, letterSpacing: '0.14em', color: 'var(--text-tertiary)' },
952
1009
  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' },
953
1010
  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' },
954
1011
  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' },
@@ -969,6 +1026,8 @@ const S = {
969
1026
  budgetWrap: { position: 'relative', display: 'flex', alignItems: 'center', gap: 8, padding: '0 10px 6px', flexShrink: 0 },
970
1027
  budgetDegraded: { fontSize: 11, color: 'var(--warning)' },
971
1028
  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
+ // 44px tall: the one way out of an unsaved chat has to be easy to hit on a phone.
1030
+ 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 },
972
1031
  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 },
973
1032
  readOnlyBar: { display: 'flex', alignItems: 'center', gap: 10, padding: '10px 14px', borderTop: '1px solid var(--border-subtle)', background: 'var(--surface-2)', flexShrink: 0 },
974
1033
  readOnlyBanner: { flex: 1, minWidth: 0, fontSize: 13, color: 'var(--text-secondary)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' },