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
@@ -1,7 +1,10 @@
1
1
  import { constants, Database } from 'bun:sqlite';
2
- import { stat } from 'node:fs/promises';
2
+ import { randomUUID } from 'node:crypto';
3
+ import { chmod, readdir, rename, rm, stat } from 'node:fs/promises';
4
+ import os from 'node:os';
3
5
  import path from 'node:path';
4
6
  import { pathToFileURL } from 'node:url';
7
+ import { createConcurrencyLimiter, mapWithConcurrency } from './concurrency';
5
8
  import {
6
9
  getDefaultOpenCodeDataDir,
7
10
  type OpenCodeModelInfo,
@@ -23,10 +26,27 @@ import {
23
26
  type JsonValue,
24
27
  workspacePathMatchesQuery,
25
28
  } from './shared';
29
+ import { runWithSqliteRetry } from './sqlite-retry';
26
30
 
27
31
  export { getDefaultOpenCodeDataDir, resolveOpenCodeDbPath };
28
32
 
29
- export const OPENCODE_READONLY_DB_OPEN_FLAGS = constants.SQLITE_OPEN_READONLY | constants.SQLITE_OPEN_URI;
33
+ export const OPENCODE_READ_DB_OPEN_FLAGS = constants.SQLITE_OPEN_READWRITE | constants.SQLITE_OPEN_URI;
34
+ const OPENCODE_READONLY_DB_OPEN_FLAGS = constants.SQLITE_OPEN_READONLY | constants.SQLITE_OPEN_URI;
35
+ const DEFAULT_OPENCODE_DB_CONCURRENCY = 2;
36
+ const DESKTOP_STATE_FILE_CONCURRENCY = 4;
37
+ const OPENCODE_OPTIONAL_SESSION_TABLES = [
38
+ 'session_context_epoch',
39
+ 'session_input',
40
+ 'session_message',
41
+ 'session_share',
42
+ 'todo',
43
+ ] as const;
44
+
45
+ type OpenCodeOptionalSessionTable = (typeof OPENCODE_OPTIONAL_SESSION_TABLES)[number];
46
+
47
+ export type DeleteOpenCodeSessionResult = {
48
+ deletedSessionIds: string[];
49
+ };
30
50
 
31
51
  type WorkspaceRow = {
32
52
  archivedSessionCount: number;
@@ -61,11 +81,11 @@ type SessionRow = {
61
81
  summaryFiles: number | null;
62
82
  textPartCount: number;
63
83
  title: string;
64
- tokensCacheRead: number;
65
- tokensCacheWrite: number;
66
- tokensInput: number;
67
- tokensOutput: number;
68
- tokensReasoning: number;
84
+ tokensCacheRead: number | null;
85
+ tokensCacheWrite: number | null;
86
+ tokensInput: number | null;
87
+ tokensOutput: number | null;
88
+ tokensReasoning: number | null;
69
89
  toolPartCount: number;
70
90
  worktree: string;
71
91
  };
@@ -87,20 +107,142 @@ type PartRow = {
87
107
 
88
108
  const MAIN_SESSION_FILTER = 's.parent_id IS NULL';
89
109
 
110
+ let nextOpenCodeDbLoadId = 1;
111
+ let activeOpenCodeDbLoads = 0;
112
+ let queuedOpenCodeDbLoads = 0;
113
+
114
+ export const resolveOpenCodeDbConcurrency = (value = process.env.SPIRACHA_OPENCODE_DB_CONCURRENCY): number => {
115
+ const parsed = Number.parseInt(value ?? '', 10);
116
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_OPENCODE_DB_CONCURRENCY;
117
+ };
118
+
119
+ const openCodeDbLimiter = createConcurrencyLimiter(resolveOpenCodeDbConcurrency());
120
+ const openCodeDesktopStateLimiter = createConcurrencyLimiter(1);
121
+
122
+ const logOpenCodeDb = (event: string, details: Record<string, unknown>) => {
123
+ if (process.env.SPIRACHA_OPENCODE_DB_LOGS !== '0') {
124
+ console.info(`[spiracha:opencode-db] ${event}`, details);
125
+ }
126
+ };
127
+
128
+ const runWithOpenCodeDbLimit = async <T>(operation: string, dbPath: string, action: () => T): Promise<T> => {
129
+ const loadId = nextOpenCodeDbLoadId;
130
+ nextOpenCodeDbLoadId += 1;
131
+ queuedOpenCodeDbLoads += 1;
132
+ const queuedAt = Date.now();
133
+
134
+ return openCodeDbLimiter(async () => {
135
+ queuedOpenCodeDbLoads -= 1;
136
+ activeOpenCodeDbLoads += 1;
137
+ const startedAt = Date.now();
138
+ logOpenCodeDb('start', {
139
+ active: activeOpenCodeDbLoads,
140
+ dbPath,
141
+ loadId,
142
+ operation,
143
+ queued: queuedOpenCodeDbLoads,
144
+ waitMs: startedAt - queuedAt,
145
+ });
146
+
147
+ try {
148
+ return action();
149
+ } catch (error) {
150
+ logOpenCodeDb('error', {
151
+ active: activeOpenCodeDbLoads,
152
+ dbPath,
153
+ durationMs: Date.now() - startedAt,
154
+ error: error instanceof Error ? error.message : String(error),
155
+ loadId,
156
+ operation,
157
+ queued: queuedOpenCodeDbLoads,
158
+ });
159
+ throw error;
160
+ } finally {
161
+ activeOpenCodeDbLoads -= 1;
162
+ logOpenCodeDb('finish', {
163
+ active: activeOpenCodeDbLoads,
164
+ dbPath,
165
+ durationMs: Date.now() - startedAt,
166
+ loadId,
167
+ operation,
168
+ queued: queuedOpenCodeDbLoads,
169
+ });
170
+ }
171
+ });
172
+ };
173
+
174
+ const getDefaultOpenCodeDesktopStateDir = (
175
+ env: NodeJS.ProcessEnv = process.env,
176
+ homeDir = os.homedir(),
177
+ ): string | null => {
178
+ const configured = env.SPIRACHA_OPENCODE_DESKTOP_STATE_DIR?.trim();
179
+ if (configured) {
180
+ return configured;
181
+ }
182
+
183
+ return process.platform === 'darwin'
184
+ ? path.join(homeDir, 'Library', 'Application Support', 'ai.opencode.desktop')
185
+ : null;
186
+ };
187
+
90
188
  const pathExists = async (target: string): Promise<boolean> => {
91
189
  return await stat(target)
92
190
  .then(() => true)
93
191
  .catch(() => false);
94
192
  };
95
193
 
96
- export const getOpenCodeReadonlyDbUri = (dbPath: string): string => {
194
+ export const getOpenCodeReadDbUri = (dbPath: string, mode: 'ro' | 'rw' = 'rw'): string => {
97
195
  const url = pathToFileURL(dbPath);
98
- url.searchParams.set('mode', 'ro');
196
+ url.searchParams.set('mode', mode);
99
197
  return url.href;
100
198
  };
101
199
 
102
- export const openOpenCodeReadonlyDb = (dbPath: string): Database => {
103
- return new Database(getOpenCodeReadonlyDbUri(dbPath), OPENCODE_READONLY_DB_OPEN_FLAGS);
200
+ const configureOpenCodeReadDb = (db: Database): Database => {
201
+ try {
202
+ db.exec('PRAGMA busy_timeout = 1000');
203
+ db.exec('PRAGMA query_only = ON');
204
+ db.query('SELECT 1 FROM sqlite_schema LIMIT 1').get();
205
+ return db;
206
+ } catch (error) {
207
+ db.close();
208
+ throw error;
209
+ }
210
+ };
211
+
212
+ export const openOpenCodeReadDb = (dbPath: string): Database => {
213
+ try {
214
+ return configureOpenCodeReadDb(
215
+ new Database(getOpenCodeReadDbUri(dbPath, 'ro'), OPENCODE_READONLY_DB_OPEN_FLAGS),
216
+ );
217
+ } catch {
218
+ return configureOpenCodeReadDb(new Database(getOpenCodeReadDbUri(dbPath), OPENCODE_READ_DB_OPEN_FLAGS));
219
+ }
220
+ };
221
+
222
+ const withOpenCodeReadonlyDb = <T>(dbPath: string, action: (db: Database) => T): T => {
223
+ return runWithSqliteRetry({
224
+ action: () => {
225
+ const db = openOpenCodeReadDb(dbPath);
226
+ try {
227
+ return action(db);
228
+ } finally {
229
+ db.close();
230
+ }
231
+ },
232
+ });
233
+ };
234
+
235
+ const withOpenCodeWritableDb = <T>(dbPath: string, action: (db: Database) => T): T => {
236
+ return runWithSqliteRetry({
237
+ action: () => {
238
+ const db = new Database(dbPath);
239
+ try {
240
+ return action(db);
241
+ } finally {
242
+ db.close();
243
+ }
244
+ },
245
+ });
104
246
  };
105
247
 
106
248
  const parseJsonValue = (value: string | null): JsonValue | string | null => {
@@ -120,6 +262,30 @@ const parseJsonObject = (value: string): Record<string, JsonValue> => {
120
262
  return asObject(parsed) ?? {};
121
263
  };
122
264
 
265
+ const parseMutableJsonObject = (value: string): Record<string, unknown> | null => {
266
+ try {
267
+ const parsed = JSON.parse(value) as unknown;
268
+ return parsed && typeof parsed === 'object' && !Array.isArray(parsed)
269
+ ? (parsed as Record<string, unknown>)
270
+ : null;
271
+ } catch {
272
+ return null;
273
+ }
274
+ };
275
+
276
+ const parseMutableJsonArray = (value: string): unknown[] | null => {
277
+ try {
278
+ const parsed = JSON.parse(value) as unknown;
279
+ return Array.isArray(parsed) ? parsed : null;
280
+ } catch {
281
+ return null;
282
+ }
283
+ };
284
+
285
+ const encodeOpenCodeDirectoryKey = (directory: string): string => {
286
+ return Buffer.from(directory).toString('base64').replace(/=+$/u, '');
287
+ };
288
+
123
289
  const cleanLabel = (value: string | null | undefined): string | null => {
124
290
  const cleaned = value?.replace(/\s+/g, ' ').trim();
125
291
  return cleaned ? cleaned : null;
@@ -197,6 +363,11 @@ const toWorkspaceGroup = (row: WorkspaceRow): OpenCodeWorkspaceGroup => {
197
363
  const toSessionSummary = (row: SessionRow): OpenCodeSessionSummary => {
198
364
  const label = getWorkspaceLabel(row.projectName, row.worktree);
199
365
  const model = parseModelInfo(row.model);
366
+ const tokensCacheRead = row.tokensCacheRead ?? 0;
367
+ const tokensCacheWrite = row.tokensCacheWrite ?? 0;
368
+ const tokensInput = row.tokensInput ?? 0;
369
+ const tokensOutput = row.tokensOutput ?? 0;
370
+ const tokensReasoning = row.tokensReasoning ?? 0;
200
371
  return {
201
372
  agent: row.agent,
202
373
  archivedAtMs: row.archivedAtMs,
@@ -219,14 +390,13 @@ const toSessionSummary = (row: SessionRow): OpenCodeSessionSummary => {
219
390
  summaryFiles: row.summaryFiles,
220
391
  textPartCount: row.textPartCount,
221
392
  title: cleanLabel(row.title) ?? row.sessionId,
222
- tokensCacheRead: row.tokensCacheRead,
223
- tokensCacheWrite: row.tokensCacheWrite,
224
- tokensInput: row.tokensInput,
225
- tokensOutput: row.tokensOutput,
226
- tokensReasoning: row.tokensReasoning,
393
+ tokensCacheRead,
394
+ tokensCacheWrite,
395
+ tokensInput,
396
+ tokensOutput,
397
+ tokensReasoning,
227
398
  toolPartCount: row.toolPartCount,
228
- totalTokens:
229
- row.tokensInput + row.tokensOutput + row.tokensReasoning + row.tokensCacheRead + row.tokensCacheWrite,
399
+ totalTokens: tokensInput + tokensOutput + tokensReasoning + tokensCacheRead + tokensCacheWrite,
230
400
  workspaceKey: getWorkspaceKey(row.projectId),
231
401
  workspaceLabel: label,
232
402
  worktree: row.worktree,
@@ -336,15 +506,14 @@ export const listOpenCodeWorkspaceGroups = async (
336
506
  return [];
337
507
  }
338
508
 
339
- const db = openOpenCodeReadonlyDb(dbPath);
340
- try {
341
- const rows = db
342
- .query(`${workspaceRowsQuery} ORDER BY lastActiveMs DESC, p.worktree ASC`)
343
- .all() as WorkspaceRow[];
344
- return rows.map(toWorkspaceGroup);
345
- } finally {
346
- db.close();
347
- }
509
+ return runWithOpenCodeDbLimit('list-workspaces', dbPath, () =>
510
+ withOpenCodeReadonlyDb(dbPath, (db) => {
511
+ const rows = db
512
+ .query(`${workspaceRowsQuery} ORDER BY lastActiveMs DESC, p.worktree ASC`)
513
+ .all() as WorkspaceRow[];
514
+ return rows.map(toWorkspaceGroup);
515
+ }),
516
+ );
348
517
  };
349
518
 
350
519
  const openCodeWorkspaceMatchesQuery = (workspace: OpenCodeWorkspaceGroup, query: string): boolean => {
@@ -392,16 +561,391 @@ export const listOpenCodeSessionsForGroup = async (
392
561
  return [];
393
562
  }
394
563
 
395
- const db = openOpenCodeReadonlyDb(dbPath);
564
+ return runWithOpenCodeDbLimit('list-sessions', dbPath, () =>
565
+ withOpenCodeReadonlyDb(dbPath, (db) => {
566
+ return readSessionSummaries(db, `s.project_id = ? AND ${MAIN_SESSION_FILTER}`, [projectId]);
567
+ }),
568
+ );
569
+ };
570
+
571
+ const readOpenCodeSessionSummary = (db: Database, sessionId: string): OpenCodeSessionSummary | null => {
572
+ return readSessionSummaries(db, `s.id = ? AND ${MAIN_SESSION_FILTER}`, [sessionId])[0] ?? null;
573
+ };
574
+
575
+ const readOpenCodeSessionTreeIds = (db: Database, sessionId: string): string[] => {
576
+ const rows = db
577
+ .query(
578
+ `WITH RECURSIVE session_tree(id) AS (
579
+ SELECT id FROM session WHERE id = ?
580
+ UNION ALL
581
+ SELECT child.id
582
+ FROM session child
583
+ JOIN session_tree parent ON child.parent_id = parent.id
584
+ )
585
+ SELECT id FROM session_tree`,
586
+ )
587
+ .all(sessionId) as Array<{ id: string }>;
588
+ return rows.map((row) => row.id);
589
+ };
590
+
591
+ const hasOpenCodeTable = (db: Database, tableName: string): boolean => {
592
+ return Boolean(db.query("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ?").get(tableName));
593
+ };
594
+
595
+ const deleteRowsBySessionId = (
596
+ db: Database,
597
+ tableName: OpenCodeOptionalSessionTable,
598
+ sessionIds: string[],
599
+ placeholders: string,
600
+ ) => {
601
+ if (hasOpenCodeTable(db, tableName)) {
602
+ db.query(`DELETE FROM ${tableName} WHERE session_id IN (${placeholders})`).run(...sessionIds);
603
+ }
604
+ };
605
+
606
+ const deleteOpenCodeEventRows = (db: Database, sessionIds: string[], placeholders: string) => {
607
+ if (hasOpenCodeTable(db, 'event')) {
608
+ db.query(`DELETE FROM event WHERE aggregate_id IN (${placeholders})`).run(...sessionIds);
609
+ }
610
+ if (hasOpenCodeTable(db, 'event_sequence')) {
611
+ db.query(`DELETE FROM event_sequence WHERE aggregate_id IN (${placeholders})`).run(...sessionIds);
612
+ }
613
+ };
614
+
615
+ const deleteEmptyOpenCodeProjects = (
616
+ db: Database,
617
+ projects: Array<{ projectId: string; worktree: string }>,
618
+ ): string[] => {
619
+ const emptyWorktrees: string[] = [];
620
+ for (const project of projects) {
621
+ const remaining = db
622
+ .query('SELECT COUNT(*) AS count FROM session WHERE project_id = ?')
623
+ .get(project.projectId) as { count: number };
624
+ if (remaining.count === 0) {
625
+ db.query('DELETE FROM project WHERE id = ?').run(project.projectId);
626
+ emptyWorktrees.push(project.worktree);
627
+ }
628
+ }
629
+ return emptyWorktrees;
630
+ };
631
+
632
+ const deleteOpenCodeSessionRows = (
633
+ db: Database,
634
+ sessionIds: string[],
635
+ placeholders: string,
636
+ projectRows: Array<{ projectId: string; worktree: string }>,
637
+ ): string[] => {
638
+ deleteOpenCodeEventRows(db, sessionIds, placeholders);
639
+ for (const tableName of OPENCODE_OPTIONAL_SESSION_TABLES) {
640
+ deleteRowsBySessionId(db, tableName, sessionIds, placeholders);
641
+ }
642
+ db.query(`DELETE FROM part WHERE session_id IN (${placeholders})`).run(...sessionIds);
643
+ db.query(`DELETE FROM message WHERE session_id IN (${placeholders})`).run(...sessionIds);
644
+ db.query(`DELETE FROM session WHERE id IN (${placeholders})`).run(...sessionIds);
645
+ return deleteEmptyOpenCodeProjects(db, projectRows);
646
+ };
647
+
648
+ const removeObjectKeysForSessionIds = (target: Record<string, unknown>, sessionIds: Set<string>) => {
649
+ let changed = false;
650
+ const sessionIdList = [...sessionIds];
651
+ for (const key of Object.keys(target)) {
652
+ if (sessionIds.has(key) || sessionIdList.some((sessionId) => key.endsWith(`/${sessionId}`))) {
653
+ delete target[key];
654
+ changed = true;
655
+ }
656
+ }
657
+ return changed;
658
+ };
659
+
660
+ const cloneMutableJsonObject = (value: unknown): Record<string, unknown> | null => {
661
+ return value && typeof value === 'object' && !Array.isArray(value)
662
+ ? (structuredClone(value) as Record<string, unknown>)
663
+ : null;
664
+ };
665
+
666
+ const updateJsonStringField = (
667
+ state: Record<string, unknown>,
668
+ key: string,
669
+ update: (value: string) => { changed: boolean; next: unknown },
670
+ ) => {
671
+ const current = state[key];
672
+ if (typeof current !== 'string') {
673
+ return false;
674
+ }
675
+
676
+ const result = update(current);
677
+ if (!result.changed) {
678
+ return false;
679
+ }
680
+
681
+ state[key] = JSON.stringify(result.next);
682
+ return true;
683
+ };
684
+
685
+ const cleanOpenCodeServerProjects = (projects: Record<string, unknown>, worktrees: Set<string>): boolean => {
686
+ let changed = false;
687
+ for (const [serverId, entries] of Object.entries(projects)) {
688
+ if (!Array.isArray(entries)) {
689
+ continue;
690
+ }
691
+
692
+ const nextEntries = entries.filter((entry) => {
693
+ return !(
694
+ entry &&
695
+ typeof entry === 'object' &&
696
+ worktrees.has(String((entry as Record<string, unknown>).worktree ?? ''))
697
+ );
698
+ });
699
+ if (nextEntries.length !== entries.length) {
700
+ projects[serverId] = nextEntries;
701
+ changed = true;
702
+ }
703
+ }
704
+ return changed;
705
+ };
706
+
707
+ const cleanOpenCodeServerLastProject = (lastProject: Record<string, unknown>, worktrees: Set<string>): boolean => {
708
+ let changed = false;
709
+ for (const [serverId, directory] of Object.entries(lastProject)) {
710
+ if (worktrees.has(String(directory))) {
711
+ delete lastProject[serverId];
712
+ changed = true;
713
+ }
714
+ }
715
+ return changed;
716
+ };
717
+
718
+ const cleanOpenCodeServerState = (value: string, worktrees: Set<string>) => {
719
+ const parsed = parseMutableJsonObject(value);
720
+ const projects = cloneMutableJsonObject(parsed?.projects);
721
+ const lastProject = cloneMutableJsonObject(parsed?.lastProject);
722
+ if (!parsed) {
723
+ return { changed: false, next: parsed };
724
+ }
725
+
726
+ const projectsChanged = projects ? cleanOpenCodeServerProjects(projects, worktrees) : false;
727
+ const lastProjectChanged = lastProject ? cleanOpenCodeServerLastProject(lastProject, worktrees) : false;
728
+ parsed.projects = projects ?? parsed.projects;
729
+ parsed.lastProject = lastProject ?? parsed.lastProject;
730
+
731
+ const changed = projectsChanged || lastProjectChanged;
732
+ return changed ? { changed: true, next: parsed } : { changed: false, next: parsed };
733
+ };
734
+
735
+ const cleanOpenCodeDesktopStateObject = (
736
+ state: Record<string, unknown>,
737
+ sessionIds: Set<string>,
738
+ worktrees: Set<string>,
739
+ ): boolean => {
740
+ let changed = false;
741
+ const encodedWorktrees = new Set([...worktrees].map(encodeOpenCodeDirectoryKey));
742
+ const sessionIdList = [...sessionIds];
743
+
744
+ for (const key of Object.keys(state)) {
745
+ if (sessionIdList.some((sessionId) => key.startsWith(`session:${sessionId}:`))) {
746
+ delete state[key];
747
+ changed = true;
748
+ }
749
+ }
750
+
751
+ changed =
752
+ updateJsonStringField(state, 'notification', (value) => {
753
+ const parsed = parseMutableJsonObject(value);
754
+ const list = parsed?.list;
755
+ if (!Array.isArray(list)) {
756
+ return { changed: false, next: parsed };
757
+ }
758
+
759
+ const nextList = list.filter((item) => {
760
+ return !(
761
+ item &&
762
+ typeof item === 'object' &&
763
+ (sessionIds.has(String((item as Record<string, unknown>).session ?? '')) ||
764
+ worktrees.has(String((item as Record<string, unknown>).directory ?? '')))
765
+ );
766
+ });
767
+ if (nextList.length === list.length) {
768
+ return { changed: false, next: parsed };
769
+ }
770
+
771
+ return { changed: true, next: { ...parsed, list: nextList } };
772
+ }) || changed;
773
+
774
+ changed =
775
+ updateJsonStringField(state, 'layout.page', (value) => {
776
+ const parsed = parseMutableJsonObject(value);
777
+ const lastProjectSession = cloneMutableJsonObject(parsed?.lastProjectSession);
778
+ if (!parsed || !lastProjectSession) {
779
+ return { changed: false, next: parsed };
780
+ }
781
+
782
+ let nextChanged = false;
783
+ for (const [directory, session] of Object.entries(lastProjectSession)) {
784
+ if (
785
+ session &&
786
+ typeof session === 'object' &&
787
+ (sessionIds.has(String((session as Record<string, unknown>).id ?? '')) ||
788
+ worktrees.has(directory) ||
789
+ worktrees.has(String((session as Record<string, unknown>).directory ?? '')))
790
+ ) {
791
+ delete lastProjectSession[directory];
792
+ nextChanged = true;
793
+ }
794
+ }
795
+
796
+ return nextChanged
797
+ ? { changed: true, next: { ...parsed, lastProjectSession } }
798
+ : { changed: false, next: parsed };
799
+ }) || changed;
800
+
801
+ changed = updateJsonStringField(state, 'server', (value) => cleanOpenCodeServerState(value, worktrees)) || changed;
802
+
803
+ changed =
804
+ updateJsonStringField(state, 'permission', (value) => {
805
+ const parsed = parseMutableJsonObject(value);
806
+ const autoAccept = cloneMutableJsonObject(parsed?.autoAccept);
807
+ if (!parsed || !autoAccept) {
808
+ return { changed: false, next: parsed };
809
+ }
810
+
811
+ const nextChanged = removeObjectKeysForSessionIds(autoAccept, sessionIds);
812
+ return nextChanged ? { changed: true, next: { ...parsed, autoAccept } } : { changed: false, next: parsed };
813
+ }) || changed;
814
+
815
+ changed =
816
+ updateJsonStringField(state, 'tabs', (value) => {
817
+ const parsed = parseMutableJsonArray(value);
818
+ if (!parsed) {
819
+ return { changed: false, next: parsed };
820
+ }
821
+
822
+ const next = parsed.filter((item) => {
823
+ return !(
824
+ item &&
825
+ typeof item === 'object' &&
826
+ (sessionIds.has(String((item as Record<string, unknown>).sessionId ?? '')) ||
827
+ encodedWorktrees.has(String((item as Record<string, unknown>).dirBase64 ?? '')))
828
+ );
829
+ });
830
+ return next.length === parsed.length ? { changed: false, next: parsed } : { changed: true, next };
831
+ }) || changed;
832
+
833
+ changed =
834
+ updateJsonStringField(state, 'workspace:model-selection', (value) => {
835
+ const parsed = parseMutableJsonObject(value);
836
+ const session = cloneMutableJsonObject(parsed?.session);
837
+ if (!parsed || !session) {
838
+ return { changed: false, next: parsed };
839
+ }
840
+
841
+ const nextChanged = removeObjectKeysForSessionIds(session, sessionIds);
842
+ return nextChanged ? { changed: true, next: { ...parsed, session } } : { changed: false, next: parsed };
843
+ }) || changed;
844
+
845
+ changed =
846
+ updateJsonStringField(state, 'workspace:followup', (value) => {
847
+ const parsed = parseMutableJsonObject(value);
848
+ if (!parsed) {
849
+ return { changed: false, next: parsed };
850
+ }
851
+
852
+ let nextChanged = false;
853
+ for (const key of ['items', 'failed', 'paused', 'edit']) {
854
+ const child = cloneMutableJsonObject(parsed[key]);
855
+ if (child && removeObjectKeysForSessionIds(child, sessionIds)) {
856
+ parsed[key] = child;
857
+ nextChanged = true;
858
+ }
859
+ }
860
+
861
+ return nextChanged ? { changed: true, next: parsed } : { changed: false, next: parsed };
862
+ }) || changed;
863
+
864
+ return changed;
865
+ };
866
+
867
+ const writeOpenCodeDesktopState = async (filePath: string, state: Record<string, unknown>): Promise<void> => {
868
+ const tempPath = `${filePath}.${randomUUID()}.tmp`;
869
+ const mode = (await stat(filePath)).mode & 0o777;
396
870
  try {
397
- return readSessionSummaries(db, `s.project_id = ? AND ${MAIN_SESSION_FILTER}`, [projectId]);
871
+ await Bun.write(tempPath, `${JSON.stringify(state, null, '\t')}\n`);
872
+ await chmod(tempPath, mode);
873
+ await rename(tempPath, filePath);
398
874
  } finally {
399
- db.close();
875
+ await rm(tempPath, { force: true });
400
876
  }
401
877
  };
402
878
 
403
- const readOpenCodeSessionSummary = (db: Database, sessionId: string): OpenCodeSessionSummary | null => {
404
- return readSessionSummaries(db, `s.id = ? AND ${MAIN_SESSION_FILTER}`, [sessionId])[0] ?? null;
879
+ export const deleteOpenCodeDesktopSessionState = async (
880
+ sessionIds: string[],
881
+ stateDir = getDefaultOpenCodeDesktopStateDir(),
882
+ worktrees: string[] = [],
883
+ ): Promise<string[]> => {
884
+ return openCodeDesktopStateLimiter(async () => {
885
+ if (!stateDir || (sessionIds.length === 0 && worktrees.length === 0) || !(await pathExists(stateDir))) {
886
+ return [];
887
+ }
888
+
889
+ const sessionIdSet = new Set(sessionIds);
890
+ const worktreeSet = new Set(worktrees);
891
+ const fileNames = (await readdir(stateDir)).filter((fileName) => fileName.endsWith('.dat'));
892
+ const changedFiles = await mapWithConcurrency(
893
+ fileNames,
894
+ DESKTOP_STATE_FILE_CONCURRENCY,
895
+ async (fileName): Promise<string | null> => {
896
+ const filePath = path.join(stateDir, fileName);
897
+ const state = parseMutableJsonObject(await Bun.file(filePath).text());
898
+ if (!state || !cleanOpenCodeDesktopStateObject(state, sessionIdSet, worktreeSet)) {
899
+ return null;
900
+ }
901
+
902
+ await writeOpenCodeDesktopState(filePath, state);
903
+ return filePath;
904
+ },
905
+ );
906
+
907
+ return changedFiles.filter((filePath): filePath is string => filePath !== null);
908
+ });
909
+ };
910
+
911
+ export const deleteOpenCodeSession = async (
912
+ dbPath: string,
913
+ sessionId: string,
914
+ ): Promise<DeleteOpenCodeSessionResult> => {
915
+ if (!(await pathExists(dbPath))) {
916
+ return { deletedSessionIds: [] };
917
+ }
918
+
919
+ const result = await runWithOpenCodeDbLimit('delete-session', dbPath, () =>
920
+ withOpenCodeWritableDb(dbPath, (db) => {
921
+ const sessionIds = readOpenCodeSessionTreeIds(db, sessionId);
922
+ if (sessionIds.length === 0) {
923
+ return { deletedSessionIds: [], emptyWorktrees: [] };
924
+ }
925
+
926
+ const placeholders = sessionIds.map(() => '?').join(', ');
927
+ const projectRows = db
928
+ .query(
929
+ `SELECT DISTINCT pr.id AS projectId, pr.worktree
930
+ FROM session s
931
+ JOIN project pr ON pr.id = s.project_id
932
+ WHERE s.id IN (${placeholders})`,
933
+ )
934
+ .all(...sessionIds) as Array<{ projectId: string; worktree: string }>;
935
+ const emptyWorktrees = db.transaction(() =>
936
+ deleteOpenCodeSessionRows(db, sessionIds, placeholders, projectRows),
937
+ )();
938
+
939
+ return { deletedSessionIds: sessionIds, emptyWorktrees };
940
+ }),
941
+ );
942
+
943
+ await deleteOpenCodeDesktopSessionState(
944
+ result.deletedSessionIds.length > 0 ? result.deletedSessionIds : [sessionId],
945
+ undefined,
946
+ result.emptyWorktrees,
947
+ );
948
+ return { deletedSessionIds: result.deletedSessionIds };
405
949
  };
406
950
 
407
951
  const getMessageRole = (raw: Record<string, JsonValue>): string => asString(raw.role ?? null) ?? 'unknown';
@@ -584,27 +1128,26 @@ export const readOpenCodeSessionTranscript = async (
584
1128
  return null;
585
1129
  }
586
1130
 
587
- const db = openOpenCodeReadonlyDb(dbPath);
588
- try {
589
- const session = readOpenCodeSessionSummary(db, sessionId);
590
- if (!session) {
591
- return null;
592
- }
593
-
594
- const messages = readMessages(db, sessionId);
595
- const partCount = messages.reduce((total, message) => total + message.parts.length, 0);
596
- const renderablePartCount = messages.reduce(
597
- (total, message) => total + message.parts.filter(isRenderablePart).length,
598
- 0,
599
- );
600
-
601
- return {
602
- messages,
603
- partCount,
604
- renderablePartCount,
605
- session,
606
- };
607
- } finally {
608
- db.close();
609
- }
1131
+ return runWithOpenCodeDbLimit('read-session', dbPath, () =>
1132
+ withOpenCodeReadonlyDb(dbPath, (db) => {
1133
+ const session = readOpenCodeSessionSummary(db, sessionId);
1134
+ if (!session) {
1135
+ return null;
1136
+ }
1137
+
1138
+ const messages = readMessages(db, sessionId);
1139
+ const partCount = messages.reduce((total, message) => total + message.parts.length, 0);
1140
+ const renderablePartCount = messages.reduce(
1141
+ (total, message) => total + message.parts.filter(isRenderablePart).length,
1142
+ 0,
1143
+ );
1144
+
1145
+ return {
1146
+ messages,
1147
+ partCount,
1148
+ renderablePartCount,
1149
+ session,
1150
+ };
1151
+ }),
1152
+ );
610
1153
  };