cc-viewer 1.6.348 → 1.7.1

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 (222) hide show
  1. package/README.md +13 -1
  2. package/cli.js +116 -37
  3. package/dist/assets/App-gUSmU6Ny.js +2 -0
  4. package/dist/assets/{MdxEditorPanel-ualoRFOL.js → MdxEditorPanel-DAR39A3S.js} +1 -1
  5. package/dist/assets/Mobile-qfRsRth0.js +1 -0
  6. package/dist/assets/{_baseUniq-CG81mgN_.js → _baseUniq-D9_E5ZDp.js} +1 -1
  7. package/dist/assets/{arc-C161J4f2.js → arc-D4iFrlhb.js} +1 -1
  8. package/dist/assets/{architectureDiagram-Q4EWVU46-BmbDMdn9.js → architectureDiagram-Q4EWVU46-f1neo7hg.js} +1 -1
  9. package/dist/assets/{blockDiagram-DXYQGD6D-DXpMvdxd.js → blockDiagram-DXYQGD6D-DWi9e9tG.js} +1 -1
  10. package/dist/assets/{c4Diagram-AHTNJAMY-8Mypg0IU.js → c4Diagram-AHTNJAMY-DAdj49A2.js} +1 -1
  11. package/dist/assets/{channel-B2khdmvc.js → channel-B3VthT0m.js} +1 -1
  12. package/dist/assets/{chunk-4BX2VUAB-I61r1g6z.js → chunk-4BX2VUAB-BrPcAHV3.js} +1 -1
  13. package/dist/assets/{chunk-4TB4RGXK-VFPlRE5E.js → chunk-4TB4RGXK-Bvopc1V9.js} +1 -1
  14. package/dist/assets/{chunk-55IACEB6-DvCamg_6.js → chunk-55IACEB6-Juw-7Z3i.js} +1 -1
  15. package/dist/assets/{chunk-EDXVE4YY-Dj4Q8RwR.js → chunk-EDXVE4YY-DVYP9tpB.js} +1 -1
  16. package/dist/assets/{chunk-FMBD7UC4-_bgQuuJh.js → chunk-FMBD7UC4-VQ7VOXl3.js} +1 -1
  17. package/dist/assets/{chunk-OYMX7WX6-3to38LyN.js → chunk-OYMX7WX6-F8Rj4ZUZ.js} +1 -1
  18. package/dist/assets/{chunk-QZHKN3VN-Zl26wKKz.js → chunk-QZHKN3VN-Dn7K3yi9.js} +1 -1
  19. package/dist/assets/{chunk-YZCP3GAM-RLYPrlh0.js → chunk-YZCP3GAM-UjO-KTsn.js} +1 -1
  20. package/dist/assets/classDiagram-6PBFFD2Q-DmSozxoO.js +1 -0
  21. package/dist/assets/classDiagram-v2-HSJHXN6E-DmSozxoO.js +1 -0
  22. package/dist/assets/clone-BQtvy9Ae.js +1 -0
  23. package/dist/assets/{cose-bilkent-S5V4N54A-CsYOjJ0V.js → cose-bilkent-S5V4N54A-BDhNj8rq.js} +1 -1
  24. package/dist/assets/{dagre-KV5264BT-C3YXXaFU.js → dagre-KV5264BT-BzdcDuyt.js} +1 -1
  25. package/dist/assets/{diagram-5BDNPKRD-DUCOg1ld.js → diagram-5BDNPKRD-DP_SX65N.js} +1 -1
  26. package/dist/assets/{diagram-G4DWMVQ6-BtlLl_zX.js → diagram-G4DWMVQ6-B-pBFw8Q.js} +1 -1
  27. package/dist/assets/{diagram-MMDJMWI5-1Qp9xfOk.js → diagram-MMDJMWI5-6XSfN1eN.js} +1 -1
  28. package/dist/assets/{diagram-TYMM5635-Cj7VMp3L.js → diagram-TYMM5635-DJvGxvpE.js} +1 -1
  29. package/dist/assets/{erDiagram-SMLLAGMA-CVZVn541.js → erDiagram-SMLLAGMA-CpqdpGYj.js} +1 -1
  30. package/dist/assets/{flowDiagram-DWJPFMVM-CiU9Fkkv.js → flowDiagram-DWJPFMVM-C9m7wjwI.js} +1 -1
  31. package/dist/assets/{ganttDiagram-T4ZO3ILL-aRaG97ID.js → ganttDiagram-T4ZO3ILL-CU0XOfY3.js} +1 -1
  32. package/dist/assets/{gitGraphDiagram-UUTBAWPF-Cr2LJlvX.js → gitGraphDiagram-UUTBAWPF-ipyGULnP.js} +1 -1
  33. package/dist/assets/{graph-B59lDyMn.js → graph-BR2KmES9.js} +1 -1
  34. package/dist/assets/{index-C1ptnCpq.js → index--wgRW_OB.js} +1 -1
  35. package/dist/assets/index-B51Ci0GL.js +2 -0
  36. package/dist/assets/{index-CRAWA8PC.js → index-C-be29ey.js} +1 -1
  37. package/dist/assets/{index-CSM1Kw8l.js → index-C16fiVhv.js} +1 -1
  38. package/dist/assets/{index-Ddk-kg22.js → index-D5YruOKD.js} +1 -1
  39. package/dist/assets/{index-BnhNIjbj.js → index-D5tSeMU7.js} +1 -1
  40. package/dist/assets/{index-vFEQq-NB.js → index-DuZQ6FqR.js} +1 -1
  41. package/dist/assets/{index-CO2hZ1s4.js → index-OWzSX2T8.js} +1 -1
  42. package/dist/assets/{infoDiagram-42DDH7IO-CnO22bw8.js → infoDiagram-42DDH7IO-BQWQjfFX.js} +1 -1
  43. package/dist/assets/{ishikawaDiagram-UXIWVN3A-ByNzpScH.js → ishikawaDiagram-UXIWVN3A-Df-KH_Hz.js} +1 -1
  44. package/dist/assets/{journeyDiagram-VCZTEJTY-yaVaCWPq.js → journeyDiagram-VCZTEJTY-CMbB_4G9.js} +1 -1
  45. package/dist/assets/{jszip.min-BdVFP3OW.js → jszip.min-B6k2t9Ch.js} +1 -1
  46. package/dist/assets/{kanban-definition-6JOO6SKY-RIPR_RQ3.js → kanban-definition-6JOO6SKY-CImdlKN3.js} +1 -1
  47. package/dist/assets/{layout-0rzel0v_.js → layout-DQl4aw09.js} +1 -1
  48. package/dist/assets/{linear-sktN0tB6.js → linear-CLD3aPlK.js} +1 -1
  49. package/dist/assets/mermaid.core-CN35dvKJ.js +7 -0
  50. package/dist/assets/{min-rspGOkz_.js → min-_D86henJ.js} +1 -1
  51. package/dist/assets/{mindmap-definition-QFDTVHPH-BsVzZzTz.js → mindmap-definition-QFDTVHPH-2JVcu1nk.js} +1 -1
  52. package/dist/assets/{pieDiagram-DEJITSTG-CsoWsw4f.js → pieDiagram-DEJITSTG-Dik-m5u7.js} +1 -1
  53. package/dist/assets/{quadrantDiagram-34T5L4WZ-CTMKuCAI.js → quadrantDiagram-34T5L4WZ-Bm5l40Jq.js} +1 -1
  54. package/dist/assets/{requirementDiagram-MS252O5E-C67256te.js → requirementDiagram-MS252O5E-Ck9w5rTj.js} +1 -1
  55. package/dist/assets/{sankeyDiagram-XADWPNL6-X8Y7KnW0.js → sankeyDiagram-XADWPNL6-BbGVCFEf.js} +1 -1
  56. package/dist/assets/{seqResourceLoaders-B5Zy_CC_.css → seqResourceLoaders-DFrHmgnK.css} +1 -1
  57. package/dist/assets/seqResourceLoaders-DZJxMMaR.js +2 -0
  58. package/dist/assets/{sequenceDiagram-FGHM5R23-Cils0TAp.js → sequenceDiagram-FGHM5R23-vjvEdksk.js} +1 -1
  59. package/dist/assets/{stateDiagram-FHFEXIEX-DFnohvpG.js → stateDiagram-FHFEXIEX-DTqMoiB2.js} +1 -1
  60. package/dist/assets/{stateDiagram-v2-QKLJ7IA2-BTX_4o81.js → stateDiagram-v2-QKLJ7IA2-CNbAwnhu.js} +1 -1
  61. package/dist/assets/{timeline-definition-GMOUNBTQ-DrIYFrzZ.js → timeline-definition-GMOUNBTQ-Cw8O-52d.js} +1 -1
  62. package/dist/assets/{vendor-antd-D5HmSDSg.js → vendor-antd-DI7JL-mE.js} +1 -1
  63. package/dist/assets/{vendor-codemirror-C5yuj7ze.js → vendor-codemirror-B9c49dtM.js} +1 -1
  64. package/dist/assets/{vendor-mdxeditor-BcEooTvb.js → vendor-mdxeditor-B6cpBtIE.js} +2 -2
  65. package/dist/assets/{vendor-qrcode-d9LCn0-9.js → vendor-qrcode-Dn90-s-u.js} +1 -1
  66. package/dist/assets/{vendor-virtuoso-K9m4MN6a.js → vendor-virtuoso-CpdSKD_6.js} +1 -1
  67. package/dist/assets/{vennDiagram-DHZGUBPP-lRqBYTr9.js → vennDiagram-DHZGUBPP-DCuqymb7.js} +1 -1
  68. package/dist/assets/{wardley-RL74JXVD-e8YfxpOe.js → wardley-RL74JXVD-BJ20wJUU.js} +1 -1
  69. package/dist/assets/{wardleyDiagram-NUSXRM2D-Bxrg774h.js → wardleyDiagram-NUSXRM2D-Vz6US3qt.js} +1 -1
  70. package/dist/assets/{xychartDiagram-5P7HB3ND-DWhZqhq8.js → xychartDiagram-5P7HB3ND-CzkLKrmi.js} +1 -1
  71. package/dist/index.html +4 -4
  72. package/findcc.js +4 -3
  73. package/package.json +3 -1
  74. package/server/i18n.js +21 -141
  75. package/server/interceptor.js +211 -403
  76. package/server/lib/async-write-queue.js +12 -1
  77. package/server/lib/delta-reconstructor.js +3 -3
  78. package/server/lib/error-report.js +45 -0
  79. package/server/lib/im-log-watcher.js +68 -8
  80. package/server/lib/interceptor-core.js +19 -174
  81. package/server/lib/log-management.js +214 -247
  82. package/server/lib/log-stream.js +59 -108
  83. package/server/lib/log-watcher.js +62 -274
  84. package/server/lib/log-zip.js +148 -0
  85. package/server/lib/session-boundary.js +266 -0
  86. package/server/lib/session-pin-store.js +10 -19
  87. package/server/lib/stats-worker.js +138 -267
  88. package/server/lib/user-prompt-extract.js +250 -0
  89. package/server/lib/v2/adapter.js +1124 -0
  90. package/server/lib/v2/blob-store.js +44 -0
  91. package/server/lib/v2/conversation-store.js +217 -0
  92. package/server/lib/v2/convert-manager.js +79 -0
  93. package/server/lib/v2/convert-worker.js +42 -0
  94. package/server/lib/v2/convert.js +409 -0
  95. package/server/lib/v2/identity.js +204 -0
  96. package/server/lib/v2/journal.js +62 -0
  97. package/server/lib/v2/layout.js +167 -0
  98. package/server/lib/v2/live-feed.js +558 -0
  99. package/server/lib/v2/meta-rows.js +360 -0
  100. package/server/lib/v2/migrate-prompt.js +54 -0
  101. package/server/lib/v2/replay.js +191 -0
  102. package/server/lib/v2/session-select.js +222 -0
  103. package/server/lib/v2/singleflight.js +104 -0
  104. package/server/lib/v2/v2-writer.js +528 -0
  105. package/server/lib/v2/verify.js +233 -0
  106. package/server/lib/wire-compress.js +167 -0
  107. package/server/routes/events.js +120 -150
  108. package/server/routes/files-fs.js +4 -2
  109. package/server/routes/im.js +11 -2
  110. package/server/routes/logs.js +209 -164
  111. package/server/routes/preferences.js +0 -3
  112. package/server/routes/project-meta.js +2 -3
  113. package/server/routes/session-pin.js +7 -8
  114. package/server/routes/v2.js +60 -0
  115. package/server/routes/workspaces.js +50 -14
  116. package/server/server.js +70 -66
  117. package/server/workspace-registry.js +24 -4
  118. package/src/utils/apiUrl.js +26 -0
  119. package/src/utils/askFallback.js +21 -0
  120. package/src/utils/askOptionDesc.js +55 -0
  121. package/src/utils/askPortalMatcher.js +54 -0
  122. package/src/utils/autoApproveOptions.js +27 -0
  123. package/src/utils/avatarAnimationPostPass.js +49 -0
  124. package/src/utils/builtinPresets.js +18 -0
  125. package/src/utils/clearCheckpoint.js +7 -0
  126. package/src/utils/commandValidator.js +45 -0
  127. package/src/utils/contentFilter.js +620 -0
  128. package/src/utils/contextRaw.js +18 -0
  129. package/src/utils/contextSidebarNavigation.js +16 -0
  130. package/src/utils/contextTurns.js +117 -0
  131. package/src/utils/displayScaleHelper.js +47 -0
  132. package/src/utils/effectiveModel.js +13 -0
  133. package/src/utils/entry-slim.js +563 -0
  134. package/src/utils/entryCache.js +144 -0
  135. package/src/utils/errorReport.js +6 -0
  136. package/src/utils/fileExpandedPathsStorage.js +70 -0
  137. package/src/utils/fileIcons.jsx +36 -0
  138. package/src/utils/fileOpen.js +37 -0
  139. package/src/utils/formatters.js +48 -0
  140. package/src/utils/gitApi.js +50 -0
  141. package/src/utils/gitTreeBuilder.js +19 -0
  142. package/src/utils/helpers.js +846 -0
  143. package/src/utils/identityHeal.js +99 -0
  144. package/src/utils/imConnState.js +59 -0
  145. package/src/utils/imOrigin.js +24 -0
  146. package/src/utils/imTr.js +7 -0
  147. package/src/utils/imageCompress.js +83 -0
  148. package/src/utils/imageDownscale.js +28 -0
  149. package/src/utils/imageResize.js +112 -0
  150. package/src/utils/ingestPipeline.js +47 -0
  151. package/src/utils/lazyWithReload.js +53 -0
  152. package/src/utils/markdown.js +30 -0
  153. package/src/utils/markdownIncremental.js +39 -0
  154. package/src/utils/markdownProfiler.js +113 -0
  155. package/src/utils/mdExtensionDetect.js +64 -0
  156. package/src/utils/memoryLinkParser.js +44 -0
  157. package/src/utils/modalMask.js +15 -0
  158. package/src/utils/monotime.js +6 -0
  159. package/src/utils/pinnedMenu.js +55 -0
  160. package/src/utils/presetShortcuts.js +14 -0
  161. package/src/utils/projectAlias.js +135 -0
  162. package/src/utils/promptClassifier.js +112 -0
  163. package/src/utils/promptDetect.js +196 -0
  164. package/src/utils/promptNav.js +60 -0
  165. package/src/utils/ptyChunkBuilder.js +251 -0
  166. package/src/utils/quickMenuHoverIntent.js +35 -0
  167. package/src/utils/rateLimitParser.js +119 -0
  168. package/src/utils/readResultPool.js +106 -0
  169. package/src/utils/refreshCachedItemProp.js +30 -0
  170. package/src/utils/requestType.js +208 -0
  171. package/src/utils/resizeCalc.js +21 -0
  172. package/src/utils/resolveLocalized.js +17 -0
  173. package/src/utils/searchApi.js +47 -0
  174. package/src/utils/searchReplace.js +51 -0
  175. package/src/utils/seqResourceLoaders.js +79 -0
  176. package/src/utils/sessionManager.js +471 -0
  177. package/src/utils/sessionMerge.js +180 -0
  178. package/src/utils/skillModalController.js +111 -0
  179. package/src/utils/skillsParser.js +116 -0
  180. package/src/utils/slashCommandLabels.js +80 -0
  181. package/src/utils/splitDragCalc.js +73 -0
  182. package/src/utils/stickyBottomController.js +643 -0
  183. package/src/utils/svgSanitize.js +48 -0
  184. package/src/utils/systemTags.js +47 -0
  185. package/src/utils/tClaude.js +20 -0
  186. package/src/utils/teamModalBuilder.js +351 -0
  187. package/src/utils/teamSessionParser.js +190 -0
  188. package/src/utils/teammateAvatars.js +194 -0
  189. package/src/utils/teammateDetector.js +121 -0
  190. package/src/utils/termDiag.js +114 -0
  191. package/src/utils/terminalClipboard.js +117 -0
  192. package/src/utils/terminalWriteQueue.js +398 -0
  193. package/src/utils/thinkingMerge.js +49 -0
  194. package/src/utils/toolCatalog.js +29 -0
  195. package/src/utils/toolResultBuilder.js +393 -0
  196. package/src/utils/toolResultClassifier.js +16 -0
  197. package/src/utils/toolResultCore.js +167 -0
  198. package/src/utils/toolsDiff.js +44 -0
  199. package/src/utils/toolsXmlFormatter.js +3 -0
  200. package/src/utils/ultraplanController.js +109 -0
  201. package/src/utils/ultraplanExperts.js +86 -0
  202. package/src/utils/ultraplanTemplates.js +154 -0
  203. package/src/utils/userImageRefs.js +49 -0
  204. package/src/utils/v3Assembler.js +112 -0
  205. package/src/utils/v3Rows.js +48 -0
  206. package/src/utils/voicePackPlayer.js +233 -0
  207. package/src/utils/webSearchGrouping.js +111 -0
  208. package/src/utils/workflowFormat.js +52 -0
  209. package/src/utils/workflowRuns.js +118 -0
  210. package/src/utils/workflowStore.js +94 -0
  211. package/dist/assets/App-VTo4NVdo.js +0 -2
  212. package/dist/assets/Mobile-CcmYvMm_.js +0 -1
  213. package/dist/assets/classDiagram-6PBFFD2Q-BXnyUgsj.js +0 -1
  214. package/dist/assets/classDiagram-v2-HSJHXN6E-BXnyUgsj.js +0 -1
  215. package/dist/assets/clone-md-Mc1-T.js +0 -1
  216. package/dist/assets/index-DCo650PG.js +0 -2
  217. package/dist/assets/mermaid.core-C2Parj1q.js +0 -7
  218. package/dist/assets/seqResourceLoaders-BVozq68i.js +0 -2
  219. package/server/lib/instance-registry.js +0 -57
  220. package/server/lib/jsonl-archive.js +0 -180
  221. package/server/lib/teammate-detect.js +0 -32
  222. package/server/lib/zip-safety.js +0 -68
@@ -0,0 +1,106 @@
1
+ /**
2
+ * Read tool_result content intern pool.
3
+ *
4
+ * 1.6.237 后实测:同一 .jsx 文件被 87 个 SubAgent / 父 user message 各持一份完整副本(30MB+ 重复)。
5
+ * 对 Read 工具的 resultText 做 content-addressed dedup,让相同内容共享同一字符串引用。
6
+ * hash 仅用 length + 前 64 / 后 64 字符避免大字符串 O(n) hash;当 length > 512
7
+ * 再加采中段 64 字符(共三段 192 字节),令仅中段不同的长串也能区分;碰撞概率极低。
8
+ *
9
+ * 抽到独立模块(无外部依赖)便于 node --test 直接 import,避免被
10
+ * toolResultBuilder.js 的传递依赖(./helpers 无 .js 后缀)污染。
11
+ */
12
+
13
+ // 容量上限设计:1000 vs entry-slim.js 的 _toolsPool/_systemPool 上限 200,差异源自流量域不同。
14
+ // tools/system 是 entry-level 配置数组,全 session 命中分布稳定,实测 pool size <5(200 极宽松);
15
+ // readResultPool 池化的是 tool_result 字符串(v4 派生层 + v5 raw payload),
16
+ // 单 session 唯一字符串数量与 tool 调用次数同阶(v4 实测 4400 calls / pool size 363)。
17
+ // B 项加入 raw payload 后预期再 +20-40%,1000 留 ~30% 余量;超阈值由 _poolEvictions 暴露。
18
+ const _MAX_READ_POOL_SIZE = 1000;
19
+ const _MIN_DEDUP_LEN = 256;
20
+ const _SIG_MID_THRESHOLD = 512;
21
+ const _readResultPool = new Map();
22
+ let _poolEvictions = 0;
23
+
24
+ // v5: sig 增加 mid-slice(当 length > 512)。对齐 entry-slim.js _systemSig 的边界增强模式。
25
+ // B 项把 raw payload tool_result block.content 也接入此 pool(含 Bash tab-padded / log 类
26
+ // 长度+前后缀重合的结构化输出),暴露面变大。length+first 64+last 64 在该场景对碰撞抗性
27
+ // 不足,加 mid-64 后要求"长度+三段共 192 字节"全匹配才视为同串。
28
+ function _readResultSig(s) {
29
+ if (s.length > _SIG_MID_THRESHOLD) {
30
+ const midIdx = Math.floor(s.length / 2);
31
+ return s.length + ':' + s.slice(0, 64) + ':' + s.slice(midIdx, midIdx + 64) + ':' + s.slice(-64);
32
+ }
33
+ return s.length + ':' + s.slice(0, 64) + ':' + s.slice(-64);
34
+ }
35
+
36
+ /**
37
+ * 把 Read 工具的 resultText 替换为 pool 中的共享引用(命中时)或注册新值。
38
+ * 短字符串(< 256)跳过 dedup(不值得 sig 开销)。
39
+ *
40
+ * @param {string} s - resultText 原文
41
+ * @returns {string} 池化后的字符串引用
42
+ */
43
+ export function internReadResult(s) {
44
+ if (typeof s !== 'string' || s.length < _MIN_DEDUP_LEN) return s;
45
+ const sig = _readResultSig(s);
46
+ const pooled = _readResultPool.get(sig);
47
+ if (pooled !== undefined) return pooled;
48
+ if (_readResultPool.size >= _MAX_READ_POOL_SIZE) {
49
+ _readResultPool.delete(_readResultPool.keys().next().value);
50
+ _poolEvictions++;
51
+ }
52
+ _readResultPool.set(sig, s);
53
+ return s;
54
+ }
55
+
56
+ /** 测试辅助:清空 Read intern pool 和 eviction counter。 */
57
+ export function _resetReadPoolForTest() {
58
+ _readResultPool.clear();
59
+ _poolEvictions = 0;
60
+ }
61
+
62
+ /** 测试辅助:观察 pool 当前 size。 */
63
+ export function _getReadPoolSizeForTest() {
64
+ return _readResultPool.size;
65
+ }
66
+
67
+ /** 诊断:FIFO 淘汰累计次数。典型 session <50;>200 提示 _MAX_READ_POOL_SIZE 需上调。 */
68
+ export function _getReadPoolEvictionsForTest() {
69
+ return _poolEvictions;
70
+ }
71
+
72
+ /**
73
+ * 通用版:Read 之外的所有 tool_result(Bash/Grep/Glob/MCP/...)也走同一 pool。
74
+ * 算法与 internReadResult 完全相同(同 _readResultSig + 同 _readResultPool),
75
+ * 名字区分只是语义上的扩展,便于 toolResultBuilder.js 默认全 intern。
76
+ *
77
+ * @param {string} s - tool_result 的 resultText 原文
78
+ * @returns {string} 池化后的字符串引用
79
+ */
80
+ export function internToolResult(s) {
81
+ return internReadResult(s);
82
+ }
83
+
84
+ /**
85
+ * 命中-aware 变体:仅在 pool 已有相同 sig 时返回共享 ref,否则注册并返回 null。
86
+ *
87
+ * 设计动机:JS string `===` 是值比较,调用方拿到 internReadResult 的返回值无法判断"这是
88
+ * pool 命中(应替换 block.content 节省内存)"还是"刚注册(保持原 ref 即可)"。
89
+ * 此函数把命中信号显式上抛——`null` = 未命中或短结果(无需替换),非 null = 命中(应替换)。
90
+ * v5 entry-slim.js 的 lazy-clone 路径依赖这个信号决定是否触发 messages 浅克隆。
91
+ *
92
+ * @param {string} s - tool_result 内容原文
93
+ * @returns {string|null} 命中时返回 pool 共享 ref;未命中(已注册)或短结果(< 256)返回 null
94
+ */
95
+ export function internToolResultIfPooled(s) {
96
+ if (typeof s !== 'string' || s.length < _MIN_DEDUP_LEN) return null;
97
+ const sig = _readResultSig(s);
98
+ const pooled = _readResultPool.get(sig);
99
+ if (pooled !== undefined) return pooled;
100
+ if (_readResultPool.size >= _MAX_READ_POOL_SIZE) {
101
+ _readResultPool.delete(_readResultPool.keys().next().value);
102
+ _poolEvictions++;
103
+ }
104
+ _readResultPool.set(sig, s);
105
+ return null;
106
+ }
@@ -0,0 +1,30 @@
1
+ import React from 'react';
2
+
3
+ /**
4
+ * 刷新缓存的 assistant ChatMessage element 上随 ChatView `_sessionItemCache` FULL HIT 而冻结的
5
+ * 过期 map prop。仅对「持有指定 toolName 的 tool_use」的 element 做 cloneElement 注入 nextMap,
6
+ * 其余原样返回;prevMap===nextMap 时返回原数组(零分配)。
7
+ *
8
+ * 背景:FULL HIT 路径直接返回 `sc.items` 时 React reconciler 看到完全相同的 element 引用就跳过
9
+ * diff,ChatMessage SCU 根本不会被调用,元素创建时冻结的旧 map(planApprovalMap / askAnswerMap)
10
+ * 永不刷新——导致 ExitPlanMode 审批后卡片不切「已批准」、AskUserQuestion 答完后仍显示 pending。
11
+ * 本函数合并自原 refreshPlanApprovalCache / refreshAskAnswerCache 两个孪生模块。
12
+ *
13
+ * @param {Array} items - 缓存的 ChatMessage React element 数组
14
+ * @param {object} prevMap - 上一轮存入 cache 时的 map 引用
15
+ * @param {object} nextMap - 本轮派生的 map 引用
16
+ * @param {string} toolName - 触发刷新的 tool_use 名('ExitPlanMode' / 'AskUserQuestion')
17
+ * @param {string} propName - 要刷新的 prop 名('planApprovalMap' / 'askAnswerMap')
18
+ * @returns {Array} 引用全等时为 items;否则为新数组(仅命中者被 cloneElement)
19
+ */
20
+ export function refreshCachedItemProp(items, prevMap, nextMap, toolName, propName) {
21
+ if (prevMap === nextMap) return items;
22
+ let dirty = false;
23
+ const out = items.map(m => {
24
+ if (!m || !m.props || m.props.role !== 'assistant' || !Array.isArray(m.props.content)) return m;
25
+ if (!m.props.content.some(b => b.type === 'tool_use' && b.name === toolName)) return m;
26
+ dirty = true;
27
+ return React.cloneElement(m, { [propName]: nextMap });
28
+ });
29
+ return dirty ? out : items;
30
+ }
@@ -0,0 +1,208 @@
1
+ /**
2
+ * 请求类型分类工具
3
+ * classifyRequest(req, nextReq?) 返回 { type, subType }
4
+ * type: 'MainAgent' | 'SubAgent' | 'Teammate' | 'Count' | 'Preflight' | 'Plan' | 'Synthetic'
5
+ *
6
+ * Synthetic: Claude Code CLI 在主会话里合成的内部工具查询
7
+ * (idle 返回 recap / 会话标题生成 / 压缩摘要等),HTTP 层 role=user 但并非用户手输。
8
+ * subType: 'Recap' | 'Title' | 'Compact' | 'Topic' | 'Summary'
9
+ */
10
+ // SYNTHETIC_PROMPTS 已抬到 contentFilter.js,让 isSystemText 也能共用(ChatView 字符串分支过滤)。
11
+ // requestType.js 继续维护 getSyntheticSubType —— 走的是"last user message + isMainAgent 门槛"的更强
12
+ // 形式判断(用于 RequestList 打 Synthetic tag);isSystemText 走的是"纯文本起首匹配"(用于对话流隐藏)。
13
+ // Extensioned import: this module chain (requestType → contentFilter →
14
+ // teammateDetector) is shared with the server (wire-v3 typeTag computation in
15
+ // server/lib/v2/meta-rows.js) — Node ESM requires explicit extensions.
16
+ import { isMainAgent, isTeammate, getSystemText, extractTeammateName, SYNTHETIC_PROMPTS } from './contentFilter.js';
17
+
18
+ function getMessageText(msg) {
19
+ const c = msg?.content;
20
+ if (typeof c === 'string') return c;
21
+ if (Array.isArray(c)) {
22
+ for (const block of c) {
23
+ if (block?.type === 'text' && block.text) return block.text;
24
+ }
25
+ }
26
+ return '';
27
+ }
28
+
29
+ function getSubAgentSubType(req) {
30
+ const body = req.body || {};
31
+ const sysText = getSystemText(body);
32
+
33
+ if (/Extract any file paths/i.test(sysText)) return 'Bash';
34
+ if (/process Bash commands/i.test(sysText)) return 'Bash';
35
+ if (/command execution specialist/i.test(sysText)) return 'Bash';
36
+ if (/file search specialist/i.test(sysText)) return 'Search';
37
+ if (/planning specialist/i.test(sysText)) return 'Plan';
38
+ if (/general-purpose agent/i.test(sysText)) return 'General';
39
+ if (/security monitor/i.test(sysText)) return 'Advisor';
40
+ if (/performing a web search/i.test(sysText)) return 'WebSearch';
41
+
42
+ const msgs = body.messages || [];
43
+ for (let i = msgs.length - 1; i >= 0; i--) {
44
+ if (msgs[i].role !== 'user') continue;
45
+ const text = getMessageText(msgs[i]);
46
+ if (/^Command:/m.test(text)) return 'Bash';
47
+ break;
48
+ }
49
+
50
+ return null;
51
+ }
52
+
53
+ /**
54
+ * 判断请求是否为 Claude Code 内部合成的工具查询。
55
+ * 必须同时满足:(1) 来自主会话(isMainAgent 通过),(2) 最后一条 user message 的起首
56
+ * 命中 SYNTHETIC_PROMPTS 白名单。返回匹配的 subType 或 null。
57
+ *
58
+ * 为什么不用"消息数短"作为启发式:拦截日志里 teammate 消息 / tool_result
59
+ * 也常出现 messages.length <= 3,会把它们误判为 Synthetic。白名单精确度更高,
60
+ * 代价是 Claude Code 新增合成类型需要手工加 pattern。
61
+ */
62
+ function getSyntheticSubType(req) {
63
+ if (!isMainAgent(req)) return null;
64
+ const msgs = req.body?.messages || [];
65
+ if (!msgs.length) return null;
66
+ const last = msgs[msgs.length - 1];
67
+ if (!last || last.role !== 'user') return null;
68
+ const text = getMessageText(last).trim();
69
+ if (!text) return null;
70
+ for (const { subType, pattern } of SYNTHETIC_PROMPTS) {
71
+ if (pattern.test(text)) return subType;
72
+ }
73
+ return null;
74
+ }
75
+
76
+ function isCountRequest(req) {
77
+ const msgs = req.body?.messages;
78
+ if (!Array.isArray(msgs) || msgs.length !== 1) return false;
79
+ const msg = msgs[0];
80
+ return msg.role === 'user' && msg.content === 'count';
81
+ }
82
+
83
+ function isQuotaCheck(req) {
84
+ const body = req.body || {};
85
+ if (body.max_tokens !== 1) return false;
86
+ if (body.system) return false;
87
+ if (Array.isArray(body.tools) && body.tools.length > 0) return false;
88
+ const msgs = body.messages;
89
+ if (!Array.isArray(msgs) || msgs.length !== 1) return false;
90
+ return msgs[0].role === 'user' && msgs[0].content === 'quota';
91
+ }
92
+
93
+ /**
94
+ * Preflight 判断:
95
+ * 1. tools 为空或不存在
96
+ * 2. messages 仅一条 user message
97
+ * 3. system 包含 Claude Code 特征
98
+ * 4. 下一条请求的 messages 中包含本条 message 的文本(前80字符匹配)
99
+ */
100
+ function isPreflightRequest(req, nextReq) {
101
+ const body = req.body || {};
102
+ const tools = body.tools;
103
+ const msgs = body.messages || [];
104
+
105
+ // 条件1: tools 为空
106
+ if (Array.isArray(tools) && tools.length > 0) return false;
107
+
108
+ // 条件2: 仅一条 message
109
+ if (msgs.length !== 1 || msgs[0].role !== 'user') return false;
110
+
111
+ const text = getMessageText(msgs[0]);
112
+ if (!text) return false;
113
+
114
+ // 排除 count 请求
115
+ if (text.trim() === 'count') return false;
116
+
117
+ // 排除工具类请求(Bash 命令、安全策略检查、系统通知等)
118
+ const trimmed = text.trim();
119
+ if (/^Command:/m.test(text) || /^<policy_spec>/i.test(trimmed) || /^<task-notification>/i.test(trimmed)) return false;
120
+
121
+ // 条件3: system 包含 Claude Code 特征,但排除 Bash 处理器
122
+ const sysText = getSystemText(body);
123
+ if (!sysText.includes('Claude Code')) return false;
124
+ if (/process Bash commands/i.test(sysText)) return false;
125
+ if (/Extract any file paths/i.test(sysText)) return false;
126
+
127
+ // 条件4: 下一条请求的 messages 中包含本条文本
128
+ if (nextReq) {
129
+ const nextMsgs = nextReq.body?.messages || [];
130
+ const sig = text.slice(0, 80);
131
+ const found = nextMsgs.some(m => {
132
+ const c = m?.content;
133
+ if (typeof c === 'string') return c.includes(sig);
134
+ if (Array.isArray(c)) {
135
+ return c.some(block => block?.type === 'text' && block.text && block.text.includes(sig));
136
+ }
137
+ return false;
138
+ });
139
+ if (found) return true;
140
+ }
141
+
142
+ return false;
143
+ }
144
+
145
+ /**
146
+ * 分类请求
147
+ * @param {object} req - 当前请求
148
+ * @param {object} [nextReq] - 下一条请求(用于 Preflight 判断)
149
+ */
150
+ export function classifyRequest(req, nextReq) {
151
+ // Teammate 子进程的请求优先识别(收敛于 contentFilter.isTeammate)
152
+ if (isTeammate(req)) {
153
+ if (req.teammate) return { type: 'Teammate', subType: req.teammate };
154
+ if (req._cachedTeammateName === undefined) {
155
+ req._cachedTeammateName = extractTeammateName(req.body) || null;
156
+ }
157
+ return { type: 'Teammate', subType: req._cachedTeammateName };
158
+ }
159
+
160
+ // Synthetic 检查要在 MainAgent 之前——这类请求 mainAgent=true,
161
+ // 不拦截就会被当作普通主会话轮次,用户看不出是 Claude Code 合成的。
162
+ const syntheticSub = getSyntheticSubType(req);
163
+ if (syntheticSub) {
164
+ return { type: 'Synthetic', subType: syntheticSub };
165
+ }
166
+
167
+ if (isMainAgent(req)) {
168
+ return { type: 'MainAgent', subType: null };
169
+ }
170
+
171
+ if (req.isCountTokens || isCountRequest(req)) {
172
+ return { type: 'Count', subType: null };
173
+ }
174
+
175
+ if (isQuotaCheck(req)) {
176
+ return { type: 'Count', subType: 'Quota' };
177
+ }
178
+
179
+ if (isPreflightRequest(req, nextReq)) {
180
+ // Preflight 内容以 "Implement the following plan:" 开头 → Plan:Prompt
181
+ const text = getMessageText((req.body?.messages || [])[0]);
182
+ if (/Implement the following plan:/i.test(text.trim())) {
183
+ return { type: 'Plan', subType: 'Prompt' };
184
+ }
185
+ return { type: 'Preflight', subType: null };
186
+ }
187
+
188
+ const subType = getSubAgentSubType(req);
189
+ return { type: 'SubAgent', subType };
190
+ }
191
+
192
+ // Tag 显示文本
193
+ export function formatRequestTag(type, subType) {
194
+ if (type === 'Teammate' && subType) return `Teammate:${subType}`;
195
+ if (type === 'Plan' && subType) return `Plan:${subType}`;
196
+ if (type === 'SubAgent' && subType) return `SubAgent:${subType}`;
197
+ if (type === 'Synthetic' && subType) return `Synthetic:${subType}`;
198
+ if (type === 'Count' && subType) return `Count:${subType}`;
199
+ return type;
200
+ }
201
+
202
+ // Teammate label: "Teammate: name(model-short)" or "Teammate: name"
203
+ export function formatTeammateLabel(name, model) {
204
+ const displayName = name || 'X';
205
+ if (!model) return `Teammate: ${displayName}`;
206
+ const short = model.replace(/^claude-/i, '').replace(/-\d{8}$/, '');
207
+ return `Teammate: ${displayName}(${short})`;
208
+ }
@@ -0,0 +1,21 @@
1
+ // 拖拽 resize 的纯几何计算 —— 把 startX/Y/curX/curY/startW/startH 换成 clamp 后的 {w, h}。
2
+ // 单测可独立验证 4 个 clamp 边界 + 任意方向(dirX/dirY ∈ {-1, +1}),不依赖 DOM/React。
3
+ //
4
+ // 方向参数语义:
5
+ // dirX = -1 → 鼠标往左拉时 w 变大(handle 在左侧,例:左上 / 左下)
6
+ // dirX = +1 → 鼠标往右拉时 w 变大(handle 在右侧,例:右上 / 右下)
7
+ // dirY 同理(-1 = handle 在上侧)
8
+ //
9
+ // 调用方需保证 clamp 区间合法(min ≤ max);若不合法则 max 取胜(避 NaN)。
10
+ export function calcResizedSize({ startX, startY, curX, curY, startW, startH, dirX, dirY, clamp }) {
11
+ const dx = (curX - startX) * dirX;
12
+ const dy = (curY - startY) * dirY;
13
+ let w = startW + dx;
14
+ let h = startH + dy;
15
+ const { minW, maxW, minH, maxH } = clamp || {};
16
+ if (typeof minW === 'number' && w < minW) w = minW;
17
+ if (typeof maxW === 'number' && w > maxW) w = maxW;
18
+ if (typeof minH === 'number' && h < minH) h = minH;
19
+ if (typeof maxH === 'number' && h > maxH) h = maxH;
20
+ return { w: Math.round(w), h: Math.round(h) };
21
+ }
@@ -0,0 +1,17 @@
1
+ // 把「纯字符串 或 {lang: str} 本地化对象」解析成当前语言下的字符串。
2
+ // 用于 ultraAgents 预设专家的 title / description:服务端原样下发本地化对象,
3
+ // 前端按 getLang() 解析,语言切换无需重新请求。(content 是单语言字符串,不走本处。)
4
+ //
5
+ // 回退顺序:精确语言 → 去区域后的主语言(zh-TW→zh, pt-BR→pt) → en → zh → 首个非空字符串值。
6
+ // 异常输入(数组 / 数字 / null / 空对象 / 全空串对象)一律安全返回 '',绝不抛错。
7
+ export function resolveLocalized(field, lang) {
8
+ if (typeof field === 'string') return field;
9
+ if (!field || typeof field !== 'object' || Array.isArray(field)) return '';
10
+ const pick = (k) => (typeof field[k] === 'string' && field[k].trim() ? field[k] : null);
11
+ const base = typeof lang === 'string' ? lang.split('-')[0] : '';
12
+ return pick(lang)
13
+ || pick(base)
14
+ || pick('en')
15
+ || pick('zh')
16
+ || (Object.values(field).find(v => typeof v === 'string' && v.trim()) || '');
17
+ }
@@ -0,0 +1,47 @@
1
+ import { apiUrl } from './apiUrl';
2
+ import { reportSwallowed } from './errorReport';
3
+
4
+ /**
5
+ * Search the project codebase for `params.query`.
6
+ * @param {object} params { query, caseSensitive, wholeWord, regex, includeGlobs, excludeGlobs }
7
+ * @param {AbortSignal} [signal] abort a superseded / cancelled search
8
+ * @returns {Promise<{results, truncated, engine, filesScanned, error?}>}
9
+ * @throws on network failure or non-OK status (except 400 invalid_regex, surfaced as {error})
10
+ */
11
+ export async function searchCode(params, signal) {
12
+ const res = await fetch(apiUrl('/api/search'), {
13
+ method: 'POST',
14
+ headers: { 'Content-Type': 'application/json' },
15
+ body: JSON.stringify(params),
16
+ signal,
17
+ });
18
+ if (res.status === 400) {
19
+ // invalid_regex is an expected, user-facing outcome — return it as data, not a throw.
20
+ const data = await res.json().catch(() => ({ error: 'error' }));
21
+ return { results: [], truncated: false, engine: 'none', filesScanned: 0, error: data.error || 'error' };
22
+ }
23
+ if (!res.ok) throw new Error(`search failed: ${res.status}`);
24
+ return res.json();
25
+ }
26
+
27
+ /**
28
+ * Replace matches across files (or dry-run to count).
29
+ * @param {object} params { query, caseSensitive, wholeWord, regex, includeGlobs, excludeGlobs,
30
+ * replacement, scope:'all'|'file'|'match', file?, line?, col?, expectText?,
31
+ * skipPaths?, dryRun? }
32
+ * @returns {Promise<{changed, skipped, total, error?}>}
33
+ */
34
+ export async function replaceInFiles(params, signal) {
35
+ const res = await fetch(apiUrl('/api/search-replace'), {
36
+ method: 'POST',
37
+ headers: { 'Content-Type': 'application/json' },
38
+ body: JSON.stringify(params),
39
+ signal,
40
+ });
41
+ if (res.status === 400) {
42
+ const data = await res.json().catch(() => ({ error: 'error' }));
43
+ return { changed: [], skipped: [], total: 0, error: data.error || 'error' };
44
+ }
45
+ if (!res.ok) throw new Error(`replace failed: ${res.status}`);
46
+ return res.json();
47
+ }
@@ -0,0 +1,51 @@
1
+ // Client mirror of the server's replace primitives (server/lib/code-replace.js +
2
+ // buildQueryRegExp/looksCatastrophic in code-search.js). Kept byte-for-byte equivalent so the
3
+ // inline replace preview equals exactly what the server writes, and so single-match targeting
4
+ // uses the SAME (node/JS) match positions the server will — never a ripgrep-derived submatch
5
+ // offset that could disagree. Parity is pinned by test/search-replace-preview.test.js.
6
+
7
+ export function escapeRegExp(s) {
8
+ return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
9
+ }
10
+
11
+ export function buildQueryRegExp({ query, regex, wholeWord, caseSensitive }) {
12
+ let pattern = regex ? query : escapeRegExp(query);
13
+ if (wholeWord) pattern = `\\b(?:${pattern})\\b`;
14
+ const flags = 'g' + (caseSensitive ? '' : 'i');
15
+ return new RegExp(pattern, flags);
16
+ }
17
+
18
+ export function looksCatastrophic(pattern) {
19
+ return /\([^()]*[+*}][^()]*\)\s*[+*]/.test(pattern)
20
+ || /\([^()]*[+*][^()]*\)\{\d/.test(pattern);
21
+ }
22
+
23
+ /** Non-global clone (for native $-expansion of a single match). */
24
+ export function noGlobal(reG) {
25
+ return new RegExp(reG.source, reG.flags.replace('g', ''));
26
+ }
27
+
28
+ /** regex → native JS $-substitution; literal → verbatim (no $ interpretation). */
29
+ export function applyMatch(matchedText, reNoG, regex, replacement) {
30
+ if (!regex) return replacement;
31
+ return matchedText.replace(reNoG, replacement);
32
+ }
33
+
34
+ /**
35
+ * For a clicked match, recover the node-engine match position on the line so the server can
36
+ * re-verify it. Finds the JS-regex match covering `submatchStart`; returns its index + text.
37
+ * @returns {{col:number, expectText:string}|null}
38
+ */
39
+ export function computeMatchTarget(lineText, submatchStart, reG) {
40
+ reG.lastIndex = 0;
41
+ let m;
42
+ let fallback = null;
43
+ while ((m = reG.exec(lineText)) !== null) {
44
+ if (m[0] === '') { reG.lastIndex++; continue; }
45
+ if (m.index === submatchStart) { reG.lastIndex = 0; return { col: m.index, expectText: m[0] }; }
46
+ if (m.index <= submatchStart && submatchStart < m.index + m[0].length) fallback = { col: m.index, expectText: m[0] };
47
+ if (m.index > submatchStart) break;
48
+ }
49
+ reG.lastIndex = 0;
50
+ return fallback;
51
+ }
@@ -0,0 +1,79 @@
1
+ // 三态加载 + seq 防污 共用实现(class 组件用)。
2
+ // 三态契约:state[key] === null → 未加载(loading)
3
+ // state[key] === false → 加载失败
4
+ // state[key] === <data> → 加载成功
5
+ // 组件 instance 上必须存在 `_<name>Seq` 计数字段(constructor 初始化为 0)。
6
+ // workspace 切换时 componentDidUpdate 应 `++this._<name>Seq` 失效在途请求。
7
+ //
8
+ // ─── 抽取边界(有意为之,请勿扩张)────────────────────────────────────────────
9
+ // 这里**只覆盖 lazy-load 静默路径**:组件首次挂载/workspace 切换时自动 fire,失败
10
+ // 落 false 让上游 chip / popover 进入空态。
11
+ // 用户主动触发的 refresh(带 toast、loading spinner、错误 message)刻意保留在
12
+ // AppHeader.jsx (handleRefreshMemory) / Mobile.jsx 各自的 inline 实现里,原因:
13
+ // 1) 主动 refresh 需要 caller 决定 toast 文案、loading 视觉、错误反馈策略;
14
+ // 若硬塞进 loader 内部,loader 必须 import message → 反向耦合 antd UI 层。
15
+ // 2) loadMemoryDetail / loadClaudeMdDetail 也是用户点击驱动,含「成功后跳详情面板」
16
+ // 副作用,同样不属于"加载即更新 state"语义,留 caller 自管更清晰。
17
+ // 即:本文件 = 静默路径,inline = 用户触发路径。两者并存是设计选择,不是抽取漏。
18
+ // ────────────────────────────────────────────────────────────────────────────
19
+
20
+ import { apiUrl } from './apiUrl.js';
21
+
22
+ async function _seqGuardedFetch(component, seqProp, url) {
23
+ const seq = ++component[seqProp];
24
+ let r;
25
+ try {
26
+ r = await fetch(url);
27
+ } catch (e) {
28
+ if (seq !== component[seqProp]) return { stale: true };
29
+ return { stale: false, ok: false, error: e, errorKind: 'network' };
30
+ }
31
+ try {
32
+ const data = await r.json();
33
+ if (seq !== component[seqProp]) return { stale: true };
34
+ return { stale: false, ok: r.ok, status: r.status, data };
35
+ } catch (e) {
36
+ if (seq !== component[seqProp]) return { stale: true };
37
+ return { stale: false, ok: false, error: e, errorKind: 'parse', status: r.status };
38
+ }
39
+ }
40
+
41
+ // 拉取项目文件系统 skills。
42
+ // 失败时若 _fsSkills 已是数组(前次成功结果)→ 保留不 clobber,
43
+ // 避免 popover chip 从乐观态回退到历史空态。
44
+ // 返回 { ok: true, skills } / { ok: false, reason: 'local_log'|'stale'|'http:NNN'|'network'|'parse'|<server msg> }
45
+ export async function loadFsSkills(component, { isLocalLog } = {}) {
46
+ if (isLocalLog) return { ok: false, reason: 'local_log' };
47
+ const res = await _seqGuardedFetch(component, '_fsSkillsSeq', apiUrl('/api/skills'));
48
+ if (res.stale) return { ok: false, reason: 'stale' };
49
+ if (res.error) {
50
+ component.setState(prev => ({ _fsSkills: Array.isArray(prev._fsSkills) ? prev._fsSkills : false }));
51
+ return { ok: false, reason: res.errorKind || res.error.message || 'network' };
52
+ }
53
+ if (!res.ok || !res.data?.ok || !Array.isArray(res.data?.skills)) {
54
+ const reason = (res.data && res.data.error) || `http:${res.status}`;
55
+ component.setState(prev => ({ _fsSkills: Array.isArray(prev._fsSkills) ? prev._fsSkills : false }));
56
+ return { ok: false, reason };
57
+ }
58
+ component.setState({ _fsSkills: res.data.skills });
59
+ return { ok: true, skills: res.data.skills };
60
+ }
61
+
62
+ // 拉取项目入口 MEMORY.md。lazy-load 失败静默回退 false;用户主动刷新走 handleRefreshMemory(带 toast)。
63
+ export async function loadProjectMemory(component) {
64
+ const res = await _seqGuardedFetch(component, '_memorySeq', apiUrl('/api/project-memory'));
65
+ if (res.stale) return;
66
+ if (res.error || !res.ok) { component.setState({ _memory: false }); return; }
67
+ component.setState({ _memory: res.data });
68
+ }
69
+
70
+ // 拉取 CLAUDE.md 候选清单。三态:null/false/[]/[{id,scope,tail,...}]。
71
+ export async function loadClaudeMdList(component) {
72
+ const res = await _seqGuardedFetch(component, '_claudeMdSeq', apiUrl('/api/claude-md'));
73
+ if (res.stale) return;
74
+ if (res.error || !res.ok || !Array.isArray(res.data?.entries)) {
75
+ component.setState({ _claudeMd: false });
76
+ return;
77
+ }
78
+ component.setState({ _claudeMd: res.data.entries });
79
+ }