@spzxg/cli-ui 0.6.2 → 0.6.6
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 +49 -0
- package/README.md +165 -19
- package/dist/cli-ui-embed-BAqonuoD.js +45952 -0
- package/dist/{cli-ui-index-BY5i3wId.js → cli-ui-index-b8wF7wrN.js} +2 -2
- package/dist/{cli-ui-jszip.min-m01fexHW.js → cli-ui-jszip.min-B0uLrCHg.js} +2 -2
- package/dist/{cli-ui-jszip.min-BZsUwI-s.js → cli-ui-jszip.min-ggP8OokP.js} +1 -1
- package/dist/{cli-ui-xlsx-CpWicJLT.js → cli-ui-xlsx-DEDdHrJJ.js} +1 -1
- package/dist/cli-ui.css +32 -1
- package/dist/cli-ui.js +1 -1
- package/package.json +17 -10
- package/src/board/BrowserPane.tsx +346 -115
- package/src/board/ChangesPane.tsx +2 -2
- package/src/board/CliBoard.tsx +79 -50
- package/src/board/FileViewer.tsx +54 -31
- package/src/board/FilesPane.tsx +32 -15
- package/src/board/PanelControls.tsx +28 -0
- package/src/board/blocks.tsx +5 -5
- package/src/board/context.ts +65 -0
- package/src/board/fileOps.ts +26 -3
- package/src/board/fileWatch.ts +94 -0
- package/src/board/folderDownload.ts +30 -14
- package/src/board/frameQuote.ts +2 -2
- package/src/board/panels.ts +33 -0
- package/src/board/textLoad.ts +9 -7
- package/src/board/workspace.ts +53 -14
- package/src/chat/CliChat.tsx +93 -40
- package/src/chat/Composer.tsx +44 -42
- package/src/chat/RunControls.tsx +99 -0
- package/src/chat/configuration.ts +15 -0
- package/src/chat/draft.ts +41 -0
- package/src/chat/export.ts +2 -2
- package/src/components.tsx +13 -0
- package/src/core/blocks.ts +17 -6
- package/src/core/callbacks.ts +8 -0
- package/src/core/client.ts +103 -19
- package/src/core/conversation.ts +4 -3
- package/src/core/id.ts +15 -0
- package/src/core/identity.ts +33 -0
- package/src/core/provider.tsx +31 -14
- package/src/core/store.ts +151 -53
- package/src/core/types.ts +64 -9
- package/src/elements.tsx +135 -29
- package/src/embed.ts +6 -8
- package/src/index.ts +24 -17
- package/src/layout/CliSplit.tsx +1 -1
- package/src/layout/split.ts +67 -30
- package/src/msglist/CliMsgList.tsx +19 -13
- package/src/operator/branchFlow.ts +35 -0
- package/src/operator/index.ts +9 -0
- package/src/operator/liveStream.ts +1 -0
- package/src/operator/node/AgentConnect.tsx +624 -0
- package/src/operator/node/AppearancePanel.tsx +35 -0
- package/src/operator/node/BranchHistory.tsx +67 -0
- package/src/operator/node/BranchNavigator.tsx +36 -0
- package/src/operator/node/BrowserPane.tsx +128 -0
- package/src/operator/node/BrowserPopup.tsx +22 -0
- package/src/operator/node/Chat.tsx +517 -0
- package/src/operator/node/CommandPalette.tsx +213 -0
- package/src/operator/node/Composer.tsx +287 -0
- package/src/operator/node/InsightsView.tsx +831 -0
- package/src/operator/node/Layout.tsx +190 -0
- package/src/operator/node/Markdown.tsx +61 -0
- package/src/operator/node/Modal.tsx +179 -0
- package/src/operator/node/NeedsYouMenu.tsx +54 -0
- package/src/operator/node/OnboardingWizard.tsx +250 -0
- package/src/operator/node/Orchestrator.tsx +634 -0
- package/src/operator/node/ProjectLanding.tsx +86 -0
- package/src/operator/node/ProjectsColumn.tsx +137 -0
- package/src/operator/node/ProviderAuth.tsx +561 -0
- package/src/operator/node/RunControls.tsx +2 -0
- package/src/operator/node/RunSwitch.tsx +113 -0
- package/src/operator/node/Services.tsx +245 -0
- package/src/operator/node/SessionRail.tsx +260 -0
- package/src/operator/node/SessionView.tsx +288 -0
- package/src/operator/node/SettingsView.tsx +690 -0
- package/src/operator/node/TaskBoard.tsx +352 -0
- package/src/operator/node/TaskChanges.tsx +530 -0
- package/src/operator/node/TaskHero.tsx +50 -0
- package/src/operator/node/TasksColumn.tsx +200 -0
- package/src/operator/node/Terminal.tsx +143 -0
- package/src/operator/node/Transcript.tsx +468 -0
- package/src/operator/node/ViewFile.tsx +79 -0
- package/src/operator/node/Welcome.tsx +101 -0
- package/src/operator/node/WorktreePrune.tsx +333 -0
- package/src/operator/node/agents.ts +58 -0
- package/src/operator/node/api.ts +40 -0
- package/src/operator/node/boardBackend.ts +82 -0
- package/src/operator/node/boardEvents.ts +22 -0
- package/src/operator/node/boardInteraction.ts +15 -0
- package/src/operator/node/boardState.ts +57 -0
- package/src/operator/node/boardWorkspace.ts +12 -0
- package/src/operator/node/branchFlow.ts +1 -0
- package/src/operator/node/composerDraft.ts +52 -0
- package/src/operator/node/fileLinks.ts +81 -0
- package/src/operator/node/format.ts +290 -0
- package/src/operator/node/github.tsx +234 -0
- package/src/operator/node/icons.tsx +88 -0
- package/src/operator/node/liveStream.ts +270 -0
- package/src/operator/node/modals.tsx +720 -0
- package/src/operator/node/navHistory.ts +70 -0
- package/src/operator/node/persist.ts +26 -0
- package/src/operator/node/protocol/agents/optIn.ts +9 -0
- package/src/operator/node/protocol/agents/types.ts +394 -0
- package/src/operator/node/protocol/approvalFailure.ts +77 -0
- package/src/operator/node/protocol/authFailure.ts +66 -0
- package/src/operator/node/protocol/customModels.ts +23 -0
- package/src/operator/node/protocol/events.ts +13 -0
- package/src/operator/node/protocol/features.ts +70 -0
- package/src/operator/node/protocol/internalUsage.ts +1 -0
- package/src/operator/node/protocol/promptLimits.ts +52 -0
- package/src/operator/node/protocol/storageRetention.ts +13 -0
- package/src/operator/node/protocol/types.ts +332 -0
- package/src/operator/node/protocol/usageLimit.ts +72 -0
- package/src/operator/node/shared.tsx +89 -0
- package/src/operator/node/transcriptModel.ts +129 -0
- package/src/operator/node/types.ts +296 -0
- package/src/operator/node/useGlobalEvents.ts +97 -0
- package/src/operator/node/useOrchestrator.ts +696 -0
- package/src/operator/node/usePrefs.ts +114 -0
- package/src/operator/node/useRecaps.ts +91 -0
- package/src/operator/node/useTaskStream.ts +280 -0
- package/src/operator.base.css +1806 -0
- package/src/operator.css +194 -16
- package/src/styles.css +49 -1
- package/src/ui/Markdown.tsx +7 -2
- package/src/ui/Popover.tsx +82 -0
- package/src/ui/TextBlock.tsx +3 -3
- package/src/ui/Transcript.tsx +17 -10
- package/src/ui/fileLinks.ts +6 -0
- package/src/ui/format.ts +1 -1
- package/src/ui/shared.tsx +1 -1
- package/src/ui/transcriptModel.ts +2 -2
- package/dist/cli-ui-embed-6IJ_jRVh.js +0 -41223
package/src/core/store.ts
CHANGED
|
@@ -10,12 +10,18 @@
|
|
|
10
10
|
*
|
|
11
11
|
* 单独用某个组件时它会自己建一个仓库;放进同一个 <CliCenterProvider>(或同一个 group 的 Web Component)就共享一个。
|
|
12
12
|
*/
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
import
|
|
13
|
+
import { clientId } from "./id";
|
|
14
|
+
import { connectionDraftScope } from "./identity";
|
|
15
|
+
import { notifyCallback } from "./callbacks";
|
|
16
|
+
import { CenterError, createCenterClient, firstLine, type CenterClient, type CenterClientOptions } from "./client";
|
|
17
|
+
import { addTurnsIfAbsent, applyItems, applyOlderPage, applySnapshot, closedCallback, convEnded, convKey, emptyConv, isChatTask, NEW_TASK, parseConvKey, TERMINAL_JOB, TERMINAL_TURN, turnCallback, upsertTurns, type ConvState } from "./conversation";
|
|
18
|
+
import { formatAnswersText } from "../ui/format";
|
|
19
|
+
import { backgroundBlock, charCount, MAX_MESSAGE_CHARS, serializeBlock } from "./blocks";
|
|
20
|
+
import { cloneBoardView, reportBoardOwner, activateBoardOwner, selectedBoardOwner, mergeBoardHistory, rememberBoardFile } from "../board/context";
|
|
21
|
+
import { cloneWorkspaceChange, subscribeWorkspaceFiles, retryWorkspaceFiles, type WorkspaceWatchOptions } from "../board/fileWatch";
|
|
22
|
+
import type { WorkspaceChangeEvent, WorkspaceWatchStatus } from "./types";
|
|
23
|
+
import { createWorkspace, type BoardView, type CliWorkspace } from "../board/workspace";
|
|
24
|
+
import type { AskAnswers, AskQuestion, CenterJob, CenterTurn, ConversationClosedCallback, ConversationTurnCallback, NewConversationOptions } from "./types";
|
|
19
25
|
|
|
20
26
|
export interface JobEntry {
|
|
21
27
|
job: CenterJob | null;
|
|
@@ -46,6 +52,8 @@ export interface CliState {
|
|
|
46
52
|
viewing: Record<string, { ref: BoardRef; off: boolean }>;
|
|
47
53
|
/** 看板现在的样子(按对话键):正在看的文件、开着的文件页签、当前页签。没有看板时没有。 */
|
|
48
54
|
boards: Record<string, BoardView>;
|
|
55
|
+
/** 最近看板背景,收起/卸载后保留,供下一条消息被动参考。 */
|
|
56
|
+
boardContext: Record<string, BoardView>;
|
|
49
57
|
/** 业务方补充的项目背景(按对话键;"" = 还没建的新对话),随下一条消息发出(见 appendContext)。 */
|
|
50
58
|
context: Record<string, ContextItem[]>;
|
|
51
59
|
}
|
|
@@ -100,12 +108,27 @@ export type CliEvent =
|
|
|
100
108
|
| { type: "board_ref"; key: string; refs: BoardRef[] }
|
|
101
109
|
/** 看板上切了页签、打开 / 关掉了文件(view 为 null = 看板关了)。 */
|
|
102
110
|
| { type: "board_change"; key: string; view: BoardView | null }
|
|
111
|
+
| { type: "board_context"; jobId: string; view: BoardView }
|
|
112
|
+
| WorkspaceChangeEvent
|
|
113
|
+
| WorkspaceWatchStatus
|
|
103
114
|
/**
|
|
104
115
|
* 补充背景变了:追加 / 去掉(pending 是还没发出的),或随消息发出了(sent 是这次发出的)。
|
|
105
116
|
* key 是对话键;新对话还没建时是 ""。
|
|
106
117
|
*/
|
|
107
118
|
| { type: "context"; key: string; pending: ContextItem[]; sent?: ContextItem[] };
|
|
108
119
|
|
|
120
|
+
/** Public workspace metadata must not be shared between business listeners. */
|
|
121
|
+
export function cloneWorkspaceEvent(event:CliEvent):CliEvent {
|
|
122
|
+
if(event.type==='files_changed')return cloneWorkspaceChange(event);
|
|
123
|
+
if(event.type==='files_watch')return {...event};
|
|
124
|
+
if(event.type==='board_ref')return {...event,refs:event.refs.map(ref=>({...ref}))};
|
|
125
|
+
if(event.type==='context')return {...event,pending:event.pending.map(item=>({...item})),...(event.sent?{sent:event.sent.map(item=>({...item}))}:{})};
|
|
126
|
+
if(event.type==='open')return {...event,target:{...event.target}};
|
|
127
|
+
if(event.type==='board_context')return {...event,view:cloneBoardView(event.view)};
|
|
128
|
+
if(event.type==='board_change')return {...event,view:event.view?cloneBoardView(event.view):null};
|
|
129
|
+
return event;
|
|
130
|
+
}
|
|
131
|
+
|
|
109
132
|
/** 看板要打开的东西。 */
|
|
110
133
|
export type OpenTarget = { kind: "file"; path: string } | { kind: "browser" } | { kind: "changes" } | { kind: "files"; path?: string }
|
|
111
134
|
/** 宿主用 extraTabs 加的页签。 */
|
|
@@ -199,7 +222,7 @@ export function createCliStore(options: CliStoreOptions = {}) {
|
|
|
199
222
|
// 列表只放宿主给的 jobIds 和本页里新建 / 接续出来的对话:不从 center 发现,也不从本地存储恢复——
|
|
200
223
|
// 不传就不加载,免得把这个产品线令牌下别的业务、别的用户的对话列出来。
|
|
201
224
|
|
|
202
|
-
let state: CliState = { selected: null, jobIds: [], jobs: {}, convs: {}, listLoading: false, listError: null, refs: {}, viewing: {}, boards: {}, context: {} };
|
|
225
|
+
let state: CliState = { selected: null, jobIds: [], jobs: {}, convs: {}, listLoading: false, listError: null, refs: {}, viewing: {}, boards: {}, boardContext: {}, context: {} };
|
|
203
226
|
const listeners = new Set<() => void>();
|
|
204
227
|
const eventListeners = new Set<(event: CliEvent) => void>();
|
|
205
228
|
const settledTurns = new Set<string>();
|
|
@@ -210,9 +233,18 @@ export function createCliStore(options: CliStoreOptions = {}) {
|
|
|
210
233
|
|
|
211
234
|
const set = (next: Partial<CliState>) => {
|
|
212
235
|
state = { ...state, ...next };
|
|
213
|
-
for (const fn of listeners) fn
|
|
236
|
+
for (const fn of [...listeners]) notifyCallback(fn, undefined);
|
|
237
|
+
};
|
|
238
|
+
// Business callbacks may synchronously navigate or register more listeners.
|
|
239
|
+
// Preserve emission order and keep a fixed audience for each notification.
|
|
240
|
+
const eventQueue:Array<{event:CliEvent;listeners:Array<(event:CliEvent)=>void>}>=[];
|
|
241
|
+
let dispatching=false;
|
|
242
|
+
const emit = (event:CliEvent) => {
|
|
243
|
+
eventQueue.push({event,listeners:[...eventListeners]});if(dispatching)return;
|
|
244
|
+
dispatching=true;
|
|
245
|
+
try{while(eventQueue.length){const next=eventQueue.shift()!;for(const fn of next.listeners)notifyCallback(fn,cloneWorkspaceEvent(next.event));}}
|
|
246
|
+
finally{dispatching=false;}
|
|
214
247
|
};
|
|
215
|
-
const emit = (event: CliEvent) => { for (const fn of eventListeners) fn(event); };
|
|
216
248
|
const patchConv = (key: string, fn: (conv: ConvState) => ConvState) => {
|
|
217
249
|
const prev = state.convs[key] ?? emptyConv(key);
|
|
218
250
|
const next = fn(prev);
|
|
@@ -286,7 +318,7 @@ export function createCliStore(options: CliStoreOptions = {}) {
|
|
|
286
318
|
|
|
287
319
|
function removeJob(jobId: string) {
|
|
288
320
|
const ids = state.jobIds.filter((id) => id !== jobId);
|
|
289
|
-
set({ jobIds: ids, selected: state.selected === jobId ? null : state.selected });
|
|
321
|
+
set({ jobIds: ids, selected: state.selected && parseConvKey(state.selected).jobId === jobId ? null : state.selected });
|
|
290
322
|
}
|
|
291
323
|
|
|
292
324
|
/** 对话接续:旧 job 换成新 job(列表位置不变,选中跟过去)。oldId / newId 可以是对话键。 */
|
|
@@ -355,24 +387,27 @@ export function createCliStore(options: CliStoreOptions = {}) {
|
|
|
355
387
|
function connect(key: string) {
|
|
356
388
|
const entry = streams.get(key);
|
|
357
389
|
if (!entry || entry.refs <= 0) return;
|
|
390
|
+
const previous = entry.stop;
|
|
391
|
+
entry.stop = null;
|
|
392
|
+
previous?.();
|
|
393
|
+
if (streams.get(key) !== entry || entry.refs <= 0) return;
|
|
358
394
|
const { jobId, taskId } = parseConvKey(key);
|
|
359
|
-
|
|
360
|
-
let
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
if (!current || current.refs <= 0) return;
|
|
395
|
+
let ended = false, retired = false;
|
|
396
|
+
let cleanup: (() => void) | null = null;
|
|
397
|
+
const owns = () => streams.get(key) === entry && entry.refs > 0 && entry.stop === retire;
|
|
398
|
+
const active = () => !retired && owns();
|
|
399
|
+
const retire = () => {
|
|
400
|
+
if (retired) return;
|
|
401
|
+
retired = true;
|
|
402
|
+
if (entry.stop === retire) entry.stop = null;
|
|
403
|
+
const dispose = cleanup; cleanup = null;
|
|
404
|
+
notifyCallback(dispose ?? undefined, undefined);
|
|
405
|
+
};
|
|
406
|
+
const close = (error: CenterError | null) => {
|
|
407
|
+
if (!active()) return;
|
|
408
|
+
retire();
|
|
409
|
+
const current = streams.get(key);
|
|
410
|
+
if (current !== entry || current.refs <= 0 || current.stop) return;
|
|
376
411
|
// 对话结束(end)不再连;中枢停机(server_shutdown)稍后重连。
|
|
377
412
|
if (ended && state.convs[key]?.endReason !== "server_shutdown") return;
|
|
378
413
|
// 404:对话不存在,重连也没用。
|
|
@@ -393,8 +428,32 @@ export function createCliStore(options: CliStoreOptions = {}) {
|
|
|
393
428
|
current.attempt += 1;
|
|
394
429
|
patchConv(key, (conv) => ({ ...conv, stream: "reconnecting", streamError: error?.message ?? null }));
|
|
395
430
|
current.timer = setTimeout(() => { current.timer = null; connect(key); }, jitter(delay));
|
|
396
|
-
|
|
397
|
-
|
|
431
|
+
};
|
|
432
|
+
entry.stop = retire;
|
|
433
|
+
patchConv(key, (conv) => ({ ...conv, stream: entry.attempt ? "reconnecting" : "connecting", streamError: null }));
|
|
434
|
+
if (!active()) return;
|
|
435
|
+
try {
|
|
436
|
+
cleanup = client.stream(jobId, {
|
|
437
|
+
onOpen: () => { if (!active()) return; entry.attempt = 0; patchConv(key, (conv) => ({ ...conv, stream: "live", streamError: null })); },
|
|
438
|
+
onSnapshot: (snapshot) => { if (active()) patchConv(key, (conv) => applySnapshot(conv, snapshot)); },
|
|
439
|
+
onChunk: (items) => { if (active()) patchConv(key, (conv) => applyItems(conv, items)); },
|
|
440
|
+
onEnd: (end) => {
|
|
441
|
+
if (!active()) return;
|
|
442
|
+
ended = true;
|
|
443
|
+
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
|
+
patchConv(key, (conv) => ({ ...conv, stream: "ended", endReason: end.reason, continuedBy, nodeRunning: false }));
|
|
445
|
+
void refreshJob(jobId, true);
|
|
446
|
+
},
|
|
447
|
+
onClose: close,
|
|
448
|
+
}, undefined, taskId);
|
|
449
|
+
// Some custom transports close synchronously before returning cleanup.
|
|
450
|
+
if (retired || !owns()) {
|
|
451
|
+
const dispose = cleanup; cleanup = null;
|
|
452
|
+
notifyCallback(dispose ?? undefined, undefined);
|
|
453
|
+
}
|
|
454
|
+
} catch (error) {
|
|
455
|
+
close(error instanceof CenterError ? error : new CenterError(String(error), "NETWORK", 0));
|
|
456
|
+
}
|
|
398
457
|
}
|
|
399
458
|
|
|
400
459
|
// 网络恢复 / 标签页切回来时,正在退避等待的连接立刻重连,不用等完退避时间。
|
|
@@ -443,16 +502,22 @@ export function createCliStore(options: CliStoreOptions = {}) {
|
|
|
443
502
|
void refreshJob(jobId);
|
|
444
503
|
connect(key);
|
|
445
504
|
}
|
|
505
|
+
const owned = entry;
|
|
506
|
+
let active = true;
|
|
446
507
|
return () => {
|
|
508
|
+
if (!active) return;
|
|
509
|
+
active = false;
|
|
447
510
|
const current = streams.get(key);
|
|
448
|
-
if (
|
|
511
|
+
if (current !== owned) return;
|
|
449
512
|
current.refs -= 1;
|
|
450
513
|
if (current.refs > 0) return;
|
|
451
|
-
|
|
452
|
-
|
|
514
|
+
// Retire the registry before calling host cleanup: it may synchronously
|
|
515
|
+
// acquire a new lease for the same task.
|
|
453
516
|
streams.delete(key);
|
|
517
|
+
if (current.timer) { clearTimeout(current.timer); current.timer = null; }
|
|
454
518
|
syncLive();
|
|
455
|
-
|
|
519
|
+
current.stop?.();
|
|
520
|
+
if (!streams.has(key)) patchConv(key, (conv) => ({ ...conv, stream: conv.stream === "ended" ? "ended" : "idle" }));
|
|
456
521
|
};
|
|
457
522
|
}
|
|
458
523
|
|
|
@@ -523,6 +588,7 @@ export function createCliStore(options: CliStoreOptions = {}) {
|
|
|
523
588
|
return target;
|
|
524
589
|
}
|
|
525
590
|
if (!key || !ref) {
|
|
591
|
+
const selectedBeforeCreate = state.selected;
|
|
526
592
|
let newId: string;
|
|
527
593
|
if (createConversation) {
|
|
528
594
|
newId = await createConversation(text);
|
|
@@ -534,7 +600,7 @@ export function createCliStore(options: CliStoreOptions = {}) {
|
|
|
534
600
|
}
|
|
535
601
|
addJob(newId);
|
|
536
602
|
// 共享选中只由「跟着选中走」的组件改;传了 jobId、自己接管的对话组件新建对话不去动别人的选中。
|
|
537
|
-
if (overrides.select !== false) set({ selected: newId });
|
|
603
|
+
if (overrides.select !== false && state.selected === selectedBeforeCreate) set({ selected: newId });
|
|
538
604
|
emit({ type: "job_change", jobId: newId, previousJobId: null, taskId: null, key: newId, previousKey: null });
|
|
539
605
|
void refreshJob(newId, true);
|
|
540
606
|
return newId;
|
|
@@ -666,7 +732,7 @@ export function createCliStore(options: CliStoreOptions = {}) {
|
|
|
666
732
|
|
|
667
733
|
function setRefs(key: string, refs: BoardRef[]) {
|
|
668
734
|
const next = { ...state.refs };
|
|
669
|
-
if (refs.length) next[key] = refs; else delete next[key];
|
|
735
|
+
if (refs.length) next[key] = refs.map(ref => ({ ...ref })); else delete next[key];
|
|
670
736
|
set({ refs: next });
|
|
671
737
|
emit({ type: "board_ref", key, refs });
|
|
672
738
|
}
|
|
@@ -723,15 +789,44 @@ export function createCliStore(options: CliStoreOptions = {}) {
|
|
|
723
789
|
}
|
|
724
790
|
|
|
725
791
|
// ── 看板现在的样子(给业务方:当前打开的文件等)────────
|
|
726
|
-
function
|
|
727
|
-
const
|
|
728
|
-
const
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
if
|
|
733
|
-
|
|
734
|
-
|
|
792
|
+
function applyBoardSelection(key:string,top:BoardView|null,history:import("../board/workspace").BoardFile[]) {
|
|
793
|
+
const current=state.boards[key],previous=state.boardContext[key];
|
|
794
|
+
const boardChanged=JSON.stringify(top)!==JSON.stringify(current??null);
|
|
795
|
+
const retained=top?{...cloneBoardView(top),history,visible:top.visible!==false}:previous?{...cloneBoardView(previous),history,visible:false}:null;
|
|
796
|
+
const contextChanged=!!retained&&JSON.stringify(previous)!==JSON.stringify(retained);
|
|
797
|
+
if(!boardChanged&&!contextChanged)return;
|
|
798
|
+
const boards={...state.boards};if(top)boards[key]=cloneBoardView(top);else delete boards[key];
|
|
799
|
+
// Commit both public views before subscribers can read them or navigate again.
|
|
800
|
+
set({...(boardChanged?{boards}:{}),...(contextChanged?{boardContext:{...state.boardContext,[key]:retained!}}:{})});
|
|
801
|
+
if(boardChanged&&state.boards[key]===boards[key])emit({type:"board_change",key,view:top?cloneBoardView(top):null});
|
|
802
|
+
// A reentrant callback may already have selected a newer file. Do not emit
|
|
803
|
+
// or restore the older automatic context over that newer selection.
|
|
804
|
+
if(contextChanged&&state.boardContext[key]===retained)emit({type:"board_context",jobId:key,view:cloneBoardView(retained!)});
|
|
805
|
+
}
|
|
806
|
+
function reportBoard(key:string,view:BoardView|null,owner="default") {
|
|
807
|
+
const sources=boardSources.get(key)??new Map<string,BoardView>();
|
|
808
|
+
let history=state.boardContext[key]?.history??[];
|
|
809
|
+
if(view){history=mergeBoardHistory(history,sources.get(owner),view);reportBoardOwner(sources,owner,cloneBoardView(view));}
|
|
810
|
+
else sources.delete(owner);
|
|
811
|
+
if(sources.size)boardSources.set(key,sources);else boardSources.delete(key);
|
|
812
|
+
const top=selectedBoardOwner(sources)?.[1]??null;applyBoardSelection(key,top,history);
|
|
813
|
+
}
|
|
814
|
+
function activateBoard(key:string,owner:string) {
|
|
815
|
+
const sources=boardSources.get(key);if(!sources)return;
|
|
816
|
+
const top=activateBoardOwner(sources,owner)?.[1];if(!top)return;
|
|
817
|
+
const file=top.current??(top.file?{path:top.file,sourceId:key}:null);
|
|
818
|
+
const history=file?rememberBoardFile(state.boardContext[key]?.history??[],file):state.boardContext[key]?.history??[];
|
|
819
|
+
applyBoardSelection(key,top,history);
|
|
820
|
+
}
|
|
821
|
+
const fileSubscriptions = new Map<string,{listeners:Set<{change?:(event:WorkspaceChangeEvent)=>void;status?:WorkspaceWatchOptions['onStatus']}>;stop:()=>void;status?:WorkspaceWatchStatus}>();
|
|
822
|
+
function watchWorkspace(key:string,listener?:(event:WorkspaceChangeEvent)=>void,options:WorkspaceWatchOptions={}) {
|
|
823
|
+
if(!client.board) throw new Error("当前后端不支持工作区文件订阅");
|
|
824
|
+
const item={change:listener,status:options.onStatus};let subscription=fileSubscriptions.get(key);
|
|
825
|
+
if(!subscription){
|
|
826
|
+
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));},{...options,onStatus:status=>{owned.status=status;emit(status);for(const next of [...owned.listeners])notifyCallback(next.status,{...status});}});
|
|
828
|
+
}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();}};
|
|
735
830
|
}
|
|
736
831
|
|
|
737
832
|
// ── 补充背景:业务方随时追加,随下一条消息发给智能体 ────────
|
|
@@ -764,7 +859,7 @@ export function createCliStore(options: CliStoreOptions = {}) {
|
|
|
764
859
|
if (next.length !== list.length) setContext(k, next);
|
|
765
860
|
}
|
|
766
861
|
/** 还没发出的补充背景。 */
|
|
767
|
-
const pendingContext = (key: string | null): ContextItem[] =>
|
|
862
|
+
const pendingContext = (key: string | null): ContextItem[] => (state.context[ctxKey(key)] ?? []).map(item => ({ ...item }));
|
|
768
863
|
|
|
769
864
|
function workspace(key?: string | null): CliWorkspace {
|
|
770
865
|
const k = key ?? state.selected;
|
|
@@ -773,6 +868,8 @@ export function createCliStore(options: CliStoreOptions = {}) {
|
|
|
773
868
|
backend: client.board,
|
|
774
869
|
key: k,
|
|
775
870
|
view: () => state.boards[k],
|
|
871
|
+
context: () => state.boardContext[k] ? cloneBoardView(state.boardContext[k]) : null,
|
|
872
|
+
watch: (listener, options) => watchWorkspace(k,listener,options),
|
|
776
873
|
open: (path) => openInBoard(k, { kind: "file", path }),
|
|
777
874
|
});
|
|
778
875
|
}
|
|
@@ -795,13 +892,18 @@ export function createCliStore(options: CliStoreOptions = {}) {
|
|
|
795
892
|
|
|
796
893
|
function openInBoard(jobId: string, target: OpenTarget): boolean {
|
|
797
894
|
let handled = false;
|
|
798
|
-
for (
|
|
895
|
+
for (const surface of [...surfaces].reverse()) {
|
|
896
|
+
try { handled = surface(jobId, { ...target }); }
|
|
897
|
+
catch (error) { console.error("[@spzxg/cli-ui] board surface callback failed", error); }
|
|
898
|
+
if (handled) break;
|
|
899
|
+
}
|
|
799
900
|
emit({ type: "open", jobId, target, handled });
|
|
800
901
|
return handled;
|
|
801
902
|
}
|
|
802
903
|
|
|
803
904
|
return {
|
|
804
905
|
client,
|
|
906
|
+
draftScope: connectionDraftScope(options),
|
|
805
907
|
getState: () => state,
|
|
806
908
|
/** 在看板里打开文件 / 浏览器 / 改动。返回有没有看板接住。 */
|
|
807
909
|
openInBoard,
|
|
@@ -821,11 +923,11 @@ export function createCliStore(options: CliStoreOptions = {}) {
|
|
|
821
923
|
addRef, removeRef, takeRefs, restoreRefs,
|
|
822
924
|
setViewing, dismissViewing, takeViewing, restoreViewing,
|
|
823
925
|
/** 看板报告自己的样子(看板组件用)。 */
|
|
824
|
-
reportBoard,
|
|
926
|
+
reportBoard, activateBoard,
|
|
825
927
|
/** 业务方追加项目背景:随下一条消息发给智能体。 */
|
|
826
928
|
appendContext, removeContext, pendingContext,
|
|
827
929
|
/** 工作区文件接口(不显示看板也能用):列文件、读文件、下载、打包。不传 = 当前选中的对话。 */
|
|
828
|
-
workspace,
|
|
930
|
+
workspace, watchWorkspace,
|
|
829
931
|
open, send, answerAsk, cancelTurn, close, loadOlder,
|
|
830
932
|
};
|
|
831
933
|
}
|
|
@@ -854,8 +956,4 @@ async function retryOnce<T>(fn: () => Promise<T>): Promise<T> {
|
|
|
854
956
|
}
|
|
855
957
|
}
|
|
856
958
|
|
|
857
|
-
|
|
858
|
-
const cryptoObj = globalThis.crypto as Crypto | undefined;
|
|
859
|
-
if (cryptoObj?.randomUUID) return cryptoObj.randomUUID();
|
|
860
|
-
return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
|
|
861
|
-
}
|
|
959
|
+
const uid = clientId;
|
package/src/core/types.ts
CHANGED
|
@@ -43,7 +43,13 @@ export interface CenterTurn {
|
|
|
43
43
|
queue_position?: number | null;
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
|
|
46
|
+
/** Node-observed human wait state. Missing/null means unavailable, not confirmed idle. */
|
|
47
|
+
export interface AwaitingInputState {
|
|
48
|
+
awaiting_input?: boolean | null;
|
|
49
|
+
awaiting_input_observed_at?: string | null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface CenterTask extends AwaitingInputState {
|
|
47
53
|
id: string;
|
|
48
54
|
job_id: string;
|
|
49
55
|
seq?: number;
|
|
@@ -63,7 +69,7 @@ export interface CenterTask {
|
|
|
63
69
|
}
|
|
64
70
|
|
|
65
71
|
/** 对话任务里的一个对话(一个子任务、一个 Agent 会话)。conversation.conversations 的一项。 */
|
|
66
|
-
export interface ConversationInfo {
|
|
72
|
+
export interface ConversationInfo extends AwaitingInputState {
|
|
67
73
|
task_id: string;
|
|
68
74
|
seq: number;
|
|
69
75
|
title: string;
|
|
@@ -79,7 +85,7 @@ export interface ConversationInfo {
|
|
|
79
85
|
created_at: string | null;
|
|
80
86
|
}
|
|
81
87
|
|
|
82
|
-
export interface ConversationSummary {
|
|
88
|
+
export interface ConversationSummary extends AwaitingInputState {
|
|
83
89
|
/** 里面的每个对话(按开的先后)。旧版 center 没有这个字段:当作只有一个对话。 */
|
|
84
90
|
conversations?: ConversationInfo[];
|
|
85
91
|
turns: number;
|
|
@@ -117,6 +123,12 @@ export interface SendResult {
|
|
|
117
123
|
turn: CenterTurn;
|
|
118
124
|
}
|
|
119
125
|
|
|
126
|
+
/** 每次浏览器操作前的随机等待范围;合法范围由任务中心与节点共同校验。 */
|
|
127
|
+
export interface BrowserPacing {
|
|
128
|
+
min_seconds: number;
|
|
129
|
+
max_seconds: number;
|
|
130
|
+
}
|
|
131
|
+
|
|
120
132
|
/** 新建对话时可带的参数(POST /api/jobs,mode=conversation)。 */
|
|
121
133
|
export interface NewConversationOptions {
|
|
122
134
|
title?: string;
|
|
@@ -127,6 +139,10 @@ export interface NewConversationOptions {
|
|
|
127
139
|
agent?: string;
|
|
128
140
|
timeoutSeconds?: number;
|
|
129
141
|
browser?: boolean;
|
|
142
|
+
/** Ego 操作手册的 YAML 正文(不是文件名或 URL);必须显式 browser:true。 */
|
|
143
|
+
browserPlaybook?: string;
|
|
144
|
+
/** 转发至节点执行器和浏览器脚本;必须显式 browser:true。 */
|
|
145
|
+
browserPacing?: BrowserPacing;
|
|
130
146
|
}
|
|
131
147
|
|
|
132
148
|
// ── Operator:节点转录 ─────────────────────────────────────────────
|
|
@@ -213,8 +229,8 @@ export type NodeEvent =
|
|
|
213
229
|
| { type: "tool"; id: string; title: string; detail: string; peek?: ToolPeek; diff?: DiffLine[]; msgId?: string; generation?: number }
|
|
214
230
|
| { type: "tool_result"; id: string; content: string; isError: boolean; peek?: ToolPeek; msgId?: string }
|
|
215
231
|
| { type: "ask"; id: string; questions: AskQuestion[]; msgId?: string; generation?: number }
|
|
216
|
-
| { type: "ask_answered"; id: string; answers: AskAnswers; msgId?: string }
|
|
217
|
-
| { type: "input"; input: InputRequest; msgId?: string; generation?: number }
|
|
232
|
+
| { type: "ask_answered"; id: string; answers: AskAnswers; msgId?: string; awaiting_input?: 0 | 1 }
|
|
233
|
+
| { type: "input"; input: InputRequest; msgId?: string; generation?: number; awaiting_input?: 0 | 1 }
|
|
218
234
|
| { type: "usage"; usage: TurnUsage }
|
|
219
235
|
| { type: "notice"; content: string; msgId?: string; generation?: number }
|
|
220
236
|
| { type: "error"; content: string; msgId?: string; generation?: number }
|
|
@@ -233,7 +249,7 @@ export type StreamItem =
|
|
|
233
249
|
| { type: "task"; task: StreamTask | BatchStreamTask };
|
|
234
250
|
|
|
235
251
|
/** 实时流看的那个对话(GET /api/jobs/:id/stream?task=)。 */
|
|
236
|
-
export interface StreamTask { id: string; seq: number; title: string; status: string; closed: boolean; remote_task_id: string | null; continue_from_task_id: string | null }
|
|
252
|
+
export interface StreamTask extends AwaitingInputState { id: string; seq: number; title: string; status: string; closed: boolean; remote_task_id: string | null; continue_from_task_id: string | null }
|
|
237
253
|
|
|
238
254
|
export interface StreamSnapshot {
|
|
239
255
|
job: { id: string; status: JobStatus; closed_reason: string | null; node_id: string | null; mode: string; title: string; conversation: ConversationSummary };
|
|
@@ -246,7 +262,7 @@ export interface StreamSnapshot {
|
|
|
246
262
|
* 一次性任务(含续派)的子任务,实时流里的样子。cli-chat 只读查看这类任务时用。
|
|
247
263
|
* prompt / opener_prompt:指定了 ?task= 时看的那个子任务才有(opener_prompt = 开出它所在节点对话的那个子任务的原文)。
|
|
248
264
|
*/
|
|
249
|
-
export interface BatchStreamTask {
|
|
265
|
+
export interface BatchStreamTask extends AwaitingInputState {
|
|
250
266
|
id: string;
|
|
251
267
|
seq: number;
|
|
252
268
|
task_key: string | null;
|
|
@@ -380,6 +396,8 @@ export interface FileEntry {
|
|
|
380
396
|
type: "dir" | "file";
|
|
381
397
|
size: number;
|
|
382
398
|
mtime: number;
|
|
399
|
+
/** Opaque metadata fingerprint for reconnect reconciliation, not an immutable content version. */
|
|
400
|
+
changeToken?: string;
|
|
383
401
|
}
|
|
384
402
|
|
|
385
403
|
export interface DirListing { type: "dir"; path: string; entries: FileEntry[]; truncated: boolean; /** 目录里的条目总数(新版节点给)。 */ total?: number }
|
|
@@ -424,15 +442,52 @@ export interface BrowserStatus {
|
|
|
424
442
|
}
|
|
425
443
|
|
|
426
444
|
/** 浏览器画面流里的一条消息(节点原样转发)。 */
|
|
427
|
-
export
|
|
445
|
+
export interface BrowserTab { id: string; title: string; url: string }
|
|
446
|
+
/** 新节点用页面身份隔离输入;旧节点可以省略这两个字段。 */
|
|
447
|
+
export interface BrowserTarget { targetId?: string; targetEpoch?: number }
|
|
448
|
+
export type BrowserMessage = BrowserTarget & (
|
|
428
449
|
| { type: "frame"; data: string; metadata?: { deviceWidth: number; deviceHeight: number } }
|
|
429
450
|
| { type: "page"; url?: string; title?: string }
|
|
451
|
+
/** 只包含当前任务内的网页;只有明确声明 canSelect 才显示可切换标签。 */
|
|
452
|
+
| { type: "tabs"; tabs: BrowserTab[]; activeId: string | null; canSelect?: boolean }
|
|
430
453
|
| { type: "error"; message?: string }
|
|
431
454
|
/** 按了复制 / 剪切:选中的文字(只发给按键的人),写进本机剪贴板。 */
|
|
432
455
|
| { type: "clipboard"; text: string }
|
|
433
456
|
/** 网页弹出了 alert / confirm / prompt(画面里看不到,单独显示)。 */
|
|
434
457
|
| { type: "dialog"; dialogType: "alert" | "confirm" | "prompt" | "beforeunload"; message: string; defaultPrompt?: string }
|
|
435
|
-
| { type: "dialog_closed" };
|
|
458
|
+
| { type: "dialog_closed" });
|
|
436
459
|
|
|
437
460
|
/** 浏览器实时连接(GET /browser/live)的画面连接状况。 */
|
|
438
461
|
export type BrowserFramesState = { state: "idle" | "connecting" | "live" | "retrying"; detail?: string };
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
export interface WorkspaceFileChange {
|
|
465
|
+
kind: "created" | "modified" | "deleted";
|
|
466
|
+
path: string;
|
|
467
|
+
file?: FileEntry;
|
|
468
|
+
previous?: FileEntry;
|
|
469
|
+
}
|
|
470
|
+
export type WorkspaceFileBatch =
|
|
471
|
+
| { type: "snapshot"; files: FileEntry[]; revision?: string; complete: boolean }
|
|
472
|
+
| { type: "changes"; changes: WorkspaceFileChange[]; revision?: string; complete: boolean }
|
|
473
|
+
| { type: "resync"; reason: string; complete: false; revision?: string };
|
|
474
|
+
export interface WorkspaceChangeEvent {
|
|
475
|
+
type: "files_changed";
|
|
476
|
+
/** 对话键;节点适配下是 task.id。 */
|
|
477
|
+
jobId: string;
|
|
478
|
+
mode: "events" | "poll";
|
|
479
|
+
coverage: "workspace" | "listed-files";
|
|
480
|
+
complete: boolean;
|
|
481
|
+
changes: WorkspaceFileChange[];
|
|
482
|
+
revision?: string;
|
|
483
|
+
resync?: boolean;
|
|
484
|
+
}
|
|
485
|
+
export interface WorkspaceWatchStatus {
|
|
486
|
+
type: "files_watch";
|
|
487
|
+
jobId: string;
|
|
488
|
+
state: "connecting" | "ready" | "reconnecting" | "error";
|
|
489
|
+
mode: "events" | "poll";
|
|
490
|
+
coverage: "workspace" | "listed-files";
|
|
491
|
+
complete: boolean;
|
|
492
|
+
error?: string;
|
|
493
|
+
}
|