spiracha 1.4.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 (319) hide show
  1. package/AGENTS.md +116 -164
  2. package/README.md +92 -237
  3. package/apps/ui/AGENTS.md +36 -7
  4. package/apps/ui/README.md +79 -6
  5. package/apps/ui/components.json +21 -0
  6. package/apps/ui/package.json +65 -0
  7. package/apps/ui/{dist/client → public}/icon.svg +2 -1
  8. package/apps/ui/src/components/antigravity-conversations-table.tsx +206 -0
  9. package/apps/ui/src/components/antigravity-keychain-panel.tsx +76 -0
  10. package/apps/ui/src/components/antigravity-workspaces-table.tsx +63 -0
  11. package/apps/ui/src/components/app-shell.tsx +124 -0
  12. package/apps/ui/src/components/breadcrumbs.tsx +53 -0
  13. package/apps/ui/src/components/claude-code-sessions-table.tsx +114 -0
  14. package/apps/ui/src/components/claude-code-workspaces-table.tsx +55 -0
  15. package/apps/ui/src/components/cursor-threads-table.tsx +189 -0
  16. package/apps/ui/src/components/cursor-workspaces-table.tsx +140 -0
  17. package/apps/ui/src/components/data-table.tsx +241 -0
  18. package/apps/ui/src/components/delete-confirm-dialog.tsx +88 -0
  19. package/apps/ui/src/components/export-dialog.tsx +184 -0
  20. package/apps/ui/src/components/json-panel.tsx +17 -0
  21. package/apps/ui/src/components/kiro-sessions-table.tsx +112 -0
  22. package/apps/ui/src/components/kiro-workspaces-table.tsx +53 -0
  23. package/apps/ui/src/components/list-search-input.tsx +18 -0
  24. package/apps/ui/src/components/loading-panel.tsx +23 -0
  25. package/apps/ui/src/components/metadata-section.tsx +31 -0
  26. package/apps/ui/src/components/metric-card.tsx +19 -0
  27. package/apps/ui/src/components/opencode-sessions-table.tsx +128 -0
  28. package/apps/ui/src/components/opencode-workspaces-table.tsx +57 -0
  29. package/apps/ui/src/components/page-header.tsx +33 -0
  30. package/apps/ui/src/components/projects-loading-state.tsx +74 -0
  31. package/apps/ui/src/components/projects-table.tsx +108 -0
  32. package/apps/ui/src/components/qoder-sessions-table.tsx +112 -0
  33. package/apps/ui/src/components/qoder-workspaces-table.tsx +53 -0
  34. package/apps/ui/src/components/recent-threads-list.tsx +52 -0
  35. package/apps/ui/src/components/reload-error-panel.tsx +20 -0
  36. package/apps/ui/src/components/text-document-panel.tsx +19 -0
  37. package/apps/ui/src/components/theme-toggle.tsx +48 -0
  38. package/apps/ui/src/components/threads-table.tsx +202 -0
  39. package/apps/ui/src/components/transcript-view.tsx +552 -0
  40. package/apps/ui/src/components/ui/alert-dialog.tsx +160 -0
  41. package/apps/ui/src/components/ui/badge.tsx +45 -0
  42. package/apps/ui/src/components/ui/button.tsx +62 -0
  43. package/apps/ui/src/components/ui/checkbox.tsx +29 -0
  44. package/apps/ui/src/components/ui/dialog.tsx +137 -0
  45. package/apps/ui/src/components/ui/dropdown-menu.tsx +217 -0
  46. package/apps/ui/src/components/ui/input.tsx +21 -0
  47. package/apps/ui/src/components/ui/scroll-area.tsx +46 -0
  48. package/apps/ui/src/components/ui/select.tsx +163 -0
  49. package/apps/ui/src/components/ui/separator.tsx +28 -0
  50. package/apps/ui/src/components/ui/sheet.tsx +107 -0
  51. package/apps/ui/src/components/ui/skeleton.tsx +7 -0
  52. package/apps/ui/src/components/ui/table.tsx +76 -0
  53. package/apps/ui/src/components/ui/tabs.tsx +71 -0
  54. package/apps/ui/src/components/ui/tooltip.tsx +44 -0
  55. package/apps/ui/src/integrations/tanstack-query/devtools.tsx +6 -0
  56. package/apps/ui/src/integrations/tanstack-query/root-provider.tsx +10 -0
  57. package/apps/ui/src/lib/antigravity-conversation-state.ts +28 -0
  58. package/apps/ui/src/lib/antigravity-queries.ts +33 -0
  59. package/apps/ui/src/lib/antigravity-server.ts +128 -0
  60. package/apps/ui/src/lib/antigravity-transcript-events.ts +335 -0
  61. package/apps/ui/src/lib/claude-code-queries.ts +26 -0
  62. package/apps/ui/src/lib/claude-code-server.ts +75 -0
  63. package/apps/ui/src/lib/claude-code-transcript-events.ts +154 -0
  64. package/apps/ui/src/lib/codex-queries.ts +66 -0
  65. package/apps/ui/src/lib/codex-server.ts +209 -0
  66. package/apps/ui/src/lib/cursor-queries.ts +37 -0
  67. package/apps/ui/src/lib/cursor-server.ts +272 -0
  68. package/apps/ui/src/lib/cursor-transcript-events.ts +229 -0
  69. package/apps/ui/src/lib/download.ts +166 -0
  70. package/apps/ui/src/lib/formatters.ts +149 -0
  71. package/apps/ui/src/lib/kiro-queries.ts +22 -0
  72. package/apps/ui/src/lib/kiro-server.ts +73 -0
  73. package/apps/ui/src/lib/kiro-transcript-events.ts +113 -0
  74. package/apps/ui/src/lib/opencode-queries.ts +37 -0
  75. package/apps/ui/src/lib/opencode-server.ts +73 -0
  76. package/apps/ui/src/lib/opencode-transcript-events.ts +221 -0
  77. package/apps/ui/src/lib/package-metadata.ts +16 -0
  78. package/apps/ui/src/lib/path-utils.ts +13 -0
  79. package/apps/ui/src/lib/qoder-queries.ts +22 -0
  80. package/apps/ui/src/lib/qoder-server.ts +79 -0
  81. package/apps/ui/src/lib/qoder-transcript-events.ts +139 -0
  82. package/apps/ui/src/lib/route-search.ts +68 -0
  83. package/apps/ui/src/lib/settings-store.tsx +59 -0
  84. package/apps/ui/src/lib/source-session-export-server.ts +56 -0
  85. package/apps/ui/src/lib/text-filter.ts +22 -0
  86. package/apps/ui/src/lib/thread-id.ts +3 -0
  87. package/apps/ui/src/lib/thread-transcript-stats.ts +57 -0
  88. package/apps/ui/src/lib/utils.ts +7 -0
  89. package/apps/ui/src/routeTree.gen.ts +730 -0
  90. package/apps/ui/src/router.tsx +20 -0
  91. package/apps/ui/src/routes/$threadId.tsx +22 -0
  92. package/apps/ui/src/routes/__root.tsx +127 -0
  93. package/apps/ui/src/routes/analytics.tsx +143 -0
  94. package/apps/ui/src/routes/antigravity-conversations.$conversationId.tsx +438 -0
  95. package/apps/ui/src/routes/antigravity.$workspaceKey.tsx +127 -0
  96. package/apps/ui/src/routes/antigravity.index.tsx +51 -0
  97. package/apps/ui/src/routes/api.v1.conversation-query.ts +12 -0
  98. package/apps/ui/src/routes/api.v1.conversations.$source.$id.export.ts +12 -0
  99. package/apps/ui/src/routes/api.v1.conversations.$source.$id.ts +12 -0
  100. package/apps/ui/src/routes/api.v1.conversations.ts +12 -0
  101. package/apps/ui/src/routes/api.v1.resolve.ts +12 -0
  102. package/apps/ui/src/routes/api.v1.sources.ts +12 -0
  103. package/apps/ui/src/routes/claude-code-sessions.$sessionId.tsx +342 -0
  104. package/apps/ui/src/routes/claude-code.$workspaceKey.tsx +135 -0
  105. package/apps/ui/src/routes/claude-code.index.tsx +48 -0
  106. package/apps/ui/src/routes/codex.$project.tsx +390 -0
  107. package/apps/ui/src/routes/codex.index.tsx +116 -0
  108. package/apps/ui/src/routes/cursor-threads.$composerId.tsx +435 -0
  109. package/apps/ui/src/routes/cursor.$workspaceKey.tsx +416 -0
  110. package/apps/ui/src/routes/cursor.index.tsx +120 -0
  111. package/apps/ui/src/routes/index.tsx +124 -0
  112. package/apps/ui/src/routes/kiro-sessions.$sessionId.tsx +345 -0
  113. package/apps/ui/src/routes/kiro.$workspaceKey.tsx +136 -0
  114. package/apps/ui/src/routes/kiro.index.tsx +48 -0
  115. package/apps/ui/src/routes/opencode-sessions.$sessionId.tsx +334 -0
  116. package/apps/ui/src/routes/opencode.$workspaceKey.tsx +135 -0
  117. package/apps/ui/src/routes/opencode.index.tsx +48 -0
  118. package/apps/ui/src/routes/qoder-sessions.$sessionId.tsx +341 -0
  119. package/apps/ui/src/routes/qoder.$workspaceKey.tsx +139 -0
  120. package/apps/ui/src/routes/qoder.index.tsx +48 -0
  121. package/apps/ui/src/routes/settings.tsx +91 -0
  122. package/apps/ui/src/routes/threads.$threadId.tsx +619 -0
  123. package/apps/ui/src/styles.css +122 -0
  124. package/apps/ui/tsconfig.json +29 -0
  125. package/apps/ui/vite.config.ts +84 -0
  126. package/bin/spiracha.ts +32 -0
  127. package/package.json +50 -60
  128. package/src/client.ts +290 -0
  129. package/src/lib/antigravity-db.ts +213 -19
  130. package/src/lib/antigravity-exporter-types.ts +1 -0
  131. package/src/lib/claude-code-db.ts +655 -0
  132. package/src/lib/claude-code-exporter-types.ts +110 -0
  133. package/src/lib/claude-code-transcript-phase.ts +25 -0
  134. package/src/lib/claude-code-transcript.ts +180 -0
  135. package/src/lib/codex-analytics.ts +1 -1
  136. package/src/lib/codex-browser-db.ts +856 -61
  137. package/src/lib/codex-browser-export.ts +24 -16
  138. package/src/lib/codex-browser-types.ts +1 -1
  139. package/src/lib/codex-thread-cache.ts +17 -1
  140. package/src/lib/{codex-exporter-types.ts → codex-thread-types.ts} +2 -30
  141. package/src/lib/{codex-exporter-transcript.ts → codex-transcript-renderer.ts} +64 -28
  142. package/src/lib/concurrency.ts +25 -0
  143. package/src/lib/conversation-api.ts +792 -0
  144. package/src/lib/conversation-data/adapter-helpers.ts +110 -0
  145. package/src/lib/conversation-data/antigravity-adapter.ts +148 -0
  146. package/src/lib/conversation-data/claude-code-adapter.ts +175 -0
  147. package/src/lib/conversation-data/codex-adapter.ts +245 -0
  148. package/src/lib/conversation-data/cursor-adapter.ts +171 -0
  149. package/src/lib/conversation-data/index.ts +311 -0
  150. package/src/lib/conversation-data/kiro-adapter.ts +140 -0
  151. package/src/lib/conversation-data/message-selector.ts +37 -0
  152. package/src/lib/conversation-data/opencode-adapter.ts +180 -0
  153. package/src/lib/conversation-data/path-match.ts +79 -0
  154. package/src/lib/conversation-data/qoder-adapter.ts +230 -0
  155. package/src/lib/conversation-data/types.ts +116 -0
  156. package/src/lib/cursor-db.ts +67 -21
  157. package/src/lib/cursor-exporter-types.ts +0 -19
  158. package/src/lib/kiro-db.ts +949 -0
  159. package/src/lib/kiro-exporter-types.ts +100 -0
  160. package/src/lib/kiro-transcript-phase.ts +41 -0
  161. package/src/lib/kiro-transcript.ts +115 -0
  162. package/src/lib/opencode-db.ts +610 -0
  163. package/src/lib/opencode-exporter-types.ts +129 -0
  164. package/src/lib/opencode-think-tags.ts +126 -0
  165. package/src/lib/opencode-transcript-phase.ts +50 -0
  166. package/src/lib/opencode-transcript.ts +194 -0
  167. package/src/lib/qoder-acp-client.ts +268 -0
  168. package/src/lib/qoder-db.ts +1601 -0
  169. package/src/lib/qoder-exporter-types.ts +114 -0
  170. package/src/lib/qoder-transcript-phase.ts +41 -0
  171. package/src/lib/qoder-transcript.ts +112 -0
  172. package/src/lib/shared.ts +25 -0
  173. package/src/lib/ui-export-archive.ts +61 -0
  174. package/apps/ui/dist/client/assets/_threadId-CAIeH5mq.js +0 -1
  175. package/apps/ui/dist/client/assets/analytics-DK2jdvmI.js +0 -1
  176. package/apps/ui/dist/client/assets/antigravity-conversations._conversationId-DmD0sN8X.js +0 -7
  177. package/apps/ui/dist/client/assets/antigravity-conversations._conversationId-KAcXtNxI.js +0 -1
  178. package/apps/ui/dist/client/assets/antigravity-keychain-panel-BRHVjFAM.js +0 -1
  179. package/apps/ui/dist/client/assets/antigravity._workspaceKey-ByitGWki.js +0 -1
  180. package/apps/ui/dist/client/assets/antigravity._workspaceKey-CGV7amas.js +0 -1
  181. package/apps/ui/dist/client/assets/antigravity.index-DEEkjlZa.js +0 -1
  182. package/apps/ui/dist/client/assets/antigravity.index-DgW25FEV.js +0 -1
  183. package/apps/ui/dist/client/assets/badge-DfWvPfKF.js +0 -1
  184. package/apps/ui/dist/client/assets/checkbox-CdHitXWQ.js +0 -1
  185. package/apps/ui/dist/client/assets/createServerFn-CgRRVpBH.js +0 -3
  186. package/apps/ui/dist/client/assets/cursor-threads._composerId-CmBFclK8.js +0 -1
  187. package/apps/ui/dist/client/assets/cursor-threads._composerId-Ti2gTm63.js +0 -1
  188. package/apps/ui/dist/client/assets/cursor._workspaceKey-CaAZjGmm.js +0 -1
  189. package/apps/ui/dist/client/assets/cursor._workspaceKey-DzD_c6j9.js +0 -1
  190. package/apps/ui/dist/client/assets/cursor.index-CrnMwDCi.js +0 -1
  191. package/apps/ui/dist/client/assets/cursor.index-PkCFVcl4.js +0 -2
  192. package/apps/ui/dist/client/assets/data-table-Br78_dk3.js +0 -4
  193. package/apps/ui/dist/client/assets/delete-confirm-dialog-BHRmyLHr.js +0 -11
  194. package/apps/ui/dist/client/assets/dist-B9pyYCVU.js +0 -1
  195. package/apps/ui/dist/client/assets/dist-BhZOxAPP.js +0 -1
  196. package/apps/ui/dist/client/assets/dist-DvMS2965.js +0 -1
  197. package/apps/ui/dist/client/assets/download-Bf0QJKD3.js +0 -1
  198. package/apps/ui/dist/client/assets/dropdown-menu-Dy5cLzTy.js +0 -1
  199. package/apps/ui/dist/client/assets/es2015-BkCWttyM.js +0 -41
  200. package/apps/ui/dist/client/assets/export-dialog-C86SYHJn.js +0 -1
  201. package/apps/ui/dist/client/assets/formatters-BM9kB7ed.js +0 -1
  202. package/apps/ui/dist/client/assets/index-CPvAP-jk.js +0 -149
  203. package/apps/ui/dist/client/assets/jsx-runtime-DGeXAQPT.js +0 -1
  204. package/apps/ui/dist/client/assets/metric-card-Dplm0ZiM.js +0 -1
  205. package/apps/ui/dist/client/assets/page-header-Cwa3p6Tl.js +0 -1
  206. package/apps/ui/dist/client/assets/projects._project-Ckf6muWZ.js +0 -1
  207. package/apps/ui/dist/client/assets/projects._project-DSDwH4NF.js +0 -1
  208. package/apps/ui/dist/client/assets/projects.index-DEfhWqa8.js +0 -1
  209. package/apps/ui/dist/client/assets/projects.index-V5ibNZII.js +0 -3
  210. package/apps/ui/dist/client/assets/react-dom-DL96Jor4.js +0 -1
  211. package/apps/ui/dist/client/assets/refresh-ccw-BgKjOiA6.js +0 -1
  212. package/apps/ui/dist/client/assets/reload-error-panel-AHqu0akK.js +0 -1
  213. package/apps/ui/dist/client/assets/routes-Df2tgpyd.js +0 -1
  214. package/apps/ui/dist/client/assets/scroll-text-fM0sMOB3.js +0 -1
  215. package/apps/ui/dist/client/assets/select-Bf2GwfPR.js +0 -1
  216. package/apps/ui/dist/client/assets/settings-5VQIzME0.js +0 -1
  217. package/apps/ui/dist/client/assets/sqlite-error-B6uZjUe8.js +0 -1
  218. package/apps/ui/dist/client/assets/styles-BhRkXgwB.css +0 -1
  219. package/apps/ui/dist/client/assets/tabs-OO4VU5KR.js +0 -7
  220. package/apps/ui/dist/client/assets/text-filter-DVAZySUS.js +0 -2
  221. package/apps/ui/dist/client/assets/threads._threadId-BB9tTsV4.js +0 -1
  222. package/apps/ui/dist/client/assets/threads._threadId-BgDhFj2I.js +0 -1
  223. package/apps/ui/dist/client/assets/useMutation-CC_B7uy5.js +0 -1
  224. package/apps/ui/dist/client/assets/useQuery-C5By6WKU.js +0 -1
  225. package/apps/ui/dist/server/assets/_tanstack-start-manifest_v-CRW2kqj0.js +0 -232
  226. package/apps/ui/dist/server/assets/_threadId-B6SrBR9E.js +0 -6
  227. package/apps/ui/dist/server/assets/analytics-CYP_LT5Y.js +0 -16
  228. package/apps/ui/dist/server/assets/analytics-CtpOMVMO.js +0 -146
  229. package/apps/ui/dist/server/assets/antigravity-conversation-state-HgzS302O.js +0 -16
  230. package/apps/ui/dist/server/assets/antigravity-conversations._conversationId-BtexWY-K.js +0 -613
  231. package/apps/ui/dist/server/assets/antigravity-conversations._conversationId-D426O-64.js +0 -11
  232. package/apps/ui/dist/server/assets/antigravity-conversations._conversationId-zMrR6v6R.js +0 -20
  233. package/apps/ui/dist/server/assets/antigravity-db-Bh8_U9uw.js +0 -580
  234. package/apps/ui/dist/server/assets/antigravity-keychain-DOiuHDwK.js +0 -126
  235. package/apps/ui/dist/server/assets/antigravity-keychain-panel-CC7FLmAK.js +0 -55
  236. package/apps/ui/dist/server/assets/antigravity-queries-CGrJO9Vr.js +0 -37
  237. package/apps/ui/dist/server/assets/antigravity-server-CHRjVFqN.js +0 -114
  238. package/apps/ui/dist/server/assets/antigravity._workspaceKey-3m_MzNFA.js +0 -11
  239. package/apps/ui/dist/server/assets/antigravity._workspaceKey-B0QeYUOh.js +0 -28
  240. package/apps/ui/dist/server/assets/antigravity._workspaceKey-C3kP-qyN.js +0 -210
  241. package/apps/ui/dist/server/assets/antigravity.index-CPCYkqCi.js +0 -104
  242. package/apps/ui/dist/server/assets/antigravity.index-DudTB3Tq.js +0 -11
  243. package/apps/ui/dist/server/assets/badge-EvdhKK_Z.js +0 -26
  244. package/apps/ui/dist/server/assets/button-CmTDnzOn.js +0 -46
  245. package/apps/ui/dist/server/assets/checkbox-C0hovF41.js +0 -19
  246. package/apps/ui/dist/server/assets/codex-queries-CtgeZ7VL.js +0 -99
  247. package/apps/ui/dist/server/assets/codex-server-CNXSJuc2.js +0 -2004
  248. package/apps/ui/dist/server/assets/concurrency-VXtYvlGh.js +0 -18
  249. package/apps/ui/dist/server/assets/createServerRpc-CF_DEwnm.js +0 -12
  250. package/apps/ui/dist/server/assets/createSsrRpc-C754NPki.js +0 -16
  251. package/apps/ui/dist/server/assets/cursor-db-cYZEU3WQ.js +0 -830
  252. package/apps/ui/dist/server/assets/cursor-exporter-types-CI3goo-c.js +0 -34
  253. package/apps/ui/dist/server/assets/cursor-queries-NCIM0Nat.js +0 -67
  254. package/apps/ui/dist/server/assets/cursor-recovery-nq-kR62j.js +0 -361
  255. package/apps/ui/dist/server/assets/cursor-server-C3q7hrp-.js +0 -213
  256. package/apps/ui/dist/server/assets/cursor-threads._composerId-BB0Y_Mao.js +0 -11
  257. package/apps/ui/dist/server/assets/cursor-threads._composerId-BPJFWfJj.js +0 -18
  258. package/apps/ui/dist/server/assets/cursor-threads._composerId-BkMMTQ3v.js +0 -582
  259. package/apps/ui/dist/server/assets/cursor-transcript-2iL3KFSK.js +0 -125
  260. package/apps/ui/dist/server/assets/cursor._workspaceKey-C5PdTaKB.js +0 -28
  261. package/apps/ui/dist/server/assets/cursor._workspaceKey-Ckj7apKI.js +0 -401
  262. package/apps/ui/dist/server/assets/cursor._workspaceKey-nmg3YIOQ.js +0 -11
  263. package/apps/ui/dist/server/assets/cursor.index-CcsX7DG0.js +0 -11
  264. package/apps/ui/dist/server/assets/cursor.index-DkWT-mxw.js +0 -189
  265. package/apps/ui/dist/server/assets/data-table-Cdct823O.js +0 -189
  266. package/apps/ui/dist/server/assets/delete-confirm-dialog-BcBNCxWB.js +0 -144
  267. package/apps/ui/dist/server/assets/download-DMmiy1xf.js +0 -92
  268. package/apps/ui/dist/server/assets/dropdown-menu-Dy_9t6TN.js +0 -36
  269. package/apps/ui/dist/server/assets/empty-plugin-adapters-B3lHh1La.js +0 -5
  270. package/apps/ui/dist/server/assets/export-dialog-BItjWgkZ.js +0 -240
  271. package/apps/ui/dist/server/assets/formatters-FJaGZgJk.js +0 -91
  272. package/apps/ui/dist/server/assets/loading-panel-BGFnWseS.js +0 -27
  273. package/apps/ui/dist/server/assets/metric-card-ByEeLu0r.js +0 -23
  274. package/apps/ui/dist/server/assets/model-label-B1NWGc65.js +0 -13
  275. package/apps/ui/dist/server/assets/page-header-VNSaM3xd.js +0 -29
  276. package/apps/ui/dist/server/assets/path-transforms-DL2IwtYd.js +0 -31
  277. package/apps/ui/dist/server/assets/projects._project-Bpbc3C-L.js +0 -20
  278. package/apps/ui/dist/server/assets/projects._project-Bshqk7JA.js +0 -12
  279. package/apps/ui/dist/server/assets/projects._project-CvZho6EQ.js +0 -395
  280. package/apps/ui/dist/server/assets/projects.index-BLXOx5eL.js +0 -12
  281. package/apps/ui/dist/server/assets/projects.index-BkLiF2FF.js +0 -182
  282. package/apps/ui/dist/server/assets/projects.index-DesYXwfi.js +0 -14
  283. package/apps/ui/dist/server/assets/reload-error-panel-BJMxY3U1.js +0 -25
  284. package/apps/ui/dist/server/assets/route-search-ts4W9MJ9.js +0 -38
  285. package/apps/ui/dist/server/assets/router-6m-ihwqA.js +0 -410
  286. package/apps/ui/dist/server/assets/routes-Bsact1uB.js +0 -184
  287. package/apps/ui/dist/server/assets/routes-BzkJgq4W.js +0 -34
  288. package/apps/ui/dist/server/assets/select-GW76p-ld.js +0 -76
  289. package/apps/ui/dist/server/assets/settings-OayxIYQQ.js +0 -100
  290. package/apps/ui/dist/server/assets/settings-store-DpEJEQ7M.js +0 -52
  291. package/apps/ui/dist/server/assets/shared-CPRNYIql.js +0 -134
  292. package/apps/ui/dist/server/assets/sqlite-error-LZDrnxdd.js +0 -13
  293. package/apps/ui/dist/server/assets/start-CI_0bSiY.js +0 -4
  294. package/apps/ui/dist/server/assets/tabs-CGA13IZM.js +0 -502
  295. package/apps/ui/dist/server/assets/text-filter-CGKxMCKt.js +0 -36
  296. package/apps/ui/dist/server/assets/threads._threadId-BSSK4nkI.js +0 -26
  297. package/apps/ui/dist/server/assets/threads._threadId-Bs0yuqO9.js +0 -18
  298. package/apps/ui/dist/server/assets/threads._threadId-D2GYkVn6.js +0 -639
  299. package/apps/ui/dist/server/assets/ui-export-files-BHLX9bhN.js +0 -83
  300. package/apps/ui/dist/server/assets/utils-C_uf36nf.js +0 -8
  301. package/apps/ui/dist/server/server.js +0 -5847
  302. package/bin/codex-chats-claude.js +0 -5
  303. package/bin/codex-chats.js +0 -5
  304. package/bin/spiracha.js +0 -5
  305. package/src/export-chats.ts +0 -120
  306. package/src/export-claude.ts +0 -36
  307. package/src/export-cursor.ts +0 -244
  308. package/src/lib/claude-exporter.ts +0 -864
  309. package/src/lib/codex-exporter-cli.ts +0 -277
  310. package/src/lib/codex-exporter-db.ts +0 -319
  311. package/src/lib/codex-exporter.ts +0 -115
  312. package/src/lib/cursor-exporter.ts +0 -266
  313. package/src/lib/interactive-cli.ts +0 -433
  314. package/src/lib/native-open.ts +0 -54
  315. package/src/mcp-server.ts +0 -137
  316. package/src/spiracha.ts +0 -116
  317. package/src/ui-cli.ts +0 -310
  318. /package/apps/ui/{dist/client → public}/manifest.json +0 -0
  319. /package/apps/ui/{dist/client → public}/robots.txt +0 -0
@@ -1,7 +1,10 @@
1
- import { Database } from 'bun:sqlite';
2
- import { rm } from 'node:fs/promises';
1
+ import { constants, Database } from 'bun:sqlite';
2
+ import { closeSync, openSync, readdirSync, readSync, type Stats, statSync } from 'node:fs';
3
+ import { rename, rm } from 'node:fs/promises';
3
4
  import os from 'node:os';
4
5
  import path from 'node:path';
6
+ import { StringDecoder } from 'node:string_decoder';
7
+ import { pathToFileURL } from 'node:url';
5
8
  import type {
6
9
  DashboardSummary,
7
10
  DeleteProjectResult,
@@ -11,9 +14,9 @@ import type {
11
14
  ThreadBrowseData,
12
15
  ThreadListEntry,
13
16
  } from './codex-browser-types';
14
- import type { ThreadRelations, ThreadRow } from './codex-exporter-types';
15
- import { DEFAULT_CODEX_DIR, DEFAULT_DB_PATH } from './codex-exporter-types';
16
17
  import { getCachedParsedCodexTranscript, getThreadRolloutLoadState } from './codex-thread-cache';
18
+ import type { ThreadRelations, ThreadRow } from './codex-thread-types';
19
+ import { DEFAULT_CODEX_DIR, DEFAULT_DB_PATH } from './codex-thread-types';
17
20
  import { mapWithConcurrency } from './concurrency';
18
21
  import { cleanInlineTitle, getPortablePathBasename } from './shared';
19
22
  import { runWithSqliteRetry } from './sqlite-retry';
@@ -30,6 +33,55 @@ type DeleteProjectOptions = {
30
33
  const SQLITE_DELETE_BATCH_SIZE = 400;
31
34
  const SESSION_FILE_DELETE_CONCURRENCY = 16;
32
35
  const THREAD_LIST_IO_CONCURRENCY = 8;
36
+ const CODEX_READONLY_DB_OPEN_FLAGS = constants.SQLITE_OPEN_READONLY | constants.SQLITE_OPEN_URI;
37
+ const JSONL_READ_CHUNK_BYTES = 64 * 1024;
38
+ const SESSION_META_READ_LIMIT_BYTES = 4 * 1024 * 1024;
39
+ const FALLBACK_STATS_HEAD_READ_LIMIT_BYTES = 512 * 1024;
40
+ const FALLBACK_STATS_TAIL_READ_LIMIT_BYTES = 512 * 1024;
41
+ const FALLBACK_STATS_RECORD_PATTERN = /"type"\s*:\s*"(?:agent_message|message|token_count|turn_context)"/u;
42
+ const THREAD_ID_PATTERN = /([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\.jsonl$/iu;
43
+ const sessionFileIndexCache = new Map<string, Map<string, string>>();
44
+ let sessionIndexMutationQueue = Promise.resolve();
45
+
46
+ type SessionIndexEntry = {
47
+ id: string;
48
+ thread_name?: string;
49
+ updated_at?: string;
50
+ };
51
+
52
+ type FallbackSessionMeta = {
53
+ agent_nickname?: string;
54
+ agent_path?: string;
55
+ agent_role?: string;
56
+ cli_version?: string;
57
+ cwd?: string;
58
+ forked_from_id?: string;
59
+ id?: string;
60
+ model_provider?: string;
61
+ parent_thread_id?: string;
62
+ source?: unknown;
63
+ thread_source?: string;
64
+ timestamp?: string;
65
+ };
66
+
67
+ type ReadFallbackThreadRowsOptions = {
68
+ includeSubagents?: boolean;
69
+ };
70
+
71
+ type FallbackRolloutStats = {
72
+ model: string | null;
73
+ tokensUsed: number;
74
+ };
75
+
76
+ type FallbackThreadRowOptions = ReadFallbackThreadRowsOptions & {
77
+ projectName?: string | null;
78
+ };
79
+
80
+ const isSqliteCantOpenError = (error: unknown) => {
81
+ return (error as { code?: unknown }).code === 'SQLITE_CANTOPEN';
82
+ };
83
+
84
+ const uniqueValues = <T>(values: T[]) => [...new Set(values)];
33
85
 
34
86
  const chunkValues = <T>(values: T[], chunkSize: number) => {
35
87
  const chunks: T[][] = [];
@@ -49,15 +101,20 @@ const isPromiseLike = (value: unknown): value is PromiseLike<unknown> => {
49
101
  return 'then' in value && typeof value.then === 'function';
50
102
  };
51
103
 
52
- const openReadonlyDb = (dbPath: string, busyTimeoutMs: number) => {
104
+ const openReadonlyDb = (dbPath: string) => {
53
105
  const db = new Database(dbPath, { readonly: true });
54
106
  try {
55
- db.exec(`PRAGMA busy_timeout = ${busyTimeoutMs}`);
107
+ db.query('SELECT name FROM sqlite_master LIMIT 1').get();
56
108
  return db;
57
109
  } catch (error) {
58
110
  db.close();
59
- throw error;
111
+ if (!isSqliteCantOpenError(error)) {
112
+ throw error;
113
+ }
60
114
  }
115
+
116
+ // Codex uses WAL mode; immutable URI reads keep Bun usable after clean shutdown removes sidecar files.
117
+ return new Database(`${pathToFileURL(dbPath).href}?immutable=1`, CODEX_READONLY_DB_OPEN_FLAGS);
61
118
  };
62
119
 
63
120
  const openWritableDb = (dbPath: string, busyTimeoutMs: number) => {
@@ -102,7 +159,7 @@ const parseJsonSafely = (value: string | null) => {
102
159
  export const withReadonlyDb = <T>(dbPath: string, callback: (db: Database) => T): T => {
103
160
  return runWithSqliteRetry({
104
161
  action: () => {
105
- const db = openReadonlyDb(dbPath, 5000);
162
+ const db = openReadonlyDb(dbPath);
106
163
  try {
107
164
  const result = callback(db);
108
165
  if (isPromiseLike(result)) {
@@ -149,12 +206,10 @@ export const resolveCodexThreadDbPath = () => {
149
206
 
150
207
  for (const candidate of candidates) {
151
208
  try {
152
- const db = runWithSqliteRetry({
153
- action: () => {
154
- return openReadonlyDb(candidate, 1500);
155
- },
156
- });
157
- db.close();
209
+ // Avoid opening candidates as a probe: Bun can make later read-only opens fail on Codex WAL databases.
210
+ if (!statSync(candidate).isFile()) {
211
+ continue;
212
+ }
158
213
  return candidate;
159
214
  } catch {}
160
215
  }
@@ -170,6 +225,597 @@ const readAllThreads = (dbPath: string): ThreadRow[] => {
170
225
  });
171
226
  };
172
227
 
228
+ const resolveCodexDirFromDbPath = (dbPath: string) => {
229
+ const dbDir = path.dirname(dbPath);
230
+ return path.basename(dbDir) === 'sqlite' ? path.dirname(dbDir) : dbDir;
231
+ };
232
+
233
+ const parseJsonlObject = <T>(line: string): T | null => {
234
+ try {
235
+ return JSON.parse(line) as T;
236
+ } catch {
237
+ return null;
238
+ }
239
+ };
240
+
241
+ const emitJsonlLine = <T>(line: string, onRecord: (record: T) => void) => {
242
+ const trimmed = line.trim();
243
+ const parsed = trimmed ? parseJsonlObject<T>(trimmed) : null;
244
+ if (parsed) {
245
+ onRecord(parsed);
246
+ }
247
+ };
248
+
249
+ const emitCompleteJsonlLines = <T>(text: string, onRecord: (record: T) => void): string => {
250
+ const lines = text.split(/\r?\n/u);
251
+ const pending = lines.pop() ?? '';
252
+ for (const line of lines) {
253
+ emitJsonlLine(line, onRecord);
254
+ }
255
+ return pending;
256
+ };
257
+
258
+ const readJsonlObjects = <T>(filePath: string, onRecord: (record: T) => void) => {
259
+ let descriptor: number | null = null;
260
+ try {
261
+ const stats = statSync(filePath);
262
+ if (!stats.isFile()) {
263
+ return;
264
+ }
265
+
266
+ descriptor = openSync(filePath, 'r');
267
+ const buffer = Buffer.alloc(JSONL_READ_CHUNK_BYTES);
268
+ const decoder = new StringDecoder('utf8');
269
+ let position = 0;
270
+ let pending = '';
271
+
272
+ while (true) {
273
+ const bytesRead = readSync(descriptor, buffer, 0, buffer.length, position);
274
+ if (bytesRead === 0) {
275
+ break;
276
+ }
277
+
278
+ position += bytesRead;
279
+ pending += decoder.write(buffer.subarray(0, bytesRead));
280
+ pending = emitCompleteJsonlLines(pending, onRecord);
281
+ }
282
+
283
+ emitJsonlLine(pending + decoder.end(), onRecord);
284
+ } catch {
285
+ return;
286
+ } finally {
287
+ if (descriptor !== null) {
288
+ closeSync(descriptor);
289
+ }
290
+ }
291
+ };
292
+
293
+ const collectJsonlObjects = <T>(filePath: string): T[] => {
294
+ const records: T[] = [];
295
+ readJsonlObjects<T>(filePath, (record) => {
296
+ records.push(record);
297
+ });
298
+ return records;
299
+ };
300
+
301
+ const readSessionIndexEntries = (codexDir: string): SessionIndexEntry[] => {
302
+ return collectJsonlObjects<SessionIndexEntry>(path.join(codexDir, 'session_index.jsonl')).filter(
303
+ (entry) => typeof entry.id === 'string' && entry.id.length > 0,
304
+ );
305
+ };
306
+
307
+ const collectSessionFilesByThreadId = (sessionsDir: string): Map<string, string> => {
308
+ const sessionFiles = new Map<string, string>();
309
+ const visit = (directory: string) => {
310
+ const entries = (() => {
311
+ try {
312
+ return readdirSync(directory, { withFileTypes: true });
313
+ } catch {
314
+ return null;
315
+ }
316
+ })();
317
+ if (!entries) {
318
+ return;
319
+ }
320
+
321
+ for (const entry of entries) {
322
+ const entryPath = path.join(directory, entry.name);
323
+ if (entry.isDirectory()) {
324
+ visit(entryPath);
325
+ continue;
326
+ }
327
+
328
+ if (!entry.isFile()) {
329
+ continue;
330
+ }
331
+
332
+ const threadId = THREAD_ID_PATTERN.exec(entry.name)?.[1];
333
+ if (threadId && !sessionFiles.has(threadId)) {
334
+ sessionFiles.set(threadId, entryPath);
335
+ }
336
+ }
337
+ };
338
+
339
+ visit(sessionsDir);
340
+ return sessionFiles;
341
+ };
342
+
343
+ const findSessionFileByThreadId = (sessionsDir: string, threadId: string): string | null => {
344
+ const lookup = (sessionFilesByThreadId: Map<string, string>) => {
345
+ const sessionFile = sessionFilesByThreadId.get(threadId);
346
+ if (!sessionFile) {
347
+ return null;
348
+ }
349
+
350
+ try {
351
+ return statSync(sessionFile).isFile() ? sessionFile : null;
352
+ } catch {
353
+ return null;
354
+ }
355
+ };
356
+
357
+ const cached = sessionFileIndexCache.get(sessionsDir);
358
+ const cachedMatch = cached ? lookup(cached) : null;
359
+ if (cachedMatch) {
360
+ return cachedMatch;
361
+ }
362
+
363
+ const refreshed = collectSessionFilesByThreadId(sessionsDir);
364
+ sessionFileIndexCache.set(sessionsDir, refreshed);
365
+ return lookup(refreshed);
366
+ };
367
+
368
+ const readSessionMetaLine = (sessionFile: string): string | null => {
369
+ let descriptor: number | null = null;
370
+ try {
371
+ descriptor = openSync(sessionFile, 'r');
372
+ const chunks: Buffer[] = [];
373
+ let totalBytes = 0;
374
+
375
+ while (totalBytes < SESSION_META_READ_LIMIT_BYTES) {
376
+ const buffer = Buffer.alloc(Math.min(64 * 1024, SESSION_META_READ_LIMIT_BYTES - totalBytes));
377
+ const bytesRead = readSync(descriptor, buffer, 0, buffer.length, totalBytes);
378
+ if (bytesRead === 0) {
379
+ break;
380
+ }
381
+
382
+ chunks.push(buffer.subarray(0, bytesRead));
383
+ totalBytes += bytesRead;
384
+ if (buffer.subarray(0, bytesRead).includes(10)) {
385
+ break;
386
+ }
387
+ }
388
+
389
+ const firstLine = Buffer.concat(chunks).toString('utf8').split(/\r?\n/u)[0]?.trim();
390
+ return firstLine || null;
391
+ } catch {
392
+ return null;
393
+ } finally {
394
+ if (descriptor !== null) {
395
+ closeSync(descriptor);
396
+ }
397
+ }
398
+ };
399
+
400
+ const readFallbackSessionMeta = (sessionFile: string): FallbackSessionMeta | null => {
401
+ const line = readSessionMetaLine(sessionFile);
402
+ if (!line) {
403
+ return null;
404
+ }
405
+
406
+ try {
407
+ const record = JSON.parse(line) as { payload?: FallbackSessionMeta; type?: string };
408
+ return record.type === 'session_meta' && record.payload ? record.payload : null;
409
+ } catch {
410
+ return null;
411
+ }
412
+ };
413
+
414
+ const parseIsoMs = (value: string | undefined, fallback: number) => {
415
+ if (!value) {
416
+ return fallback;
417
+ }
418
+
419
+ const parsed = Date.parse(value);
420
+ return Number.isFinite(parsed) ? parsed : fallback;
421
+ };
422
+
423
+ const stringOrNull = (value: unknown) => (typeof value === 'string' && value.length > 0 ? value : null);
424
+
425
+ const numberOrNull = (value: unknown) => (typeof value === 'number' && Number.isFinite(value) ? value : null);
426
+
427
+ const objectOrNull = (value: unknown) => {
428
+ return typeof value === 'object' && value !== null && !Array.isArray(value)
429
+ ? (value as Record<string, unknown>)
430
+ : null;
431
+ };
432
+
433
+ const isFallbackSubagent = (sessionMeta: FallbackSessionMeta) => {
434
+ return Boolean(
435
+ sessionMeta.thread_source === 'subagent' ||
436
+ stringOrNull(sessionMeta.parent_thread_id) ||
437
+ stringOrNull(sessionMeta.forked_from_id),
438
+ );
439
+ };
440
+
441
+ const updateFallbackRolloutStatsFromRecord = (record: Record<string, unknown>, stats: FallbackRolloutStats): void => {
442
+ const payload = objectOrNull(record.payload);
443
+ if (!payload) {
444
+ return;
445
+ }
446
+
447
+ if (record.type === 'turn_context') {
448
+ stats.model = stringOrNull(payload.model) ?? stats.model;
449
+ return;
450
+ }
451
+
452
+ const payloadType = stringOrNull(payload.type);
453
+ if (payloadType === 'message' || payloadType === 'agent_message') {
454
+ stats.model = stringOrNull(payload.model) ?? stats.model;
455
+ return;
456
+ }
457
+
458
+ if (payloadType !== 'token_count') {
459
+ return;
460
+ }
461
+
462
+ const info = objectOrNull(payload.info);
463
+ const totalTokenUsage = objectOrNull(info?.total_token_usage);
464
+ stats.tokensUsed = numberOrNull(totalTokenUsage?.total_tokens) ?? stats.tokensUsed;
465
+ };
466
+
467
+ const readFallbackStatsLine = (line: string, stats: FallbackRolloutStats) => {
468
+ const trimmed = line.trim();
469
+ if (!trimmed || !FALLBACK_STATS_RECORD_PATTERN.test(trimmed)) {
470
+ return;
471
+ }
472
+
473
+ const record = parseJsonlObject<Record<string, unknown>>(trimmed);
474
+ if (record) {
475
+ updateFallbackRolloutStatsFromRecord(record, stats);
476
+ }
477
+ };
478
+
479
+ const emitCompleteFallbackStatsLines = (text: string, stats: FallbackRolloutStats): string => {
480
+ const lines = text.split(/\r?\n/u);
481
+ const pending = lines.pop() ?? '';
482
+ for (const line of lines) {
483
+ readFallbackStatsLine(line, stats);
484
+ }
485
+ return pending;
486
+ };
487
+
488
+ const readFallbackRolloutStatsHead = (sessionFile: string, stats: FallbackRolloutStats, fileStats: Stats) => {
489
+ let descriptor: number | null = null;
490
+ try {
491
+ if (!fileStats.isFile()) {
492
+ return 0;
493
+ }
494
+
495
+ descriptor = openSync(sessionFile, 'r');
496
+ const buffer = Buffer.alloc(JSONL_READ_CHUNK_BYTES);
497
+ const decoder = new StringDecoder('utf8');
498
+ const readLimitBytes = Math.min(fileStats.size, FALLBACK_STATS_HEAD_READ_LIMIT_BYTES);
499
+ let position = 0;
500
+ let pending = '';
501
+
502
+ while (position < readLimitBytes) {
503
+ const bytesRead = readSync(
504
+ descriptor,
505
+ buffer,
506
+ 0,
507
+ Math.min(buffer.length, readLimitBytes - position),
508
+ position,
509
+ );
510
+ if (bytesRead === 0) {
511
+ break;
512
+ }
513
+
514
+ position += bytesRead;
515
+ pending += decoder.write(buffer.subarray(0, bytesRead));
516
+ pending = emitCompleteFallbackStatsLines(pending, stats);
517
+ }
518
+
519
+ if (position >= fileStats.size) {
520
+ readFallbackStatsLine(pending + decoder.end(), stats);
521
+ return position;
522
+ }
523
+
524
+ decoder.end();
525
+ return position;
526
+ } catch {
527
+ return 0;
528
+ } finally {
529
+ if (descriptor !== null) {
530
+ closeSync(descriptor);
531
+ }
532
+ }
533
+ };
534
+
535
+ const trimPartialLeadingJsonlLine = (text: string) => {
536
+ if (text.startsWith('\r\n')) {
537
+ return text.slice(2);
538
+ }
539
+
540
+ if (text.startsWith('\n')) {
541
+ return text.slice(1);
542
+ }
543
+
544
+ const match = /\r?\n/u.exec(text);
545
+ return match ? text.slice(match.index + match[0].length) : '';
546
+ };
547
+
548
+ const readFallbackRolloutStatsTail = (
549
+ sessionFile: string,
550
+ stats: FallbackRolloutStats,
551
+ fileStats: Stats,
552
+ coveredPrefixBytes: number,
553
+ ) => {
554
+ let descriptor: number | null = null;
555
+ try {
556
+ if (!fileStats.isFile() || fileStats.size === 0) {
557
+ return;
558
+ }
559
+
560
+ const suffixStart = Math.max(coveredPrefixBytes, fileStats.size - FALLBACK_STATS_TAIL_READ_LIMIT_BYTES);
561
+ if (suffixStart >= fileStats.size) {
562
+ return;
563
+ }
564
+
565
+ const readStart = suffixStart > 0 ? suffixStart - 1 : 0;
566
+ const readLimitBytes = fileStats.size - readStart;
567
+ descriptor = openSync(sessionFile, 'r');
568
+ const buffer = Buffer.alloc(JSONL_READ_CHUNK_BYTES);
569
+ const decoder = new StringDecoder('utf8');
570
+ let position = readStart;
571
+ let remainingBytes = readLimitBytes;
572
+ let text = '';
573
+
574
+ while (remainingBytes > 0) {
575
+ const bytesRead = readSync(descriptor, buffer, 0, Math.min(buffer.length, remainingBytes), position);
576
+ if (bytesRead === 0) {
577
+ break;
578
+ }
579
+
580
+ position += bytesRead;
581
+ remainingBytes -= bytesRead;
582
+ text += decoder.write(buffer.subarray(0, bytesRead));
583
+ }
584
+
585
+ text += decoder.end();
586
+ const completeText = readStart > 0 ? trimPartialLeadingJsonlLine(text) : text;
587
+ for (const line of completeText.split(/\r?\n/u)) {
588
+ readFallbackStatsLine(line, stats);
589
+ }
590
+ } catch {
591
+ return;
592
+ } finally {
593
+ if (descriptor !== null) {
594
+ closeSync(descriptor);
595
+ }
596
+ }
597
+ };
598
+
599
+ const readFallbackRolloutStats = (sessionFile: string): FallbackRolloutStats => {
600
+ const stats: FallbackRolloutStats = {
601
+ model: null,
602
+ tokensUsed: 0,
603
+ };
604
+
605
+ try {
606
+ const fileStats = statSync(sessionFile);
607
+ if (!fileStats.isFile()) {
608
+ return stats;
609
+ }
610
+
611
+ const coveredPrefixBytes = readFallbackRolloutStatsHead(sessionFile, stats, fileStats);
612
+ readFallbackRolloutStatsTail(sessionFile, stats, fileStats, coveredPrefixBytes);
613
+ } catch {
614
+ return stats;
615
+ }
616
+
617
+ return stats;
618
+ };
619
+
620
+ const buildFallbackThreadRow = (
621
+ entry: SessionIndexEntry,
622
+ sessionFile: string,
623
+ sessionMeta: FallbackSessionMeta,
624
+ rolloutStats: FallbackRolloutStats,
625
+ ): ThreadRow | null => {
626
+ const cwd = stringOrNull(sessionMeta.cwd);
627
+ if (!cwd) {
628
+ return null;
629
+ }
630
+
631
+ let mtimeMs = Date.now();
632
+ try {
633
+ mtimeMs = statSync(sessionFile).mtimeMs;
634
+ } catch {}
635
+
636
+ const updatedAtMs = parseIsoMs(entry.updated_at, mtimeMs);
637
+ const createdAtMs = parseIsoMs(sessionMeta.timestamp, updatedAtMs);
638
+ const title = entry.thread_name?.trim() || path.basename(sessionFile, '.jsonl');
639
+ const source = stringOrNull(sessionMeta.source) ?? 'session_file';
640
+
641
+ return {
642
+ agent_nickname: sessionMeta.agent_nickname ?? null,
643
+ agent_path: sessionMeta.agent_path ?? null,
644
+ agent_role: sessionMeta.agent_role ?? null,
645
+ approval_mode: 'unknown',
646
+ archived: 0,
647
+ archived_at: null,
648
+ cli_version: sessionMeta.cli_version ?? '',
649
+ created_at: Math.floor(createdAtMs / 1000),
650
+ created_at_ms: Math.floor(createdAtMs),
651
+ cwd,
652
+ first_user_message: title,
653
+ git_branch: null,
654
+ git_origin_url: null,
655
+ git_sha: null,
656
+ has_user_event: sessionMeta.thread_source === 'user' ? 1 : 0,
657
+ id: entry.id,
658
+ memory_mode: 'enabled',
659
+ model: rolloutStats.model,
660
+ model_provider: sessionMeta.model_provider ?? 'unknown',
661
+ preview: title,
662
+ reasoning_effort: null,
663
+ rollout_path: sessionFile,
664
+ sandbox_policy: '{}',
665
+ source,
666
+ thread_source: sessionMeta.thread_source ?? null,
667
+ title,
668
+ tokens_used: rolloutStats.tokensUsed,
669
+ updated_at: Math.floor(updatedAtMs / 1000),
670
+ updated_at_ms: Math.floor(updatedAtMs),
671
+ };
672
+ };
673
+
674
+ const readFallbackThreadRow = (
675
+ entry: SessionIndexEntry,
676
+ sessionFile: string,
677
+ options: FallbackThreadRowOptions = {},
678
+ ): ThreadRow | null => {
679
+ const sessionMeta = readFallbackSessionMeta(sessionFile);
680
+ if (!sessionMeta) {
681
+ return null;
682
+ }
683
+
684
+ if (!options.includeSubagents && isFallbackSubagent(sessionMeta)) {
685
+ return null;
686
+ }
687
+
688
+ const cwd = stringOrNull(sessionMeta.cwd);
689
+ if (!cwd || (options.projectName && getPortablePathBasename(cwd) !== options.projectName)) {
690
+ return null;
691
+ }
692
+
693
+ return buildFallbackThreadRow(entry, sessionFile, sessionMeta, readFallbackRolloutStats(sessionFile));
694
+ };
695
+
696
+ const readFallbackThreadRows = (
697
+ dbPath: string,
698
+ existingThreadIds: Set<string>,
699
+ projectName: string | null = null,
700
+ options: ReadFallbackThreadRowsOptions = {},
701
+ ): ThreadRow[] => {
702
+ const codexDir = resolveCodexDirFromDbPath(dbPath);
703
+ const sessionFilesByThreadId = collectSessionFilesByThreadId(path.join(codexDir, 'sessions'));
704
+ const fallbackThreads: ThreadRow[] = [];
705
+
706
+ for (const entry of readSessionIndexEntries(codexDir)) {
707
+ if (existingThreadIds.has(entry.id)) {
708
+ continue;
709
+ }
710
+
711
+ const sessionFile = sessionFilesByThreadId.get(entry.id);
712
+ if (!sessionFile) {
713
+ continue;
714
+ }
715
+
716
+ const fallbackThread = readFallbackThreadRow(entry, sessionFile, {
717
+ ...options,
718
+ projectName,
719
+ });
720
+ if (!fallbackThread) {
721
+ continue;
722
+ }
723
+
724
+ fallbackThreads.push(fallbackThread);
725
+ }
726
+
727
+ return fallbackThreads;
728
+ };
729
+
730
+ const readFallbackThreadRowById = (
731
+ dbPath: string,
732
+ threadId: string,
733
+ options: ReadFallbackThreadRowsOptions = {},
734
+ ): ThreadRow | null => {
735
+ const codexDir = resolveCodexDirFromDbPath(dbPath);
736
+ const entry = readSessionIndexEntries(codexDir).find((candidate) => candidate.id === threadId);
737
+ if (!entry) {
738
+ return null;
739
+ }
740
+
741
+ const sessionFile = findSessionFileByThreadId(path.join(codexDir, 'sessions'), threadId);
742
+ if (!sessionFile) {
743
+ return null;
744
+ }
745
+
746
+ return readFallbackThreadRow(entry, sessionFile, options);
747
+ };
748
+
749
+ const mergeFallbackThreadRows = (dbPath: string, threads: ThreadRow[], projectName: string | null = null) => {
750
+ const threadIds = new Set(threads.map((thread) => thread.id));
751
+ return [...threads, ...readFallbackThreadRows(dbPath, threadIds, projectName)].sort((left, right) => {
752
+ const updatedDifference = toTimestampMs(right) - toTimestampMs(left);
753
+ if (updatedDifference !== 0) {
754
+ return updatedDifference;
755
+ }
756
+
757
+ return right.id.localeCompare(left.id);
758
+ });
759
+ };
760
+
761
+ const applyRolloutActivityTimestamps = (threads: ThreadRow[]) => {
762
+ return threads
763
+ .map((thread) => {
764
+ let rolloutUpdatedAtMs = toTimestampMs(thread);
765
+ try {
766
+ rolloutUpdatedAtMs = Math.max(rolloutUpdatedAtMs, statSync(thread.rollout_path).mtimeMs);
767
+ } catch {}
768
+
769
+ if (rolloutUpdatedAtMs <= toTimestampMs(thread)) {
770
+ return thread;
771
+ }
772
+
773
+ return {
774
+ ...thread,
775
+ updated_at: Math.floor(rolloutUpdatedAtMs / 1000),
776
+ updated_at_ms: Math.floor(rolloutUpdatedAtMs),
777
+ };
778
+ })
779
+ .sort((left, right) => {
780
+ const updatedDifference = toTimestampMs(right) - toTimestampMs(left);
781
+ if (updatedDifference !== 0) {
782
+ return updatedDifference;
783
+ }
784
+
785
+ return right.id.localeCompare(left.id);
786
+ });
787
+ };
788
+
789
+ const buildDashboardRecentThreads = (threads: ThreadRow[]) => {
790
+ const bestThreadByProject = new Map<string, ThreadRow>();
791
+ for (const thread of threads) {
792
+ const project = getPortablePathBasename(thread.cwd);
793
+ if (!project) {
794
+ continue;
795
+ }
796
+
797
+ const current = bestThreadByProject.get(project);
798
+ if (!current || toTimestampMs(thread) > toTimestampMs(current)) {
799
+ bestThreadByProject.set(project, thread);
800
+ }
801
+ }
802
+
803
+ return [...bestThreadByProject.values()]
804
+ .sort((left, right) => {
805
+ const updatedDifference = toTimestampMs(right) - toTimestampMs(left);
806
+ if (updatedDifference !== 0) {
807
+ return updatedDifference;
808
+ }
809
+
810
+ return right.id.localeCompare(left.id);
811
+ })
812
+ .slice(0, 5)
813
+ .map((thread) => ({
814
+ project: getPortablePathBasename(thread.cwd),
815
+ thread: compactThreadListRow(thread),
816
+ }));
817
+ };
818
+
173
819
  const filterThreadsByProject = (threads: ThreadRow[], projectName: string | null) => {
174
820
  if (!projectName) {
175
821
  return threads;
@@ -366,11 +1012,140 @@ const deleteThreadSessionFiles = async (sessionFiles: string[]) => {
366
1012
  return uniqueSessionFiles;
367
1013
  };
368
1014
 
1015
+ const getSessionFilesForThreadIds = (dbPath: string, threadIds: string[]) => {
1016
+ if (threadIds.length === 0) {
1017
+ return [];
1018
+ }
1019
+
1020
+ const codexDir = resolveCodexDirFromDbPath(dbPath);
1021
+ if (threadIds.length === 1) {
1022
+ const sessionFile = findSessionFileByThreadId(path.join(codexDir, 'sessions'), threadIds[0]!);
1023
+ return sessionFile ? [sessionFile] : [];
1024
+ }
1025
+
1026
+ const sessionFilesByThreadId = collectSessionFilesByThreadId(path.join(codexDir, 'sessions'));
1027
+ return threadIds
1028
+ .map((threadId) => sessionFilesByThreadId.get(threadId))
1029
+ .filter((value): value is string => Boolean(value));
1030
+ };
1031
+
1032
+ const filterSessionIndexLines = (lines: string[], threadIds: Set<string>) => {
1033
+ const removedThreadIds: string[] = [];
1034
+ const retainedLines: string[] = [];
1035
+
1036
+ for (const line of lines) {
1037
+ const trimmed = line.trim();
1038
+ if (!trimmed) {
1039
+ continue;
1040
+ }
1041
+
1042
+ const entry = parseJsonlObject<SessionIndexEntry>(trimmed);
1043
+ if (entry?.id && threadIds.has(entry.id)) {
1044
+ removedThreadIds.push(entry.id);
1045
+ continue;
1046
+ }
1047
+
1048
+ retainedLines.push(trimmed);
1049
+ }
1050
+
1051
+ return { removedThreadIds, retainedLines };
1052
+ };
1053
+
1054
+ const writeSessionIndexLines = async (sessionIndexPath: string, codexDir: string, retainedLines: string[]) => {
1055
+ const tempSessionIndexPath = path.join(
1056
+ codexDir,
1057
+ `.session_index.${Date.now()}.${Math.random().toString(36).slice(2)}.tmp`,
1058
+ );
1059
+ try {
1060
+ await Bun.write(tempSessionIndexPath, retainedLines.length > 0 ? `${retainedLines.join('\n')}\n` : '');
1061
+ await rename(tempSessionIndexPath, sessionIndexPath);
1062
+ } catch (error) {
1063
+ await rm(tempSessionIndexPath, { force: true });
1064
+ throw error;
1065
+ }
1066
+ };
1067
+
1068
+ const removeSessionIndexEntries = async (codexDir: string, threadIds: string[]) => {
1069
+ const runMutation = async () => {
1070
+ const uniqueThreadIds = new Set(threadIds);
1071
+ if (uniqueThreadIds.size === 0) {
1072
+ return [];
1073
+ }
1074
+
1075
+ const sessionIndexPath = path.join(codexDir, 'session_index.jsonl');
1076
+ if (!(await Bun.file(sessionIndexPath).exists())) {
1077
+ return [];
1078
+ }
1079
+
1080
+ const lines = (await Bun.file(sessionIndexPath).text()).split(/\r?\n/u);
1081
+ const { removedThreadIds, retainedLines } = filterSessionIndexLines(lines, uniqueThreadIds);
1082
+
1083
+ if (removedThreadIds.length === 0) {
1084
+ return [];
1085
+ }
1086
+
1087
+ await writeSessionIndexLines(sessionIndexPath, codexDir, retainedLines);
1088
+ return uniqueValues(removedThreadIds);
1089
+ };
1090
+
1091
+ const mutation = sessionIndexMutationQueue.then(runMutation, runMutation);
1092
+ sessionIndexMutationQueue = mutation.then(
1093
+ () => undefined,
1094
+ () => undefined,
1095
+ );
1096
+ return mutation;
1097
+ };
1098
+
1099
+ const listFallbackThreadIdsForProject = (dbPath: string, existingThreadIds: Set<string>, projectName: string) => {
1100
+ const codexDir = resolveCodexDirFromDbPath(dbPath);
1101
+ const sessionFilesByThreadId = collectSessionFilesByThreadId(path.join(codexDir, 'sessions'));
1102
+ const fallbackThreadIds: string[] = [];
1103
+
1104
+ for (const entry of readSessionIndexEntries(codexDir)) {
1105
+ if (existingThreadIds.has(entry.id) || !sessionFilesByThreadId.has(entry.id)) {
1106
+ continue;
1107
+ }
1108
+
1109
+ const sessionMeta = readFallbackSessionMeta(sessionFilesByThreadId.get(entry.id)!);
1110
+ if (!sessionMeta || isFallbackSubagent(sessionMeta)) {
1111
+ continue;
1112
+ }
1113
+
1114
+ const cwd = stringOrNull(sessionMeta.cwd);
1115
+ if (cwd && getPortablePathBasename(cwd) === projectName) {
1116
+ fallbackThreadIds.push(entry.id);
1117
+ }
1118
+ }
1119
+
1120
+ return fallbackThreadIds;
1121
+ };
1122
+
1123
+ const deleteSessionIndexEntriesForThreads = async (
1124
+ dbPath: string,
1125
+ threadIds: string[],
1126
+ dbDeletedSessionFiles: string[],
1127
+ deleteSessionFiles: boolean,
1128
+ ) => {
1129
+ const codexDir = resolveCodexDirFromDbPath(dbPath);
1130
+ const removedThreadIds = await removeSessionIndexEntries(codexDir, threadIds);
1131
+ const fallbackSessionFiles = deleteSessionFiles ? getSessionFilesForThreadIds(dbPath, removedThreadIds) : [];
1132
+
1133
+ return {
1134
+ deletedSessionFiles: deleteSessionFiles
1135
+ ? await deleteThreadSessionFiles([...dbDeletedSessionFiles, ...fallbackSessionFiles])
1136
+ : [],
1137
+ deletedThreadIds: removedThreadIds,
1138
+ };
1139
+ };
1140
+
369
1141
  export const listCodexProjects = (dbPath: string): ProjectSummary[] => {
370
- return mapProjectSummaries(buildProjectSummaryMap(readAllThreads(dbPath)));
1142
+ return mapProjectSummaries(
1143
+ buildProjectSummaryMap(applyRolloutActivityTimestamps(mergeFallbackThreadRows(dbPath, readAllThreads(dbPath)))),
1144
+ );
371
1145
  };
372
1146
 
373
1147
  type ListProjectThreadsOptions = {
1148
+ includeTranscriptStats?: boolean;
374
1149
  largeTranscriptThresholdBytes?: number;
375
1150
  };
376
1151
 
@@ -387,11 +1162,30 @@ export const listProjectThreads = async (
387
1162
  projectName: string,
388
1163
  options: ListProjectThreadsOptions = {},
389
1164
  ): Promise<ThreadListEntry[]> => {
390
- const threads = filterThreadsByProject(readAllThreads(dbPath), projectName);
391
- const entries = await mapWithConcurrency(threads, THREAD_LIST_IO_CONCURRENCY, async (thread) => {
1165
+ const threads = mergeFallbackThreadRows(
1166
+ dbPath,
1167
+ filterThreadsByProject(readAllThreads(dbPath), projectName),
1168
+ projectName,
1169
+ );
1170
+ const activeThreads = applyRolloutActivityTimestamps(threads);
1171
+ const entries = await mapWithConcurrency(activeThreads, THREAD_LIST_IO_CONCURRENCY, async (thread) => {
392
1172
  const rollout = await getThreadRolloutLoadState(thread.rollout_path, options.largeTranscriptThresholdBytes);
393
1173
 
394
- if (rollout.shouldDeferTranscriptLoad) {
1174
+ if (rollout.fileSizeBytes === null) {
1175
+ return {
1176
+ project: projectName,
1177
+ rolloutSizeBytes: null,
1178
+ stats: {
1179
+ deferred: false,
1180
+ execCommandCount: 0,
1181
+ toolCallCount: 0,
1182
+ webSearchEventCount: 0,
1183
+ },
1184
+ thread: compactThreadListRow(thread),
1185
+ };
1186
+ }
1187
+
1188
+ if (rollout.shouldDeferTranscriptLoad || options.includeTranscriptStats === false) {
395
1189
  return {
396
1190
  project: projectName,
397
1191
  rolloutSizeBytes: rollout.fileSizeBytes,
@@ -426,18 +1220,20 @@ export const listProjectThreads = async (
426
1220
  export const getThreadBrowseData = (dbPath: string, threadId: string): ThreadBrowseData => {
427
1221
  return withReadonlyDb(dbPath, (db) => {
428
1222
  const existingTableNames = getExistingTableNames(db);
429
- const thread = db.query('SELECT * FROM threads WHERE id = ? LIMIT 1').get(threadId) as ThreadRow | null;
1223
+ const dbThread = db.query('SELECT * FROM threads WHERE id = ? LIMIT 1').get(threadId) as ThreadRow | null;
1224
+ const thread = dbThread ?? readFallbackThreadRowById(dbPath, threadId, { includeSubagents: true }) ?? null;
430
1225
  if (!thread) {
431
1226
  throw new Error(`Thread not found: ${threadId}`);
432
1227
  }
433
1228
 
434
- const dynamicTools = existingTableNames.has('thread_dynamic_tools')
435
- ? (db
436
- .query(
437
- 'SELECT thread_id, position, name, description, input_schema, defer_loading, namespace FROM thread_dynamic_tools WHERE thread_id = ? ORDER BY position ASC',
438
- )
439
- .all(threadId) as Array<Record<string, number | string | null>>)
440
- : [];
1229
+ const dynamicTools =
1230
+ dbThread && existingTableNames.has('thread_dynamic_tools')
1231
+ ? (db
1232
+ .query(
1233
+ 'SELECT thread_id, position, name, description, input_schema, defer_loading, namespace FROM thread_dynamic_tools WHERE thread_id = ? ORDER BY position ASC',
1234
+ )
1235
+ .all(threadId) as Array<Record<string, number | string | null>>)
1236
+ : [];
441
1237
 
442
1238
  return {
443
1239
  dynamicTools: dynamicTools.map((row) => parseDynamicToolRow(row)),
@@ -449,7 +1245,7 @@ export const getThreadBrowseData = (dbPath: string, threadId: string): ThreadBro
449
1245
  };
450
1246
 
451
1247
  export const getCodexDashboardSummary = (dbPath: string): DashboardSummary => {
452
- const threads = readAllThreads(dbPath);
1248
+ const threads = applyRolloutActivityTimestamps(mergeFallbackThreadRows(dbPath, readAllThreads(dbPath)));
453
1249
  const projects = mapProjectSummaries(buildProjectSummaryMap(threads));
454
1250
  const threadsWithRelations = withReadonlyDb(dbPath, (db) => {
455
1251
  if (!getExistingTableNames(db).has('thread_spawn_edges')) {
@@ -467,13 +1263,7 @@ export const getCodexDashboardSummary = (dbPath: string): DashboardSummary => {
467
1263
  return {
468
1264
  activeThreads: threads.filter((thread) => !thread.archived).length,
469
1265
  archivedThreads: threads.filter((thread) => Boolean(thread.archived)).length,
470
- recentThreads: threads
471
- .slice(0, 5)
472
- .filter((thread) => Boolean(getPortablePathBasename(thread.cwd)))
473
- .map((thread) => ({
474
- project: getPortablePathBasename(thread.cwd),
475
- thread: compactThreadListRow(thread),
476
- })),
1266
+ recentThreads: buildDashboardRecentThreads(threads),
477
1267
  threadsWithRelations,
478
1268
  topProjectsByThreadCount: [...projects]
479
1269
  .sort((left, right) => {
@@ -496,21 +1286,22 @@ export const deleteCodexThread = async (
496
1286
  threadId: string,
497
1287
  options: DeleteThreadOptions = {},
498
1288
  ): Promise<DeleteThreadsResult> => {
1289
+ const threadIds = [threadId];
499
1290
  const result = withWritableDb(dbPath, (db) => {
500
- return deleteThreadIds(db, [threadId]);
1291
+ return deleteThreadIds(db, threadIds);
501
1292
  });
502
1293
 
503
1294
  try {
504
- if (options.deleteSessionFiles) {
505
- return {
506
- ...result,
507
- deletedSessionFiles: await deleteThreadSessionFiles(result.deletedSessionFiles),
508
- };
509
- }
1295
+ const sessionIndexResult = await deleteSessionIndexEntriesForThreads(
1296
+ dbPath,
1297
+ threadIds,
1298
+ result.deletedSessionFiles,
1299
+ Boolean(options.deleteSessionFiles),
1300
+ );
510
1301
 
511
1302
  return {
512
- ...result,
513
- deletedSessionFiles: [],
1303
+ deletedSessionFiles: sessionIndexResult.deletedSessionFiles,
1304
+ deletedThreadIds: uniqueValues([...result.deletedThreadIds, ...sessionIndexResult.deletedThreadIds]),
514
1305
  };
515
1306
  } finally {
516
1307
  await invalidateCodexUiCaches();
@@ -522,21 +1313,22 @@ export const deleteCodexThreads = async (
522
1313
  threadIds: string[],
523
1314
  options: DeleteThreadOptions = {},
524
1315
  ): Promise<DeleteThreadsResult> => {
1316
+ const uniqueThreadIds = uniqueValues(threadIds);
525
1317
  const result = withWritableDb(dbPath, (db) => {
526
- return deleteThreadIds(db, threadIds);
1318
+ return deleteThreadIds(db, uniqueThreadIds);
527
1319
  });
528
1320
 
529
1321
  try {
530
- if (options.deleteSessionFiles) {
531
- return {
532
- ...result,
533
- deletedSessionFiles: await deleteThreadSessionFiles(result.deletedSessionFiles),
534
- };
535
- }
1322
+ const sessionIndexResult = await deleteSessionIndexEntriesForThreads(
1323
+ dbPath,
1324
+ uniqueThreadIds,
1325
+ result.deletedSessionFiles,
1326
+ Boolean(options.deleteSessionFiles),
1327
+ );
536
1328
 
537
1329
  return {
538
- ...result,
539
- deletedSessionFiles: [],
1330
+ deletedSessionFiles: sessionIndexResult.deletedSessionFiles,
1331
+ deletedThreadIds: uniqueValues([...result.deletedThreadIds, ...sessionIndexResult.deletedThreadIds]),
540
1332
  };
541
1333
  } finally {
542
1334
  await invalidateCodexUiCaches();
@@ -548,6 +1340,8 @@ export const deleteCodexProject = async (
548
1340
  projectName: string,
549
1341
  options: DeleteProjectOptions = {},
550
1342
  ): Promise<DeleteProjectResult> => {
1343
+ const existingThreadIds = new Set(readAllThreads(dbPath).map((thread) => thread.id));
1344
+ const fallbackThreadIds = listFallbackThreadIdsForProject(dbPath, existingThreadIds, projectName);
551
1345
  const result = withWritableDb(dbPath, (db) => {
552
1346
  const threads = db.query('SELECT id, cwd FROM threads').all() as Array<{ cwd: string; id: string }>;
553
1347
  const threadIds = threads
@@ -562,16 +1356,17 @@ export const deleteCodexProject = async (
562
1356
  });
563
1357
 
564
1358
  try {
565
- if (options.deleteSessionFiles) {
566
- return {
567
- ...result,
568
- deletedSessionFiles: await deleteThreadSessionFiles(result.deletedSessionFiles),
569
- };
570
- }
1359
+ const sessionIndexResult = await deleteSessionIndexEntriesForThreads(
1360
+ dbPath,
1361
+ [...result.deletedThreadIds, ...fallbackThreadIds],
1362
+ result.deletedSessionFiles,
1363
+ Boolean(options.deleteSessionFiles),
1364
+ );
571
1365
 
572
1366
  return {
573
1367
  ...result,
574
- deletedSessionFiles: [],
1368
+ deletedSessionFiles: sessionIndexResult.deletedSessionFiles,
1369
+ deletedThreadIds: uniqueValues([...result.deletedThreadIds, ...sessionIndexResult.deletedThreadIds]),
575
1370
  };
576
1371
  } finally {
577
1372
  await invalidateCodexUiCaches();
@@ -579,9 +1374,9 @@ export const deleteCodexProject = async (
579
1374
  };
580
1375
 
581
1376
  export const listScopedThreads = (dbPath: string, projectName: string | null): ThreadRow[] => {
582
- return filterThreadsByProject(readAllThreads(dbPath), projectName);
1377
+ return mergeFallbackThreadRows(dbPath, filterThreadsByProject(readAllThreads(dbPath), projectName), projectName);
583
1378
  };
584
1379
 
585
1380
  export const invalidateCodexUiCaches = async () => {
586
- await invalidateCacheByPrefix('analytics-', 'thread-');
1381
+ await invalidateCacheByPrefix('analytics-', 'thread-', 'thread-preview-');
587
1382
  };