spiracha 1.5.0 → 2.1.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 (401) hide show
  1. package/AGENTS.md +114 -205
  2. package/README.md +94 -269
  3. package/apps/ui/AGENTS.md +17 -7
  4. package/apps/ui/README.md +52 -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 +245 -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 +125 -0
  11. package/apps/ui/src/components/breadcrumbs.tsx +71 -0
  12. package/apps/ui/src/components/claude-code-sessions-table.tsx +150 -0
  13. package/apps/ui/src/components/claude-code-workspaces-table.tsx +57 -0
  14. package/apps/ui/src/components/cursor-threads-table.tsx +152 -0
  15. package/apps/ui/src/components/cursor-workspaces-table.tsx +140 -0
  16. package/apps/ui/src/components/data-table.tsx +243 -0
  17. package/apps/ui/src/components/delete-confirm-dialog.tsx +88 -0
  18. package/apps/ui/src/components/export-dialog.tsx +174 -0
  19. package/apps/ui/src/components/grok-sessions-table.tsx +146 -0
  20. package/apps/ui/src/components/grok-workspaces-table.tsx +53 -0
  21. package/apps/ui/src/components/json-panel.tsx +17 -0
  22. package/apps/ui/src/components/kiro-sessions-table.tsx +148 -0
  23. package/apps/ui/src/components/kiro-workspaces-table.tsx +53 -0
  24. package/apps/ui/src/components/list-search-input.tsx +18 -0
  25. package/apps/ui/src/components/loading-panel.tsx +23 -0
  26. package/apps/ui/src/components/metadata-section.tsx +31 -0
  27. package/apps/ui/src/components/metric-card.tsx +19 -0
  28. package/apps/ui/src/components/opencode-sessions-table.tsx +164 -0
  29. package/apps/ui/src/components/opencode-workspaces-table.tsx +57 -0
  30. package/apps/ui/src/components/page-header.tsx +39 -0
  31. package/apps/ui/src/components/projects-loading-state.tsx +74 -0
  32. package/apps/ui/src/components/projects-table.tsx +108 -0
  33. package/apps/ui/src/components/qoder-sessions-table.tsx +133 -0
  34. package/apps/ui/src/components/qoder-workspaces-table.tsx +57 -0
  35. package/apps/ui/src/components/recent-threads-list.tsx +52 -0
  36. package/apps/ui/src/components/reload-error-panel.tsx +20 -0
  37. package/apps/ui/src/components/selection-actions-toolbar.tsx +80 -0
  38. package/apps/ui/src/components/text-document-panel.tsx +19 -0
  39. package/apps/ui/src/components/theme-toggle.tsx +48 -0
  40. package/apps/ui/src/components/threads-table.tsx +165 -0
  41. package/apps/ui/src/components/transcript-view.tsx +648 -0
  42. package/apps/ui/src/components/ui/alert-dialog.tsx +160 -0
  43. package/apps/ui/src/components/ui/badge.tsx +45 -0
  44. package/apps/ui/src/components/ui/button.tsx +62 -0
  45. package/apps/ui/src/components/ui/checkbox.tsx +29 -0
  46. package/apps/ui/src/components/ui/dialog.tsx +137 -0
  47. package/apps/ui/src/components/ui/dropdown-menu.tsx +217 -0
  48. package/apps/ui/src/components/ui/input.tsx +21 -0
  49. package/apps/ui/src/components/ui/scroll-area.tsx +46 -0
  50. package/apps/ui/src/components/ui/select.tsx +163 -0
  51. package/apps/ui/src/components/ui/separator.tsx +28 -0
  52. package/apps/ui/src/components/ui/sheet.tsx +107 -0
  53. package/apps/ui/src/components/ui/skeleton.tsx +7 -0
  54. package/apps/ui/src/components/ui/table.tsx +76 -0
  55. package/apps/ui/src/components/ui/tabs.tsx +71 -0
  56. package/apps/ui/src/components/ui/tooltip.tsx +44 -0
  57. package/apps/ui/src/integrations/tanstack-query/devtools.tsx +6 -0
  58. package/apps/ui/src/integrations/tanstack-query/root-provider.tsx +10 -0
  59. package/apps/ui/src/lib/antigravity-conversation-state.ts +41 -0
  60. package/apps/ui/src/lib/antigravity-queries.ts +33 -0
  61. package/apps/ui/src/lib/antigravity-server.ts +273 -0
  62. package/apps/ui/src/lib/antigravity-transcript-events.ts +343 -0
  63. package/apps/ui/src/lib/claude-code-queries.ts +34 -0
  64. package/apps/ui/src/lib/claude-code-server.ts +243 -0
  65. package/apps/ui/src/lib/claude-code-transcript-events.ts +161 -0
  66. package/apps/ui/src/lib/codex-queries.ts +82 -0
  67. package/apps/ui/src/lib/codex-server.ts +291 -0
  68. package/apps/ui/src/lib/cursor-queries.ts +37 -0
  69. package/apps/ui/src/lib/cursor-server.ts +315 -0
  70. package/apps/ui/src/lib/cursor-transcript-events.ts +229 -0
  71. package/apps/ui/src/lib/download.ts +166 -0
  72. package/apps/ui/src/lib/formatters.ts +147 -0
  73. package/apps/ui/src/lib/grok-queries.ts +22 -0
  74. package/apps/ui/src/lib/grok-server.ts +169 -0
  75. package/apps/ui/src/lib/grok-transcript-events.ts +149 -0
  76. package/apps/ui/src/lib/kiro-queries.ts +22 -0
  77. package/apps/ui/src/lib/kiro-server.ts +151 -0
  78. package/apps/ui/src/lib/kiro-transcript-events.ts +113 -0
  79. package/apps/ui/src/lib/opencode-queries.ts +37 -0
  80. package/apps/ui/src/lib/opencode-server.ts +151 -0
  81. package/apps/ui/src/lib/opencode-transcript-events.ts +221 -0
  82. package/apps/ui/src/lib/package-metadata.ts +16 -0
  83. package/apps/ui/src/lib/path-utils.ts +13 -0
  84. package/apps/ui/src/lib/qoder-queries.ts +22 -0
  85. package/apps/ui/src/lib/qoder-server.ts +135 -0
  86. package/apps/ui/src/lib/qoder-transcript-events.ts +139 -0
  87. package/apps/ui/src/lib/route-search.ts +182 -0
  88. package/apps/ui/src/lib/settings-store.tsx +59 -0
  89. package/apps/ui/src/lib/source-session-export-server.ts +139 -0
  90. package/apps/ui/src/lib/text-filter.ts +22 -0
  91. package/apps/ui/src/lib/thread-id.ts +3 -0
  92. package/apps/ui/src/lib/thread-transcript-load.ts +15 -0
  93. package/apps/ui/src/lib/thread-transcript-stats.ts +57 -0
  94. package/apps/ui/src/lib/utils.ts +7 -0
  95. package/apps/ui/src/lib/workspace-delete-navigation.ts +12 -0
  96. package/apps/ui/src/routeTree.gen.ts +837 -0
  97. package/apps/ui/src/router.tsx +20 -0
  98. package/apps/ui/src/routes/$threadId.tsx +22 -0
  99. package/apps/ui/src/routes/__root.tsx +127 -0
  100. package/apps/ui/src/routes/analytics.tsx +143 -0
  101. package/apps/ui/src/routes/antigravity-conversations.$conversationId.tsx +504 -0
  102. package/apps/ui/src/routes/antigravity.$workspaceKey.tsx +365 -0
  103. package/apps/ui/src/routes/antigravity.index.tsx +51 -0
  104. package/apps/ui/src/routes/api.v1.conversation-query.ts +12 -0
  105. package/apps/ui/src/routes/api.v1.conversations.$source.$id.export.ts +12 -0
  106. package/apps/ui/src/routes/api.v1.conversations.$source.$id.ts +16 -0
  107. package/apps/ui/src/routes/api.v1.conversations.delete.ts +12 -0
  108. package/apps/ui/src/routes/api.v1.conversations.export.ts +12 -0
  109. package/apps/ui/src/routes/api.v1.conversations.ts +12 -0
  110. package/apps/ui/src/routes/api.v1.resolve.ts +12 -0
  111. package/apps/ui/src/routes/api.v1.sources.ts +12 -0
  112. package/apps/ui/src/routes/claude-code-sessions.$sessionId.tsx +468 -0
  113. package/apps/ui/src/routes/claude-code.$workspaceKey.tsx +274 -0
  114. package/apps/ui/src/routes/claude-code.index.tsx +48 -0
  115. package/apps/ui/src/routes/codex.$project.tsx +390 -0
  116. package/apps/ui/src/routes/codex.index.tsx +116 -0
  117. package/apps/ui/src/routes/cursor-threads.$composerId.tsx +435 -0
  118. package/apps/ui/src/routes/cursor.$workspaceKey.tsx +426 -0
  119. package/apps/ui/src/routes/cursor.index.tsx +120 -0
  120. package/apps/ui/src/routes/grok-sessions.$sessionId.tsx +388 -0
  121. package/apps/ui/src/routes/grok.$workspaceKey.tsx +301 -0
  122. package/apps/ui/src/routes/grok.index.tsx +48 -0
  123. package/apps/ui/src/routes/index.tsx +124 -0
  124. package/apps/ui/src/routes/kiro-sessions.$sessionId.tsx +396 -0
  125. package/apps/ui/src/routes/kiro.$workspaceKey.tsx +303 -0
  126. package/apps/ui/src/routes/kiro.index.tsx +48 -0
  127. package/apps/ui/src/routes/opencode-sessions.$sessionId.tsx +420 -0
  128. package/apps/ui/src/routes/opencode.$workspaceKey.tsx +327 -0
  129. package/apps/ui/src/routes/opencode.index.tsx +61 -0
  130. package/apps/ui/src/routes/qoder-sessions.$sessionId.tsx +344 -0
  131. package/apps/ui/src/routes/qoder.$workspaceKey.tsx +186 -0
  132. package/apps/ui/src/routes/qoder.index.tsx +48 -0
  133. package/apps/ui/src/routes/settings.tsx +91 -0
  134. package/apps/ui/src/routes/threads.$threadId.tsx +1103 -0
  135. package/apps/ui/src/styles.css +122 -0
  136. package/apps/ui/tsconfig.json +29 -0
  137. package/apps/ui/vite.config.ts +84 -0
  138. package/bin/spiracha.ts +32 -0
  139. package/package.json +54 -80
  140. package/src/client.ts +423 -0
  141. package/src/lib/antigravity-db.ts +414 -44
  142. package/src/lib/antigravity-exporter-types.ts +4 -0
  143. package/src/lib/antigravity-projects.ts +201 -0
  144. package/src/lib/claude-code-db.ts +498 -32
  145. package/src/lib/claude-code-exporter-types.ts +5 -0
  146. package/src/lib/claude-code-transcript-phase.ts +60 -1
  147. package/src/lib/claude-code-transcript.ts +8 -5
  148. package/src/lib/codex-analytics.ts +1 -1
  149. package/src/lib/codex-browser-db.ts +410 -74
  150. package/src/lib/codex-browser-export.ts +29 -41
  151. package/src/lib/codex-browser-types.ts +1 -1
  152. package/src/lib/codex-thread-cache.ts +41 -13
  153. package/src/lib/codex-thread-parser.ts +86 -35
  154. package/src/lib/{codex-exporter-types.ts → codex-thread-types.ts} +2 -30
  155. package/src/lib/codex-transcript-filter.ts +31 -0
  156. package/src/lib/{codex-exporter-transcript.ts → codex-transcript-renderer.ts} +104 -48
  157. package/src/lib/concurrency.ts +41 -0
  158. package/src/lib/conversation-api.ts +1172 -0
  159. package/src/lib/conversation-data/adapter-helpers.ts +110 -0
  160. package/src/lib/conversation-data/antigravity-adapter.ts +167 -0
  161. package/src/lib/conversation-data/claude-code-adapter.ts +206 -0
  162. package/src/lib/conversation-data/codex-adapter.ts +269 -0
  163. package/src/lib/conversation-data/cursor-adapter.ts +205 -0
  164. package/src/lib/conversation-data/grok-adapter.ts +210 -0
  165. package/src/lib/conversation-data/index.ts +386 -0
  166. package/src/lib/conversation-data/kiro-adapter.ts +174 -0
  167. package/src/lib/conversation-data/message-selector.ts +37 -0
  168. package/src/lib/conversation-data/opencode-adapter.ts +202 -0
  169. package/src/lib/conversation-data/path-match.ts +79 -0
  170. package/src/lib/conversation-data/qoder-adapter.ts +255 -0
  171. package/src/lib/conversation-data/types.ts +159 -0
  172. package/src/lib/conversation-zip-export.ts +57 -0
  173. package/src/lib/cursor-db.ts +100 -25
  174. package/src/lib/cursor-exporter-types.ts +0 -19
  175. package/src/lib/grok-db.ts +1026 -0
  176. package/src/lib/grok-exporter-types.ts +110 -0
  177. package/src/lib/grok-transcript-phase.ts +52 -0
  178. package/src/lib/grok-transcript.ts +154 -0
  179. package/src/lib/kiro-db.ts +52 -1
  180. package/src/lib/model-label.ts +11 -3
  181. package/src/lib/opencode-db.ts +598 -55
  182. package/src/lib/opencode-transcript.ts +8 -5
  183. package/src/lib/qoder-acp-client.ts +268 -0
  184. package/src/lib/qoder-db.ts +1601 -0
  185. package/src/lib/qoder-exporter-types.ts +114 -0
  186. package/src/lib/qoder-transcript-phase.ts +41 -0
  187. package/src/lib/qoder-transcript.ts +112 -0
  188. package/src/lib/shared.ts +12 -0
  189. package/src/lib/transcript-load-limiter.ts +82 -0
  190. package/src/lib/ui-cache.ts +43 -17
  191. package/src/lib/ui-export-archive.ts +60 -0
  192. package/apps/ui/dist/client/assets/_threadId-CAIeH5mq.js +0 -1
  193. package/apps/ui/dist/client/assets/analytics-DOy1b6Sh.js +0 -1
  194. package/apps/ui/dist/client/assets/antigravity-conversations._conversationId-DqySVO2K.js +0 -7
  195. package/apps/ui/dist/client/assets/antigravity-conversations._conversationId-KAcXtNxI.js +0 -1
  196. package/apps/ui/dist/client/assets/antigravity-keychain-panel-B77qJz6f.js +0 -1
  197. package/apps/ui/dist/client/assets/antigravity._workspaceKey-B3NM6gRH.js +0 -1
  198. package/apps/ui/dist/client/assets/antigravity._workspaceKey-ByitGWki.js +0 -1
  199. package/apps/ui/dist/client/assets/antigravity.index-DP9JC2P9.js +0 -1
  200. package/apps/ui/dist/client/assets/antigravity.index-DgW25FEV.js +0 -1
  201. package/apps/ui/dist/client/assets/badge-C3i4OKs8.js +0 -1
  202. package/apps/ui/dist/client/assets/checkbox-CdHitXWQ.js +0 -1
  203. package/apps/ui/dist/client/assets/claude-code-sessions._sessionId-8pQ55ci2.js +0 -1
  204. package/apps/ui/dist/client/assets/claude-code-sessions._sessionId-DVz5UmtU.js +0 -1
  205. package/apps/ui/dist/client/assets/claude-code._workspaceKey-BBKskSum.js +0 -1
  206. package/apps/ui/dist/client/assets/claude-code._workspaceKey-OkikMUFs.js +0 -1
  207. package/apps/ui/dist/client/assets/claude-code.index-Cwu76dbb.js +0 -1
  208. package/apps/ui/dist/client/assets/claude-code.index-D6eMd08t.js +0 -1
  209. package/apps/ui/dist/client/assets/createServerFn-B-KkwmHU.js +0 -3
  210. package/apps/ui/dist/client/assets/cursor-threads._composerId-CmBFclK8.js +0 -1
  211. package/apps/ui/dist/client/assets/cursor-threads._composerId-hBWlP1D-.js +0 -1
  212. package/apps/ui/dist/client/assets/cursor._workspaceKey-BFPkIRi-.js +0 -1
  213. package/apps/ui/dist/client/assets/cursor._workspaceKey-CaAZjGmm.js +0 -1
  214. package/apps/ui/dist/client/assets/cursor.index-8y4o20kb.js +0 -2
  215. package/apps/ui/dist/client/assets/cursor.index-CrnMwDCi.js +0 -1
  216. package/apps/ui/dist/client/assets/data-table-Br78_dk3.js +0 -4
  217. package/apps/ui/dist/client/assets/delete-confirm-dialog-Bxs07u-j.js +0 -7
  218. package/apps/ui/dist/client/assets/dist-B9pyYCVU.js +0 -1
  219. package/apps/ui/dist/client/assets/dist-BrJhN2A0.js +0 -5
  220. package/apps/ui/dist/client/assets/dist-Cmd3AIfD.js +0 -1
  221. package/apps/ui/dist/client/assets/dist-DvMS2965.js +0 -1
  222. package/apps/ui/dist/client/assets/download-Bf0QJKD3.js +0 -1
  223. package/apps/ui/dist/client/assets/dropdown-menu-D5cvPaZt.js +0 -1
  224. package/apps/ui/dist/client/assets/es2015-ad6040dw.js +0 -41
  225. package/apps/ui/dist/client/assets/export-dialog-Dr8d2MqD.js +0 -1
  226. package/apps/ui/dist/client/assets/formatters-BtqyrX_c.js +0 -1
  227. package/apps/ui/dist/client/assets/index-C-STgIH4.js +0 -166
  228. package/apps/ui/dist/client/assets/jsx-runtime-DGeXAQPT.js +0 -1
  229. package/apps/ui/dist/client/assets/kiro-sessions._sessionId-CKgfJ7GR.js +0 -1
  230. package/apps/ui/dist/client/assets/kiro-sessions._sessionId-zvIJHtQF.js +0 -1
  231. package/apps/ui/dist/client/assets/kiro._workspaceKey-DhVuJuZB.js +0 -1
  232. package/apps/ui/dist/client/assets/kiro._workspaceKey-QArsjkTY.js +0 -1
  233. package/apps/ui/dist/client/assets/kiro.index-C-fr4m7i.js +0 -1
  234. package/apps/ui/dist/client/assets/kiro.index-OYgAdtZd.js +0 -1
  235. package/apps/ui/dist/client/assets/metric-card-Dplm0ZiM.js +0 -1
  236. package/apps/ui/dist/client/assets/opencode-sessions._sessionId-DLjiTst-.js +0 -1
  237. package/apps/ui/dist/client/assets/opencode-sessions._sessionId-biBwrRbt.js +0 -8
  238. package/apps/ui/dist/client/assets/opencode._workspaceKey-C7OIzMOJ.js +0 -1
  239. package/apps/ui/dist/client/assets/opencode._workspaceKey-sBakX0oI.js +0 -1
  240. package/apps/ui/dist/client/assets/opencode.index-Bl_kNs9N.js +0 -1
  241. package/apps/ui/dist/client/assets/opencode.index-CN53bVmb.js +0 -1
  242. package/apps/ui/dist/client/assets/page-header-Cwa3p6Tl.js +0 -1
  243. package/apps/ui/dist/client/assets/preload-helper-B8fFrdxC.js +0 -1
  244. package/apps/ui/dist/client/assets/projects._project-D8WloxQN.js +0 -1
  245. package/apps/ui/dist/client/assets/projects._project-DSDwH4NF.js +0 -1
  246. package/apps/ui/dist/client/assets/projects.index-5xRtuJdX.js +0 -3
  247. package/apps/ui/dist/client/assets/projects.index-DEfhWqa8.js +0 -1
  248. package/apps/ui/dist/client/assets/react-dom-DL96Jor4.js +0 -1
  249. package/apps/ui/dist/client/assets/refresh-ccw-BgKjOiA6.js +0 -1
  250. package/apps/ui/dist/client/assets/reload-error-panel-AHqu0akK.js +0 -1
  251. package/apps/ui/dist/client/assets/routes-DnzF_9CX.js +0 -1
  252. package/apps/ui/dist/client/assets/scroll-text-fM0sMOB3.js +0 -1
  253. package/apps/ui/dist/client/assets/select-CYF6pF37.js +0 -1
  254. package/apps/ui/dist/client/assets/settings-Dx70jgP_.js +0 -1
  255. package/apps/ui/dist/client/assets/sqlite-error-B6uZjUe8.js +0 -1
  256. package/apps/ui/dist/client/assets/styles-B7Zi9EFA.css +0 -1
  257. package/apps/ui/dist/client/assets/tabs-CmuhkT-c.js +0 -7
  258. package/apps/ui/dist/client/assets/text-filter-DVAZySUS.js +0 -2
  259. package/apps/ui/dist/client/assets/thread-transcript-stats-BUNz6u7F.js +0 -1
  260. package/apps/ui/dist/client/assets/threads._threadId-BB9tTsV4.js +0 -1
  261. package/apps/ui/dist/client/assets/threads._threadId-BMRSFow3.js +0 -1
  262. package/apps/ui/dist/client/assets/useMutation-BSFult2i.js +0 -1
  263. package/apps/ui/dist/client/assets/useQuery-CjINKZf1.js +0 -1
  264. package/apps/ui/dist/server/assets/_tanstack-start-manifest_v-CQ8Jjs3B.js +0 -387
  265. package/apps/ui/dist/server/assets/_threadId-B6SrBR9E.js +0 -6
  266. package/apps/ui/dist/server/assets/analytics-CYP_LT5Y.js +0 -16
  267. package/apps/ui/dist/server/assets/analytics-CtpOMVMO.js +0 -146
  268. package/apps/ui/dist/server/assets/antigravity-conversation-state-HgzS302O.js +0 -16
  269. package/apps/ui/dist/server/assets/antigravity-conversations._conversationId-BKOyL2T6.js +0 -613
  270. package/apps/ui/dist/server/assets/antigravity-conversations._conversationId-D426O-64.js +0 -11
  271. package/apps/ui/dist/server/assets/antigravity-conversations._conversationId-eS87Dx7g.js +0 -20
  272. package/apps/ui/dist/server/assets/antigravity-db-Bh8_U9uw.js +0 -580
  273. package/apps/ui/dist/server/assets/antigravity-keychain-DOiuHDwK.js +0 -126
  274. package/apps/ui/dist/server/assets/antigravity-keychain-panel-CC7FLmAK.js +0 -55
  275. package/apps/ui/dist/server/assets/antigravity-queries-CGrJO9Vr.js +0 -37
  276. package/apps/ui/dist/server/assets/antigravity-server-CHRjVFqN.js +0 -114
  277. package/apps/ui/dist/server/assets/antigravity._workspaceKey-3m_MzNFA.js +0 -11
  278. package/apps/ui/dist/server/assets/antigravity._workspaceKey-B0QeYUOh.js +0 -28
  279. package/apps/ui/dist/server/assets/antigravity._workspaceKey-C3kP-qyN.js +0 -210
  280. package/apps/ui/dist/server/assets/antigravity.index-CPCYkqCi.js +0 -104
  281. package/apps/ui/dist/server/assets/antigravity.index-DudTB3Tq.js +0 -11
  282. package/apps/ui/dist/server/assets/badge-EvdhKK_Z.js +0 -26
  283. package/apps/ui/dist/server/assets/button-CmTDnzOn.js +0 -46
  284. package/apps/ui/dist/server/assets/checkbox-C0hovF41.js +0 -19
  285. package/apps/ui/dist/server/assets/claude-code-db-Ik3VStKZ.js +0 -361
  286. package/apps/ui/dist/server/assets/claude-code-queries-Cgzi4K-e.js +0 -37
  287. package/apps/ui/dist/server/assets/claude-code-server-PfraClXS.js +0 -72
  288. package/apps/ui/dist/server/assets/claude-code-sessions._sessionId-C55oe6zj.js +0 -18
  289. package/apps/ui/dist/server/assets/claude-code-sessions._sessionId-D1RYYi3j.js +0 -473
  290. package/apps/ui/dist/server/assets/claude-code-sessions._sessionId-DSvPLEUL.js +0 -11
  291. package/apps/ui/dist/server/assets/claude-code-transcript-C-bxh3cS.js +0 -139
  292. package/apps/ui/dist/server/assets/claude-code-transcript-phase-DmUMc8VU.js +0 -10
  293. package/apps/ui/dist/server/assets/claude-code._workspaceKey-B5Xg_-tE.js +0 -28
  294. package/apps/ui/dist/server/assets/claude-code._workspaceKey-BPlzyXPa.js +0 -11
  295. package/apps/ui/dist/server/assets/claude-code._workspaceKey-DqDve2wl.js +0 -191
  296. package/apps/ui/dist/server/assets/claude-code.index-B7yYR7dD.js +0 -92
  297. package/apps/ui/dist/server/assets/claude-code.index-BUHThG1W.js +0 -11
  298. package/apps/ui/dist/server/assets/codex-queries-CtgeZ7VL.js +0 -99
  299. package/apps/ui/dist/server/assets/codex-server-CGXU4H8-.js +0 -2294
  300. package/apps/ui/dist/server/assets/concurrency-VXtYvlGh.js +0 -18
  301. package/apps/ui/dist/server/assets/createServerRpc-CF_DEwnm.js +0 -12
  302. package/apps/ui/dist/server/assets/createSsrRpc-C754NPki.js +0 -16
  303. package/apps/ui/dist/server/assets/cursor-db-CZnYy7aT.js +0 -830
  304. package/apps/ui/dist/server/assets/cursor-exporter-types-CI3goo-c.js +0 -34
  305. package/apps/ui/dist/server/assets/cursor-queries-NCIM0Nat.js +0 -67
  306. package/apps/ui/dist/server/assets/cursor-recovery-CXpO8KcD.js +0 -361
  307. package/apps/ui/dist/server/assets/cursor-server-DNu52A3K.js +0 -213
  308. package/apps/ui/dist/server/assets/cursor-threads._composerId-BB0Y_Mao.js +0 -11
  309. package/apps/ui/dist/server/assets/cursor-threads._composerId-Ckn6ItHI.js +0 -582
  310. package/apps/ui/dist/server/assets/cursor-threads._composerId-p9-ARAzc.js +0 -18
  311. package/apps/ui/dist/server/assets/cursor-transcript-B7tGnqtn.js +0 -125
  312. package/apps/ui/dist/server/assets/cursor._workspaceKey-CZuohIUL.js +0 -28
  313. package/apps/ui/dist/server/assets/cursor._workspaceKey-DPp5JSjy.js +0 -401
  314. package/apps/ui/dist/server/assets/cursor._workspaceKey-nmg3YIOQ.js +0 -11
  315. package/apps/ui/dist/server/assets/cursor.index-CcsX7DG0.js +0 -11
  316. package/apps/ui/dist/server/assets/cursor.index-DkWT-mxw.js +0 -189
  317. package/apps/ui/dist/server/assets/data-table-Cdct823O.js +0 -189
  318. package/apps/ui/dist/server/assets/delete-confirm-dialog-BcBNCxWB.js +0 -144
  319. package/apps/ui/dist/server/assets/download-DMmiy1xf.js +0 -92
  320. package/apps/ui/dist/server/assets/dropdown-menu-Dy_9t6TN.js +0 -36
  321. package/apps/ui/dist/server/assets/empty-plugin-adapters-B3lHh1La.js +0 -5
  322. package/apps/ui/dist/server/assets/export-dialog-D88ze9Gy.js +0 -240
  323. package/apps/ui/dist/server/assets/formatters-FJaGZgJk.js +0 -91
  324. package/apps/ui/dist/server/assets/kiro-db--fXrED_-.js +0 -552
  325. package/apps/ui/dist/server/assets/kiro-queries-CzNlM2ok.js +0 -37
  326. package/apps/ui/dist/server/assets/kiro-server-9WRxg2Au.js +0 -72
  327. package/apps/ui/dist/server/assets/kiro-sessions._sessionId-CRY2c7YO.js +0 -11
  328. package/apps/ui/dist/server/assets/kiro-sessions._sessionId-F1UlwxmR.js +0 -449
  329. package/apps/ui/dist/server/assets/kiro-sessions._sessionId-bvTulT7C.js +0 -18
  330. package/apps/ui/dist/server/assets/kiro-transcript-DoekQBaH.js +0 -112
  331. package/apps/ui/dist/server/assets/kiro-transcript-phase-CJG40nQg.js +0 -27
  332. package/apps/ui/dist/server/assets/kiro._workspaceKey-BDe9sQwo.js +0 -11
  333. package/apps/ui/dist/server/assets/kiro._workspaceKey-CJsjgNwO.js +0 -28
  334. package/apps/ui/dist/server/assets/kiro._workspaceKey-D_-4Qb7y.js +0 -193
  335. package/apps/ui/dist/server/assets/kiro.index-C__-UROk.js +0 -11
  336. package/apps/ui/dist/server/assets/kiro.index-CmTbqeYj.js +0 -93
  337. package/apps/ui/dist/server/assets/loading-panel-BGFnWseS.js +0 -27
  338. package/apps/ui/dist/server/assets/metric-card-ByEeLu0r.js +0 -23
  339. package/apps/ui/dist/server/assets/model-label-B1NWGc65.js +0 -13
  340. package/apps/ui/dist/server/assets/opencode-db-BM7KjOzc.js +0 -397
  341. package/apps/ui/dist/server/assets/opencode-queries-BeNYXnQv.js +0 -50
  342. package/apps/ui/dist/server/assets/opencode-server-BpQ_kRze.js +0 -72
  343. package/apps/ui/dist/server/assets/opencode-sessions._sessionId-CGQ-t0vD.js +0 -518
  344. package/apps/ui/dist/server/assets/opencode-sessions._sessionId-D8Q7in_Z.js +0 -18
  345. package/apps/ui/dist/server/assets/opencode-sessions._sessionId-DKOvi0lV.js +0 -11
  346. package/apps/ui/dist/server/assets/opencode-think-tags-CAoD-EcZ.js +0 -87
  347. package/apps/ui/dist/server/assets/opencode-transcript-BNTcGNym.js +0 -141
  348. package/apps/ui/dist/server/assets/opencode-transcript-phase-OqLkiQD0.js +0 -32
  349. package/apps/ui/dist/server/assets/opencode._workspaceKey-BdqcWHw1.js +0 -11
  350. package/apps/ui/dist/server/assets/opencode._workspaceKey-CfH96HTT.js +0 -28
  351. package/apps/ui/dist/server/assets/opencode._workspaceKey-DdbHPDrO.js +0 -207
  352. package/apps/ui/dist/server/assets/opencode.index-BbVzkJXP.js +0 -11
  353. package/apps/ui/dist/server/assets/opencode.index-Vh_Gq_1L.js +0 -93
  354. package/apps/ui/dist/server/assets/page-header-VNSaM3xd.js +0 -29
  355. package/apps/ui/dist/server/assets/path-transforms-DL2IwtYd.js +0 -31
  356. package/apps/ui/dist/server/assets/projects._project-4io5LO0E.js +0 -20
  357. package/apps/ui/dist/server/assets/projects._project-Bshqk7JA.js +0 -12
  358. package/apps/ui/dist/server/assets/projects._project-D6ZoGhJ8.js +0 -395
  359. package/apps/ui/dist/server/assets/projects.index-BLXOx5eL.js +0 -12
  360. package/apps/ui/dist/server/assets/projects.index-BkLiF2FF.js +0 -182
  361. package/apps/ui/dist/server/assets/projects.index-DesYXwfi.js +0 -14
  362. package/apps/ui/dist/server/assets/reload-error-panel-BJMxY3U1.js +0 -25
  363. package/apps/ui/dist/server/assets/route-search-ts4W9MJ9.js +0 -38
  364. package/apps/ui/dist/server/assets/router-Sac2DGk6.js +0 -518
  365. package/apps/ui/dist/server/assets/routes-Bsact1uB.js +0 -184
  366. package/apps/ui/dist/server/assets/routes-BzkJgq4W.js +0 -34
  367. package/apps/ui/dist/server/assets/select-GW76p-ld.js +0 -76
  368. package/apps/ui/dist/server/assets/settings-OayxIYQQ.js +0 -100
  369. package/apps/ui/dist/server/assets/settings-store-DpEJEQ7M.js +0 -52
  370. package/apps/ui/dist/server/assets/shared-DyhChtHf.js +0 -137
  371. package/apps/ui/dist/server/assets/source-session-export-server-DNutCawb.js +0 -38
  372. package/apps/ui/dist/server/assets/sqlite-error-LZDrnxdd.js +0 -13
  373. package/apps/ui/dist/server/assets/start-B_HFwkkk.js +0 -4
  374. package/apps/ui/dist/server/assets/tabs-DkaChNVg.js +0 -501
  375. package/apps/ui/dist/server/assets/text-filter-CGKxMCKt.js +0 -36
  376. package/apps/ui/dist/server/assets/thread-transcript-stats-Dh34mt2u.js +0 -45
  377. package/apps/ui/dist/server/assets/threads._threadId-BSSK4nkI.js +0 -26
  378. package/apps/ui/dist/server/assets/threads._threadId-BeXEc4Lo.js +0 -648
  379. package/apps/ui/dist/server/assets/threads._threadId-Ci0S4mKU.js +0 -18
  380. package/apps/ui/dist/server/assets/ui-export-files-BHLX9bhN.js +0 -83
  381. package/apps/ui/dist/server/assets/utils-C_uf36nf.js +0 -8
  382. package/apps/ui/dist/server/server.js +0 -5895
  383. package/bin/codex-chats-claude.js +0 -5
  384. package/bin/codex-chats.js +0 -5
  385. package/bin/spiracha.js +0 -5
  386. package/src/export-chats.ts +0 -120
  387. package/src/export-claude.ts +0 -36
  388. package/src/export-cursor.ts +0 -244
  389. package/src/lib/claude-exporter.ts +0 -864
  390. package/src/lib/codex-exporter-cli.ts +0 -277
  391. package/src/lib/codex-exporter-db.ts +0 -319
  392. package/src/lib/codex-exporter.ts +0 -115
  393. package/src/lib/cursor-exporter.ts +0 -266
  394. package/src/lib/interactive-cli.ts +0 -433
  395. package/src/lib/native-open.ts +0 -54
  396. package/src/mcp-server.ts +0 -137
  397. package/src/spiracha.ts +0 -116
  398. package/src/ui-cli.ts +0 -310
  399. /package/apps/ui/{dist/client → public}/icon.svg +0 -0
  400. /package/apps/ui/{dist/client → public}/manifest.json +0 -0
  401. /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
+ };