@spzxg/cli-ui 0.6.11 → 0.6.13

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 (36) hide show
  1. package/CHANGELOG.md +20 -1
  2. package/README.md +9 -3
  3. package/dist/{cli-ui-embed-CbxWozLO.js → cli-ui-embed-CXaGT9e4.js} +14911 -14636
  4. package/dist/{cli-ui-index-ImgEunSf.js → cli-ui-index-bMQJjEBW.js} +2 -2
  5. package/dist/{cli-ui-jszip.min-DGdGLbxr.js → cli-ui-jszip.min-AMeWMDT5.js} +2 -2
  6. package/dist/{cli-ui-jszip.min-DWmufeZi.js → cli-ui-jszip.min-D_w3a6AF.js} +1 -1
  7. package/dist/{cli-ui-xlsx-s2TcknA1.js → cli-ui-xlsx-DMzew4IO.js} +1 -1
  8. package/dist/cli-ui.css +1 -1
  9. package/dist/cli-ui.js +1 -1
  10. package/package.json +1 -1
  11. package/src/board/CliBoard.tsx +2 -3
  12. package/src/board/FileViewer.tsx +3 -3
  13. package/src/board/FilesPane.tsx +1 -1
  14. package/src/board/fileScope.ts +6 -3
  15. package/src/board/fileSync.ts +12 -4
  16. package/src/board/fileSyncProcessor.ts +4 -3
  17. package/src/board/fileSyncTransport.ts +2 -2
  18. package/src/board/fileWatch.ts +36 -20
  19. package/src/chat/CliChat.tsx +13 -2
  20. package/src/core/client.ts +4 -0
  21. package/src/core/conversation.ts +10 -3
  22. package/src/core/identity.ts +2 -14
  23. package/src/core/sha256.ts +21 -0
  24. package/src/core/subagents.ts +78 -0
  25. package/src/core/types.ts +12 -2
  26. package/src/operator/node/Chat.tsx +10 -4
  27. package/src/operator/node/Layout.tsx +3 -1
  28. package/src/operator/node/SessionRail.tsx +2 -2
  29. package/src/operator/node/Transcript.tsx +7 -2
  30. package/src/operator/node/modals.tsx +13 -2
  31. package/src/operator/node/protocol/types.ts +5 -2
  32. package/src/operator/node/useTaskStream.ts +8 -4
  33. package/src/operator.base.css +35 -0
  34. package/src/operator.css +35 -0
  35. package/src/ui/SubagentChat.tsx +197 -0
  36. package/src/ui/Transcript.tsx +5 -0
@@ -1,4 +1,7 @@
1
1
  "use client";
2
+ import { SubagentChatScope, SubagentHistoryBoundary } from "../../ui/SubagentChat";
3
+ import { parentMessages, type SubagentTranscript } from "../../core/subagents";
4
+ import { jget } from "./api";
2
5
  import { useCallback, useContext, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
3
6
  import type { AskQuestion, AskAnswers } from "./protocol/types";
4
7
  import { Icon } from "./icons";
@@ -79,6 +82,9 @@ export type ChatProps = {
79
82
  export function Chat(props: ChatProps) { return <ChatSession key={props.task.id} {...props} />; }
80
83
  function ChatSession({ project, task, tasks, agents, messages, running, transcriptLoading, older=0, onLoadOlder,
81
84
  controls, configBusy, composer, active=true, onSelectTask, onFork, onSend, onStop, onClear, onReconnect, onAnswer, onCancelQueued }: ChatProps) {
85
+ const parent = useMemo(() => parentMessages(messages), [messages]);
86
+ const loadSubagent = useCallback((id: string, generation: number, before?: string) => jget<SubagentTranscript>(`/api/tasks/${encodeURIComponent(task.id)}/subagents/${encodeURIComponent(id)}?generation=${generation}&limit=200${before ? `&before=${encodeURIComponent(before)}` : ""}`), [task.id]);
87
+ const renderSubagentMessage = useCallback((message: Msg, name: string) => <MessageView m={message} initial={false} hideWho={false} agent={task.agent} agentLabel={name} userLabel="收到的任务" speakerLabel={name} readOnly />, [task.agent]);
82
88
  const scrollRef = useRef<HTMLDivElement>(null);
83
89
  useChatScrollbar(scrollRef, task.id);
84
90
  const [quoteRequest, setQuoteRequest] = useState<Quote|null>(null);
@@ -182,7 +188,7 @@ function ChatSession({ project, task, tasks, agents, messages, running, transcri
182
188
  // reveals RENDER_STEP more above at a time (paging older messages in from the
183
189
  // server once the loaded ones run out) and drops rows far below; scrolling down
184
190
  // brings those back. "Jump to latest" returns to following.
185
- const items = useMemo(() => buildTranscriptItems(messages, older > 0), [messages, older]);
191
+ const items = useMemo(() => buildTranscriptItems(parent, older > 0), [parent, older]);
186
192
  // First / last rendered row by key; null = derived (newest rows).
187
193
  const [win, setWin] = useState<{ start: string | null; end: string | null }>({ start: null, end: null });
188
194
  const { startIdx, endIdx } = useMemo(() => {
@@ -443,11 +449,11 @@ function ChatSession({ project, task, tasks, agents, messages, running, transcri
443
449
  useEffect(()=>{if(!task.parent_task_id && active) scrollToBottom("auto");},[active,task.id]);
444
450
  const queued = <QueuedMessages scopeKey={task.id} messages={messages.filter(message => message.role === "queued")} onCancel={message => stableCancelQueued(message.id)} attachmentUrl={attachment => `/api/tasks/${encodeURIComponent(attachment.taskId)}/uploads/${encodeURIComponent(attachment.name)}`} renderMessage={message => <MessageView m={message} initial={false} hideWho readOnly />} />;
445
451
  return (<FileLinkContext.Provider value={fileScope}>
446
- <>
452
+ <SubagentChatScope scopeKey={task.id} messages={messages} load={loadSubagent} renderMessage={renderSubagentMessage}>
447
453
  <div className="transcript-wrap">
448
454
  <div className="transcript" ref={scrollRef} onScroll={onScroll}>
449
455
  <div className="tw">
450
- <BranchHistory entries={flow.entries} tasks={tasks} agents={agents} onSelect={onSelectTask} onRetry={flow.retry} onQuote={(m,source,selected)=>quote(m,source,selected)} />
456
+ <SubagentHistoryBoundary><BranchHistory entries={flow.entries} tasks={tasks} agents={agents} onSelect={onSelectTask} onRetry={flow.retry} onQuote={(m,source,selected)=>quote(m,source,selected)} /></SubagentHistoryBoundary>
451
457
  {task.parent_task_id && <div className="flow-task-heading current"><span>沿此分支继续</span><strong>{task.title}</strong></div>}
452
458
  {transcriptLoading && messages.length === 0 && (
453
459
  // Snapshot hasn't streamed in yet — sketch a user turn and an agent
@@ -513,7 +519,7 @@ function ChatSession({ project, task, tasks, agents, messages, running, transcri
513
519
  </div>
514
520
  {composer?.show !== false ? <Composer tasks={tasks} onFork={onFork} task={task} agentLabel={agentLabel(agents, task.agent)} disabled={task.started!==1} configBusy={configBusy} running={running} onSend={async text => { const sent = await onSend(text); if (sent && !running) scrollToBottom("auto"); return sent; }} onStop={onStop} onClear={onClear} quoteRequest={quoteRequest} onQuoteHandled={()=>setQuoteRequest(null)} controls={controls}
515
521
  fileUpload={composer?.fileUpload} queued={queued} /> : queued}
516
- </></FileLinkContext.Provider>
522
+ </SubagentChatScope></FileLinkContext.Provider>
517
523
  );
518
524
 
519
525
  }
@@ -88,7 +88,9 @@ export function ColResize({ min, max, onWidth, onReset, side = "left" }: { min:
88
88
  const move = (e: MouseEvent) => {
89
89
  if (!dragging.current) return;
90
90
  if (side === "right") {
91
- const col = ref.current?.nextElementSibling as HTMLElement | null;
91
+ // WorkspaceDrawer keeps display:contents wrappers on desktop; measure the visible rail.
92
+ const next = ref.current?.nextElementSibling as HTMLElement | null;
93
+ const col = next?.querySelector<HTMLElement>(".sess-rail") ?? next;
92
94
  if (!col) return;
93
95
  const right = col.getBoundingClientRect().right;
94
96
  onWidthRef.current(Math.max(min, Math.min(max, Math.round(right - e.clientX))));
@@ -122,7 +122,7 @@ function ContextPane({ task, sessions, running, onClear }: { task: TaskRow; sess
122
122
  );
123
123
  }
124
124
 
125
- export function SessionRail({ project, task, sessions, running, onResolveWithAI, onMerged, onPrCreated, onClear, onCollapse, onSwitchToChat,
125
+ export function SessionRail({ project, task, sessions, running, onResolveWithAI, onMerged, onPrCreated, onClear, onSwitchToChat,
126
126
  tabs, availableTabs, defaultTab, autoContext, watchFiles = true, fileWatchIntervalMs, onFilesChange, onFilesWatch, onBoardContextChange, onEvent, openRequest, onRequestHandled, onQuote, inlineLayout, onExpand, visible = true }: OperatorBoardOptions & {
127
127
  project: ProjectRow; task: TaskRow; sessions: Session[]; running: boolean;
128
128
  onResolveWithAI: (taskId: string) => Promise<ResolveResult>; onMerged?: () => void;
@@ -268,7 +268,7 @@ export function SessionRail({ project, task, sessions, running, onResolveWithAI,
268
268
  </div>
269
269
  <PanelControls toolbarRef={setToolbarHost} available={available} open={panels.open} label={label} onShow={id=>{panels.show(id);setTab(id);inlineLayout?.onActivate();}} onClose={close}
270
270
  upload={available.includes('files')?{onChoose:upload.choose,disabled:upload.busy||!uploader,hint:canUpload?undefined:'启动会话并创建工作区后可上传文件'}:undefined}/>
271
- {inlineLayout ? onExpand&&<button className="rail-collapse" onClick={onExpand} title="展开右侧看板" aria-label="展开看板">{Icon.chevRight()}</button> : <button className="rail-collapse" onClick={onCollapse} title="隐藏面板" aria-label="隐藏看板">{Icon.chevRight()}</button>}
271
+ {inlineLayout && onExpand && <button className="rail-collapse" onClick={onExpand} title="展开右侧看板" aria-label="展开看板">{Icon.chevRight()}</button>}
272
272
  </div>;
273
273
  return <aside className={`sess-rail${inlineLayout?' sess-rail-inline':''}`} aria-label="任务看板" hidden={!contentVisible} style={!contentVisible?{display:'none'}:undefined} onPointerDown={()=>activateBoardState(task.id,owner)} onClickCapture={()=>activateBoardState(task.id,owner)} onFocusCapture={()=>activateBoardState(task.id,owner)}>
274
274
  {inlineLayout ? inlineLayout.tabsHost&&createPortal(railTabs,inlineLayout.tabsHost) : railTabs}
@@ -1,4 +1,6 @@
1
1
  "use client";
2
+ import { subagentFields } from "../../core/subagents";
3
+ import { SubagentEntry } from "../../ui/SubagentChat";
2
4
  import { useContext } from "react";
3
5
  import { OperatorBoardContext } from "./boardInteraction";
4
6
  import { FileLinkContext } from "./fileLinks";
@@ -322,7 +324,7 @@ function QuotedUserText({ text }: { text: string }) {
322
324
  return quoted ? <><ReferenceCard title={quoted.quote.author} text={quoted.quote.text} renderContent={value => <Markdown>{value}</Markdown>} />{quoted.text && <Markdown>{quoted.text}</Markdown>}</> : <Markdown>{text}</Markdown>;
323
325
  }
324
326
 
325
- export const MessageView = memo(function MessageView({ m, initial, hideWho, running, agent, agentLabel = "智能体", onAnswer, onInput, onBrowser, onCancelQueued, onClear, onReconnect, onRetry, onQuote, readOnly }: { readOnly?: boolean; onQuote?: (m: Msg, selected?: string) => void; m: Msg; initial: boolean; hideWho: boolean; running?: boolean; agent?: string | null; agentLabel?: string; onAnswer?: (askId: string, questions: AskQuestion[], answers: AskAnswers) => void; onInput?: (inputId: string, values: string[]) => Promise<string | null>; onBrowser?: (action: "handback") => Promise<string | null>; onCancelQueued?: (pendingId: string) => void; onClear?: () => void; onReconnect?: () => void; onRetry?: (msgId: string) => void }) {
327
+ export const MessageView = memo(function MessageView({ m, initial, hideWho, running, agent, agentLabel = "智能体", userLabel = "你", speakerLabel = "智能体", onAnswer, onInput, onBrowser, onCancelQueued, onClear, onReconnect, onRetry, onQuote, readOnly }: { readOnly?: boolean; userLabel?: string; speakerLabel?: string; onQuote?: (m: Msg, selected?: string) => void; m: Msg; initial: boolean; hideWho: boolean; running?: boolean; agent?: string | null; agentLabel?: string; onAnswer?: (askId: string, questions: AskQuestion[], answers: AskAnswers) => void; onInput?: (inputId: string, values: string[]) => Promise<string | null>; onBrowser?: (action: "handback") => Promise<string | null>; onCancelQueued?: (pendingId: string) => void; onClear?: () => void; onReconnect?: () => void; onRetry?: (msgId: string) => void }) {
326
328
  const openBoard=useContext(OperatorBoardContext);
327
329
  const fileScope=useContext(FileLinkContext);
328
330
  const onShowBrowser=()=>{if(fileScope&&openBoard)openBoard({kind:'browser',taskId:fileScope.taskId});else showTaskBrowser(fileScope?.taskId);};
@@ -345,6 +347,9 @@ export const MessageView = memo(function MessageView({ m, initial, hideWho, runn
345
347
  }
346
348
  if (m.role === "tool") {
347
349
  const data = toolDataOf(m);
350
+ const child = subagentFields(m);
351
+ if (child.subagentMessage) return null;
352
+ if (child.subagent) return <SubagentEntry info={child.subagent} generation={m.generation} />;
348
353
  if (data.input) {
349
354
  return <div className="msg msg-tool" inert={readOnly||undefined}><InputView input={data.input} agentLabel={agentLabel} onSubmit={onInput} onShowBrowser={onShowBrowser} /></div>;
350
355
  }
@@ -445,7 +450,7 @@ export const MessageView = memo(function MessageView({ m, initial, hideWho, runn
445
450
  {!hideWho && (
446
451
  <div className="who">
447
452
  <Avatar who={isUser ? "user" : "cc"} agent={agent} />
448
- {isUser ? "你" : "智能体"}
453
+ {isUser ? userLabel : speakerLabel}
449
454
  {initial && <span className="badge">初始提示</span>}
450
455
  {m.ts != null && <span className="msg-time">{clockTime(m.ts)}</span>}
451
456
  </div>
@@ -200,6 +200,9 @@ export function NewTaskModal({ project, agents, tasks, onClose, onCreate, onOpen
200
200
  const [sendContext, setSendContext] = useState(project.send_context !== 0);
201
201
  const [deps, setDeps] = useState<string[]>([]);
202
202
  const [autoStart, setAutoStart] = useState(false);
203
+ const submitting = useRef(false);
204
+ const [creating, setCreating] = useState(false);
205
+ const [createError, setCreateError] = useState<string | null>(null);
203
206
  // The task's real browser (docs/BROWSER.md): offered only while the feature is on.
204
207
  const showBrowser = clientFeatures().browser;
205
208
  const [browser, setBrowser] = useState(false);
@@ -219,7 +222,14 @@ export function NewTaskModal({ project, agents, tasks, onClose, onCreate, onOpen
219
222
  // launch otherwise, so offering "start now" here would just produce a 400).
220
223
  const agentReady = selAgent ? !agentStateHint(selAgent) : true;
221
224
  const canStart = !blocked && agentReady;
222
- const create = () => can && onCreate({ title: title.trim(), desc: desc.trim(), priority, agent, startNow: startNow && canStart, sendContext, depends_on: deps, auto_start: autoStart && deps.length > 0, browser: showBrowser && browser });
225
+ const create = async () => {
226
+ if (!can || submitting.current) return;
227
+ submitting.current = true; setCreating(true); setCreateError(null);
228
+ try {
229
+ await onCreate({ title: title.trim(), desc: desc.trim(), priority, agent, startNow: startNow && canStart, sendContext, depends_on: deps, auto_start: autoStart && deps.length > 0, browser: showBrowser && browser });
230
+ } catch (error) { setCreateError(error instanceof Error ? error.message : String(error)); }
231
+ finally { submitting.current = false; setCreating(false); }
232
+ };
223
233
  return (
224
234
  <Modal title="新建任务" sub={`${project.name} · 标题与描述构成 ${agentLabel(agents, agent)} 的任务上下文`} onClose={onClose}
225
235
  footer={<>
@@ -227,9 +237,10 @@ export function NewTaskModal({ project, agents, tasks, onClose, onCreate, onOpen
227
237
  title={blocked ? "暂时无法开始 —— 该任务被尚未完成的任务阻塞" : !agentReady ? `${selAgent?.label} ${agentStateHint(selAgent) === "未安装" ? "的命令行工具未安装在服务器上" : "尚未连接"},无法启动会话` : undefined}>
228
238
  <input type="checkbox" checked={startNow && canStart} disabled={!canStart} onChange={(e) => setStartNow(e.target.checked)} /> 立即开始会话
229
239
  </label>
240
+ {createError && <span className="hlp" role="alert" style={{ color: "var(--red)", margin: 0 }}>创建未完成:{createError}</span>}
230
241
  <span className="spacer" />
231
242
  <button className="btn btn-ghost" onClick={onClose}>取消</button>
232
- <button className="btn btn-accent" disabled={!can} onClick={create}>{Icon.plus()} 创建任务</button>
243
+ <button className="btn btn-accent" disabled={!can || creating} onClick={create}>{Icon.plus()} {creating ? "创建任务中…" : "创建任务"}</button>
233
244
  </>}>
234
245
  <div className="field">
235
246
  <div className="lab">标题</div>
@@ -1,3 +1,4 @@
1
+ import type { SubagentInfo, SubagentMessage } from "../../../core/subagents";
1
2
  interface AgentProfile { base_url: string; token: string; model: string; wire_api?: string | null; api?: string | null; reasoning?: string | null }
2
3
 
3
4
  export type Priority = "hi" | "med" | "lo";
@@ -236,8 +237,8 @@ export type StreamEvent =
236
237
  | { type: "session"; sessionId: string }
237
238
  | { type: "model"; model: string }
238
239
  | { type: "assistant"; content: string }
239
- | { type: "tool"; id: string; title: string; detail: string; peek?: ToolPeek; diff?: DiffLine[] }
240
- | { type: "tool_result"; id: string; content: string; isError: boolean; peek?: ToolPeek }
240
+ | { type: "tool"; id: string; title: string; detail: string; peek?: ToolPeek; diff?: DiffLine[]; subagent?: SubagentInfo; subagentMessage?: SubagentMessage }
241
+ | { type: "tool_result"; id: string; content: string; isError: boolean; peek?: ToolPeek; subagent?: SubagentInfo; subagentMessage?: SubagentMessage }
241
242
  | { type: "ask"; id: string; questions: AskQuestion[] }
242
243
  | { type: "ask_answered"; id: string; answers: AskAnswers; awaiting_input?: 0 | 1 }
243
244
  // An input card was raised or settled (the whole card each time; upsert by msgId).
@@ -311,6 +312,8 @@ export type GlobalEvent = GlobalTaskEvent | AgentAuthEvent;
311
312
 
312
313
  // How a tool call is stored (JSON) in a "tool" message's content.
313
314
  export interface ToolData {
315
+ subagent?: SubagentInfo;
316
+ subagentMessage?: SubagentMessage;
314
317
  title: string;
315
318
  detail?: string;
316
319
  result?: string;
@@ -119,8 +119,8 @@ export function useTaskStream({ selTask, selProjRef, agentsRef, setTaskRunning,
119
119
  else if (ev.type === "model") setTasks((prev) => prev.map((x) => (x.id === taskId ? { ...x, resolved_model: ev.model } : x)));
120
120
  else if (ev.type === "assistant") upsertMsg(taskId, { id: ev.msgId ?? `a-${Date.now()}-${Math.random()}`, role: "assistant", content: ev.content, generation: gen, ts: ev.ts });
121
121
  else if (ev.type === "tool") {
122
- const data: ToolData = { title: ev.title, detail: ev.detail, peek: ev.peek, diff: ev.diff };
123
- upsertMsg(taskId, { id: ev.msgId ?? `t-${Date.now()}-${Math.random()}`, role: "tool", content: JSON.stringify(data), generation: gen, toolId: ev.id });
122
+ const data: ToolData = { title: ev.title, detail: ev.detail, peek: ev.peek, diff: ev.diff, subagent: ev.subagent, subagentMessage: ev.subagentMessage };
123
+ upsertMsg(taskId, { id: ev.msgId ?? `t-${Date.now()}-${Math.random()}`, role: "tool", content: JSON.stringify(data), generation: gen, toolId: ev.id, ts: ev.ts });
124
124
  } else if (ev.type === "tool_result") {
125
125
  // Match by DB message id (works on snapshot-loaded messages too), falling
126
126
  // back to the in-memory tool_use id. Newest first: results land near the end.
@@ -128,10 +128,14 @@ export function useTaskStream({ selTask, selProjRef, agentsRef, setTaskRunning,
128
128
  const arr = prev[taskId] ?? [];
129
129
  let i = arr.length - 1;
130
130
  while (i >= 0 && (arr[i].role !== "tool" || (arr[i].id !== ev.msgId && arr[i].toolId !== ev.id))) i--;
131
- if (i < 0) return prev;
131
+ if (i < 0) {
132
+ if (!ev.msgId || (!ev.subagent && !ev.subagentMessage)) return prev;
133
+ const data: ToolData = { title: ev.subagent?.title ?? ev.subagentMessage?.tool?.title ?? "子代理消息", subagent: ev.subagent, subagentMessage: ev.subagentMessage };
134
+ return { ...prev, [taskId]: [...arr, { id: ev.msgId, role: "tool", content: JSON.stringify(data), generation: gen, toolId: ev.id, ts: ev.ts }] };
135
+ }
132
136
  const m = arr[i];
133
137
  let content: string;
134
- try { const d = JSON.parse(m.content) as ToolData; d.result = ev.content; d.isError = ev.isError; if (ev.peek) d.peek = ev.peek; content = JSON.stringify(d); } catch { return prev; }
138
+ try { const d = JSON.parse(m.content) as ToolData; d.result = ev.content; d.isError = ev.isError; if (ev.subagent) d.subagent = ev.subagent; if (ev.subagentMessage) d.subagentMessage = ev.subagentMessage; if (ev.peek) d.peek = ev.peek; content = JSON.stringify(d); } catch { return prev; }
135
139
  const next = arr.slice();
136
140
  next[i] = { ...m, content };
137
141
  return { ...prev, [taskId]: next };
@@ -4,6 +4,35 @@
4
4
  (ported from the Claude Design hi-fi prototype)
5
5
  ========================================================================== */
6
6
 
7
+ /* Native CLI subagents share the transcript renderer and keep their own viewer. */
8
+ .subagent-chat-scope{display:flex;position:relative;flex:1;min-height:0;min-width:0;overflow:hidden;}
9
+ .subagent-chat-main{display:flex;flex-direction:column;flex:1;min-height:0;min-width:0;}
10
+ .subagent-chat-main>.session{flex:1;min-height:0;}
11
+ .subagent-entry{display:flex;align-items:center;gap:9px;width:100%;padding:11px 13px;border:1px solid var(--line-strong);border-radius:var(--r);background:var(--surface);color:var(--ink);text-align:left;}
12
+ .subagent-entry:not(:disabled):hover{border-color:var(--accent);background:var(--accent-soft);}
13
+ .subagent-entry:disabled{cursor:default;}
14
+ .subagent-entry-icon{display:flex;color:var(--accent);}
15
+ .subagent-entry-name{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-weight:600;}
16
+ .subagent-entry svg{flex:none;width:16px;height:16px;}
17
+ .subagent-status{font-size:11px;color:var(--ink-3);white-space:nowrap;}
18
+ .subagent-status.running{color:var(--accent);}
19
+ .subagent-status.completed{color:var(--green);}
20
+ .subagent-status.failed{color:var(--red);}
21
+ .subagent-chat-panel{display:flex;flex-direction:column;position:relative;flex:0 0 46%;width:46%;min-width:0;max-width:560px;min-height:0;border-left:1px solid var(--line-strong);background:var(--panel);z-index:30;}
22
+ .subagent-chat-header{display:flex;align-items:center;gap:10px;padding:14px 16px;border-bottom:1px solid var(--line);}
23
+ .subagent-chat-header>div{display:flex;flex:1;min-width:0;flex-direction:column;gap:3px;}
24
+ .subagent-chat-header strong{font-size:14px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
25
+ .subagent-chat-caption{font-size:10px;color:var(--ink-3);}
26
+ .subagent-chat-header .icon-btn{flex:none;}
27
+ .subagent-chat-transcript{min-height:0;flex:1;overflow:auto;overscroll-behavior:contain;}
28
+ .subagent-chat-transcript .tw{width:100%;max-width:none;padding:16px;}
29
+ .subagent-chat-transcript .msg{max-width:100%;}
30
+ .subagent-chat-note{font-size:12px;color:var(--ink-3);line-height:1.6;margin-bottom:14px;overflow-wrap:anywhere;}
31
+ .subagent-chat-note.error{color:var(--red);}
32
+ .subagent-chat-latest{align-self:center;margin:6px 0;}
33
+ .subagent-chat-scrim{position:absolute;inset:0;background:rgba(0,0,0,.25);z-index:29;}
34
+ .subagent-chat-scope.narrow>.subagent-chat-panel{position:absolute;inset:0 0 0 auto;width:min(100%,560px);max-width:100%;box-shadow:-8px 0 24px rgba(0,0,0,.16);}
35
+
7
36
  :root{
8
37
  /* ---- cool near-black "mission control" palette (dark = default) ---- */
9
38
  --bg:#080a0e;
@@ -1842,6 +1871,12 @@ button.pop-item:hover{background:var(--surface-2);}
1842
1871
  [data-chat-layout]>.cli-refs,[data-chat-layout]>.cli-queue{width:calc(100% - 2 * var(--cli-chat-gutter) - var(--cli-chat-scrollbar,0px));max-width:var(--cli-chat-content-max);margin-inline:auto;margin-inline-start:max(var(--cli-chat-gutter),calc((100% - var(--cli-chat-scrollbar,0px) - var(--cli-chat-content-max)) / 2));flex-shrink:0;}
1843
1872
  @container chat-column (max-width:900px){[data-chat-layout]>*{--cli-chat-content-max:100%;}}
1844
1873
  @container chat-column (max-width:640px){[data-chat-layout]>*{--cli-chat-gutter:12px;}.session [data-chat-layout]>.composer{padding-inline-end:var(--cli-chat-gutter);}}
1874
+ @media (min-width:761px){
1875
+ [data-chat-layout]{--cli-chat-gutter:32px;}
1876
+ .session [data-chat-layout] .composer{padding-inline:var(--cli-chat-gutter);padding-inline-end:calc(var(--cli-chat-gutter) + var(--cli-chat-scrollbar,0px));}
1877
+ [data-chat-layout] .composer>.composer-inner{width:100%;max-width:var(--cli-chat-content-max);margin-inline:auto;}
1878
+ @container chat-column (max-width:640px){[data-chat-layout]>*{--cli-chat-gutter:24px;}.session [data-chat-layout] .composer{padding-inline-end:var(--cli-chat-gutter);}}
1879
+ }
1845
1880
  .task-origin{display:flex;align-items:center;gap:5px;font-size:10px;color:var(--ink-3);padding-top:7px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
1846
1881
  .fork-sources{display:flex;flex-wrap:wrap;gap:6px;padding:12px 14px 0;}
1847
1882
  .fork-sources>span{max-width:100%;display:flex;align-items:center;gap:6px;padding:6px 8px;border:1px solid var(--line);background:var(--surface-2);border-radius:8px;font-size:11px;color:var(--ink-2);overflow-wrap:anywhere;}
package/src/operator.css CHANGED
@@ -6,6 +6,35 @@
6
6
  (ported from the Claude Design hi-fi prototype)
7
7
  ========================================================================== */
8
8
 
9
+ /* Native CLI subagents share the transcript renderer and keep their own viewer. */
10
+ .cli-ui .subagent-chat-scope{display:flex;position:relative;flex:1;min-height:0;min-width:0;overflow:hidden;}
11
+ .cli-ui .subagent-chat-main{display:flex;flex-direction:column;flex:1;min-height:0;min-width:0;}
12
+ .cli-ui .subagent-chat-main>.session{flex:1;min-height:0;}
13
+ .cli-ui .subagent-entry{display:flex;align-items:center;gap:9px;width:100%;padding:11px 13px;border:1px solid var(--line-strong);border-radius:var(--r);background:var(--surface);color:var(--ink);text-align:left;}
14
+ .cli-ui .subagent-entry:not(:disabled):hover{border-color:var(--accent);background:var(--accent-soft);}
15
+ .cli-ui .subagent-entry:disabled{cursor:default;}
16
+ .cli-ui .subagent-entry-icon{display:flex;color:var(--accent);}
17
+ .cli-ui .subagent-entry-name{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-weight:600;}
18
+ .cli-ui .subagent-entry svg{flex:none;width:16px;height:16px;}
19
+ .cli-ui .subagent-status{font-size:11px;color:var(--ink-3);white-space:nowrap;}
20
+ .cli-ui .subagent-status.running{color:var(--accent);}
21
+ .cli-ui .subagent-status.completed{color:var(--green);}
22
+ .cli-ui .subagent-status.failed{color:var(--red);}
23
+ .cli-ui .subagent-chat-panel{display:flex;flex-direction:column;position:relative;flex:0 0 46%;width:46%;min-width:0;max-width:560px;min-height:0;border-left:1px solid var(--line-strong);background:var(--panel);z-index:30;}
24
+ .cli-ui .subagent-chat-header{display:flex;align-items:center;gap:10px;padding:14px 16px;border-bottom:1px solid var(--line);}
25
+ .cli-ui .subagent-chat-header>div{display:flex;flex:1;min-width:0;flex-direction:column;gap:3px;}
26
+ .cli-ui .subagent-chat-header strong{font-size:14px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
27
+ .cli-ui .subagent-chat-caption{font-size:10px;color:var(--ink-3);}
28
+ .cli-ui .subagent-chat-header .icon-btn{flex:none;}
29
+ .cli-ui .subagent-chat-transcript{min-height:0;flex:1;overflow:auto;overscroll-behavior:contain;}
30
+ .cli-ui .subagent-chat-transcript .tw{width:100%;max-width:none;padding:16px;}
31
+ .cli-ui .subagent-chat-transcript .msg{max-width:100%;}
32
+ .cli-ui .subagent-chat-note{font-size:12px;color:var(--ink-3);line-height:1.6;margin-bottom:14px;overflow-wrap:anywhere;}
33
+ .cli-ui .subagent-chat-note.error{color:var(--red);}
34
+ .cli-ui .subagent-chat-latest{align-self:center;margin:6px 0;}
35
+ .cli-ui .subagent-chat-scrim{position:absolute;inset:0;background:rgba(0,0,0,.25);z-index:29;}
36
+ .cli-ui .subagent-chat-scope.narrow>.subagent-chat-panel{position:absolute;inset:0 0 0 auto;width:min(100%,560px);max-width:100%;box-shadow:-8px 0 24px rgba(0,0,0,.16);}
37
+
9
38
  .cli-ui{
10
39
  /* ---- cool near-black "mission control" palette (dark = default) ---- */
11
40
  --bg:#080a0e;
@@ -1842,6 +1871,12 @@
1842
1871
  .cli-ui [data-chat-layout]>.cli-refs,.cli-ui [data-chat-layout]>.cli-queue{width:calc(100% - 2 * var(--cli-chat-gutter) - var(--cli-chat-scrollbar,0px));max-width:var(--cli-chat-content-max);margin-inline:auto;margin-inline-start:max(var(--cli-chat-gutter),calc((100% - var(--cli-chat-scrollbar,0px) - var(--cli-chat-content-max)) / 2));flex-shrink:0;}
1843
1872
  @container chat-column (max-width:900px){.cli-ui [data-chat-layout]>*{--cli-chat-content-max:100%;}}
1844
1873
  @container chat-column (max-width:640px){.cli-ui [data-chat-layout]>*{--cli-chat-gutter:12px;}.cli-ui .session [data-chat-layout]>.composer{padding-inline-end:var(--cli-chat-gutter);}}
1874
+ @media (min-width:761px){
1875
+ .cli-ui [data-chat-layout]{--cli-chat-gutter:32px;}
1876
+ .cli-ui .session [data-chat-layout] .composer{padding-inline:var(--cli-chat-gutter);padding-inline-end:calc(var(--cli-chat-gutter) + var(--cli-chat-scrollbar,0px));}
1877
+ .cli-ui [data-chat-layout] .composer>.composer-inner{width:100%;max-width:var(--cli-chat-content-max);margin-inline:auto;}
1878
+ @container chat-column (max-width:640px){.cli-ui [data-chat-layout]>*{--cli-chat-gutter:24px;}.cli-ui .session [data-chat-layout] .composer{padding-inline-end:var(--cli-chat-gutter);}}
1879
+ }
1845
1880
  .cli-ui .task-origin{display:flex;align-items:center;gap:5px;font-size:10px;color:var(--ink-3);padding-top:7px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
1846
1881
  .cli-ui .fork-sources{display:flex;flex-wrap:wrap;gap:6px;padding:12px 14px 0;}
1847
1882
  .cli-ui .fork-sources>span{max-width:100%;display:flex;align-items:center;gap:6px;padding:6px 8px;border:1px solid var(--line);background:var(--surface-2);border-radius:8px;font-size:11px;color:var(--ink-2);overflow-wrap:anywhere;}
@@ -0,0 +1,197 @@
1
+ import { createContext, useCallback, useContext, useEffect, useLayoutEffect, useMemo, useRef, useState, type ReactNode } from "react";
2
+ import type { Msg } from "../core/types";
3
+ import { childMessages, subagentFields, type LoadSubagent, type SubagentInfo, type SubagentTranscript } from "../core/subagents";
4
+ import { Icon } from "./icons";
5
+
6
+ const STATUS = { running: "运行中", completed: "已完成", failed: "失败", cancelled: "已取消", unknown: "状态未知" };
7
+ type Selection = { scope: string; generation: number; info: SubagentInfo };
8
+ const SubagentContext = createContext<((info: SubagentInfo, generation: number) => void) | null>(null);
9
+
10
+ export function SubagentHistoryBoundary({ children }: { children: ReactNode }) { return <SubagentContext.Provider value={null}>{children}</SubagentContext.Provider>; }
11
+
12
+ export function SubagentEntry({ info, generation }: { info: SubagentInfo; generation: number }) {
13
+ const open = useContext(SubagentContext);
14
+ return <div className="msg msg-tool">
15
+ <button type="button" className="subagent-entry" disabled={!open} onClick={() => open?.(info, generation)} aria-label={`查看子代理:${info.title}`}>
16
+ <span className="subagent-entry-icon">{Icon.spark()}</span>
17
+ <span className="subagent-entry-name">{info.title}</span>
18
+ <span className={`subagent-status ${info.status}`}>{STATUS[info.status] ?? STATUS.unknown}</span>
19
+ {Icon.chevRight()}
20
+ </button>
21
+ </div>;
22
+ }
23
+
24
+ /** One shared viewer for embedded Chat and the native Operator chat. It never controls execution. */
25
+ export function SubagentChatScope({ scopeKey, messages, load, renderMessage, children }: {
26
+ scopeKey: string; messages: Msg[]; load?: LoadSubagent;
27
+ renderMessage: (message: Msg, agentName: string) => ReactNode; children: ReactNode;
28
+ }) {
29
+ const [selection, setSelection] = useState<Selection | null>(null);
30
+ const selected = selection?.scope === scopeKey ? selection : null;
31
+ const open = useCallback((info: SubagentInfo, generation: number) => setSelection({ scope: scopeKey, generation, info }), [scopeKey]);
32
+ const close = useCallback(() => setSelection(null), []);
33
+ const root = useRef<HTMLDivElement>(null);
34
+ const [narrow, setNarrow] = useState(true);
35
+ useLayoutEffect(() => {
36
+ const element = root.current;
37
+ if (!element) return;
38
+ const measure = () => { const width = element.getBoundingClientRect().width; if (width > 0) setNarrow(width < 900); };
39
+ measure();
40
+ const observer = typeof ResizeObserver === "undefined" ? null : new ResizeObserver(measure);
41
+ observer?.observe(element);
42
+ return () => observer?.disconnect();
43
+ }, []);
44
+ return <SubagentContext.Provider value={open}>
45
+ <div ref={root} className={`subagent-chat-scope${selected ? " has-subagent" : ""}${narrow ? " narrow" : ""}`}>
46
+ <div className="subagent-chat-main" inert={!!selected && narrow || undefined}>{children}</div>
47
+ {selected && <SubagentPanel key={`${scopeKey}:${selected.generation}:${selected.info.id}`} selection={selected} messages={messages} load={load} renderMessage={renderMessage} onClose={close} modal={narrow} />}
48
+ </div>
49
+ </SubagentContext.Provider>;
50
+ }
51
+
52
+ function SubagentPanel({ selection, messages, load, renderMessage, onClose, modal }: {
53
+ selection: Selection; messages: Msg[]; load?: LoadSubagent;
54
+ renderMessage: (message: Msg, agentName: string) => ReactNode; onClose: () => void; modal: boolean;
55
+ }) {
56
+ const [snapshot, setSnapshot] = useState<(SubagentTranscript & { before?: string }) | null>(null);
57
+ const [loadingOlder, setLoadingOlder] = useState(false);
58
+ const [loading, setLoading] = useState(!!load);
59
+ const [error, setError] = useState<string | null>(null);
60
+ const [attempt, setAttempt] = useState(0);
61
+ const panel = useRef<HTMLElement>(null);
62
+ const closeButton = useRef<HTMLButtonElement>(null);
63
+ const scroll = useRef<HTMLDivElement>(null);
64
+ const follow = useRef(true);
65
+ const [following, setFollowing] = useState(true);
66
+ const [shownCount, setShownCount] = useState(120);
67
+ const { id } = selection.info;
68
+
69
+ useEffect(() => {
70
+ let disposed = false;
71
+ let timer: ReturnType<typeof setTimeout> | undefined;
72
+ if (!load) { setLoading(false); return; }
73
+ setLoading(true); setError(null);
74
+ const refresh = async (initial = false) => {
75
+ if (disposed) return;
76
+ if (!initial && document.visibilityState === "hidden") { timer = setTimeout(() => void refresh(), 5000); return; }
77
+ try {
78
+ const value = await load(id, selection.generation);
79
+ if (value.subagent?.id !== id || !Array.isArray(value.messages)) throw new Error("子代理记录与当前选择不匹配");
80
+ if (disposed) return;
81
+ setSnapshot(previous => {
82
+ const overlap = previous?.messages.some(old => value.messages.some(row => row.id === old.id));
83
+ const byId = new Map((previous?.messages ?? []).map(row => [row.id, row]));
84
+ for (const row of value.messages) byId.set(row.id, row);
85
+ return { ...value, messages: [...byId.values()],
86
+ older: overlap ? previous?.older ?? value.older : value.older,
87
+ before: overlap ? previous?.before : value.messages[0]?.id };
88
+ }); setError(null);
89
+ if (value.refreshable && ["running", "unknown"].includes(value.subagent.status)) timer = setTimeout(() => void refresh(), 5000);
90
+ } catch (cause) {
91
+ if (!disposed) setError(cause instanceof Error ? cause.message : String(cause));
92
+ } finally { if (!disposed) setLoading(false); }
93
+ };
94
+ void refresh(true);
95
+ return () => { disposed = true; if (timer) clearTimeout(timer); };
96
+ }, [load, id, selection.generation, attempt]);
97
+
98
+ const info = useMemo(() => {
99
+ let latest = snapshot?.subagent ?? selection.info;
100
+ for (const message of messages) {
101
+ if (message.generation !== selection.generation) continue;
102
+ const candidate = subagentFields(message).subagent;
103
+ if (candidate?.id === id && (candidate.updatedAt ?? 0) >= (latest.updatedAt ?? 0)) latest = candidate;
104
+ }
105
+ return latest;
106
+ }, [messages, snapshot, selection, id]);
107
+ const rows = useMemo(() => {
108
+ const merged = new Map((snapshot?.messages ?? []).map(row => [row.id, row]));
109
+ for (const row of childMessages(messages, id, selection.generation)) {
110
+ const previous = merged.get(row.id);
111
+ const updated = (value: typeof row) => {
112
+ const fields = subagentFields(value);
113
+ return fields.subagentMessage?.updatedAt ?? fields.subagent?.updatedAt ?? 0;
114
+ };
115
+ if (!previous || updated(row) >= updated(previous)) merged.set(row.id, row);
116
+ }
117
+ return childMessages([...merged.values()], id, selection.generation).sort((a, b) => (a.ts ?? a.created_at ?? 0) - (b.ts ?? b.created_at ?? 0)).map(row => {
118
+ const child = subagentFields(row).subagentMessage;
119
+ return { id: row.id, generation: row.generation, ts: row.ts ?? row.created_at,
120
+ role: child?.role ?? "tool", content: child?.tool ? JSON.stringify(child.tool) : child?.content ?? row.content } as Msg;
121
+ });
122
+ }, [snapshot, messages, id, selection.generation]);
123
+ const shown = rows.slice(-shownCount);
124
+ const promptAlreadyShown = rows.some(row => row.role === "user" && row.content === info.task);
125
+ const loadOlder = async () => {
126
+ if (rows.length > shownCount) { setShownCount(value => value + 120); return; }
127
+ if (!load || !snapshot?.before || loadingOlder) return;
128
+ const before = snapshot.before;
129
+ setLoadingOlder(true); setError(null);
130
+ try {
131
+ const value = await load(id, selection.generation, before);
132
+ if (value.subagent?.id !== id || !Array.isArray(value.messages)) throw new Error("子代理记录与当前选择不匹配");
133
+ setSnapshot(previous => {
134
+ const byId = new Map(value.messages.map(row => [row.id, row]));
135
+ for (const row of previous?.messages ?? []) byId.set(row.id, row);
136
+ return { ...(previous ?? value), messages: [...byId.values()],
137
+ ...(previous?.before === before ? { older: value.older, before: value.messages[0]?.id ?? before } : {}) };
138
+ });
139
+ setShownCount(count => count + 200);
140
+ } catch (cause) { setError(cause instanceof Error ? cause.message : String(cause)); }
141
+ finally { setLoadingOlder(false); }
142
+ };
143
+
144
+ useEffect(() => {
145
+ const previous = document.activeElement as HTMLElement | null;
146
+ closeButton.current?.focus({ preventScroll: true });
147
+ return () => {
148
+ if ((document.activeElement === document.body || panel.current?.contains(document.activeElement)) && previous?.isConnected && !previous.closest("[inert],[hidden]")) previous.focus({ preventScroll: true });
149
+ };
150
+ }, []);
151
+ useEffect(() => {
152
+ const onKey = (event: KeyboardEvent) => {
153
+ if (event.defaultPrevented) return;
154
+ const other = (document.activeElement as HTMLElement | null)?.closest('[role="dialog"]');
155
+ if (other && other !== panel.current) return;
156
+ if (event.key === "Escape") { event.preventDefault(); onClose(); return; }
157
+ if (!modal || event.key !== "Tab" || !panel.current) return;
158
+ const items = [...panel.current.querySelectorAll<HTMLElement>('button:not(:disabled),a[href],input:not(:disabled),textarea:not(:disabled),[tabindex="0"]')]
159
+ .filter(item => !item.closest("[hidden],[inert]") && getComputedStyle(item).display !== "none");
160
+ const first = items[0], last = items.at(-1);
161
+ if (!first || !last) return;
162
+ if (!panel.current.contains(document.activeElement) || (event.shiftKey && document.activeElement === first)) { event.preventDefault(); (event.shiftKey ? last : first).focus(); }
163
+ else if (!event.shiftKey && document.activeElement === last) { event.preventDefault(); first.focus(); }
164
+ };
165
+ window.addEventListener("keydown", onKey);
166
+ return () => window.removeEventListener("keydown", onKey);
167
+ }, [modal, onClose]);
168
+ useLayoutEffect(() => { if (follow.current && scroll.current) scroll.current.scrollTop = scroll.current.scrollHeight; }, [rows, loading]);
169
+
170
+ return <>
171
+ {modal && <button className="subagent-chat-scrim" type="button" tabIndex={-1} aria-label="收起子代理详情" onClick={onClose} />}
172
+ <aside ref={panel} className="subagent-chat-panel" role="dialog" aria-modal={modal || undefined} aria-label={`子代理:${info.title}`}>
173
+ <div className="subagent-chat-header">
174
+ <div><span className="subagent-chat-caption">子代理</span><strong>{info.title}</strong></div>
175
+ <span className={`subagent-status ${info.status}`}>{STATUS[info.status] ?? STATUS.unknown}</span>
176
+ <button ref={closeButton} type="button" className="icon-btn" title="关闭子代理详情" aria-label="关闭子代理详情" onClick={onClose}>{Icon.x()}</button>
177
+ </div>
178
+ <div ref={scroll} className="subagent-chat-transcript transcript" onScroll={() => {
179
+ const element = scroll.current;
180
+ follow.current = !!element && element.scrollHeight - element.scrollTop - element.clientHeight < 60;
181
+ setFollowing(follow.current);
182
+ }}>
183
+ <div className="tw">
184
+ {loading && <div className="subagent-chat-note" role="status">正在读取子代理记录…</div>}
185
+ {error && <div className="subagent-chat-note error" role="alert">读取历史失败:{error} <button className="btn btn-ghost btn-sm" onClick={() => setAttempt(value => value + 1)}>重试</button></div>}
186
+ {!load && <div className="subagent-chat-note">当前接入未提供子代理历史接口,以下为已加载的记录。</div>}
187
+ {info.detailAvailability !== "full" && <div className="subagent-chat-note">{info.detailReason || (info.detailAvailability === "unavailable" ? "此智能体暂未提供子代理运行过程。" : "此智能体仅提供了部分运行记录。")}</div>}
188
+ {info.task && !promptAlreadyShown && renderMessage({ id: `prompt:${id}`, role: "user", content: info.task, generation: selection.generation }, info.title)}
189
+ {(rows.length > shownCount || (snapshot?.older ?? 0) > 0) && <div className="load-older"><button type="button" className="btn btn-ghost btn-sm" disabled={loadingOlder} onClick={() => { follow.current = false; setFollowing(false); void loadOlder(); }}>{loadingOlder ? "正在读取更早的记录…" : "加载更早的子代理记录"}</button></div>}
190
+ {shown.map(row => <div key={row.id}>{renderMessage(row, info.title)}</div>)}
191
+ {!loading && !error && !rows.length && <div className="subagent-chat-note">尚无可展示的运行记录。</div>}
192
+ </div>
193
+ </div>
194
+ {!following && <button type="button" className="btn btn-ghost btn-sm subagent-chat-latest" onClick={() => { follow.current = true; setFollowing(true); if (scroll.current) scroll.current.scrollTop = scroll.current.scrollHeight; }}>跳到最新</button>}
195
+ </aside>
196
+ </>;
197
+ }
@@ -1,5 +1,7 @@
1
1
  // 转录里的一行。移植自 operator-oss/app/orchestrator/Transcript.tsx,类名、结构、文案保持一致;
2
2
  // 去掉了对话任务用不到的部分(/clear、重新登录、浏览器交还……这些动作 center 不提供)。
3
+ import { subagentFields } from "../core/subagents";
4
+ import { SubagentEntry } from "./SubagentChat";
3
5
  import { memo, useContext, useId, useState } from "react";
4
6
  import { FileOpenContext, FileLinkContext } from "./fileLinks";
5
7
  import type { AskAnswers, AskQuestion, InputRequest, Msg, ToolData, ToolPeek } from "../core/types";
@@ -370,6 +372,9 @@ export const MessageView = memo(function MessageView({ m, initial: isInitial, hi
370
372
  }
371
373
  if (m.role === "tool") {
372
374
  const data = toolDataOf(m);
375
+ const child = subagentFields(m);
376
+ if (child.subagentMessage) return null;
377
+ if (child.subagent) return <SubagentEntry info={child.subagent} generation={m.generation} />;
373
378
  if (data.input) return <div className="msg msg-tool"><InputView input={data.input} agentLabel={agentLabel} onSubmit={handlers.onInput} onBrowser={handlers.onShowBrowser} /></div>;
374
379
  if (data.ask) return <div className="msg msg-tool"><AskView data={data} agentLabel={agentLabel} onAnswer={handlers.onAnswer ? (answers) => handlers.onAnswer!(data.ask?.id || m.toolId || "", data.ask?.questions ?? [], answers) : undefined} /></div>;
375
380
  return <div className="msg msg-tool"><ToolView data={data} /></div>;