@spzxg/cli-ui 0.6.6 → 0.6.8
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/CHANGELOG.md +40 -1
- package/README.md +90 -21
- package/dist/cli-ui-embed-Be9_d9jB.js +49691 -0
- package/dist/{cli-ui-index-b8wF7wrN.js → cli-ui-index-CzNfiynu.js} +2 -2
- package/dist/{cli-ui-jszip.min-B0uLrCHg.js → cli-ui-jszip.min-BL0mSekN.js} +2 -2
- package/dist/{cli-ui-jszip.min-ggP8OokP.js → cli-ui-jszip.min-DjCIAfse.js} +1 -1
- package/dist/{cli-ui-xlsx-DEDdHrJJ.js → cli-ui-xlsx-CscI4xk0.js} +1 -1
- package/dist/cli-ui.css +1 -1
- package/dist/cli-ui.js +1 -1
- package/package.json +3 -1
- package/src/board/CliBoard.tsx +128 -100
- package/src/board/FileViewer.tsx +259 -132
- package/src/board/FilesPane.tsx +129 -40
- package/src/board/HtmlReadingPreview.tsx +52 -0
- package/src/board/MarkdownPreview.tsx +134 -0
- package/src/board/PanelControls.tsx +19 -7
- package/src/board/fileKind.ts +138 -0
- package/src/board/fileOps.ts +9 -2
- package/src/board/fileScope.ts +29 -0
- package/src/board/fileSync.ts +343 -0
- package/src/board/fileSyncCache.ts +250 -0
- package/src/board/fileSyncProcessor.ts +71 -0
- package/src/board/fileSyncQueue.ts +56 -0
- package/src/board/fileSyncTransport.ts +85 -0
- package/src/board/fileWatch.ts +177 -76
- package/src/board/folderDownload.ts +39 -5
- package/src/board/frameQuote.ts +10 -8
- package/src/board/htmlReading.ts +148 -0
- package/src/board/textLoad.ts +14 -39
- package/src/board/useBoardUpload.tsx +51 -0
- package/src/board/useBoardVisibility.ts +35 -0
- package/src/board/workspace.ts +38 -6
- package/src/chat/CliChat.tsx +121 -51
- package/src/chat/Composer.tsx +54 -22
- package/src/chat/GroupChat.tsx +113 -0
- package/src/chat/RunControls.tsx +143 -46
- package/src/chat/draft.ts +12 -4
- package/src/chat/useExecutionConfig.ts +93 -0
- package/src/components.tsx +1 -1
- package/src/core/blocks.ts +2 -1
- package/src/core/client.ts +65 -61
- package/src/core/config.ts +6 -2
- package/src/core/documentUpload.ts +196 -0
- package/src/core/execution.ts +24 -0
- package/src/core/store.ts +81 -20
- package/src/core/types.ts +3 -1
- package/src/index.ts +4 -0
- package/src/msglist/CliMsgList.tsx +9 -2
- package/src/operator/node/Chat.tsx +11 -9
- package/src/operator/node/Composer.tsx +103 -75
- package/src/operator/node/Orchestrator.tsx +1 -1
- package/src/operator/node/SessionRail.tsx +71 -32
- package/src/operator/node/SessionView.tsx +31 -28
- package/src/operator/node/Transcript.tsx +21 -9
- package/src/operator/node/boardBackend.ts +0 -19
- package/src/operator/node/boardEvents.ts +21 -3
- package/src/operator/node/boardState.ts +18 -1
- package/src/operator/node/boardWorkspace.ts +4 -1
- package/src/operator/node/composerDraft.ts +6 -2
- package/src/operator/node/format.ts +4 -2
- package/src/operator/node/liveStream.ts +14 -2
- package/src/operator/node/useTaskStream.ts +3 -1
- package/src/operator.base.css +150 -43
- package/src/operator.css +150 -43
- package/src/styles.css +253 -39
- package/src/ui/Markdown.tsx +2 -2
- package/src/ui/Popover.tsx +25 -12
- package/src/ui/QueuedMessages.tsx +69 -0
- package/src/ui/ReferenceCard.tsx +28 -0
- package/src/ui/TextBlock.tsx +6 -4
- package/src/ui/Transcript.tsx +20 -10
- package/src/ui/UploadProgress.tsx +46 -0
- package/src/ui/WorkspaceDrawer.tsx +64 -0
- package/src/ui/WorkspaceTabs.tsx +60 -0
- package/src/ui/format.ts +3 -1
- package/src/ui/reference.ts +40 -0
- package/src/ui/useChatScrollbar.ts +29 -0
- package/src/ui/useComposerAutosize.ts +54 -0
- package/dist/cli-ui-embed-BAqonuoD.js +0 -45952
package/src/core/store.ts
CHANGED
|
@@ -19,6 +19,7 @@ import { formatAnswersText } from "../ui/format";
|
|
|
19
19
|
import { backgroundBlock, charCount, MAX_MESSAGE_CHARS, serializeBlock } from "./blocks";
|
|
20
20
|
import { cloneBoardView, reportBoardOwner, activateBoardOwner, selectedBoardOwner, mergeBoardHistory, rememberBoardFile } from "../board/context";
|
|
21
21
|
import { cloneWorkspaceChange, subscribeWorkspaceFiles, retryWorkspaceFiles, type WorkspaceWatchOptions } from "../board/fileWatch";
|
|
22
|
+
import { retainFileScope } from "../board/fileScope";
|
|
22
23
|
import type { WorkspaceChangeEvent, WorkspaceWatchStatus } from "./types";
|
|
23
24
|
import { createWorkspace, type BoardView, type CliWorkspace } from "../board/workspace";
|
|
24
25
|
import type { AskAnswers, AskQuestion, CenterJob, CenterTurn, ConversationClosedCallback, ConversationTurnCallback, NewConversationOptions } from "./types";
|
|
@@ -52,6 +53,12 @@ export interface CliState {
|
|
|
52
53
|
viewing: Record<string, { ref: BoardRef; off: boolean }>;
|
|
53
54
|
/** 看板现在的样子(按对话键):正在看的文件、开着的文件页签、当前页签。没有看板时没有。 */
|
|
54
55
|
boards: Record<string, BoardView>;
|
|
56
|
+
/** 当前对话是否有展开且可见的外置看板;Chat 内嵌看板不计入。 */
|
|
57
|
+
externalBoards: Record<string, boolean>;
|
|
58
|
+
/** 同一工作区中是否有展开且可见的对话列表。 */
|
|
59
|
+
listVisible: boolean;
|
|
60
|
+
/** 外置看板的模块和只读限制,内嵌视图沿用同一约束。 */
|
|
61
|
+
externalBoardOptions: Record<string, { availableTabs: ("files" | "changes" | "browser")[]; readOnly: boolean }>;
|
|
55
62
|
/** 最近看板背景,收起/卸载后保留,供下一条消息被动参考。 */
|
|
56
63
|
boardContext: Record<string, BoardView>;
|
|
57
64
|
/** 业务方补充的项目背景(按对话键;"" = 还没建的新对话),随下一条消息发出(见 appendContext)。 */
|
|
@@ -146,7 +153,7 @@ export interface CliStoreOptions extends CenterClientOptions {
|
|
|
146
153
|
*/
|
|
147
154
|
newConversation?: NewConversationOptions;
|
|
148
155
|
/** 自己建对话(例如经业务后端建,以便把回调密钥留在服务端):返回 center 的 job_id。 */
|
|
149
|
-
createConversation?: (firstMessage: string) => Promise<string>;
|
|
156
|
+
createConversation?: (firstMessage: string, options?: NewConversationOptions) => Promise<string>;
|
|
150
157
|
/**
|
|
151
158
|
* 每条消息的 business_id(center 按它去重,回调里原样带回)。缺省 `cli-<uuid>`。
|
|
152
159
|
* kind = "create" 是第一条(新建对话),"message" 是之后的每条。
|
|
@@ -156,10 +163,12 @@ export interface CliStoreOptions extends CenterClientOptions {
|
|
|
156
163
|
|
|
157
164
|
/** 发一条消息时可以单独指定的新建参数(组件自己的 props),优先于仓库的。 */
|
|
158
165
|
export interface SendOptions {
|
|
166
|
+
expectedExecutionRevision?: number;
|
|
167
|
+
executionConfig?: import("./execution").ExecutionOptions;
|
|
159
168
|
/** 新建对话后是否改仓库的选中(缺省改)。 */
|
|
160
169
|
select?: boolean;
|
|
161
170
|
newConversation?: NewConversationOptions;
|
|
162
|
-
createConversation?: (firstMessage: string) => Promise<string>;
|
|
171
|
+
createConversation?: (firstMessage: string, options?: NewConversationOptions) => Promise<string>;
|
|
163
172
|
businessIdFor?: (text: string, kind: "create" | "message") => string;
|
|
164
173
|
}
|
|
165
174
|
|
|
@@ -222,7 +231,7 @@ export function createCliStore(options: CliStoreOptions = {}) {
|
|
|
222
231
|
// 列表只放宿主给的 jobIds 和本页里新建 / 接续出来的对话:不从 center 发现,也不从本地存储恢复——
|
|
223
232
|
// 不传就不加载,免得把这个产品线令牌下别的业务、别的用户的对话列出来。
|
|
224
233
|
|
|
225
|
-
let state: CliState = { selected: null, jobIds: [], jobs: {}, convs: {}, listLoading: false, listError: null, refs: {}, viewing: {}, boards: {}, boardContext: {}, context: {} };
|
|
234
|
+
let state: CliState = { selected: null, jobIds: [], jobs: {}, convs: {}, listLoading: false, listError: null, refs: {}, viewing: {}, boards: {}, externalBoards: {}, listVisible: false, externalBoardOptions: {}, boardContext: {}, context: {} };
|
|
226
235
|
const listeners = new Set<() => void>();
|
|
227
236
|
const eventListeners = new Set<(event: CliEvent) => void>();
|
|
228
237
|
const settledTurns = new Set<string>();
|
|
@@ -392,10 +401,11 @@ export function createCliStore(options: CliStoreOptions = {}) {
|
|
|
392
401
|
previous?.();
|
|
393
402
|
if (streams.get(key) !== entry || entry.refs <= 0) return;
|
|
394
403
|
const { jobId, taskId } = parseConvKey(key);
|
|
395
|
-
let ended = false, retired = false;
|
|
404
|
+
let ended = false, retired = false, snapshotReceived = false;
|
|
396
405
|
let cleanup: (() => void) | null = null;
|
|
397
406
|
const owns = () => streams.get(key) === entry && entry.refs > 0 && entry.stop === retire;
|
|
398
407
|
const active = () => !retired && owns();
|
|
408
|
+
const refreshFiles = () => { if (active() && client.board) retryWorkspaceFiles(client.board, key); };
|
|
399
409
|
const retire = () => {
|
|
400
410
|
if (retired) return;
|
|
401
411
|
retired = true;
|
|
@@ -435,13 +445,37 @@ export function createCliStore(options: CliStoreOptions = {}) {
|
|
|
435
445
|
try {
|
|
436
446
|
cleanup = client.stream(jobId, {
|
|
437
447
|
onOpen: () => { if (!active()) return; entry.attempt = 0; patchConv(key, (conv) => ({ ...conv, stream: "live", streamError: null })); },
|
|
438
|
-
onSnapshot: (snapshot) => {
|
|
439
|
-
|
|
440
|
-
|
|
448
|
+
onSnapshot: (snapshot) => {
|
|
449
|
+
if (!active()) return;
|
|
450
|
+
patchConv(key, (conv) => applySnapshot(conv, snapshot));
|
|
451
|
+
// Each connection catches up once, including rounds completed while disconnected.
|
|
452
|
+
if (!snapshotReceived) { snapshotReceived = true; refreshFiles(); }
|
|
453
|
+
},
|
|
454
|
+
onChunk: (items) => {
|
|
441
455
|
if (!active()) return;
|
|
456
|
+
let refresh = false;
|
|
457
|
+
patchConv(key, (conv) => {
|
|
458
|
+
let next = conv;
|
|
459
|
+
for (const item of items) {
|
|
460
|
+
const previous = next;
|
|
461
|
+
next = applyItems(next, [item]);
|
|
462
|
+
// applyItems also rejects events from another batch task's node session.
|
|
463
|
+
if (next === previous) continue;
|
|
464
|
+
// Observe every event: end + next user in one chunk can leave running=true.
|
|
465
|
+
if (item.type === "node" && item.event.type === "turn_end" || item.type === "transcript") refresh = true;
|
|
466
|
+
if (item.type === "turn" && item.turn.status !== "merged" && TERMINAL_TURN.has(item.turn.status)
|
|
467
|
+
&& !TERMINAL_TURN.has(previous.turns.find((turn) => turn.turn_id === item.turn.turn_id)?.status ?? "")) refresh = true;
|
|
468
|
+
}
|
|
469
|
+
return next;
|
|
470
|
+
});
|
|
471
|
+
if (refresh) refreshFiles();
|
|
472
|
+
},
|
|
473
|
+
onEnd: (end) => {
|
|
474
|
+
if (!active() || ended) return;
|
|
442
475
|
ended = true;
|
|
443
476
|
const continuedBy = taskId ? (end.continued_by_job_id && end.continued_by_task_id ? convKey(end.continued_by_job_id, end.continued_by_task_id) : null) : end.continued_by_job_id ?? null;
|
|
444
477
|
patchConv(key, (conv) => ({ ...conv, stream: "ended", endReason: end.reason, continuedBy, nodeRunning: false }));
|
|
478
|
+
refreshFiles();
|
|
445
479
|
void refreshJob(jobId, true);
|
|
446
480
|
},
|
|
447
481
|
onClose: close,
|
|
@@ -569,7 +603,8 @@ export function createCliStore(options: CliStoreOptions = {}) {
|
|
|
569
603
|
|
|
570
604
|
async function sendWithText(key: string | null, text: string, overrides: SendOptions): Promise<string> {
|
|
571
605
|
// 发消息的组件自己的参数优先(同一个仓库里可以有几个对话组件,各带各的新建参数),没有再用仓库的。
|
|
572
|
-
const
|
|
606
|
+
const originalOptions = overrides.newConversation ?? options.newConversation;
|
|
607
|
+
const newConversation = overrides.executionConfig ? { ...originalOptions, agent: overrides.executionConfig.agent ?? originalOptions?.agent, executionConfig: overrides.executionConfig } : originalOptions;
|
|
573
608
|
const createConversation = overrides.createConversation ?? options.createConversation;
|
|
574
609
|
const businessIdFor = overrides.businessIdFor ?? options.businessIdFor;
|
|
575
610
|
const ref = key ? parseConvKey(key) : null;
|
|
@@ -578,7 +613,7 @@ export function createCliStore(options: CliStoreOptions = {}) {
|
|
|
578
613
|
if (ref?.taskId === NEW_TASK) {
|
|
579
614
|
// 在对话任务里新开一个对话:标题取第一行,智能体 / 浏览器模式沿用对话任务的第一个对话(center 决定)。
|
|
580
615
|
if (!client.openConversation) throw new CenterError("当前后端不支持在对话任务里新开对话", "UNSUPPORTED", 0);
|
|
581
|
-
const result = await retryOnce(() => client.openConversation!(ref.jobId, text, { title: firstLine(text, 60), businessId }));
|
|
616
|
+
const result = await retryOnce(() => client.openConversation!(ref.jobId, text, { title: firstLine(text, 60), businessId, agent: newConversation?.agent, executionConfig: newConversation?.executionConfig, browser: newConversation?.browser, timeoutSeconds: newConversation?.timeoutSeconds }));
|
|
582
617
|
const target = convKey(result.job_id, result.task_id ?? result.turn.task_id);
|
|
583
618
|
patchConv(target, (conv) => ({ ...conv, turns: addTurnsIfAbsent(conv.turns, [{ ...result.turn, queue_position: result.queue_position }]) }));
|
|
584
619
|
addJobIfMissing(result.job_id);
|
|
@@ -591,7 +626,7 @@ export function createCliStore(options: CliStoreOptions = {}) {
|
|
|
591
626
|
const selectedBeforeCreate = state.selected;
|
|
592
627
|
let newId: string;
|
|
593
628
|
if (createConversation) {
|
|
594
|
-
newId = await createConversation(text);
|
|
629
|
+
newId = newConversation?.executionConfig ? await createConversation(text, newConversation) : await createConversation(text);
|
|
595
630
|
} else {
|
|
596
631
|
// 新建只发一次:center 的 POST /api/jobs 没有幂等,建好了却没收到响应时重发会建出第二个对话。
|
|
597
632
|
const created = await client.createConversation(text, { ...newConversation, title: newConversation?.title || firstLine(text), businessId: newConversation?.businessId || businessId });
|
|
@@ -621,7 +656,7 @@ export function createCliStore(options: CliStoreOptions = {}) {
|
|
|
621
656
|
void refreshJob(ref.jobId, true);
|
|
622
657
|
return key;
|
|
623
658
|
}
|
|
624
|
-
const result = await retryOnce(() => client.sendMessage(ref.jobId, text, { businessId, ...(ref.taskId ? { taskId: ref.taskId } : {}) }));
|
|
659
|
+
const result = await retryOnce(() => client.sendMessage(ref.jobId, text, { businessId, ...(ref.taskId ? { taskId: ref.taskId } : {}), ...(overrides.expectedExecutionRevision !== undefined ? { expectedExecutionRevision: overrides.expectedExecutionRevision } : {}) }));
|
|
625
660
|
const jobTarget = result.job_id || ref.jobId;
|
|
626
661
|
// 指定了对话的:接续后跟到新对话任务里对应的那个对话;只有 job_id 的照旧只换 job。
|
|
627
662
|
const target = ref.taskId ? convKey(jobTarget, result.task_id ?? result.turn.task_id ?? ref.taskId) : jobTarget;
|
|
@@ -727,7 +762,28 @@ export function createCliStore(options: CliStoreOptions = {}) {
|
|
|
727
762
|
}
|
|
728
763
|
|
|
729
764
|
// 同一个仓库里登记的看板(cli-board)。后登记的优先(例如弹出的看板盖在常驻的上面)。
|
|
730
|
-
const surfaces: BoardSurface[] = [];
|
|
765
|
+
const surfaces: { surface: BoardSurface; owner?: string; inline?: boolean; priority?: () => number }[] = [];
|
|
766
|
+
const visibleLists = new Set<string>();
|
|
767
|
+
function reportListPresence(owner: string, visible: boolean) {
|
|
768
|
+
if (visible) visibleLists.add(owner); else visibleLists.delete(owner);
|
|
769
|
+
const listVisible = visibleLists.size > 0;
|
|
770
|
+
if (state.listVisible !== listVisible) set({ listVisible });
|
|
771
|
+
}
|
|
772
|
+
const externalBoards = new Map<string, Map<string, { visible: boolean; availableTabs?: readonly ("files" | "changes" | "browser")[]; readOnly?: boolean }>>();
|
|
773
|
+
function reportBoardPresence(key: string, owner: string, visible: boolean | null, options: { availableTabs?: readonly ("files" | "changes" | "browser")[]; readOnly?: boolean } = {}) {
|
|
774
|
+
const owners = externalBoards.get(key) ?? new Map<string, { visible: boolean; availableTabs?: readonly ("files" | "changes" | "browser")[]; readOnly?: boolean }>();
|
|
775
|
+
if (visible === null) owners.delete(owner); else owners.set(owner, { visible, ...options });
|
|
776
|
+
if (owners.size) externalBoards.set(key, owners); else externalBoards.delete(key);
|
|
777
|
+
const values = [...owners.values()];
|
|
778
|
+
const shown = values.some(value => value.visible);
|
|
779
|
+
const limits = owners.size ? { availableTabs: (["files", "changes", "browser"] as const).filter(tab => values.every(value => !value.availableTabs || value.availableTabs.includes(tab))), readOnly: values.some(value => value.readOnly) } : undefined;
|
|
780
|
+
if (!!state.externalBoards[key] === shown && JSON.stringify(state.externalBoardOptions[key]) === JSON.stringify(limits)) return;
|
|
781
|
+
const next = { ...state.externalBoards };
|
|
782
|
+
if (shown) next[key] = true; else delete next[key];
|
|
783
|
+
const configurations = { ...state.externalBoardOptions };
|
|
784
|
+
if (limits) configurations[key] = limits; else delete configurations[key];
|
|
785
|
+
set({ externalBoards: next, externalBoardOptions: configurations });
|
|
786
|
+
}
|
|
731
787
|
// ── 看板 → 对话的引用 ─────────────────────────────
|
|
732
788
|
|
|
733
789
|
function setRefs(key: string, refs: BoardRef[]) {
|
|
@@ -821,12 +877,13 @@ export function createCliStore(options: CliStoreOptions = {}) {
|
|
|
821
877
|
const fileSubscriptions = new Map<string,{listeners:Set<{change?:(event:WorkspaceChangeEvent)=>void;status?:WorkspaceWatchOptions['onStatus']}>;stop:()=>void;status?:WorkspaceWatchStatus}>();
|
|
822
878
|
function watchWorkspace(key:string,listener?:(event:WorkspaceChangeEvent)=>void,options:WorkspaceWatchOptions={}) {
|
|
823
879
|
if(!client.board) throw new Error("当前后端不支持工作区文件订阅");
|
|
880
|
+
const releaseScope=options.paths?.length||options.dirs?.length?retainFileScope(client.board,key,{paths:[...options.paths??[]],dirs:[...options.dirs??[]]}):()=>{};
|
|
824
881
|
const item={change:listener,status:options.onStatus};let subscription=fileSubscriptions.get(key);
|
|
825
882
|
if(!subscription){
|
|
826
883
|
subscription={listeners:new Set([item]),stop:()=>{}};fileSubscriptions.set(key,subscription);const owned=subscription;
|
|
827
|
-
owned.stop=subscribeWorkspaceFiles(client.board,key,event=>{emit(event);for(const next of [...owned.listeners])notifyCallback(next.change,cloneWorkspaceChange(event));},{
|
|
884
|
+
owned.stop=subscribeWorkspaceFiles(client.board,key,event=>{emit(event);for(const next of [...owned.listeners])notifyCallback(next.change,cloneWorkspaceChange(event));},{intervalMs:options.intervalMs,onStatus:status=>{owned.status=status;emit(status);for(const next of [...owned.listeners])notifyCallback(next.status,{...status});}});
|
|
828
885
|
}else{subscription.listeners.add(item);if(subscription.status)notifyCallback(item.status,{...subscription.status});retryWorkspaceFiles(client.board,key);}
|
|
829
|
-
const owned=subscription;let active=true;return()=>{if(!active)return;active=false;owned.listeners.delete(item);if(!owned.listeners.size){if(fileSubscriptions.get(key)===owned)fileSubscriptions.delete(key);owned.stop();}};
|
|
886
|
+
const owned=subscription;let active=true;return()=>{if(!active)return;active=false;owned.listeners.delete(item);if(!owned.listeners.size){if(fileSubscriptions.get(key)===owned)fileSubscriptions.delete(key);owned.stop();}releaseScope();};
|
|
830
887
|
}
|
|
831
888
|
|
|
832
889
|
// ── 补充背景:业务方随时追加,随下一条消息发给智能体 ────────
|
|
@@ -890,9 +947,11 @@ export function createCliStore(options: CliStoreOptions = {}) {
|
|
|
890
947
|
if (ref && cur && cur.ref.id === ref.id && cur.off) setViewingState(key, { ...cur, off: false });
|
|
891
948
|
}
|
|
892
949
|
|
|
893
|
-
function openInBoard(jobId: string, target: OpenTarget): boolean {
|
|
950
|
+
function openInBoard(jobId: string, target: OpenTarget, preferredInlineOwner?: string): boolean {
|
|
894
951
|
let handled = false;
|
|
895
|
-
|
|
952
|
+
const available = [...surfaces].reverse().filter(item => !preferredInlineOwner || !item.inline || item.owner === preferredInlineOwner)
|
|
953
|
+
.sort((a, b) => (b.priority?.() ?? 2) - (a.priority?.() ?? 2));
|
|
954
|
+
for (const { surface } of available) {
|
|
896
955
|
try { handled = surface(jobId, { ...target }); }
|
|
897
956
|
catch (error) { console.error("[@spzxg/cli-ui] board surface callback failed", error); }
|
|
898
957
|
if (handled) break;
|
|
@@ -903,14 +962,16 @@ export function createCliStore(options: CliStoreOptions = {}) {
|
|
|
903
962
|
|
|
904
963
|
return {
|
|
905
964
|
client,
|
|
965
|
+
getNewConversationOptions: () => options.newConversation,
|
|
906
966
|
draftScope: connectionDraftScope(options),
|
|
907
967
|
getState: () => state,
|
|
908
968
|
/** 在看板里打开文件 / 浏览器 / 改动。返回有没有看板接住。 */
|
|
909
969
|
openInBoard,
|
|
910
970
|
/** 看板登记自己;返回注销函数。 */
|
|
911
|
-
registerBoard(surface: BoardSurface) {
|
|
912
|
-
|
|
913
|
-
|
|
971
|
+
registerBoard(surface: BoardSurface, options: { owner?: string; inline?: boolean; priority?: () => number } = {}) {
|
|
972
|
+
const entry = { surface, ...options };
|
|
973
|
+
surfaces.push(entry);
|
|
974
|
+
return () => { const i = surfaces.indexOf(entry); if (i >= 0) surfaces.splice(i, 1); };
|
|
914
975
|
},
|
|
915
976
|
subscribe(fn: () => void) { listeners.add(fn); return () => { listeners.delete(fn); }; },
|
|
916
977
|
on(fn: (event: CliEvent) => void) { eventListeners.add(fn); return () => { eventListeners.delete(fn); }; },
|
|
@@ -923,7 +984,7 @@ export function createCliStore(options: CliStoreOptions = {}) {
|
|
|
923
984
|
addRef, removeRef, takeRefs, restoreRefs,
|
|
924
985
|
setViewing, dismissViewing, takeViewing, restoreViewing,
|
|
925
986
|
/** 看板报告自己的样子(看板组件用)。 */
|
|
926
|
-
reportBoard, activateBoard,
|
|
987
|
+
reportBoard, activateBoard, reportBoardPresence, reportListPresence,
|
|
927
988
|
/** 业务方追加项目背景:随下一条消息发给智能体。 */
|
|
928
989
|
appendContext, removeContext, pendingContext,
|
|
929
990
|
/** 工作区文件接口(不显示看板也能用):列文件、读文件、下载、打包。不传 = 当前选中的对话。 */
|
package/src/core/types.ts
CHANGED
|
@@ -131,6 +131,8 @@ export interface BrowserPacing {
|
|
|
131
131
|
|
|
132
132
|
/** 新建对话时可带的参数(POST /api/jobs,mode=conversation)。 */
|
|
133
133
|
export interface NewConversationOptions {
|
|
134
|
+
/** 首轮执行配置。由服务端校验、持久化,并用于后续派发和重试。 */
|
|
135
|
+
executionConfig?: import("./execution").ExecutionOptions;
|
|
134
136
|
title?: string;
|
|
135
137
|
/** 业务自己的 ID(tasks[0].business_id):第一轮的回调里原样带回,用来和业务数据对应。 */
|
|
136
138
|
businessId?: string;
|
|
@@ -400,7 +402,7 @@ export interface FileEntry {
|
|
|
400
402
|
changeToken?: string;
|
|
401
403
|
}
|
|
402
404
|
|
|
403
|
-
export interface DirListing { type: "dir"; path: string; entries: FileEntry[]; truncated: boolean; /** 目录里的条目总数(新版节点给)。 */ total?: number }
|
|
405
|
+
export interface DirListing { type: "dir"; path: string; entries: FileEntry[]; truncated: boolean; /** 目录里的条目总数(新版节点给)。 */ total?: number; nextCursor?: string; revision?: string }
|
|
404
406
|
export interface FileInfo { type: "file"; path: string; name: string; size: number; mtime: number; contentType: string; inline: boolean }
|
|
405
407
|
|
|
406
408
|
export interface DiffFile {
|
package/src/index.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { CliMsgList, CliChat, CliBoard, type CliMsgListProps, type CliChatProps, type CliBoardProps } from "./components";
|
|
2
|
+
export type { GroupChatAdapter, GroupChatMessage, GroupChatReference } from "./chat/GroupChat";
|
|
2
3
|
/**
|
|
3
4
|
* @spzxg/cli-ui —— 任务中心(cli-task-center)对话任务的前端积木,数据全部来自 center 服务。
|
|
4
5
|
*
|
|
@@ -42,6 +43,7 @@ export {
|
|
|
42
43
|
type TextBlock, type MessagePart,
|
|
43
44
|
} from "./core/blocks";
|
|
44
45
|
export type * from "./core/types";
|
|
46
|
+
export type { ExecutionOptions, ExecutionConfiguration } from "./core/execution";
|
|
45
47
|
|
|
46
48
|
|
|
47
49
|
export { RunControls, type RunControlProps, type RunCapabilities, type RunTask } from "./chat/RunControls";
|
|
@@ -50,3 +52,5 @@ export { formatBoardContext, appendBoardContext, BOARD_HISTORY_MAX } from "./boa
|
|
|
50
52
|
export { subscribeWorkspaceFiles, type WorkspaceWatchOptions } from "./board/fileWatch";
|
|
51
53
|
export type { WorkspaceHandlers } from "./core/provider";
|
|
52
54
|
export type { BoardFile } from "./board/workspace";
|
|
55
|
+
|
|
56
|
+
export { syncFile, clearFileSyncNamespace, FILE_SYNC_MAX, type FileSyncTransport, type FileSyncManifest, type FileSyncProgress } from "./board/fileSync";
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// cli-msglist:消息对话列表。界面取自 Operator 的任务栏(TasksColumn 的列表视图,不做看板),
|
|
2
2
|
// 一张卡片是 center 里的一个对话任务(mode=conversation)。对话任务里开了几个对话时,卡片下面按对话展开,
|
|
3
3
|
// 点哪个对话选哪个(选中值是对话键 "job#task");卡片上的「+」在这个对话任务里新开一个对话(同一个工作区)。
|
|
4
|
-
import { useEffect, useMemo, useState, type CSSProperties } from "react";
|
|
4
|
+
import { useEffect, useId, useLayoutEffect, useMemo, useRef, useState, type CSSProperties } from "react";
|
|
5
5
|
import { CliErrorBoundary } from "../ui/ErrorBoundary";
|
|
6
6
|
import type { CliEvent, JobEntry } from "../core/store";
|
|
7
7
|
import type { CliStoreOptions, CliStore } from "../core/store";
|
|
@@ -12,6 +12,7 @@ import { Icon } from "../ui/icons";
|
|
|
12
12
|
import { agentLabel, relTime, toMs } from "../ui/format";
|
|
13
13
|
import { AgentBadge, ErrNote, Skel, StatusDot, type DotCls } from "../ui/shared";
|
|
14
14
|
import { resolveMsgListConfig, type CliMsgListConfig, type ResolvedMsgListConfig } from "../core/config";
|
|
15
|
+
import { useBoardVisibility } from "../board/useBoardVisibility";
|
|
15
16
|
|
|
16
17
|
/** onTurnEnd / onConversationEnd:浏览器里从实时流算出的结果通知,不依赖 center 的 callback.url。 */
|
|
17
18
|
export interface CliMsgListProps extends CliStoreOptions, ResultHandlers {
|
|
@@ -284,6 +285,12 @@ function MsgListInner({ jobIds, selectedId, onSelect, onNew, title = "对话", c
|
|
|
284
285
|
const cfg = useMemo(() => resolveMsgListConfig(config), [configKey]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
285
286
|
const h = cfg.header;
|
|
286
287
|
const store = useCliStore();
|
|
288
|
+
const rootRef = useRef<HTMLDivElement>(null);
|
|
289
|
+
const owner = useId();
|
|
290
|
+
const visible = useBoardVisibility(rootRef, true);
|
|
291
|
+
const expanded = visible && !className?.split(/\s+/).includes("collapsed");
|
|
292
|
+
useLayoutEffect(() => { store.reportListPresence(owner, expanded); }, [store, owner, expanded]);
|
|
293
|
+
useLayoutEffect(() => () => { store.reportListPresence(owner, false); }, [store, owner]);
|
|
287
294
|
const ids = useCliState((s) => s.jobIds);
|
|
288
295
|
const jobs = useCliState((s) => s.jobs);
|
|
289
296
|
const shared = useCliState((s) => s.selected);
|
|
@@ -343,7 +350,7 @@ function MsgListInner({ jobIds, selectedId, onSelect, onNew, title = "对话", c
|
|
|
343
350
|
const showBanner = showRow || showContext;
|
|
344
351
|
|
|
345
352
|
return (
|
|
346
|
-
<div className={`cli-ui cli-msglist ${className ?? ""}`} data-theme={theme} style={{ ...style, ...(width !== undefined ? { width } : null) }}>
|
|
353
|
+
<div ref={rootRef} className={`cli-ui cli-msglist ${className ?? ""}`} data-theme={theme} style={{ ...style, ...(width !== undefined ? { width } : null) }}>
|
|
347
354
|
<div className="col col-tasks">
|
|
348
355
|
{showBanner && <div className="proj-banner">
|
|
349
356
|
{showRow && <div className="pb-row">
|
|
@@ -12,10 +12,13 @@ import { OperatorBoardContext } from "./boardInteraction";
|
|
|
12
12
|
import { toolDataOf } from "./transcriptModel";
|
|
13
13
|
import { FileLinkContext } from "./fileLinks";
|
|
14
14
|
import { Composer } from "./Composer";
|
|
15
|
+
import { QueuedMessages } from "../../ui/QueuedMessages";
|
|
16
|
+
import { useChatScrollbar } from "../../ui/useChatScrollbar";
|
|
15
17
|
import { BranchHistory, useBranchHistory } from "./BranchHistory";
|
|
16
18
|
import { BranchNavigator } from "./BranchNavigator";
|
|
17
19
|
import type { Quote } from "./composerDraft";
|
|
18
20
|
import type { RunControlProps } from "./RunControls";
|
|
21
|
+
import type { ResolvedChatConfig } from "../../core/config";
|
|
19
22
|
const QUIET_AFTER_MS = 180_000;
|
|
20
23
|
function RunningIndicator({ agent, startedAt, lastActivityRef }: { agent: string | null; startedAt: number | null; lastActivityRef: React.MutableRefObject<number> }) {
|
|
21
24
|
const [now, setNow] = useState(() => Date.now());
|
|
@@ -66,6 +69,7 @@ export type ChatProps = {
|
|
|
66
69
|
running: boolean; blockedBy?: string[]; transcriptLoading?: boolean;
|
|
67
70
|
older?: number; onLoadOlder?: (beforeApply?: () => void) => Promise<boolean>;
|
|
68
71
|
controls: RunControlProps; configBusy: boolean; active?: boolean;
|
|
72
|
+
composer?: Pick<ResolvedChatConfig["composer"], "show" | "fileUpload">;
|
|
69
73
|
onSelectTask: (id: string) => void; onFork: (text: string, sourceIds: string[], requestId: string) => Promise<boolean>;
|
|
70
74
|
onSend: (text: string) => Promise<boolean>; onStop: () => Promise<boolean>;
|
|
71
75
|
onStart: () => void; onEdit: () => void; onClear: () => void; onSetSendContext: (v: boolean) => void;
|
|
@@ -74,8 +78,9 @@ export type ChatProps = {
|
|
|
74
78
|
};
|
|
75
79
|
export function Chat(props: ChatProps) { return <ChatSession key={props.task.id} {...props} />; }
|
|
76
80
|
function ChatSession({ project, task, tasks, agents, messages, running, transcriptLoading, older=0, onLoadOlder,
|
|
77
|
-
controls, configBusy, active=true, onSelectTask, onFork, onSend, onStop, onClear, onReconnect, onAnswer, onCancelQueued }: ChatProps) {
|
|
81
|
+
controls, configBusy, composer, active=true, onSelectTask, onFork, onSend, onStop, onClear, onReconnect, onAnswer, onCancelQueued }: ChatProps) {
|
|
78
82
|
const scrollRef = useRef<HTMLDivElement>(null);
|
|
83
|
+
useChatScrollbar(scrollRef, task.id);
|
|
79
84
|
const [quoteRequest, setQuoteRequest] = useState<Quote|null>(null);
|
|
80
85
|
const flow=useBranchHistory(task,tasks);
|
|
81
86
|
const fileScope=useMemo(()=>({taskId:task.id}),[task.id]);
|
|
@@ -83,7 +88,7 @@ function ChatSession({ project, task, tasks, agents, messages, running, transcri
|
|
|
83
88
|
const view=active?'chat':'changes';
|
|
84
89
|
const quote=(m: Msg, source: TaskRow=task, selected?:string)=>{
|
|
85
90
|
const text=selected||splitAttachments(m.content).text;
|
|
86
|
-
if(text.trim()) setQuoteRequest({id:m.id, author:`${m.role==='user'?'你':agentLabel(agents,source.agent)}${source.id===task.id?'':` · ${source.title}`}`,text});
|
|
91
|
+
if(text.trim()) setQuoteRequest({id:m.id, author:`${m.role==='user'?'你':agentLabel(agents,source.agent)}${source.id===task.id?'':` · ${source.title}`}`,text,sourceTaskId:source.id});
|
|
87
92
|
};
|
|
88
93
|
const openBoard=useContext(OperatorBoardContext);
|
|
89
94
|
const handoffs=useRef<Set<string>|null>(null);
|
|
@@ -414,7 +419,7 @@ function ChatSession({ project, task, tasks, agents, messages, running, transcri
|
|
|
414
419
|
// window sheds rows above. Layout effect, so it lands before paint (no jump).
|
|
415
420
|
useLayoutEffect(() => {
|
|
416
421
|
if (pinned.current && scrollRef.current) scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
|
|
417
|
-
}, [messages, running, startIdx, endIdx]);
|
|
422
|
+
}, [messages, running, startIdx, endIdx, win]);
|
|
418
423
|
// …and when the pane itself resizes (the composer growing, a banner
|
|
419
424
|
// appearing). Content resizes are left alone, so expanding a tool card at
|
|
420
425
|
// the bottom doesn't yank its top out of view.
|
|
@@ -436,6 +441,7 @@ function ChatSession({ project, task, tasks, agents, messages, running, transcri
|
|
|
436
441
|
pinned.current=false; setAtBottom(false); flowPositioned.current=true;
|
|
437
442
|
},[active,flow.ready,task.parent_task_id]);
|
|
438
443
|
useEffect(()=>{if(!task.parent_task_id && active) scrollToBottom("auto");},[active,task.id]);
|
|
444
|
+
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 />} />;
|
|
439
445
|
return (<FileLinkContext.Provider value={fileScope}>
|
|
440
446
|
<>
|
|
441
447
|
<div className="transcript-wrap">
|
|
@@ -488,11 +494,6 @@ function ChatSession({ project, task, tasks, agents, messages, running, transcri
|
|
|
488
494
|
{running && !awaitingAnswer && !hasBelow && (
|
|
489
495
|
<RunningIndicator agent={task.agent} startedAt={turnStartedAt} lastActivityRef={lastActivityRef} />
|
|
490
496
|
)}
|
|
491
|
-
{/* Follow-ups queued mid-turn, pinned below the live turn — they
|
|
492
|
-
send in order once it ends. */}
|
|
493
|
-
{!hasBelow && messages.filter((m) => m.role === "queued").map((m) => (
|
|
494
|
-
<MessageView key={m.id} m={m} initial={false} hideWho={false} onAnswer={stableAnswer} onCancelQueued={stableCancelQueued} />
|
|
495
|
-
))}
|
|
496
497
|
{!hasBelow && <BranchNavigator sourceId={task.id} projectId={project.id} tasks={tasks} completed={task.status==='done'} onSelect={onSelectTask} />}
|
|
497
498
|
</div>
|
|
498
499
|
</div>
|
|
@@ -510,7 +511,8 @@ function ChatSession({ project, task, tasks, agents, messages, running, transcri
|
|
|
510
511
|
)}
|
|
511
512
|
</div>
|
|
512
513
|
</div>
|
|
513
|
-
<Composer tasks={tasks} onFork={onFork} task={task} agentLabel={agentLabel(agents, task.agent)} disabled={task.started!==1} configBusy={configBusy} running={running} onSend={onSend} onStop={onStop} onClear={onClear} quoteRequest={quoteRequest} onQuoteHandled={()=>setQuoteRequest(null)} controls={controls}
|
|
514
|
+
{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
|
+
fileUpload={composer?.fileUpload} queued={queued} /> : queued}
|
|
514
516
|
</></FileLinkContext.Provider>
|
|
515
517
|
);
|
|
516
518
|
|