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