spiracha 1.5.0 → 2.0.0

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 (365) hide show
  1. package/AGENTS.md +114 -205
  2. package/README.md +89 -269
  3. package/apps/ui/AGENTS.md +17 -7
  4. package/apps/ui/README.md +34 -8
  5. package/apps/ui/components.json +21 -0
  6. package/apps/ui/package.json +65 -0
  7. package/apps/ui/src/components/antigravity-conversations-table.tsx +206 -0
  8. package/apps/ui/src/components/antigravity-keychain-panel.tsx +76 -0
  9. package/apps/ui/src/components/antigravity-workspaces-table.tsx +63 -0
  10. package/apps/ui/src/components/app-shell.tsx +124 -0
  11. package/apps/ui/src/components/breadcrumbs.tsx +53 -0
  12. package/apps/ui/src/components/claude-code-sessions-table.tsx +114 -0
  13. package/apps/ui/src/components/claude-code-workspaces-table.tsx +55 -0
  14. package/apps/ui/src/components/cursor-threads-table.tsx +189 -0
  15. package/apps/ui/src/components/cursor-workspaces-table.tsx +140 -0
  16. package/apps/ui/src/components/data-table.tsx +241 -0
  17. package/apps/ui/src/components/delete-confirm-dialog.tsx +88 -0
  18. package/apps/ui/src/components/export-dialog.tsx +184 -0
  19. package/apps/ui/src/components/json-panel.tsx +17 -0
  20. package/apps/ui/src/components/kiro-sessions-table.tsx +112 -0
  21. package/apps/ui/src/components/kiro-workspaces-table.tsx +53 -0
  22. package/apps/ui/src/components/list-search-input.tsx +18 -0
  23. package/apps/ui/src/components/loading-panel.tsx +23 -0
  24. package/apps/ui/src/components/metadata-section.tsx +31 -0
  25. package/apps/ui/src/components/metric-card.tsx +19 -0
  26. package/apps/ui/src/components/opencode-sessions-table.tsx +128 -0
  27. package/apps/ui/src/components/opencode-workspaces-table.tsx +57 -0
  28. package/apps/ui/src/components/page-header.tsx +33 -0
  29. package/apps/ui/src/components/projects-loading-state.tsx +74 -0
  30. package/apps/ui/src/components/projects-table.tsx +108 -0
  31. package/apps/ui/src/components/qoder-sessions-table.tsx +112 -0
  32. package/apps/ui/src/components/qoder-workspaces-table.tsx +53 -0
  33. package/apps/ui/src/components/recent-threads-list.tsx +52 -0
  34. package/apps/ui/src/components/reload-error-panel.tsx +20 -0
  35. package/apps/ui/src/components/text-document-panel.tsx +19 -0
  36. package/apps/ui/src/components/theme-toggle.tsx +48 -0
  37. package/apps/ui/src/components/threads-table.tsx +202 -0
  38. package/apps/ui/src/components/transcript-view.tsx +552 -0
  39. package/apps/ui/src/components/ui/alert-dialog.tsx +160 -0
  40. package/apps/ui/src/components/ui/badge.tsx +45 -0
  41. package/apps/ui/src/components/ui/button.tsx +62 -0
  42. package/apps/ui/src/components/ui/checkbox.tsx +29 -0
  43. package/apps/ui/src/components/ui/dialog.tsx +137 -0
  44. package/apps/ui/src/components/ui/dropdown-menu.tsx +217 -0
  45. package/apps/ui/src/components/ui/input.tsx +21 -0
  46. package/apps/ui/src/components/ui/scroll-area.tsx +46 -0
  47. package/apps/ui/src/components/ui/select.tsx +163 -0
  48. package/apps/ui/src/components/ui/separator.tsx +28 -0
  49. package/apps/ui/src/components/ui/sheet.tsx +107 -0
  50. package/apps/ui/src/components/ui/skeleton.tsx +7 -0
  51. package/apps/ui/src/components/ui/table.tsx +76 -0
  52. package/apps/ui/src/components/ui/tabs.tsx +71 -0
  53. package/apps/ui/src/components/ui/tooltip.tsx +44 -0
  54. package/apps/ui/src/integrations/tanstack-query/devtools.tsx +6 -0
  55. package/apps/ui/src/integrations/tanstack-query/root-provider.tsx +10 -0
  56. package/apps/ui/src/lib/antigravity-conversation-state.ts +28 -0
  57. package/apps/ui/src/lib/antigravity-queries.ts +33 -0
  58. package/apps/ui/src/lib/antigravity-server.ts +128 -0
  59. package/apps/ui/src/lib/antigravity-transcript-events.ts +335 -0
  60. package/apps/ui/src/lib/claude-code-queries.ts +26 -0
  61. package/apps/ui/src/lib/claude-code-server.ts +75 -0
  62. package/apps/ui/src/lib/claude-code-transcript-events.ts +154 -0
  63. package/apps/ui/src/lib/codex-queries.ts +66 -0
  64. package/apps/ui/src/lib/codex-server.ts +209 -0
  65. package/apps/ui/src/lib/cursor-queries.ts +37 -0
  66. package/apps/ui/src/lib/cursor-server.ts +272 -0
  67. package/apps/ui/src/lib/cursor-transcript-events.ts +229 -0
  68. package/apps/ui/src/lib/download.ts +166 -0
  69. package/apps/ui/src/lib/formatters.ts +149 -0
  70. package/apps/ui/src/lib/kiro-queries.ts +22 -0
  71. package/apps/ui/src/lib/kiro-server.ts +73 -0
  72. package/apps/ui/src/lib/kiro-transcript-events.ts +113 -0
  73. package/apps/ui/src/lib/opencode-queries.ts +37 -0
  74. package/apps/ui/src/lib/opencode-server.ts +73 -0
  75. package/apps/ui/src/lib/opencode-transcript-events.ts +221 -0
  76. package/apps/ui/src/lib/package-metadata.ts +16 -0
  77. package/apps/ui/src/lib/path-utils.ts +13 -0
  78. package/apps/ui/src/lib/qoder-queries.ts +22 -0
  79. package/apps/ui/src/lib/qoder-server.ts +79 -0
  80. package/apps/ui/src/lib/qoder-transcript-events.ts +139 -0
  81. package/apps/ui/src/lib/route-search.ts +68 -0
  82. package/apps/ui/src/lib/settings-store.tsx +59 -0
  83. package/apps/ui/src/lib/source-session-export-server.ts +56 -0
  84. package/apps/ui/src/lib/text-filter.ts +22 -0
  85. package/apps/ui/src/lib/thread-id.ts +3 -0
  86. package/apps/ui/src/lib/thread-transcript-stats.ts +57 -0
  87. package/apps/ui/src/lib/utils.ts +7 -0
  88. package/apps/ui/src/routeTree.gen.ts +730 -0
  89. package/apps/ui/src/router.tsx +20 -0
  90. package/apps/ui/src/routes/$threadId.tsx +22 -0
  91. package/apps/ui/src/routes/__root.tsx +127 -0
  92. package/apps/ui/src/routes/analytics.tsx +143 -0
  93. package/apps/ui/src/routes/antigravity-conversations.$conversationId.tsx +438 -0
  94. package/apps/ui/src/routes/antigravity.$workspaceKey.tsx +127 -0
  95. package/apps/ui/src/routes/antigravity.index.tsx +51 -0
  96. package/apps/ui/src/routes/api.v1.conversation-query.ts +12 -0
  97. package/apps/ui/src/routes/api.v1.conversations.$source.$id.export.ts +12 -0
  98. package/apps/ui/src/routes/api.v1.conversations.$source.$id.ts +12 -0
  99. package/apps/ui/src/routes/api.v1.conversations.ts +12 -0
  100. package/apps/ui/src/routes/api.v1.resolve.ts +12 -0
  101. package/apps/ui/src/routes/api.v1.sources.ts +12 -0
  102. package/apps/ui/src/routes/claude-code-sessions.$sessionId.tsx +342 -0
  103. package/apps/ui/src/routes/claude-code.$workspaceKey.tsx +135 -0
  104. package/apps/ui/src/routes/claude-code.index.tsx +48 -0
  105. package/apps/ui/src/routes/codex.$project.tsx +390 -0
  106. package/apps/ui/src/routes/codex.index.tsx +116 -0
  107. package/apps/ui/src/routes/cursor-threads.$composerId.tsx +435 -0
  108. package/apps/ui/src/routes/cursor.$workspaceKey.tsx +416 -0
  109. package/apps/ui/src/routes/cursor.index.tsx +120 -0
  110. package/apps/ui/src/routes/index.tsx +124 -0
  111. package/apps/ui/src/routes/kiro-sessions.$sessionId.tsx +345 -0
  112. package/apps/ui/src/routes/kiro.$workspaceKey.tsx +136 -0
  113. package/apps/ui/src/routes/kiro.index.tsx +48 -0
  114. package/apps/ui/src/routes/opencode-sessions.$sessionId.tsx +334 -0
  115. package/apps/ui/src/routes/opencode.$workspaceKey.tsx +135 -0
  116. package/apps/ui/src/routes/opencode.index.tsx +48 -0
  117. package/apps/ui/src/routes/qoder-sessions.$sessionId.tsx +341 -0
  118. package/apps/ui/src/routes/qoder.$workspaceKey.tsx +139 -0
  119. package/apps/ui/src/routes/qoder.index.tsx +48 -0
  120. package/apps/ui/src/routes/settings.tsx +91 -0
  121. package/apps/ui/src/routes/threads.$threadId.tsx +619 -0
  122. package/apps/ui/src/styles.css +122 -0
  123. package/apps/ui/tsconfig.json +29 -0
  124. package/apps/ui/vite.config.ts +84 -0
  125. package/bin/spiracha.ts +32 -0
  126. package/package.json +46 -73
  127. package/src/client.ts +290 -0
  128. package/src/lib/antigravity-db.ts +213 -19
  129. package/src/lib/antigravity-exporter-types.ts +1 -0
  130. package/src/lib/codex-analytics.ts +1 -1
  131. package/src/lib/codex-browser-db.ts +410 -74
  132. package/src/lib/codex-browser-export.ts +6 -14
  133. package/src/lib/codex-browser-types.ts +1 -1
  134. package/src/lib/{codex-exporter-types.ts → codex-thread-types.ts} +2 -30
  135. package/src/lib/{codex-exporter-transcript.ts → codex-transcript-renderer.ts} +64 -28
  136. package/src/lib/conversation-api.ts +792 -0
  137. package/src/lib/conversation-data/adapter-helpers.ts +110 -0
  138. package/src/lib/conversation-data/antigravity-adapter.ts +148 -0
  139. package/src/lib/conversation-data/claude-code-adapter.ts +175 -0
  140. package/src/lib/conversation-data/codex-adapter.ts +245 -0
  141. package/src/lib/conversation-data/cursor-adapter.ts +171 -0
  142. package/src/lib/conversation-data/index.ts +311 -0
  143. package/src/lib/conversation-data/kiro-adapter.ts +140 -0
  144. package/src/lib/conversation-data/message-selector.ts +37 -0
  145. package/src/lib/conversation-data/opencode-adapter.ts +180 -0
  146. package/src/lib/conversation-data/path-match.ts +79 -0
  147. package/src/lib/conversation-data/qoder-adapter.ts +230 -0
  148. package/src/lib/conversation-data/types.ts +116 -0
  149. package/src/lib/cursor-db.ts +67 -21
  150. package/src/lib/cursor-exporter-types.ts +0 -19
  151. package/src/lib/qoder-acp-client.ts +268 -0
  152. package/src/lib/qoder-db.ts +1601 -0
  153. package/src/lib/qoder-exporter-types.ts +114 -0
  154. package/src/lib/qoder-transcript-phase.ts +41 -0
  155. package/src/lib/qoder-transcript.ts +112 -0
  156. package/apps/ui/dist/client/assets/_threadId-CAIeH5mq.js +0 -1
  157. package/apps/ui/dist/client/assets/analytics-DOy1b6Sh.js +0 -1
  158. package/apps/ui/dist/client/assets/antigravity-conversations._conversationId-DqySVO2K.js +0 -7
  159. package/apps/ui/dist/client/assets/antigravity-conversations._conversationId-KAcXtNxI.js +0 -1
  160. package/apps/ui/dist/client/assets/antigravity-keychain-panel-B77qJz6f.js +0 -1
  161. package/apps/ui/dist/client/assets/antigravity._workspaceKey-B3NM6gRH.js +0 -1
  162. package/apps/ui/dist/client/assets/antigravity._workspaceKey-ByitGWki.js +0 -1
  163. package/apps/ui/dist/client/assets/antigravity.index-DP9JC2P9.js +0 -1
  164. package/apps/ui/dist/client/assets/antigravity.index-DgW25FEV.js +0 -1
  165. package/apps/ui/dist/client/assets/badge-C3i4OKs8.js +0 -1
  166. package/apps/ui/dist/client/assets/checkbox-CdHitXWQ.js +0 -1
  167. package/apps/ui/dist/client/assets/claude-code-sessions._sessionId-8pQ55ci2.js +0 -1
  168. package/apps/ui/dist/client/assets/claude-code-sessions._sessionId-DVz5UmtU.js +0 -1
  169. package/apps/ui/dist/client/assets/claude-code._workspaceKey-BBKskSum.js +0 -1
  170. package/apps/ui/dist/client/assets/claude-code._workspaceKey-OkikMUFs.js +0 -1
  171. package/apps/ui/dist/client/assets/claude-code.index-Cwu76dbb.js +0 -1
  172. package/apps/ui/dist/client/assets/claude-code.index-D6eMd08t.js +0 -1
  173. package/apps/ui/dist/client/assets/createServerFn-B-KkwmHU.js +0 -3
  174. package/apps/ui/dist/client/assets/cursor-threads._composerId-CmBFclK8.js +0 -1
  175. package/apps/ui/dist/client/assets/cursor-threads._composerId-hBWlP1D-.js +0 -1
  176. package/apps/ui/dist/client/assets/cursor._workspaceKey-BFPkIRi-.js +0 -1
  177. package/apps/ui/dist/client/assets/cursor._workspaceKey-CaAZjGmm.js +0 -1
  178. package/apps/ui/dist/client/assets/cursor.index-8y4o20kb.js +0 -2
  179. package/apps/ui/dist/client/assets/cursor.index-CrnMwDCi.js +0 -1
  180. package/apps/ui/dist/client/assets/data-table-Br78_dk3.js +0 -4
  181. package/apps/ui/dist/client/assets/delete-confirm-dialog-Bxs07u-j.js +0 -7
  182. package/apps/ui/dist/client/assets/dist-B9pyYCVU.js +0 -1
  183. package/apps/ui/dist/client/assets/dist-BrJhN2A0.js +0 -5
  184. package/apps/ui/dist/client/assets/dist-Cmd3AIfD.js +0 -1
  185. package/apps/ui/dist/client/assets/dist-DvMS2965.js +0 -1
  186. package/apps/ui/dist/client/assets/download-Bf0QJKD3.js +0 -1
  187. package/apps/ui/dist/client/assets/dropdown-menu-D5cvPaZt.js +0 -1
  188. package/apps/ui/dist/client/assets/es2015-ad6040dw.js +0 -41
  189. package/apps/ui/dist/client/assets/export-dialog-Dr8d2MqD.js +0 -1
  190. package/apps/ui/dist/client/assets/formatters-BtqyrX_c.js +0 -1
  191. package/apps/ui/dist/client/assets/index-C-STgIH4.js +0 -166
  192. package/apps/ui/dist/client/assets/jsx-runtime-DGeXAQPT.js +0 -1
  193. package/apps/ui/dist/client/assets/kiro-sessions._sessionId-CKgfJ7GR.js +0 -1
  194. package/apps/ui/dist/client/assets/kiro-sessions._sessionId-zvIJHtQF.js +0 -1
  195. package/apps/ui/dist/client/assets/kiro._workspaceKey-DhVuJuZB.js +0 -1
  196. package/apps/ui/dist/client/assets/kiro._workspaceKey-QArsjkTY.js +0 -1
  197. package/apps/ui/dist/client/assets/kiro.index-C-fr4m7i.js +0 -1
  198. package/apps/ui/dist/client/assets/kiro.index-OYgAdtZd.js +0 -1
  199. package/apps/ui/dist/client/assets/metric-card-Dplm0ZiM.js +0 -1
  200. package/apps/ui/dist/client/assets/opencode-sessions._sessionId-DLjiTst-.js +0 -1
  201. package/apps/ui/dist/client/assets/opencode-sessions._sessionId-biBwrRbt.js +0 -8
  202. package/apps/ui/dist/client/assets/opencode._workspaceKey-C7OIzMOJ.js +0 -1
  203. package/apps/ui/dist/client/assets/opencode._workspaceKey-sBakX0oI.js +0 -1
  204. package/apps/ui/dist/client/assets/opencode.index-Bl_kNs9N.js +0 -1
  205. package/apps/ui/dist/client/assets/opencode.index-CN53bVmb.js +0 -1
  206. package/apps/ui/dist/client/assets/page-header-Cwa3p6Tl.js +0 -1
  207. package/apps/ui/dist/client/assets/preload-helper-B8fFrdxC.js +0 -1
  208. package/apps/ui/dist/client/assets/projects._project-D8WloxQN.js +0 -1
  209. package/apps/ui/dist/client/assets/projects._project-DSDwH4NF.js +0 -1
  210. package/apps/ui/dist/client/assets/projects.index-5xRtuJdX.js +0 -3
  211. package/apps/ui/dist/client/assets/projects.index-DEfhWqa8.js +0 -1
  212. package/apps/ui/dist/client/assets/react-dom-DL96Jor4.js +0 -1
  213. package/apps/ui/dist/client/assets/refresh-ccw-BgKjOiA6.js +0 -1
  214. package/apps/ui/dist/client/assets/reload-error-panel-AHqu0akK.js +0 -1
  215. package/apps/ui/dist/client/assets/routes-DnzF_9CX.js +0 -1
  216. package/apps/ui/dist/client/assets/scroll-text-fM0sMOB3.js +0 -1
  217. package/apps/ui/dist/client/assets/select-CYF6pF37.js +0 -1
  218. package/apps/ui/dist/client/assets/settings-Dx70jgP_.js +0 -1
  219. package/apps/ui/dist/client/assets/sqlite-error-B6uZjUe8.js +0 -1
  220. package/apps/ui/dist/client/assets/styles-B7Zi9EFA.css +0 -1
  221. package/apps/ui/dist/client/assets/tabs-CmuhkT-c.js +0 -7
  222. package/apps/ui/dist/client/assets/text-filter-DVAZySUS.js +0 -2
  223. package/apps/ui/dist/client/assets/thread-transcript-stats-BUNz6u7F.js +0 -1
  224. package/apps/ui/dist/client/assets/threads._threadId-BB9tTsV4.js +0 -1
  225. package/apps/ui/dist/client/assets/threads._threadId-BMRSFow3.js +0 -1
  226. package/apps/ui/dist/client/assets/useMutation-BSFult2i.js +0 -1
  227. package/apps/ui/dist/client/assets/useQuery-CjINKZf1.js +0 -1
  228. package/apps/ui/dist/server/assets/_tanstack-start-manifest_v-CQ8Jjs3B.js +0 -387
  229. package/apps/ui/dist/server/assets/_threadId-B6SrBR9E.js +0 -6
  230. package/apps/ui/dist/server/assets/analytics-CYP_LT5Y.js +0 -16
  231. package/apps/ui/dist/server/assets/analytics-CtpOMVMO.js +0 -146
  232. package/apps/ui/dist/server/assets/antigravity-conversation-state-HgzS302O.js +0 -16
  233. package/apps/ui/dist/server/assets/antigravity-conversations._conversationId-BKOyL2T6.js +0 -613
  234. package/apps/ui/dist/server/assets/antigravity-conversations._conversationId-D426O-64.js +0 -11
  235. package/apps/ui/dist/server/assets/antigravity-conversations._conversationId-eS87Dx7g.js +0 -20
  236. package/apps/ui/dist/server/assets/antigravity-db-Bh8_U9uw.js +0 -580
  237. package/apps/ui/dist/server/assets/antigravity-keychain-DOiuHDwK.js +0 -126
  238. package/apps/ui/dist/server/assets/antigravity-keychain-panel-CC7FLmAK.js +0 -55
  239. package/apps/ui/dist/server/assets/antigravity-queries-CGrJO9Vr.js +0 -37
  240. package/apps/ui/dist/server/assets/antigravity-server-CHRjVFqN.js +0 -114
  241. package/apps/ui/dist/server/assets/antigravity._workspaceKey-3m_MzNFA.js +0 -11
  242. package/apps/ui/dist/server/assets/antigravity._workspaceKey-B0QeYUOh.js +0 -28
  243. package/apps/ui/dist/server/assets/antigravity._workspaceKey-C3kP-qyN.js +0 -210
  244. package/apps/ui/dist/server/assets/antigravity.index-CPCYkqCi.js +0 -104
  245. package/apps/ui/dist/server/assets/antigravity.index-DudTB3Tq.js +0 -11
  246. package/apps/ui/dist/server/assets/badge-EvdhKK_Z.js +0 -26
  247. package/apps/ui/dist/server/assets/button-CmTDnzOn.js +0 -46
  248. package/apps/ui/dist/server/assets/checkbox-C0hovF41.js +0 -19
  249. package/apps/ui/dist/server/assets/claude-code-db-Ik3VStKZ.js +0 -361
  250. package/apps/ui/dist/server/assets/claude-code-queries-Cgzi4K-e.js +0 -37
  251. package/apps/ui/dist/server/assets/claude-code-server-PfraClXS.js +0 -72
  252. package/apps/ui/dist/server/assets/claude-code-sessions._sessionId-C55oe6zj.js +0 -18
  253. package/apps/ui/dist/server/assets/claude-code-sessions._sessionId-D1RYYi3j.js +0 -473
  254. package/apps/ui/dist/server/assets/claude-code-sessions._sessionId-DSvPLEUL.js +0 -11
  255. package/apps/ui/dist/server/assets/claude-code-transcript-C-bxh3cS.js +0 -139
  256. package/apps/ui/dist/server/assets/claude-code-transcript-phase-DmUMc8VU.js +0 -10
  257. package/apps/ui/dist/server/assets/claude-code._workspaceKey-B5Xg_-tE.js +0 -28
  258. package/apps/ui/dist/server/assets/claude-code._workspaceKey-BPlzyXPa.js +0 -11
  259. package/apps/ui/dist/server/assets/claude-code._workspaceKey-DqDve2wl.js +0 -191
  260. package/apps/ui/dist/server/assets/claude-code.index-B7yYR7dD.js +0 -92
  261. package/apps/ui/dist/server/assets/claude-code.index-BUHThG1W.js +0 -11
  262. package/apps/ui/dist/server/assets/codex-queries-CtgeZ7VL.js +0 -99
  263. package/apps/ui/dist/server/assets/codex-server-CGXU4H8-.js +0 -2294
  264. package/apps/ui/dist/server/assets/concurrency-VXtYvlGh.js +0 -18
  265. package/apps/ui/dist/server/assets/createServerRpc-CF_DEwnm.js +0 -12
  266. package/apps/ui/dist/server/assets/createSsrRpc-C754NPki.js +0 -16
  267. package/apps/ui/dist/server/assets/cursor-db-CZnYy7aT.js +0 -830
  268. package/apps/ui/dist/server/assets/cursor-exporter-types-CI3goo-c.js +0 -34
  269. package/apps/ui/dist/server/assets/cursor-queries-NCIM0Nat.js +0 -67
  270. package/apps/ui/dist/server/assets/cursor-recovery-CXpO8KcD.js +0 -361
  271. package/apps/ui/dist/server/assets/cursor-server-DNu52A3K.js +0 -213
  272. package/apps/ui/dist/server/assets/cursor-threads._composerId-BB0Y_Mao.js +0 -11
  273. package/apps/ui/dist/server/assets/cursor-threads._composerId-Ckn6ItHI.js +0 -582
  274. package/apps/ui/dist/server/assets/cursor-threads._composerId-p9-ARAzc.js +0 -18
  275. package/apps/ui/dist/server/assets/cursor-transcript-B7tGnqtn.js +0 -125
  276. package/apps/ui/dist/server/assets/cursor._workspaceKey-CZuohIUL.js +0 -28
  277. package/apps/ui/dist/server/assets/cursor._workspaceKey-DPp5JSjy.js +0 -401
  278. package/apps/ui/dist/server/assets/cursor._workspaceKey-nmg3YIOQ.js +0 -11
  279. package/apps/ui/dist/server/assets/cursor.index-CcsX7DG0.js +0 -11
  280. package/apps/ui/dist/server/assets/cursor.index-DkWT-mxw.js +0 -189
  281. package/apps/ui/dist/server/assets/data-table-Cdct823O.js +0 -189
  282. package/apps/ui/dist/server/assets/delete-confirm-dialog-BcBNCxWB.js +0 -144
  283. package/apps/ui/dist/server/assets/download-DMmiy1xf.js +0 -92
  284. package/apps/ui/dist/server/assets/dropdown-menu-Dy_9t6TN.js +0 -36
  285. package/apps/ui/dist/server/assets/empty-plugin-adapters-B3lHh1La.js +0 -5
  286. package/apps/ui/dist/server/assets/export-dialog-D88ze9Gy.js +0 -240
  287. package/apps/ui/dist/server/assets/formatters-FJaGZgJk.js +0 -91
  288. package/apps/ui/dist/server/assets/kiro-db--fXrED_-.js +0 -552
  289. package/apps/ui/dist/server/assets/kiro-queries-CzNlM2ok.js +0 -37
  290. package/apps/ui/dist/server/assets/kiro-server-9WRxg2Au.js +0 -72
  291. package/apps/ui/dist/server/assets/kiro-sessions._sessionId-CRY2c7YO.js +0 -11
  292. package/apps/ui/dist/server/assets/kiro-sessions._sessionId-F1UlwxmR.js +0 -449
  293. package/apps/ui/dist/server/assets/kiro-sessions._sessionId-bvTulT7C.js +0 -18
  294. package/apps/ui/dist/server/assets/kiro-transcript-DoekQBaH.js +0 -112
  295. package/apps/ui/dist/server/assets/kiro-transcript-phase-CJG40nQg.js +0 -27
  296. package/apps/ui/dist/server/assets/kiro._workspaceKey-BDe9sQwo.js +0 -11
  297. package/apps/ui/dist/server/assets/kiro._workspaceKey-CJsjgNwO.js +0 -28
  298. package/apps/ui/dist/server/assets/kiro._workspaceKey-D_-4Qb7y.js +0 -193
  299. package/apps/ui/dist/server/assets/kiro.index-C__-UROk.js +0 -11
  300. package/apps/ui/dist/server/assets/kiro.index-CmTbqeYj.js +0 -93
  301. package/apps/ui/dist/server/assets/loading-panel-BGFnWseS.js +0 -27
  302. package/apps/ui/dist/server/assets/metric-card-ByEeLu0r.js +0 -23
  303. package/apps/ui/dist/server/assets/model-label-B1NWGc65.js +0 -13
  304. package/apps/ui/dist/server/assets/opencode-db-BM7KjOzc.js +0 -397
  305. package/apps/ui/dist/server/assets/opencode-queries-BeNYXnQv.js +0 -50
  306. package/apps/ui/dist/server/assets/opencode-server-BpQ_kRze.js +0 -72
  307. package/apps/ui/dist/server/assets/opencode-sessions._sessionId-CGQ-t0vD.js +0 -518
  308. package/apps/ui/dist/server/assets/opencode-sessions._sessionId-D8Q7in_Z.js +0 -18
  309. package/apps/ui/dist/server/assets/opencode-sessions._sessionId-DKOvi0lV.js +0 -11
  310. package/apps/ui/dist/server/assets/opencode-think-tags-CAoD-EcZ.js +0 -87
  311. package/apps/ui/dist/server/assets/opencode-transcript-BNTcGNym.js +0 -141
  312. package/apps/ui/dist/server/assets/opencode-transcript-phase-OqLkiQD0.js +0 -32
  313. package/apps/ui/dist/server/assets/opencode._workspaceKey-BdqcWHw1.js +0 -11
  314. package/apps/ui/dist/server/assets/opencode._workspaceKey-CfH96HTT.js +0 -28
  315. package/apps/ui/dist/server/assets/opencode._workspaceKey-DdbHPDrO.js +0 -207
  316. package/apps/ui/dist/server/assets/opencode.index-BbVzkJXP.js +0 -11
  317. package/apps/ui/dist/server/assets/opencode.index-Vh_Gq_1L.js +0 -93
  318. package/apps/ui/dist/server/assets/page-header-VNSaM3xd.js +0 -29
  319. package/apps/ui/dist/server/assets/path-transforms-DL2IwtYd.js +0 -31
  320. package/apps/ui/dist/server/assets/projects._project-4io5LO0E.js +0 -20
  321. package/apps/ui/dist/server/assets/projects._project-Bshqk7JA.js +0 -12
  322. package/apps/ui/dist/server/assets/projects._project-D6ZoGhJ8.js +0 -395
  323. package/apps/ui/dist/server/assets/projects.index-BLXOx5eL.js +0 -12
  324. package/apps/ui/dist/server/assets/projects.index-BkLiF2FF.js +0 -182
  325. package/apps/ui/dist/server/assets/projects.index-DesYXwfi.js +0 -14
  326. package/apps/ui/dist/server/assets/reload-error-panel-BJMxY3U1.js +0 -25
  327. package/apps/ui/dist/server/assets/route-search-ts4W9MJ9.js +0 -38
  328. package/apps/ui/dist/server/assets/router-Sac2DGk6.js +0 -518
  329. package/apps/ui/dist/server/assets/routes-Bsact1uB.js +0 -184
  330. package/apps/ui/dist/server/assets/routes-BzkJgq4W.js +0 -34
  331. package/apps/ui/dist/server/assets/select-GW76p-ld.js +0 -76
  332. package/apps/ui/dist/server/assets/settings-OayxIYQQ.js +0 -100
  333. package/apps/ui/dist/server/assets/settings-store-DpEJEQ7M.js +0 -52
  334. package/apps/ui/dist/server/assets/shared-DyhChtHf.js +0 -137
  335. package/apps/ui/dist/server/assets/source-session-export-server-DNutCawb.js +0 -38
  336. package/apps/ui/dist/server/assets/sqlite-error-LZDrnxdd.js +0 -13
  337. package/apps/ui/dist/server/assets/start-B_HFwkkk.js +0 -4
  338. package/apps/ui/dist/server/assets/tabs-DkaChNVg.js +0 -501
  339. package/apps/ui/dist/server/assets/text-filter-CGKxMCKt.js +0 -36
  340. package/apps/ui/dist/server/assets/thread-transcript-stats-Dh34mt2u.js +0 -45
  341. package/apps/ui/dist/server/assets/threads._threadId-BSSK4nkI.js +0 -26
  342. package/apps/ui/dist/server/assets/threads._threadId-BeXEc4Lo.js +0 -648
  343. package/apps/ui/dist/server/assets/threads._threadId-Ci0S4mKU.js +0 -18
  344. package/apps/ui/dist/server/assets/ui-export-files-BHLX9bhN.js +0 -83
  345. package/apps/ui/dist/server/assets/utils-C_uf36nf.js +0 -8
  346. package/apps/ui/dist/server/server.js +0 -5895
  347. package/bin/codex-chats-claude.js +0 -5
  348. package/bin/codex-chats.js +0 -5
  349. package/bin/spiracha.js +0 -5
  350. package/src/export-chats.ts +0 -120
  351. package/src/export-claude.ts +0 -36
  352. package/src/export-cursor.ts +0 -244
  353. package/src/lib/claude-exporter.ts +0 -864
  354. package/src/lib/codex-exporter-cli.ts +0 -277
  355. package/src/lib/codex-exporter-db.ts +0 -319
  356. package/src/lib/codex-exporter.ts +0 -115
  357. package/src/lib/cursor-exporter.ts +0 -266
  358. package/src/lib/interactive-cli.ts +0 -433
  359. package/src/lib/native-open.ts +0 -54
  360. package/src/mcp-server.ts +0 -137
  361. package/src/spiracha.ts +0 -116
  362. package/src/ui-cli.ts +0 -310
  363. /package/apps/ui/{dist/client → public}/icon.svg +0 -0
  364. /package/apps/ui/{dist/client → public}/manifest.json +0 -0
  365. /package/apps/ui/{dist/client → public}/robots.txt +0 -0
@@ -0,0 +1,1601 @@
1
+ import { Database } from 'bun:sqlite';
2
+ import { readdir, stat } from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import { mapWithConcurrency } from './concurrency';
5
+ import { loadQoderAcpSession, type QoderAcpSessionUpdate, resolveQoderAcpSocketPath } from './qoder-acp-client';
6
+ import {
7
+ getDefaultQoderUserDir,
8
+ type QoderSessionSummary,
9
+ type QoderSessionTranscript,
10
+ type QoderTranscriptEntry,
11
+ type QoderTranscriptPart,
12
+ type QoderWorkspaceGroup,
13
+ resolveQoderCliProjectsDir,
14
+ resolveQoderGlobalStateDb,
15
+ resolveQoderWorkspaceStorageDir,
16
+ } from './qoder-exporter-types';
17
+ import {
18
+ asObject,
19
+ asString,
20
+ cleanExtractedText,
21
+ cleanInlineTitle,
22
+ getPortablePathBasename,
23
+ isWorkspacePathQuery,
24
+ type JsonValue,
25
+ workspacePathMatchesQuery,
26
+ } from './shared';
27
+
28
+ export {
29
+ getDefaultQoderUserDir,
30
+ resolveQoderCliProjectsDir,
31
+ resolveQoderGlobalStateDb,
32
+ resolveQoderWorkspaceStorageDir,
33
+ };
34
+
35
+ const READ_CONCURRENCY = 8;
36
+ const WORKSPACE_KEY_PREFIX = 'workspace:';
37
+ const LOCAL_HISTORY_KEY_PATTERN = /^lingma\.chat\.localHistory\.(.+)\.quest$/u;
38
+ const MODEL_CONFIG_KEYS = ['aicoding.modelConfigs.cache.assistant', 'aicoding.modelConfigs.cache.quest'] as const;
39
+ const ACP_RECENT_SESSION_WINDOW_MS = 24 * 60 * 60 * 1000;
40
+ const QODER_MODEL_LABELS: Record<string, string> = {
41
+ dfmodel: 'DeepSeek V4 Flash',
42
+ dmodel: 'DeepSeek V4 Pro',
43
+ gm51model: 'GLM 5.2',
44
+ gmodel: 'GLM 5',
45
+ kmodel: 'Kimi K2.7 Code',
46
+ mmodel: 'MiniMax M3',
47
+ q35model: 'Qwen 3.5 Plus',
48
+ q35model_preview: 'Qwen 3.7 Max DogFooding',
49
+ qmodel: 'Qwen 3.7 Plus',
50
+ qmodel_latest: 'Qwen 3.7 Max',
51
+ };
52
+
53
+ type ItemTableRow = {
54
+ key: string;
55
+ value: string;
56
+ };
57
+
58
+ type QoderHistoryEntry = {
59
+ historyKey: string;
60
+ id: string;
61
+ raw: Record<string, JsonValue>;
62
+ sessionId: string;
63
+ timestampMs: number | null;
64
+ text: string;
65
+ title: string;
66
+ workspaceStorageId: string;
67
+ };
68
+
69
+ type QoderTaskEntry = {
70
+ agentClass: string | null;
71
+ createdAtMs: number | null;
72
+ executionMode: string | null;
73
+ executionRequestId: string | null;
74
+ id: string;
75
+ model: string | null;
76
+ query: string | null;
77
+ raw: Record<string, JsonValue>;
78
+ sessionIds: string[];
79
+ status: string | null;
80
+ title: string | null;
81
+ updatedAtMs: number | null;
82
+ workspacePath: string;
83
+ };
84
+
85
+ type QoderSessionRecord = {
86
+ histories: QoderHistoryEntry[];
87
+ sessionId: string;
88
+ task: QoderTaskEntry | null;
89
+ worktree: string;
90
+ workspaceStorageId: string | null;
91
+ };
92
+
93
+ type QoderStateData = {
94
+ fileOperationCount: number;
95
+ lastActiveAtMs: number | null;
96
+ rawState: Record<string, JsonValue> | null;
97
+ requestId: string | null;
98
+ snapshotFileCount: number;
99
+ statePath: string | null;
100
+ };
101
+
102
+ type SessionStats = {
103
+ assistantMessageCount: number;
104
+ fileOperationCount: number;
105
+ messageCount: number;
106
+ renderablePartCount: number;
107
+ snapshotFileCount: number;
108
+ userMessageCount: number;
109
+ };
110
+
111
+ type QoderModelConfigState = {
112
+ assistantDefaultModel: string | null;
113
+ questDefaultModel: string | null;
114
+ };
115
+
116
+ type QoderDataRecords = {
117
+ modelConfig: QoderModelConfigState;
118
+ records: QoderSessionRecord[];
119
+ workspaceStorageIds: string[];
120
+ };
121
+
122
+ type QoderTranscriptReadOptions = {
123
+ acpDrainMs?: number;
124
+ acpSocketPath?: string | null;
125
+ acpTimeoutMs?: number;
126
+ enableAcp?: boolean;
127
+ };
128
+
129
+ const pathExists = async (target: string): Promise<boolean> => {
130
+ return await stat(target)
131
+ .then(() => true)
132
+ .catch(() => false);
133
+ };
134
+
135
+ const toIso = (value: number | null): string | null => {
136
+ return value === null ? null : new Date(value).toISOString();
137
+ };
138
+
139
+ const parseTimestampMs = (value: JsonValue | undefined): number | null => {
140
+ if (typeof value === 'number' && Number.isFinite(value)) {
141
+ // Qoder stores some task timestamps in Unix seconds and others in epoch milliseconds.
142
+ return value > 0 && value < 10_000_000_000 ? value * 1000 : value;
143
+ }
144
+
145
+ if (typeof value === 'string') {
146
+ const numeric = Number(value);
147
+ if (Number.isFinite(numeric)) {
148
+ // Match the numeric branch so seconds-like string values sort correctly.
149
+ return numeric > 0 && numeric < 10_000_000_000 ? numeric * 1000 : numeric;
150
+ }
151
+
152
+ const parsed = Date.parse(value);
153
+ return Number.isFinite(parsed) ? parsed : null;
154
+ }
155
+
156
+ return null;
157
+ };
158
+
159
+ const cleanLabel = (value: string | null | undefined): string | null => {
160
+ const cleaned = value?.replace(/\s+/g, ' ').trim();
161
+ return cleaned ? cleaned : null;
162
+ };
163
+
164
+ const normalizePromptText = (value: string | null | undefined): string | null => {
165
+ const cleaned = value
166
+ ?.replace(/\r\n?/gu, '\n')
167
+ .replace(/\\r\\n|\\n/gu, '\n')
168
+ .trimEnd();
169
+ return cleaned ? cleaned : null;
170
+ };
171
+
172
+ const getWorkspaceKey = (worktree: string): string => {
173
+ return `${WORKSPACE_KEY_PREFIX}${Buffer.from(worktree, 'utf8').toString('base64url')}`;
174
+ };
175
+
176
+ const warnQoder = (event: string, details: Record<string, JsonValue>) => {
177
+ console.warn(`[spiracha:qoder] ${event}`, details);
178
+ };
179
+
180
+ const getWorktreeFromWorkspaceKey = (workspaceKey: string): string | null => {
181
+ if (!workspaceKey.startsWith(WORKSPACE_KEY_PREFIX)) {
182
+ warnQoder('invalid_workspace_key', { workspaceKey });
183
+ return null;
184
+ }
185
+
186
+ try {
187
+ const decoded = Buffer.from(workspaceKey.slice(WORKSPACE_KEY_PREFIX.length), 'base64url').toString('utf8');
188
+ if (!decoded || decoded.includes('\uFFFD')) {
189
+ warnQoder('invalid_workspace_key', { workspaceKey });
190
+ return null;
191
+ }
192
+
193
+ return decoded;
194
+ } catch {
195
+ warnQoder('invalid_workspace_key', { workspaceKey });
196
+ return null;
197
+ }
198
+ };
199
+
200
+ const getWorkspaceLabel = (worktree: string): string => {
201
+ return getPortablePathBasename(worktree) || worktree;
202
+ };
203
+
204
+ const getWorkspaceUri = (worktree: string): string => {
205
+ return worktree.startsWith(path.sep) ? `file://${worktree}` : worktree;
206
+ };
207
+
208
+ const parseJsonValue = (value: string): JsonValue | null => {
209
+ try {
210
+ return JSON.parse(value) as JsonValue;
211
+ } catch {
212
+ return null;
213
+ }
214
+ };
215
+
216
+ const readGlobalRows = async (globalStateDb = resolveQoderGlobalStateDb()): Promise<ItemTableRow[]> => {
217
+ if (!(await pathExists(globalStateDb))) {
218
+ return [];
219
+ }
220
+
221
+ let db: Database | null = null;
222
+ try {
223
+ db = new Database(globalStateDb, { readonly: true, strict: true });
224
+ return db
225
+ .query(
226
+ "select key, value from ItemTable where key like 'lingma.chat.localHistory.%.quest' or key = 'aicoding.questTaskListSnapshot' or key in ('aicoding.modelConfigs.cache.assistant', 'aicoding.modelConfigs.cache.quest')",
227
+ )
228
+ .all() as ItemTableRow[];
229
+ } catch {
230
+ return [];
231
+ } finally {
232
+ db?.close();
233
+ }
234
+ };
235
+
236
+ const asJsonObject = (value: JsonValue | null): Record<string, JsonValue> | null => {
237
+ return value === null ? null : asObject(value);
238
+ };
239
+
240
+ const parseHistoryRows = (rows: ItemTableRow[]): QoderHistoryEntry[] => {
241
+ const histories: QoderHistoryEntry[] = [];
242
+
243
+ for (const row of rows) {
244
+ const match = LOCAL_HISTORY_KEY_PATTERN.exec(row.key);
245
+ if (!match) {
246
+ continue;
247
+ }
248
+
249
+ const workspaceStorageId = match[1] ?? '';
250
+ const value = parseJsonValue(row.value);
251
+ const items = Array.isArray(value) ? value : [];
252
+
253
+ items.forEach((item, index) => {
254
+ const raw = asJsonObject(item);
255
+ const sessionId = asString(raw?.sessionId ?? null);
256
+ if (!raw || !sessionId) {
257
+ return;
258
+ }
259
+
260
+ const id = asString(raw.id ?? null) ?? `${workspaceStorageId}:${index}`;
261
+ const rawTitle = asString(raw.title ?? null);
262
+ const title = cleanLabel(rawTitle) ?? sessionId;
263
+ histories.push({
264
+ historyKey: row.key,
265
+ id,
266
+ raw,
267
+ sessionId,
268
+ text: normalizePromptText(rawTitle) ?? title,
269
+ timestampMs: parseTimestampMs(raw.timestamp),
270
+ title,
271
+ workspaceStorageId,
272
+ });
273
+ });
274
+ }
275
+
276
+ return histories;
277
+ };
278
+
279
+ const decodeFileUri = (value: string): string | null => {
280
+ if (!value.startsWith('file://')) {
281
+ return null;
282
+ }
283
+
284
+ try {
285
+ return decodeURIComponent(new URL(value).pathname);
286
+ } catch {
287
+ return null;
288
+ }
289
+ };
290
+
291
+ const getStringValue = (raw: Record<string, JsonValue>, keys: string[]): string | null => {
292
+ for (const key of keys) {
293
+ const value = cleanLabel(asString(raw[key] ?? null));
294
+ if (value) {
295
+ return value;
296
+ }
297
+ }
298
+
299
+ return null;
300
+ };
301
+
302
+ const parseJsonObjectString = (value: JsonValue | undefined): Record<string, JsonValue> | null => {
303
+ const text = asString(value ?? null);
304
+ return text ? asJsonObject(parseJsonValue(text)) : null;
305
+ };
306
+
307
+ const normalizeQoderModelLabel = (value: string | null): string | null => {
308
+ return value ? (QODER_MODEL_LABELS[value] ?? value) : null;
309
+ };
310
+
311
+ const getTaskModel = (raw: Record<string, JsonValue>): string | null => {
312
+ const directModel = getStringValue(raw, [
313
+ 'model',
314
+ 'modelLabel',
315
+ 'modelName',
316
+ 'modelId',
317
+ 'modelVersion',
318
+ 'selectedModel',
319
+ ]);
320
+ if (directModel) {
321
+ return normalizeQoderModelLabel(directModel);
322
+ }
323
+
324
+ const executionSessionExtra = parseJsonObjectString(raw.executionSessionExtra);
325
+ const questTaskInfo = asObject(executionSessionExtra?.questTaskInfo ?? null);
326
+ const executionConfig = asObject(questTaskInfo?.executionConfig ?? null);
327
+ return normalizeQoderModelLabel(
328
+ getStringValue(questTaskInfo ?? {}, ['model', 'modelLabel', 'modelName', 'modelId', 'selectedModel']) ??
329
+ getStringValue(executionConfig ?? {}, ['model', 'modelLabel', 'modelName', 'modelId', 'selectedModel']),
330
+ );
331
+ };
332
+
333
+ const getTimestampValue = (raw: Record<string, JsonValue>, keys: string[]): number | null => {
334
+ for (const key of keys) {
335
+ const value = parseTimestampMs(raw[key]);
336
+ if (value !== null) {
337
+ return value;
338
+ }
339
+ }
340
+
341
+ return null;
342
+ };
343
+
344
+ const getWorkspacePathFromTask = (folder: string, task: Record<string, JsonValue>): string => {
345
+ const workspaceUri = asString(task.workspaceUri ?? null);
346
+ const decodedUri = workspaceUri ? decodeFileUri(workspaceUri) : null;
347
+ return decodedUri ?? folder;
348
+ };
349
+
350
+ const getTaskSessionIds = (raw: Record<string, JsonValue>, id: string): string[] => {
351
+ const executionSessionId = asString(raw.executionSessionId ?? null);
352
+ const designSessionId = asString(raw.designSessionId ?? null);
353
+ return [
354
+ ...new Set([executionSessionId, `${id}.session.execution`, designSessionId, id].filter(Boolean)),
355
+ ] as string[];
356
+ };
357
+
358
+ const parseTaskItem = (folder: string, item: JsonValue): QoderTaskEntry | null => {
359
+ const raw = asJsonObject(item);
360
+ const id = asString(raw?.id ?? null);
361
+ if (!raw || !id) {
362
+ return null;
363
+ }
364
+
365
+ return {
366
+ agentClass: getStringValue(raw, ['agentClass']),
367
+ createdAtMs: getTimestampValue(raw, ['createdAt', 'createTime']),
368
+ executionMode: getStringValue(raw, ['executionMode', 'questType']),
369
+ executionRequestId: getStringValue(raw, ['executionRequestId', 'designRequestId']),
370
+ id,
371
+ model: getTaskModel(raw),
372
+ query: getStringValue(raw, ['query', 'userRequirements']),
373
+ raw,
374
+ sessionIds: getTaskSessionIds(raw, id),
375
+ status: getStringValue(raw, ['status', 'bootStatus', 'prevStatus']),
376
+ title: getStringValue(raw, ['title', 'name', 'query']),
377
+ updatedAtMs: getTimestampValue(raw, [
378
+ 'updatedAtTimestamp',
379
+ 'executeEndAt',
380
+ 'executeEndTime',
381
+ 'finishedAt',
382
+ 'endTime',
383
+ 'lastUserQueryAt',
384
+ 'createdAt',
385
+ 'createTime',
386
+ ]),
387
+ workspacePath: getWorkspacePathFromTask(folder, raw),
388
+ };
389
+ };
390
+
391
+ const parseFolderTasks = (folder: string, value: JsonValue): QoderTaskEntry[] => {
392
+ const folderEntry = asObject(value);
393
+ const folderTasks = Array.isArray(folderEntry?.tasks) ? folderEntry.tasks : [];
394
+ return folderTasks.flatMap((item) => {
395
+ const task = parseTaskItem(folder, item);
396
+ return task ? [task] : [];
397
+ });
398
+ };
399
+
400
+ const parseTaskSnapshotRows = (rows: ItemTableRow[]): QoderTaskEntry[] => {
401
+ const snapshotRow = rows.find((row) => row.key === 'aicoding.questTaskListSnapshot');
402
+ const snapshot = asJsonObject(snapshotRow ? parseJsonValue(snapshotRow.value) : null);
403
+ const folders = asObject(snapshot?.folders ?? null);
404
+ if (!folders) {
405
+ return [];
406
+ }
407
+
408
+ return Object.entries(folders).flatMap(([folder, value]) => parseFolderTasks(folder, value));
409
+ };
410
+
411
+ const getConfiguredModelName = (value: JsonValue): string | null => {
412
+ const config = asObject(value);
413
+ return getStringValue(config ?? {}, ['key', 'name', 'model', 'modelId']);
414
+ };
415
+
416
+ const parseDefaultModelConfig = (rows: ItemTableRow[], key: (typeof MODEL_CONFIG_KEYS)[number]): string | null => {
417
+ const row = rows.find((item) => item.key === key);
418
+ const configs = row ? parseJsonValue(row.value) : null;
419
+ if (!Array.isArray(configs)) {
420
+ return null;
421
+ }
422
+
423
+ const selected =
424
+ configs.find((item) => {
425
+ const config = asObject(item);
426
+ return config?.selected === true;
427
+ }) ??
428
+ configs.find((item) => {
429
+ const config = asObject(item);
430
+ return config?.enabled === true && config?.isDefault === true;
431
+ }) ??
432
+ configs.find((item) => {
433
+ const config = asObject(item);
434
+ return config?.isDefault === true;
435
+ });
436
+
437
+ return normalizeQoderModelLabel(getConfiguredModelName(selected ?? null));
438
+ };
439
+
440
+ const parseModelConfigState = (rows: ItemTableRow[]): QoderModelConfigState => ({
441
+ assistantDefaultModel: parseDefaultModelConfig(rows, 'aicoding.modelConfigs.cache.assistant'),
442
+ questDefaultModel: parseDefaultModelConfig(rows, 'aicoding.modelConfigs.cache.quest'),
443
+ });
444
+
445
+ const getModelFallback = (modelConfig: QoderModelConfigState): string | null => {
446
+ return modelConfig.questDefaultModel ?? modelConfig.assistantDefaultModel;
447
+ };
448
+
449
+ const stripTrailingPathPunctuation = (value: string): string => {
450
+ return value.replace(/[),.;\]]+$/u, '');
451
+ };
452
+
453
+ const inferWorkspaceFromAbsolutePath = (value: string): string | null => {
454
+ const normalized = stripTrailingPathPunctuation(value);
455
+ const marker = `${path.sep}workspace${path.sep}`;
456
+ const markerIndex = normalized.indexOf(marker);
457
+ if (markerIndex >= 0) {
458
+ const prefixEnd = markerIndex + marker.length;
459
+ const nextSeparator = normalized.indexOf(path.sep, prefixEnd);
460
+ return nextSeparator >= 0 ? normalized.slice(0, nextSeparator) : normalized;
461
+ }
462
+
463
+ return path.dirname(normalized);
464
+ };
465
+
466
+ const inferWorkspaceFromText = (value: string | null): string | null => {
467
+ if (!value) {
468
+ return null;
469
+ }
470
+
471
+ const matches = value.match(/\/[^\s"'`)\]]+/gu) ?? [];
472
+ for (const match of matches) {
473
+ const workspace = inferWorkspaceFromAbsolutePath(match);
474
+ if (workspace) {
475
+ return workspace;
476
+ }
477
+ }
478
+
479
+ return null;
480
+ };
481
+
482
+ const findTaskForSession = (
483
+ tasksBySessionId: Map<string, QoderTaskEntry>,
484
+ sessionId: string,
485
+ ): QoderTaskEntry | null => {
486
+ if (tasksBySessionId.has(sessionId)) {
487
+ return tasksBySessionId.get(sessionId) ?? null;
488
+ }
489
+
490
+ if (sessionId.endsWith('.session.execution')) {
491
+ return tasksBySessionId.get(sessionId.replace(/\.session\.execution$/u, '')) ?? null;
492
+ }
493
+
494
+ return tasksBySessionId.get(`${sessionId}.session.execution`) ?? null;
495
+ };
496
+
497
+ const buildTasksBySessionId = (tasks: QoderTaskEntry[]): Map<string, QoderTaskEntry> => {
498
+ const tasksBySessionId = new Map<string, QoderTaskEntry>();
499
+ for (const task of tasks) {
500
+ for (const sessionId of task.sessionIds) {
501
+ tasksBySessionId.set(sessionId, task);
502
+ }
503
+ }
504
+ return tasksBySessionId;
505
+ };
506
+
507
+ const groupHistoriesBySessionId = (histories: QoderHistoryEntry[]): Map<string, QoderHistoryEntry[]> => {
508
+ const historiesBySessionId = new Map<string, QoderHistoryEntry[]>();
509
+ for (const history of histories) {
510
+ const sessionHistories = historiesBySessionId.get(history.sessionId) ?? [];
511
+ sessionHistories.push(history);
512
+ historiesBySessionId.set(history.sessionId, sessionHistories);
513
+ }
514
+ return historiesBySessionId;
515
+ };
516
+
517
+ const sortHistories = (histories: QoderHistoryEntry[]): QoderHistoryEntry[] => {
518
+ return [...histories].sort(
519
+ (left, right) => (left.timestampMs ?? 0) - (right.timestampMs ?? 0) || left.id.localeCompare(right.id),
520
+ );
521
+ };
522
+
523
+ const createRecordFromHistories = (
524
+ sessionId: string,
525
+ sessionHistories: QoderHistoryEntry[],
526
+ task: QoderTaskEntry | null,
527
+ ): QoderSessionRecord => {
528
+ const histories = sortHistories(sessionHistories);
529
+ const inferredWorktree =
530
+ task?.workspacePath ?? inferWorkspaceFromText(histories.map((history) => history.text).join('\n'));
531
+ const workspaceStorageId = histories[0]?.workspaceStorageId ?? null;
532
+
533
+ return {
534
+ histories,
535
+ sessionId,
536
+ task,
537
+ workspaceStorageId,
538
+ worktree: inferredWorktree ?? `Qoder workspace ${workspaceStorageId ?? 'unknown'}`,
539
+ };
540
+ };
541
+
542
+ const createTaskOnlyRecords = (tasks: QoderTaskEntry[], usedTaskIds: Set<string>): QoderSessionRecord[] => {
543
+ return tasks.flatMap((task) => {
544
+ if (usedTaskIds.has(task.id)) {
545
+ return [];
546
+ }
547
+
548
+ return [
549
+ {
550
+ histories: [],
551
+ sessionId: task.sessionIds[0] ?? task.id,
552
+ task,
553
+ workspaceStorageId: null,
554
+ worktree: task.workspacePath,
555
+ },
556
+ ];
557
+ });
558
+ };
559
+
560
+ const groupRecords = (histories: QoderHistoryEntry[], tasks: QoderTaskEntry[]): QoderSessionRecord[] => {
561
+ const tasksBySessionId = buildTasksBySessionId(tasks);
562
+ const usedTaskIds = new Set<string>();
563
+ const records = [...groupHistoriesBySessionId(histories).entries()].map(([sessionId, sessionHistories]) => {
564
+ const task = findTaskForSession(tasksBySessionId, sessionId);
565
+ if (task) {
566
+ usedTaskIds.add(task.id);
567
+ }
568
+
569
+ return createRecordFromHistories(sessionId, sessionHistories, task);
570
+ });
571
+
572
+ return [...records, ...createTaskOnlyRecords(tasks, usedTaskIds)];
573
+ };
574
+
575
+ const listWorkspaceStorageIds = async (workspaceStorageDir: string): Promise<string[]> => {
576
+ const entries = await readdir(workspaceStorageDir, { withFileTypes: true }).catch(() => []);
577
+ return entries
578
+ .filter((entry) => entry.isDirectory())
579
+ .map((entry) => entry.name)
580
+ .sort();
581
+ };
582
+
583
+ const getStateDirectoryCandidates = (sessionId: string): string[] => {
584
+ const candidates = [sessionId];
585
+ if (sessionId.endsWith('.session.execution')) {
586
+ candidates.push(sessionId.replace(/\.session\.execution$/u, ''));
587
+ } else {
588
+ candidates.push(`${sessionId}.session.execution`);
589
+ }
590
+
591
+ return [...new Set(candidates)];
592
+ };
593
+
594
+ const locateStatePath = async (
595
+ workspaceStorageDir: string,
596
+ workspaceStorageIds: string[],
597
+ record: QoderSessionRecord,
598
+ ): Promise<{ statePath: string | null; workspaceStorageId: string | null }> => {
599
+ const storageIds = record.workspaceStorageId
600
+ ? [record.workspaceStorageId, ...workspaceStorageIds.filter((id) => id !== record.workspaceStorageId)]
601
+ : workspaceStorageIds;
602
+
603
+ for (const workspaceStorageId of storageIds) {
604
+ for (const directoryName of getStateDirectoryCandidates(record.sessionId)) {
605
+ const statePath = path.join(
606
+ workspaceStorageDir,
607
+ workspaceStorageId,
608
+ 'chatEditingSessions',
609
+ directoryName,
610
+ 'state.json',
611
+ );
612
+ if (await pathExists(statePath)) {
613
+ return { statePath, workspaceStorageId };
614
+ }
615
+ }
616
+ }
617
+
618
+ return { statePath: null, workspaceStorageId: record.workspaceStorageId };
619
+ };
620
+
621
+ const readJsonObject = async (filePath: string): Promise<Record<string, JsonValue> | null> => {
622
+ const value = (await Bun.file(filePath)
623
+ .json()
624
+ .catch(() => null)) as JsonValue | null;
625
+ return asJsonObject(value);
626
+ };
627
+
628
+ const readStateData = async (
629
+ workspaceStorageDir: string,
630
+ workspaceStorageIds: string[],
631
+ record: QoderSessionRecord,
632
+ ): Promise<QoderStateData> => {
633
+ const located = await locateStatePath(workspaceStorageDir, workspaceStorageIds, record);
634
+ if (!located.statePath) {
635
+ return {
636
+ fileOperationCount: 0,
637
+ lastActiveAtMs: null,
638
+ rawState: null,
639
+ requestId: null,
640
+ snapshotFileCount: 0,
641
+ statePath: null,
642
+ };
643
+ }
644
+
645
+ const rawState = await readJsonObject(located.statePath);
646
+ const stateStats = await stat(located.statePath)
647
+ .then((stats) => ({ mtimeMs: stats.mtimeMs }))
648
+ .catch(() => null);
649
+ const timeline = asObject(rawState?.timeline ?? null);
650
+ const operations = Array.isArray(timeline?.operations) ? timeline.operations : [];
651
+ const recentSnapshot = asObject(rawState?.recentSnapshot ?? null);
652
+ const snapshots = Array.isArray(recentSnapshot?.entries) ? recentSnapshot.entries : [];
653
+ const requestId =
654
+ operations
655
+ .map((operation) => asString(asObject(operation)?.requestId ?? null))
656
+ .find((value): value is string => Boolean(value)) ??
657
+ snapshots
658
+ .map((snapshot) => asString(asObject(asObject(snapshot)?.telemetryInfo ?? null)?.requestId ?? null))
659
+ .find((value): value is string => Boolean(value)) ??
660
+ null;
661
+
662
+ return {
663
+ fileOperationCount: operations.length,
664
+ lastActiveAtMs: stateStats?.mtimeMs ?? null,
665
+ rawState,
666
+ requestId,
667
+ snapshotFileCount: snapshots.length,
668
+ statePath: located.statePath,
669
+ };
670
+ };
671
+
672
+ const createEmptyStats = (): SessionStats => ({
673
+ assistantMessageCount: 0,
674
+ fileOperationCount: 0,
675
+ messageCount: 0,
676
+ renderablePartCount: 0,
677
+ snapshotFileCount: 0,
678
+ userMessageCount: 0,
679
+ });
680
+
681
+ const isRenderablePart = (part: QoderTranscriptPart): boolean => {
682
+ return part.type === 'text' && Boolean(part.text?.trim());
683
+ };
684
+
685
+ const updateStatsFromEntry = (stats: SessionStats, entry: QoderTranscriptEntry) => {
686
+ if (entry.entryType === 'tool_call') {
687
+ stats.fileOperationCount += 1;
688
+ }
689
+
690
+ if (entry.role === 'assistant' || entry.role === 'user') {
691
+ stats.messageCount += 1;
692
+ }
693
+
694
+ if (entry.role === 'assistant') {
695
+ stats.assistantMessageCount += 1;
696
+ }
697
+
698
+ if (entry.role === 'user') {
699
+ stats.userMessageCount += 1;
700
+ }
701
+
702
+ stats.renderablePartCount += entry.parts.filter(isRenderablePart).length;
703
+ };
704
+
705
+ const createStatsFromEntries = (entries: QoderTranscriptEntry[], snapshotFileCount: number): SessionStats => {
706
+ const stats = createEmptyStats();
707
+ stats.snapshotFileCount = snapshotFileCount;
708
+ for (const entry of entries) {
709
+ updateStatsFromEntry(stats, entry);
710
+ }
711
+ return stats;
712
+ };
713
+
714
+ const getRecordTitle = (record: QoderSessionRecord): string => {
715
+ return cleanInlineTitle(record.task?.title ?? record.histories[0]?.title ?? record.sessionId);
716
+ };
717
+
718
+ const maxNullable = (...values: (number | null)[]): number | null => {
719
+ return values.reduce<number | null>((latest, value) => {
720
+ if (value === null) {
721
+ return latest;
722
+ }
723
+
724
+ return latest === null ? value : Math.max(latest, value);
725
+ }, null);
726
+ };
727
+
728
+ const getCreatedAtMs = (record: QoderSessionRecord): number | null => {
729
+ const historyCreatedAt = record.histories.reduce<number | null>((earliest, history) => {
730
+ if (history.timestampMs === null) {
731
+ return earliest;
732
+ }
733
+
734
+ return earliest === null ? history.timestampMs : Math.min(earliest, history.timestampMs);
735
+ }, null);
736
+ return record.task?.createdAtMs ?? historyCreatedAt;
737
+ };
738
+
739
+ const getLastActiveAtMs = (record: QoderSessionRecord, state: QoderStateData): number | null => {
740
+ const latestHistoryAt = record.histories.reduce<number | null>((latest, history) => {
741
+ return maxNullable(latest, history.timestampMs);
742
+ }, null);
743
+ return maxNullable(record.task?.updatedAtMs ?? null, latestHistoryAt, state.lastActiveAtMs);
744
+ };
745
+
746
+ const toSessionSummary = (
747
+ record: QoderSessionRecord,
748
+ state: QoderStateData,
749
+ stats: SessionStats,
750
+ modelFallback: string | null = null,
751
+ ): QoderSessionSummary => {
752
+ const createdAtMs = getCreatedAtMs(record);
753
+ const lastActiveAtMs = getLastActiveAtMs(record, state);
754
+ const workspaceLabel = getWorkspaceLabel(record.worktree);
755
+
756
+ return {
757
+ ...stats,
758
+ agentClass: record.task?.agentClass ?? null,
759
+ createdAtIso: toIso(createdAtMs),
760
+ createdAtMs,
761
+ executionMode: record.task?.executionMode ?? null,
762
+ historyIds: record.histories.map((history) => history.id),
763
+ lastActiveAtIso: toIso(lastActiveAtMs),
764
+ lastActiveAtMs,
765
+ model: record.task?.model ?? modelFallback,
766
+ query: record.task?.query ?? record.histories[0]?.title ?? null,
767
+ requestId: state.requestId ?? record.task?.executionRequestId ?? null,
768
+ sessionId: record.sessionId,
769
+ sourceStatePath: state.statePath,
770
+ status: record.task?.status ?? null,
771
+ taskId: record.task?.id ?? null,
772
+ title: getRecordTitle(record),
773
+ workspaceKey: getWorkspaceKey(record.worktree),
774
+ workspaceLabel,
775
+ workspacePath: record.worktree.startsWith(path.sep) ? record.worktree : null,
776
+ workspaceStorageId: record.workspaceStorageId,
777
+ worktree: record.worktree,
778
+ };
779
+ };
780
+
781
+ const loadRecords = async (
782
+ globalStateDb = resolveQoderGlobalStateDb(),
783
+ workspaceStorageDir = resolveQoderWorkspaceStorageDir(),
784
+ ): Promise<QoderDataRecords> => {
785
+ const [rows, workspaceStorageIds] = await Promise.all([
786
+ readGlobalRows(globalStateDb),
787
+ listWorkspaceStorageIds(workspaceStorageDir),
788
+ ]);
789
+ return {
790
+ modelConfig: parseModelConfigState(rows),
791
+ records: groupRecords(parseHistoryRows(rows), parseTaskSnapshotRows(rows)),
792
+ workspaceStorageIds,
793
+ };
794
+ };
795
+
796
+ const readRecordSummary = async (
797
+ record: QoderSessionRecord,
798
+ workspaceStorageDir: string,
799
+ workspaceStorageIds: string[],
800
+ modelFallback: string | null,
801
+ ): Promise<QoderSessionSummary> => {
802
+ const state = await readStateData(workspaceStorageDir, workspaceStorageIds, record);
803
+ const entries = buildLocalTranscriptEntries(record, state);
804
+ const stats = createStatsFromEntries(entries, state.snapshotFileCount);
805
+ return toSessionSummary(record, state, stats, modelFallback);
806
+ };
807
+
808
+ const compareNullableMsDesc = (left: number | null, right: number | null): number => {
809
+ return (right ?? 0) - (left ?? 0);
810
+ };
811
+
812
+ const sumSessions = (sessions: QoderSessionSummary[], key: keyof SessionStats): number => {
813
+ return sessions.reduce((total, session) => total + session[key], 0);
814
+ };
815
+
816
+ const toWorkspaceGroup = (worktree: string, sessions: QoderSessionSummary[]): QoderWorkspaceGroup => {
817
+ const lastActiveAtMs = sessions.reduce<number | null>((latest, session) => {
818
+ return maxNullable(latest, session.lastActiveAtMs);
819
+ }, null);
820
+
821
+ return {
822
+ assistantMessageCount: sumSessions(sessions, 'assistantMessageCount'),
823
+ fileOperationCount: sumSessions(sessions, 'fileOperationCount'),
824
+ key: getWorkspaceKey(worktree),
825
+ label: getWorkspaceLabel(worktree),
826
+ lastActiveAtIso: toIso(lastActiveAtMs),
827
+ lastActiveAtMs,
828
+ messageCount: sumSessions(sessions, 'messageCount'),
829
+ renderablePartCount: sumSessions(sessions, 'renderablePartCount'),
830
+ sessionCount: sessions.length,
831
+ snapshotFileCount: sumSessions(sessions, 'snapshotFileCount'),
832
+ uri: getWorkspaceUri(worktree),
833
+ userMessageCount: sumSessions(sessions, 'userMessageCount'),
834
+ workspaceStorageIds: [...new Set(sessions.flatMap((session) => session.workspaceStorageId ?? []))],
835
+ worktree,
836
+ };
837
+ };
838
+
839
+ export const listQoderWorkspaceGroups = async (
840
+ globalStateDb = resolveQoderGlobalStateDb(),
841
+ workspaceStorageDir = resolveQoderWorkspaceStorageDir(),
842
+ ): Promise<QoderWorkspaceGroup[]> => {
843
+ const { modelConfig, records, workspaceStorageIds } = await loadRecords(globalStateDb, workspaceStorageDir);
844
+ const modelFallback = getModelFallback(modelConfig);
845
+ const summaries = await mapWithConcurrency(records, READ_CONCURRENCY, (record) =>
846
+ readRecordSummary(record, workspaceStorageDir, workspaceStorageIds, modelFallback),
847
+ );
848
+ const sessionsByWorktree = new Map<string, QoderSessionSummary[]>();
849
+
850
+ for (const session of summaries) {
851
+ const sessions = sessionsByWorktree.get(session.worktree) ?? [];
852
+ sessions.push(session);
853
+ sessionsByWorktree.set(session.worktree, sessions);
854
+ }
855
+
856
+ return [...sessionsByWorktree.entries()]
857
+ .map(([worktree, sessions]) => toWorkspaceGroup(worktree, sessions))
858
+ .sort(
859
+ (left, right) =>
860
+ compareNullableMsDesc(left.lastActiveAtMs, right.lastActiveAtMs) ||
861
+ left.worktree.localeCompare(right.worktree),
862
+ );
863
+ };
864
+
865
+ const qoderWorkspaceMatchesQuery = (workspace: QoderWorkspaceGroup, query: string): boolean => {
866
+ const raw = query.trim();
867
+ if (!raw) {
868
+ return true;
869
+ }
870
+
871
+ const lowered = raw.toLowerCase();
872
+ if (workspace.key.toLowerCase() === lowered || workspace.label.toLowerCase() === lowered) {
873
+ return true;
874
+ }
875
+
876
+ if (isWorkspacePathQuery(raw)) {
877
+ return workspacePathMatchesQuery(workspace.worktree, raw);
878
+ }
879
+
880
+ return getPortablePathBasename(workspace.worktree).toLowerCase() === lowered;
881
+ };
882
+
883
+ export const findQoderWorkspaceGroups = (groups: QoderWorkspaceGroup[], query: string): QoderWorkspaceGroup[] => {
884
+ return groups.filter((group) => qoderWorkspaceMatchesQuery(group, query));
885
+ };
886
+
887
+ const sortSessions = (sessions: QoderSessionSummary[]): QoderSessionSummary[] => {
888
+ return [...sessions].sort(
889
+ (left, right) =>
890
+ compareNullableMsDesc(left.lastActiveAtMs, right.lastActiveAtMs) || left.title.localeCompare(right.title),
891
+ );
892
+ };
893
+
894
+ export const listQoderSessionsForGroup = async (
895
+ workspaceKey: string,
896
+ globalStateDb = resolveQoderGlobalStateDb(),
897
+ workspaceStorageDir = resolveQoderWorkspaceStorageDir(),
898
+ ): Promise<QoderSessionSummary[]> => {
899
+ const worktree = getWorktreeFromWorkspaceKey(workspaceKey);
900
+ if (!worktree) {
901
+ return [];
902
+ }
903
+
904
+ const { modelConfig, records, workspaceStorageIds } = await loadRecords(globalStateDb, workspaceStorageDir);
905
+ const matchingRecords = records.filter((record) => record.worktree === worktree);
906
+ const modelFallback = getModelFallback(modelConfig);
907
+ const summaries = await mapWithConcurrency(matchingRecords, READ_CONCURRENCY, (record) =>
908
+ readRecordSummary(record, workspaceStorageDir, workspaceStorageIds, modelFallback),
909
+ );
910
+ return sortSessions(summaries);
911
+ };
912
+
913
+ const parseTextPart = (raw: Record<string, JsonValue>, text: string): QoderTranscriptPart => ({
914
+ raw,
915
+ text,
916
+ type: 'text',
917
+ });
918
+
919
+ const buildHistoryEntry = (history: QoderHistoryEntry, index: number): QoderTranscriptEntry => ({
920
+ entryId: history.id || `history:${index}`,
921
+ entryType: 'message',
922
+ parts: [
923
+ parseTextPart(
924
+ {
925
+ historyKey: history.historyKey,
926
+ id: history.id,
927
+ source: 'qoderLocalHistory',
928
+ title: history.title,
929
+ },
930
+ history.text,
931
+ ),
932
+ ],
933
+ raw: history.raw,
934
+ requestId: null,
935
+ role: 'user',
936
+ timestamp: toIso(history.timestampMs),
937
+ });
938
+
939
+ const getUriPath = (operation: Record<string, JsonValue>): string => {
940
+ const uri = asObject(operation.uri ?? null);
941
+ return (
942
+ asString(uri?.fsPath ?? null) ??
943
+ asString(uri?.path ?? null) ??
944
+ asString(uri?.external ?? null) ??
945
+ asString(operation.resource ?? null) ??
946
+ 'unknown file'
947
+ );
948
+ };
949
+
950
+ const getEditPreview = (operation: Record<string, JsonValue>): string | null => {
951
+ const edits = Array.isArray(operation.edits) ? operation.edits : [];
952
+ const text = edits
953
+ .map((edit) => asString(asObject(edit)?.text ?? null))
954
+ .filter((value): value is string => Boolean(value?.trim()))
955
+ .join('\n\n')
956
+ .trim();
957
+ if (!text) {
958
+ return null;
959
+ }
960
+
961
+ const cleaned = cleanExtractedText(text);
962
+ return cleaned.length > 1200 ? `${cleaned.slice(0, 1200).trimEnd()}\n...[truncated]` : cleaned;
963
+ };
964
+
965
+ const buildOperationCommand = (operation: Record<string, JsonValue>): { command: string; toolName: string } => {
966
+ const type = asString(operation.type ?? null) ?? 'operation';
967
+ const filePath = getUriPath(operation);
968
+
969
+ if (type === 'create') {
970
+ return { command: `Create file: ${filePath}`, toolName: 'create_file' };
971
+ }
972
+
973
+ if (type === 'delete') {
974
+ return { command: `Delete file: ${filePath}`, toolName: 'delete_file' };
975
+ }
976
+
977
+ if (type === 'textEdit') {
978
+ const edits = Array.isArray(operation.edits) ? operation.edits.length : 0;
979
+ const preview = getEditPreview(operation);
980
+ return {
981
+ command: [`Edit file: ${filePath}`, `Edits: ${edits}`, preview ? `Preview:\n${preview}` : '']
982
+ .filter(Boolean)
983
+ .join('\n'),
984
+ toolName: 'edit_file',
985
+ };
986
+ }
987
+
988
+ return { command: `${cleanInlineTitle(type)}: ${filePath}`, toolName: type };
989
+ };
990
+
991
+ const buildOperationEntry = (
992
+ operation: Record<string, JsonValue>,
993
+ index: number,
994
+ statePath: string | null,
995
+ ): QoderTranscriptEntry => {
996
+ const requestId = asString(operation.requestId ?? null);
997
+ const operationId =
998
+ asString(operation.operationId ?? null) ?? asString(operation.type ?? null) ?? `operation:${index}`;
999
+ const summary = buildOperationCommand(operation);
1000
+
1001
+ return {
1002
+ entryId: `${statePath ?? 'state'}:${operationId}:${index}`,
1003
+ entryType: 'tool_call',
1004
+ parts: [
1005
+ parseTextPart(
1006
+ {
1007
+ command: summary.command,
1008
+ operationIndex: index,
1009
+ sourceStatePath: statePath,
1010
+ toolName: summary.toolName,
1011
+ type: 'qoderFileOperation',
1012
+ },
1013
+ summary.command,
1014
+ ),
1015
+ ],
1016
+ raw: operation,
1017
+ requestId,
1018
+ role: 'tool',
1019
+ timestamp: null,
1020
+ };
1021
+ };
1022
+
1023
+ type QoderCliPart = {
1024
+ entryType: QoderTranscriptEntry['entryType'];
1025
+ raw: Record<string, JsonValue>;
1026
+ role: string;
1027
+ text: string;
1028
+ };
1029
+
1030
+ type QoderCliTranscript = {
1031
+ entries: QoderTranscriptEntry[];
1032
+ model: string | null;
1033
+ path: string | null;
1034
+ };
1035
+
1036
+ const getCliWorkspaceDirectoryName = (worktree: string): string => {
1037
+ return worktree.replace(/[\\/]+/gu, '-');
1038
+ };
1039
+
1040
+ const getCliTranscriptCandidates = (projectsDir: string, record: QoderSessionRecord): string[] => {
1041
+ return [
1042
+ path.join(projectsDir, `${record.sessionId}.jsonl`),
1043
+ path.join(projectsDir, getCliWorkspaceDirectoryName(record.worktree), `${record.sessionId}.jsonl`),
1044
+ ];
1045
+ };
1046
+
1047
+ const locateCliTranscriptPath = async (projectsDir: string, record: QoderSessionRecord): Promise<string | null> => {
1048
+ for (const candidate of getCliTranscriptCandidates(projectsDir, record)) {
1049
+ if (await pathExists(candidate)) {
1050
+ return candidate;
1051
+ }
1052
+ }
1053
+
1054
+ return null;
1055
+ };
1056
+
1057
+ const stringifyCliValue = (value: JsonValue | undefined): string | null => {
1058
+ if (typeof value === 'string') {
1059
+ return value;
1060
+ }
1061
+
1062
+ if (typeof value === 'number' || typeof value === 'boolean') {
1063
+ return String(value);
1064
+ }
1065
+
1066
+ if (value === null || value === undefined) {
1067
+ return null;
1068
+ }
1069
+
1070
+ return JSON.stringify(value, null, 2);
1071
+ };
1072
+
1073
+ const getCliTextValue = (value: JsonValue | undefined): string | null => {
1074
+ if (Array.isArray(value)) {
1075
+ const text = value
1076
+ .map((item) => getCliTextValue(item))
1077
+ .filter((item): item is string => Boolean(item?.trim()))
1078
+ .join('\n');
1079
+ return text ? text : null;
1080
+ }
1081
+
1082
+ const objectValue = asObject(value ?? null);
1083
+ if (objectValue) {
1084
+ return (
1085
+ getCliTextValue(objectValue.text) ??
1086
+ getCliTextValue(objectValue.content) ??
1087
+ getCliTextValue(objectValue.result) ??
1088
+ stringifyCliValue(objectValue)
1089
+ );
1090
+ }
1091
+
1092
+ return stringifyCliValue(value);
1093
+ };
1094
+
1095
+ const getCliPartData = (part: Record<string, JsonValue>): Record<string, JsonValue> => {
1096
+ return asObject(part.data ?? null) ?? part;
1097
+ };
1098
+
1099
+ const getCliToolName = (part: Record<string, JsonValue>, data: Record<string, JsonValue>): string => {
1100
+ return asString(data.name ?? null) ?? asString(part.name ?? null) ?? 'qoder_tool';
1101
+ };
1102
+
1103
+ const formatCliToolCall = (part: Record<string, JsonValue>, data: Record<string, JsonValue>): string | null => {
1104
+ const name = getCliToolName(part, data);
1105
+ const input = getCliTextValue(data.input ?? part.input);
1106
+ const text = [name, input].filter((value): value is string => Boolean(value?.trim())).join('\n');
1107
+ return text || null;
1108
+ };
1109
+
1110
+ const cliTextPartToTranscriptPart = (part: Record<string, JsonValue>, role: string): QoderCliPart | null => {
1111
+ const data = getCliPartData(part);
1112
+ const text = getCliTextValue(data.text ?? part.text);
1113
+ return text ? { entryType: 'message', raw: part, role, text } : null;
1114
+ };
1115
+
1116
+ const cliReasoningPartToTranscriptPart = (part: Record<string, JsonValue>, type: string): QoderCliPart | null => {
1117
+ const data = getCliPartData(part);
1118
+ const text = getCliTextValue(data.thinking ?? data.signature ?? part.thinking ?? part.text);
1119
+ return text ? { entryType: 'message', raw: { ...part, sourceType: type }, role: 'assistant', text } : null;
1120
+ };
1121
+
1122
+ const cliToolCallPartToTranscriptPart = (part: Record<string, JsonValue>): QoderCliPart | null => {
1123
+ const data = getCliPartData(part);
1124
+ const text = formatCliToolCall(part, data);
1125
+ return text
1126
+ ? {
1127
+ entryType: 'tool_call',
1128
+ raw: {
1129
+ ...part,
1130
+ command: text,
1131
+ toolName: getCliToolName(part, data),
1132
+ },
1133
+ role: 'tool',
1134
+ text,
1135
+ }
1136
+ : null;
1137
+ };
1138
+
1139
+ const cliToolOutputPartToTranscriptPart = (part: Record<string, JsonValue>): QoderCliPart | null => {
1140
+ const data = getCliPartData(part);
1141
+ const text = getCliTextValue(data.content ?? data.output ?? part.content);
1142
+ return text
1143
+ ? {
1144
+ entryType: 'tool_output',
1145
+ raw: {
1146
+ ...part,
1147
+ toolCallId: asString(data.tool_use_id ?? part.tool_use_id ?? null),
1148
+ toolName: getCliToolName(part, data),
1149
+ },
1150
+ role: 'tool',
1151
+ text,
1152
+ }
1153
+ : null;
1154
+ };
1155
+
1156
+ const cliPartToTranscriptPart = (part: Record<string, JsonValue>, role: string): QoderCliPart | null => {
1157
+ const type = asString(part.type ?? null);
1158
+ switch (type) {
1159
+ case 'text':
1160
+ return cliTextPartToTranscriptPart(part, role);
1161
+ case 'reasoning':
1162
+ case 'thinking':
1163
+ return cliReasoningPartToTranscriptPart(part, type);
1164
+ case 'tool_call':
1165
+ case 'tool_use':
1166
+ return cliToolCallPartToTranscriptPart(part);
1167
+ case 'tool_result':
1168
+ case 'tool_output':
1169
+ return cliToolOutputPartToTranscriptPart(part);
1170
+ default:
1171
+ return null;
1172
+ }
1173
+ };
1174
+
1175
+ const getCliLineParts = (raw: Record<string, JsonValue>): Record<string, JsonValue>[] => {
1176
+ const parts = Array.isArray(raw.parts) ? raw.parts : asObject(raw.message ?? null)?.content;
1177
+ return Array.isArray(parts)
1178
+ ? parts.map((part) => asObject(part)).filter((part): part is Record<string, JsonValue> => Boolean(part))
1179
+ : [];
1180
+ };
1181
+
1182
+ const getCliLineRole = (raw: Record<string, JsonValue>): string => {
1183
+ return (
1184
+ asString(raw.role ?? null) ??
1185
+ asString(asObject(raw.message ?? null)?.role ?? null) ??
1186
+ asString(raw.type ?? null) ??
1187
+ 'unknown'
1188
+ );
1189
+ };
1190
+
1191
+ const normalizeCliModel = (model: string | null, modelFallback: string | null): string | null => {
1192
+ if (!model) {
1193
+ return null;
1194
+ }
1195
+
1196
+ if (model === 'auto') {
1197
+ return modelFallback;
1198
+ }
1199
+
1200
+ return normalizeQoderModelLabel(model);
1201
+ };
1202
+
1203
+ const parseCliTranscriptLine = (
1204
+ raw: Record<string, JsonValue>,
1205
+ lineIndex: number,
1206
+ sourcePath: string,
1207
+ ): QoderTranscriptEntry[] => {
1208
+ const role = getCliLineRole(raw);
1209
+ const timestamp = toIso(parseTimestampMs(raw.created_at ?? raw.timestamp ?? raw.updated_at));
1210
+ const parentId = asString(raw.id ?? raw.uuid ?? null) ?? `${sourcePath}:${lineIndex}`;
1211
+ return getCliLineParts(raw).flatMap((part, partIndex) => {
1212
+ const parsed = cliPartToTranscriptPart(part, role);
1213
+ if (!parsed) {
1214
+ return [];
1215
+ }
1216
+
1217
+ return [
1218
+ {
1219
+ entryId: `${parentId}:${partIndex}`,
1220
+ entryType: parsed.entryType,
1221
+ parts: [
1222
+ parseTextPart(
1223
+ {
1224
+ ...parsed.raw,
1225
+ source: 'qoderCliTranscript',
1226
+ sourcePath,
1227
+ },
1228
+ parsed.text,
1229
+ ),
1230
+ ],
1231
+ raw,
1232
+ requestId: asString(raw.request_set_id ?? raw.requestSetId ?? null),
1233
+ role: parsed.role,
1234
+ timestamp,
1235
+ },
1236
+ ];
1237
+ });
1238
+ };
1239
+
1240
+ const readCliTranscriptEntries = async (
1241
+ projectsDir: string,
1242
+ record: QoderSessionRecord,
1243
+ modelFallback: string | null,
1244
+ ): Promise<QoderCliTranscript> => {
1245
+ const transcriptPath = await locateCliTranscriptPath(projectsDir, record);
1246
+ if (!transcriptPath) {
1247
+ return { entries: [], model: null, path: null };
1248
+ }
1249
+
1250
+ const text = await Bun.file(transcriptPath)
1251
+ .text()
1252
+ .catch(() => '');
1253
+ let model: string | null = null;
1254
+ const entries = text.split(/\r?\n/u).flatMap((line, lineIndex) => {
1255
+ if (!line.trim()) {
1256
+ return [];
1257
+ }
1258
+
1259
+ const raw = asJsonObject(parseJsonValue(line));
1260
+ model ??= normalizeCliModel(asString(raw?.model ?? null), modelFallback);
1261
+ return raw ? parseCliTranscriptLine(raw, lineIndex, transcriptPath) : [];
1262
+ });
1263
+
1264
+ return { entries, model, path: transcriptPath };
1265
+ };
1266
+
1267
+ const getRawStringValue = (raw: Record<string, JsonValue>, keys: string[]): string | null => {
1268
+ for (const key of keys) {
1269
+ const value = asString(raw[key] ?? null);
1270
+ if (value?.trim()) {
1271
+ return value;
1272
+ }
1273
+ }
1274
+
1275
+ return null;
1276
+ };
1277
+
1278
+ const stringifyAcpValue = (value: JsonValue | undefined): string | null => {
1279
+ if (typeof value === 'string') {
1280
+ return value;
1281
+ }
1282
+
1283
+ if (typeof value === 'number' || typeof value === 'boolean') {
1284
+ return String(value);
1285
+ }
1286
+
1287
+ if (value === null || value === undefined) {
1288
+ return null;
1289
+ }
1290
+
1291
+ return JSON.stringify(value, null, 2);
1292
+ };
1293
+
1294
+ const getAcpContentText = (update: Record<string, JsonValue>): string | null => {
1295
+ const content = asObject(update.content ?? null);
1296
+ const data = asObject(update.data ?? null);
1297
+ return (
1298
+ getRawStringValue(content ?? {}, ['text', 'content', 'thinking']) ??
1299
+ getRawStringValue(data ?? {}, ['text', 'content', 'thinking', 'output']) ??
1300
+ getRawStringValue(update, ['text', 'content', 'thinking', 'message', 'delta'])
1301
+ );
1302
+ };
1303
+
1304
+ const getAcpTimestamp = (update: Record<string, JsonValue>): string | null => {
1305
+ return toIso(parseTimestampMs(update.timestamp ?? update.created_at ?? update.createdAt ?? update.updated_at));
1306
+ };
1307
+
1308
+ const buildAcpMessageEntry = (
1309
+ event: QoderAcpSessionUpdate,
1310
+ index: number,
1311
+ role: 'assistant' | 'user',
1312
+ ): QoderTranscriptEntry | null => {
1313
+ const text = getAcpContentText(event.update);
1314
+ if (!text?.trim()) {
1315
+ return null;
1316
+ }
1317
+
1318
+ return {
1319
+ entryId: `qoder-acp:${event.sessionId}:${index}`,
1320
+ entryType: 'message',
1321
+ parts: [
1322
+ parseTextPart(
1323
+ {
1324
+ requestId: event.requestId,
1325
+ sessionUpdate: event.update.sessionUpdate ?? null,
1326
+ source: 'qoderAcpSessionLoad',
1327
+ },
1328
+ text,
1329
+ ),
1330
+ ],
1331
+ raw: event.update,
1332
+ requestId: event.requestId,
1333
+ role,
1334
+ timestamp: getAcpTimestamp(event.update),
1335
+ };
1336
+ };
1337
+
1338
+ const getAcpToolId = (update: Record<string, JsonValue>, index: number): string => {
1339
+ return (
1340
+ getRawStringValue(update, ['toolCallId', 'tool_call_id', 'callId', 'id']) ??
1341
+ getRawStringValue(asObject(update.toolCall ?? null) ?? {}, ['id', 'toolCallId']) ??
1342
+ `tool:${index}`
1343
+ );
1344
+ };
1345
+
1346
+ const getAcpToolName = (update: Record<string, JsonValue>): string => {
1347
+ return (
1348
+ getRawStringValue(update, ['toolName', 'name', 'title', 'kind']) ??
1349
+ getRawStringValue(asObject(update.toolCall ?? null) ?? {}, ['toolName', 'name', 'title', 'kind']) ??
1350
+ 'qoder_tool'
1351
+ );
1352
+ };
1353
+
1354
+ const buildAcpToolCallText = (update: Record<string, JsonValue>): string | null => {
1355
+ const toolCall = asObject(update.toolCall ?? null);
1356
+ const name = getAcpToolName(update);
1357
+ const input =
1358
+ stringifyAcpValue(update.input) ??
1359
+ stringifyAcpValue(update.arguments) ??
1360
+ stringifyAcpValue(update.rawInput) ??
1361
+ stringifyAcpValue(toolCall?.input) ??
1362
+ stringifyAcpValue(toolCall?.arguments);
1363
+ return [name, input].filter((value): value is string => Boolean(value?.trim())).join('\n') || null;
1364
+ };
1365
+
1366
+ const buildAcpToolOutputText = (update: Record<string, JsonValue>): string | null => {
1367
+ const text =
1368
+ getAcpContentText(update) ??
1369
+ stringifyAcpValue(update.output) ??
1370
+ stringifyAcpValue(update.result) ??
1371
+ stringifyAcpValue(update.rawOutput);
1372
+ return text?.trim() ? text : null;
1373
+ };
1374
+
1375
+ const buildAcpToolEntry = (
1376
+ event: QoderAcpSessionUpdate,
1377
+ index: number,
1378
+ entryType: 'tool_call' | 'tool_output',
1379
+ ): QoderTranscriptEntry | null => {
1380
+ const text = entryType === 'tool_call' ? buildAcpToolCallText(event.update) : buildAcpToolOutputText(event.update);
1381
+ if (!text) {
1382
+ return null;
1383
+ }
1384
+
1385
+ const toolCallId = getAcpToolId(event.update, index);
1386
+ const toolName = getAcpToolName(event.update);
1387
+ return {
1388
+ entryId: `qoder-acp:${event.sessionId}:${toolCallId}:${index}`,
1389
+ entryType,
1390
+ parts: [
1391
+ parseTextPart(
1392
+ {
1393
+ requestId: event.requestId,
1394
+ sessionUpdate: event.update.sessionUpdate ?? null,
1395
+ source: 'qoderAcpSessionLoad',
1396
+ toolCallId,
1397
+ toolName,
1398
+ },
1399
+ text,
1400
+ ),
1401
+ ],
1402
+ raw: event.update,
1403
+ requestId: event.requestId,
1404
+ role: 'tool',
1405
+ timestamp: getAcpTimestamp(event.update),
1406
+ };
1407
+ };
1408
+
1409
+ const acpUpdateToEntry = (event: QoderAcpSessionUpdate, index: number): QoderTranscriptEntry | null => {
1410
+ switch (event.update.sessionUpdate) {
1411
+ case 'user_message_chunk':
1412
+ return buildAcpMessageEntry(event, index, 'user');
1413
+ case 'agent_thought_chunk':
1414
+ case 'agent_message_chunk':
1415
+ return buildAcpMessageEntry(event, index, 'assistant');
1416
+ case 'tool_call':
1417
+ return buildAcpToolEntry(event, index, 'tool_call');
1418
+ case 'tool_call_update':
1419
+ return buildAcpToolEntry(event, index, 'tool_output');
1420
+ default:
1421
+ return null;
1422
+ }
1423
+ };
1424
+
1425
+ const getAcpModel = (events: QoderAcpSessionUpdate[]): string | null => {
1426
+ for (const event of [...events].reverse()) {
1427
+ if (event.update.sessionUpdate !== 'current_model_update') {
1428
+ continue;
1429
+ }
1430
+
1431
+ const model = normalizeQoderModelLabel(getRawStringValue(event.update, ['modelId', 'model', 'modelName']));
1432
+ if (model) {
1433
+ return model;
1434
+ }
1435
+ }
1436
+
1437
+ return null;
1438
+ };
1439
+
1440
+ const getTaskIdForAcpLoad = (record: QoderSessionRecord): string | null => {
1441
+ return record.task?.id ?? (record.sessionId.replace(/\.session\.execution$/u, '') || null);
1442
+ };
1443
+
1444
+ const shouldUseAcp = (
1445
+ record: QoderSessionRecord,
1446
+ state: QoderStateData,
1447
+ cliTranscript: QoderCliTranscript,
1448
+ options: QoderTranscriptReadOptions,
1449
+ globalStateDb: string,
1450
+ workspaceStorageDir: string,
1451
+ ): boolean => {
1452
+ if (options.enableAcp === false) {
1453
+ return false;
1454
+ }
1455
+
1456
+ if (cliTranscript.entries.some((entry) => entry.role === 'assistant')) {
1457
+ return false;
1458
+ }
1459
+
1460
+ if (options.acpSocketPath) {
1461
+ return true;
1462
+ }
1463
+
1464
+ if (globalStateDb !== resolveQoderGlobalStateDb() || workspaceStorageDir !== resolveQoderWorkspaceStorageDir()) {
1465
+ return false;
1466
+ }
1467
+
1468
+ const lastActiveAtMs = getLastActiveAtMs(record, state);
1469
+ return lastActiveAtMs !== null && Date.now() - lastActiveAtMs <= ACP_RECENT_SESSION_WINDOW_MS;
1470
+ };
1471
+
1472
+ const readAcpTranscriptEntries = async (
1473
+ record: QoderSessionRecord,
1474
+ state: QoderStateData,
1475
+ cliTranscript: QoderCliTranscript,
1476
+ options: QoderTranscriptReadOptions,
1477
+ globalStateDb: string,
1478
+ workspaceStorageDir: string,
1479
+ ): Promise<{ entries: QoderTranscriptEntry[]; model: string | null; socketPath: string | null }> => {
1480
+ if (!shouldUseAcp(record, state, cliTranscript, options, globalStateDb, workspaceStorageDir)) {
1481
+ return { entries: [], model: null, socketPath: null };
1482
+ }
1483
+
1484
+ const loaded = await loadQoderAcpSession({
1485
+ cwd: record.worktree,
1486
+ drainMs: options.acpDrainMs,
1487
+ sessionId: record.sessionId,
1488
+ socketPath: options.acpSocketPath ?? resolveQoderAcpSocketPath(),
1489
+ taskId: getTaskIdForAcpLoad(record),
1490
+ timeoutMs: options.acpTimeoutMs,
1491
+ });
1492
+ if (!loaded) {
1493
+ return { entries: [], model: null, socketPath: null };
1494
+ }
1495
+
1496
+ return {
1497
+ entries: loaded.events
1498
+ .map((event, index) => acpUpdateToEntry(event, index))
1499
+ .filter((entry): entry is QoderTranscriptEntry => Boolean(entry)),
1500
+ model: getAcpModel(loaded.events),
1501
+ socketPath: loaded.socketPath,
1502
+ };
1503
+ };
1504
+
1505
+ const buildLocalTranscriptEntryGroups = (
1506
+ record: QoderSessionRecord,
1507
+ state: QoderStateData,
1508
+ ): { historyEntries: QoderTranscriptEntry[]; operationEntries: QoderTranscriptEntry[] } => {
1509
+ const historyEntries = record.histories.map(buildHistoryEntry);
1510
+ const timeline = asObject(state.rawState?.timeline ?? null);
1511
+ const operations = Array.isArray(timeline?.operations) ? timeline.operations : [];
1512
+ const operationEntries = operations.flatMap((operation, index) => {
1513
+ const raw = asObject(operation);
1514
+ return raw ? [buildOperationEntry(raw, index, state.statePath)] : [];
1515
+ });
1516
+ return { historyEntries, operationEntries };
1517
+ };
1518
+
1519
+ const buildLocalTranscriptEntries = (record: QoderSessionRecord, state: QoderStateData): QoderTranscriptEntry[] => {
1520
+ const { historyEntries, operationEntries } = buildLocalTranscriptEntryGroups(record, state);
1521
+ return [...historyEntries, ...operationEntries];
1522
+ };
1523
+
1524
+ const buildTranscriptEntries = async (
1525
+ record: QoderSessionRecord,
1526
+ state: QoderStateData,
1527
+ cliProjectsDir: string,
1528
+ modelFallback: string | null,
1529
+ options: QoderTranscriptReadOptions,
1530
+ globalStateDb: string,
1531
+ workspaceStorageDir: string,
1532
+ ): Promise<{
1533
+ acpSocketPath: string | null;
1534
+ cliTranscriptPath: string | null;
1535
+ entries: QoderTranscriptEntry[];
1536
+ model: string | null;
1537
+ }> => {
1538
+ const { historyEntries, operationEntries } = buildLocalTranscriptEntryGroups(record, state);
1539
+ const cliTranscript = await readCliTranscriptEntries(cliProjectsDir, record, modelFallback);
1540
+ const acpTranscript = await readAcpTranscriptEntries(
1541
+ record,
1542
+ state,
1543
+ cliTranscript,
1544
+ options,
1545
+ globalStateDb,
1546
+ workspaceStorageDir,
1547
+ );
1548
+ const transcriptEntries = cliTranscript.entries.some((entry) => entry.role === 'assistant')
1549
+ ? cliTranscript.entries
1550
+ : acpTranscript.entries;
1551
+ const shouldIncludeHistory = !transcriptEntries.some((entry) => entry.role === 'user');
1552
+
1553
+ return {
1554
+ acpSocketPath: acpTranscript.socketPath,
1555
+ cliTranscriptPath: cliTranscript.path,
1556
+ entries: [...(shouldIncludeHistory ? historyEntries : []), ...transcriptEntries, ...operationEntries],
1557
+ model: acpTranscript.model ?? cliTranscript.model ?? modelFallback,
1558
+ };
1559
+ };
1560
+
1561
+ export const readQoderSessionTranscript = async (
1562
+ globalStateDb: string,
1563
+ workspaceStorageDir: string,
1564
+ sessionId: string,
1565
+ cliProjectsDir = resolveQoderCliProjectsDir(),
1566
+ options: QoderTranscriptReadOptions = {},
1567
+ ): Promise<QoderSessionTranscript | null> => {
1568
+ const { modelConfig, records, workspaceStorageIds } = await loadRecords(globalStateDb, workspaceStorageDir);
1569
+ const record = records.find((candidate) => candidate.sessionId === sessionId);
1570
+ if (!record) {
1571
+ return null;
1572
+ }
1573
+
1574
+ const state = await readStateData(workspaceStorageDir, workspaceStorageIds, record);
1575
+ const modelFallback = getModelFallback(modelConfig);
1576
+ const { acpSocketPath, cliTranscriptPath, entries, model } = await buildTranscriptEntries(
1577
+ record,
1578
+ state,
1579
+ cliProjectsDir,
1580
+ modelFallback,
1581
+ options,
1582
+ globalStateDb,
1583
+ workspaceStorageDir,
1584
+ );
1585
+ const stats = createStatsFromEntries(entries, state.snapshotFileCount);
1586
+
1587
+ return {
1588
+ entries,
1589
+ rawSession: {
1590
+ histories: record.histories.map((history) => history.raw),
1591
+ sourceAcpSocketPath: acpSocketPath,
1592
+ sourceCliTranscriptPath: cliTranscriptPath,
1593
+ sourceStatePath: state.statePath,
1594
+ state: state.rawState,
1595
+ task: record.task?.raw ?? null,
1596
+ workspaceStorageId: record.workspaceStorageId,
1597
+ },
1598
+ renderablePartCount: stats.renderablePartCount,
1599
+ session: toSessionSummary(record, state, stats, model),
1600
+ };
1601
+ };