@spzxg/cli-ui 0.6.1 → 0.6.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (133) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/README.md +145 -13
  3. package/dist/cli-ui-embed-BmLN2Jjg.js +43290 -0
  4. package/dist/{cli-ui-index-BFpRZXn4.js → cli-ui-index-D4qfPHMd.js} +2 -2
  5. package/dist/{cli-ui-jszip.min-B9tqMA-e.js → cli-ui-jszip.min-DC9898pC.js} +1 -1
  6. package/dist/{cli-ui-jszip.min-DIavJzUG.js → cli-ui-jszip.min-DpEl0G1z.js} +2 -2
  7. package/dist/{cli-ui-xlsx-BKHhqcoa.js → cli-ui-xlsx-zs7oEtPj.js} +1 -1
  8. package/dist/cli-ui.css +32 -1
  9. package/dist/cli-ui.js +1 -1
  10. package/package.json +14 -8
  11. package/src/board/BrowserPane.tsx +160 -74
  12. package/src/board/ChangesPane.tsx +2 -2
  13. package/src/board/CliBoard.tsx +79 -50
  14. package/src/board/FileViewer.tsx +32 -22
  15. package/src/board/FilesPane.tsx +32 -15
  16. package/src/board/PanelControls.tsx +28 -0
  17. package/src/board/blocks.tsx +5 -5
  18. package/src/board/context.ts +65 -0
  19. package/src/board/fileOps.ts +26 -3
  20. package/src/board/fileWatch.ts +88 -0
  21. package/src/board/folderDownload.ts +30 -14
  22. package/src/board/panels.ts +33 -0
  23. package/src/board/textLoad.ts +2 -2
  24. package/src/board/workspace.ts +38 -9
  25. package/src/chat/CliChat.tsx +113 -53
  26. package/src/chat/Composer.tsx +68 -57
  27. package/src/chat/RunControls.tsx +99 -0
  28. package/src/chat/configuration.ts +15 -0
  29. package/src/chat/draft.ts +41 -0
  30. package/src/chat/export.ts +2 -2
  31. package/src/components.tsx +13 -0
  32. package/src/core/blocks.ts +2 -2
  33. package/src/core/callbacks.ts +8 -0
  34. package/src/core/client.ts +16 -6
  35. package/src/core/config.ts +11 -15
  36. package/src/core/conversation.ts +4 -4
  37. package/src/core/id.ts +15 -0
  38. package/src/core/identity.ts +33 -0
  39. package/src/core/provider.tsx +24 -14
  40. package/src/core/store.ts +83 -26
  41. package/src/core/types.ts +34 -0
  42. package/src/elements.tsx +135 -29
  43. package/src/embed.ts +6 -8
  44. package/src/index.ts +24 -17
  45. package/src/layout/CliSplit.tsx +1 -1
  46. package/src/msglist/CliMsgList.tsx +10 -10
  47. package/src/operator/branchFlow.ts +35 -0
  48. package/src/operator/index.ts +9 -0
  49. package/src/operator/liveStream.ts +1 -0
  50. package/src/operator/node/AgentConnect.tsx +624 -0
  51. package/src/operator/node/AppearancePanel.tsx +35 -0
  52. package/src/operator/node/BranchHistory.tsx +67 -0
  53. package/src/operator/node/BranchNavigator.tsx +36 -0
  54. package/src/operator/node/BrowserPane.tsx +128 -0
  55. package/src/operator/node/BrowserPopup.tsx +22 -0
  56. package/src/operator/node/Chat.tsx +517 -0
  57. package/src/operator/node/CommandPalette.tsx +213 -0
  58. package/src/operator/node/Composer.tsx +287 -0
  59. package/src/operator/node/InsightsView.tsx +831 -0
  60. package/src/operator/node/Layout.tsx +190 -0
  61. package/src/operator/node/Markdown.tsx +61 -0
  62. package/src/operator/node/Modal.tsx +179 -0
  63. package/src/operator/node/NeedsYouMenu.tsx +54 -0
  64. package/src/operator/node/OnboardingWizard.tsx +250 -0
  65. package/src/operator/node/Orchestrator.tsx +634 -0
  66. package/src/operator/node/ProjectLanding.tsx +86 -0
  67. package/src/operator/node/ProjectsColumn.tsx +137 -0
  68. package/src/operator/node/ProviderAuth.tsx +561 -0
  69. package/src/operator/node/RunControls.tsx +2 -0
  70. package/src/operator/node/RunSwitch.tsx +113 -0
  71. package/src/operator/node/Services.tsx +245 -0
  72. package/src/operator/node/SessionRail.tsx +260 -0
  73. package/src/operator/node/SessionView.tsx +288 -0
  74. package/src/operator/node/SettingsView.tsx +690 -0
  75. package/src/operator/node/TaskBoard.tsx +352 -0
  76. package/src/operator/node/TaskChanges.tsx +530 -0
  77. package/src/operator/node/TaskHero.tsx +50 -0
  78. package/src/operator/node/TasksColumn.tsx +200 -0
  79. package/src/operator/node/Terminal.tsx +143 -0
  80. package/src/operator/node/Transcript.tsx +463 -0
  81. package/src/operator/node/ViewFile.tsx +79 -0
  82. package/src/operator/node/Welcome.tsx +101 -0
  83. package/src/operator/node/WorktreePrune.tsx +333 -0
  84. package/src/operator/node/agents.ts +58 -0
  85. package/src/operator/node/api.ts +40 -0
  86. package/src/operator/node/boardBackend.ts +80 -0
  87. package/src/operator/node/boardEvents.ts +22 -0
  88. package/src/operator/node/boardInteraction.ts +15 -0
  89. package/src/operator/node/boardState.ts +57 -0
  90. package/src/operator/node/boardWorkspace.ts +12 -0
  91. package/src/operator/node/branchFlow.ts +1 -0
  92. package/src/operator/node/composerDraft.ts +52 -0
  93. package/src/operator/node/fileLinks.ts +81 -0
  94. package/src/operator/node/format.ts +290 -0
  95. package/src/operator/node/github.tsx +234 -0
  96. package/src/operator/node/icons.tsx +88 -0
  97. package/src/operator/node/liveStream.ts +270 -0
  98. package/src/operator/node/modals.tsx +720 -0
  99. package/src/operator/node/navHistory.ts +70 -0
  100. package/src/operator/node/persist.ts +26 -0
  101. package/src/operator/node/protocol/agents/optIn.ts +9 -0
  102. package/src/operator/node/protocol/agents/types.ts +394 -0
  103. package/src/operator/node/protocol/approvalFailure.ts +77 -0
  104. package/src/operator/node/protocol/authFailure.ts +66 -0
  105. package/src/operator/node/protocol/customModels.ts +23 -0
  106. package/src/operator/node/protocol/events.ts +13 -0
  107. package/src/operator/node/protocol/features.ts +70 -0
  108. package/src/operator/node/protocol/internalUsage.ts +1 -0
  109. package/src/operator/node/protocol/promptLimits.ts +52 -0
  110. package/src/operator/node/protocol/storageRetention.ts +13 -0
  111. package/src/operator/node/protocol/types.ts +332 -0
  112. package/src/operator/node/protocol/usageLimit.ts +72 -0
  113. package/src/operator/node/shared.tsx +89 -0
  114. package/src/operator/node/transcriptModel.ts +129 -0
  115. package/src/operator/node/types.ts +296 -0
  116. package/src/operator/node/useGlobalEvents.ts +97 -0
  117. package/src/operator/node/useOrchestrator.ts +696 -0
  118. package/src/operator/node/usePrefs.ts +114 -0
  119. package/src/operator/node/useRecaps.ts +91 -0
  120. package/src/operator/node/useTaskStream.ts +279 -0
  121. package/src/operator.base.css +1798 -0
  122. package/src/operator.css +186 -16
  123. package/src/styles.css +51 -0
  124. package/src/ui/Markdown.tsx +7 -2
  125. package/src/ui/Popover.tsx +77 -0
  126. package/src/ui/TextBlock.tsx +3 -3
  127. package/src/ui/Transcript.tsx +10 -8
  128. package/src/ui/fileLinks.ts +6 -0
  129. package/src/ui/format.ts +1 -1
  130. package/src/ui/icons.tsx +1 -0
  131. package/src/ui/shared.tsx +1 -1
  132. package/src/ui/transcriptModel.ts +2 -2
  133. package/dist/cli-ui-embed-Dk2QGHxn.js +0 -41146
package/src/core/store.ts CHANGED
@@ -10,12 +10,18 @@
10
10
  *
11
11
  * 单独用某个组件时它会自己建一个仓库;放进同一个 <CliCenterProvider>(或同一个 group 的 Web Component)就共享一个。
12
12
  */
13
- import { CenterError, createCenterClient, firstLine, type CenterClient, type CenterClientOptions } from "./client.js";
14
- import { addTurnsIfAbsent, applyItems, applyOlderPage, applySnapshot, closedCallback, convEnded, convKey, emptyConv, isChatTask, NEW_TASK, parseConvKey, TERMINAL_JOB, TERMINAL_TURN, turnCallback, upsertTurns, type ConvState } from "./conversation.js";
15
- import { formatAnswersText } from "../ui/format.js";
16
- import { backgroundBlock, charCount, MAX_MESSAGE_CHARS, serializeBlock } from "./blocks.js";
17
- import { createWorkspace, type BoardView, type CliWorkspace } from "../board/workspace.js";
18
- import type { AskAnswers, AskQuestion, CenterJob, CenterTurn, ConversationClosedCallback, ConversationTurnCallback, NewConversationOptions } from "./types.js";
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,24 @@ 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_context')return {...event,view:cloneBoardView(event.view)};
125
+ if(event.type==='board_change')return {...event,view:event.view?cloneBoardView(event.view):null};
126
+ return event;
127
+ }
128
+
109
129
  /** 看板要打开的东西。 */
110
130
  export type OpenTarget = { kind: "file"; path: string } | { kind: "browser" } | { kind: "changes" } | { kind: "files"; path?: string }
111
131
  /** 宿主用 extraTabs 加的页签。 */
@@ -199,7 +219,7 @@ export function createCliStore(options: CliStoreOptions = {}) {
199
219
  // 列表只放宿主给的 jobIds 和本页里新建 / 接续出来的对话:不从 center 发现,也不从本地存储恢复——
200
220
  // 不传就不加载,免得把这个产品线令牌下别的业务、别的用户的对话列出来。
201
221
 
202
- let state: CliState = { selected: null, jobIds: [], jobs: {}, convs: {}, listLoading: false, listError: null, refs: {}, viewing: {}, boards: {}, context: {} };
222
+ let state: CliState = { selected: null, jobIds: [], jobs: {}, convs: {}, listLoading: false, listError: null, refs: {}, viewing: {}, boards: {}, boardContext: {}, context: {} };
203
223
  const listeners = new Set<() => void>();
204
224
  const eventListeners = new Set<(event: CliEvent) => void>();
205
225
  const settledTurns = new Set<string>();
@@ -210,9 +230,18 @@ export function createCliStore(options: CliStoreOptions = {}) {
210
230
 
211
231
  const set = (next: Partial<CliState>) => {
212
232
  state = { ...state, ...next };
213
- for (const fn of listeners) fn();
233
+ for (const fn of [...listeners]) notifyCallback(fn, undefined);
234
+ };
235
+ // Business callbacks may synchronously navigate or register more listeners.
236
+ // Preserve emission order and keep a fixed audience for each notification.
237
+ const eventQueue:Array<{event:CliEvent;listeners:Array<(event:CliEvent)=>void>}>=[];
238
+ let dispatching=false;
239
+ const emit = (event:CliEvent) => {
240
+ eventQueue.push({event,listeners:[...eventListeners]});if(dispatching)return;
241
+ dispatching=true;
242
+ try{while(eventQueue.length){const next=eventQueue.shift()!;for(const fn of next.listeners)notifyCallback(fn,cloneWorkspaceEvent(next.event));}}
243
+ finally{dispatching=false;}
214
244
  };
215
- const emit = (event: CliEvent) => { for (const fn of eventListeners) fn(event); };
216
245
  const patchConv = (key: string, fn: (conv: ConvState) => ConvState) => {
217
246
  const prev = state.convs[key] ?? emptyConv(key);
218
247
  const next = fn(prev);
@@ -286,7 +315,7 @@ export function createCliStore(options: CliStoreOptions = {}) {
286
315
 
287
316
  function removeJob(jobId: string) {
288
317
  const ids = state.jobIds.filter((id) => id !== jobId);
289
- set({ jobIds: ids, selected: state.selected === jobId ? null : state.selected });
318
+ set({ jobIds: ids, selected: state.selected && parseConvKey(state.selected).jobId === jobId ? null : state.selected });
290
319
  }
291
320
 
292
321
  /** 对话接续:旧 job 换成新 job(列表位置不变,选中跟过去)。oldId / newId 可以是对话键。 */
@@ -723,15 +752,44 @@ export function createCliStore(options: CliStoreOptions = {}) {
723
752
  }
724
753
 
725
754
  // ── 看板现在的样子(给业务方:当前打开的文件等)────────
726
- function reportBoard(key: string, view: BoardView | null, owner = "default") {
727
- const top = latestReport(boardSources, key, owner, view ? { ...view, files: [...view.files] } : null);
728
- const cur = state.boards[key];
729
- if (!top && !cur) return;
730
- if (top && cur && cur.tab === top.tab && cur.dir === top.dir && cur.file === top.file && cur.files.join("\n") === top.files.join("\n")) return;
731
- const next = { ...state.boards };
732
- if (top) next[key] = top; else delete next[key];
733
- set({ boards: next });
734
- emit({ type: "board_change", key, view: top ? { ...top, files: [...top.files] } : null });
755
+ function applyBoardSelection(key:string,top:BoardView|null,history:import("../board/workspace").BoardFile[]) {
756
+ const current=state.boards[key],previous=state.boardContext[key];
757
+ const boardChanged=JSON.stringify(top)!==JSON.stringify(current??null);
758
+ const retained=top?{...cloneBoardView(top),history,visible:top.visible!==false}:previous?{...cloneBoardView(previous),history,visible:false}:null;
759
+ const contextChanged=!!retained&&JSON.stringify(previous)!==JSON.stringify(retained);
760
+ if(!boardChanged&&!contextChanged)return;
761
+ const boards={...state.boards};if(top)boards[key]=cloneBoardView(top);else delete boards[key];
762
+ // Commit both public views before subscribers can read them or navigate again.
763
+ set({...(boardChanged?{boards}:{}),...(contextChanged?{boardContext:{...state.boardContext,[key]:retained!}}:{})});
764
+ if(boardChanged&&state.boards[key]===boards[key])emit({type:"board_change",key,view:top?cloneBoardView(top):null});
765
+ // A reentrant callback may already have selected a newer file. Do not emit
766
+ // or restore the older automatic context over that newer selection.
767
+ if(contextChanged&&state.boardContext[key]===retained)emit({type:"board_context",jobId:key,view:cloneBoardView(retained!)});
768
+ }
769
+ function reportBoard(key:string,view:BoardView|null,owner="default") {
770
+ const sources=boardSources.get(key)??new Map<string,BoardView>();
771
+ let history=state.boardContext[key]?.history??[];
772
+ if(view){history=mergeBoardHistory(history,sources.get(owner),view);reportBoardOwner(sources,owner,cloneBoardView(view));}
773
+ else sources.delete(owner);
774
+ if(sources.size)boardSources.set(key,sources);else boardSources.delete(key);
775
+ const top=selectedBoardOwner(sources)?.[1]??null;applyBoardSelection(key,top,history);
776
+ }
777
+ function activateBoard(key:string,owner:string) {
778
+ const sources=boardSources.get(key);if(!sources)return;
779
+ const top=activateBoardOwner(sources,owner)?.[1];if(!top)return;
780
+ const file=top.current??(top.file?{path:top.file,sourceId:key}:null);
781
+ const history=file?rememberBoardFile(state.boardContext[key]?.history??[],file):state.boardContext[key]?.history??[];
782
+ applyBoardSelection(key,top,history);
783
+ }
784
+ const fileSubscriptions = new Map<string,{listeners:Set<{change?:(event:WorkspaceChangeEvent)=>void;status?:WorkspaceWatchOptions['onStatus']}>;stop:()=>void;status?:WorkspaceWatchStatus}>();
785
+ function watchWorkspace(key:string,listener?:(event:WorkspaceChangeEvent)=>void,options:WorkspaceWatchOptions={}) {
786
+ if(!client.board) throw new Error("当前后端不支持工作区文件订阅");
787
+ const item={change:listener,status:options.onStatus};let subscription=fileSubscriptions.get(key);
788
+ if(!subscription){
789
+ subscription={listeners:new Set([item]),stop:()=>{}};fileSubscriptions.set(key,subscription);const owned=subscription;
790
+ 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});}});
791
+ }else{subscription.listeners.add(item);if(subscription.status)notifyCallback(item.status,{...subscription.status});retryWorkspaceFiles(client.board,key);}
792
+ 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
793
  }
736
794
 
737
795
  // ── 补充背景:业务方随时追加,随下一条消息发给智能体 ────────
@@ -773,6 +831,8 @@ export function createCliStore(options: CliStoreOptions = {}) {
773
831
  backend: client.board,
774
832
  key: k,
775
833
  view: () => state.boards[k],
834
+ context: () => state.boardContext[k] ? cloneBoardView(state.boardContext[k]) : null,
835
+ watch: (listener, options) => watchWorkspace(k,listener,options),
776
836
  open: (path) => openInBoard(k, { kind: "file", path }),
777
837
  });
778
838
  }
@@ -802,6 +862,7 @@ export function createCliStore(options: CliStoreOptions = {}) {
802
862
 
803
863
  return {
804
864
  client,
865
+ draftScope: connectionDraftScope(options),
805
866
  getState: () => state,
806
867
  /** 在看板里打开文件 / 浏览器 / 改动。返回有没有看板接住。 */
807
868
  openInBoard,
@@ -821,11 +882,11 @@ export function createCliStore(options: CliStoreOptions = {}) {
821
882
  addRef, removeRef, takeRefs, restoreRefs,
822
883
  setViewing, dismissViewing, takeViewing, restoreViewing,
823
884
  /** 看板报告自己的样子(看板组件用)。 */
824
- reportBoard,
885
+ reportBoard, activateBoard,
825
886
  /** 业务方追加项目背景:随下一条消息发给智能体。 */
826
887
  appendContext, removeContext, pendingContext,
827
888
  /** 工作区文件接口(不显示看板也能用):列文件、读文件、下载、打包。不传 = 当前选中的对话。 */
828
- workspace,
889
+ workspace, watchWorkspace,
829
890
  open, send, answerAsk, cancelTurn, close, loadOlder,
830
891
  };
831
892
  }
@@ -854,8 +915,4 @@ async function retryOnce<T>(fn: () => Promise<T>): Promise<T> {
854
915
  }
855
916
  }
856
917
 
857
- function uid() {
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
- }
918
+ const uid = clientId;
package/src/core/types.ts CHANGED
@@ -380,6 +380,8 @@ export interface FileEntry {
380
380
  type: "dir" | "file";
381
381
  size: number;
382
382
  mtime: number;
383
+ /** Opaque metadata fingerprint for reconnect reconciliation, not an immutable content version. */
384
+ changeToken?: string;
383
385
  }
384
386
 
385
387
  export interface DirListing { type: "dir"; path: string; entries: FileEntry[]; truncated: boolean; /** 目录里的条目总数(新版节点给)。 */ total?: number }
@@ -436,3 +438,35 @@ export type BrowserMessage =
436
438
 
437
439
  /** 浏览器实时连接(GET /browser/live)的画面连接状况。 */
438
440
  export type BrowserFramesState = { state: "idle" | "connecting" | "live" | "retrying"; detail?: string };
441
+
442
+
443
+ export interface WorkspaceFileChange {
444
+ kind: "created" | "modified" | "deleted";
445
+ path: string;
446
+ file?: FileEntry;
447
+ previous?: FileEntry;
448
+ }
449
+ export type WorkspaceFileBatch =
450
+ | { type: "snapshot"; files: FileEntry[]; revision?: string; complete: boolean }
451
+ | { type: "changes"; changes: WorkspaceFileChange[]; revision?: string; complete: boolean }
452
+ | { type: "resync"; reason: string; complete: false; revision?: string };
453
+ export interface WorkspaceChangeEvent {
454
+ type: "files_changed";
455
+ /** 对话键;节点适配下是 task.id。 */
456
+ jobId: string;
457
+ mode: "events" | "poll";
458
+ coverage: "workspace" | "listed-files";
459
+ complete: boolean;
460
+ changes: WorkspaceFileChange[];
461
+ revision?: string;
462
+ resync?: boolean;
463
+ }
464
+ export interface WorkspaceWatchStatus {
465
+ type: "files_watch";
466
+ jobId: string;
467
+ state: "connecting" | "ready" | "reconnecting" | "error";
468
+ mode: "events" | "poll";
469
+ coverage: "workspace" | "listed-files";
470
+ complete: boolean;
471
+ error?: string;
472
+ }
package/src/elements.tsx CHANGED
@@ -28,34 +28,111 @@
28
28
  * 渲染在 light DOM 里(不用 Shadow DOM),样式靠页面引入 styles.css,作用域都在 .cli-ui 内。
29
29
  */
30
30
  import { createRoot, type Root } from "react-dom/client";
31
- import { CliMsgList } from "./msglist/CliMsgList.js";
32
- import { CliChat } from "./chat/CliChat.js";
33
- import { CliBoard, type BoardTab } from "./board/CliBoard.js";
34
- import { CliBrowserView, CliFilePreview } from "./board/blocks.js";
35
- import { createCliStore, type CliStore } from "./core/store.js";
36
- import { convKey } from "./core/conversation.js";
37
- import type { CliWorkspace } from "./board/workspace.js";
38
- import { attachSplit, type SplitHandle } from "./layout/split.js";
31
+ import { CliMsgList, CliChat, CliBoard } from "./components";
32
+ import type { BoardTab } from "./board/CliBoard";
33
+ import { CliBrowserView, CliFilePreview } from "./board/blocks";
34
+ import { createCliStore, cloneWorkspaceEvent, type CliEvent, type CliStore } from "./core/store";
35
+ import { convKey } from "./core/conversation";
36
+ import type { CliWorkspace } from "./board/workspace";
37
+ import { attachSplit, type SplitHandle } from "./layout/split";
39
38
 
40
- const groups = new Map<string, CliStore>();
39
+ type StoreOptions = ReturnType<typeof storeOptions>;
40
+ type GroupMember = { el: HTMLElement; refresh: () => void };
41
+ type StoreBucket = { store: CliStore; options: StoreOptions; members: Set<GroupMember> };
42
+ type StoreGroup = { buckets: Map<string, StoreBucket>; preferred: StoreBucket | null; owner: GroupMember | null; configured: boolean; members: Set<GroupMember> };
43
+ type GroupBinding = { groupName: string; group: StoreGroup; bucket: StoreBucket };
44
+ const groups = new Map<string, StoreGroup>();
45
+ const STORE_ATTRS = ["base-url", "token", "line", "namespace", "new-conversation"] as const;
46
+ const identity = (options: StoreOptions) => JSON.stringify(options);
47
+ const configured = (el: HTMLElement) => STORE_ATTRS.some(name => el.hasAttribute(name));
48
+
49
+ const memberships = new WeakMap<GroupMember, { groupName: string; group: StoreGroup }>();
50
+ function notifyGroup(group: StoreGroup, except?: GroupMember) {
51
+ const peers = [...group.members];
52
+ queueMicrotask(() => { for (const peer of peers) if (peer !== except) peer.refresh(); });
53
+ }
54
+ function leaveGroup(member: GroupMember) {
55
+ const membership = memberships.get(member);
56
+ if (!membership) return;
57
+ const { groupName, group } = membership;
58
+ group.members.delete(member);
59
+ for (const [key, bucket] of group.buckets) {
60
+ bucket.members.delete(member);
61
+ if (!bucket.members.size && bucket !== group.preferred) group.buckets.delete(key);
62
+ }
63
+ if (group.owner === member) group.owner = null;
64
+ memberships.delete(member);
65
+ if (!group.members.size) groups.delete(groupName); else notifyGroup(group);
66
+ }
67
+ function releaseGroup(binding: GroupBinding, member: GroupMember, retainOwner = false) {
68
+ const { group, bucket } = binding;
69
+ bucket.members.delete(member);
70
+ if (!retainOwner && group.owner === member) group.owner = null;
71
+ if (!bucket.members.size && bucket !== group.preferred) group.buckets.delete(identity(bucket.options));
72
+ notifyGroup(group, member);
73
+ }
74
+
75
+ function inheritedOptions(el: HTMLElement, base: StoreOptions | null): StoreOptions {
76
+ if (!base) return storeOptions(el);
77
+ return {
78
+ baseUrl: el.hasAttribute("base-url") ? el.getAttribute("base-url")! : base.baseUrl,
79
+ token: el.hasAttribute("token") ? el.getAttribute("token")! : base.token,
80
+ line: el.hasAttribute("line") ? el.getAttribute("line")! : base.line,
81
+ namespace: el.hasAttribute("namespace") ? el.getAttribute("namespace")! : base.namespace,
82
+ newConversation: el.hasAttribute("new-conversation") ? jsonAttr(el, "new-conversation") : base.newConversation,
83
+ };
84
+ }
41
85
 
42
86
  // 服务端渲染(Next.js 等)时没有 HTMLElement:模块照样能导入,defineCliElements() 在服务端什么都不做。
43
87
  const Base = (typeof HTMLElement === "undefined" ? class {} : HTMLElement) as typeof HTMLElement;
44
88
 
45
- function storeFor(el: HTMLElement): CliStore | undefined {
46
- const group = el.getAttribute("group");
47
- if (!group) return undefined;
48
- const baseUrl = el.getAttribute("base-url") ?? undefined;
49
- const key = `${group}|${baseUrl ?? ""}`;
50
- let store = groups.get(key);
51
- if (!store) {
52
- store = createCliStore(storeOptions(el));
53
- groups.set(key, store);
89
+ function storeFor(el: HTMLElement, member: GroupMember, previous?: GroupBinding): GroupBinding | undefined {
90
+ const groupName = el.getAttribute("group");
91
+ if (!groupName) { leaveGroup(member); return undefined; }
92
+ if (memberships.get(member)?.groupName !== groupName) leaveGroup(member);
93
+ let group = groups.get(groupName);
94
+ if (!group) {
95
+ group = { buckets: new Map(), preferred: null, owner: null, configured: false, members: new Set() };
96
+ groups.set(groupName, group);
54
97
  }
55
- return store;
98
+ group.members.add(member);
99
+ memberships.set(member, { groupName, group });
100
+ const ownsDefaults = group.owner === member || (!group.configured && configured(el));
101
+ const matches = [...group.buckets.values()].filter(item => [...item.members].some(peer => configured(peer.el))).filter(item => {
102
+ const values = storeOptions(el);
103
+ return STORE_ATTRS.every(name => {
104
+ if (!el.hasAttribute(name)) return true;
105
+ const key = ({ "base-url": "baseUrl", token: "token", line: "line", namespace: "namespace", "new-conversation": "newConversation" } as const)[name as typeof STORE_ATTRS[number]];
106
+ return JSON.stringify(values[key as keyof StoreOptions]) === JSON.stringify(item.options[key as keyof StoreOptions]);
107
+ });
108
+ });
109
+ if (!ownsDefaults && matches.length > 1) throw new Error("同一 group 有多个连接配置,请明确设置 base-url、token、line 或 namespace,避免混用会话。");
110
+ const options = inheritedOptions(el, ownsDefaults ? null : matches[0]?.options ?? group.preferred?.options ?? null);
111
+ const key = identity(options);
112
+ let bucket = group.buckets.get(key);
113
+ const newBucket = !bucket;
114
+ if (!bucket) {
115
+ bucket = { store: createCliStore(options), options, members: new Set() };
116
+ group.buckets.set(key, bucket);
117
+ }
118
+ bucket.members.add(member);
119
+ if (newBucket) notifyGroup(group, member);
120
+ if (!group.preferred || ownsDefaults) {
121
+ const changed = group.preferred !== bucket;
122
+ group.preferred = bucket;
123
+ if (ownsDefaults) { group.configured = true; group.owner = member; }
124
+ if (changed) {
125
+ // Config may arrive after a sibling with only group was connected. Move
126
+ // inheriting siblings together; explicitly different credentials remain isolated.
127
+ notifyGroup(group, member);
128
+ }
129
+ }
130
+ const binding = { groupName, group, bucket };
131
+ if (previous && (previous.group !== group || previous.bucket !== bucket)) releaseGroup(previous, member, previous.group === group);
132
+ return binding;
56
133
  }
57
134
 
58
- const ATTRS = ["base-url", "token", "line", "namespace", "group", "selected-id", "job-id", "task-id", "title", "color", "theme", "context", "read-only", "config", "new-conversation", "tabs", "default-tab", "file-open", "path", "no-quote", "hidden"];
135
+ const ATTRS = ["base-url", "token", "line", "namespace", "group", "selected-id", "job-id", "task-id", "title", "color", "theme", "context", "read-only", "config", "new-conversation", "tabs", "available-tabs", "default-tab", "file-open", "path", "no-quote", "hidden", "watch-files", "passive-context"];
59
136
 
60
137
  function jsonAttr(el: HTMLElement, name: string): object | undefined {
61
138
  const raw = el.getAttribute(name);
@@ -77,6 +154,9 @@ abstract class CliElement extends Base {
77
154
  static observedAttributes = ATTRS;
78
155
  private root: Root | null = null;
79
156
  private ownStore: CliStore | undefined;
157
+ private ownStoreKey: string | undefined;
158
+ private groupBinding: GroupBinding | undefined;
159
+ private groupMember: GroupMember = { el: this, refresh: () => { if (this.root) this.render(); } };
80
160
  private configValue: object | undefined;
81
161
 
82
162
  /** 界面配置(对象)。优先于 config 属性里的 JSON。 */
@@ -90,7 +170,12 @@ abstract class CliElement extends Base {
90
170
 
91
171
  /** 这个元素用的仓库(同一个 group 的元素共用一个)。高级用法:store.on(...)、store.select(...)。 */
92
172
  get store(): CliStore {
93
- return storeFor(this) ?? (this.ownStore ??= createCliStore(storeOptions(this)));
173
+ const binding = storeFor(this, this.groupMember, this.groupBinding);
174
+ if (binding) { this.groupBinding = binding; return binding.bucket.store; }
175
+ if (this.groupBinding) { releaseGroup(this.groupBinding, this.groupMember); this.groupBinding = undefined; }
176
+ const options = storeOptions(this), key = identity(options);
177
+ if (!this.ownStore || this.ownStoreKey !== key) { this.ownStore = createCliStore(options); this.ownStoreKey = key; }
178
+ return this.ownStore;
94
179
  }
95
180
  /** 元素自己写的对话(job-id + task-id);没写时 null(跟着这一组的选中走)。 */
96
181
  private ownKey(): string | null {
@@ -129,7 +214,12 @@ abstract class CliElement extends Base {
129
214
  }
130
215
  disconnectedCallback() {
131
216
  // 移动节点时会先断开再连上:推到下一轮再卸载。
132
- queueMicrotask(() => { if (!this.isConnected) { this.root?.unmount(); this.root = null; } });
217
+ queueMicrotask(() => { if (!this.isConnected) {
218
+ this.root?.unmount(); this.root = null;
219
+ leaveGroup(this.groupMember); this.groupBinding = undefined;
220
+ this.ownStore = undefined; this.ownStoreKey = undefined;
221
+ this.appliedSelection = null; this.selectionStore = null;
222
+ } });
133
223
  }
134
224
  attributeChangedCallback() { this.syncHidden(); if (this.root) this.render(); }
135
225
  private syncHidden() {
@@ -138,12 +228,15 @@ abstract class CliElement extends Base {
138
228
  }
139
229
 
140
230
  private appliedSelection: string | null = null;
231
+ private selectionStore: CliStore | null = null;
141
232
  /** selected-id:这一组一起看哪个对话。属性变了才切(列表点击换了选中,不会被属性拉回去)。隐藏的元素也生效。 */
142
233
  private applySelection() {
143
234
  const selection = this.getAttribute("selected-id");
144
- if (selection !== null && selection !== this.appliedSelection) {
235
+ const store = this.store;
236
+ if (selection !== null && (selection !== this.appliedSelection || store !== this.selectionStore)) {
145
237
  this.appliedSelection = selection;
146
- this.store.select(selection || null);
238
+ this.selectionStore = store;
239
+ store.select(selection || null);
147
240
  }
148
241
  }
149
242
  protected common() {
@@ -153,7 +246,12 @@ abstract class CliElement extends Base {
153
246
  title: this.getAttribute("title") ?? undefined,
154
247
  color: this.getAttribute("color") ?? undefined,
155
248
  theme: (this.getAttribute("theme") as "light" | "dark" | null) ?? undefined,
156
- onEvent: (event: unknown) => this.dispatchEvent(new CustomEvent("cli-event", { detail: event, bubbles: true })),
249
+ onEvent: (event: unknown) => {
250
+ this.dispatchEvent(new CustomEvent("cli-event", { detail: cloneWorkspaceEvent(event as CliEvent), bubbles: true }));
251
+ const type=(event as {type?:string}).type;
252
+ const name=type==='files_changed'?'cli-files-change':type==='files_watch'?'cli-files-watch':type==='board_context'?'cli-board-context':null;
253
+ if(name)this.dispatchEvent(new CustomEvent(name,{detail:event,bubbles:true}));
254
+ },
157
255
  onTurnEnd: (payload: unknown) => this.dispatchEvent(new CustomEvent("cli-turn-end", { detail: payload, bubbles: true })),
158
256
  onConversationEnd: (payload: unknown) => this.dispatchEvent(new CustomEvent("cli-conversation-end", { detail: payload, bubbles: true })),
159
257
  onCallback: (payload: unknown) => this.dispatchEvent(new CustomEvent("cli-callback", { detail: payload, bubbles: true })),
@@ -164,8 +262,14 @@ abstract class CliElement extends Base {
164
262
  protected abstract element(): React.ReactNode;
165
263
  // 隐藏的元素什么都不渲染:不轮询、不占连接;workspace() / appendContext() 照样能用。
166
264
  private render() {
167
- this.applySelection();
168
- this.root?.render(this.hasAttribute("hidden") ? null : this.element());
265
+ try {
266
+ this.applySelection();
267
+ this.root?.render(this.hasAttribute("hidden") ? null : this.element());
268
+ } catch (error) {
269
+ // Configuration errors must unmount the previous connection, never leave
270
+ // another account's old interface interactive under new attributes.
271
+ this.root?.render(this.hasAttribute("hidden") ? null : <div className="cli-ui cli-error" role="alert">{error instanceof Error ? error.message : "组件连接配置无效"}</div>);
272
+ }
169
273
  }
170
274
  }
171
275
 
@@ -183,7 +287,7 @@ class CliChatElement extends CliElement {
183
287
  const jobId = this.getAttribute("job-id");
184
288
  const taskId = this.getAttribute("task-id");
185
289
  const fileOpen = this.getAttribute("file-open") as "auto" | "board" | "modal" | "link" | null;
186
- return <CliChat {...this.common()} {...(jobId !== null ? { jobId: jobId || null } : {})} {...(taskId ? { taskId } : {})} readOnly={this.hasAttribute("read-only")} {...(fileOpen ? { fileOpen } : {})} />;
290
+ return <CliChat {...this.common()} {...(jobId !== null ? { jobId: jobId || null } : {})} {...(taskId ? { taskId } : {})} passiveContext={this.getAttribute("passive-context")!=="false"} readOnly={this.hasAttribute("read-only")} {...(fileOpen ? { fileOpen } : {})} />;
187
291
  }
188
292
  }
189
293
 
@@ -191,12 +295,14 @@ class CliBoardElement extends CliElement {
191
295
  protected element() {
192
296
  const jobId = this.getAttribute("job-id");
193
297
  const tabs = this.getAttribute("tabs")?.split(",").map((t) => t.trim()).filter(Boolean) as BoardTab[] | undefined;
298
+ const availableTabs = this.getAttribute("available-tabs")?.split(",").map(t=>t.trim()).filter(Boolean) as BoardTab[] | undefined;
194
299
  const defaultTab = (this.getAttribute("default-tab") as BoardTab | null) ?? undefined;
195
300
  // 结果事件只由 <cli-chat> 发(同上);看板只发 cli-event。
196
301
  const { onTurnEnd: _a, onConversationEnd: _b, onCallback: _c, title: _t, color: _k, config: _g, ...common } = this.common();
197
302
  const taskId = this.getAttribute("task-id");
198
- return <CliBoard {...common} {...(jobId !== null ? { jobId: jobId || null } : {})} {...(taskId ? { taskId } : {})} tabs={tabs?.length ? tabs : undefined} defaultTab={defaultTab} readOnly={this.hasAttribute("read-only")}
303
+ return <CliBoard {...common} {...(jobId !== null ? { jobId: jobId || null } : {})} {...(taskId ? { taskId } : {})} tabs={this.hasAttribute("tabs") ? tabs ?? [] : undefined} availableTabs={availableTabs} defaultTab={defaultTab} readOnly={this.hasAttribute("read-only")}
199
304
  // 同一个 group 里有对话:看板里「引用到对话」带进那个对话的下一条消息;no-quote 关掉。
305
+ watchFiles={this.getAttribute("watch-files")!=="false"}
200
306
  quoteToChat={this.hasAttribute("group") && !this.hasAttribute("no-quote")} />;
201
307
  }
202
308
  }
package/src/embed.ts CHANGED
@@ -14,14 +14,12 @@
14
14
  import "./styles.css";
15
15
  import { createElement } from "react";
16
16
  import { createRoot } from "react-dom/client";
17
- import { defineCliElements } from "./elements.js";
18
- import { CliChat, type CliChatProps } from "./chat/CliChat.js";
19
- import { CliMsgList, type CliMsgListProps } from "./msglist/CliMsgList.js";
20
- import { CliBoard, type CliBoardProps } from "./board/CliBoard.js";
21
- import { CliBrowserView, CliFilePreview, type CliBrowserViewProps, type CliFilePreviewProps } from "./board/blocks.js";
22
- import { CliCenterProvider } from "./core/provider.js";
23
- import { createCliStore } from "./core/store.js";
24
- import { createCenterClient } from "./core/client.js";
17
+ import { defineCliElements } from "./elements";
18
+ import { CliChat, CliMsgList, CliBoard, type CliChatProps, type CliMsgListProps, type CliBoardProps } from "./components";
19
+ import { CliBrowserView, CliFilePreview, type CliBrowserViewProps, type CliFilePreviewProps } from "./board/blocks";
20
+ import { CliCenterProvider } from "./core/provider";
21
+ import { createCliStore } from "./core/store";
22
+ import { createCenterClient } from "./core/client";
25
23
 
26
24
  export interface Mounted<P> {
27
25
  /** 合并更新参数(只传要改的)。 */
package/src/index.ts CHANGED
@@ -1,3 +1,4 @@
1
+ export { CliMsgList, CliChat, CliBoard, type CliMsgListProps, type CliChatProps, type CliBoardProps } from "./components";
1
2
  /**
2
3
  * @spzxg/cli-ui —— 任务中心(cli-task-center)对话任务的前端积木,数据全部来自 center 服务。
3
4
  *
@@ -19,27 +20,33 @@
19
20
  * import "@spzxg/cli-ui/styles.css";
20
21
  * 非 React 页面用 Web Component:import { defineCliElements } from "@spzxg/cli-ui/elements"。
21
22
  */
22
- export { CliMsgList, type CliMsgListProps } from "./msglist/CliMsgList.js";
23
- export { CliChat, type CliChatProps } from "./chat/CliChat.js";
24
- export { CliBoard, type CliBoardProps, type BoardTab, type BoardExtraTab } from "./board/CliBoard.js";
25
- export { downloadFolder, zipFolder, listFolder, FOLDER_MAX_FILES, FOLDER_MAX_BYTES, type FolderResult, type FolderZip, type FolderProgress } from "./board/folderDownload.js";
26
- export { createWorkspace, WORKSPACE_TEXT_MAX, WORKSPACE_LIST_MAX, WORKSPACE_FILE_MAX, type CliWorkspace, type BoardView } from "./board/workspace.js";
27
- export { CliFilePreview, CliBrowserView, type CliFilePreviewProps, type CliBrowserViewProps } from "./board/blocks.js";
28
- export { FileViewer, downloadFile, type FileViewerProps, type FileAction } from "./board/FileViewer.js";
29
- export { BrowserPane } from "./board/BrowserPane.js";
30
- export { CliSplit, type CliSplitProps } from "./layout/CliSplit.js";
31
- export { attachSplit, type SplitPane, type SplitOptions, type SplitHandle } from "./layout/split.js";
32
- export { CliCenterProvider, useCliStore, useCliState, type CliCenterProviderProps, type ResultHandlers } from "./core/provider.js";
23
+ export { type BoardTab, type BoardExtraTab } from "./board/CliBoard";
24
+ export { downloadFolder, zipFolder, listFolder, FOLDER_MAX_FILES, FOLDER_MAX_BYTES, type FolderResult, type FolderZip, type FolderProgress } from "./board/folderDownload";
25
+ export { createWorkspace, WORKSPACE_TEXT_MAX, WORKSPACE_LIST_MAX, WORKSPACE_FILE_MAX, type CliWorkspace, type BoardView } from "./board/workspace";
26
+ export { CliFilePreview, CliBrowserView, type CliFilePreviewProps, type CliBrowserViewProps } from "./board/blocks";
27
+ export { FileViewer, downloadFile, type FileViewerProps, type FileAction } from "./board/FileViewer";
28
+ export { BrowserPane } from "./board/BrowserPane";
29
+ export { CliSplit, type CliSplitProps } from "./layout/CliSplit";
30
+ export { attachSplit, type SplitPane, type SplitOptions, type SplitHandle } from "./layout/split";
31
+ export { CliCenterProvider, useCliStore, useCliState, type CliCenterProviderProps, type ResultHandlers } from "./core/provider";
33
32
 
34
33
  export {
35
34
  resolveChatConfig, resolveMsgListConfig, DEFAULT_CHAT_CONFIG, DEFAULT_MSGLIST_CONFIG,
36
35
  type CliChatConfig, type CliMsgListConfig, type ResolvedChatConfig, type ResolvedMsgListConfig,
37
- } from "./core/config.js";
38
- export { createCliStore, CONTEXT_ITEM_MAX, type CliStore, type CliStoreOptions, type CliState, type CliEvent, type JobEntry, type OpenTarget, type BoardRef, type ContextItem } from "./core/store.js";
39
- export { createCenterClient, CenterError, boardFileUrl, type CenterClient, type CenterClientOptions, type StreamHandlers, type BoardBackend, type ConversationBackend } from "./core/client.js";
40
- export { viewMessages, isRunning, runningTurn, convKey, parseConvKey, NEW_TASK, type ConvState } from "./core/conversation.js";
36
+ } from "./core/config";
37
+ export { createCliStore, CONTEXT_ITEM_MAX, type CliStore, type CliStoreOptions, type CliState, type CliEvent, type JobEntry, type OpenTarget, type BoardRef, type ContextItem } from "./core/store";
38
+ export { createCenterClient, CenterError, boardFileUrl, type CenterClient, type CenterClientOptions, type StreamHandlers, type BoardBackend, type ConversationBackend } from "./core/client";
39
+ export { viewMessages, isRunning, runningTurn, convKey, parseConvKey, NEW_TASK, type ConvState } from "./core/conversation";
41
40
  export {
42
41
  composeMessage, splitMessage, serializeBlock, readFileText, isLongText, ACCEPTED_EXTENSIONS, MAX_MESSAGE_CHARS,
43
42
  type TextBlock, type MessagePart,
44
- } from "./core/blocks.js";
45
- export type * from "./core/types.js";
43
+ } from "./core/blocks";
44
+ export type * from "./core/types";
45
+
46
+
47
+ export { RunControls, type RunControlProps, type RunCapabilities, type RunTask } from "./chat/RunControls";
48
+
49
+ export { formatBoardContext, appendBoardContext, BOARD_HISTORY_MAX } from "./board/context";
50
+ export { subscribeWorkspaceFiles, type WorkspaceWatchOptions } from "./board/fileWatch";
51
+ export type { WorkspaceHandlers } from "./core/provider";
52
+ export type { BoardFile } from "./board/workspace";
@@ -12,7 +12,7 @@
12
12
  * 三个组件在同一个 Provider 里照常联动(列表选中 → 对话和看板跟着切;对话里点文件 → 看板打开),分栏只管宽度。
13
13
  */
14
14
  import { Children, useEffect, useRef, type CSSProperties, type ReactNode } from "react";
15
- import { attachSplit, type SplitHandle, type SplitPane } from "./split.js";
15
+ import { attachSplit, type SplitHandle, type SplitPane } from "./split";
16
16
 
17
17
  export type CliSplitProps = {
18
18
  children: ReactNode;
@@ -2,16 +2,16 @@
2
2
  // 一张卡片是 center 里的一个对话任务(mode=conversation)。对话任务里开了几个对话时,卡片下面按对话展开,
3
3
  // 点哪个对话选哪个(选中值是对话键 "job#task");卡片上的「+」在这个对话任务里新开一个对话(同一个工作区)。
4
4
  import { useEffect, useMemo, useState, type CSSProperties } from "react";
5
- import { CliErrorBoundary } from "../ui/ErrorBoundary.js";
6
- import type { CliEvent, JobEntry } from "../core/store.js";
7
- import type { CliStoreOptions, CliStore } from "../core/store.js";
8
- import { useCliState, useCliStore, useCliStoreContext, useComponentEvents, withOwnProvider, type ResultHandlers } from "../core/provider.js";
9
- import { convKey, NEW_TASK, parseConvKey, TERMINAL_JOB } from "../core/conversation.js";
10
- import type { ConversationInfo } from "../core/types.js";
11
- import { Icon } from "../ui/icons.js";
12
- import { agentLabel, relTime, toMs } from "../ui/format.js";
13
- import { AgentBadge, ErrNote, Skel, StatusDot, type DotCls } from "../ui/shared.js";
14
- import { resolveMsgListConfig, type CliMsgListConfig, type ResolvedMsgListConfig } from "../core/config.js";
5
+ import { CliErrorBoundary } from "../ui/ErrorBoundary";
6
+ import type { CliEvent, JobEntry } from "../core/store";
7
+ import type { CliStoreOptions, CliStore } from "../core/store";
8
+ import { useCliState, useCliStore, useCliStoreContext, useComponentEvents, withOwnProvider, type ResultHandlers } from "../core/provider";
9
+ import { convKey, NEW_TASK, parseConvKey, TERMINAL_JOB } from "../core/conversation";
10
+ import type { ConversationInfo } from "../core/types";
11
+ import { Icon } from "../ui/icons";
12
+ import { agentLabel, relTime, toMs } from "../ui/format";
13
+ import { AgentBadge, ErrNote, Skel, StatusDot, type DotCls } from "../ui/shared";
14
+ import { resolveMsgListConfig, type CliMsgListConfig, type ResolvedMsgListConfig } from "../core/config";
15
15
 
16
16
  /** onTurnEnd / onConversationEnd:浏览器里从实时流算出的结果通知,不依赖 center 的 callback.url。 */
17
17
  export interface CliMsgListProps extends CliStoreOptions, ResultHandlers {