@spzxg/cli-ui 0.6.1 → 0.6.2
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/README.md +8 -2
- package/dist/{cli-ui-embed-Dk2QGHxn.js → cli-ui-embed-6IJ_jRVh.js} +11108 -11031
- package/dist/{cli-ui-index-BFpRZXn4.js → cli-ui-index-BY5i3wId.js} +2 -2
- package/dist/{cli-ui-jszip.min-B9tqMA-e.js → cli-ui-jszip.min-BZsUwI-s.js} +1 -1
- package/dist/{cli-ui-jszip.min-DIavJzUG.js → cli-ui-jszip.min-m01fexHW.js} +2 -2
- package/dist/{cli-ui-xlsx-BKHhqcoa.js → cli-ui-xlsx-CpWicJLT.js} +1 -1
- package/dist/cli-ui.css +1 -1
- package/dist/cli-ui.js +1 -1
- package/package.json +1 -1
- package/src/chat/CliChat.tsx +41 -24
- package/src/chat/Composer.tsx +33 -22
- package/src/core/client.ts +8 -0
- package/src/core/config.ts +11 -15
- package/src/core/conversation.ts +1 -1
- package/src/styles.css +31 -0
- package/src/ui/icons.tsx +1 -0
package/dist/cli-ui.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { C as a, b as i, d as r, e as t, f as o, h as C, i as l, j as n, k as m, m as d, l as u, n as w, o as B, p as f } from "./cli-ui-embed-
|
|
1
|
+
import { C as a, b as i, d as r, e as t, f as o, h as C, i as l, j as n, k as m, m as d, l as u, n as w, o as B, p as f } from "./cli-ui-embed-6IJ_jRVh.js";
|
|
2
2
|
export {
|
|
3
3
|
a as CliBoard,
|
|
4
4
|
i as CliBrowserView,
|
package/package.json
CHANGED
package/src/chat/CliChat.tsx
CHANGED
|
@@ -7,7 +7,7 @@ import type { CliEvent, CliStore, CliStoreOptions } from "../core/store.js";
|
|
|
7
7
|
import { useCliState, useCliStore, useCliStoreContext, useComponentEvents, withOwnProvider, type ResultHandlers } from "../core/provider.js";
|
|
8
8
|
import { boardContextBlock, serializeBlock, viewingBlock } from "../core/blocks.js";
|
|
9
9
|
import { convKey, emptyConv, isChatTask, isNewInJob, isRunning, parseConvKey, runningTurn, TERMINAL_JOB, viewMessages } from "../core/conversation.js";
|
|
10
|
-
import type { AskAnswers, AskQuestion, Msg } from "../core/types.js";
|
|
10
|
+
import type { AskAnswers, AskQuestion, CenterTask, Msg } from "../core/types.js";
|
|
11
11
|
import { Icon } from "../ui/icons.js";
|
|
12
12
|
import { agentLabel as labelOf, fmtTokens, formatAnswersText, modelLabel } from "../ui/format.js";
|
|
13
13
|
import { AgentBadge, Avatar, Skel, StatusDot, type DotCls } from "../ui/shared.js";
|
|
@@ -174,17 +174,27 @@ function ChatInner({ jobId: jobIdProp, taskId: taskIdProp, title = "对话", col
|
|
|
174
174
|
})), [allMessages, cfg.transcript.tools]);
|
|
175
175
|
const running = conv ? isRunning(conv) : false;
|
|
176
176
|
const job = entry?.job;
|
|
177
|
-
//
|
|
178
|
-
// 例外是对话式子任务(续派时 mode=conversation):当对话用,随时能接着发(= 续派一条接着它),不显示子任务状态。
|
|
177
|
+
// 一次性任务没有轮次:补充消息通过 continueTask 接在当前子任务的对话后面。
|
|
179
178
|
const batch = !!conv?.batch || (!!job && job.mode !== "conversation");
|
|
180
179
|
const chat = !!taskId && isChatTask(conv);
|
|
181
|
-
// 普通子任务结束后(完成 / 失败 / 取消)也能接着聊:发出去 = 续派一条接着它的对话式子任务(同一个 Agent 会话、同一个工作区)。
|
|
182
|
-
// 还在跑的时候输入框照样显示、但不能发,提示结束后再接着聊(需要登录、验证码时在看板的浏览器里接管)。
|
|
183
180
|
const batchTaskStatus = batch ? conv?.batchTask?.status ?? job?.tasks?.find((t) => t.id === (taskId ?? conv?.task?.id))?.status ?? null : null;
|
|
184
|
-
const
|
|
185
|
-
const readOnly = !!readOnlyProp || !cfg.composer.show || (batch && !
|
|
186
|
-
|
|
187
|
-
|
|
181
|
+
const batchWritable = batch && !!(taskId ?? conv?.task?.id) && !!store.client.continueTask;
|
|
182
|
+
const readOnly = !!readOnlyProp || !cfg.composer.show || (batch && !batchWritable);
|
|
183
|
+
const showComposer = !readOnlyProp && cfg.composer.show;
|
|
184
|
+
// 最新消息可能还在排队:沿当前对话的接续链找正在执行 / 暂停的子任务,不能误停同 Job 的其他对话。
|
|
185
|
+
const controlTask = useMemo(() => {
|
|
186
|
+
if (!batch || !conv?.batchTask) return null;
|
|
187
|
+
const tasks = new Map<string, Pick<CenterTask, "id" | "status" | "continue_from_task_id">>((job?.tasks ?? []).map((t) => [t.id, t]));
|
|
188
|
+
tasks.set(conv.batchTask.id, { ...conv.batchTask, continue_from_task_id: conv.batchTask.continue_from_task_id ?? undefined });
|
|
189
|
+
const chain = [];
|
|
190
|
+
const seen = new Set<string>();
|
|
191
|
+
let t = tasks.get(conv.batchTask.id);
|
|
192
|
+
while (t && !seen.has(t.id)) { chain.push(t); seen.add(t.id); t = tasks.get(t.continue_from_task_id ?? ""); }
|
|
193
|
+
return chain.find((t) => ["dispatching", "running", "verifying"].includes(t.status))
|
|
194
|
+
?? chain.find((t) => t.status === "paused")
|
|
195
|
+
?? chain.reverse().find((t) => ["queued", "blocked"].includes(t.status)) ?? null;
|
|
196
|
+
}, [batch, conv?.batchTask, job?.tasks]);
|
|
197
|
+
const batchPending = batch && !!batchTaskStatus && !SETTLED_TASK.has(batchTaskStatus);
|
|
188
198
|
// 这个对话对应的子任务:指定了就是它,否则对话任务的第一个(「新开」时借用第一个的智能体显示);
|
|
189
199
|
// 一次性任务没指定时是实时流挑的那个(最近一个派发过的)。
|
|
190
200
|
const shownTaskId = taskId ?? (batch ? conv?.task?.id : undefined);
|
|
@@ -194,10 +204,10 @@ function ChatInner({ jobId: jobIdProp, taskId: taskIdProp, title = "对话", col
|
|
|
194
204
|
const agentText = labelOf(agent);
|
|
195
205
|
const jobStatus = conv?.job?.status ?? job?.status ?? null;
|
|
196
206
|
// 对话式子任务没有「结束」:大任务结束了,再发一条会重新打开它。
|
|
197
|
-
//
|
|
198
|
-
const jobEnded = !chat && !
|
|
207
|
+
// 支持续派的普通子任务同理:大任务结束不挡输入框。
|
|
208
|
+
const jobEnded = !chat && !batchWritable && !!jobStatus && TERMINAL_JOB.has(jobStatus);
|
|
199
209
|
// 只关掉了这一个对话(对话任务还开着):按「这个对话已结束」显示,不能再发消息。
|
|
200
|
-
const convClosed = !jobEnded && !
|
|
210
|
+
const convClosed = !jobEnded && !batchWritable && !!(conv?.task?.closed ?? info?.closed);
|
|
201
211
|
const status = convClosed ? "succeeded" : jobStatus;
|
|
202
212
|
const closedReason = convClosed ? "conversation_closed" : conv?.job?.closed_reason ?? job?.conversation?.closed_reason ?? null;
|
|
203
213
|
const ended = jobEnded || convClosed;
|
|
@@ -385,7 +395,8 @@ function ChatInner({ jobId: jobIdProp, taskId: taskIdProp, title = "对话", col
|
|
|
385
395
|
const [loadingOlder, setLoadingOlder] = useState(false);
|
|
386
396
|
const olderRef = useRef(older); olderRef.current = older;
|
|
387
397
|
const keyRef2 = useRef(key); keyRef2.current = key;
|
|
388
|
-
const items = useMemo(() => buildTranscriptItems(committed, older > 0)
|
|
398
|
+
const items = useMemo(() => buildTranscriptItems(committed, older > 0)
|
|
399
|
+
.filter((it) => cfg.transcript.initialPrompt || it.kind !== "msg" || !it.initial), [committed, older, cfg.transcript.initialPrompt]);
|
|
389
400
|
// 展开了的「续等」折叠行(按折叠行的 key)。
|
|
390
401
|
const [openFolds, setOpenFolds] = useState<Set<string>>(() => new Set());
|
|
391
402
|
const [win, setWin] = useState<{ start: string | null; end: string | null }>({ start: null, end: null });
|
|
@@ -541,9 +552,7 @@ function ChatInner({ jobId: jobIdProp, taskId: taskIdProp, title = "对话", col
|
|
|
541
552
|
const composerDisabled = !!readOnly || finalEnd || conv?.stream === "error";
|
|
542
553
|
const disabledHint = readOnly
|
|
543
554
|
? batch && !readOnlyProp && cfg.composer.show
|
|
544
|
-
?
|
|
545
|
-
: batchTaskStatus === "blocked" || batchTaskStatus === "queued" ? "子任务还没开始,开始并结束后可以接着它聊"
|
|
546
|
-
: "子任务运行中,结束后可以接着它继续聊;需要登录等操作请在右侧浏览器里接管")
|
|
555
|
+
? "当前任务暂不支持发送消息"
|
|
547
556
|
: "只读"
|
|
548
557
|
: finalEnd ? "对话已结束,不能再发消息" : conv?.streamError ?? undefined;
|
|
549
558
|
|
|
@@ -573,15 +582,10 @@ function ChatInner({ jobId: jobIdProp, taskId: taskIdProp, title = "对话", col
|
|
|
573
582
|
<div className={`cli-ui cli-chat ${className ?? ""}`} data-theme={theme} style={style}>
|
|
574
583
|
<div className="session">
|
|
575
584
|
{h.show && (h.crumb || h.export || h.title || h.agent || h.usage || h.model || h.status || h.close) && <div className="sess-head">
|
|
576
|
-
{(h.crumb || h.
|
|
577
|
-
{
|
|
578
|
-
{h.crumb && <>
|
|
585
|
+
{(h.crumb || h.title) && <div className="sh-main">
|
|
586
|
+
{h.crumb && <div className="crumb">
|
|
579
587
|
<span className="pic" style={{ width: 16, height: 16, borderRadius: 5, background: color, display: "grid", placeItems: "center", color: "#fff", fontSize: 9, fontWeight: 700 }}>{title[0]}</span>
|
|
580
588
|
{title} <span className="sep">/</span> 对话
|
|
581
|
-
</>}
|
|
582
|
-
{h.export && key && allMessages.length > 0 && (
|
|
583
|
-
<button className="crumb-export" onClick={() => exportMarkdown(convTitle, taskId ? `${jobId}-${taskId}` : jobId!, allMessages, conv?.turns ?? [])} title="导出这个对话的完整过程(Markdown)">{Icon.download()} 导出执行过程</button>
|
|
584
|
-
)}
|
|
585
589
|
</div>}
|
|
586
590
|
{h.title && <div className="sh-title"><span className="sht-text" title={convTitle}>{convTitle}</span></div>}
|
|
587
591
|
</div>}
|
|
@@ -603,6 +607,12 @@ function ChatInner({ jobId: jobIdProp, taskId: taskIdProp, title = "对话", col
|
|
|
603
607
|
<StatusDot cls={dotCls} running={running} awaiting={awaiting} title={statusText} />
|
|
604
608
|
<span className="cv">{statusText}</span>
|
|
605
609
|
</span>}
|
|
610
|
+
{h.export && key && <button type="button" className="btn btn-line btn-sm cli-export" disabled={!allMessages.length}
|
|
611
|
+
aria-label="导出执行过程(Markdown)"
|
|
612
|
+
title={older > 0 ? "下载已加载的执行过程(Markdown);更早的消息尚未加载" : "下载执行过程(Markdown),包含初始上下文和工具记录"}
|
|
613
|
+
onClick={() => exportMarkdown(convTitle, taskId ? `${jobId}-${taskId}` : jobId!, allMessages, conv?.turns ?? [])}>
|
|
614
|
+
{Icon.download()}<span>{older > 0 ? "导出已加载" : "导出"}</span>
|
|
615
|
+
</button>}
|
|
606
616
|
{h.close && key && !isNew && allowClose && !readOnly && !ended && !batch && (
|
|
607
617
|
<button className="btn btn-line btn-sm" onClick={() => setClosing("confirm")} disabled={closing === "busy"} title={taskId ? "只结束这个对话:之后它不再接收消息,正在跑的一轮会被中止;同一任务里的其他对话不受影响" : "结束这个对话:之后不再接收消息,正在跑的一轮会被中止"}>{Icon.stop()} 结束对话</button>
|
|
608
618
|
)}
|
|
@@ -734,9 +744,16 @@ function ChatInner({ jobId: jobIdProp, taskId: taskIdProp, title = "对话", col
|
|
|
734
744
|
placeholder={fillTemplate(key && !isNew ? cfg.composer.placeholder : cfg.composer.placeholderNew, { title: convTitle, agent: agentText })}
|
|
735
745
|
disabled={composerDisabled}
|
|
736
746
|
disabledHint={cfg.composer.placeholderDisabled ? fillTemplate(cfg.composer.placeholderDisabled, { title: convTitle, agent: agentText }) : disabledHint}
|
|
737
|
-
running={running}
|
|
747
|
+
running={running || batchPending}
|
|
738
748
|
onSend={send}
|
|
739
749
|
onStop={cfg.composer.stop && key && conv && runningTurn(conv) ? stop : undefined}
|
|
750
|
+
taskControl={cfg.composer.stop && !readOnlyProp && controlTask && jobId && store.client.pauseTask && store.client.resumeTask ? {
|
|
751
|
+
paused: controlTask.status === "paused",
|
|
752
|
+
onToggle: async () => {
|
|
753
|
+
const action = controlTask.status === "paused" ? store.client.resumeTask! : store.client.pauseTask!;
|
|
754
|
+
await report(action(controlTask.id).then(() => store.refreshJob(jobId, true)), controlTask.status === "paused" ? "继续任务失败" : "暂停任务失败");
|
|
755
|
+
},
|
|
756
|
+
} : undefined}
|
|
740
757
|
options={composerOptions}
|
|
741
758
|
/>
|
|
742
759
|
)}
|
package/src/chat/Composer.tsx
CHANGED
|
@@ -30,7 +30,7 @@ export interface ComposerOptions {
|
|
|
30
30
|
queue: boolean;
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
export function Composer({ draft, placeholder, disabled, disabledHint, running, onSend, onStop, options }: {
|
|
33
|
+
export function Composer({ draft, placeholder, disabled, disabledHint, running, onSend, onStop, taskControl, options }: {
|
|
34
34
|
/** 草稿按这个键存在 localStorage(一般是对话 id)。 */
|
|
35
35
|
draft: string;
|
|
36
36
|
placeholder: string;
|
|
@@ -39,6 +39,7 @@ export function Composer({ draft, placeholder, disabled, disabledHint, running,
|
|
|
39
39
|
running: boolean;
|
|
40
40
|
onSend: (text: string) => Promise<void>;
|
|
41
41
|
onStop?: () => Promise<void> | void;
|
|
42
|
+
taskControl?: { paused: boolean; onToggle: () => Promise<void> };
|
|
42
43
|
options: ComposerOptions;
|
|
43
44
|
}) {
|
|
44
45
|
const [val, setVal] = useState(() => loadDraft(draft));
|
|
@@ -47,6 +48,7 @@ export function Composer({ draft, placeholder, disabled, disabledHint, running,
|
|
|
47
48
|
const [error, setError] = useState<string | null>(null);
|
|
48
49
|
const [sending, setSending] = useState(false);
|
|
49
50
|
const [stopping, setStopping] = useState(false);
|
|
51
|
+
const [controlling, setControlling] = useState(false);
|
|
50
52
|
const [dragging, setDragging] = useState(false);
|
|
51
53
|
const dragDepth = useRef(0);
|
|
52
54
|
const seq = useRef(0);
|
|
@@ -56,7 +58,7 @@ export function Composer({ draft, placeholder, disabled, disabledHint, running,
|
|
|
56
58
|
useEffect(() => { if (!running) setStopping(false); }, [running]);
|
|
57
59
|
useEffect(() => { saveDraft(draft, val); }, [draft, val]);
|
|
58
60
|
const autosize = (el: HTMLTextAreaElement) => { el.style.height = "auto"; el.style.height = Math.min(el.scrollHeight, 160) + "px"; };
|
|
59
|
-
useEffect(() => { if (ref.current) autosize(ref.current); }, [draft]);
|
|
61
|
+
useEffect(() => { if (ref.current) autosize(ref.current); }, [draft, disabled]);
|
|
60
62
|
|
|
61
63
|
const nextKey = () => `blk-${++seq.current}`;
|
|
62
64
|
const addBlock = (block: TextBlock) => setBlocks((prev) => [...prev, { ...block, key: nextKey() }]);
|
|
@@ -108,6 +110,10 @@ export function Composer({ draft, placeholder, disabled, disabledHint, running,
|
|
|
108
110
|
|
|
109
111
|
const isFileDrag = (e: React.DragEvent) => Array.from(e.dataTransfer?.types ?? []).includes("Files");
|
|
110
112
|
const hasFileDrag = (e: React.DragEvent) => options.fileUpload && isFileDrag(e);
|
|
113
|
+
const stopButton = running && onStop && (
|
|
114
|
+
<button type="button" className="send stop" onClick={() => { setStopping(true); void Promise.resolve(onStop()).catch(() => setStopping(false)); }}
|
|
115
|
+
disabled={stopping} aria-label={stopping ? "正在停止" : "停止当前轮次"} title={stopping ? "正在停止…" : "停止当前轮次"}>{Icon.stop()}</button>
|
|
116
|
+
);
|
|
111
117
|
|
|
112
118
|
return (
|
|
113
119
|
<div className="composer">
|
|
@@ -145,8 +151,8 @@ export function Composer({ draft, placeholder, disabled, disabledHint, running,
|
|
|
145
151
|
)}
|
|
146
152
|
<div className="comp-area">
|
|
147
153
|
<textarea
|
|
148
|
-
ref={ref} rows={
|
|
149
|
-
placeholder={disabled ? disabledHint ?? "暂时不能发送" : running && options.queue ? options.placeholderRunning : placeholder}
|
|
154
|
+
ref={ref} rows={2} value={val} disabled={disabled} aria-label="消息内容"
|
|
155
|
+
placeholder={disabled ? disabledHint ?? "暂时不能发送" : taskControl?.paused ? "任务已暂停,可补充消息,继续后依次执行…" : running && options.queue ? options.placeholderRunning : placeholder}
|
|
150
156
|
onChange={(e) => { setVal(e.target.value); autosize(e.target); }}
|
|
151
157
|
onKeyDown={(e) => { if (e.key === "Enter" && !e.shiftKey && !e.nativeEvent.isComposing) { e.preventDefault(); void submit(); } }}
|
|
152
158
|
onPaste={(e) => {
|
|
@@ -166,27 +172,32 @@ export function Composer({ draft, placeholder, disabled, disabledHint, running,
|
|
|
166
172
|
}
|
|
167
173
|
}}
|
|
168
174
|
/>
|
|
169
|
-
{running ? (
|
|
170
|
-
<div className="send-group">
|
|
171
|
-
{canSend && <button className="send queue" onClick={() => void submit()} title="把这条后续消息排队 —— 当前轮次结束后自动发送" disabled={disabled}>{Icon.send()}</button>}
|
|
172
|
-
{onStop && <button className="send stop" onClick={() => { setStopping(true); void Promise.resolve(onStop()).catch(() => setStopping(false)); }} disabled={stopping} title={stopping ? "正在停止…" : "停止当前轮次"}>{Icon.stop()}</button>}
|
|
173
|
-
</div>
|
|
174
|
-
) : (
|
|
175
|
-
<button className="send" disabled={!canSend || disabled} onClick={() => void submit()}>{Icon.send()}</button>
|
|
176
|
-
)}
|
|
177
175
|
</div>
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
176
|
+
<div className="comp-foot">
|
|
177
|
+
{options.fileUpload && !disabled && <>
|
|
178
|
+
<input ref={fileRef} type="file" accept={ACCEPT_ATTR} multiple hidden onChange={(e) => { addFiles(Array.from(e.target.files ?? [])); e.target.value = ""; }} />
|
|
179
|
+
<button type="button" className="cli-attach" title={`添加文档:${ACCEPTED_LABEL}(也可拖拽进来),内容会转成文字发送`}
|
|
180
|
+
onClick={() => fileRef.current?.click()}>{Icon.clip()} {options.fileButtonText}</button>
|
|
181
|
+
</>}
|
|
182
|
+
{options.hints && !disabled && <span className="hint comp-hint-extra">Shift + Enter 换行</span>}
|
|
183
|
+
<span className="spacer" />
|
|
182
184
|
{options.charCount && total > 0 && <span className={`hint cli-count ${tooLong ? "over" : ""}`}>{fmtCount(total)} 字</span>}
|
|
183
185
|
{!options.charCount && tooLong && <span className="hint cli-count over">{fmtCount(total)} 字</span>}
|
|
184
|
-
<
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
186
|
+
<div className="send-group">
|
|
187
|
+
{taskControl && <button type="button" className="btn btn-line cli-task-control" disabled={controlling}
|
|
188
|
+
aria-label={taskControl.paused ? "继续任务" : "暂停任务"}
|
|
189
|
+
title={taskControl.paused ? "重新派发当前子任务,排队消息随后执行" : "暂停当前子任务,继续时重新派发"}
|
|
190
|
+
onClick={async () => { setControlling(true); try { await taskControl.onToggle(); } catch { /* 错误由调用方显示,保留草稿和当前状态 */ } finally { setControlling(false); } }}>
|
|
191
|
+
{taskControl.paused ? Icon.play() : Icon.pause()}<span>{controlling ? "处理中…" : taskControl.paused ? "继续" : "暂停"}</span>
|
|
192
|
+
</button>}
|
|
193
|
+
{(!running || options.queue) && <button type="button" className={`send cli-send${running ? " queue" : ""}`} disabled={disabled || !canSend}
|
|
194
|
+
onClick={() => void submit()} aria-label={sending ? "正在发送" : running ? "排队发送" : "发送消息"}
|
|
195
|
+
title={taskControl?.paused ? "排队发送,继续任务后依次执行" : running ? "排队发送,当前轮次结束后执行" : "发送消息(Enter)"}>
|
|
196
|
+
{Icon.send()}<span>{sending ? "发送中…" : running ? "排队发送" : "发送"}</span>
|
|
197
|
+
</button>}
|
|
198
|
+
{stopButton}
|
|
199
|
+
</div>
|
|
200
|
+
</div>
|
|
190
201
|
</div>
|
|
191
202
|
</div>
|
|
192
203
|
</div>
|
package/src/core/client.ts
CHANGED
|
@@ -75,6 +75,9 @@ const ERROR_TEXT: Record<string, string> = {
|
|
|
75
75
|
viewer_not_ready: "画面连接还没就绪,正在重连",
|
|
76
76
|
link_expired: "文件链接已失效,请刷新",
|
|
77
77
|
task_not_found: "这个子任务不在对应的任务里",
|
|
78
|
+
task_already_finished: "当前子任务已经结束,请刷新状态后继续",
|
|
79
|
+
task_not_paused: "当前子任务没有暂停,请刷新状态后重试",
|
|
80
|
+
job_already_paused: "整个任务已暂停,请先恢复整个任务",
|
|
78
81
|
};
|
|
79
82
|
|
|
80
83
|
/**
|
|
@@ -102,6 +105,9 @@ export interface ConversationBackend {
|
|
|
102
105
|
* 返回新子任务的 id。
|
|
103
106
|
*/
|
|
104
107
|
continueTask?(jobId: string, taskId: string, text: string, opts?: { title?: string; businessId?: string }): Promise<{ task_id: string }>;
|
|
108
|
+
/** 暂停 / 继续一次性任务中的单个子任务。继续会重新派发该子任务。 */
|
|
109
|
+
pauseTask?(taskId: string): Promise<CenterTask>;
|
|
110
|
+
resumeTask?(taskId: string): Promise<CenterTask>;
|
|
105
111
|
/** 在对话任务里新开一个对话(同一节点、同一工作区),text 是它的第一条消息。 */
|
|
106
112
|
openConversation?(jobId: string, text: string, opts?: { title?: string; agent?: string; browser?: boolean; businessId?: string; timeoutSeconds?: number }): Promise<SendResult>;
|
|
107
113
|
cancelTurn(jobId: string, turnId: string): Promise<CenterTurn>;
|
|
@@ -336,6 +342,8 @@ export function createCenterClient(options: CenterClientOptions = {}): Conversat
|
|
|
336
342
|
});
|
|
337
343
|
return { task_id: result.task_ids[0] };
|
|
338
344
|
},
|
|
345
|
+
pauseTask: (taskId: string) => post<CenterTask>(`/api/tasks/${id(taskId)}/pause`, {}),
|
|
346
|
+
resumeTask: (taskId: string) => post<CenterTask>(`/api/tasks/${id(taskId)}/resume`, {}),
|
|
339
347
|
/** 同一个 business_id 重发只开一个对话。 */
|
|
340
348
|
openConversation: (jobId: string, text: string, opts: { title?: string; agent?: string; browser?: boolean; businessId?: string; timeoutSeconds?: number } = {}) =>
|
|
341
349
|
post<SendResult>(`/api/jobs/${id(jobId)}/conversations`, {
|
package/src/core/config.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* 组件的界面配置:哪些模块显示、文案怎么写。全部可选,缺省值见下方 DEFAULT_*_CONFIG。
|
|
3
3
|
*
|
|
4
4
|
* <CliChat config={{ header: { title: true, status: true }, composer: { placeholder: "问点什么…", fileUpload: false } }} />
|
|
5
5
|
* <CliMsgList config={{ header: { refresh: false }, card: { turns: false } }} />
|
|
@@ -39,7 +39,7 @@ export interface CliChatConfig {
|
|
|
39
39
|
placeholderRunning?: string;
|
|
40
40
|
/** 不能发送时(对话已结束、只读)的占位文案。 */
|
|
41
41
|
placeholderDisabled?: string;
|
|
42
|
-
/**
|
|
42
|
+
/** 底部「Shift + Enter 换行」提示;发送快捷键仍为 Enter。 */
|
|
43
43
|
hints?: boolean;
|
|
44
44
|
/** 底部字数统计。 */
|
|
45
45
|
charCount?: boolean;
|
|
@@ -47,7 +47,7 @@ export interface CliChatConfig {
|
|
|
47
47
|
fileUpload?: boolean;
|
|
48
48
|
/** 「文件」按钮的文字。 */
|
|
49
49
|
fileButtonText?: string;
|
|
50
|
-
/**
|
|
50
|
+
/** 执行控制:普通对话停止当前轮次,一次性子任务暂停 / 继续。 */
|
|
51
51
|
stop?: boolean;
|
|
52
52
|
/** 运行中允许发消息排队;false = 运行中不能发。 */
|
|
53
53
|
queue?: boolean;
|
|
@@ -60,6 +60,8 @@ export interface CliChatConfig {
|
|
|
60
60
|
sessionLabel?: boolean;
|
|
61
61
|
/** 消息时间。 */
|
|
62
62
|
timestamps?: boolean;
|
|
63
|
+
/** 已随项目上下文发送的初始提示。缺省不渲染;true 显示原文。 */
|
|
64
|
+
initialPrompt?: boolean;
|
|
63
65
|
/** 首条消息上的「初始提示」「已随项目上下文发送」标记。 */
|
|
64
66
|
initialBadge?: boolean;
|
|
65
67
|
/** 工具调用卡片(命令、读写文件……);false 只留文字往来(提问卡片照常显示)。 */
|
|
@@ -87,7 +89,7 @@ export interface ResolvedChatConfig {
|
|
|
87
89
|
show: boolean; placeholder: string; placeholderNew: string; placeholderRunning: string; placeholderDisabled: string | null;
|
|
88
90
|
hints: boolean; charCount: boolean; fileUpload: boolean; fileButtonText: string; stop: boolean; queue: boolean;
|
|
89
91
|
};
|
|
90
|
-
transcript: { nav: boolean; sessionLabel: boolean; timestamps: boolean; initialBadge: boolean; tools: boolean; runningIndicator: boolean; status: boolean };
|
|
92
|
+
transcript: { nav: boolean; sessionLabel: boolean; timestamps: boolean; initialPrompt: boolean; initialBadge: boolean; tools: boolean; runningIndicator: boolean; status: boolean };
|
|
91
93
|
labels: { user: string; agent: string };
|
|
92
94
|
empty: { show: boolean; title: string; description: string };
|
|
93
95
|
/** 按种类显示 / 不显示;"*" 是没列出的种类。用 refShown 判断。 */
|
|
@@ -102,13 +104,13 @@ export const DEFAULT_CHAT_CONFIG: ResolvedChatConfig = {
|
|
|
102
104
|
header: { show: true, crumb: false, export: false, title: false, agent: false, usage: false, model: false, status: false, close: false },
|
|
103
105
|
composer: {
|
|
104
106
|
show: true,
|
|
105
|
-
placeholder: "
|
|
106
|
-
placeholderNew: "
|
|
107
|
+
placeholder: "回复 {agent}…",
|
|
108
|
+
placeholderNew: "输入消息,开始新对话…",
|
|
107
109
|
placeholderRunning: "排队一条后续消息…(本轮结束后发送)",
|
|
108
110
|
placeholderDisabled: null,
|
|
109
111
|
hints: true, charCount: true, fileUpload: true, fileButtonText: "文件", stop: true, queue: true,
|
|
110
112
|
},
|
|
111
|
-
transcript: { nav: true, sessionLabel: true, timestamps: true, initialBadge: true, tools: true, runningIndicator: true, status: false },
|
|
113
|
+
transcript: { nav: true, sessionLabel: true, timestamps: true, initialPrompt: false, initialBadge: true, tools: true, runningIndicator: true, status: false },
|
|
112
114
|
labels: { user: "你", agent: "智能体" },
|
|
113
115
|
empty: { show: true, title: "开始一个新对话", description: "在下面输入第一条消息。可以附上 Word、Excel、Markdown、TXT、HTML、CSV 文档,内容会转成文字一起发送。" },
|
|
114
116
|
refs: { "*": true, viewing: false },
|
|
@@ -119,12 +121,6 @@ export function resolveChatConfig(config: CliChatConfig = {}): ResolvedChatConfi
|
|
|
119
121
|
const header = config.header === true ? allOn(d.header) : toggle(config.header, d.header);
|
|
120
122
|
const composer = toggle(config.composer, d.composer);
|
|
121
123
|
const empty = toggle(config.empty, d.empty);
|
|
122
|
-
// 没有文件上传时,缺省文案里的「可拖入文件」提示也去掉。
|
|
123
|
-
if (!composer.fileUpload) {
|
|
124
|
-
const strip = (text: string) => text.replace(/(可拖入 Word \/ Excel \/ Markdown 等文档)$/, "");
|
|
125
|
-
if (!isObj(config.composer) || config.composer.placeholder === undefined) composer.placeholder = strip(composer.placeholder);
|
|
126
|
-
if (!isObj(config.composer) || config.composer.placeholderNew === undefined) composer.placeholderNew = strip(composer.placeholderNew);
|
|
127
|
-
}
|
|
128
124
|
return {
|
|
129
125
|
header,
|
|
130
126
|
composer,
|
|
@@ -146,7 +142,7 @@ export interface CliMsgListConfig {
|
|
|
146
142
|
newButton?: boolean;
|
|
147
143
|
/** 「+ 对话」按钮的文字。 */
|
|
148
144
|
newButtonText?: string;
|
|
149
|
-
/**
|
|
145
|
+
/** 手动刷新按钮,缺省隐藏;列表仍按 pollMs 自动更新。 */
|
|
150
146
|
refresh?: boolean;
|
|
151
147
|
/** 上下文说明框(需同时给 context 或 onEditContext)。 */
|
|
152
148
|
context?: boolean;
|
|
@@ -192,7 +188,7 @@ export interface ResolvedMsgListConfig {
|
|
|
192
188
|
}
|
|
193
189
|
|
|
194
190
|
export const DEFAULT_MSGLIST_CONFIG: ResolvedMsgListConfig = {
|
|
195
|
-
header: { show: true, title: true, newButton: true, newButtonText: "对话", refresh:
|
|
191
|
+
header: { show: true, title: true, newButton: true, newButtonText: "对话", refresh: false, context: true },
|
|
196
192
|
search: "auto",
|
|
197
193
|
searchPlaceholder: "搜索对话…",
|
|
198
194
|
grouped: false,
|
package/src/core/conversation.ts
CHANGED
|
@@ -403,10 +403,10 @@ function batchViewMessages(conv: ConvState): Msg[] {
|
|
|
403
403
|
}
|
|
404
404
|
const generation = out.length ? out[out.length - 1].generation : 1;
|
|
405
405
|
const note = (id: string, content: string) => out.push({ id: `batch-${id}`, role: "system", content, generation });
|
|
406
|
+
for (const q of task?.queued ?? []) out.push({ id: `batch-q-${q.id}`, role: "queued", content: q.prompt, generation, ...(q.created_at ? { ts: new Date(q.created_at).getTime() } : {}) });
|
|
406
407
|
if (task?.conversation) {
|
|
407
408
|
// 对话式:像对话一样显示——还没派发的消息是「排队中」气泡,没有「子任务」字样。
|
|
408
409
|
if (CHAT_TASK_ERROR[task.status]) note(`err-${task.id}-${task.attempt}`, `⚠ ${CHAT_TASK_ERROR[task.status]}${task.last_error ? `:${describeTurnError(task.last_error)}` : ""}`);
|
|
409
|
-
for (const q of task.queued ?? []) out.push({ id: `batch-q-${q.id}`, role: "queued", content: q.prompt, generation, ...(q.created_at ? { ts: new Date(q.created_at).getTime() } : {}) });
|
|
410
410
|
return out;
|
|
411
411
|
}
|
|
412
412
|
if (!task) {
|
package/src/styles.css
CHANGED
|
@@ -46,6 +46,37 @@
|
|
|
46
46
|
.cli-ui .cli-count{font-family:var(--mono);}
|
|
47
47
|
.cli-ui .cli-count.over{color:var(--red);}
|
|
48
48
|
|
|
49
|
+
/* 组件在侧栏中也保持清晰的标题、操作和输入区。 */
|
|
50
|
+
.cli-ui.cli-chat{container:cli-chat / inline-size;}
|
|
51
|
+
.cli-ui .sess-head .sh-main{min-width:0;flex:1 1 180px;}
|
|
52
|
+
.cli-ui .sess-head .sh-tools{min-width:0;max-width:100%;flex-wrap:wrap;justify-content:flex-end;margin-left:auto;}
|
|
53
|
+
.cli-ui .sess-head .crumb{overflow-wrap:anywhere;}
|
|
54
|
+
.cli-ui .cli-export{white-space:nowrap;}
|
|
55
|
+
.cli-ui .comp-area textarea{min-width:0;min-height:48px;}
|
|
56
|
+
.cli-ui .comp-foot{gap:8px;padding:8px 10px;background:transparent;border-top:0;flex-wrap:wrap;}
|
|
57
|
+
.cli-ui .cli-attach{display:inline-flex;align-items:center;gap:5px;min-height:32px;padding:0 8px;border-radius:7px;color:var(--ink-3);font-size:12px;}
|
|
58
|
+
.cli-ui .cli-attach:hover{background:var(--surface-2);color:var(--ink);}
|
|
59
|
+
.cli-ui .cli-attach svg{width:15px;height:15px;}
|
|
60
|
+
.cli-ui .send.cli-send{display:inline-flex;justify-content:center;gap:5px;width:auto;min-width:76px;padding:0 12px;font-size:12.5px;font-weight:600;}
|
|
61
|
+
.cli-ui .send.cli-send:disabled{background:var(--surface-2);color:var(--ink-4);box-shadow:none;opacity:1;}
|
|
62
|
+
.cli-ui .cli-export:focus-visible,.cli-ui .cli-attach:focus-visible,.cli-ui .send:focus-visible{outline:2px solid var(--accent);outline-offset:3px;}
|
|
63
|
+
.cli-ui .cli-task-control{height:34px;padding:0 10px;white-space:nowrap;}
|
|
64
|
+
.cli-ui .cli-task-control:focus-visible{outline:2px solid var(--accent);outline-offset:3px;}
|
|
65
|
+
@container cli-chat (max-width:560px){
|
|
66
|
+
.cli-ui .sess-head{gap:8px;padding:12px 14px;}
|
|
67
|
+
.cli-ui .sess-head .sh-main{flex-basis:100%;}
|
|
68
|
+
.cli-ui .sess-head .sh-tools{flex:1;justify-content:flex-start;margin-left:0;}
|
|
69
|
+
.cli-ui .sess-head .cli-export{margin-left:auto;}
|
|
70
|
+
.cli-ui .tw{padding:0 16px;}
|
|
71
|
+
.cli-ui .composer{padding:10px 12px;}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/* 普通文本和 URL 换行;代码、宽表格只在各自区域内横向滚动。聊天和看板预览共用。 */
|
|
75
|
+
.cli-ui .transcript-wrap,.cli-ui .transcript,.cli-ui .tw,.cli-ui .msg,.cli-ui .msg-body,.cli-ui .md{min-width:0;}
|
|
76
|
+
.cli-ui .msg-body,.cli-ui .md{overflow-wrap:anywhere;}
|
|
77
|
+
.cli-ui .md pre{max-width:100%;overflow-wrap:normal;}
|
|
78
|
+
.cli-ui .md table{display:block;max-width:100%;overflow-x:auto;}
|
|
79
|
+
|
|
49
80
|
/* 排队气泡下的等待原因(中枢给的:节点连不上、没有可用节点……) */
|
|
50
81
|
.cli-ui .cli-queued-note{margin-top:6px;font-size:12px;color:var(--amber);}
|
|
51
82
|
|
package/src/ui/icons.tsx
CHANGED
|
@@ -54,6 +54,7 @@ export const Icon = {
|
|
|
54
54
|
external: (p?: P) => S(<><path d="M14 4h6v6" /><path d="M20 4l-9 9" /><path d="M19 13v5a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V7a2 2 0 0 1 2-2h5" /></>, p),
|
|
55
55
|
copy: (p?: P) => S(<><rect x={9} y={9} width={12} height={12} rx={2} /><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" /></>, p),
|
|
56
56
|
play: (p?: P) => S(<path d="M7 5l11 7-11 7z" />, p),
|
|
57
|
+
pause: (p?: P) => S(<><path d="M9 5v14" /><path d="M15 5v14" /></>, p),
|
|
57
58
|
stop: (p?: P) => S(<rect x={6} y={6} width={12} height={12} rx={2} />, p),
|
|
58
59
|
sliders: (p?: P) => S(<><line x1={4} y1={21} x2={4} y2={14} /><line x1={4} y1={10} x2={4} y2={3} /><line x1={12} y1={21} x2={12} y2={12} /><line x1={12} y1={8} x2={12} y2={3} /><line x1={20} y1={21} x2={20} y2={16} /><line x1={20} y1={12} x2={20} y2={3} /><line x1={1} y1={14} x2={7} y2={14} /><line x1={9} y1={8} x2={15} y2={8} /><line x1={17} y1={16} x2={23} y2={16} /></>, p),
|
|
59
60
|
search: (p?: P) => S(<><circle cx={11} cy={11} r={7} /><line x1={21} y1={21} x2={16.65} y2={16.65} /></>, p),
|