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
@@ -1,256 +1,127 @@
1
- // Stats Worker — 后台线程,扫描 JSONL 日志生成项目级统计 JSON
1
+ // Stats Worker — background thread generating per-project stats JSON.
2
+ // wire-v2 (1.7.0): the scan unit is a v2 SESSION DIRECTORY — usage/model come
3
+ // from journal req/done lines, previews from main-conversation event slices;
4
+ // nothing goes through the adapter (a full replay per tick would be pure
5
+ // waste, review P1). Legacy v1 *.jsonl files are no longer counted — their
6
+ // numbers return once the user migrates (ccv convert / the migrate prompt).
2
7
  import { parentPort } from 'node:worker_threads';
3
8
  import { readFileSync, writeFileSync, existsSync, readdirSync, statSync } from 'node:fs';
4
9
  import { join, basename } from 'node:path';
5
- import { resolveJsonlPath } from './jsonl-archive.js';
10
+ import { readJsonlTolerant, listSessionIds } from './v2/replay.js';
11
+ import { dirSizeSync } from './v2/layout.js';
12
+ import {
13
+ INTER_SESSION_TYPES, isSystemText, extractUserTexts, isSuggestionMode,
14
+ collectPromptsFromEvents, sortEpochFiles,
15
+ } from './user-prompt-extract.js';
16
+
17
+ // Prompt/preview extraction moved to the shared server/lib/user-prompt-extract.js
18
+ // (also feeds the V2Writer prompts.jsonl cache and the log-list read side).
19
+ // Re-exported here so existing importers/tests keep working.
20
+ export { INTER_SESSION_TYPES, isSystemText, extractUserTexts };
6
21
 
7
22
  // 统计 schema 版本号,新增统计字段时递增,强制旧缓存失效重新解析
8
- const STATS_VERSION = 8;
9
-
10
- // 跨会话 / teammate 协议通知 type 白名单(须与 src/utils/contentFilter.js 的 INTER_SESSION_NOTIFICATION_TYPES
11
- // 一致;新增 type 时两处都要加)。单一数组派生 Set(brace 扫描用)+ 正则(isSystemText 用),避免本文件内漂移。
12
- // test/stats-worker-notification-filter.test.js 有 frontend↔server 同步守卫断言。
13
- export const INTER_SESSION_TYPES = [
14
- 'idle_notification', 'shutdown_request', 'shutdown_response', 'shutdown_approved',
15
- 'teammate_terminated', 'plan_approval_request', 'plan_approval_response',
16
- ];
17
- const INTER_SESSION_TYPES_SET = new Set(INTER_SESSION_TYPES);
18
- const INTER_SESSION_TYPES_RE = new RegExp(`"type"\\s*:\\s*"(?:${INTER_SESSION_TYPES.join('|')})"`);
19
-
20
- /**
21
- * 判断文本是否为系统注入文本。
22
- * 注意:这是服务端「子集」实现,只覆盖 project-stats 预览过滤所需规则,并非 contentFilter 的完整副本——
23
- * 完整分类(synthetic prompt、二次回收等)见 src/utils/contentFilter.js:isSystemText。前端(dist)与后端(server)
24
- * 分属两个 bundle、无法直接共享同一模块,故此处仅同步「会泄漏进预览」的关键规则:系统标签 + 跨会话队友通知。
25
- */
26
- function isSystemText(text) {
27
- if (!text) return true;
28
- const trimmed = text.trim();
29
- if (!trimmed) return true;
30
- // 包含 plan 内容的文本块不应被过滤(即使开头有系统标签)
31
- if (/Implement the following plan:/i.test(trimmed)) return false;
32
- if (/^<[a-zA-Z_][\w-]*[\s>]/i.test(trimmed)) return true; // 含 <teammate-message> 包裹形态
33
- if (/^\[SUGGESTION MODE:/i.test(trimmed)) return true;
34
- if (/^Your response was cut off because it exceeded the output token limit/i.test(trimmed)) return true;
35
- if (/^Base directory for this skill:/i.test(trimmed)) return true;
36
- // 未包裹的跨会话队友通知:前缀行 / 新版 caveat / 裸协议 JSON —— 防泄漏进 stats 预览当成用户 prompt
37
- if (/^Another Claude session sent a message:/i.test(trimmed)) return true;
38
- if (/^This came from another Claude session\b/i.test(trimmed)) return true;
39
- if (trimmed.startsWith('{') && INTER_SESSION_TYPES_RE.test(trimmed)) return true;
40
- return false;
41
- }
42
-
43
- // 单次扫描剔除顶层协议通知 JSON(brace 配对,正确处理嵌套;跳过字符串字面量内的花括号 / 转义)。
44
- // 与 src/utils/contentFilter.js 的 scanTopLevelJsonObjects + extractProtocolNotifications 同语义——
45
- // 旧的 `\{[^{}]*...[^{}]*\}` 正则无法跨嵌套花括号,含嵌套字段的协议体(如 plan_approval_*)会漏剥。
46
- function stripProtocolJson(s) {
47
- if (typeof s !== 'string' || s.indexOf('{') === -1) return s;
48
- let out = '', cursor = 0, depth = 0, start = -1, inStr = false, esc = false;
49
- for (let i = 0; i < s.length; i++) {
50
- const ch = s[i];
51
- if (inStr) {
52
- if (esc) esc = false;
53
- else if (ch === '\\') esc = true;
54
- else if (ch === '"') inStr = false;
55
- continue;
56
- }
57
- if (ch === '"') inStr = true;
58
- else if (ch === '{') { if (depth === 0) start = i; depth++; }
59
- else if (ch === '}' && depth > 0) {
60
- depth--;
61
- if (depth === 0 && start >= 0) {
62
- let j; try { j = JSON.parse(s.slice(start, i + 1)); } catch { j = null; }
63
- if (j && typeof j.type === 'string' && INTER_SESSION_TYPES_SET.has(j.type)) {
64
- out += s.slice(cursor, start);
65
- cursor = i + 1;
66
- }
67
- start = -1;
68
- }
69
- }
70
- }
71
- out += s.slice(cursor);
72
- return out;
73
- }
74
-
75
- /**
76
- * 剥离系统注入标签,保留标签外的用户文本(仅用于 string 类型 content 的混合内容场景)
77
- */
78
- function stripSystemTags(text) {
79
- let out = text
80
- .replace(/<(system-reminder|local-command-caveat|project-reminder|important-instruction-reminders|file-modified-reminder|todo-reminder|user-prompt-submit-hook|local-command-stdout|command-name|task-notification|environment_details|context|teammate-message)\b[^>]*>[\s\S]*?<\/\1>/gi, '')
81
- // 未包裹跨会话队友通知的 chrome:前缀行 + 两版 caveat + 裸协议 JSON(含嵌套),剥掉后回收用户混入正文
82
- .replace(/^Another Claude session sent a message:\s*/i, '')
83
- .replace(/(^|\n)This came from another Claude session[\s\S]*?(?=\n\n|$)/i, '')
84
- .replace(/(^|\n)IMPORTANT: This is NOT from your user[\s\S]*?(?=\n\n|$)/i, '');
85
- out = stripProtocolJson(out);
86
- return out.trim();
87
- }
88
-
89
- /**
90
- * 从 messages 数组中提取用户 prompt 文本列表
91
- * (与 src/App.jsx:extractUserTexts + src/utils/contentFilter.js:classifyUserContent 保持同步)
92
- */
93
- function extractUserTexts(messages) {
94
- const texts = [];
95
- for (const msg of messages) {
96
- if (msg.role !== 'user') continue;
97
- if (typeof msg.content === 'string') {
98
- // string content 可能混合系统标签与用户文本,先剥离标签再判断
99
- const text = stripSystemTags(msg.content).trim();
100
- if (text && !isSystemText(text)) {
101
- if (/^Implement the following plan:/i.test(text)) continue;
102
- texts.push(text);
103
- }
104
- } else if (Array.isArray(msg.content)) {
105
- const hasCommand = msg.content.some(b => b.type === 'text' && /<command-message>/i.test(b.text || ''));
106
- const userParts = [];
107
- for (const b of msg.content) {
108
- if (b.type !== 'text') continue;
109
- const text = (b.text || '').trim();
110
- if (!text || isSystemText(text)) continue;
111
- if (hasCommand && /<command-message>/i.test(text)) continue;
112
- if (/^Implement the following plan:/i.test(text)) continue;
113
- userParts.push(text);
114
- }
115
- if (userParts.length > 0) {
116
- texts.push(userParts.join(' '));
117
- }
118
- }
119
- }
120
- return texts;
121
- }
122
-
123
- /**
124
- * 检测请求是否为 SUGGESTION MODE(预测用户下一步输入)
125
- */
126
- function isSuggestionMode(messages) {
127
- if (!Array.isArray(messages) || messages.length === 0) return false;
128
- const last = messages[messages.length - 1];
129
- if (last?.role !== 'user') return false;
130
- const content = last.content;
131
- if (Array.isArray(content)) {
132
- return content.some(b => b.type === 'text' && /^\[SUGGESTION MODE:/i.test((b.text || '').trim()));
133
- }
134
- if (typeof content === 'string') return /^\[SUGGESTION MODE:/im.test(content.trim());
135
- return false;
136
- }
23
+ // v9: v2 session-dir units (files map keyed `sessions/<sid>`), journal-based counts
24
+ // v10: per-session `size` = recursive session-dir bytes (was journal-only);
25
+ // `journalSize` carries the incremental-cache key
26
+ const STATS_VERSION = 10;
137
27
 
138
28
  /**
139
- * 解析单个 JSONL 文件,提取模型使用次数和 token 统计
140
- * @param {string} filePath JSONL 文件绝对路径
141
- * @returns {{ models: Object, summary: Object }}
29
+ * Parse one v2 session directory into the same stats shape parseJsonlFile
30
+ * produced for a v1 file.
31
+ *
32
+ * Sources (all cheap, content-free except previews):
33
+ * - journal req lines → requestCount, per-model counts, main-turn tracking
34
+ * inputs (kind/msgTo/epoch/seq);
35
+ * - journal done lines (folded first-wins per §14) → per-model token usage;
36
+ * - conversations/main/e<N>.jsonl event slices → user-prompt previews +
37
+ * SUGGESTION MODE detection (append slices carry exactly the new messages,
38
+ * so no prevTextCount bookkeeping is needed; snapshot repeats are absorbed
39
+ * by the preview dedup set).
40
+ * @param {string} sessionDir absolute session directory
41
+ * @returns {{ models: Object, summary: Object, preview: string[] }}
142
42
  */
143
- function parseJsonlFile(filePath) {
43
+ function parseSessionDir(sessionDir) {
144
44
  const models = {};
145
45
  let requestCount = 0;
146
- let sessionCount = 0;
147
- let turnCount = 0;
148
- let maxMsgLen = 0;
149
46
  let totalInput = 0;
150
47
  let totalOutput = 0;
151
48
  let totalCacheRead = 0;
152
49
  let totalCacheCreation = 0;
153
- const preview = [];
154
- let prevTextCount = 0;
155
- let lastCollectedSig = ''; // 上次收集的 prompt 签名,用于去重同轮重复请求
156
-
157
- try {
158
- const realPath = resolveJsonlPath(filePath);
159
- const content = readFileSync(realPath, 'utf-8');
160
- if (!content.trim()) return { models, summary: { requestCount: 0, sessionCount: 0, turnCount: 0, input_tokens: 0, output_tokens: 0, cache_read_input_tokens: 0, cache_creation_input_tokens: 0 }, preview: [] };
161
-
162
- const entries = content.split('\n---\n').filter(p => p.trim());
163
- for (const raw of entries) {
164
- try {
165
- const entry = JSON.parse(raw);
166
- requestCount++;
167
-
168
- // 会话轮次统计(仅 MainAgent,排除 SUGGESTION MODE)
169
- if (entry.mainAgent && Array.isArray(entry.body?.messages)) {
170
- const msgs = entry.body.messages;
171
- // Delta storage: 使用 _totalMessageCount(delta 条目)或 msgs.length(旧格式)
172
- const len = entry._totalMessageCount || msgs.length;
173
- if (!isSuggestionMode(msgs)) {
174
- // messages 数量大幅缩减:新会话(/clear 等),重置追踪
175
- if (len < maxMsgLen * 0.5 && (maxMsgLen - len) > 4) {
176
- maxMsgLen = 0;
177
- prevTextCount = 0;
178
- }
179
- const isNewTurn = len > maxMsgLen;
180
- if (isNewTurn) {
181
- maxMsgLen = len;
182
- turnCount++;
183
- }
184
- // 收集用户 prompt:新轮次 或 新会话(len===1 且非首次)
185
- // 用内容签名去重,避免同一轮的重复请求重复收集
186
- if (isNewTurn || (len === 1 && sessionCount > 0)) {
187
- if (len === 1 && !isNewTurn) {
188
- // 新会话但未触发 shrink 检测(上一会话较短),重置基线
189
- prevTextCount = 0;
190
- maxMsgLen = len;
191
- }
192
- const texts = extractUserTexts(msgs);
193
- // Delta storage: delta 条目的 msgs 只有新增部分,prevTextCount 不适用,从 0 开始收集
194
- const textStart = entry._deltaFormat ? 0 : prevTextCount;
195
- // 生成本次 prompt 签名,用于跳过同轮重复请求
196
- const sig = texts.join('\x00');
197
- if (sig === lastCollectedSig && !isNewTurn) {
198
- // 同一轮的重复请求(内容完全相同),跳过
199
- } else {
200
- for (let ti = textStart; ti < texts.length; ti++) {
201
- const flat = texts[ti].replace(/[\r\n]+/g, ' ').trim();
202
- if (flat) preview.push(flat.slice(0, 100));
203
- }
204
- prevTextCount = texts.length;
205
- lastCollectedSig = sig;
206
- }
207
- }
208
- if (len === 1) sessionCount++;
209
- }
210
- }
211
-
212
- // 提取模型名:优先 body.model,其次 response.body.model
213
- const model = entry.body?.model || entry.response?.body?.model;
214
- if (!model) continue;
215
50
 
51
+ const reqs = new Map(); // seq → req line
52
+ const doneSeqs = new Set();
53
+ for (const line of readJsonlTolerant(join(sessionDir, 'journal.jsonl'))) {
54
+ if (line.ph === 'req') {
55
+ if (reqs.has(line.seq)) continue;
56
+ reqs.set(line.seq, line);
57
+ requestCount++;
58
+ const model = line.model;
59
+ if (model) {
216
60
  if (!models[model]) {
217
61
  models[model] = { count: 0, input_tokens: 0, output_tokens: 0, cache_read_input_tokens: 0, cache_creation_input_tokens: 0 };
218
62
  }
219
63
  models[model].count++;
64
+ }
65
+ } else if (line.ph === 'done' && !doneSeqs.has(line.seq)) {
66
+ doneSeqs.add(line.seq);
67
+ const usage = line.usage;
68
+ if (!usage) continue;
69
+ const req = reqs.get(line.seq);
70
+ const model = req && req.model;
71
+ const inp = usage.in || 0;
72
+ const out = usage.out || 0;
73
+ const cacheRead = usage.cr || 0;
74
+ const cacheCreate = usage.cw || 0;
75
+ if (model && models[model]) {
76
+ models[model].input_tokens += inp;
77
+ models[model].output_tokens += out;
78
+ models[model].cache_read_input_tokens += cacheRead;
79
+ models[model].cache_creation_input_tokens += cacheCreate;
80
+ }
81
+ totalInput += inp;
82
+ totalOutput += out;
83
+ totalCacheRead += cacheRead;
84
+ totalCacheCreation += cacheCreate;
85
+ }
86
+ }
220
87
 
221
- // 提取 usage 可能在 response.body.usage
222
- const usage = entry.response?.body?.usage;
223
- if (usage) {
224
- const inp = usage.input_tokens || 0;
225
- const out = usage.output_tokens || 0;
226
- const cacheRead = usage.cache_read_input_tokens || usage.cache_creation_input_tokens ? (usage.cache_read_input_tokens || 0) : 0;
227
- const cacheCreate = usage.cache_creation_input_tokens || 0;
228
-
229
- models[model].input_tokens += inp;
230
- models[model].output_tokens += out;
231
- models[model].cache_read_input_tokens += cacheRead;
232
- models[model].cache_creation_input_tokens += cacheCreate;
233
-
234
- totalInput += inp;
235
- totalOutput += out;
236
- totalCacheRead += cacheRead;
237
- totalCacheCreation += cacheCreate;
238
- }
239
- } catch {
240
- // 跳过无法解析的条目
88
+ // Main-conversation event slices: previews + suggestion-mode seqs. The
89
+ // fold itself lives in the shared collectPromptsFromEvents; the suggestion
90
+ // seq set is a stats-only concern, gathered in the same pass.
91
+ const suggestionSeqs = new Set();
92
+ const acc = {};
93
+ const mainConvDir = join(sessionDir, 'conversations', 'main');
94
+ if (existsSync(mainConvDir)) {
95
+ let epochFiles = [];
96
+ try {
97
+ epochFiles = sortEpochFiles(readdirSync(mainConvDir));
98
+ } catch { /* unreadable conv dir — counts still stand */ }
99
+ for (const f of epochFiles) {
100
+ const events = readJsonlTolerant(join(mainConvDir, f));
101
+ for (const ev of events) {
102
+ if (Array.isArray(ev.msgs) && ev.msgs.length > 0 && isSuggestionMode(ev.msgs)) suggestionSeqs.add(ev.seq);
241
103
  }
104
+ collectPromptsFromEvents(events, acc);
242
105
  }
243
- } catch {
244
- // 文件读取失败
245
106
  }
107
+ const preview = acc.out || [];
246
108
 
247
- // 去重 preview:保留首次出现顺序,移除重复文本
248
- const seenPreview = new Set();
249
- const uniquePreview = [];
250
- for (const p of preview) {
251
- if (!seenPreview.has(p)) {
252
- seenPreview.add(p);
253
- uniquePreview.push(p);
109
+ // Turn/session tracking over main req lines — the v1 wire-growth formula,
110
+ // driven by journal msgTo counts instead of message arrays.
111
+ let turnCount = 0;
112
+ let maxMsgLen = 0;
113
+ const epochs = new Set();
114
+ const mainSeqs = [...reqs.values()]
115
+ .filter(r => r.kind === 'main' && typeof r.msgTo === 'number')
116
+ .sort((a, b) => a.seq - b.seq);
117
+ for (const r of mainSeqs) {
118
+ if (suggestionSeqs.has(r.seq)) continue;
119
+ if (typeof r.epoch === 'number') epochs.add(r.epoch);
120
+ const len = r.msgTo;
121
+ if (len < maxMsgLen * 0.5 && (maxMsgLen - len) > 4) maxMsgLen = 0; // /clear-style shrink
122
+ if (len > maxMsgLen) {
123
+ maxMsgLen = len;
124
+ turnCount++;
254
125
  }
255
126
  }
256
127
 
@@ -258,14 +129,14 @@ function parseJsonlFile(filePath) {
258
129
  models,
259
130
  summary: {
260
131
  requestCount,
261
- sessionCount,
132
+ sessionCount: epochs.size,
262
133
  turnCount,
263
134
  input_tokens: totalInput,
264
135
  output_tokens: totalOutput,
265
136
  cache_read_input_tokens: totalCacheRead,
266
137
  cache_creation_input_tokens: totalCacheCreation,
267
138
  },
268
- preview: uniquePreview,
139
+ preview,
269
140
  };
270
141
  }
271
142
 
@@ -288,42 +159,37 @@ function generateProjectStats(projectDir, projectName, onlyFile) {
288
159
  existing = null;
289
160
  }
290
161
 
291
- // 列出所有 JSONL 文件(排除 _temp.jsonl)
292
- let jsonlFiles;
293
- try {
294
- jsonlFiles = readdirSync(projectDir)
295
- .filter(f => (f.endsWith('.jsonl') || f.endsWith('.jsonl.zip')) && !f.endsWith('_temp.jsonl'))
296
- .sort();
297
- } catch {
298
- return;
299
- }
300
-
301
- if (jsonlFiles.length === 0) return;
162
+ // Scan units = v2 session dirs. The cache key is the journal's size+mtime:
163
+ // every request/completion appends journal lines, so any change moves it.
164
+ const sessionIds = listSessionIds(projectDir).sort();
165
+ if (sessionIds.length === 0) return;
302
166
 
303
167
  const filesStats = {};
304
168
  const topModels = {};
305
169
 
306
- for (const f of jsonlFiles) {
307
- const filePath = join(projectDir, f);
170
+ for (const sid of sessionIds) {
171
+ const unitKey = `sessions/${sid}`;
172
+ const journalPath = join(projectDir, 'sessions', sid, 'journal.jsonl');
308
173
  let stat;
309
174
  try {
310
- stat = statSync(filePath);
175
+ stat = statSync(journalPath);
311
176
  } catch {
312
- continue;
177
+ continue; // dir without a journal is not a session yet
313
178
  }
314
179
 
315
- const size = stat.size;
180
+ // v10: journalSize is the incremental-cache KEY (journal size+mtime moves
181
+ // on every append); `size` is the DISPLAY value = recursive session-dir
182
+ // bytes (folder size), persisted alongside.
183
+ const journalSize = stat.size;
316
184
  const lastModified = stat.mtime.toISOString();
317
185
 
318
- // 增量优化:如果有已有统计且文件未变化且 schema 版本一致,直接复用
186
+ // 增量优化:如果有已有统计且 journal 未变化且 schema 版本一致,直接复用
319
187
  if (existing?._v === STATS_VERSION
320
- && existing?.files?.[f] && existing.files[f].size === size && existing.files[f].lastModified === lastModified) {
321
- // 如果指定了 onlyFile 且不是此文件,跳过重新解析
322
- if (!onlyFile || onlyFile !== f) {
323
- filesStats[f] = existing.files[f];
324
- // 汇总模型
325
- if (filesStats[f].models) {
326
- for (const [model, data] of Object.entries(filesStats[f].models)) {
188
+ && existing?.files?.[unitKey] && existing.files[unitKey].journalSize === journalSize && existing.files[unitKey].lastModified === lastModified) {
189
+ if (!onlyFile || onlyFile !== unitKey) {
190
+ filesStats[unitKey] = existing.files[unitKey];
191
+ if (filesStats[unitKey].models) {
192
+ for (const [model, data] of Object.entries(filesStats[unitKey].models)) {
327
193
  if (!topModels[model]) topModels[model] = 0;
328
194
  topModels[model] += data.count;
329
195
  }
@@ -332,23 +198,26 @@ function generateProjectStats(projectDir, projectName, onlyFile) {
332
198
  }
333
199
  }
334
200
 
335
- // 需要重新解析
336
- const parsed = parseJsonlFile(filePath);
337
- filesStats[f] = {
201
+ const sessionDir = join(projectDir, 'sessions', sid);
202
+ const parsed = parseSessionDir(sessionDir);
203
+ filesStats[unitKey] = {
338
204
  models: parsed.models,
339
205
  summary: parsed.summary,
340
206
  preview: parsed.preview,
341
- size,
207
+ size: dirSizeSync(sessionDir),
208
+ journalSize,
342
209
  lastModified,
343
210
  };
344
211
 
345
- // 汇总模型使用次数
346
212
  for (const [model, data] of Object.entries(parsed.models)) {
347
213
  if (!topModels[model]) topModels[model] = 0;
348
214
  topModels[model] += data.count;
349
215
  }
350
216
  }
351
217
 
218
+ // No parsable session yet (dirs without journals) — keep whatever exists.
219
+ if (Object.keys(filesStats).length === 0) return;
220
+
352
221
  // 计算全局汇总
353
222
  let totalRequests = 0;
354
223
  let totalSessions = 0;
@@ -378,7 +247,7 @@ function generateProjectStats(projectDir, projectName, onlyFile) {
378
247
  requestCount: totalRequests,
379
248
  sessionCount: totalSessions,
380
249
  turnCount: totalTurns,
381
- fileCount: jsonlFiles.length,
250
+ fileCount: Object.keys(filesStats).length,
382
251
  input_tokens: totalInput,
383
252
  output_tokens: totalOutput,
384
253
  cache_read_input_tokens: totalCacheRead,
@@ -411,9 +280,6 @@ function scanAllProjects(logDir) {
411
280
  }
412
281
  }
413
282
 
414
- // 供单元测试引用的内部纯函数(worker 入口逻辑走 parentPort,不受 export 影响)
415
- export { isSystemText, extractUserTexts };
416
-
417
283
  // Worker 消息处理
418
284
  parentPort?.on('message', (msg) => {
419
285
  switch (msg.type) {
@@ -429,10 +295,15 @@ parentPort?.on('message', (msg) => {
429
295
  case 'update': {
430
296
  const { logDir, projectName, logFile } = msg;
431
297
  const projectDir = join(logDir, projectName);
432
- const fileName = basename(logFile);
298
+ // logFile is a session DIR path from the live feed (v2); the unit key is
299
+ // `sessions/<sid>`. A legacy basename is passed through harmlessly (it
300
+ // just never matches a unit → plain incremental run).
301
+ const onlyUnit = String(logFile || '').includes(`${'/'}sessions${'/'}`) || String(logFile || '').includes(`\\sessions\\`)
302
+ ? `sessions/${basename(logFile)}`
303
+ : basename(logFile || '');
433
304
  if (existsSync(projectDir)) {
434
- generateProjectStats(projectDir, projectName, fileName);
435
- parentPort?.postMessage({ type: 'update-done', projectName, logFile: fileName });
305
+ generateProjectStats(projectDir, projectName, onlyUnit);
306
+ parentPort?.postMessage({ type: 'update-done', projectName, logFile: onlyUnit });
436
307
  }
437
308
  break;
438
309
  }