@xgspz/cli-ui 0.0.0-stage → 0.6.15

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 (169) hide show
  1. package/CHANGELOG.md +165 -0
  2. package/README.md +517 -2
  3. package/dist/cli-ui-embed-vIXVNs6k.js +50295 -0
  4. package/dist/cli-ui-index-R24x28OX.js +11209 -0
  5. package/dist/cli-ui-jszip.min-DUghzByz.js +2324 -0
  6. package/dist/cli-ui-jszip.min-DfH6rdQD.js +26 -0
  7. package/dist/cli-ui-xlsx-CP0lJhff.js +93 -0
  8. package/dist/cli-ui.css +32 -0
  9. package/dist/cli-ui.js +17 -0
  10. package/package.json +60 -4
  11. package/src/board/BrowserPane.tsx +969 -0
  12. package/src/board/ChangesPane.tsx +185 -0
  13. package/src/board/CliBoard.tsx +484 -0
  14. package/src/board/FileViewer.tsx +721 -0
  15. package/src/board/FilesPane.tsx +262 -0
  16. package/src/board/HtmlReadingPreview.tsx +52 -0
  17. package/src/board/MarkdownPreview.tsx +134 -0
  18. package/src/board/PanelControls.tsx +40 -0
  19. package/src/board/blocks.tsx +97 -0
  20. package/src/board/context.ts +65 -0
  21. package/src/board/diffLines.ts +46 -0
  22. package/src/board/fileKind.ts +250 -0
  23. package/src/board/fileOps.ts +85 -0
  24. package/src/board/fileScope.ts +32 -0
  25. package/src/board/fileSync.ts +351 -0
  26. package/src/board/fileSyncCache.ts +250 -0
  27. package/src/board/fileSyncProcessor.ts +72 -0
  28. package/src/board/fileSyncQueue.ts +56 -0
  29. package/src/board/fileSyncTransport.ts +85 -0
  30. package/src/board/fileWatch.ts +211 -0
  31. package/src/board/folderDownload.ts +211 -0
  32. package/src/board/frameQuote.ts +52 -0
  33. package/src/board/htmlReading.ts +148 -0
  34. package/src/board/panels.ts +33 -0
  35. package/src/board/textLoad.ts +76 -0
  36. package/src/board/useBoardUpload.tsx +51 -0
  37. package/src/board/useBoardVisibility.ts +35 -0
  38. package/src/board/workspace.ts +200 -0
  39. package/src/board/zip.ts +85 -0
  40. package/src/chat/CliChat.tsx +960 -0
  41. package/src/chat/Composer.tsx +239 -0
  42. package/src/chat/GroupChat.tsx +113 -0
  43. package/src/chat/RunControls.tsx +196 -0
  44. package/src/chat/configuration.ts +15 -0
  45. package/src/chat/draft.ts +49 -0
  46. package/src/chat/export.ts +34 -0
  47. package/src/chat/queueEdit.ts +27 -0
  48. package/src/chat/useExecutionConfig.ts +93 -0
  49. package/src/components.tsx +13 -0
  50. package/src/core/blocks.ts +205 -0
  51. package/src/core/callbacks.ts +8 -0
  52. package/src/core/client.ts +822 -0
  53. package/src/core/config.ts +252 -0
  54. package/src/core/conversation.ts +581 -0
  55. package/src/core/documentUpload.ts +196 -0
  56. package/src/core/execution.ts +24 -0
  57. package/src/core/id.ts +15 -0
  58. package/src/core/identity.ts +21 -0
  59. package/src/core/provider.tsx +141 -0
  60. package/src/core/sha256.ts +21 -0
  61. package/src/core/sse.ts +51 -0
  62. package/src/core/store.ts +1109 -0
  63. package/src/core/subagents.ts +78 -0
  64. package/src/core/textDecode.ts +24 -0
  65. package/src/core/types.ts +507 -0
  66. package/src/core/xlsx.ts +142 -0
  67. package/src/elements.tsx +376 -0
  68. package/src/embed.ts +56 -0
  69. package/src/index.ts +56 -0
  70. package/src/layout/CliSplit.tsx +53 -0
  71. package/src/layout/split.ts +251 -0
  72. package/src/msglist/CliMsgList.tsx +422 -0
  73. package/src/operator/branchFlow.ts +35 -0
  74. package/src/operator/index.ts +9 -0
  75. package/src/operator/liveStream.ts +1 -0
  76. package/src/operator/node/AgentConnect.tsx +619 -0
  77. package/src/operator/node/AppearancePanel.tsx +35 -0
  78. package/src/operator/node/BranchHistory.tsx +67 -0
  79. package/src/operator/node/BranchNavigator.tsx +36 -0
  80. package/src/operator/node/BrowserPane.tsx +128 -0
  81. package/src/operator/node/BrowserPopup.tsx +22 -0
  82. package/src/operator/node/Chat.tsx +538 -0
  83. package/src/operator/node/CommandPalette.tsx +213 -0
  84. package/src/operator/node/Composer.tsx +316 -0
  85. package/src/operator/node/InsightsView.tsx +831 -0
  86. package/src/operator/node/Layout.tsx +192 -0
  87. package/src/operator/node/Markdown.tsx +61 -0
  88. package/src/operator/node/Modal.tsx +179 -0
  89. package/src/operator/node/NeedsYouMenu.tsx +54 -0
  90. package/src/operator/node/OnboardingWizard.tsx +248 -0
  91. package/src/operator/node/Orchestrator.tsx +644 -0
  92. package/src/operator/node/ProjectLanding.tsx +86 -0
  93. package/src/operator/node/ProjectsColumn.tsx +137 -0
  94. package/src/operator/node/ProviderAuth.tsx +559 -0
  95. package/src/operator/node/RunControls.tsx +2 -0
  96. package/src/operator/node/RunSwitch.tsx +113 -0
  97. package/src/operator/node/Services.tsx +245 -0
  98. package/src/operator/node/SessionRail.tsx +299 -0
  99. package/src/operator/node/SessionView.tsx +292 -0
  100. package/src/operator/node/SettingsView.tsx +690 -0
  101. package/src/operator/node/TaskBoard.tsx +352 -0
  102. package/src/operator/node/TaskChanges.tsx +526 -0
  103. package/src/operator/node/TaskHero.tsx +50 -0
  104. package/src/operator/node/TasksColumn.tsx +200 -0
  105. package/src/operator/node/Terminal.tsx +143 -0
  106. package/src/operator/node/Transcript.tsx +485 -0
  107. package/src/operator/node/ViewFile.tsx +79 -0
  108. package/src/operator/node/Welcome.tsx +101 -0
  109. package/src/operator/node/WorktreePrune.tsx +333 -0
  110. package/src/operator/node/agents.ts +58 -0
  111. package/src/operator/node/api.ts +57 -0
  112. package/src/operator/node/boardBackend.ts +63 -0
  113. package/src/operator/node/boardEvents.ts +40 -0
  114. package/src/operator/node/boardInteraction.ts +15 -0
  115. package/src/operator/node/boardState.ts +74 -0
  116. package/src/operator/node/boardWorkspace.ts +15 -0
  117. package/src/operator/node/boundedPoll.ts +47 -0
  118. package/src/operator/node/branchFlow.ts +1 -0
  119. package/src/operator/node/composerDraft.ts +78 -0
  120. package/src/operator/node/fileLinks.ts +81 -0
  121. package/src/operator/node/format.ts +292 -0
  122. package/src/operator/node/github.tsx +232 -0
  123. package/src/operator/node/icons.tsx +88 -0
  124. package/src/operator/node/liveStream.ts +323 -0
  125. package/src/operator/node/modals.tsx +766 -0
  126. package/src/operator/node/navHistory.ts +70 -0
  127. package/src/operator/node/persist.ts +26 -0
  128. package/src/operator/node/protocol/agents/optIn.ts +9 -0
  129. package/src/operator/node/protocol/agents/types.ts +394 -0
  130. package/src/operator/node/protocol/approvalFailure.ts +77 -0
  131. package/src/operator/node/protocol/authFailure.ts +66 -0
  132. package/src/operator/node/protocol/customModels.ts +23 -0
  133. package/src/operator/node/protocol/events.ts +13 -0
  134. package/src/operator/node/protocol/features.ts +70 -0
  135. package/src/operator/node/protocol/internalUsage.ts +1 -0
  136. package/src/operator/node/protocol/promptLimits.ts +52 -0
  137. package/src/operator/node/protocol/storageRetention.ts +13 -0
  138. package/src/operator/node/protocol/types.ts +335 -0
  139. package/src/operator/node/protocol/usageLimit.ts +72 -0
  140. package/src/operator/node/shared.tsx +89 -0
  141. package/src/operator/node/transcriptModel.ts +129 -0
  142. package/src/operator/node/types.ts +305 -0
  143. package/src/operator/node/useGlobalEvents.ts +97 -0
  144. package/src/operator/node/useOrchestrator.ts +697 -0
  145. package/src/operator/node/usePrefs.ts +114 -0
  146. package/src/operator/node/useRecaps.ts +102 -0
  147. package/src/operator/node/useTaskStream.ts +286 -0
  148. package/src/operator.base.css +1952 -0
  149. package/src/operator.css +1952 -0
  150. package/src/styles.css +579 -0
  151. package/src/ui/ErrorBoundary.tsx +38 -0
  152. package/src/ui/Markdown.tsx +60 -0
  153. package/src/ui/Popover.tsx +95 -0
  154. package/src/ui/QueuedMessages.tsx +110 -0
  155. package/src/ui/ReferenceCard.tsx +28 -0
  156. package/src/ui/SubagentChat.tsx +233 -0
  157. package/src/ui/TextBlock.tsx +51 -0
  158. package/src/ui/Transcript.tsx +435 -0
  159. package/src/ui/UploadProgress.tsx +46 -0
  160. package/src/ui/WorkspaceDrawer.tsx +64 -0
  161. package/src/ui/WorkspaceTabs.tsx +60 -0
  162. package/src/ui/fileLinks.ts +52 -0
  163. package/src/ui/format.ts +103 -0
  164. package/src/ui/icons.tsx +92 -0
  165. package/src/ui/reference.ts +40 -0
  166. package/src/ui/shared.tsx +38 -0
  167. package/src/ui/transcriptModel.ts +151 -0
  168. package/src/ui/useChatScrollbar.ts +29 -0
  169. package/src/ui/useComposerAutosize.ts +54 -0
@@ -0,0 +1,142 @@
1
+ // Excel(.xlsx / .xlsm)→ 纯文本:每个工作表一段 CSV,发给智能体。
2
+ //
3
+ // .xlsx 是 ZIP 里的一堆 XML:workbook.xml 列出工作表,sharedStrings.xml 是共享字符串,worksheets/sheetN.xml 是单元格。
4
+ // 只取单元格的值(公式取缓存的结果),不要格式、图片、图表;日期按单元格格式认出来写成 2026-08-10(带时间的加上时分),
5
+ // 百分比写成 0.6%(和表格里看到的一致)。
6
+ // 老版 .xls(二进制)读不了,调用方提示另存为 .xlsx。
7
+
8
+ export interface SheetText { name: string; hidden: boolean; rows: string[][] }
9
+
10
+ const NS_MAIN = "http://schemas.openxmlformats.org/spreadsheetml/2006/main";
11
+ const NS_REL = "http://schemas.openxmlformats.org/officeDocument/2006/relationships";
12
+
13
+ /** 内置的日期 / 时间数字格式编号。 */
14
+ const BUILTIN_DATE_FMTS = new Set([14, 15, 16, 17, 18, 19, 20, 21, 22, 27, 30, 36, 45, 46, 47, 50, 57]);
15
+
16
+ const parseXml = (text: string) => new DOMParser().parseFromString(text, "application/xml");
17
+ const els = (root: Document | Element, tag: string) => Array.from(root.getElementsByTagNameNS(NS_MAIN, tag));
18
+
19
+ /** 「A1」→ 列号(从 0 开始)。 */
20
+ export function columnIndex(ref: string) {
21
+ let n = 0;
22
+ for (const ch of ref) {
23
+ const c = ch.toUpperCase().charCodeAt(0);
24
+ if (c < 65 || c > 90) break;
25
+ n = n * 26 + (c - 64);
26
+ }
27
+ return n - 1;
28
+ }
29
+
30
+ /** 自定义格式里有年月日时(引号里的、[颜色] 不算)就当日期。 */
31
+ function isDateFormat(code: string) {
32
+ const bare = code.replace(/"[^"]*"/g, "").replace(/\[[^\]]*\]/g, "").replace(/\\./g, "");
33
+ return /[ymdhs]/i.test(bare) && !/^[#0.,%E+\-\s]*$/i.test(bare);
34
+ }
35
+
36
+ /** Excel 序列日期 → 2026-08-10 / 2026-08-10 14:30。 */
37
+ export function excelDate(serial: number) {
38
+ const ms = Math.round((serial - 25569) * 86_400_000); // 1970-01-01 = 25569
39
+ const d = new Date(ms);
40
+ if (Number.isNaN(d.getTime())) return String(serial);
41
+ const p = (n: number) => String(n).padStart(2, "0");
42
+ const date = `${d.getUTCFullYear()}-${p(d.getUTCMonth() + 1)}-${p(d.getUTCDate())}`;
43
+ const secs = d.getUTCHours() * 3600 + d.getUTCMinutes() * 60 + d.getUTCSeconds();
44
+ if (!secs) return date;
45
+ const time = `${p(d.getUTCHours())}:${p(d.getUTCMinutes())}${d.getUTCSeconds() ? `:${p(d.getUTCSeconds())}` : ""}`;
46
+ return serial < 1 ? time : `${date} ${time}`;
47
+ }
48
+
49
+ /** 单元格里的文字:<t> 连起来(富文本是多段 <r><t>),不要注音(<rPh>)。 */
50
+ function richText(el: Element) {
51
+ return els(el, "t").filter((t) => t.parentElement?.localName !== "rPh").map((t) => t.textContent ?? "").join("");
52
+ }
53
+
54
+ /** 读出所有工作表(按工作簿里的顺序)。 */
55
+ export async function readXlsx(data: ArrayBuffer): Promise<SheetText[]> {
56
+ // 按需加载(mammoth 也用它,不额外增加体积)。
57
+ const mod = (await import("jszip")) as unknown as { default?: typeof import("jszip") } & typeof import("jszip");
58
+ const JSZip = mod.default ?? mod;
59
+ const zip = await JSZip.loadAsync(data);
60
+ const read = async (path: string) => { const f = zip.file(path); return f ? f.async("string") : null; };
61
+
62
+ const workbook = await read("xl/workbook.xml");
63
+ if (!workbook) throw new Error("不是 Excel 工作簿");
64
+ const rels = new Map<string, string>();
65
+ const relsXml = await read("xl/_rels/workbook.xml.rels");
66
+ if (relsXml) {
67
+ for (const r of Array.from(parseXml(relsXml).getElementsByTagName("Relationship"))) {
68
+ const target = r.getAttribute("Target") ?? "";
69
+ rels.set(r.getAttribute("Id") ?? "", target.startsWith("/") ? target.slice(1) : `xl/${target}`);
70
+ }
71
+ }
72
+ const shared: string[] = [];
73
+ const sharedXml = await read("xl/sharedStrings.xml");
74
+ if (sharedXml) for (const si of els(parseXml(sharedXml), "si")) shared.push(richText(si));
75
+
76
+ // 哪些样式是日期 / 百分比:cellXfs 第 i 个 → numFmtId → 内置格式或自定义格式。
77
+ const dateStyles = new Set<number>();
78
+ const percentStyles = new Set<number>();
79
+ const stylesXml = await read("xl/styles.xml");
80
+ if (stylesXml) {
81
+ const styles = parseXml(stylesXml);
82
+ const custom = new Map(els(styles, "numFmt").map((f) => [Number(f.getAttribute("numFmtId")), f.getAttribute("formatCode") ?? ""]));
83
+ const xfs = els(styles, "cellXfs")[0];
84
+ if (xfs) {
85
+ Array.from(xfs.children).filter((x) => x.localName === "xf").forEach((xf, i) => {
86
+ const id = Number(xf.getAttribute("numFmtId") ?? 0);
87
+ const code = custom.get(id);
88
+ if (BUILTIN_DATE_FMTS.has(id) || (code !== undefined && isDateFormat(code))) dateStyles.add(i);
89
+ else if (id === 9 || id === 10 || (code !== undefined && code.replace(/"[^"]*"/g, "").includes("%"))) percentStyles.add(i);
90
+ });
91
+ }
92
+ }
93
+
94
+ const sheets: SheetText[] = [];
95
+ for (const s of els(parseXml(workbook), "sheet")) {
96
+ const name = s.getAttribute("name") ?? `Sheet${sheets.length + 1}`;
97
+ const path = rels.get(s.getAttributeNS(NS_REL, "id") ?? s.getAttribute("r:id") ?? "") ?? `xl/worksheets/sheet${sheets.length + 1}.xml`;
98
+ const xml = await read(path);
99
+ if (!xml) continue;
100
+ const rows: string[][] = [];
101
+ for (const row of els(parseXml(xml), "row")) {
102
+ const r = Number(row.getAttribute("r")) || rows.length + 1;
103
+ const cells: string[] = [];
104
+ let next = 0;
105
+ for (const c of Array.from(row.children).filter((x) => x.localName === "c")) {
106
+ const ref = c.getAttribute("r");
107
+ const col = ref ? columnIndex(ref) : next;
108
+ next = col + 1;
109
+ const type = c.getAttribute("t");
110
+ const v = els(c, "v")[0]?.textContent ?? "";
111
+ let value: string;
112
+ if (type === "s") value = shared[Number(v)] ?? "";
113
+ else if (type === "inlineStr") value = richText(c);
114
+ else if (type === "b") value = v === "1" ? "TRUE" : v === "0" ? "FALSE" : v;
115
+ else if (type === "str" || type === "e" || !v) value = v;
116
+ else if (dateStyles.has(Number(c.getAttribute("s") ?? -1)) && Number.isFinite(Number(v))) value = excelDate(Number(v));
117
+ else if (percentStyles.has(Number(c.getAttribute("s") ?? -1)) && Number.isFinite(Number(v))) value = `${Number((Number(v) * 100).toPrecision(12))}%`;
118
+ else value = v;
119
+ cells[col] = value;
120
+ }
121
+ rows[r - 1] = Array.from(cells, (x) => x ?? "");
122
+ }
123
+ const dense = Array.from(rows, (x) => x ?? []);
124
+ // 去掉末尾的空行、每行末尾的空格子。
125
+ while (dense.length && dense[dense.length - 1].every((x) => !x)) dense.pop();
126
+ for (const row of dense) while (row.length && !row[row.length - 1]) row.pop();
127
+ sheets.push({ name, hidden: (s.getAttribute("state") ?? "visible") !== "visible", rows: dense });
128
+ }
129
+ return sheets;
130
+ }
131
+
132
+ const csvCell = (v: string) => (/[",\n\r]/.test(v) ? `"${v.replace(/"/g, '""')}"` : v);
133
+
134
+ /** 工作表 → 发给智能体的文字:每个工作表一个标题 + CSV。 */
135
+ export function sheetsToText(sheets: SheetText[]) {
136
+ return sheets
137
+ .map((s) => {
138
+ const head = `## 工作表:${s.name}${s.hidden ? "(隐藏)" : ""}(${s.rows.length} 行)`;
139
+ return s.rows.length ? `${head}\n${s.rows.map((r) => r.map(csvCell).join(",")).join("\n")}` : `${head}\n(空)`;
140
+ })
141
+ .join("\n\n");
142
+ }
@@ -0,0 +1,376 @@
1
+ /**
2
+ * Web Component 版:任何页面(不用 React)都能直接放 <cli-msglist> / <cli-chat>。
3
+ *
4
+ * import "@xgspz/cli-ui/styles.css";
5
+ * import { defineCliElements } from "@xgspz/cli-ui/elements";
6
+ * defineCliElements();
7
+ *
8
+ * <cli-msglist group="a" base-url="/api/center" title="战略"></cli-msglist>
9
+ * <cli-chat group="a"></cli-chat>
10
+ * <cli-board group="a"></cli-board> (右侧看板:文件 / 改动 / 浏览器;对话里点文件会在这里打开)
11
+ * <cli-file-preview job-id="job_x" path="outputs/报告.md"></cli-file-preview> (单独的文件预览 + 下载)
12
+ * <cli-browser job-id="job_x"></cli-browser> (单独的浏览器实时画面)
13
+ * <cli-split storage-key="a"> <cli-msglist …/> <cli-chat …/> <cli-board …/> </cli-split>
14
+ * (可拖动分栏:子元素每个是一栏,栏之间左右拖动改宽度;panes='[{"size":300},{},{"size":"42%"}]')
15
+ *
16
+ * 同一个 group 的元素共享一个仓库,切换时一起变:列表点哪个,对话和看板就显示哪个;不写 group 各自独立。
17
+ * 要一起切换,group 里的元素就不要写 job-id(写了的只看自己那个);从外面打开某个对话,给任意一个元素写
18
+ * selected-id="job_x" 或 "job_x#task_y"(改它就一起切过去),选中变化在 cli-event 的 select 事件里。
19
+ * 属性:base-url、token、line、namespace、group、job-id、task-id(对话任务里的哪个对话,"new" = 新开一个;对话 / 看板)、
20
+ * title、color、theme、context、read-only、
21
+ * tabs(看板页签,逗号分隔:files,changes,browser)、default-tab、file-open(对话:auto / board / modal / link)、
22
+ * config(界面配置 JSON)、new-conversation(新建对话参数 JSON:title / businessId / workspace / callback / agent …)。
23
+ * 界面配置也可以直接赋对象:document.querySelector("cli-chat").config = { header: { export: false } }。
24
+ * 事件(全在浏览器里从实时流算出,和 center 的 callback.url 无关):
25
+ * cli-turn-end(一轮结束)、cli-conversation-end(对话结束),detail 字段与 center 回调体一致;
26
+ * cli-event(全部事件:select / conversation_turn / conversation_closed / job_change / open / error);
27
+ * cli-callback 是旧名字,等同于前两个合在一起,保留兼容。
28
+ * 渲染在 light DOM 里(不用 Shadow DOM),样式靠页面引入 styles.css,作用域都在 .cli-ui 内。
29
+ */
30
+ import { createRoot, type Root } from "react-dom/client";
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";
38
+
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
+ }
85
+
86
+ // 服务端渲染(Next.js 等)时没有 HTMLElement:模块照样能导入,defineCliElements() 在服务端什么都不做。
87
+ const Base = (typeof HTMLElement === "undefined" ? class {} : HTMLElement) as typeof HTMLElement;
88
+
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);
97
+ }
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;
133
+ }
134
+
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"];
136
+
137
+ function jsonAttr(el: HTMLElement, name: string): object | undefined {
138
+ const raw = el.getAttribute(name);
139
+ if (!raw) return undefined;
140
+ try { return JSON.parse(raw) as object; } catch { console.warn(`<${el.localName}> 的 ${name} 不是合法 JSON,已忽略`); return undefined; }
141
+ }
142
+
143
+ function storeOptions(el: HTMLElement) {
144
+ return {
145
+ baseUrl: el.getAttribute("base-url") ?? undefined,
146
+ token: el.getAttribute("token") ?? undefined,
147
+ line: el.getAttribute("line") ?? undefined,
148
+ namespace: el.getAttribute("namespace") ?? el.getAttribute("group") ?? undefined,
149
+ newConversation: jsonAttr(el, "new-conversation"),
150
+ };
151
+ }
152
+
153
+ abstract class CliElement extends Base {
154
+ static observedAttributes = ATTRS;
155
+ private root: Root | null = null;
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(); } };
160
+ private configValue: object | undefined;
161
+
162
+ /** 界面配置(对象)。优先于 config 属性里的 JSON。 */
163
+ get config(): object | undefined {
164
+ return this.configValue ?? jsonAttr(this, "config");
165
+ }
166
+ set config(value: object | undefined) {
167
+ this.configValue = value;
168
+ if (this.root) this.render();
169
+ }
170
+
171
+ /** 这个元素用的仓库(同一个 group 的元素共用一个)。高级用法:store.on(...)、store.select(...)。 */
172
+ get store(): CliStore {
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;
179
+ }
180
+ /** 元素自己写的对话(job-id + task-id);没写时 null(跟着这一组的选中走)。 */
181
+ private ownKey(): string | null {
182
+ const job = this.getAttribute("job-id");
183
+ if (!job) return null;
184
+ const task = this.getAttribute("task-id");
185
+ return task ? convKey(job, task) : job;
186
+ }
187
+ /**
188
+ * 工作区文件接口:列文件、读文件、下载、打包、当前打开的文件。不传 jobId = 这个元素看的对话(没写 job-id 时是这一组选中的)。
189
+ * 看板可以隐藏(<cli-board hidden>)或者干脆不放,接口照样能用。
190
+ */
191
+ workspace(jobId?: string): CliWorkspace {
192
+ return this.store.workspace(jobId ?? this.ownKey() ?? undefined);
193
+ }
194
+ /**
195
+ * 追加项目背景:下一条消息发出时一起带给智能体。opts.jobId 不传 = 这个元素看的对话(还没有对话时,随新对话的第一条消息带上)。
196
+ * 返回这条背景的 id(同一个 id 再追加会替换)。
197
+ */
198
+ appendContext(text: string, opts: { title?: string; id?: string; jobId?: string } = {}): string {
199
+ const key = opts.jobId ?? this.ownKey() ?? this.store.getState().selected;
200
+ return this.store.appendContext(key, text, { title: opts.title, id: opts.id });
201
+ }
202
+ /** 去掉还没发出的补充背景(不传 id = 全部)。 */
203
+ removeContext(id?: string, jobId?: string) {
204
+ this.store.removeContext(jobId ?? this.ownKey() ?? this.store.getState().selected, id);
205
+ }
206
+
207
+ connectedCallback() {
208
+ // 缺省撑满父容器;组件自己内部滚动(宿主给定高度即可)。hidden:不显示、不连接(接口照样能用)。
209
+ this.syncHidden();
210
+ this.style.height ||= "100%";
211
+ this.style.minHeight ||= "0";
212
+ this.root ??= createRoot(this);
213
+ this.render();
214
+ }
215
+ disconnectedCallback() {
216
+ // 移动节点时会先断开再连上:推到下一轮再卸载。
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
+ } });
223
+ }
224
+ attributeChangedCallback() { this.syncHidden(); if (this.root) this.render(); }
225
+ private syncHidden() {
226
+ if (this.hasAttribute("hidden")) this.style.display = "none";
227
+ else if (!this.style.display || this.style.display === "none") this.style.display = "block";
228
+ }
229
+
230
+ private appliedSelection: string | null = null;
231
+ private selectionStore: CliStore | null = null;
232
+ /** selected-id:这一组一起看哪个对话。属性变了才切(列表点击换了选中,不会被属性拉回去)。隐藏的元素也生效。 */
233
+ private applySelection() {
234
+ const selection = this.getAttribute("selected-id");
235
+ const store = this.store;
236
+ if (selection !== null && (selection !== this.appliedSelection || store !== this.selectionStore)) {
237
+ this.appliedSelection = selection;
238
+ this.selectionStore = store;
239
+ store.select(selection || null);
240
+ }
241
+ }
242
+ protected common() {
243
+ const store = this.store;
244
+ return {
245
+ store,
246
+ title: this.getAttribute("title") ?? undefined,
247
+ color: this.getAttribute("color") ?? undefined,
248
+ theme: (this.getAttribute("theme") as "light" | "dark" | null) ?? undefined,
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
+ },
255
+ onTurnEnd: (payload: unknown) => this.dispatchEvent(new CustomEvent("cli-turn-end", { detail: payload, bubbles: true })),
256
+ onConversationEnd: (payload: unknown) => this.dispatchEvent(new CustomEvent("cli-conversation-end", { detail: payload, bubbles: true })),
257
+ onCallback: (payload: unknown) => this.dispatchEvent(new CustomEvent("cli-callback", { detail: payload, bubbles: true })),
258
+ style: { height: "100%" },
259
+ config: this.config as never,
260
+ };
261
+ }
262
+ protected abstract element(): React.ReactNode;
263
+ // 隐藏的元素什么都不渲染:不轮询、不占连接;workspace() / appendContext() 照样能用。
264
+ private render() {
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
+ }
273
+ }
274
+ }
275
+
276
+ class CliMsgListElement extends CliElement {
277
+ protected element() {
278
+ const context = this.getAttribute("context");
279
+ // 结果事件只由 <cli-chat> 发:同一个 group 里列表和对话共用一个仓库,两边都发的话 document 上会收到两遍。
280
+ const { onTurnEnd: _a, onConversationEnd: _b, onCallback: _c, ...common } = this.common();
281
+ return <CliMsgList {...common} {...(context !== null ? { context } : {})} />;
282
+ }
283
+ }
284
+
285
+ class CliChatElement extends CliElement {
286
+ protected element() {
287
+ const jobId = this.getAttribute("job-id");
288
+ const taskId = this.getAttribute("task-id");
289
+ const fileOpen = this.getAttribute("file-open") as "auto" | "board" | "modal" | "link" | null;
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 } : {})} />;
291
+ }
292
+ }
293
+
294
+ class CliBoardElement extends CliElement {
295
+ protected element() {
296
+ const jobId = this.getAttribute("job-id");
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;
299
+ const defaultTab = (this.getAttribute("default-tab") as BoardTab | null) ?? undefined;
300
+ // 结果事件只由 <cli-chat> 发(同上);看板只发 cli-event。
301
+ const { onTurnEnd: _a, onConversationEnd: _b, onCallback: _c, title: _t, color: _k, config: _g, ...common } = this.common();
302
+ const taskId = this.getAttribute("task-id");
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")}
304
+ // 同一个 group 里有对话:看板里「引用到对话」带进那个对话的下一条消息;no-quote 关掉。
305
+ watchFiles={this.getAttribute("watch-files")!=="false"}
306
+ quoteToChat={this.hasAttribute("group") && !this.hasAttribute("no-quote")} />;
307
+ }
308
+ }
309
+
310
+ class CliFilePreviewElement extends CliElement {
311
+ protected element() {
312
+ const jobId = this.getAttribute("job-id");
313
+ const { onTurnEnd: _a, onConversationEnd: _b, onCallback: _c, title: _t, color: _k, config: _g, ...common } = this.common();
314
+ return <CliFilePreview {...common} {...(jobId !== null ? { jobId: jobId || null } : {})} path={this.getAttribute("path") ?? ""} />;
315
+ }
316
+ }
317
+
318
+ class CliBrowserElement extends CliElement {
319
+ protected element() {
320
+ const jobId = this.getAttribute("job-id");
321
+ const { onTurnEnd: _a, onConversationEnd: _b, onCallback: _c, title: _t, color: _k, config: _g, ...common } = this.common();
322
+ return <CliBrowserView {...common} {...(jobId !== null ? { jobId: jobId || null } : {})} readOnly={this.hasAttribute("read-only")} />;
323
+ }
324
+ }
325
+
326
+ /** <cli-split>:可拖动分栏(规则见 layout/split.ts)。属性 panes(JSON,每栏 size / min / max)、storage-key(记住宽度)。 */
327
+ class CliSplitElement extends Base {
328
+ static observedAttributes = ["panes", "storage-key"];
329
+ private handle: SplitHandle | null = null;
330
+ private options() {
331
+ let panes: unknown;
332
+ try { panes = JSON.parse(this.getAttribute("panes") ?? "null"); } catch { panes = null; }
333
+ return {
334
+ ...(Array.isArray(panes) ? { panes } : {}),
335
+ storageKey: this.getAttribute("storage-key") ?? undefined,
336
+ onResize: (sizes: (number | null)[]) => this.dispatchEvent(new CustomEvent("cli-resize", { detail: { sizes } })),
337
+ };
338
+ }
339
+ connectedCallback() {
340
+ this.style.height ||= "100%";
341
+ this.handle ??= attachSplit(this, this.options());
342
+ }
343
+ disconnectedCallback() {
344
+ queueMicrotask(() => { if (!this.isConnected) { this.handle?.destroy(); this.handle = null; } });
345
+ }
346
+ attributeChangedCallback() { this.handle?.update(this.options()); }
347
+ }
348
+
349
+ export function defineCliElements() {
350
+ if (typeof customElements === "undefined") return;
351
+ if (!customElements.get("cli-msglist")) customElements.define("cli-msglist", CliMsgListElement);
352
+ if (!customElements.get("cli-chat")) customElements.define("cli-chat", CliChatElement);
353
+ if (!customElements.get("cli-board")) customElements.define("cli-board", CliBoardElement);
354
+ if (!customElements.get("cli-file-preview")) customElements.define("cli-file-preview", CliFilePreviewElement);
355
+ if (!customElements.get("cli-browser")) customElements.define("cli-browser", CliBrowserElement);
356
+ if (!customElements.get("cli-split")) customElements.define("cli-split", CliSplitElement);
357
+ }
358
+
359
+ /** 元素上给业务方用的方法(<cli-msglist> / <cli-chat> / <cli-board> / <cli-file-preview> / <cli-browser> 都有)。 */
360
+ export interface CliElementApi extends HTMLElement {
361
+ readonly store: CliStore;
362
+ config: object | undefined;
363
+ workspace(jobId?: string): CliWorkspace;
364
+ appendContext(text: string, opts?: { title?: string; id?: string; jobId?: string }): string;
365
+ removeContext(id?: string, jobId?: string): void;
366
+ }
367
+
368
+ declare global {
369
+ interface HTMLElementTagNameMap {
370
+ "cli-msglist": CliElementApi;
371
+ "cli-chat": CliElementApi;
372
+ "cli-board": CliElementApi;
373
+ "cli-file-preview": CliElementApi;
374
+ "cli-browser": CliElementApi;
375
+ }
376
+ }
package/src/embed.ts ADDED
@@ -0,0 +1,56 @@
1
+ /**
2
+ * 给第三方页面用的一体化入口:引入一个 JS 和一个 CSS 就能用,不需要 React、打包工具或 TypeScript。
3
+ *
4
+ * <link rel="stylesheet" href="/static/cli-ui/cli-ui.css">
5
+ * <script type="module" src="/static/cli-ui/cli-ui.js"></script>
6
+ * <cli-chat base-url="/api/center" job-id="job_xxx"></cli-chat>
7
+ *
8
+ * 引入后自动注册 <cli-chat> / <cli-msglist> / <cli-board>;想用 JS 控制(传回调函数、随时改参数)就用
9
+ * window.CliUI.mountChat / mountMsgList / mountBoard。几个 mount 要联动(对话里点文件在看板打开)时传同一个 store:
10
+ * const store = CliUI.createCliStore({ baseUrl: "/api/center" });
11
+ * CliUI.mountChat(a, { store }); CliUI.mountBoard(b, { store });
12
+ * 产物由 `npm run build:embed -w @xgspz/cli-ui` 生成在 dist/。
13
+ */
14
+ import "./styles.css";
15
+ import { createElement } from "react";
16
+ import { createRoot } from "react-dom/client";
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";
23
+
24
+ export interface Mounted<P> {
25
+ /** 合并更新参数(只传要改的)。 */
26
+ update(next: Partial<P>): void;
27
+ unmount(): void;
28
+ }
29
+
30
+ function mount<P extends object>(Component: (props: P) => unknown, el: Element, props: P): Mounted<P> {
31
+ const root = createRoot(el);
32
+ let current = props;
33
+ const render = () => root.render(createElement(Component as never, { style: { height: "100%" }, ...current }));
34
+ render();
35
+ return {
36
+ update(next) { current = { ...current, ...next }; render(); },
37
+ unmount() { root.unmount(); },
38
+ };
39
+ }
40
+
41
+ /** 在一个元素里挂对话组件。参数与 React 的 <CliChat> 相同(jobId、newConversation、onTurnEnd、config……)。 */
42
+ export const mountChat = (el: Element, props: CliChatProps = {}) => mount(CliChat, el, props);
43
+ /** 在一个元素里挂列表组件。参数与 React 的 <CliMsgList> 相同。 */
44
+ export const mountMsgList = (el: Element, props: CliMsgListProps = {}) => mount(CliMsgList, el, props);
45
+ /** 在一个元素里挂右侧看板。参数与 React 的 <CliBoard> 相同(jobId、tabs、fileActions……)。 */
46
+ export const mountBoard = (el: Element, props: CliBoardProps = {}) => mount(CliBoard, el, props);
47
+ /** 单独的文件预览 + 下载:{ jobId, path, baseUrl… }。 */
48
+ export const mountFilePreview = (el: Element, props: CliFilePreviewProps) => mount(CliFilePreview, el, props);
49
+ /** 单独的浏览器实时画面:{ jobId, readOnly, baseUrl… }。 */
50
+ export const mountBrowser = (el: Element, props: CliBrowserViewProps = {}) => mount(CliBrowserView, el, props);
51
+
52
+ export { defineCliElements, CliChat, CliMsgList, CliBoard, CliFilePreview, CliBrowserView, CliCenterProvider, createCliStore, createCenterClient };
53
+
54
+ defineCliElements();
55
+ const api = { mountChat, mountMsgList, mountBoard, mountFilePreview, mountBrowser, createCliStore, createCenterClient, defineCliElements };
56
+ (globalThis as { CliUI?: typeof api }).CliUI = api;
package/src/index.ts ADDED
@@ -0,0 +1,56 @@
1
+ export { CliMsgList, CliChat, CliBoard, type CliMsgListProps, type CliChatProps, type CliBoardProps } from "./components";
2
+ export type { GroupChatAdapter, GroupChatMessage, GroupChatReference } from "./chat/GroupChat";
3
+ /**
4
+ * @xgspz/cli-ui —— 任务中心(cli-task-center)对话任务的前端积木,数据全部来自 center 服务。
5
+ *
6
+ * cli-msglist 消息对话列表(CliMsgList)
7
+ * cli-chat 对话(CliChat)
8
+ * cli-board 右侧看板:工作区文件(预览 / 下载)、改动、任务浏览器(CliBoard)
9
+ * 其中两个核心也能单独用:CliFilePreview(文件预览 + 下载)、CliBrowserView(浏览器实时画面)
10
+ * CliSplit 可拖动分栏:把上面几个横着排,栏之间左右拖动改宽度(Web Component:<cli-split>)
11
+ *
12
+ * 单独用:
13
+ * <CliChat baseUrl="/api/center" jobId="job_xxx" />
14
+ * <CliChat baseUrl="/api/center" jobId="job_xxx" taskId="task_yyy" /> (对话任务里的某个对话;taskId="new" 新开一个)
15
+ * 组合用(同一个 Provider 里共享选中和实时流):
16
+ * <CliCenterProvider baseUrl="/api/center" namespace="strategy">
17
+ * <CliMsgList title="战略" />
18
+ * <CliChat />
19
+ * </CliCenterProvider>
20
+ *
21
+ * import "@xgspz/cli-ui/styles.css";
22
+ * 非 React 页面用 Web Component:import { defineCliElements } from "@xgspz/cli-ui/elements"。
23
+ */
24
+ export { type BoardTab, type BoardExtraTab } from "./board/CliBoard";
25
+ export { downloadFolder, zipFolder, listFolder, FOLDER_MAX_FILES, FOLDER_MAX_BYTES, type FolderResult, type FolderZip, type FolderProgress } from "./board/folderDownload";
26
+ export { createWorkspace, WORKSPACE_TEXT_MAX, WORKSPACE_LIST_MAX, WORKSPACE_FILE_MAX, type CliWorkspace, type BoardView } from "./board/workspace";
27
+ export { CliFilePreview, CliBrowserView, type CliFilePreviewProps, type CliBrowserViewProps } from "./board/blocks";
28
+ export { FileViewer, downloadFile, type FileViewerProps, type FileAction } from "./board/FileViewer";
29
+ export { BrowserPane } from "./board/BrowserPane";
30
+ export { CliSplit, type CliSplitProps } from "./layout/CliSplit";
31
+ export { attachSplit, type SplitPane, type SplitOptions, type SplitHandle } from "./layout/split";
32
+ export { CliCenterProvider, useCliStore, useCliState, type CliCenterProviderProps, type ResultHandlers } from "./core/provider";
33
+
34
+ export {
35
+ resolveChatConfig, resolveMsgListConfig, DEFAULT_CHAT_CONFIG, DEFAULT_MSGLIST_CONFIG,
36
+ type CliChatConfig, type CliMsgListConfig, type ResolvedChatConfig, type ResolvedMsgListConfig,
37
+ } from "./core/config";
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";
39
+ export { createCenterClient, CenterError, boardFileUrl, type CenterClient, type CenterClientOptions, type StreamHandlers, type BoardBackend, type ConversationBackend } from "./core/client";
40
+ export { viewMessages, isRunning, runningTurn, convKey, parseConvKey, NEW_TASK, type ConvState } from "./core/conversation";
41
+ export {
42
+ composeMessage, splitMessage, serializeBlock, readFileText, isLongText, ACCEPTED_EXTENSIONS, MAX_MESSAGE_CHARS,
43
+ type TextBlock, type MessagePart,
44
+ } from "./core/blocks";
45
+ export type * from "./core/types";
46
+ export type { ExecutionOptions, ExecutionConfiguration } from "./core/execution";
47
+
48
+
49
+ export { RunControls, type RunControlProps, type RunCapabilities, type RunTask } from "./chat/RunControls";
50
+
51
+ export { formatBoardContext, appendBoardContext, BOARD_HISTORY_MAX } from "./board/context";
52
+ export { subscribeWorkspaceFiles, type WorkspaceWatchOptions } from "./board/fileWatch";
53
+ export type { WorkspaceHandlers } from "./core/provider";
54
+ export type { BoardFile } from "./board/workspace";
55
+
56
+ export { syncFile, clearFileSyncNamespace, FILE_SYNC_MAX, type FileSyncTransport, type FileSyncManifest, type FileSyncProgress } from "./board/fileSync";