@corenel/web-ui 0.6.0 → 0.8.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.map +1 -1
- package/dist/Composer.js +184 -172
- package/dist/Composer.js.map +1 -1
- package/dist/ContextBar.d.ts +1 -4
- package/dist/ContextBar.d.ts.map +1 -1
- package/dist/ContextBar.js +2 -2
- package/dist/ContextBar.js.map +1 -1
- package/dist/ContextManager.d.ts +3 -6
- package/dist/ContextManager.d.ts.map +1 -1
- package/dist/ContextManager.js +58 -10
- package/dist/ContextManager.js.map +1 -1
- package/dist/SummaryTranscript.d.ts +11 -1
- package/dist/SummaryTranscript.d.ts.map +1 -1
- package/dist/SummaryTranscript.js +47 -10
- package/dist/SummaryTranscript.js.map +1 -1
- package/dist/ToolFold.d.ts +9 -0
- package/dist/ToolFold.d.ts.map +1 -0
- package/dist/ToolFold.js +32 -0
- package/dist/ToolFold.js.map +1 -0
- package/dist/Transcript.d.ts +30 -1
- package/dist/Transcript.d.ts.map +1 -1
- package/dist/Transcript.js +132 -23
- package/dist/Transcript.js.map +1 -1
- package/dist/agentChat/AgentChat.d.ts +11 -3
- package/dist/agentChat/AgentChat.d.ts.map +1 -1
- package/dist/agentChat/AgentChat.js +174 -86
- package/dist/agentChat/AgentChat.js.map +1 -1
- package/dist/agentChat/OptionsDrawer.d.ts +0 -23
- package/dist/agentChat/OptionsDrawer.d.ts.map +1 -1
- package/dist/agentChat/OptionsDrawer.js +10 -45
- package/dist/agentChat/OptionsDrawer.js.map +1 -1
- package/dist/agentChat/PlanReview.d.ts +24 -0
- package/dist/agentChat/PlanReview.d.ts.map +1 -0
- package/dist/agentChat/PlanReview.js +103 -0
- package/dist/agentChat/PlanReview.js.map +1 -0
- package/dist/agentChat/SkillsSection.js +4 -2
- package/dist/agentChat/SkillsSection.js.map +1 -1
- package/dist/agentChat/controller.d.ts +20 -2
- package/dist/agentChat/controller.d.ts.map +1 -1
- package/dist/agentChat/controller.js.map +1 -1
- package/dist/arrivalEffect.d.ts +28 -0
- package/dist/arrivalEffect.d.ts.map +1 -0
- package/dist/arrivalEffect.js +114 -0
- package/dist/arrivalEffect.js.map +1 -0
- package/dist/commandTurn.d.ts.map +1 -1
- package/dist/commandTurn.js +3 -2
- package/dist/commandTurn.js.map +1 -1
- package/dist/fx/AsteroidsOverlay.d.ts +6 -0
- package/dist/fx/AsteroidsOverlay.d.ts.map +1 -0
- package/dist/fx/AsteroidsOverlay.js +166 -0
- package/dist/fx/AsteroidsOverlay.js.map +1 -0
- package/dist/fx/FlapRow.d.ts +8 -0
- package/dist/fx/FlapRow.d.ts.map +1 -0
- package/dist/fx/FlapRow.js +12 -0
- package/dist/fx/FlapRow.js.map +1 -0
- package/dist/fx/FxText.d.ts +11 -0
- package/dist/fx/FxText.d.ts.map +1 -0
- package/dist/fx/FxText.js +50 -0
- package/dist/fx/FxText.js.map +1 -0
- package/dist/fx/LiveAnswer.d.ts +17 -0
- package/dist/fx/LiveAnswer.d.ts.map +1 -0
- package/dist/fx/LiveAnswer.js +298 -0
- package/dist/fx/LiveAnswer.js.map +1 -0
- package/dist/fx/MatrixRain.d.ts +5 -0
- package/dist/fx/MatrixRain.d.ts.map +1 -0
- package/dist/fx/MatrixRain.js +73 -0
- package/dist/fx/MatrixRain.js.map +1 -0
- package/dist/fx/NetsplitJoin.d.ts +3 -0
- package/dist/fx/NetsplitJoin.d.ts.map +1 -0
- package/dist/fx/NetsplitJoin.js +13 -0
- package/dist/fx/NetsplitJoin.js.map +1 -0
- package/dist/fx/arrivalModes.d.ts +27 -0
- package/dist/fx/arrivalModes.d.ts.map +1 -0
- package/dist/fx/arrivalModes.js +107 -0
- package/dist/fx/arrivalModes.js.map +1 -0
- package/dist/fx/tail.d.ts +16 -0
- package/dist/fx/tail.d.ts.map +1 -0
- package/dist/fx/tail.js +54 -0
- package/dist/fx/tail.js.map +1 -0
- package/dist/fx/useReveal.d.ts +2 -0
- package/dist/fx/useReveal.d.ts.map +1 -0
- package/dist/fx/useReveal.js +59 -0
- package/dist/fx/useReveal.js.map +1 -0
- package/dist/observed.d.ts +16 -0
- package/dist/observed.d.ts.map +1 -0
- package/dist/observed.js +33 -0
- package/dist/observed.js.map +1 -0
- package/dist/toolRender.d.ts +6 -2
- package/dist/toolRender.d.ts.map +1 -1
- package/dist/toolRender.js +27 -17
- package/dist/toolRender.js.map +1 -1
- package/dist/transcriptSkin.d.ts +23 -0
- package/dist/transcriptSkin.d.ts.map +1 -0
- package/dist/transcriptSkin.js +255 -0
- package/dist/transcriptSkin.js.map +1 -0
- package/dist/transcriptSkinCss.d.ts +4 -0
- package/dist/transcriptSkinCss.d.ts.map +1 -0
- package/dist/transcriptSkinCss.js +136 -0
- package/dist/transcriptSkinCss.js.map +1 -0
- package/dist/usageSummary.d.ts +31 -0
- package/dist/usageSummary.d.ts.map +1 -0
- package/dist/usageSummary.js +39 -0
- package/dist/usageSummary.js.map +1 -0
- package/dist/verificationView.d.ts +3 -0
- package/dist/verificationView.d.ts.map +1 -0
- package/dist/verificationView.js +37 -0
- package/dist/verificationView.js.map +1 -0
- package/package.json +7 -6
|
@@ -7,17 +7,23 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
7
7
|
* persona/policy selection and the system-parts toggles are owned here (harness
|
|
8
8
|
* get/set); everything editor-specific (context source, tips lifecycle, budget
|
|
9
9
|
* form, host slash commands) comes in via props. */
|
|
10
|
-
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
10
|
+
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
11
11
|
import { Transcript, ToolLine, DiffProposal } from '../Transcript';
|
|
12
12
|
import { ToolResultCard } from '../toolRender';
|
|
13
|
-
import { Markdown } from '../RichContent';
|
|
14
13
|
import { Composer } from '../Composer';
|
|
15
14
|
import { SendLater, useSendLater } from '../sendLater/SendLater';
|
|
16
15
|
import { AskUser } from '../AskUser';
|
|
17
16
|
import { Lightbox } from '../Lightbox';
|
|
18
17
|
import { ContextBar } from '../ContextBar';
|
|
19
18
|
import { useTranscriptView } from '../transcriptView';
|
|
20
|
-
import {
|
|
19
|
+
import { useTranscriptSkin, skinCommand } from '../transcriptSkin';
|
|
20
|
+
import { ToolFold } from '../ToolFold';
|
|
21
|
+
import { useArrivalEffect, effectCommand, prefersReducedMotion, resolveArrivalEffect } from '../arrivalEffect';
|
|
22
|
+
import { LiveAnswer } from '../fx/LiveAnswer';
|
|
23
|
+
import { FlapRow } from '../fx/FlapRow';
|
|
24
|
+
import { MatrixRain } from '../fx/MatrixRain';
|
|
25
|
+
import { NetsplitJoin } from '../fx/NetsplitJoin';
|
|
26
|
+
import { OptionsDrawer } from './OptionsDrawer';
|
|
21
27
|
import { useControllerSession, useControllerGlobal } from './controller';
|
|
22
28
|
import { getSelectedPolicyName, setSelectedPolicyName } from '@corenel/harness/guardrail';
|
|
23
29
|
import { loadPersonas, getSelectedPersona, setSelectedPersona } from '@corenel/harness/prompts/config';
|
|
@@ -37,8 +43,10 @@ import { trayCommand, transcriptTurns } from '../commandTurn';
|
|
|
37
43
|
import { composerHistory } from '../composerHistory';
|
|
38
44
|
import { usePendingQueue, addPending, removePending, editPending, setPendingState, getPending, setPendingNote } from './pendingQueue';
|
|
39
45
|
import { fmtTok } from '@corenel/ui-kit/usage';
|
|
46
|
+
import { isCheckpointTool } from '@corenel/protocol';
|
|
47
|
+
import { PlanApprovalCard, SUBMIT_PLAN, planTextOf } from './PlanReview';
|
|
40
48
|
import { useBrand } from '@corenel/ui-kit/host';
|
|
41
|
-
import { IcPlus, IcZap, IcTrash, IcChevron, IcX, IcRefresh, IcKey, IcCheck, IcSliders, IcUser, IcFile,
|
|
49
|
+
import { IcPlus, IcZap, IcTrash, IcChevron, IcX, IcRefresh, IcKey, IcCheck, IcSliders, IcUser, IcFile, IcHelp, IcEdit, IcLayout, Spinner, LiveCursor } from '@corenel/ui-kit/icons';
|
|
42
50
|
/* No NOOP default for onApplyEdit, deliberately. Transcript treats the prop's
|
|
43
51
|
* ABSENCE as "this surface has nothing to apply an edit to" and renders the
|
|
44
52
|
* review row as a statement instead of dead buttons. Substituting a no-op here
|
|
@@ -69,14 +77,14 @@ const PS = {
|
|
|
69
77
|
textDone: { fontSize: 13, color: 'var(--text-tertiary)', textDecoration: 'line-through' },
|
|
70
78
|
};
|
|
71
79
|
export function AgentChat(props) {
|
|
72
|
-
const { controller, sessionId, onSessionChange, sessions, aboveComposer, contextWindow, systemSource, offloaded,
|
|
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;
|
|
73
81
|
const { name: brandName } = useBrand(); // host-injected product name (CorenelHost seam)
|
|
74
82
|
// The engine owns the conversation, run loop, worker, and persistence — this
|
|
75
83
|
// column is a view over ONE controller session. The active session id is
|
|
76
84
|
// CONTROLLED by the host (sessionId/onSessionChange).
|
|
77
85
|
const session = useControllerSession(controller, sessionId ?? '');
|
|
78
86
|
const globalTick = useControllerGlobal(controller); // cross-session changes (titles, run badges)
|
|
79
|
-
const { turns, input, mode, running, live, recovery, error, suggestions, attachments, pendingPerm, pendingAsk, policyNotes, degradedModel, recEvents, plan, pendingCommand } = session;
|
|
87
|
+
const { turns, input, mode, running, live, recovery, error, suggestions, attachments, pendingPerm, pendingAsk, approvalSuggestion, policyNotes, degradedModel, recEvents, plan, pendingCommand, loaded } = session;
|
|
80
88
|
// Thin engine-routed setters so the render below reads like the old local-state
|
|
81
89
|
// version (same call sites, engine-backed).
|
|
82
90
|
const setInput = useCallback((v) => { if (sessionId)
|
|
@@ -113,6 +121,11 @@ export function AgentChat(props) {
|
|
|
113
121
|
// so the scrubber's in-flow folded view reclaims its space.
|
|
114
122
|
const [timelineScrubbed, setTimelineScrubbed] = useState(false);
|
|
115
123
|
const [transcriptView] = useTranscriptView();
|
|
124
|
+
const [transcriptSkin, setTranscriptSkin] = useTranscriptSkin();
|
|
125
|
+
const [arrivalEffect, setArrivalEffect] = useArrivalEffect();
|
|
126
|
+
// The effect that plays: the setting ('match' follows the skin's pairing),
|
|
127
|
+
// and always off when the system asks for reduced motion.
|
|
128
|
+
const fx = resolveArrivalEffect(arrivalEffect, transcriptSkin, prefersReducedMotion());
|
|
116
129
|
// Installed skills, for the "/" command menu (the drawer's SkillsSection loads its
|
|
117
130
|
// own copy). Subscribe so the menu re-renders when a skill is installed/removed;
|
|
118
131
|
// the same signal bumps registryTick so the drawer's tool groups recompute.
|
|
@@ -141,15 +154,6 @@ export function AgentChat(props) {
|
|
|
141
154
|
return subscribeSkills(reload);
|
|
142
155
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
143
156
|
}, [capabilities?.skills, capabilities?.tools]);
|
|
144
|
-
// Discovered MCP tools for the "/" command menu — mount-load from cache, then a
|
|
145
|
-
// forced refresh each time the drawer opens (mirrors the old AgentPanel's
|
|
146
|
-
// mount-load + drawer-open refresh of its shared list), plus after a
|
|
147
|
-
// drawer-driven MCP mutation (onConnectionsChanged, Fix 4) so the slash menu
|
|
148
|
-
// doesn't go stale between drawer opens.
|
|
149
|
-
const [mcpKey, setMcpKey] = useState(0);
|
|
150
|
-
useEffect(() => { if (drawerOpen)
|
|
151
|
-
setMcpKey((k) => k + 1); }, [drawerOpen]);
|
|
152
|
-
const mcpTools = useMcpTools(connections, mcpKey);
|
|
153
157
|
const [ctxMenuOpen, setCtxMenuOpen] = useState(false);
|
|
154
158
|
const fileInputRef = useRef(null);
|
|
155
159
|
const [uploadNotice, setUploadNotice] = useState(null);
|
|
@@ -289,6 +293,27 @@ export function AgentChat(props) {
|
|
|
289
293
|
return c && !dismissed.has(c.turnId) ? c : null;
|
|
290
294
|
}, [turns, dismissed]);
|
|
291
295
|
const shownTurns = useMemo(() => transcriptTurns(turns), [turns]);
|
|
296
|
+
// Whether the conversation already has an answer (Netsplit joins only once).
|
|
297
|
+
const hasAnswer = useMemo(
|
|
298
|
+
// Multimodal (array) content counts too: any non-empty assistant content.
|
|
299
|
+
() => shownTurns.some((t) => t.messages.some((m) => m.role === 'assistant' && (typeof m.content === 'string' ? m.content.trim() !== '' : Array.isArray(m.content) && m.content.length > 0))), [shownTurns]);
|
|
300
|
+
/* Arrival-effect hand-off. Many models deliver the whole answer in one burst
|
|
301
|
+
* a second or two before the turn settles, so the in-progress area vanishes
|
|
302
|
+
* mid-reveal. When it does, the newest settled answer carries on from where
|
|
303
|
+
* the reveal was, then shows plain (Transcript clears it via onHandoffDone).
|
|
304
|
+
* Decided during render, not in an effect, so the full text never flashes
|
|
305
|
+
* for a frame first. In memory only: a reload or another chat shows plain. */
|
|
306
|
+
const liveShownRef = useRef(0);
|
|
307
|
+
const [handoff, setHandoff] = useState(null);
|
|
308
|
+
const [handoffWatch, setHandoffWatch] = useState({ live: !!live, sessionId });
|
|
309
|
+
if (handoffWatch.live !== !!live || handoffWatch.sessionId !== sessionId) {
|
|
310
|
+
setHandoffWatch({ live: !!live, sessionId });
|
|
311
|
+
const settled = handoffWatch.live && !live && handoffWatch.sessionId === sessionId;
|
|
312
|
+
const last = shownTurns[shownTurns.length - 1];
|
|
313
|
+
setHandoff(settled && fx !== 'none' && last ? { turnId: last.id, effect: fx, from: liveShownRef.current } : null);
|
|
314
|
+
if (live)
|
|
315
|
+
liveShownRef.current = 0;
|
|
316
|
+
}
|
|
292
317
|
/* What Up/Down recall. Derived from the turns rather than kept as you type, so
|
|
293
318
|
* it survives a reload and always matches what is on screen. Includes typed
|
|
294
319
|
* commands, prefix restored, because re-running the last one is the single
|
|
@@ -374,17 +399,23 @@ export function AgentChat(props) {
|
|
|
374
399
|
if (sessionId)
|
|
375
400
|
controller.resolvePerm(sessionId, allow, always);
|
|
376
401
|
};
|
|
402
|
+
const respondSuggestion = (c) => {
|
|
403
|
+
if (sessionId)
|
|
404
|
+
controller.respondSuggestion(sessionId, c);
|
|
405
|
+
};
|
|
377
406
|
// The "/" command menu = the generic commands, the host's extras, then every
|
|
378
|
-
// available tool
|
|
407
|
+
// available tool (registry MCP tools included) and skill as an `insert` command that prefills an
|
|
379
408
|
// editable request. Rebuilt when the registries change. `context` appears only
|
|
380
409
|
// when the host supplies an add-context popup to open.
|
|
381
410
|
const slashCommands = useMemo(() => {
|
|
382
411
|
// Order matches the old AgentPanel: Session, Workspace(+host extras), Prompt,
|
|
383
|
-
// Help, then Tools/Skills
|
|
412
|
+
// Help, then Tools/Skills inserts. `help` is generic but lands AFTER the
|
|
384
413
|
// host's slashExtra merge so a host's Workspace/Prompt entries stay ahead of it.
|
|
385
414
|
const beforeExtra = [
|
|
386
415
|
{ name: 'new', hint: 'start a fresh chat', section: 'Session', icon: _jsx(IcPlus, { size: 13 }), kind: 'action' },
|
|
387
416
|
{ 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' },
|
|
418
|
+
{ name: 'effect', hint: 'Change how new text arrives', section: 'Session', icon: _jsx(IcZap, { size: 13 }), kind: 'action' },
|
|
388
419
|
...(renderContextMenu ? [{ name: 'context', hint: 'attach files or selection', section: 'Workspace', icon: _jsx(IcFile, { size: 13 }), kind: 'action' }] : []),
|
|
389
420
|
{ name: 'persona', hint: 'switch the agent persona', section: 'Workspace', icon: _jsx(IcUser, { size: 13 }), kind: 'action' },
|
|
390
421
|
];
|
|
@@ -405,12 +436,9 @@ export function AgentChat(props) {
|
|
|
405
436
|
const skillCmds = skills
|
|
406
437
|
.filter((s) => s.enabled && !staticNames.has(s.name))
|
|
407
438
|
.map((s) => ({ name: s.name, hint: trunc(s.description), section: 'Skills', icon: _jsx(IcZap, { size: 13 }), kind: 'insert', template: `Use the ${s.name} skill to ` }));
|
|
408
|
-
|
|
409
|
-
.filter((t) => !!t.name && !t.error && !staticNames.has(t.name))
|
|
410
|
-
.map((t) => ({ name: t.name, hint: trunc(t.description || t.serverLabel), section: 'MCP', icon: _jsx(IcCode, { size: 13 }), kind: 'insert', template: `Use the ${t.name} MCP tool (${t.serverLabel}) to ` }));
|
|
411
|
-
return [...staticCmds, ...toolCmds, ...skillCmds, ...mcpCmds];
|
|
439
|
+
return [...staticCmds, ...toolCmds, ...skillCmds];
|
|
412
440
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
413
|
-
}, [renderContextMenu, slashExtra, capabilities, skills,
|
|
441
|
+
}, [renderContextMenu, slashExtra, capabilities, skills, registryTick]);
|
|
414
442
|
const slashMap = useMemo(() => Object.fromEntries(slashCommands.map((c) => [c.name, c])), [slashCommands]);
|
|
415
443
|
// Per-send options for the engine. Rebuilt per call so persona/policy/budget are
|
|
416
444
|
// read fresh; `partsOverride` reads through the ref so a toggle right before send
|
|
@@ -467,11 +495,11 @@ export function AgentChat(props) {
|
|
|
467
495
|
};
|
|
468
496
|
// /help renders from the CURRENT resolved slash set, not a static blurb — so
|
|
469
497
|
// every surface (editor, /agent, /craft) lists exactly its own commands. Tools/
|
|
470
|
-
// Skills
|
|
498
|
+
// Skills are excluded (those are insert-a-request shortcuts, not "commands"
|
|
471
499
|
// in the help sense); only the Session/Workspace/Prompt/Help-style entries
|
|
472
500
|
// (the generic set + a host's slashExtra) are listed.
|
|
473
501
|
const buildHelpText = () => {
|
|
474
|
-
const cmds = slashCommands.filter((c) => c.section !== 'Tools' && c.section !== 'Skills'
|
|
502
|
+
const cmds = slashCommands.filter((c) => c.section !== 'Tools' && c.section !== 'Skills');
|
|
475
503
|
const lines = cmds.map((c) => `- \`/${c.name}\` — ${c.hint}`).join('\n');
|
|
476
504
|
return `**${brandName}** — type \`/\` for commands:\n\n${lines}\n\nPick a mode (auto/edit/plan/brainstorm) by the composer.`;
|
|
477
505
|
};
|
|
@@ -487,7 +515,7 @@ export function AgentChat(props) {
|
|
|
487
515
|
else
|
|
488
516
|
pendingLoadsRef.current.push(name);
|
|
489
517
|
};
|
|
490
|
-
const runSlash = (name) => {
|
|
518
|
+
const runSlash = (name, arg = '') => {
|
|
491
519
|
const def = slashMap[name];
|
|
492
520
|
if (!def)
|
|
493
521
|
return 'none';
|
|
@@ -517,6 +545,24 @@ export function AgentChat(props) {
|
|
|
517
545
|
if (sessionId)
|
|
518
546
|
controller.note(sessionId, buildHelpText());
|
|
519
547
|
break;
|
|
548
|
+
case 'skin': {
|
|
549
|
+
// Local only: a note turn is never sent to the model.
|
|
550
|
+
const r = skinCommand(arg, transcriptSkin);
|
|
551
|
+
if (r.next)
|
|
552
|
+
setTranscriptSkin(r.next);
|
|
553
|
+
if (sessionId)
|
|
554
|
+
controller.note(sessionId, r.note);
|
|
555
|
+
break;
|
|
556
|
+
}
|
|
557
|
+
case 'effect': {
|
|
558
|
+
// Local only, like /skin: a note turn is never sent to the model.
|
|
559
|
+
const r = effectCommand(arg, arrivalEffect);
|
|
560
|
+
if (r.next)
|
|
561
|
+
setArrivalEffect(r.next);
|
|
562
|
+
if (sessionId)
|
|
563
|
+
controller.note(sessionId, r.note);
|
|
564
|
+
break;
|
|
565
|
+
}
|
|
520
566
|
default: return 'none';
|
|
521
567
|
}
|
|
522
568
|
return 'handled';
|
|
@@ -558,7 +604,8 @@ export function AgentChat(props) {
|
|
|
558
604
|
setInput(prior);
|
|
559
605
|
}
|
|
560
606
|
if (text.startsWith('/') && images.length === 0) {
|
|
561
|
-
const
|
|
607
|
+
const [cmd, ...rest] = text.slice(1).split(' ');
|
|
608
|
+
const r = runSlash(cmd, rest.join(' '));
|
|
562
609
|
if (r === 'handled') {
|
|
563
610
|
setInput('');
|
|
564
611
|
return;
|
|
@@ -574,8 +621,14 @@ export function AgentChat(props) {
|
|
|
574
621
|
* does -- same guards, same transcript, same everything. It deliberately does
|
|
575
622
|
* NOT go through `send` above, which reads the composer and would dispatch
|
|
576
623
|
* slash commands a second time; the text was already resolved when it was
|
|
577
|
-
* parked. `running` holds firing until the current turn finishes.
|
|
578
|
-
|
|
624
|
+
* parked. `running` holds firing until the current turn finishes.
|
|
625
|
+
*
|
|
626
|
+
* WATCHING COUNTS AS BUSY. The queue is global, not per chat, so a due item
|
|
627
|
+
* would otherwise fire into whatever this column shows: into a finished
|
|
628
|
+
* browser run it continues that run as a chat nobody asked for, and into a
|
|
629
|
+
* daemon watch the send is a no-op -- after takeDue has already removed the
|
|
630
|
+
* item, so it is lost. Busy leaves it queued for a chat that can take it. */
|
|
631
|
+
const { items: laterItems, now: laterNow } = useSendLater((text) => { void sendMessage(text, []); }, running || !!readOnly);
|
|
579
632
|
/* The pending queue: what you typed while a turn was running.
|
|
580
633
|
*
|
|
581
634
|
* Flush is deliberately ONE AT A TIME. Concatenating the queue into a single
|
|
@@ -761,78 +814,104 @@ export function AgentChat(props) {
|
|
|
761
814
|
restoreToolResult: (turnId, blobId, content) => (sessionId ? controller.restoreToolResult(sessionId, turnId, blobId, content) : false),
|
|
762
815
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
763
816
|
}), [sessionId, session.turns]);
|
|
764
|
-
const menuEnabled = features?.sessionMenu !== false;
|
|
817
|
+
const menuEnabled = features?.sessionMenu !== false && !readOnly;
|
|
765
818
|
// Defaults true: every existing surface runs our loop, and only a proxied
|
|
766
819
|
// engine opts out.
|
|
767
820
|
const ownLoop = features?.ownLoop !== false;
|
|
768
821
|
const sessionTitle = sessionList.find((s) => s.id === sessionId)?.title || (title ?? brandName);
|
|
769
822
|
const modeEntries = useMemo(() => modes.map((m) => ({ id: m.id, label: m.label, description: m.description ?? '', accent: m.accent })), [modes]);
|
|
823
|
+
// A host card per live tool, built from the same call shape the settled
|
|
824
|
+
// transcript hands renderToolCard, so a card does not change on settle. A
|
|
825
|
+
// non-null card wins in every skin and effect: never folded, never a board row.
|
|
826
|
+
const liveCards = new Map();
|
|
827
|
+
for (const t of live?.tools ?? []) {
|
|
828
|
+
const card = renderToolCard?.({ id: t.id, type: 'function', function: { name: t.name, arguments: JSON.stringify(t.args || {}) } });
|
|
829
|
+
if (card)
|
|
830
|
+
liveCards.set(t.id, card);
|
|
831
|
+
}
|
|
770
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) {
|
|
771
833
|
e.stopPropagation();
|
|
772
834
|
startRename(sessionId);
|
|
773
|
-
} }, title: sessionId ? 'Double-click to rename' : undefined, children: [_jsx(IcZap, { size: 14 }), _jsx("span", { style: S.sessionName, children: sessionTitle }), _jsx(IcChevron, { size: 12 })] })), running && (_jsxs("span", { style: S.runChip, title: "The model is working \u2014 press Stop to cancel", children: [_jsx(Spinner, { size: 12 }), _jsx("span", { children: runElapsed > 0 ? `${runElapsed}s` : 'working…' })] })), _jsx("div", { style: { flex: 1, minWidth: 8 } }), headerExtras, _jsx("button", { style: S.iconBtn, onClick: newSession, title: "New chat", "aria-label": "New chat", children: _jsx(IcPlus, { size: 16 }) }), _jsx("button", { style: { ...S.iconBtn, ...(drawerOpen ? S.iconBtnActive : null) }, onClick: () => setDrawerOpen((o) => !o), title: fixedIdentity ? 'Display & capabilities' : `Persona, display & capabilities — ${personaLabel}`, "aria-label": "Panel options", children: _jsx(IcSliders, { size: 16 }) }), !embedded && onClose && _jsx("button", { style: S.iconBtn, onClick: onClose, title: "Close (\u2318K to reopen)", "aria-label": "Close", children: _jsx(IcX, { size: 16 }) }), menuEnabled && menuOpen && (_jsxs("div", { style: S.menu, onMouseLeave: () => setMenuOpen(false), children: [_jsxs("button", { style: S.menuItem, onClick: newSession, children: [_jsx(IcPlus, { size: 13 }), " New chat"] }), _jsx("div", { style: S.menuLabel, children: "Recent" }), sessionList.length === 0 && _jsx("div", { style: S.menuEmpty, children: "No saved chats" }), sessionList.slice(0, 12).map((s) => (_jsx("div", { style: S.menuRow, children: renaming && renaming.id === s.id ? (_jsx("input", { autoFocus: true, style: S.renameInput, value: renaming.title, onChange: (e) => setRenaming({ id: s.id, title: e.target.value }), onKeyDown: renameKey, onBlur: commitRename, placeholder: "Session name", "aria-label": "Rename session", onClick: (e) => e.stopPropagation() })) : (_jsxs(_Fragment, { children: [_jsx("button", { style: menuSession(s.id === sessionId), onClick: () => switchSession(s.id), children: s.title || 'Untitled' }), _jsx("button", { style: S.menuDel, onClick: () => startRename(s.id), title: "Rename", children: _jsx(IcEdit, { size: 12 }) }), _jsx("button", { style: S.menuDel, onClick: () => deleteSession(s.id), title: "Delete", children: _jsx(IcTrash, { size: 12 }) })] })) }, s.id)))] }))] }), ownLoop && _jsx(OptionsDrawer, { open: drawerOpen, onClose: () => setDrawerOpen(false), personaName: personaName, onPersona: pickPersona, policyName: policyName, onPolicy: selectPolicy, hideIdentity: !!fixedIdentity, budgetSection: budgetSection?.({ override, setOverride }), settingsExtras: settingsExtras,
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
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'
|
|
781
853
|
? pendingPerm.summary
|
|
782
|
-
: (_jsxs(_Fragment, { children: [pendingPerm.requester === 'you'
|
|
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) => {
|
|
783
859
|
const files = Array.from(e.target.files ?? []);
|
|
784
860
|
e.target.value = '';
|
|
785
861
|
setCtxMenuOpen(false);
|
|
786
862
|
void processFiles(files);
|
|
787
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)
|
|
788
864
|
controller.resolveAsk(sessionId, answers); }, onCancel: () => { if (sessionId)
|
|
789
|
-
controller.resolveAsk(sessionId, null); } })), degradedModel && (_jsx("div", { style: S.budgetWrap, children: _jsxs("span", { style: S.budgetDegraded, children: ["Degraded to ", degradedModel, " (budget)"] }) })),
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
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) })] }));
|
|
836
915
|
}
|
|
837
916
|
// Session-row style is keyed by "active" — kept out of S (which is a flat style
|
|
838
917
|
// map) and typed as its own helper.
|
|
@@ -857,6 +936,9 @@ const S = {
|
|
|
857
936
|
// Wraps the settled Transcript + live-run block so a host-injected LiveTimeline
|
|
858
937
|
// (position: absolute) can overlay them while scrubbed back. `body` already
|
|
859
938
|
// owns the scroll (overflowY: auto) — this is just the positioning context.
|
|
939
|
+
/* Centred in the column like the opener it replaces, so the two states swap
|
|
940
|
+
* in place instead of the conversation jumping when the load lands. */
|
|
941
|
+
loadingTranscript: { display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 8, padding: '48px 16px', color: 'var(--text-tertiary)', fontSize: 13 },
|
|
860
942
|
transcriptWrap: { position: 'relative', display: 'flex', flexDirection: 'column', gap: 16 },
|
|
861
943
|
live: { display: 'flex', flexDirection: 'column', gap: 8 },
|
|
862
944
|
liveToolRow: { display: 'flex', alignItems: 'center', padding: '6px 10px', border: '1px solid var(--border-subtle)', borderRadius: 'var(--radius-md)', background: 'var(--surface-2)' },
|
|
@@ -873,16 +955,22 @@ const S = {
|
|
|
873
955
|
ctxMenu: { margin: '0 10px', padding: 8, background: 'var(--surface-2)', border: '1px solid var(--border-default)', borderRadius: 'var(--radius-md)', display: 'flex', flexDirection: 'column', gap: 4 },
|
|
874
956
|
perm: { border: '1px solid var(--warning)', borderRadius: 'var(--radius-md)', background: 'var(--warning-soft)', padding: 12, display: 'flex', flexDirection: 'column', gap: 8 },
|
|
875
957
|
permHead: { display: 'inline-flex', alignItems: 'center', gap: 7, fontSize: 13, fontWeight: 600, color: 'var(--text-primary)' },
|
|
876
|
-
|
|
958
|
+
// pre-wrap: a submit_plan summary is the whole multi-line plan being approved.
|
|
959
|
+
// Wraps a long path or URL at 390px, and a long plan scrolls inside the card
|
|
960
|
+
// so Allow and Deny stay in view.
|
|
961
|
+
permBody: { fontSize: 13, lineHeight: 1.5, color: 'var(--text-secondary)', whiteSpace: 'pre-wrap', overflowWrap: 'anywhere', maxHeight: '45vh', overflowY: 'auto' },
|
|
877
962
|
permActions: { display: 'flex', gap: 8, flexWrap: 'wrap' },
|
|
878
963
|
permDeny: { height: 30, padding: '0 12px', border: '1px solid var(--border-default)', borderRadius: 'var(--radius-sm)', background: 'var(--surface-1)', color: 'var(--text-secondary)', fontSize: 12.5, fontWeight: 600, cursor: 'pointer' },
|
|
879
964
|
permAllowSession: { height: 30, padding: '0 12px', border: '1px solid var(--border-default)', borderRadius: 'var(--radius-sm)', background: 'var(--surface-1)', color: 'var(--text-secondary)', fontSize: 12.5, fontWeight: 600, cursor: 'pointer' },
|
|
880
965
|
permAllow: { display: 'inline-flex', alignItems: 'center', gap: 5, height: 30, padding: '0 14px', border: 0, borderRadius: 'var(--radius-sm)', background: 'var(--grad-brand)', color: 'var(--text-on-accent, #fff)', fontSize: 12.5, fontWeight: 600, cursor: 'pointer' },
|
|
966
|
+
approvalSuggest: { border: '1px solid var(--border-subtle)', borderRadius: 'var(--radius-md)', background: 'var(--surface-2)', padding: 12, display: 'flex', flexDirection: 'column', gap: 8 },
|
|
881
967
|
// Per-run budget control — the run chip's soft note when a degrade breach
|
|
882
968
|
// switched the in-flight model.
|
|
883
969
|
budgetWrap: { position: 'relative', display: 'flex', alignItems: 'center', gap: 8, padding: '0 10px 6px', flexShrink: 0 },
|
|
884
970
|
budgetDegraded: { fontSize: 11, color: 'var(--warning)' },
|
|
885
971
|
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 },
|
|
886
972
|
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
|
+
readOnlyBar: { display: 'flex', alignItems: 'center', gap: 10, padding: '10px 14px', borderTop: '1px solid var(--border-subtle)', background: 'var(--surface-2)', flexShrink: 0 },
|
|
974
|
+
readOnlyBanner: { flex: 1, minWidth: 0, fontSize: 13, color: 'var(--text-secondary)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' },
|
|
887
975
|
};
|
|
888
976
|
//# sourceMappingURL=AgentChat.js.map
|