spiracha 1.5.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (401) hide show
  1. package/AGENTS.md +114 -205
  2. package/README.md +94 -269
  3. package/apps/ui/AGENTS.md +17 -7
  4. package/apps/ui/README.md +52 -8
  5. package/apps/ui/components.json +21 -0
  6. package/apps/ui/package.json +65 -0
  7. package/apps/ui/src/components/antigravity-conversations-table.tsx +245 -0
  8. package/apps/ui/src/components/antigravity-keychain-panel.tsx +76 -0
  9. package/apps/ui/src/components/antigravity-workspaces-table.tsx +63 -0
  10. package/apps/ui/src/components/app-shell.tsx +125 -0
  11. package/apps/ui/src/components/breadcrumbs.tsx +71 -0
  12. package/apps/ui/src/components/claude-code-sessions-table.tsx +150 -0
  13. package/apps/ui/src/components/claude-code-workspaces-table.tsx +57 -0
  14. package/apps/ui/src/components/cursor-threads-table.tsx +152 -0
  15. package/apps/ui/src/components/cursor-workspaces-table.tsx +140 -0
  16. package/apps/ui/src/components/data-table.tsx +243 -0
  17. package/apps/ui/src/components/delete-confirm-dialog.tsx +88 -0
  18. package/apps/ui/src/components/export-dialog.tsx +174 -0
  19. package/apps/ui/src/components/grok-sessions-table.tsx +146 -0
  20. package/apps/ui/src/components/grok-workspaces-table.tsx +53 -0
  21. package/apps/ui/src/components/json-panel.tsx +17 -0
  22. package/apps/ui/src/components/kiro-sessions-table.tsx +148 -0
  23. package/apps/ui/src/components/kiro-workspaces-table.tsx +53 -0
  24. package/apps/ui/src/components/list-search-input.tsx +18 -0
  25. package/apps/ui/src/components/loading-panel.tsx +23 -0
  26. package/apps/ui/src/components/metadata-section.tsx +31 -0
  27. package/apps/ui/src/components/metric-card.tsx +19 -0
  28. package/apps/ui/src/components/opencode-sessions-table.tsx +164 -0
  29. package/apps/ui/src/components/opencode-workspaces-table.tsx +57 -0
  30. package/apps/ui/src/components/page-header.tsx +39 -0
  31. package/apps/ui/src/components/projects-loading-state.tsx +74 -0
  32. package/apps/ui/src/components/projects-table.tsx +108 -0
  33. package/apps/ui/src/components/qoder-sessions-table.tsx +133 -0
  34. package/apps/ui/src/components/qoder-workspaces-table.tsx +57 -0
  35. package/apps/ui/src/components/recent-threads-list.tsx +52 -0
  36. package/apps/ui/src/components/reload-error-panel.tsx +20 -0
  37. package/apps/ui/src/components/selection-actions-toolbar.tsx +80 -0
  38. package/apps/ui/src/components/text-document-panel.tsx +19 -0
  39. package/apps/ui/src/components/theme-toggle.tsx +48 -0
  40. package/apps/ui/src/components/threads-table.tsx +165 -0
  41. package/apps/ui/src/components/transcript-view.tsx +648 -0
  42. package/apps/ui/src/components/ui/alert-dialog.tsx +160 -0
  43. package/apps/ui/src/components/ui/badge.tsx +45 -0
  44. package/apps/ui/src/components/ui/button.tsx +62 -0
  45. package/apps/ui/src/components/ui/checkbox.tsx +29 -0
  46. package/apps/ui/src/components/ui/dialog.tsx +137 -0
  47. package/apps/ui/src/components/ui/dropdown-menu.tsx +217 -0
  48. package/apps/ui/src/components/ui/input.tsx +21 -0
  49. package/apps/ui/src/components/ui/scroll-area.tsx +46 -0
  50. package/apps/ui/src/components/ui/select.tsx +163 -0
  51. package/apps/ui/src/components/ui/separator.tsx +28 -0
  52. package/apps/ui/src/components/ui/sheet.tsx +107 -0
  53. package/apps/ui/src/components/ui/skeleton.tsx +7 -0
  54. package/apps/ui/src/components/ui/table.tsx +76 -0
  55. package/apps/ui/src/components/ui/tabs.tsx +71 -0
  56. package/apps/ui/src/components/ui/tooltip.tsx +44 -0
  57. package/apps/ui/src/integrations/tanstack-query/devtools.tsx +6 -0
  58. package/apps/ui/src/integrations/tanstack-query/root-provider.tsx +10 -0
  59. package/apps/ui/src/lib/antigravity-conversation-state.ts +41 -0
  60. package/apps/ui/src/lib/antigravity-queries.ts +33 -0
  61. package/apps/ui/src/lib/antigravity-server.ts +273 -0
  62. package/apps/ui/src/lib/antigravity-transcript-events.ts +343 -0
  63. package/apps/ui/src/lib/claude-code-queries.ts +34 -0
  64. package/apps/ui/src/lib/claude-code-server.ts +243 -0
  65. package/apps/ui/src/lib/claude-code-transcript-events.ts +161 -0
  66. package/apps/ui/src/lib/codex-queries.ts +82 -0
  67. package/apps/ui/src/lib/codex-server.ts +291 -0
  68. package/apps/ui/src/lib/cursor-queries.ts +37 -0
  69. package/apps/ui/src/lib/cursor-server.ts +315 -0
  70. package/apps/ui/src/lib/cursor-transcript-events.ts +229 -0
  71. package/apps/ui/src/lib/download.ts +166 -0
  72. package/apps/ui/src/lib/formatters.ts +147 -0
  73. package/apps/ui/src/lib/grok-queries.ts +22 -0
  74. package/apps/ui/src/lib/grok-server.ts +169 -0
  75. package/apps/ui/src/lib/grok-transcript-events.ts +149 -0
  76. package/apps/ui/src/lib/kiro-queries.ts +22 -0
  77. package/apps/ui/src/lib/kiro-server.ts +151 -0
  78. package/apps/ui/src/lib/kiro-transcript-events.ts +113 -0
  79. package/apps/ui/src/lib/opencode-queries.ts +37 -0
  80. package/apps/ui/src/lib/opencode-server.ts +151 -0
  81. package/apps/ui/src/lib/opencode-transcript-events.ts +221 -0
  82. package/apps/ui/src/lib/package-metadata.ts +16 -0
  83. package/apps/ui/src/lib/path-utils.ts +13 -0
  84. package/apps/ui/src/lib/qoder-queries.ts +22 -0
  85. package/apps/ui/src/lib/qoder-server.ts +135 -0
  86. package/apps/ui/src/lib/qoder-transcript-events.ts +139 -0
  87. package/apps/ui/src/lib/route-search.ts +182 -0
  88. package/apps/ui/src/lib/settings-store.tsx +59 -0
  89. package/apps/ui/src/lib/source-session-export-server.ts +139 -0
  90. package/apps/ui/src/lib/text-filter.ts +22 -0
  91. package/apps/ui/src/lib/thread-id.ts +3 -0
  92. package/apps/ui/src/lib/thread-transcript-load.ts +15 -0
  93. package/apps/ui/src/lib/thread-transcript-stats.ts +57 -0
  94. package/apps/ui/src/lib/utils.ts +7 -0
  95. package/apps/ui/src/lib/workspace-delete-navigation.ts +12 -0
  96. package/apps/ui/src/routeTree.gen.ts +837 -0
  97. package/apps/ui/src/router.tsx +20 -0
  98. package/apps/ui/src/routes/$threadId.tsx +22 -0
  99. package/apps/ui/src/routes/__root.tsx +127 -0
  100. package/apps/ui/src/routes/analytics.tsx +143 -0
  101. package/apps/ui/src/routes/antigravity-conversations.$conversationId.tsx +504 -0
  102. package/apps/ui/src/routes/antigravity.$workspaceKey.tsx +365 -0
  103. package/apps/ui/src/routes/antigravity.index.tsx +51 -0
  104. package/apps/ui/src/routes/api.v1.conversation-query.ts +12 -0
  105. package/apps/ui/src/routes/api.v1.conversations.$source.$id.export.ts +12 -0
  106. package/apps/ui/src/routes/api.v1.conversations.$source.$id.ts +16 -0
  107. package/apps/ui/src/routes/api.v1.conversations.delete.ts +12 -0
  108. package/apps/ui/src/routes/api.v1.conversations.export.ts +12 -0
  109. package/apps/ui/src/routes/api.v1.conversations.ts +12 -0
  110. package/apps/ui/src/routes/api.v1.resolve.ts +12 -0
  111. package/apps/ui/src/routes/api.v1.sources.ts +12 -0
  112. package/apps/ui/src/routes/claude-code-sessions.$sessionId.tsx +468 -0
  113. package/apps/ui/src/routes/claude-code.$workspaceKey.tsx +274 -0
  114. package/apps/ui/src/routes/claude-code.index.tsx +48 -0
  115. package/apps/ui/src/routes/codex.$project.tsx +390 -0
  116. package/apps/ui/src/routes/codex.index.tsx +116 -0
  117. package/apps/ui/src/routes/cursor-threads.$composerId.tsx +435 -0
  118. package/apps/ui/src/routes/cursor.$workspaceKey.tsx +426 -0
  119. package/apps/ui/src/routes/cursor.index.tsx +120 -0
  120. package/apps/ui/src/routes/grok-sessions.$sessionId.tsx +388 -0
  121. package/apps/ui/src/routes/grok.$workspaceKey.tsx +301 -0
  122. package/apps/ui/src/routes/grok.index.tsx +48 -0
  123. package/apps/ui/src/routes/index.tsx +124 -0
  124. package/apps/ui/src/routes/kiro-sessions.$sessionId.tsx +396 -0
  125. package/apps/ui/src/routes/kiro.$workspaceKey.tsx +303 -0
  126. package/apps/ui/src/routes/kiro.index.tsx +48 -0
  127. package/apps/ui/src/routes/opencode-sessions.$sessionId.tsx +420 -0
  128. package/apps/ui/src/routes/opencode.$workspaceKey.tsx +327 -0
  129. package/apps/ui/src/routes/opencode.index.tsx +61 -0
  130. package/apps/ui/src/routes/qoder-sessions.$sessionId.tsx +344 -0
  131. package/apps/ui/src/routes/qoder.$workspaceKey.tsx +186 -0
  132. package/apps/ui/src/routes/qoder.index.tsx +48 -0
  133. package/apps/ui/src/routes/settings.tsx +91 -0
  134. package/apps/ui/src/routes/threads.$threadId.tsx +1103 -0
  135. package/apps/ui/src/styles.css +122 -0
  136. package/apps/ui/tsconfig.json +29 -0
  137. package/apps/ui/vite.config.ts +84 -0
  138. package/bin/spiracha.ts +32 -0
  139. package/package.json +54 -80
  140. package/src/client.ts +423 -0
  141. package/src/lib/antigravity-db.ts +414 -44
  142. package/src/lib/antigravity-exporter-types.ts +4 -0
  143. package/src/lib/antigravity-projects.ts +201 -0
  144. package/src/lib/claude-code-db.ts +498 -32
  145. package/src/lib/claude-code-exporter-types.ts +5 -0
  146. package/src/lib/claude-code-transcript-phase.ts +60 -1
  147. package/src/lib/claude-code-transcript.ts +8 -5
  148. package/src/lib/codex-analytics.ts +1 -1
  149. package/src/lib/codex-browser-db.ts +410 -74
  150. package/src/lib/codex-browser-export.ts +29 -41
  151. package/src/lib/codex-browser-types.ts +1 -1
  152. package/src/lib/codex-thread-cache.ts +41 -13
  153. package/src/lib/codex-thread-parser.ts +86 -35
  154. package/src/lib/{codex-exporter-types.ts → codex-thread-types.ts} +2 -30
  155. package/src/lib/codex-transcript-filter.ts +31 -0
  156. package/src/lib/{codex-exporter-transcript.ts → codex-transcript-renderer.ts} +104 -48
  157. package/src/lib/concurrency.ts +41 -0
  158. package/src/lib/conversation-api.ts +1172 -0
  159. package/src/lib/conversation-data/adapter-helpers.ts +110 -0
  160. package/src/lib/conversation-data/antigravity-adapter.ts +167 -0
  161. package/src/lib/conversation-data/claude-code-adapter.ts +206 -0
  162. package/src/lib/conversation-data/codex-adapter.ts +269 -0
  163. package/src/lib/conversation-data/cursor-adapter.ts +205 -0
  164. package/src/lib/conversation-data/grok-adapter.ts +210 -0
  165. package/src/lib/conversation-data/index.ts +386 -0
  166. package/src/lib/conversation-data/kiro-adapter.ts +174 -0
  167. package/src/lib/conversation-data/message-selector.ts +37 -0
  168. package/src/lib/conversation-data/opencode-adapter.ts +202 -0
  169. package/src/lib/conversation-data/path-match.ts +79 -0
  170. package/src/lib/conversation-data/qoder-adapter.ts +255 -0
  171. package/src/lib/conversation-data/types.ts +159 -0
  172. package/src/lib/conversation-zip-export.ts +57 -0
  173. package/src/lib/cursor-db.ts +100 -25
  174. package/src/lib/cursor-exporter-types.ts +0 -19
  175. package/src/lib/grok-db.ts +1026 -0
  176. package/src/lib/grok-exporter-types.ts +110 -0
  177. package/src/lib/grok-transcript-phase.ts +52 -0
  178. package/src/lib/grok-transcript.ts +154 -0
  179. package/src/lib/kiro-db.ts +52 -1
  180. package/src/lib/model-label.ts +11 -3
  181. package/src/lib/opencode-db.ts +598 -55
  182. package/src/lib/opencode-transcript.ts +8 -5
  183. package/src/lib/qoder-acp-client.ts +268 -0
  184. package/src/lib/qoder-db.ts +1601 -0
  185. package/src/lib/qoder-exporter-types.ts +114 -0
  186. package/src/lib/qoder-transcript-phase.ts +41 -0
  187. package/src/lib/qoder-transcript.ts +112 -0
  188. package/src/lib/shared.ts +12 -0
  189. package/src/lib/transcript-load-limiter.ts +82 -0
  190. package/src/lib/ui-cache.ts +43 -17
  191. package/src/lib/ui-export-archive.ts +60 -0
  192. package/apps/ui/dist/client/assets/_threadId-CAIeH5mq.js +0 -1
  193. package/apps/ui/dist/client/assets/analytics-DOy1b6Sh.js +0 -1
  194. package/apps/ui/dist/client/assets/antigravity-conversations._conversationId-DqySVO2K.js +0 -7
  195. package/apps/ui/dist/client/assets/antigravity-conversations._conversationId-KAcXtNxI.js +0 -1
  196. package/apps/ui/dist/client/assets/antigravity-keychain-panel-B77qJz6f.js +0 -1
  197. package/apps/ui/dist/client/assets/antigravity._workspaceKey-B3NM6gRH.js +0 -1
  198. package/apps/ui/dist/client/assets/antigravity._workspaceKey-ByitGWki.js +0 -1
  199. package/apps/ui/dist/client/assets/antigravity.index-DP9JC2P9.js +0 -1
  200. package/apps/ui/dist/client/assets/antigravity.index-DgW25FEV.js +0 -1
  201. package/apps/ui/dist/client/assets/badge-C3i4OKs8.js +0 -1
  202. package/apps/ui/dist/client/assets/checkbox-CdHitXWQ.js +0 -1
  203. package/apps/ui/dist/client/assets/claude-code-sessions._sessionId-8pQ55ci2.js +0 -1
  204. package/apps/ui/dist/client/assets/claude-code-sessions._sessionId-DVz5UmtU.js +0 -1
  205. package/apps/ui/dist/client/assets/claude-code._workspaceKey-BBKskSum.js +0 -1
  206. package/apps/ui/dist/client/assets/claude-code._workspaceKey-OkikMUFs.js +0 -1
  207. package/apps/ui/dist/client/assets/claude-code.index-Cwu76dbb.js +0 -1
  208. package/apps/ui/dist/client/assets/claude-code.index-D6eMd08t.js +0 -1
  209. package/apps/ui/dist/client/assets/createServerFn-B-KkwmHU.js +0 -3
  210. package/apps/ui/dist/client/assets/cursor-threads._composerId-CmBFclK8.js +0 -1
  211. package/apps/ui/dist/client/assets/cursor-threads._composerId-hBWlP1D-.js +0 -1
  212. package/apps/ui/dist/client/assets/cursor._workspaceKey-BFPkIRi-.js +0 -1
  213. package/apps/ui/dist/client/assets/cursor._workspaceKey-CaAZjGmm.js +0 -1
  214. package/apps/ui/dist/client/assets/cursor.index-8y4o20kb.js +0 -2
  215. package/apps/ui/dist/client/assets/cursor.index-CrnMwDCi.js +0 -1
  216. package/apps/ui/dist/client/assets/data-table-Br78_dk3.js +0 -4
  217. package/apps/ui/dist/client/assets/delete-confirm-dialog-Bxs07u-j.js +0 -7
  218. package/apps/ui/dist/client/assets/dist-B9pyYCVU.js +0 -1
  219. package/apps/ui/dist/client/assets/dist-BrJhN2A0.js +0 -5
  220. package/apps/ui/dist/client/assets/dist-Cmd3AIfD.js +0 -1
  221. package/apps/ui/dist/client/assets/dist-DvMS2965.js +0 -1
  222. package/apps/ui/dist/client/assets/download-Bf0QJKD3.js +0 -1
  223. package/apps/ui/dist/client/assets/dropdown-menu-D5cvPaZt.js +0 -1
  224. package/apps/ui/dist/client/assets/es2015-ad6040dw.js +0 -41
  225. package/apps/ui/dist/client/assets/export-dialog-Dr8d2MqD.js +0 -1
  226. package/apps/ui/dist/client/assets/formatters-BtqyrX_c.js +0 -1
  227. package/apps/ui/dist/client/assets/index-C-STgIH4.js +0 -166
  228. package/apps/ui/dist/client/assets/jsx-runtime-DGeXAQPT.js +0 -1
  229. package/apps/ui/dist/client/assets/kiro-sessions._sessionId-CKgfJ7GR.js +0 -1
  230. package/apps/ui/dist/client/assets/kiro-sessions._sessionId-zvIJHtQF.js +0 -1
  231. package/apps/ui/dist/client/assets/kiro._workspaceKey-DhVuJuZB.js +0 -1
  232. package/apps/ui/dist/client/assets/kiro._workspaceKey-QArsjkTY.js +0 -1
  233. package/apps/ui/dist/client/assets/kiro.index-C-fr4m7i.js +0 -1
  234. package/apps/ui/dist/client/assets/kiro.index-OYgAdtZd.js +0 -1
  235. package/apps/ui/dist/client/assets/metric-card-Dplm0ZiM.js +0 -1
  236. package/apps/ui/dist/client/assets/opencode-sessions._sessionId-DLjiTst-.js +0 -1
  237. package/apps/ui/dist/client/assets/opencode-sessions._sessionId-biBwrRbt.js +0 -8
  238. package/apps/ui/dist/client/assets/opencode._workspaceKey-C7OIzMOJ.js +0 -1
  239. package/apps/ui/dist/client/assets/opencode._workspaceKey-sBakX0oI.js +0 -1
  240. package/apps/ui/dist/client/assets/opencode.index-Bl_kNs9N.js +0 -1
  241. package/apps/ui/dist/client/assets/opencode.index-CN53bVmb.js +0 -1
  242. package/apps/ui/dist/client/assets/page-header-Cwa3p6Tl.js +0 -1
  243. package/apps/ui/dist/client/assets/preload-helper-B8fFrdxC.js +0 -1
  244. package/apps/ui/dist/client/assets/projects._project-D8WloxQN.js +0 -1
  245. package/apps/ui/dist/client/assets/projects._project-DSDwH4NF.js +0 -1
  246. package/apps/ui/dist/client/assets/projects.index-5xRtuJdX.js +0 -3
  247. package/apps/ui/dist/client/assets/projects.index-DEfhWqa8.js +0 -1
  248. package/apps/ui/dist/client/assets/react-dom-DL96Jor4.js +0 -1
  249. package/apps/ui/dist/client/assets/refresh-ccw-BgKjOiA6.js +0 -1
  250. package/apps/ui/dist/client/assets/reload-error-panel-AHqu0akK.js +0 -1
  251. package/apps/ui/dist/client/assets/routes-DnzF_9CX.js +0 -1
  252. package/apps/ui/dist/client/assets/scroll-text-fM0sMOB3.js +0 -1
  253. package/apps/ui/dist/client/assets/select-CYF6pF37.js +0 -1
  254. package/apps/ui/dist/client/assets/settings-Dx70jgP_.js +0 -1
  255. package/apps/ui/dist/client/assets/sqlite-error-B6uZjUe8.js +0 -1
  256. package/apps/ui/dist/client/assets/styles-B7Zi9EFA.css +0 -1
  257. package/apps/ui/dist/client/assets/tabs-CmuhkT-c.js +0 -7
  258. package/apps/ui/dist/client/assets/text-filter-DVAZySUS.js +0 -2
  259. package/apps/ui/dist/client/assets/thread-transcript-stats-BUNz6u7F.js +0 -1
  260. package/apps/ui/dist/client/assets/threads._threadId-BB9tTsV4.js +0 -1
  261. package/apps/ui/dist/client/assets/threads._threadId-BMRSFow3.js +0 -1
  262. package/apps/ui/dist/client/assets/useMutation-BSFult2i.js +0 -1
  263. package/apps/ui/dist/client/assets/useQuery-CjINKZf1.js +0 -1
  264. package/apps/ui/dist/server/assets/_tanstack-start-manifest_v-CQ8Jjs3B.js +0 -387
  265. package/apps/ui/dist/server/assets/_threadId-B6SrBR9E.js +0 -6
  266. package/apps/ui/dist/server/assets/analytics-CYP_LT5Y.js +0 -16
  267. package/apps/ui/dist/server/assets/analytics-CtpOMVMO.js +0 -146
  268. package/apps/ui/dist/server/assets/antigravity-conversation-state-HgzS302O.js +0 -16
  269. package/apps/ui/dist/server/assets/antigravity-conversations._conversationId-BKOyL2T6.js +0 -613
  270. package/apps/ui/dist/server/assets/antigravity-conversations._conversationId-D426O-64.js +0 -11
  271. package/apps/ui/dist/server/assets/antigravity-conversations._conversationId-eS87Dx7g.js +0 -20
  272. package/apps/ui/dist/server/assets/antigravity-db-Bh8_U9uw.js +0 -580
  273. package/apps/ui/dist/server/assets/antigravity-keychain-DOiuHDwK.js +0 -126
  274. package/apps/ui/dist/server/assets/antigravity-keychain-panel-CC7FLmAK.js +0 -55
  275. package/apps/ui/dist/server/assets/antigravity-queries-CGrJO9Vr.js +0 -37
  276. package/apps/ui/dist/server/assets/antigravity-server-CHRjVFqN.js +0 -114
  277. package/apps/ui/dist/server/assets/antigravity._workspaceKey-3m_MzNFA.js +0 -11
  278. package/apps/ui/dist/server/assets/antigravity._workspaceKey-B0QeYUOh.js +0 -28
  279. package/apps/ui/dist/server/assets/antigravity._workspaceKey-C3kP-qyN.js +0 -210
  280. package/apps/ui/dist/server/assets/antigravity.index-CPCYkqCi.js +0 -104
  281. package/apps/ui/dist/server/assets/antigravity.index-DudTB3Tq.js +0 -11
  282. package/apps/ui/dist/server/assets/badge-EvdhKK_Z.js +0 -26
  283. package/apps/ui/dist/server/assets/button-CmTDnzOn.js +0 -46
  284. package/apps/ui/dist/server/assets/checkbox-C0hovF41.js +0 -19
  285. package/apps/ui/dist/server/assets/claude-code-db-Ik3VStKZ.js +0 -361
  286. package/apps/ui/dist/server/assets/claude-code-queries-Cgzi4K-e.js +0 -37
  287. package/apps/ui/dist/server/assets/claude-code-server-PfraClXS.js +0 -72
  288. package/apps/ui/dist/server/assets/claude-code-sessions._sessionId-C55oe6zj.js +0 -18
  289. package/apps/ui/dist/server/assets/claude-code-sessions._sessionId-D1RYYi3j.js +0 -473
  290. package/apps/ui/dist/server/assets/claude-code-sessions._sessionId-DSvPLEUL.js +0 -11
  291. package/apps/ui/dist/server/assets/claude-code-transcript-C-bxh3cS.js +0 -139
  292. package/apps/ui/dist/server/assets/claude-code-transcript-phase-DmUMc8VU.js +0 -10
  293. package/apps/ui/dist/server/assets/claude-code._workspaceKey-B5Xg_-tE.js +0 -28
  294. package/apps/ui/dist/server/assets/claude-code._workspaceKey-BPlzyXPa.js +0 -11
  295. package/apps/ui/dist/server/assets/claude-code._workspaceKey-DqDve2wl.js +0 -191
  296. package/apps/ui/dist/server/assets/claude-code.index-B7yYR7dD.js +0 -92
  297. package/apps/ui/dist/server/assets/claude-code.index-BUHThG1W.js +0 -11
  298. package/apps/ui/dist/server/assets/codex-queries-CtgeZ7VL.js +0 -99
  299. package/apps/ui/dist/server/assets/codex-server-CGXU4H8-.js +0 -2294
  300. package/apps/ui/dist/server/assets/concurrency-VXtYvlGh.js +0 -18
  301. package/apps/ui/dist/server/assets/createServerRpc-CF_DEwnm.js +0 -12
  302. package/apps/ui/dist/server/assets/createSsrRpc-C754NPki.js +0 -16
  303. package/apps/ui/dist/server/assets/cursor-db-CZnYy7aT.js +0 -830
  304. package/apps/ui/dist/server/assets/cursor-exporter-types-CI3goo-c.js +0 -34
  305. package/apps/ui/dist/server/assets/cursor-queries-NCIM0Nat.js +0 -67
  306. package/apps/ui/dist/server/assets/cursor-recovery-CXpO8KcD.js +0 -361
  307. package/apps/ui/dist/server/assets/cursor-server-DNu52A3K.js +0 -213
  308. package/apps/ui/dist/server/assets/cursor-threads._composerId-BB0Y_Mao.js +0 -11
  309. package/apps/ui/dist/server/assets/cursor-threads._composerId-Ckn6ItHI.js +0 -582
  310. package/apps/ui/dist/server/assets/cursor-threads._composerId-p9-ARAzc.js +0 -18
  311. package/apps/ui/dist/server/assets/cursor-transcript-B7tGnqtn.js +0 -125
  312. package/apps/ui/dist/server/assets/cursor._workspaceKey-CZuohIUL.js +0 -28
  313. package/apps/ui/dist/server/assets/cursor._workspaceKey-DPp5JSjy.js +0 -401
  314. package/apps/ui/dist/server/assets/cursor._workspaceKey-nmg3YIOQ.js +0 -11
  315. package/apps/ui/dist/server/assets/cursor.index-CcsX7DG0.js +0 -11
  316. package/apps/ui/dist/server/assets/cursor.index-DkWT-mxw.js +0 -189
  317. package/apps/ui/dist/server/assets/data-table-Cdct823O.js +0 -189
  318. package/apps/ui/dist/server/assets/delete-confirm-dialog-BcBNCxWB.js +0 -144
  319. package/apps/ui/dist/server/assets/download-DMmiy1xf.js +0 -92
  320. package/apps/ui/dist/server/assets/dropdown-menu-Dy_9t6TN.js +0 -36
  321. package/apps/ui/dist/server/assets/empty-plugin-adapters-B3lHh1La.js +0 -5
  322. package/apps/ui/dist/server/assets/export-dialog-D88ze9Gy.js +0 -240
  323. package/apps/ui/dist/server/assets/formatters-FJaGZgJk.js +0 -91
  324. package/apps/ui/dist/server/assets/kiro-db--fXrED_-.js +0 -552
  325. package/apps/ui/dist/server/assets/kiro-queries-CzNlM2ok.js +0 -37
  326. package/apps/ui/dist/server/assets/kiro-server-9WRxg2Au.js +0 -72
  327. package/apps/ui/dist/server/assets/kiro-sessions._sessionId-CRY2c7YO.js +0 -11
  328. package/apps/ui/dist/server/assets/kiro-sessions._sessionId-F1UlwxmR.js +0 -449
  329. package/apps/ui/dist/server/assets/kiro-sessions._sessionId-bvTulT7C.js +0 -18
  330. package/apps/ui/dist/server/assets/kiro-transcript-DoekQBaH.js +0 -112
  331. package/apps/ui/dist/server/assets/kiro-transcript-phase-CJG40nQg.js +0 -27
  332. package/apps/ui/dist/server/assets/kiro._workspaceKey-BDe9sQwo.js +0 -11
  333. package/apps/ui/dist/server/assets/kiro._workspaceKey-CJsjgNwO.js +0 -28
  334. package/apps/ui/dist/server/assets/kiro._workspaceKey-D_-4Qb7y.js +0 -193
  335. package/apps/ui/dist/server/assets/kiro.index-C__-UROk.js +0 -11
  336. package/apps/ui/dist/server/assets/kiro.index-CmTbqeYj.js +0 -93
  337. package/apps/ui/dist/server/assets/loading-panel-BGFnWseS.js +0 -27
  338. package/apps/ui/dist/server/assets/metric-card-ByEeLu0r.js +0 -23
  339. package/apps/ui/dist/server/assets/model-label-B1NWGc65.js +0 -13
  340. package/apps/ui/dist/server/assets/opencode-db-BM7KjOzc.js +0 -397
  341. package/apps/ui/dist/server/assets/opencode-queries-BeNYXnQv.js +0 -50
  342. package/apps/ui/dist/server/assets/opencode-server-BpQ_kRze.js +0 -72
  343. package/apps/ui/dist/server/assets/opencode-sessions._sessionId-CGQ-t0vD.js +0 -518
  344. package/apps/ui/dist/server/assets/opencode-sessions._sessionId-D8Q7in_Z.js +0 -18
  345. package/apps/ui/dist/server/assets/opencode-sessions._sessionId-DKOvi0lV.js +0 -11
  346. package/apps/ui/dist/server/assets/opencode-think-tags-CAoD-EcZ.js +0 -87
  347. package/apps/ui/dist/server/assets/opencode-transcript-BNTcGNym.js +0 -141
  348. package/apps/ui/dist/server/assets/opencode-transcript-phase-OqLkiQD0.js +0 -32
  349. package/apps/ui/dist/server/assets/opencode._workspaceKey-BdqcWHw1.js +0 -11
  350. package/apps/ui/dist/server/assets/opencode._workspaceKey-CfH96HTT.js +0 -28
  351. package/apps/ui/dist/server/assets/opencode._workspaceKey-DdbHPDrO.js +0 -207
  352. package/apps/ui/dist/server/assets/opencode.index-BbVzkJXP.js +0 -11
  353. package/apps/ui/dist/server/assets/opencode.index-Vh_Gq_1L.js +0 -93
  354. package/apps/ui/dist/server/assets/page-header-VNSaM3xd.js +0 -29
  355. package/apps/ui/dist/server/assets/path-transforms-DL2IwtYd.js +0 -31
  356. package/apps/ui/dist/server/assets/projects._project-4io5LO0E.js +0 -20
  357. package/apps/ui/dist/server/assets/projects._project-Bshqk7JA.js +0 -12
  358. package/apps/ui/dist/server/assets/projects._project-D6ZoGhJ8.js +0 -395
  359. package/apps/ui/dist/server/assets/projects.index-BLXOx5eL.js +0 -12
  360. package/apps/ui/dist/server/assets/projects.index-BkLiF2FF.js +0 -182
  361. package/apps/ui/dist/server/assets/projects.index-DesYXwfi.js +0 -14
  362. package/apps/ui/dist/server/assets/reload-error-panel-BJMxY3U1.js +0 -25
  363. package/apps/ui/dist/server/assets/route-search-ts4W9MJ9.js +0 -38
  364. package/apps/ui/dist/server/assets/router-Sac2DGk6.js +0 -518
  365. package/apps/ui/dist/server/assets/routes-Bsact1uB.js +0 -184
  366. package/apps/ui/dist/server/assets/routes-BzkJgq4W.js +0 -34
  367. package/apps/ui/dist/server/assets/select-GW76p-ld.js +0 -76
  368. package/apps/ui/dist/server/assets/settings-OayxIYQQ.js +0 -100
  369. package/apps/ui/dist/server/assets/settings-store-DpEJEQ7M.js +0 -52
  370. package/apps/ui/dist/server/assets/shared-DyhChtHf.js +0 -137
  371. package/apps/ui/dist/server/assets/source-session-export-server-DNutCawb.js +0 -38
  372. package/apps/ui/dist/server/assets/sqlite-error-LZDrnxdd.js +0 -13
  373. package/apps/ui/dist/server/assets/start-B_HFwkkk.js +0 -4
  374. package/apps/ui/dist/server/assets/tabs-DkaChNVg.js +0 -501
  375. package/apps/ui/dist/server/assets/text-filter-CGKxMCKt.js +0 -36
  376. package/apps/ui/dist/server/assets/thread-transcript-stats-Dh34mt2u.js +0 -45
  377. package/apps/ui/dist/server/assets/threads._threadId-BSSK4nkI.js +0 -26
  378. package/apps/ui/dist/server/assets/threads._threadId-BeXEc4Lo.js +0 -648
  379. package/apps/ui/dist/server/assets/threads._threadId-Ci0S4mKU.js +0 -18
  380. package/apps/ui/dist/server/assets/ui-export-files-BHLX9bhN.js +0 -83
  381. package/apps/ui/dist/server/assets/utils-C_uf36nf.js +0 -8
  382. package/apps/ui/dist/server/server.js +0 -5895
  383. package/bin/codex-chats-claude.js +0 -5
  384. package/bin/codex-chats.js +0 -5
  385. package/bin/spiracha.js +0 -5
  386. package/src/export-chats.ts +0 -120
  387. package/src/export-claude.ts +0 -36
  388. package/src/export-cursor.ts +0 -244
  389. package/src/lib/claude-exporter.ts +0 -864
  390. package/src/lib/codex-exporter-cli.ts +0 -277
  391. package/src/lib/codex-exporter-db.ts +0 -319
  392. package/src/lib/codex-exporter.ts +0 -115
  393. package/src/lib/cursor-exporter.ts +0 -266
  394. package/src/lib/interactive-cli.ts +0 -433
  395. package/src/lib/native-open.ts +0 -54
  396. package/src/mcp-server.ts +0 -137
  397. package/src/spiracha.ts +0 -116
  398. package/src/ui-cli.ts +0 -310
  399. /package/apps/ui/{dist/client → public}/icon.svg +0 -0
  400. /package/apps/ui/{dist/client → public}/manifest.json +0 -0
  401. /package/apps/ui/{dist/client → public}/robots.txt +0 -0
@@ -0,0 +1,1103 @@
1
+ import type {
2
+ DynamicToolDefinition,
3
+ MessageEvent,
4
+ ParsedCodexTranscript,
5
+ ThreadEvent,
6
+ } from '@spiracha/lib/codex-browser-types';
7
+ import { useMutation, useQuery, useQueryClient, useSuspenseQuery } from '@tanstack/react-query';
8
+ import { createFileRoute, Link, useNavigate } from '@tanstack/react-router';
9
+ import { ChevronDown, ChevronUp, Download, Search, Trash2 } from 'lucide-react';
10
+ import type { KeyboardEvent } from 'react';
11
+ import { startTransition, useEffect, useMemo, useState } from 'react';
12
+ import { Breadcrumbs } from '#/components/breadcrumbs';
13
+ import { DeleteConfirmDialog } from '#/components/delete-confirm-dialog';
14
+ import { ExportDialog } from '#/components/export-dialog';
15
+ import { JsonPanel } from '#/components/json-panel';
16
+ import { LoadingPanel } from '#/components/loading-panel';
17
+ import { MetadataSection } from '#/components/metadata-section';
18
+ import { MetricCard } from '#/components/metric-card';
19
+ import { PageHeader } from '#/components/page-header';
20
+ import {
21
+ getTranscriptEventKey,
22
+ shouldShowEvent,
23
+ type TranscriptSortOrder,
24
+ TranscriptView,
25
+ } from '#/components/transcript-view';
26
+ import { Badge } from '#/components/ui/badge';
27
+ import { Button } from '#/components/ui/button';
28
+ import { Checkbox } from '#/components/ui/checkbox';
29
+ import { Input } from '#/components/ui/input';
30
+ import { Tabs, TabsContent, TabsList, TabsTrigger } from '#/components/ui/tabs';
31
+ import {
32
+ threadSnapshotQueryOptions,
33
+ threadTranscriptPreviewQueryOptions,
34
+ threadTranscriptQueryOptions,
35
+ } from '#/lib/codex-queries';
36
+ import { deleteThreadFn, exportThreadFn, type getThreadSnapshotFn } from '#/lib/codex-server';
37
+ import { downloadTextFile, downloadUrlFile } from '#/lib/download';
38
+ import {
39
+ formatBooleanLabel,
40
+ formatBytes,
41
+ formatDateTime,
42
+ formatList,
43
+ formatModelLabel,
44
+ formatTokens,
45
+ } from '#/lib/formatters';
46
+ import { applyPathTransforms } from '#/lib/path-utils';
47
+ import {
48
+ parseThreadTranscriptSearch,
49
+ type ThreadTranscriptSearch,
50
+ withThreadTranscriptSearch,
51
+ } from '#/lib/route-search';
52
+ import { useSettings } from '#/lib/settings-store';
53
+ import { shouldLoadFullThreadTranscript } from '#/lib/thread-transcript-load';
54
+
55
+ type ThreadSnapshotResponse = Awaited<ReturnType<typeof getThreadSnapshotFn>>;
56
+ type ThreadTranscript = ParsedCodexTranscript;
57
+ type ThreadSnapshot = Omit<ThreadSnapshotResponse, 'availableTools' | 'transcript'> & {
58
+ availableTools: DynamicToolDefinition[];
59
+ transcript: ThreadTranscript | null;
60
+ };
61
+
62
+ type TranscriptSearchResult = {
63
+ event: MessageEvent;
64
+ eventIndex: number;
65
+ eventKey: string;
66
+ messageNumber: number;
67
+ phase: string | null;
68
+ roleLabel: string;
69
+ snippet: string;
70
+ };
71
+
72
+ type TranscriptSearchPanelProps = {
73
+ activeResultIndex: number;
74
+ query: string;
75
+ results: TranscriptSearchResult[];
76
+ onJumpToResult: (index: number) => void;
77
+ onQueryChange: (value: string) => void;
78
+ };
79
+
80
+ type TranscriptSearchFilters = {
81
+ showCommentary: boolean;
82
+ showExtraEvents: boolean;
83
+ showToolCalls: boolean;
84
+ showUserMessages: boolean;
85
+ };
86
+
87
+ const SEARCH_SNIPPET_RADIUS = 72;
88
+
89
+ const getTranscriptFiltersFromSearch = (search: ThreadTranscriptSearch) => ({
90
+ showCommentary: search.commentary === true,
91
+ showExtraEvents: search.extra === true,
92
+ showToolCalls: search.tools === true,
93
+ showUserMessages: search.user === true,
94
+ });
95
+
96
+ type ThreadTranscriptFilters = ReturnType<typeof getTranscriptFiltersFromSearch>;
97
+
98
+ const normalizeTranscriptSearchText = (value: string) => value.replace(/\s+/gu, ' ').trim();
99
+
100
+ const getTranscriptSearchRoleLabel = (event: MessageEvent, assistantModel: string | null) => {
101
+ if (event.role === 'assistant') {
102
+ return formatModelLabel(event.model ?? assistantModel);
103
+ }
104
+
105
+ return event.role === 'system' ? 'System' : 'User';
106
+ };
107
+
108
+ const buildTranscriptSearchSnippet = (text: string, query: string) => {
109
+ const normalizedText = normalizeTranscriptSearchText(text);
110
+ const normalizedQuery = normalizeTranscriptSearchText(query).toLowerCase();
111
+ const matchIndex = normalizedText.toLowerCase().indexOf(normalizedQuery);
112
+
113
+ if (matchIndex < 0) {
114
+ return normalizedText.slice(0, SEARCH_SNIPPET_RADIUS * 2);
115
+ }
116
+
117
+ const start = Math.max(0, matchIndex - SEARCH_SNIPPET_RADIUS);
118
+ const end = Math.min(normalizedText.length, matchIndex + normalizedQuery.length + SEARCH_SNIPPET_RADIUS);
119
+ const prefix = start > 0 ? '...' : '';
120
+ const suffix = end < normalizedText.length ? '...' : '';
121
+
122
+ return `${prefix}${normalizedText.slice(start, end)}${suffix}`;
123
+ };
124
+
125
+ const buildTranscriptSearchResults = (
126
+ events: ThreadEvent[],
127
+ query: string,
128
+ assistantModel: string | null,
129
+ filters: TranscriptSearchFilters,
130
+ transform: (text: string) => string,
131
+ ): TranscriptSearchResult[] => {
132
+ const normalizedQuery = normalizeTranscriptSearchText(query).toLowerCase();
133
+ if (!normalizedQuery) {
134
+ return [];
135
+ }
136
+
137
+ const results: TranscriptSearchResult[] = [];
138
+ let messageNumber = 0;
139
+
140
+ events.forEach((event, index) => {
141
+ if (event.kind !== 'message') {
142
+ return;
143
+ }
144
+
145
+ if (
146
+ !shouldShowEvent(
147
+ event,
148
+ filters.showToolCalls,
149
+ filters.showExtraEvents,
150
+ filters.showCommentary,
151
+ filters.showUserMessages,
152
+ )
153
+ ) {
154
+ return;
155
+ }
156
+
157
+ messageNumber += 1;
158
+ const searchText = normalizeTranscriptSearchText(transform(event.text));
159
+ if (!searchText.toLowerCase().includes(normalizedQuery)) {
160
+ return;
161
+ }
162
+
163
+ results.push({
164
+ event,
165
+ eventIndex: index,
166
+ eventKey: getTranscriptEventKey(event, index),
167
+ messageNumber,
168
+ phase: event.phase,
169
+ roleLabel: getTranscriptSearchRoleLabel(event, assistantModel),
170
+ snippet: buildTranscriptSearchSnippet(searchText, query),
171
+ });
172
+ });
173
+
174
+ return results;
175
+ };
176
+
177
+ type TranscriptControlsProps = {
178
+ rawJsonDisabled?: boolean;
179
+ showCommentary: boolean;
180
+ showExtraEvents: boolean;
181
+ showRawJson: boolean;
182
+ showToolCalls: boolean;
183
+ showUserMessages: boolean;
184
+ onShowCommentaryChange: (checked: boolean) => void;
185
+ onShowExtraEventsChange: (checked: boolean) => void;
186
+ onShowRawJsonChange: (checked: boolean) => void;
187
+ onShowToolCallsChange: (checked: boolean) => void;
188
+ onShowUserMessagesChange: (checked: boolean) => void;
189
+ };
190
+
191
+ type ThreadMetadataProps = {
192
+ snapshot: ThreadSnapshot;
193
+ };
194
+
195
+ const buildThreadItems = (snapshot: ThreadSnapshot) => {
196
+ return [
197
+ { label: 'Thread ID', value: <span data-mono="true">{snapshot.thread.id}</span> },
198
+ { label: 'Project', value: snapshot.project },
199
+ { label: 'CWD', value: <span data-mono="true">{snapshot.thread.cwd}</span> },
200
+ {
201
+ label: 'Created',
202
+ value: (
203
+ <span suppressHydrationWarning>
204
+ {formatDateTime(snapshot.thread.created_at_ms ?? snapshot.thread.created_at * 1000)}
205
+ </span>
206
+ ),
207
+ },
208
+ {
209
+ label: 'Updated',
210
+ value: (
211
+ <span suppressHydrationWarning>
212
+ {formatDateTime(snapshot.thread.updated_at_ms ?? snapshot.thread.updated_at * 1000)}
213
+ </span>
214
+ ),
215
+ },
216
+ {
217
+ label: 'Session started',
218
+ value: (
219
+ <span suppressHydrationWarning>
220
+ {formatDateTime(snapshot.transcript?.sessionMeta.timestamp ?? null)}
221
+ </span>
222
+ ),
223
+ },
224
+ { label: 'Rollout size', value: formatBytes(snapshot.rollout.fileSizeBytes) },
225
+ { label: 'Archived', value: formatBooleanLabel(Boolean(snapshot.thread.archived)) },
226
+ {
227
+ label: 'Archived at',
228
+ value: (
229
+ <span suppressHydrationWarning>
230
+ {formatDateTime(snapshot.thread.archived_at ? snapshot.thread.archived_at * 1000 : null)}
231
+ </span>
232
+ ),
233
+ },
234
+ ];
235
+ };
236
+
237
+ const buildRuntimeItems = (snapshot: ThreadSnapshot) => {
238
+ return [
239
+ { label: 'Source', value: snapshot.thread.source },
240
+ { label: 'Originator', value: snapshot.transcript?.sessionMeta.originator ?? 'n/a' },
241
+ { label: 'Model provider', value: snapshot.thread.model_provider },
242
+ { label: 'Reasoning effort', value: snapshot.thread.reasoning_effort ?? 'n/a' },
243
+ { label: 'CLI version', value: snapshot.thread.cli_version },
244
+ { label: 'Approval mode', value: snapshot.thread.approval_mode },
245
+ { label: 'Memory mode', value: snapshot.thread.memory_mode },
246
+ { label: 'Has user event', value: formatBooleanLabel(Boolean(snapshot.thread.has_user_event)) },
247
+ ];
248
+ };
249
+
250
+ const buildGitItems = (snapshot: ThreadSnapshot) => {
251
+ return [
252
+ { label: 'Git branch', value: snapshot.thread.git_branch ?? 'n/a' },
253
+ { label: 'Git SHA', value: snapshot.thread.git_sha ?? 'n/a' },
254
+ { label: 'Git remote', value: snapshot.thread.git_origin_url ?? 'n/a' },
255
+ { label: 'Agent nickname', value: snapshot.thread.agent_nickname ?? 'n/a' },
256
+ { label: 'Agent role', value: snapshot.thread.agent_role ?? 'n/a' },
257
+ { label: 'Agent path', value: snapshot.thread.agent_path ?? 'n/a' },
258
+ ];
259
+ };
260
+
261
+ const buildRelationItems = (snapshot: ThreadSnapshot) => {
262
+ const parentThreadValue = snapshot.relations.parentThreadId ? (
263
+ <Link
264
+ className="text-[var(--accent)]"
265
+ params={{ threadId: snapshot.relations.parentThreadId }}
266
+ to="/threads/$threadId"
267
+ >
268
+ {snapshot.relations.parentThreadId}
269
+ </Link>
270
+ ) : (
271
+ 'n/a'
272
+ );
273
+
274
+ const childThreadValue =
275
+ snapshot.relations.childEdges.length > 0 ? (
276
+ <div className="flex flex-wrap gap-2">
277
+ {snapshot.relations.childEdges.map((edge) => (
278
+ <Link
279
+ key={edge.child_thread_id}
280
+ className="rounded-full border border-[var(--border)] px-3 py-1 text-[var(--accent)] text-xs"
281
+ params={{ threadId: edge.child_thread_id }}
282
+ to="/threads/$threadId"
283
+ >
284
+ {edge.child_thread_id}
285
+ </Link>
286
+ ))}
287
+ </div>
288
+ ) : (
289
+ 'n/a'
290
+ );
291
+
292
+ return [
293
+ { label: 'Parent thread', value: parentThreadValue },
294
+ { label: 'Child threads', value: childThreadValue },
295
+ { label: 'First user message', value: snapshot.thread.first_user_message || 'n/a' },
296
+ { label: 'Preview', value: snapshot.thread.preview || 'n/a' },
297
+ ];
298
+ };
299
+
300
+ const buildTranscriptStatsItems = (snapshot: ThreadSnapshot) => {
301
+ if (!snapshot.transcript) {
302
+ if (snapshot.transcriptState === 'missing') {
303
+ return [
304
+ { label: 'Transcript load', value: 'Transcript file missing from disk' },
305
+ { label: 'Rollout path', value: snapshot.thread.rollout_path },
306
+ {
307
+ label: 'Preview mode',
308
+ value: 'Export and transcript browsing are unavailable until the file exists again.',
309
+ },
310
+ ];
311
+ }
312
+
313
+ return [
314
+ { label: 'Transcript load', value: 'Deferred for oversized rollout' },
315
+ { label: 'Rollout size', value: formatBytes(snapshot.rollout.fileSizeBytes) },
316
+ { label: 'Deferred load', value: 'Load the transcript manually to inspect it.' },
317
+ ];
318
+ }
319
+
320
+ return [
321
+ {
322
+ label: 'Event kinds',
323
+ value: formatList([...new Set(snapshot.transcript.events.map((event) => event.kind))]),
324
+ },
325
+ { label: 'Stats scope', value: snapshot.transcript.statsArePartial ? 'Preview only' : 'Full transcript' },
326
+ { label: 'Tool calls', value: String(snapshot.transcript.stats.toolCallCount) },
327
+ { label: 'Exec calls', value: String(snapshot.transcript.stats.execCommandCount) },
328
+ { label: 'Web search events', value: String(snapshot.transcript.stats.webSearchEventCount) },
329
+ { label: 'Assistant messages', value: String(snapshot.transcript.stats.assistantMessageCount) },
330
+ { label: 'Commentary updates', value: String(snapshot.transcript.stats.commentaryCount) },
331
+ ];
332
+ };
333
+
334
+ function TranscriptControls({
335
+ rawJsonDisabled = false,
336
+ showCommentary,
337
+ showExtraEvents,
338
+ showRawJson,
339
+ showToolCalls,
340
+ showUserMessages,
341
+ onShowCommentaryChange,
342
+ onShowExtraEventsChange,
343
+ onShowRawJsonChange,
344
+ onShowToolCallsChange,
345
+ onShowUserMessagesChange,
346
+ }: TranscriptControlsProps) {
347
+ return (
348
+ <div className="flex flex-wrap gap-4 rounded-xl border border-[var(--border)] bg-[var(--panel)] px-4 py-3 shadow-[var(--panel-shadow)]">
349
+ <div className="flex items-center gap-2 text-sm">
350
+ <Checkbox
351
+ checked={showToolCalls}
352
+ id="transcript-show-tool-calls"
353
+ onCheckedChange={(checked) => onShowToolCallsChange(checked === true)}
354
+ />
355
+ <label htmlFor="transcript-show-tool-calls">Show tool calls</label>
356
+ </div>
357
+ <div className="flex items-center gap-2 text-sm">
358
+ <Checkbox
359
+ checked={showCommentary}
360
+ id="transcript-show-commentary"
361
+ onCheckedChange={(checked) => onShowCommentaryChange(checked === true)}
362
+ />
363
+ <label htmlFor="transcript-show-commentary">Show commentary</label>
364
+ </div>
365
+ <div className="flex items-center gap-2 text-sm">
366
+ <Checkbox
367
+ checked={showExtraEvents}
368
+ id="transcript-show-extra-events"
369
+ onCheckedChange={(checked) => onShowExtraEventsChange(checked === true)}
370
+ />
371
+ <label htmlFor="transcript-show-extra-events">Show extra events</label>
372
+ </div>
373
+ <div className="flex items-center gap-2 text-sm">
374
+ <Checkbox
375
+ checked={showRawJson}
376
+ disabled={rawJsonDisabled}
377
+ id="transcript-show-raw-json"
378
+ onCheckedChange={(checked) => onShowRawJsonChange(checked === true)}
379
+ />
380
+ <label htmlFor="transcript-show-raw-json">Raw JSON</label>
381
+ </div>
382
+ <div className="flex items-center gap-2 text-sm">
383
+ <Checkbox
384
+ checked={showUserMessages}
385
+ id="transcript-show-user-messages"
386
+ onCheckedChange={(checked) => onShowUserMessagesChange(checked === true)}
387
+ />
388
+ <label htmlFor="transcript-show-user-messages">User</label>
389
+ </div>
390
+ </div>
391
+ );
392
+ }
393
+
394
+ function TranscriptSearchPanel({
395
+ activeResultIndex,
396
+ query,
397
+ results,
398
+ onJumpToResult,
399
+ onQueryChange,
400
+ }: TranscriptSearchPanelProps) {
401
+ const hasQuery = normalizeTranscriptSearchText(query).length > 0;
402
+ const hasResults = results.length > 0;
403
+ const statusLabel = hasQuery
404
+ ? hasResults
405
+ ? `${activeResultIndex + 1} / ${results.length}`
406
+ : 'No matches'
407
+ : 'Search';
408
+
409
+ const handleKeyDown = (event: KeyboardEvent<HTMLInputElement>) => {
410
+ if (event.key !== 'Enter' || !hasResults) {
411
+ return;
412
+ }
413
+
414
+ event.preventDefault();
415
+ onJumpToResult(event.shiftKey ? activeResultIndex - 1 : activeResultIndex);
416
+ };
417
+
418
+ return (
419
+ <section className="rounded-xl border border-[var(--border)] bg-[var(--panel)] px-4 py-3 shadow-[var(--panel-shadow)]">
420
+ <div className="flex flex-col gap-3 lg:flex-row lg:items-center">
421
+ <div className="relative min-w-0 flex-1">
422
+ <Search className="pointer-events-none absolute top-1/2 left-3 size-4 -translate-y-1/2 text-[var(--muted-foreground)]" />
423
+ <Input
424
+ aria-label="Search transcript messages"
425
+ className="h-10 rounded-full border-[var(--border)] bg-[var(--panel-secondary)] pr-4 pl-9"
426
+ placeholder="Search transcript messages"
427
+ value={query}
428
+ onChange={(event) => onQueryChange(event.target.value)}
429
+ onKeyDown={handleKeyDown}
430
+ />
431
+ </div>
432
+
433
+ <div className="flex flex-wrap items-center gap-2">
434
+ <span className="min-w-20 text-right text-[var(--muted-foreground)] text-sm">{statusLabel}</span>
435
+ <Button
436
+ className="rounded-full"
437
+ disabled={!hasResults}
438
+ size="sm"
439
+ type="button"
440
+ variant="outline"
441
+ onClick={() => onJumpToResult(activeResultIndex - 1)}
442
+ >
443
+ <ChevronUp className="size-4" />
444
+ Prev
445
+ </Button>
446
+ <Button
447
+ className="rounded-full"
448
+ disabled={!hasResults}
449
+ size="sm"
450
+ type="button"
451
+ variant="outline"
452
+ onClick={() => onJumpToResult(activeResultIndex + 1)}
453
+ >
454
+ <ChevronDown className="size-4" />
455
+ Next
456
+ </Button>
457
+ </div>
458
+ </div>
459
+
460
+ {hasQuery && hasResults ? (
461
+ <div className="mt-3 max-h-72 overflow-auto rounded-xl border border-[var(--border)] bg-[var(--panel-secondary)]">
462
+ {results.map((result, index) => (
463
+ <button
464
+ key={result.eventKey}
465
+ aria-current={index === activeResultIndex ? 'true' : undefined}
466
+ className="block w-full border-[var(--border)] border-b px-3 py-2.5 text-left transition last:border-b-0 hover:bg-[var(--panel)] aria-current:bg-[var(--panel)]"
467
+ type="button"
468
+ onClick={() => onJumpToResult(index)}
469
+ >
470
+ <span className="flex flex-wrap items-center gap-2">
471
+ <span className="font-medium text-sm">Message {result.messageNumber}</span>
472
+ <Badge variant="outline">{result.roleLabel}</Badge>
473
+ {result.phase ? <Badge variant="outline">{result.phase}</Badge> : null}
474
+ </span>
475
+ <span className="mt-1 block min-w-0 break-words text-[var(--muted-foreground)] text-sm leading-5 [overflow-wrap:anywhere]">
476
+ {result.snippet}
477
+ </span>
478
+ </button>
479
+ ))}
480
+ </div>
481
+ ) : null}
482
+ </section>
483
+ );
484
+ }
485
+
486
+ function ThreadMetadataPanels({ snapshot }: ThreadMetadataProps) {
487
+ return (
488
+ <div className="grid gap-4 xl:grid-cols-[1.1fr_0.9fr]">
489
+ <div className="space-y-4">
490
+ <MetadataSection items={buildThreadItems(snapshot)} title="Thread" />
491
+ <MetadataSection items={buildRuntimeItems(snapshot)} title="Runtime" />
492
+ <MetadataSection items={buildGitItems(snapshot)} title="Git and agent" />
493
+ </div>
494
+
495
+ <div className="space-y-4">
496
+ <MetadataSection items={buildRelationItems(snapshot)} title="Relations and summary" />
497
+
498
+ <section className="rounded-[1.6rem] border border-[var(--border)] bg-[var(--panel)] p-5 shadow-[var(--panel-shadow)]">
499
+ <h3 className="font-semibold text-[var(--muted-foreground)] text-sm uppercase tracking-[0.18em]">
500
+ Available tools
501
+ </h3>
502
+ <div className="mt-4 space-y-3">
503
+ {snapshot.availableTools.map((tool) => (
504
+ <div
505
+ key={`${tool.name}-${tool.namespace ?? 'global'}`}
506
+ className="rounded-xl border border-[var(--border)] bg-[var(--panel-secondary)] p-3.5"
507
+ >
508
+ <div className="flex flex-wrap items-center gap-2">
509
+ <p className="font-medium font-mono text-sm">{tool.name}</p>
510
+ {tool.namespace ? <Badge variant="outline">{tool.namespace}</Badge> : null}
511
+ </div>
512
+ <p className="mt-1.5 text-[var(--muted-foreground)] text-sm">
513
+ {tool.description || 'No description.'}
514
+ </p>
515
+ </div>
516
+ ))}
517
+ </div>
518
+ </section>
519
+
520
+ <MetadataSection items={buildTranscriptStatsItems(snapshot)} title="Transcript stats" />
521
+ </div>
522
+ </div>
523
+ );
524
+ }
525
+
526
+ function ThreadRawPanels({ snapshot }: ThreadMetadataProps) {
527
+ if (!snapshot.transcript) {
528
+ return (
529
+ <div className="rounded-xl border border-[var(--border)] bg-[var(--panel)] px-5 py-4 text-sm">
530
+ {snapshot.transcriptState === 'missing'
531
+ ? 'The rollout JSONL file is missing from disk, so raw transcript payloads are unavailable.'
532
+ : 'Raw transcript payloads are deferred for oversized rollouts. Use Export if you only need the full thread contents, or load the transcript manually from the Transcript tab.'}
533
+ </div>
534
+ );
535
+ }
536
+
537
+ return (
538
+ <div className="space-y-4">
539
+ <JsonPanel title="Session meta" value={snapshot.transcript.sessionMeta} />
540
+ <JsonPanel title="Turn contexts" value={snapshot.transcript.turnContexts} />
541
+ <JsonPanel title="Sandbox policy" value={snapshot.thread.sandbox_policy} />
542
+ </div>
543
+ );
544
+ }
545
+
546
+ function DeferredTranscriptNotice({
547
+ fileSizeBytes,
548
+ missing,
549
+ pending,
550
+ onLoad,
551
+ }: {
552
+ fileSizeBytes: number | null;
553
+ missing?: boolean;
554
+ pending: boolean;
555
+ onLoad: () => void;
556
+ }) {
557
+ return (
558
+ <section className="rounded-[1.6rem] border border-[var(--border)] bg-[var(--panel)] p-5 shadow-[var(--panel-shadow)]">
559
+ <h3 className="font-semibold text-base">
560
+ {missing ? 'Transcript file missing' : 'This is a very big thread'}
561
+ </h3>
562
+ <p className="mt-2 text-[var(--muted-foreground)] text-sm leading-6">
563
+ {missing
564
+ ? 'The rollout JSONL referenced by this thread is no longer present on disk. Export may still work if the file is restored, but transcript browsing is unavailable right now.'
565
+ : `Spiracha skipped loading the transcript automatically because the rollout file is ${formatBytes(fileSizeBytes)}. Export still works immediately. Load the full transcript when you need to inspect it here.`}
566
+ </p>
567
+ {missing ? null : (
568
+ <div className="mt-4">
569
+ <Button disabled={pending} variant="outline" onClick={onLoad}>
570
+ {pending ? 'Loading full thread...' : 'Load Full Thread'}
571
+ </Button>
572
+ </div>
573
+ )}
574
+ </section>
575
+ );
576
+ }
577
+
578
+ function LargeThreadPreviewNotice({
579
+ fileSizeBytes,
580
+ fullTranscriptLoaded,
581
+ pending,
582
+ onLoad,
583
+ }: {
584
+ fileSizeBytes: number | null;
585
+ fullTranscriptLoaded: boolean;
586
+ pending: boolean;
587
+ onLoad: () => void;
588
+ }) {
589
+ const buttonLabel = fullTranscriptLoaded
590
+ ? 'Full thread loaded'
591
+ : pending
592
+ ? 'Loading full thread...'
593
+ : 'Load Full Thread';
594
+
595
+ return (
596
+ <section className="rounded-[1.6rem] border border-[var(--border)] bg-[var(--panel)] p-5 shadow-[var(--panel-shadow)]">
597
+ <h3 className="font-semibold text-base">Showing the latest matching messages</h3>
598
+ <p className="mt-2 text-[var(--muted-foreground)] text-sm leading-6">
599
+ This rollout is {formatBytes(fileSizeBytes)}, so Spiracha loaded a small window from the end of the
600
+ thread using the current transcript filters. Load the full thread when you need earlier messages.
601
+ </p>
602
+ <div className="mt-4">
603
+ <Button disabled={pending || fullTranscriptLoaded} variant="outline" onClick={onLoad}>
604
+ {buttonLabel}
605
+ </Button>
606
+ </div>
607
+ </section>
608
+ );
609
+ }
610
+
611
+ function ThreadErrorComponent({ error }: { error: Error }) {
612
+ const isSqlite = error.message.includes('unable to open database') || error.message.includes('database is locked');
613
+ return (
614
+ <div className="rounded-xl border border-[var(--border)] bg-[var(--panel)] px-6 py-10 text-center">
615
+ <p className="font-medium text-[var(--destructive)] text-sm">
616
+ {isSqlite ? 'Database unavailable' : 'Failed to load thread'}
617
+ </p>
618
+ <p className="mt-2 text-[var(--muted-foreground)] text-sm">
619
+ {isSqlite ? 'Codex may have an exclusive lock on the database. Reload to retry.' : error.message}
620
+ </p>
621
+ <button
622
+ className="mt-4 text-[var(--accent)] text-sm underline-offset-2 hover:underline"
623
+ type="button"
624
+ onClick={() => window.location.reload()}
625
+ >
626
+ Reload
627
+ </button>
628
+ </div>
629
+ );
630
+ }
631
+
632
+ const getThreadExportErrorMessage = (transcriptMissing: boolean, error: unknown): string | null => {
633
+ if (transcriptMissing) {
634
+ return 'The rollout JSONL file is missing from disk, so this thread cannot be exported right now.';
635
+ }
636
+
637
+ return error instanceof Error ? error.message : null;
638
+ };
639
+
640
+ export const Route = createFileRoute('/threads/$threadId')({
641
+ component: ThreadDetailPage,
642
+ errorComponent: ThreadErrorComponent,
643
+ loader: ({ context, deps, params }) =>
644
+ context.queryClient.ensureQueryData(
645
+ threadSnapshotQueryOptions(
646
+ params.threadId,
647
+ (deps as { transcriptFilters: ThreadTranscriptFilters }).transcriptFilters,
648
+ ),
649
+ ),
650
+ loaderDeps: ({ search }) => ({
651
+ transcriptFilters: getTranscriptFiltersFromSearch(parseThreadTranscriptSearch(search)),
652
+ }),
653
+ pendingComponent: () => (
654
+ <LoadingPanel
655
+ description="Loading the transcript, metadata, and parsed event stream for this thread."
656
+ title="Loading thread"
657
+ />
658
+ ),
659
+ validateSearch: parseThreadTranscriptSearch,
660
+ });
661
+
662
+ const useThreadTranscriptRouteSearch = () => {
663
+ const navigate = useNavigate({ from: Route.fullPath });
664
+ const search = Route.useSearch();
665
+ const transcriptFilters = useMemo(() => getTranscriptFiltersFromSearch(search), [search]);
666
+
667
+ const updateTranscriptSearch = (patch: Partial<ThreadTranscriptSearch>) => {
668
+ startTransition(() => {
669
+ void navigate({
670
+ params: true,
671
+ replace: true,
672
+ search: (previous: Record<string, unknown>) => withThreadTranscriptSearch(previous, patch),
673
+ });
674
+ });
675
+ };
676
+
677
+ return {
678
+ navigate,
679
+ search,
680
+ transcriptFilters,
681
+ updateTranscriptSearch,
682
+ };
683
+ };
684
+
685
+ const useTranscriptSearchResults = ({
686
+ assistantModel,
687
+ filters,
688
+ projectPath,
689
+ query,
690
+ settings,
691
+ transcript,
692
+ }: {
693
+ assistantModel: string | null;
694
+ filters: TranscriptSearchFilters;
695
+ projectPath: string;
696
+ query: string;
697
+ settings: ReturnType<typeof useSettings>['settings'];
698
+ transcript: ThreadTranscript | null;
699
+ }) =>
700
+ useMemo(
701
+ () =>
702
+ transcript
703
+ ? buildTranscriptSearchResults(transcript.events, query, assistantModel, filters, (text) =>
704
+ applyPathTransforms(text, settings, projectPath),
705
+ )
706
+ : [],
707
+ [assistantModel, filters, projectPath, query, settings, transcript],
708
+ );
709
+
710
+ function ThreadTranscriptTab({
711
+ activeEventJumpSignal,
712
+ activeSearchResultIndex,
713
+ activeTranscriptEventKey,
714
+ fullTranscriptLoaded,
715
+ loadError,
716
+ loadingFullTranscript,
717
+ searchInput,
718
+ searchResults,
719
+ showRawJson,
720
+ sortOrder,
721
+ snapshot,
722
+ transcript,
723
+ transcriptFilters,
724
+ onJumpToSearchResult,
725
+ onLoadFullThread,
726
+ onSearchInputChange,
727
+ onSortOrderChange,
728
+ onTranscriptFilterChange,
729
+ }: {
730
+ activeEventJumpSignal: number;
731
+ activeSearchResultIndex: number;
732
+ activeTranscriptEventKey: string | null;
733
+ fullTranscriptLoaded: boolean;
734
+ loadError: unknown;
735
+ loadingFullTranscript: boolean;
736
+ searchInput: string;
737
+ searchResults: TranscriptSearchResult[];
738
+ showRawJson: boolean;
739
+ sortOrder: TranscriptSortOrder;
740
+ snapshot: ThreadSnapshot;
741
+ transcript: ThreadTranscript | null;
742
+ transcriptFilters: ThreadTranscriptFilters;
743
+ onJumpToSearchResult: (index: number) => void;
744
+ onLoadFullThread: () => void;
745
+ onSearchInputChange: (value: string) => void;
746
+ onSortOrderChange: (value: TranscriptSortOrder) => void;
747
+ onTranscriptFilterChange: (patch: Partial<ThreadTranscriptSearch>) => void;
748
+ }) {
749
+ return (
750
+ <TabsContent className="space-y-3" value="transcript">
751
+ <TranscriptControls
752
+ rawJsonDisabled={!transcript?.rawIncluded}
753
+ showCommentary={transcriptFilters.showCommentary}
754
+ showExtraEvents={transcriptFilters.showExtraEvents}
755
+ showRawJson={showRawJson}
756
+ showToolCalls={transcriptFilters.showToolCalls}
757
+ showUserMessages={transcriptFilters.showUserMessages}
758
+ onShowCommentaryChange={(commentary) => onTranscriptFilterChange({ commentary })}
759
+ onShowExtraEventsChange={(extra) => onTranscriptFilterChange({ extra })}
760
+ onShowRawJsonChange={(raw) => onTranscriptFilterChange({ raw })}
761
+ onShowToolCallsChange={(tools) => onTranscriptFilterChange({ tools })}
762
+ onShowUserMessagesChange={(user) => onTranscriptFilterChange({ user })}
763
+ />
764
+ {transcript?.isPartial && snapshot.rollout.shouldDeferTranscriptLoad ? (
765
+ <LargeThreadPreviewNotice
766
+ fileSizeBytes={snapshot.rollout.fileSizeBytes}
767
+ fullTranscriptLoaded={fullTranscriptLoaded}
768
+ pending={loadingFullTranscript}
769
+ onLoad={onLoadFullThread}
770
+ />
771
+ ) : null}
772
+ {transcript ? (
773
+ <TranscriptSearchPanel
774
+ activeResultIndex={activeSearchResultIndex}
775
+ query={searchInput}
776
+ results={searchResults}
777
+ onJumpToResult={onJumpToSearchResult}
778
+ onQueryChange={onSearchInputChange}
779
+ />
780
+ ) : null}
781
+ {transcript ? (
782
+ <TranscriptView
783
+ activeEventJumpSignal={activeEventJumpSignal}
784
+ activeEventKey={activeTranscriptEventKey}
785
+ assistantModel={snapshot.thread.model}
786
+ events={transcript.events}
787
+ projectPath={snapshot.thread.cwd}
788
+ showCommentary={transcriptFilters.showCommentary}
789
+ showExtraEvents={transcriptFilters.showExtraEvents}
790
+ showRawJson={showRawJson && transcript.rawIncluded}
791
+ showToolCalls={transcriptFilters.showToolCalls}
792
+ showUserMessages={transcriptFilters.showUserMessages}
793
+ sortOrder={sortOrder}
794
+ onSortOrderChange={onSortOrderChange}
795
+ />
796
+ ) : (
797
+ <DeferredTranscriptNotice
798
+ fileSizeBytes={snapshot.rollout.fileSizeBytes}
799
+ missing={snapshot.transcriptState === 'missing'}
800
+ pending={loadingFullTranscript}
801
+ onLoad={onLoadFullThread}
802
+ />
803
+ )}
804
+ {loadError ? (
805
+ <p className="text-[var(--destructive)] text-sm">
806
+ Failed to load transcript preview:{' '}
807
+ {loadError instanceof Error ? loadError.message : 'Unknown error'}
808
+ </p>
809
+ ) : null}
810
+ </TabsContent>
811
+ );
812
+ }
813
+
814
+ function ThreadDetailPage() {
815
+ const { navigate, search, transcriptFilters, updateTranscriptSearch } = useThreadTranscriptRouteSearch();
816
+ const queryClient = useQueryClient();
817
+ const params = Route.useParams();
818
+ const snapshot = useSuspenseQuery(threadSnapshotQueryOptions(params.threadId, transcriptFilters)).data;
819
+ const { settings } = useSettings();
820
+ const transcriptMissing = snapshot.transcriptState === 'missing';
821
+ const [shouldLoadTranscript, setShouldLoadTranscript] = useState(
822
+ !snapshot.rollout.shouldDeferTranscriptLoad && !transcriptMissing,
823
+ );
824
+ const showRawJson = search.raw === true;
825
+ const sortOrder: TranscriptSortOrder = search.sort ?? 'earliest';
826
+ const transcriptSearchInput = search.q ?? '';
827
+ const [activeSearchResultIndex, setActiveSearchResultIndex] = useState(0);
828
+ const [activeTranscriptEventKey, setActiveTranscriptEventKey] = useState<string | null>(null);
829
+ const [activeEventJumpSignal, setActiveEventJumpSignal] = useState(0);
830
+ const [exportOpen, setExportOpen] = useState(false);
831
+ const [deleteOpen, setDeleteOpen] = useState(false);
832
+ const transcriptPreviewQuery = useQuery({
833
+ ...threadTranscriptPreviewQueryOptions(params.threadId, transcriptFilters),
834
+ enabled: snapshot.rollout.shouldDeferTranscriptLoad && !shouldLoadTranscript && !transcriptMissing,
835
+ });
836
+ const transcriptQuery = useQuery({
837
+ ...threadTranscriptQueryOptions(params.threadId),
838
+ enabled: shouldLoadFullThreadTranscript({
839
+ shouldLoadTranscript,
840
+ snapshotTranscript: snapshot.transcript,
841
+ transcriptMissing,
842
+ }),
843
+ });
844
+ const transcript = transcriptQuery.data ?? transcriptPreviewQuery.data ?? snapshot.transcript ?? null;
845
+ const viewSnapshot = {
846
+ ...snapshot,
847
+ availableTools:
848
+ snapshot.availableTools.length > 0 ? snapshot.availableTools : (transcript?.sessionMeta.dynamicTools ?? []),
849
+ transcript,
850
+ };
851
+ const fullTranscriptLoaded = Boolean(transcriptQuery.data && !transcriptQuery.data.isPartial);
852
+ const transcriptSearchResults = useTranscriptSearchResults({
853
+ assistantModel: snapshot.thread.model,
854
+ filters: transcriptFilters,
855
+ projectPath: snapshot.thread.cwd,
856
+ query: transcriptSearchInput,
857
+ settings,
858
+ transcript,
859
+ });
860
+
861
+ useEffect(() => {
862
+ setActiveSearchResultIndex((current) =>
863
+ transcriptSearchResults.length === 0 ? 0 : Math.min(current, transcriptSearchResults.length - 1),
864
+ );
865
+ setActiveTranscriptEventKey((current) =>
866
+ current && transcriptSearchResults.some((result) => result.eventKey === current) ? current : null,
867
+ );
868
+ }, [transcriptSearchResults]);
869
+
870
+ const updateTranscriptSearchInput = (value: string) => {
871
+ setActiveSearchResultIndex(0);
872
+ setActiveTranscriptEventKey(null);
873
+ updateTranscriptSearch({ q: value });
874
+ };
875
+
876
+ const updateTranscriptFilter = (patch: Partial<ThreadTranscriptSearch>) => {
877
+ setActiveSearchResultIndex(0);
878
+ setActiveTranscriptEventKey(null);
879
+ updateTranscriptSearch(patch);
880
+ };
881
+
882
+ const updateSortOrder = (sort: TranscriptSortOrder) => {
883
+ updateTranscriptSearch({ sort });
884
+ };
885
+
886
+ const jumpToTranscriptSearchResult = (index: number) => {
887
+ if (transcriptSearchResults.length === 0) {
888
+ return;
889
+ }
890
+
891
+ const wrappedIndex =
892
+ ((index % transcriptSearchResults.length) + transcriptSearchResults.length) %
893
+ transcriptSearchResults.length;
894
+ const result = transcriptSearchResults[wrappedIndex]!;
895
+
896
+ setActiveSearchResultIndex(wrappedIndex);
897
+ setActiveTranscriptEventKey(result.eventKey);
898
+ setActiveEventJumpSignal((current) => current + 1);
899
+ };
900
+
901
+ const exportThreadMutation = useMutation({
902
+ mutationFn: async (options: {
903
+ includeCommentary: boolean;
904
+ includeTools: boolean;
905
+ includeMetadata: boolean;
906
+ outputFormat: 'md' | 'txt';
907
+ zipArchive: boolean;
908
+ }) => {
909
+ console.info('[spiracha:export-ui] request', {
910
+ outputFormat: options.outputFormat,
911
+ project: snapshot.project,
912
+ selectedThreadCount: 1,
913
+ selectedThreadIds: [snapshot.thread.id],
914
+ zipArchive: options.zipArchive,
915
+ });
916
+
917
+ const download = await exportThreadFn({
918
+ data: {
919
+ ...options,
920
+ ...settings,
921
+ threadId: snapshot.thread.id,
922
+ },
923
+ });
924
+
925
+ console.info('[spiracha:export-ui] response', {
926
+ downloadUrl: download.mode === 'download_url' ? download.downloadUrl : null,
927
+ fileName: download.fileName,
928
+ mode: download.mode,
929
+ project: snapshot.project,
930
+ selectedThreadCount: 1,
931
+ });
932
+
933
+ if (download.mode === 'download') {
934
+ downloadTextFile(download.fileName, download.content, download.mimeType);
935
+ return;
936
+ }
937
+
938
+ await downloadUrlFile(download.fileName, download.downloadUrl);
939
+ },
940
+ onError: (error) => {
941
+ console.error('[spiracha:export-ui] failed', {
942
+ error: error instanceof Error ? error.message : String(error),
943
+ project: snapshot.project,
944
+ selectedThreadCount: 1,
945
+ selectedThreadIds: [snapshot.thread.id],
946
+ });
947
+ },
948
+ onSuccess: async () => {
949
+ setExportOpen(false);
950
+ },
951
+ });
952
+
953
+ const deleteThreadMutation = useMutation({
954
+ mutationFn: (input: { deleteSessionFiles: boolean }) =>
955
+ deleteThreadFn({
956
+ data: {
957
+ deleteSessionFiles: input.deleteSessionFiles,
958
+ threadId: snapshot.thread.id,
959
+ },
960
+ }),
961
+ onSuccess: async () => {
962
+ await Promise.all([
963
+ queryClient.invalidateQueries({ queryKey: ['analytics'] }),
964
+ queryClient.invalidateQueries({ queryKey: ['dashboard'] }),
965
+ queryClient.invalidateQueries({ queryKey: ['project-threads', snapshot.project] }),
966
+ queryClient.invalidateQueries({ queryKey: ['projects'] }),
967
+ ]);
968
+
969
+ navigate({
970
+ params: {
971
+ project: snapshot.project,
972
+ },
973
+ to: '/codex/$project',
974
+ });
975
+ },
976
+ });
977
+
978
+ return (
979
+ <div className="space-y-5">
980
+ <PageHeader
981
+ actions={
982
+ <>
983
+ <Button className="rounded-full" variant="outline" onClick={() => setExportOpen(true)}>
984
+ <Download className="mr-2 size-4" />
985
+ Export
986
+ </Button>
987
+ <Button
988
+ className="rounded-full border-[var(--destructive)]/20 text-[var(--destructive)]"
989
+ variant="outline"
990
+ onClick={() => setDeleteOpen(true)}
991
+ >
992
+ <Trash2 className="mr-2 size-4" />
993
+ Delete
994
+ </Button>
995
+ </>
996
+ }
997
+ breadcrumb={
998
+ <Breadcrumbs
999
+ items={[
1000
+ { label: 'Codex', to: '/codex' },
1001
+ {
1002
+ label: snapshot.project,
1003
+ params: { project: snapshot.project },
1004
+ to: '/codex/$project',
1005
+ },
1006
+ { label: snapshot.thread.title, title: snapshot.thread.title, truncate: true },
1007
+ ]}
1008
+ />
1009
+ }
1010
+ />
1011
+
1012
+ <div className="grid gap-3 md:grid-cols-2 xl:grid-cols-4">
1013
+ <MetricCard label="Model" value={snapshot.thread.model ?? 'unknown'} />
1014
+ <MetricCard label="Tokens" value={formatTokens(snapshot.thread.tokens_used)} />
1015
+ <MetricCard
1016
+ label="Updated"
1017
+ value={
1018
+ <span suppressHydrationWarning>
1019
+ {formatDateTime(snapshot.thread.updated_at_ms ?? snapshot.thread.updated_at * 1000)}
1020
+ </span>
1021
+ }
1022
+ />
1023
+ <MetricCard
1024
+ label="Thread source"
1025
+ value={snapshot.thread.thread_source ?? transcript?.sessionMeta.threadSource ?? 'n/a'}
1026
+ />
1027
+ </div>
1028
+
1029
+ <Tabs className="space-y-4" defaultValue="transcript">
1030
+ <TabsList className="grid w-fit min-w-[24rem] grid-cols-3 rounded-full border border-[var(--border)] bg-[var(--panel)] p-1">
1031
+ <TabsTrigger className="rounded-full px-5 text-sm" value="transcript">
1032
+ Transcript
1033
+ </TabsTrigger>
1034
+ <TabsTrigger className="rounded-full px-5 text-sm" value="metadata">
1035
+ Metadata
1036
+ </TabsTrigger>
1037
+ <TabsTrigger className="rounded-full px-5 text-sm" value="raw">
1038
+ Raw
1039
+ </TabsTrigger>
1040
+ </TabsList>
1041
+
1042
+ <ThreadTranscriptTab
1043
+ activeEventJumpSignal={activeEventJumpSignal}
1044
+ activeSearchResultIndex={activeSearchResultIndex}
1045
+ activeTranscriptEventKey={activeTranscriptEventKey}
1046
+ fullTranscriptLoaded={fullTranscriptLoaded}
1047
+ loadError={
1048
+ transcriptQuery.isError
1049
+ ? transcriptQuery.error
1050
+ : transcriptPreviewQuery.isError
1051
+ ? transcriptPreviewQuery.error
1052
+ : null
1053
+ }
1054
+ loadingFullTranscript={transcriptQuery.isFetching}
1055
+ searchInput={transcriptSearchInput}
1056
+ searchResults={transcriptSearchResults}
1057
+ showRawJson={showRawJson}
1058
+ sortOrder={sortOrder}
1059
+ snapshot={snapshot}
1060
+ transcript={transcript}
1061
+ transcriptFilters={transcriptFilters}
1062
+ onJumpToSearchResult={jumpToTranscriptSearchResult}
1063
+ onLoadFullThread={() => setShouldLoadTranscript(true)}
1064
+ onSearchInputChange={updateTranscriptSearchInput}
1065
+ onSortOrderChange={updateSortOrder}
1066
+ onTranscriptFilterChange={updateTranscriptFilter}
1067
+ />
1068
+
1069
+ <TabsContent value="metadata">
1070
+ <ThreadMetadataPanels snapshot={viewSnapshot} />
1071
+ </TabsContent>
1072
+
1073
+ <TabsContent value="raw">
1074
+ <ThreadRawPanels snapshot={viewSnapshot} />
1075
+ </TabsContent>
1076
+ </Tabs>
1077
+
1078
+ <DeleteConfirmDialog
1079
+ confirmLabel={deleteThreadMutation.isPending ? 'Deleting...' : 'Delete thread'}
1080
+ defaultDeleteSessionFiles
1081
+ description="Delete this thread from the Codex database. Enable Delete Session files if you also want to remove the rollout JSONL from disk."
1082
+ open={deleteOpen}
1083
+ showDeleteSessionFilesOption
1084
+ title="Delete this thread from Codex DB?"
1085
+ onConfirm={({ deleteSessionFiles }) => deleteThreadMutation.mutate({ deleteSessionFiles })}
1086
+ onOpenChange={setDeleteOpen}
1087
+ />
1088
+
1089
+ <ExportDialog
1090
+ disabled={transcriptMissing}
1091
+ errorMessage={getThreadExportErrorMessage(transcriptMissing, exportThreadMutation.error)}
1092
+ open={exportOpen}
1093
+ pending={exportThreadMutation.isPending}
1094
+ onExport={(options) => {
1095
+ if (!transcriptMissing) {
1096
+ exportThreadMutation.mutate(options);
1097
+ }
1098
+ }}
1099
+ onOpenChange={setExportOpen}
1100
+ />
1101
+ </div>
1102
+ );
1103
+ }