spiracha 1.5.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 +114 -205
- package/README.md +89 -269
- package/apps/ui/AGENTS.md +17 -7
- package/apps/ui/README.md +34 -8
- package/apps/ui/components.json +21 -0
- package/apps/ui/package.json +65 -0
- 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 +46 -73
- 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/codex-analytics.ts +1 -1
- package/src/lib/codex-browser-db.ts +410 -74
- package/src/lib/codex-browser-export.ts +6 -14
- package/src/lib/codex-browser-types.ts +1 -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/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/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/apps/ui/dist/client/assets/_threadId-CAIeH5mq.js +0 -1
- package/apps/ui/dist/client/assets/analytics-DOy1b6Sh.js +0 -1
- package/apps/ui/dist/client/assets/antigravity-conversations._conversationId-DqySVO2K.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-B77qJz6f.js +0 -1
- package/apps/ui/dist/client/assets/antigravity._workspaceKey-B3NM6gRH.js +0 -1
- package/apps/ui/dist/client/assets/antigravity._workspaceKey-ByitGWki.js +0 -1
- package/apps/ui/dist/client/assets/antigravity.index-DP9JC2P9.js +0 -1
- package/apps/ui/dist/client/assets/antigravity.index-DgW25FEV.js +0 -1
- package/apps/ui/dist/client/assets/badge-C3i4OKs8.js +0 -1
- package/apps/ui/dist/client/assets/checkbox-CdHitXWQ.js +0 -1
- package/apps/ui/dist/client/assets/claude-code-sessions._sessionId-8pQ55ci2.js +0 -1
- package/apps/ui/dist/client/assets/claude-code-sessions._sessionId-DVz5UmtU.js +0 -1
- package/apps/ui/dist/client/assets/claude-code._workspaceKey-BBKskSum.js +0 -1
- package/apps/ui/dist/client/assets/claude-code._workspaceKey-OkikMUFs.js +0 -1
- package/apps/ui/dist/client/assets/claude-code.index-Cwu76dbb.js +0 -1
- package/apps/ui/dist/client/assets/claude-code.index-D6eMd08t.js +0 -1
- package/apps/ui/dist/client/assets/createServerFn-B-KkwmHU.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-hBWlP1D-.js +0 -1
- package/apps/ui/dist/client/assets/cursor._workspaceKey-BFPkIRi-.js +0 -1
- package/apps/ui/dist/client/assets/cursor._workspaceKey-CaAZjGmm.js +0 -1
- package/apps/ui/dist/client/assets/cursor.index-8y4o20kb.js +0 -2
- package/apps/ui/dist/client/assets/cursor.index-CrnMwDCi.js +0 -1
- package/apps/ui/dist/client/assets/data-table-Br78_dk3.js +0 -4
- package/apps/ui/dist/client/assets/delete-confirm-dialog-Bxs07u-j.js +0 -7
- package/apps/ui/dist/client/assets/dist-B9pyYCVU.js +0 -1
- package/apps/ui/dist/client/assets/dist-BrJhN2A0.js +0 -5
- package/apps/ui/dist/client/assets/dist-Cmd3AIfD.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-D5cvPaZt.js +0 -1
- package/apps/ui/dist/client/assets/es2015-ad6040dw.js +0 -41
- package/apps/ui/dist/client/assets/export-dialog-Dr8d2MqD.js +0 -1
- package/apps/ui/dist/client/assets/formatters-BtqyrX_c.js +0 -1
- package/apps/ui/dist/client/assets/index-C-STgIH4.js +0 -166
- package/apps/ui/dist/client/assets/jsx-runtime-DGeXAQPT.js +0 -1
- package/apps/ui/dist/client/assets/kiro-sessions._sessionId-CKgfJ7GR.js +0 -1
- package/apps/ui/dist/client/assets/kiro-sessions._sessionId-zvIJHtQF.js +0 -1
- package/apps/ui/dist/client/assets/kiro._workspaceKey-DhVuJuZB.js +0 -1
- package/apps/ui/dist/client/assets/kiro._workspaceKey-QArsjkTY.js +0 -1
- package/apps/ui/dist/client/assets/kiro.index-C-fr4m7i.js +0 -1
- package/apps/ui/dist/client/assets/kiro.index-OYgAdtZd.js +0 -1
- package/apps/ui/dist/client/assets/metric-card-Dplm0ZiM.js +0 -1
- package/apps/ui/dist/client/assets/opencode-sessions._sessionId-DLjiTst-.js +0 -1
- package/apps/ui/dist/client/assets/opencode-sessions._sessionId-biBwrRbt.js +0 -8
- package/apps/ui/dist/client/assets/opencode._workspaceKey-C7OIzMOJ.js +0 -1
- package/apps/ui/dist/client/assets/opencode._workspaceKey-sBakX0oI.js +0 -1
- package/apps/ui/dist/client/assets/opencode.index-Bl_kNs9N.js +0 -1
- package/apps/ui/dist/client/assets/opencode.index-CN53bVmb.js +0 -1
- package/apps/ui/dist/client/assets/page-header-Cwa3p6Tl.js +0 -1
- package/apps/ui/dist/client/assets/preload-helper-B8fFrdxC.js +0 -1
- package/apps/ui/dist/client/assets/projects._project-D8WloxQN.js +0 -1
- package/apps/ui/dist/client/assets/projects._project-DSDwH4NF.js +0 -1
- package/apps/ui/dist/client/assets/projects.index-5xRtuJdX.js +0 -3
- package/apps/ui/dist/client/assets/projects.index-DEfhWqa8.js +0 -1
- 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-DnzF_9CX.js +0 -1
- package/apps/ui/dist/client/assets/scroll-text-fM0sMOB3.js +0 -1
- package/apps/ui/dist/client/assets/select-CYF6pF37.js +0 -1
- package/apps/ui/dist/client/assets/settings-Dx70jgP_.js +0 -1
- package/apps/ui/dist/client/assets/sqlite-error-B6uZjUe8.js +0 -1
- package/apps/ui/dist/client/assets/styles-B7Zi9EFA.css +0 -1
- package/apps/ui/dist/client/assets/tabs-CmuhkT-c.js +0 -7
- package/apps/ui/dist/client/assets/text-filter-DVAZySUS.js +0 -2
- package/apps/ui/dist/client/assets/thread-transcript-stats-BUNz6u7F.js +0 -1
- package/apps/ui/dist/client/assets/threads._threadId-BB9tTsV4.js +0 -1
- package/apps/ui/dist/client/assets/threads._threadId-BMRSFow3.js +0 -1
- package/apps/ui/dist/client/assets/useMutation-BSFult2i.js +0 -1
- package/apps/ui/dist/client/assets/useQuery-CjINKZf1.js +0 -1
- package/apps/ui/dist/server/assets/_tanstack-start-manifest_v-CQ8Jjs3B.js +0 -387
- 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-BKOyL2T6.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-eS87Dx7g.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/claude-code-db-Ik3VStKZ.js +0 -361
- package/apps/ui/dist/server/assets/claude-code-queries-Cgzi4K-e.js +0 -37
- package/apps/ui/dist/server/assets/claude-code-server-PfraClXS.js +0 -72
- package/apps/ui/dist/server/assets/claude-code-sessions._sessionId-C55oe6zj.js +0 -18
- package/apps/ui/dist/server/assets/claude-code-sessions._sessionId-D1RYYi3j.js +0 -473
- package/apps/ui/dist/server/assets/claude-code-sessions._sessionId-DSvPLEUL.js +0 -11
- package/apps/ui/dist/server/assets/claude-code-transcript-C-bxh3cS.js +0 -139
- package/apps/ui/dist/server/assets/claude-code-transcript-phase-DmUMc8VU.js +0 -10
- package/apps/ui/dist/server/assets/claude-code._workspaceKey-B5Xg_-tE.js +0 -28
- package/apps/ui/dist/server/assets/claude-code._workspaceKey-BPlzyXPa.js +0 -11
- package/apps/ui/dist/server/assets/claude-code._workspaceKey-DqDve2wl.js +0 -191
- package/apps/ui/dist/server/assets/claude-code.index-B7yYR7dD.js +0 -92
- package/apps/ui/dist/server/assets/claude-code.index-BUHThG1W.js +0 -11
- package/apps/ui/dist/server/assets/codex-queries-CtgeZ7VL.js +0 -99
- package/apps/ui/dist/server/assets/codex-server-CGXU4H8-.js +0 -2294
- 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-CZnYy7aT.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-CXpO8KcD.js +0 -361
- package/apps/ui/dist/server/assets/cursor-server-DNu52A3K.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-Ckn6ItHI.js +0 -582
- package/apps/ui/dist/server/assets/cursor-threads._composerId-p9-ARAzc.js +0 -18
- package/apps/ui/dist/server/assets/cursor-transcript-B7tGnqtn.js +0 -125
- package/apps/ui/dist/server/assets/cursor._workspaceKey-CZuohIUL.js +0 -28
- package/apps/ui/dist/server/assets/cursor._workspaceKey-DPp5JSjy.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-D88ze9Gy.js +0 -240
- package/apps/ui/dist/server/assets/formatters-FJaGZgJk.js +0 -91
- package/apps/ui/dist/server/assets/kiro-db--fXrED_-.js +0 -552
- package/apps/ui/dist/server/assets/kiro-queries-CzNlM2ok.js +0 -37
- package/apps/ui/dist/server/assets/kiro-server-9WRxg2Au.js +0 -72
- package/apps/ui/dist/server/assets/kiro-sessions._sessionId-CRY2c7YO.js +0 -11
- package/apps/ui/dist/server/assets/kiro-sessions._sessionId-F1UlwxmR.js +0 -449
- package/apps/ui/dist/server/assets/kiro-sessions._sessionId-bvTulT7C.js +0 -18
- package/apps/ui/dist/server/assets/kiro-transcript-DoekQBaH.js +0 -112
- package/apps/ui/dist/server/assets/kiro-transcript-phase-CJG40nQg.js +0 -27
- package/apps/ui/dist/server/assets/kiro._workspaceKey-BDe9sQwo.js +0 -11
- package/apps/ui/dist/server/assets/kiro._workspaceKey-CJsjgNwO.js +0 -28
- package/apps/ui/dist/server/assets/kiro._workspaceKey-D_-4Qb7y.js +0 -193
- package/apps/ui/dist/server/assets/kiro.index-C__-UROk.js +0 -11
- package/apps/ui/dist/server/assets/kiro.index-CmTbqeYj.js +0 -93
- 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/opencode-db-BM7KjOzc.js +0 -397
- package/apps/ui/dist/server/assets/opencode-queries-BeNYXnQv.js +0 -50
- package/apps/ui/dist/server/assets/opencode-server-BpQ_kRze.js +0 -72
- package/apps/ui/dist/server/assets/opencode-sessions._sessionId-CGQ-t0vD.js +0 -518
- package/apps/ui/dist/server/assets/opencode-sessions._sessionId-D8Q7in_Z.js +0 -18
- package/apps/ui/dist/server/assets/opencode-sessions._sessionId-DKOvi0lV.js +0 -11
- package/apps/ui/dist/server/assets/opencode-think-tags-CAoD-EcZ.js +0 -87
- package/apps/ui/dist/server/assets/opencode-transcript-BNTcGNym.js +0 -141
- package/apps/ui/dist/server/assets/opencode-transcript-phase-OqLkiQD0.js +0 -32
- package/apps/ui/dist/server/assets/opencode._workspaceKey-BdqcWHw1.js +0 -11
- package/apps/ui/dist/server/assets/opencode._workspaceKey-CfH96HTT.js +0 -28
- package/apps/ui/dist/server/assets/opencode._workspaceKey-DdbHPDrO.js +0 -207
- package/apps/ui/dist/server/assets/opencode.index-BbVzkJXP.js +0 -11
- package/apps/ui/dist/server/assets/opencode.index-Vh_Gq_1L.js +0 -93
- 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-4io5LO0E.js +0 -20
- package/apps/ui/dist/server/assets/projects._project-Bshqk7JA.js +0 -12
- package/apps/ui/dist/server/assets/projects._project-D6ZoGhJ8.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-Sac2DGk6.js +0 -518
- 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-DyhChtHf.js +0 -137
- package/apps/ui/dist/server/assets/source-session-export-server-DNutCawb.js +0 -38
- package/apps/ui/dist/server/assets/sqlite-error-LZDrnxdd.js +0 -13
- package/apps/ui/dist/server/assets/start-B_HFwkkk.js +0 -4
- package/apps/ui/dist/server/assets/tabs-DkaChNVg.js +0 -501
- package/apps/ui/dist/server/assets/text-filter-CGKxMCKt.js +0 -36
- package/apps/ui/dist/server/assets/thread-transcript-stats-Dh34mt2u.js +0 -45
- package/apps/ui/dist/server/assets/threads._threadId-BSSK4nkI.js +0 -26
- package/apps/ui/dist/server/assets/threads._threadId-BeXEc4Lo.js +0 -648
- package/apps/ui/dist/server/assets/threads._threadId-Ci0S4mKU.js +0 -18
- 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 -5895
- 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}/icon.svg +0 -0
- /package/apps/ui/{dist/client → public}/manifest.json +0 -0
- /package/apps/ui/{dist/client → public}/robots.txt +0 -0
|
@@ -0,0 +1,1601 @@
|
|
|
1
|
+
import { Database } from 'bun:sqlite';
|
|
2
|
+
import { readdir, stat } from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { mapWithConcurrency } from './concurrency';
|
|
5
|
+
import { loadQoderAcpSession, type QoderAcpSessionUpdate, resolveQoderAcpSocketPath } from './qoder-acp-client';
|
|
6
|
+
import {
|
|
7
|
+
getDefaultQoderUserDir,
|
|
8
|
+
type QoderSessionSummary,
|
|
9
|
+
type QoderSessionTranscript,
|
|
10
|
+
type QoderTranscriptEntry,
|
|
11
|
+
type QoderTranscriptPart,
|
|
12
|
+
type QoderWorkspaceGroup,
|
|
13
|
+
resolveQoderCliProjectsDir,
|
|
14
|
+
resolveQoderGlobalStateDb,
|
|
15
|
+
resolveQoderWorkspaceStorageDir,
|
|
16
|
+
} from './qoder-exporter-types';
|
|
17
|
+
import {
|
|
18
|
+
asObject,
|
|
19
|
+
asString,
|
|
20
|
+
cleanExtractedText,
|
|
21
|
+
cleanInlineTitle,
|
|
22
|
+
getPortablePathBasename,
|
|
23
|
+
isWorkspacePathQuery,
|
|
24
|
+
type JsonValue,
|
|
25
|
+
workspacePathMatchesQuery,
|
|
26
|
+
} from './shared';
|
|
27
|
+
|
|
28
|
+
export {
|
|
29
|
+
getDefaultQoderUserDir,
|
|
30
|
+
resolveQoderCliProjectsDir,
|
|
31
|
+
resolveQoderGlobalStateDb,
|
|
32
|
+
resolveQoderWorkspaceStorageDir,
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const READ_CONCURRENCY = 8;
|
|
36
|
+
const WORKSPACE_KEY_PREFIX = 'workspace:';
|
|
37
|
+
const LOCAL_HISTORY_KEY_PATTERN = /^lingma\.chat\.localHistory\.(.+)\.quest$/u;
|
|
38
|
+
const MODEL_CONFIG_KEYS = ['aicoding.modelConfigs.cache.assistant', 'aicoding.modelConfigs.cache.quest'] as const;
|
|
39
|
+
const ACP_RECENT_SESSION_WINDOW_MS = 24 * 60 * 60 * 1000;
|
|
40
|
+
const QODER_MODEL_LABELS: Record<string, string> = {
|
|
41
|
+
dfmodel: 'DeepSeek V4 Flash',
|
|
42
|
+
dmodel: 'DeepSeek V4 Pro',
|
|
43
|
+
gm51model: 'GLM 5.2',
|
|
44
|
+
gmodel: 'GLM 5',
|
|
45
|
+
kmodel: 'Kimi K2.7 Code',
|
|
46
|
+
mmodel: 'MiniMax M3',
|
|
47
|
+
q35model: 'Qwen 3.5 Plus',
|
|
48
|
+
q35model_preview: 'Qwen 3.7 Max DogFooding',
|
|
49
|
+
qmodel: 'Qwen 3.7 Plus',
|
|
50
|
+
qmodel_latest: 'Qwen 3.7 Max',
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
type ItemTableRow = {
|
|
54
|
+
key: string;
|
|
55
|
+
value: string;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
type QoderHistoryEntry = {
|
|
59
|
+
historyKey: string;
|
|
60
|
+
id: string;
|
|
61
|
+
raw: Record<string, JsonValue>;
|
|
62
|
+
sessionId: string;
|
|
63
|
+
timestampMs: number | null;
|
|
64
|
+
text: string;
|
|
65
|
+
title: string;
|
|
66
|
+
workspaceStorageId: string;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
type QoderTaskEntry = {
|
|
70
|
+
agentClass: string | null;
|
|
71
|
+
createdAtMs: number | null;
|
|
72
|
+
executionMode: string | null;
|
|
73
|
+
executionRequestId: string | null;
|
|
74
|
+
id: string;
|
|
75
|
+
model: string | null;
|
|
76
|
+
query: string | null;
|
|
77
|
+
raw: Record<string, JsonValue>;
|
|
78
|
+
sessionIds: string[];
|
|
79
|
+
status: string | null;
|
|
80
|
+
title: string | null;
|
|
81
|
+
updatedAtMs: number | null;
|
|
82
|
+
workspacePath: string;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
type QoderSessionRecord = {
|
|
86
|
+
histories: QoderHistoryEntry[];
|
|
87
|
+
sessionId: string;
|
|
88
|
+
task: QoderTaskEntry | null;
|
|
89
|
+
worktree: string;
|
|
90
|
+
workspaceStorageId: string | null;
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
type QoderStateData = {
|
|
94
|
+
fileOperationCount: number;
|
|
95
|
+
lastActiveAtMs: number | null;
|
|
96
|
+
rawState: Record<string, JsonValue> | null;
|
|
97
|
+
requestId: string | null;
|
|
98
|
+
snapshotFileCount: number;
|
|
99
|
+
statePath: string | null;
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
type SessionStats = {
|
|
103
|
+
assistantMessageCount: number;
|
|
104
|
+
fileOperationCount: number;
|
|
105
|
+
messageCount: number;
|
|
106
|
+
renderablePartCount: number;
|
|
107
|
+
snapshotFileCount: number;
|
|
108
|
+
userMessageCount: number;
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
type QoderModelConfigState = {
|
|
112
|
+
assistantDefaultModel: string | null;
|
|
113
|
+
questDefaultModel: string | null;
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
type QoderDataRecords = {
|
|
117
|
+
modelConfig: QoderModelConfigState;
|
|
118
|
+
records: QoderSessionRecord[];
|
|
119
|
+
workspaceStorageIds: string[];
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
type QoderTranscriptReadOptions = {
|
|
123
|
+
acpDrainMs?: number;
|
|
124
|
+
acpSocketPath?: string | null;
|
|
125
|
+
acpTimeoutMs?: number;
|
|
126
|
+
enableAcp?: boolean;
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const pathExists = async (target: string): Promise<boolean> => {
|
|
130
|
+
return await stat(target)
|
|
131
|
+
.then(() => true)
|
|
132
|
+
.catch(() => false);
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
const toIso = (value: number | null): string | null => {
|
|
136
|
+
return value === null ? null : new Date(value).toISOString();
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
const parseTimestampMs = (value: JsonValue | undefined): number | null => {
|
|
140
|
+
if (typeof value === 'number' && Number.isFinite(value)) {
|
|
141
|
+
// Qoder stores some task timestamps in Unix seconds and others in epoch milliseconds.
|
|
142
|
+
return value > 0 && value < 10_000_000_000 ? value * 1000 : value;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (typeof value === 'string') {
|
|
146
|
+
const numeric = Number(value);
|
|
147
|
+
if (Number.isFinite(numeric)) {
|
|
148
|
+
// Match the numeric branch so seconds-like string values sort correctly.
|
|
149
|
+
return numeric > 0 && numeric < 10_000_000_000 ? numeric * 1000 : numeric;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const parsed = Date.parse(value);
|
|
153
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return null;
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
const cleanLabel = (value: string | null | undefined): string | null => {
|
|
160
|
+
const cleaned = value?.replace(/\s+/g, ' ').trim();
|
|
161
|
+
return cleaned ? cleaned : null;
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
const normalizePromptText = (value: string | null | undefined): string | null => {
|
|
165
|
+
const cleaned = value
|
|
166
|
+
?.replace(/\r\n?/gu, '\n')
|
|
167
|
+
.replace(/\\r\\n|\\n/gu, '\n')
|
|
168
|
+
.trimEnd();
|
|
169
|
+
return cleaned ? cleaned : null;
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
const getWorkspaceKey = (worktree: string): string => {
|
|
173
|
+
return `${WORKSPACE_KEY_PREFIX}${Buffer.from(worktree, 'utf8').toString('base64url')}`;
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
const warnQoder = (event: string, details: Record<string, JsonValue>) => {
|
|
177
|
+
console.warn(`[spiracha:qoder] ${event}`, details);
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
const getWorktreeFromWorkspaceKey = (workspaceKey: string): string | null => {
|
|
181
|
+
if (!workspaceKey.startsWith(WORKSPACE_KEY_PREFIX)) {
|
|
182
|
+
warnQoder('invalid_workspace_key', { workspaceKey });
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
try {
|
|
187
|
+
const decoded = Buffer.from(workspaceKey.slice(WORKSPACE_KEY_PREFIX.length), 'base64url').toString('utf8');
|
|
188
|
+
if (!decoded || decoded.includes('\uFFFD')) {
|
|
189
|
+
warnQoder('invalid_workspace_key', { workspaceKey });
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return decoded;
|
|
194
|
+
} catch {
|
|
195
|
+
warnQoder('invalid_workspace_key', { workspaceKey });
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
const getWorkspaceLabel = (worktree: string): string => {
|
|
201
|
+
return getPortablePathBasename(worktree) || worktree;
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
const getWorkspaceUri = (worktree: string): string => {
|
|
205
|
+
return worktree.startsWith(path.sep) ? `file://${worktree}` : worktree;
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
const parseJsonValue = (value: string): JsonValue | null => {
|
|
209
|
+
try {
|
|
210
|
+
return JSON.parse(value) as JsonValue;
|
|
211
|
+
} catch {
|
|
212
|
+
return null;
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
const readGlobalRows = async (globalStateDb = resolveQoderGlobalStateDb()): Promise<ItemTableRow[]> => {
|
|
217
|
+
if (!(await pathExists(globalStateDb))) {
|
|
218
|
+
return [];
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
let db: Database | null = null;
|
|
222
|
+
try {
|
|
223
|
+
db = new Database(globalStateDb, { readonly: true, strict: true });
|
|
224
|
+
return db
|
|
225
|
+
.query(
|
|
226
|
+
"select key, value from ItemTable where key like 'lingma.chat.localHistory.%.quest' or key = 'aicoding.questTaskListSnapshot' or key in ('aicoding.modelConfigs.cache.assistant', 'aicoding.modelConfigs.cache.quest')",
|
|
227
|
+
)
|
|
228
|
+
.all() as ItemTableRow[];
|
|
229
|
+
} catch {
|
|
230
|
+
return [];
|
|
231
|
+
} finally {
|
|
232
|
+
db?.close();
|
|
233
|
+
}
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
const asJsonObject = (value: JsonValue | null): Record<string, JsonValue> | null => {
|
|
237
|
+
return value === null ? null : asObject(value);
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
const parseHistoryRows = (rows: ItemTableRow[]): QoderHistoryEntry[] => {
|
|
241
|
+
const histories: QoderHistoryEntry[] = [];
|
|
242
|
+
|
|
243
|
+
for (const row of rows) {
|
|
244
|
+
const match = LOCAL_HISTORY_KEY_PATTERN.exec(row.key);
|
|
245
|
+
if (!match) {
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const workspaceStorageId = match[1] ?? '';
|
|
250
|
+
const value = parseJsonValue(row.value);
|
|
251
|
+
const items = Array.isArray(value) ? value : [];
|
|
252
|
+
|
|
253
|
+
items.forEach((item, index) => {
|
|
254
|
+
const raw = asJsonObject(item);
|
|
255
|
+
const sessionId = asString(raw?.sessionId ?? null);
|
|
256
|
+
if (!raw || !sessionId) {
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const id = asString(raw.id ?? null) ?? `${workspaceStorageId}:${index}`;
|
|
261
|
+
const rawTitle = asString(raw.title ?? null);
|
|
262
|
+
const title = cleanLabel(rawTitle) ?? sessionId;
|
|
263
|
+
histories.push({
|
|
264
|
+
historyKey: row.key,
|
|
265
|
+
id,
|
|
266
|
+
raw,
|
|
267
|
+
sessionId,
|
|
268
|
+
text: normalizePromptText(rawTitle) ?? title,
|
|
269
|
+
timestampMs: parseTimestampMs(raw.timestamp),
|
|
270
|
+
title,
|
|
271
|
+
workspaceStorageId,
|
|
272
|
+
});
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return histories;
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
const decodeFileUri = (value: string): string | null => {
|
|
280
|
+
if (!value.startsWith('file://')) {
|
|
281
|
+
return null;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
try {
|
|
285
|
+
return decodeURIComponent(new URL(value).pathname);
|
|
286
|
+
} catch {
|
|
287
|
+
return null;
|
|
288
|
+
}
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
const getStringValue = (raw: Record<string, JsonValue>, keys: string[]): string | null => {
|
|
292
|
+
for (const key of keys) {
|
|
293
|
+
const value = cleanLabel(asString(raw[key] ?? null));
|
|
294
|
+
if (value) {
|
|
295
|
+
return value;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
return null;
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
const parseJsonObjectString = (value: JsonValue | undefined): Record<string, JsonValue> | null => {
|
|
303
|
+
const text = asString(value ?? null);
|
|
304
|
+
return text ? asJsonObject(parseJsonValue(text)) : null;
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
const normalizeQoderModelLabel = (value: string | null): string | null => {
|
|
308
|
+
return value ? (QODER_MODEL_LABELS[value] ?? value) : null;
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
const getTaskModel = (raw: Record<string, JsonValue>): string | null => {
|
|
312
|
+
const directModel = getStringValue(raw, [
|
|
313
|
+
'model',
|
|
314
|
+
'modelLabel',
|
|
315
|
+
'modelName',
|
|
316
|
+
'modelId',
|
|
317
|
+
'modelVersion',
|
|
318
|
+
'selectedModel',
|
|
319
|
+
]);
|
|
320
|
+
if (directModel) {
|
|
321
|
+
return normalizeQoderModelLabel(directModel);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const executionSessionExtra = parseJsonObjectString(raw.executionSessionExtra);
|
|
325
|
+
const questTaskInfo = asObject(executionSessionExtra?.questTaskInfo ?? null);
|
|
326
|
+
const executionConfig = asObject(questTaskInfo?.executionConfig ?? null);
|
|
327
|
+
return normalizeQoderModelLabel(
|
|
328
|
+
getStringValue(questTaskInfo ?? {}, ['model', 'modelLabel', 'modelName', 'modelId', 'selectedModel']) ??
|
|
329
|
+
getStringValue(executionConfig ?? {}, ['model', 'modelLabel', 'modelName', 'modelId', 'selectedModel']),
|
|
330
|
+
);
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
const getTimestampValue = (raw: Record<string, JsonValue>, keys: string[]): number | null => {
|
|
334
|
+
for (const key of keys) {
|
|
335
|
+
const value = parseTimestampMs(raw[key]);
|
|
336
|
+
if (value !== null) {
|
|
337
|
+
return value;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
return null;
|
|
342
|
+
};
|
|
343
|
+
|
|
344
|
+
const getWorkspacePathFromTask = (folder: string, task: Record<string, JsonValue>): string => {
|
|
345
|
+
const workspaceUri = asString(task.workspaceUri ?? null);
|
|
346
|
+
const decodedUri = workspaceUri ? decodeFileUri(workspaceUri) : null;
|
|
347
|
+
return decodedUri ?? folder;
|
|
348
|
+
};
|
|
349
|
+
|
|
350
|
+
const getTaskSessionIds = (raw: Record<string, JsonValue>, id: string): string[] => {
|
|
351
|
+
const executionSessionId = asString(raw.executionSessionId ?? null);
|
|
352
|
+
const designSessionId = asString(raw.designSessionId ?? null);
|
|
353
|
+
return [
|
|
354
|
+
...new Set([executionSessionId, `${id}.session.execution`, designSessionId, id].filter(Boolean)),
|
|
355
|
+
] as string[];
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
const parseTaskItem = (folder: string, item: JsonValue): QoderTaskEntry | null => {
|
|
359
|
+
const raw = asJsonObject(item);
|
|
360
|
+
const id = asString(raw?.id ?? null);
|
|
361
|
+
if (!raw || !id) {
|
|
362
|
+
return null;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
return {
|
|
366
|
+
agentClass: getStringValue(raw, ['agentClass']),
|
|
367
|
+
createdAtMs: getTimestampValue(raw, ['createdAt', 'createTime']),
|
|
368
|
+
executionMode: getStringValue(raw, ['executionMode', 'questType']),
|
|
369
|
+
executionRequestId: getStringValue(raw, ['executionRequestId', 'designRequestId']),
|
|
370
|
+
id,
|
|
371
|
+
model: getTaskModel(raw),
|
|
372
|
+
query: getStringValue(raw, ['query', 'userRequirements']),
|
|
373
|
+
raw,
|
|
374
|
+
sessionIds: getTaskSessionIds(raw, id),
|
|
375
|
+
status: getStringValue(raw, ['status', 'bootStatus', 'prevStatus']),
|
|
376
|
+
title: getStringValue(raw, ['title', 'name', 'query']),
|
|
377
|
+
updatedAtMs: getTimestampValue(raw, [
|
|
378
|
+
'updatedAtTimestamp',
|
|
379
|
+
'executeEndAt',
|
|
380
|
+
'executeEndTime',
|
|
381
|
+
'finishedAt',
|
|
382
|
+
'endTime',
|
|
383
|
+
'lastUserQueryAt',
|
|
384
|
+
'createdAt',
|
|
385
|
+
'createTime',
|
|
386
|
+
]),
|
|
387
|
+
workspacePath: getWorkspacePathFromTask(folder, raw),
|
|
388
|
+
};
|
|
389
|
+
};
|
|
390
|
+
|
|
391
|
+
const parseFolderTasks = (folder: string, value: JsonValue): QoderTaskEntry[] => {
|
|
392
|
+
const folderEntry = asObject(value);
|
|
393
|
+
const folderTasks = Array.isArray(folderEntry?.tasks) ? folderEntry.tasks : [];
|
|
394
|
+
return folderTasks.flatMap((item) => {
|
|
395
|
+
const task = parseTaskItem(folder, item);
|
|
396
|
+
return task ? [task] : [];
|
|
397
|
+
});
|
|
398
|
+
};
|
|
399
|
+
|
|
400
|
+
const parseTaskSnapshotRows = (rows: ItemTableRow[]): QoderTaskEntry[] => {
|
|
401
|
+
const snapshotRow = rows.find((row) => row.key === 'aicoding.questTaskListSnapshot');
|
|
402
|
+
const snapshot = asJsonObject(snapshotRow ? parseJsonValue(snapshotRow.value) : null);
|
|
403
|
+
const folders = asObject(snapshot?.folders ?? null);
|
|
404
|
+
if (!folders) {
|
|
405
|
+
return [];
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
return Object.entries(folders).flatMap(([folder, value]) => parseFolderTasks(folder, value));
|
|
409
|
+
};
|
|
410
|
+
|
|
411
|
+
const getConfiguredModelName = (value: JsonValue): string | null => {
|
|
412
|
+
const config = asObject(value);
|
|
413
|
+
return getStringValue(config ?? {}, ['key', 'name', 'model', 'modelId']);
|
|
414
|
+
};
|
|
415
|
+
|
|
416
|
+
const parseDefaultModelConfig = (rows: ItemTableRow[], key: (typeof MODEL_CONFIG_KEYS)[number]): string | null => {
|
|
417
|
+
const row = rows.find((item) => item.key === key);
|
|
418
|
+
const configs = row ? parseJsonValue(row.value) : null;
|
|
419
|
+
if (!Array.isArray(configs)) {
|
|
420
|
+
return null;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
const selected =
|
|
424
|
+
configs.find((item) => {
|
|
425
|
+
const config = asObject(item);
|
|
426
|
+
return config?.selected === true;
|
|
427
|
+
}) ??
|
|
428
|
+
configs.find((item) => {
|
|
429
|
+
const config = asObject(item);
|
|
430
|
+
return config?.enabled === true && config?.isDefault === true;
|
|
431
|
+
}) ??
|
|
432
|
+
configs.find((item) => {
|
|
433
|
+
const config = asObject(item);
|
|
434
|
+
return config?.isDefault === true;
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
return normalizeQoderModelLabel(getConfiguredModelName(selected ?? null));
|
|
438
|
+
};
|
|
439
|
+
|
|
440
|
+
const parseModelConfigState = (rows: ItemTableRow[]): QoderModelConfigState => ({
|
|
441
|
+
assistantDefaultModel: parseDefaultModelConfig(rows, 'aicoding.modelConfigs.cache.assistant'),
|
|
442
|
+
questDefaultModel: parseDefaultModelConfig(rows, 'aicoding.modelConfigs.cache.quest'),
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
const getModelFallback = (modelConfig: QoderModelConfigState): string | null => {
|
|
446
|
+
return modelConfig.questDefaultModel ?? modelConfig.assistantDefaultModel;
|
|
447
|
+
};
|
|
448
|
+
|
|
449
|
+
const stripTrailingPathPunctuation = (value: string): string => {
|
|
450
|
+
return value.replace(/[),.;\]]+$/u, '');
|
|
451
|
+
};
|
|
452
|
+
|
|
453
|
+
const inferWorkspaceFromAbsolutePath = (value: string): string | null => {
|
|
454
|
+
const normalized = stripTrailingPathPunctuation(value);
|
|
455
|
+
const marker = `${path.sep}workspace${path.sep}`;
|
|
456
|
+
const markerIndex = normalized.indexOf(marker);
|
|
457
|
+
if (markerIndex >= 0) {
|
|
458
|
+
const prefixEnd = markerIndex + marker.length;
|
|
459
|
+
const nextSeparator = normalized.indexOf(path.sep, prefixEnd);
|
|
460
|
+
return nextSeparator >= 0 ? normalized.slice(0, nextSeparator) : normalized;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
return path.dirname(normalized);
|
|
464
|
+
};
|
|
465
|
+
|
|
466
|
+
const inferWorkspaceFromText = (value: string | null): string | null => {
|
|
467
|
+
if (!value) {
|
|
468
|
+
return null;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
const matches = value.match(/\/[^\s"'`)\]]+/gu) ?? [];
|
|
472
|
+
for (const match of matches) {
|
|
473
|
+
const workspace = inferWorkspaceFromAbsolutePath(match);
|
|
474
|
+
if (workspace) {
|
|
475
|
+
return workspace;
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
return null;
|
|
480
|
+
};
|
|
481
|
+
|
|
482
|
+
const findTaskForSession = (
|
|
483
|
+
tasksBySessionId: Map<string, QoderTaskEntry>,
|
|
484
|
+
sessionId: string,
|
|
485
|
+
): QoderTaskEntry | null => {
|
|
486
|
+
if (tasksBySessionId.has(sessionId)) {
|
|
487
|
+
return tasksBySessionId.get(sessionId) ?? null;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
if (sessionId.endsWith('.session.execution')) {
|
|
491
|
+
return tasksBySessionId.get(sessionId.replace(/\.session\.execution$/u, '')) ?? null;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
return tasksBySessionId.get(`${sessionId}.session.execution`) ?? null;
|
|
495
|
+
};
|
|
496
|
+
|
|
497
|
+
const buildTasksBySessionId = (tasks: QoderTaskEntry[]): Map<string, QoderTaskEntry> => {
|
|
498
|
+
const tasksBySessionId = new Map<string, QoderTaskEntry>();
|
|
499
|
+
for (const task of tasks) {
|
|
500
|
+
for (const sessionId of task.sessionIds) {
|
|
501
|
+
tasksBySessionId.set(sessionId, task);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
return tasksBySessionId;
|
|
505
|
+
};
|
|
506
|
+
|
|
507
|
+
const groupHistoriesBySessionId = (histories: QoderHistoryEntry[]): Map<string, QoderHistoryEntry[]> => {
|
|
508
|
+
const historiesBySessionId = new Map<string, QoderHistoryEntry[]>();
|
|
509
|
+
for (const history of histories) {
|
|
510
|
+
const sessionHistories = historiesBySessionId.get(history.sessionId) ?? [];
|
|
511
|
+
sessionHistories.push(history);
|
|
512
|
+
historiesBySessionId.set(history.sessionId, sessionHistories);
|
|
513
|
+
}
|
|
514
|
+
return historiesBySessionId;
|
|
515
|
+
};
|
|
516
|
+
|
|
517
|
+
const sortHistories = (histories: QoderHistoryEntry[]): QoderHistoryEntry[] => {
|
|
518
|
+
return [...histories].sort(
|
|
519
|
+
(left, right) => (left.timestampMs ?? 0) - (right.timestampMs ?? 0) || left.id.localeCompare(right.id),
|
|
520
|
+
);
|
|
521
|
+
};
|
|
522
|
+
|
|
523
|
+
const createRecordFromHistories = (
|
|
524
|
+
sessionId: string,
|
|
525
|
+
sessionHistories: QoderHistoryEntry[],
|
|
526
|
+
task: QoderTaskEntry | null,
|
|
527
|
+
): QoderSessionRecord => {
|
|
528
|
+
const histories = sortHistories(sessionHistories);
|
|
529
|
+
const inferredWorktree =
|
|
530
|
+
task?.workspacePath ?? inferWorkspaceFromText(histories.map((history) => history.text).join('\n'));
|
|
531
|
+
const workspaceStorageId = histories[0]?.workspaceStorageId ?? null;
|
|
532
|
+
|
|
533
|
+
return {
|
|
534
|
+
histories,
|
|
535
|
+
sessionId,
|
|
536
|
+
task,
|
|
537
|
+
workspaceStorageId,
|
|
538
|
+
worktree: inferredWorktree ?? `Qoder workspace ${workspaceStorageId ?? 'unknown'}`,
|
|
539
|
+
};
|
|
540
|
+
};
|
|
541
|
+
|
|
542
|
+
const createTaskOnlyRecords = (tasks: QoderTaskEntry[], usedTaskIds: Set<string>): QoderSessionRecord[] => {
|
|
543
|
+
return tasks.flatMap((task) => {
|
|
544
|
+
if (usedTaskIds.has(task.id)) {
|
|
545
|
+
return [];
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
return [
|
|
549
|
+
{
|
|
550
|
+
histories: [],
|
|
551
|
+
sessionId: task.sessionIds[0] ?? task.id,
|
|
552
|
+
task,
|
|
553
|
+
workspaceStorageId: null,
|
|
554
|
+
worktree: task.workspacePath,
|
|
555
|
+
},
|
|
556
|
+
];
|
|
557
|
+
});
|
|
558
|
+
};
|
|
559
|
+
|
|
560
|
+
const groupRecords = (histories: QoderHistoryEntry[], tasks: QoderTaskEntry[]): QoderSessionRecord[] => {
|
|
561
|
+
const tasksBySessionId = buildTasksBySessionId(tasks);
|
|
562
|
+
const usedTaskIds = new Set<string>();
|
|
563
|
+
const records = [...groupHistoriesBySessionId(histories).entries()].map(([sessionId, sessionHistories]) => {
|
|
564
|
+
const task = findTaskForSession(tasksBySessionId, sessionId);
|
|
565
|
+
if (task) {
|
|
566
|
+
usedTaskIds.add(task.id);
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
return createRecordFromHistories(sessionId, sessionHistories, task);
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
return [...records, ...createTaskOnlyRecords(tasks, usedTaskIds)];
|
|
573
|
+
};
|
|
574
|
+
|
|
575
|
+
const listWorkspaceStorageIds = async (workspaceStorageDir: string): Promise<string[]> => {
|
|
576
|
+
const entries = await readdir(workspaceStorageDir, { withFileTypes: true }).catch(() => []);
|
|
577
|
+
return entries
|
|
578
|
+
.filter((entry) => entry.isDirectory())
|
|
579
|
+
.map((entry) => entry.name)
|
|
580
|
+
.sort();
|
|
581
|
+
};
|
|
582
|
+
|
|
583
|
+
const getStateDirectoryCandidates = (sessionId: string): string[] => {
|
|
584
|
+
const candidates = [sessionId];
|
|
585
|
+
if (sessionId.endsWith('.session.execution')) {
|
|
586
|
+
candidates.push(sessionId.replace(/\.session\.execution$/u, ''));
|
|
587
|
+
} else {
|
|
588
|
+
candidates.push(`${sessionId}.session.execution`);
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
return [...new Set(candidates)];
|
|
592
|
+
};
|
|
593
|
+
|
|
594
|
+
const locateStatePath = async (
|
|
595
|
+
workspaceStorageDir: string,
|
|
596
|
+
workspaceStorageIds: string[],
|
|
597
|
+
record: QoderSessionRecord,
|
|
598
|
+
): Promise<{ statePath: string | null; workspaceStorageId: string | null }> => {
|
|
599
|
+
const storageIds = record.workspaceStorageId
|
|
600
|
+
? [record.workspaceStorageId, ...workspaceStorageIds.filter((id) => id !== record.workspaceStorageId)]
|
|
601
|
+
: workspaceStorageIds;
|
|
602
|
+
|
|
603
|
+
for (const workspaceStorageId of storageIds) {
|
|
604
|
+
for (const directoryName of getStateDirectoryCandidates(record.sessionId)) {
|
|
605
|
+
const statePath = path.join(
|
|
606
|
+
workspaceStorageDir,
|
|
607
|
+
workspaceStorageId,
|
|
608
|
+
'chatEditingSessions',
|
|
609
|
+
directoryName,
|
|
610
|
+
'state.json',
|
|
611
|
+
);
|
|
612
|
+
if (await pathExists(statePath)) {
|
|
613
|
+
return { statePath, workspaceStorageId };
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
return { statePath: null, workspaceStorageId: record.workspaceStorageId };
|
|
619
|
+
};
|
|
620
|
+
|
|
621
|
+
const readJsonObject = async (filePath: string): Promise<Record<string, JsonValue> | null> => {
|
|
622
|
+
const value = (await Bun.file(filePath)
|
|
623
|
+
.json()
|
|
624
|
+
.catch(() => null)) as JsonValue | null;
|
|
625
|
+
return asJsonObject(value);
|
|
626
|
+
};
|
|
627
|
+
|
|
628
|
+
const readStateData = async (
|
|
629
|
+
workspaceStorageDir: string,
|
|
630
|
+
workspaceStorageIds: string[],
|
|
631
|
+
record: QoderSessionRecord,
|
|
632
|
+
): Promise<QoderStateData> => {
|
|
633
|
+
const located = await locateStatePath(workspaceStorageDir, workspaceStorageIds, record);
|
|
634
|
+
if (!located.statePath) {
|
|
635
|
+
return {
|
|
636
|
+
fileOperationCount: 0,
|
|
637
|
+
lastActiveAtMs: null,
|
|
638
|
+
rawState: null,
|
|
639
|
+
requestId: null,
|
|
640
|
+
snapshotFileCount: 0,
|
|
641
|
+
statePath: null,
|
|
642
|
+
};
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
const rawState = await readJsonObject(located.statePath);
|
|
646
|
+
const stateStats = await stat(located.statePath)
|
|
647
|
+
.then((stats) => ({ mtimeMs: stats.mtimeMs }))
|
|
648
|
+
.catch(() => null);
|
|
649
|
+
const timeline = asObject(rawState?.timeline ?? null);
|
|
650
|
+
const operations = Array.isArray(timeline?.operations) ? timeline.operations : [];
|
|
651
|
+
const recentSnapshot = asObject(rawState?.recentSnapshot ?? null);
|
|
652
|
+
const snapshots = Array.isArray(recentSnapshot?.entries) ? recentSnapshot.entries : [];
|
|
653
|
+
const requestId =
|
|
654
|
+
operations
|
|
655
|
+
.map((operation) => asString(asObject(operation)?.requestId ?? null))
|
|
656
|
+
.find((value): value is string => Boolean(value)) ??
|
|
657
|
+
snapshots
|
|
658
|
+
.map((snapshot) => asString(asObject(asObject(snapshot)?.telemetryInfo ?? null)?.requestId ?? null))
|
|
659
|
+
.find((value): value is string => Boolean(value)) ??
|
|
660
|
+
null;
|
|
661
|
+
|
|
662
|
+
return {
|
|
663
|
+
fileOperationCount: operations.length,
|
|
664
|
+
lastActiveAtMs: stateStats?.mtimeMs ?? null,
|
|
665
|
+
rawState,
|
|
666
|
+
requestId,
|
|
667
|
+
snapshotFileCount: snapshots.length,
|
|
668
|
+
statePath: located.statePath,
|
|
669
|
+
};
|
|
670
|
+
};
|
|
671
|
+
|
|
672
|
+
const createEmptyStats = (): SessionStats => ({
|
|
673
|
+
assistantMessageCount: 0,
|
|
674
|
+
fileOperationCount: 0,
|
|
675
|
+
messageCount: 0,
|
|
676
|
+
renderablePartCount: 0,
|
|
677
|
+
snapshotFileCount: 0,
|
|
678
|
+
userMessageCount: 0,
|
|
679
|
+
});
|
|
680
|
+
|
|
681
|
+
const isRenderablePart = (part: QoderTranscriptPart): boolean => {
|
|
682
|
+
return part.type === 'text' && Boolean(part.text?.trim());
|
|
683
|
+
};
|
|
684
|
+
|
|
685
|
+
const updateStatsFromEntry = (stats: SessionStats, entry: QoderTranscriptEntry) => {
|
|
686
|
+
if (entry.entryType === 'tool_call') {
|
|
687
|
+
stats.fileOperationCount += 1;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
if (entry.role === 'assistant' || entry.role === 'user') {
|
|
691
|
+
stats.messageCount += 1;
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
if (entry.role === 'assistant') {
|
|
695
|
+
stats.assistantMessageCount += 1;
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
if (entry.role === 'user') {
|
|
699
|
+
stats.userMessageCount += 1;
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
stats.renderablePartCount += entry.parts.filter(isRenderablePart).length;
|
|
703
|
+
};
|
|
704
|
+
|
|
705
|
+
const createStatsFromEntries = (entries: QoderTranscriptEntry[], snapshotFileCount: number): SessionStats => {
|
|
706
|
+
const stats = createEmptyStats();
|
|
707
|
+
stats.snapshotFileCount = snapshotFileCount;
|
|
708
|
+
for (const entry of entries) {
|
|
709
|
+
updateStatsFromEntry(stats, entry);
|
|
710
|
+
}
|
|
711
|
+
return stats;
|
|
712
|
+
};
|
|
713
|
+
|
|
714
|
+
const getRecordTitle = (record: QoderSessionRecord): string => {
|
|
715
|
+
return cleanInlineTitle(record.task?.title ?? record.histories[0]?.title ?? record.sessionId);
|
|
716
|
+
};
|
|
717
|
+
|
|
718
|
+
const maxNullable = (...values: (number | null)[]): number | null => {
|
|
719
|
+
return values.reduce<number | null>((latest, value) => {
|
|
720
|
+
if (value === null) {
|
|
721
|
+
return latest;
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
return latest === null ? value : Math.max(latest, value);
|
|
725
|
+
}, null);
|
|
726
|
+
};
|
|
727
|
+
|
|
728
|
+
const getCreatedAtMs = (record: QoderSessionRecord): number | null => {
|
|
729
|
+
const historyCreatedAt = record.histories.reduce<number | null>((earliest, history) => {
|
|
730
|
+
if (history.timestampMs === null) {
|
|
731
|
+
return earliest;
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
return earliest === null ? history.timestampMs : Math.min(earliest, history.timestampMs);
|
|
735
|
+
}, null);
|
|
736
|
+
return record.task?.createdAtMs ?? historyCreatedAt;
|
|
737
|
+
};
|
|
738
|
+
|
|
739
|
+
const getLastActiveAtMs = (record: QoderSessionRecord, state: QoderStateData): number | null => {
|
|
740
|
+
const latestHistoryAt = record.histories.reduce<number | null>((latest, history) => {
|
|
741
|
+
return maxNullable(latest, history.timestampMs);
|
|
742
|
+
}, null);
|
|
743
|
+
return maxNullable(record.task?.updatedAtMs ?? null, latestHistoryAt, state.lastActiveAtMs);
|
|
744
|
+
};
|
|
745
|
+
|
|
746
|
+
const toSessionSummary = (
|
|
747
|
+
record: QoderSessionRecord,
|
|
748
|
+
state: QoderStateData,
|
|
749
|
+
stats: SessionStats,
|
|
750
|
+
modelFallback: string | null = null,
|
|
751
|
+
): QoderSessionSummary => {
|
|
752
|
+
const createdAtMs = getCreatedAtMs(record);
|
|
753
|
+
const lastActiveAtMs = getLastActiveAtMs(record, state);
|
|
754
|
+
const workspaceLabel = getWorkspaceLabel(record.worktree);
|
|
755
|
+
|
|
756
|
+
return {
|
|
757
|
+
...stats,
|
|
758
|
+
agentClass: record.task?.agentClass ?? null,
|
|
759
|
+
createdAtIso: toIso(createdAtMs),
|
|
760
|
+
createdAtMs,
|
|
761
|
+
executionMode: record.task?.executionMode ?? null,
|
|
762
|
+
historyIds: record.histories.map((history) => history.id),
|
|
763
|
+
lastActiveAtIso: toIso(lastActiveAtMs),
|
|
764
|
+
lastActiveAtMs,
|
|
765
|
+
model: record.task?.model ?? modelFallback,
|
|
766
|
+
query: record.task?.query ?? record.histories[0]?.title ?? null,
|
|
767
|
+
requestId: state.requestId ?? record.task?.executionRequestId ?? null,
|
|
768
|
+
sessionId: record.sessionId,
|
|
769
|
+
sourceStatePath: state.statePath,
|
|
770
|
+
status: record.task?.status ?? null,
|
|
771
|
+
taskId: record.task?.id ?? null,
|
|
772
|
+
title: getRecordTitle(record),
|
|
773
|
+
workspaceKey: getWorkspaceKey(record.worktree),
|
|
774
|
+
workspaceLabel,
|
|
775
|
+
workspacePath: record.worktree.startsWith(path.sep) ? record.worktree : null,
|
|
776
|
+
workspaceStorageId: record.workspaceStorageId,
|
|
777
|
+
worktree: record.worktree,
|
|
778
|
+
};
|
|
779
|
+
};
|
|
780
|
+
|
|
781
|
+
const loadRecords = async (
|
|
782
|
+
globalStateDb = resolveQoderGlobalStateDb(),
|
|
783
|
+
workspaceStorageDir = resolveQoderWorkspaceStorageDir(),
|
|
784
|
+
): Promise<QoderDataRecords> => {
|
|
785
|
+
const [rows, workspaceStorageIds] = await Promise.all([
|
|
786
|
+
readGlobalRows(globalStateDb),
|
|
787
|
+
listWorkspaceStorageIds(workspaceStorageDir),
|
|
788
|
+
]);
|
|
789
|
+
return {
|
|
790
|
+
modelConfig: parseModelConfigState(rows),
|
|
791
|
+
records: groupRecords(parseHistoryRows(rows), parseTaskSnapshotRows(rows)),
|
|
792
|
+
workspaceStorageIds,
|
|
793
|
+
};
|
|
794
|
+
};
|
|
795
|
+
|
|
796
|
+
const readRecordSummary = async (
|
|
797
|
+
record: QoderSessionRecord,
|
|
798
|
+
workspaceStorageDir: string,
|
|
799
|
+
workspaceStorageIds: string[],
|
|
800
|
+
modelFallback: string | null,
|
|
801
|
+
): Promise<QoderSessionSummary> => {
|
|
802
|
+
const state = await readStateData(workspaceStorageDir, workspaceStorageIds, record);
|
|
803
|
+
const entries = buildLocalTranscriptEntries(record, state);
|
|
804
|
+
const stats = createStatsFromEntries(entries, state.snapshotFileCount);
|
|
805
|
+
return toSessionSummary(record, state, stats, modelFallback);
|
|
806
|
+
};
|
|
807
|
+
|
|
808
|
+
const compareNullableMsDesc = (left: number | null, right: number | null): number => {
|
|
809
|
+
return (right ?? 0) - (left ?? 0);
|
|
810
|
+
};
|
|
811
|
+
|
|
812
|
+
const sumSessions = (sessions: QoderSessionSummary[], key: keyof SessionStats): number => {
|
|
813
|
+
return sessions.reduce((total, session) => total + session[key], 0);
|
|
814
|
+
};
|
|
815
|
+
|
|
816
|
+
const toWorkspaceGroup = (worktree: string, sessions: QoderSessionSummary[]): QoderWorkspaceGroup => {
|
|
817
|
+
const lastActiveAtMs = sessions.reduce<number | null>((latest, session) => {
|
|
818
|
+
return maxNullable(latest, session.lastActiveAtMs);
|
|
819
|
+
}, null);
|
|
820
|
+
|
|
821
|
+
return {
|
|
822
|
+
assistantMessageCount: sumSessions(sessions, 'assistantMessageCount'),
|
|
823
|
+
fileOperationCount: sumSessions(sessions, 'fileOperationCount'),
|
|
824
|
+
key: getWorkspaceKey(worktree),
|
|
825
|
+
label: getWorkspaceLabel(worktree),
|
|
826
|
+
lastActiveAtIso: toIso(lastActiveAtMs),
|
|
827
|
+
lastActiveAtMs,
|
|
828
|
+
messageCount: sumSessions(sessions, 'messageCount'),
|
|
829
|
+
renderablePartCount: sumSessions(sessions, 'renderablePartCount'),
|
|
830
|
+
sessionCount: sessions.length,
|
|
831
|
+
snapshotFileCount: sumSessions(sessions, 'snapshotFileCount'),
|
|
832
|
+
uri: getWorkspaceUri(worktree),
|
|
833
|
+
userMessageCount: sumSessions(sessions, 'userMessageCount'),
|
|
834
|
+
workspaceStorageIds: [...new Set(sessions.flatMap((session) => session.workspaceStorageId ?? []))],
|
|
835
|
+
worktree,
|
|
836
|
+
};
|
|
837
|
+
};
|
|
838
|
+
|
|
839
|
+
export const listQoderWorkspaceGroups = async (
|
|
840
|
+
globalStateDb = resolveQoderGlobalStateDb(),
|
|
841
|
+
workspaceStorageDir = resolveQoderWorkspaceStorageDir(),
|
|
842
|
+
): Promise<QoderWorkspaceGroup[]> => {
|
|
843
|
+
const { modelConfig, records, workspaceStorageIds } = await loadRecords(globalStateDb, workspaceStorageDir);
|
|
844
|
+
const modelFallback = getModelFallback(modelConfig);
|
|
845
|
+
const summaries = await mapWithConcurrency(records, READ_CONCURRENCY, (record) =>
|
|
846
|
+
readRecordSummary(record, workspaceStorageDir, workspaceStorageIds, modelFallback),
|
|
847
|
+
);
|
|
848
|
+
const sessionsByWorktree = new Map<string, QoderSessionSummary[]>();
|
|
849
|
+
|
|
850
|
+
for (const session of summaries) {
|
|
851
|
+
const sessions = sessionsByWorktree.get(session.worktree) ?? [];
|
|
852
|
+
sessions.push(session);
|
|
853
|
+
sessionsByWorktree.set(session.worktree, sessions);
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
return [...sessionsByWorktree.entries()]
|
|
857
|
+
.map(([worktree, sessions]) => toWorkspaceGroup(worktree, sessions))
|
|
858
|
+
.sort(
|
|
859
|
+
(left, right) =>
|
|
860
|
+
compareNullableMsDesc(left.lastActiveAtMs, right.lastActiveAtMs) ||
|
|
861
|
+
left.worktree.localeCompare(right.worktree),
|
|
862
|
+
);
|
|
863
|
+
};
|
|
864
|
+
|
|
865
|
+
const qoderWorkspaceMatchesQuery = (workspace: QoderWorkspaceGroup, query: string): boolean => {
|
|
866
|
+
const raw = query.trim();
|
|
867
|
+
if (!raw) {
|
|
868
|
+
return true;
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
const lowered = raw.toLowerCase();
|
|
872
|
+
if (workspace.key.toLowerCase() === lowered || workspace.label.toLowerCase() === lowered) {
|
|
873
|
+
return true;
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
if (isWorkspacePathQuery(raw)) {
|
|
877
|
+
return workspacePathMatchesQuery(workspace.worktree, raw);
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
return getPortablePathBasename(workspace.worktree).toLowerCase() === lowered;
|
|
881
|
+
};
|
|
882
|
+
|
|
883
|
+
export const findQoderWorkspaceGroups = (groups: QoderWorkspaceGroup[], query: string): QoderWorkspaceGroup[] => {
|
|
884
|
+
return groups.filter((group) => qoderWorkspaceMatchesQuery(group, query));
|
|
885
|
+
};
|
|
886
|
+
|
|
887
|
+
const sortSessions = (sessions: QoderSessionSummary[]): QoderSessionSummary[] => {
|
|
888
|
+
return [...sessions].sort(
|
|
889
|
+
(left, right) =>
|
|
890
|
+
compareNullableMsDesc(left.lastActiveAtMs, right.lastActiveAtMs) || left.title.localeCompare(right.title),
|
|
891
|
+
);
|
|
892
|
+
};
|
|
893
|
+
|
|
894
|
+
export const listQoderSessionsForGroup = async (
|
|
895
|
+
workspaceKey: string,
|
|
896
|
+
globalStateDb = resolveQoderGlobalStateDb(),
|
|
897
|
+
workspaceStorageDir = resolveQoderWorkspaceStorageDir(),
|
|
898
|
+
): Promise<QoderSessionSummary[]> => {
|
|
899
|
+
const worktree = getWorktreeFromWorkspaceKey(workspaceKey);
|
|
900
|
+
if (!worktree) {
|
|
901
|
+
return [];
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
const { modelConfig, records, workspaceStorageIds } = await loadRecords(globalStateDb, workspaceStorageDir);
|
|
905
|
+
const matchingRecords = records.filter((record) => record.worktree === worktree);
|
|
906
|
+
const modelFallback = getModelFallback(modelConfig);
|
|
907
|
+
const summaries = await mapWithConcurrency(matchingRecords, READ_CONCURRENCY, (record) =>
|
|
908
|
+
readRecordSummary(record, workspaceStorageDir, workspaceStorageIds, modelFallback),
|
|
909
|
+
);
|
|
910
|
+
return sortSessions(summaries);
|
|
911
|
+
};
|
|
912
|
+
|
|
913
|
+
const parseTextPart = (raw: Record<string, JsonValue>, text: string): QoderTranscriptPart => ({
|
|
914
|
+
raw,
|
|
915
|
+
text,
|
|
916
|
+
type: 'text',
|
|
917
|
+
});
|
|
918
|
+
|
|
919
|
+
const buildHistoryEntry = (history: QoderHistoryEntry, index: number): QoderTranscriptEntry => ({
|
|
920
|
+
entryId: history.id || `history:${index}`,
|
|
921
|
+
entryType: 'message',
|
|
922
|
+
parts: [
|
|
923
|
+
parseTextPart(
|
|
924
|
+
{
|
|
925
|
+
historyKey: history.historyKey,
|
|
926
|
+
id: history.id,
|
|
927
|
+
source: 'qoderLocalHistory',
|
|
928
|
+
title: history.title,
|
|
929
|
+
},
|
|
930
|
+
history.text,
|
|
931
|
+
),
|
|
932
|
+
],
|
|
933
|
+
raw: history.raw,
|
|
934
|
+
requestId: null,
|
|
935
|
+
role: 'user',
|
|
936
|
+
timestamp: toIso(history.timestampMs),
|
|
937
|
+
});
|
|
938
|
+
|
|
939
|
+
const getUriPath = (operation: Record<string, JsonValue>): string => {
|
|
940
|
+
const uri = asObject(operation.uri ?? null);
|
|
941
|
+
return (
|
|
942
|
+
asString(uri?.fsPath ?? null) ??
|
|
943
|
+
asString(uri?.path ?? null) ??
|
|
944
|
+
asString(uri?.external ?? null) ??
|
|
945
|
+
asString(operation.resource ?? null) ??
|
|
946
|
+
'unknown file'
|
|
947
|
+
);
|
|
948
|
+
};
|
|
949
|
+
|
|
950
|
+
const getEditPreview = (operation: Record<string, JsonValue>): string | null => {
|
|
951
|
+
const edits = Array.isArray(operation.edits) ? operation.edits : [];
|
|
952
|
+
const text = edits
|
|
953
|
+
.map((edit) => asString(asObject(edit)?.text ?? null))
|
|
954
|
+
.filter((value): value is string => Boolean(value?.trim()))
|
|
955
|
+
.join('\n\n')
|
|
956
|
+
.trim();
|
|
957
|
+
if (!text) {
|
|
958
|
+
return null;
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
const cleaned = cleanExtractedText(text);
|
|
962
|
+
return cleaned.length > 1200 ? `${cleaned.slice(0, 1200).trimEnd()}\n...[truncated]` : cleaned;
|
|
963
|
+
};
|
|
964
|
+
|
|
965
|
+
const buildOperationCommand = (operation: Record<string, JsonValue>): { command: string; toolName: string } => {
|
|
966
|
+
const type = asString(operation.type ?? null) ?? 'operation';
|
|
967
|
+
const filePath = getUriPath(operation);
|
|
968
|
+
|
|
969
|
+
if (type === 'create') {
|
|
970
|
+
return { command: `Create file: ${filePath}`, toolName: 'create_file' };
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
if (type === 'delete') {
|
|
974
|
+
return { command: `Delete file: ${filePath}`, toolName: 'delete_file' };
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
if (type === 'textEdit') {
|
|
978
|
+
const edits = Array.isArray(operation.edits) ? operation.edits.length : 0;
|
|
979
|
+
const preview = getEditPreview(operation);
|
|
980
|
+
return {
|
|
981
|
+
command: [`Edit file: ${filePath}`, `Edits: ${edits}`, preview ? `Preview:\n${preview}` : '']
|
|
982
|
+
.filter(Boolean)
|
|
983
|
+
.join('\n'),
|
|
984
|
+
toolName: 'edit_file',
|
|
985
|
+
};
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
return { command: `${cleanInlineTitle(type)}: ${filePath}`, toolName: type };
|
|
989
|
+
};
|
|
990
|
+
|
|
991
|
+
const buildOperationEntry = (
|
|
992
|
+
operation: Record<string, JsonValue>,
|
|
993
|
+
index: number,
|
|
994
|
+
statePath: string | null,
|
|
995
|
+
): QoderTranscriptEntry => {
|
|
996
|
+
const requestId = asString(operation.requestId ?? null);
|
|
997
|
+
const operationId =
|
|
998
|
+
asString(operation.operationId ?? null) ?? asString(operation.type ?? null) ?? `operation:${index}`;
|
|
999
|
+
const summary = buildOperationCommand(operation);
|
|
1000
|
+
|
|
1001
|
+
return {
|
|
1002
|
+
entryId: `${statePath ?? 'state'}:${operationId}:${index}`,
|
|
1003
|
+
entryType: 'tool_call',
|
|
1004
|
+
parts: [
|
|
1005
|
+
parseTextPart(
|
|
1006
|
+
{
|
|
1007
|
+
command: summary.command,
|
|
1008
|
+
operationIndex: index,
|
|
1009
|
+
sourceStatePath: statePath,
|
|
1010
|
+
toolName: summary.toolName,
|
|
1011
|
+
type: 'qoderFileOperation',
|
|
1012
|
+
},
|
|
1013
|
+
summary.command,
|
|
1014
|
+
),
|
|
1015
|
+
],
|
|
1016
|
+
raw: operation,
|
|
1017
|
+
requestId,
|
|
1018
|
+
role: 'tool',
|
|
1019
|
+
timestamp: null,
|
|
1020
|
+
};
|
|
1021
|
+
};
|
|
1022
|
+
|
|
1023
|
+
type QoderCliPart = {
|
|
1024
|
+
entryType: QoderTranscriptEntry['entryType'];
|
|
1025
|
+
raw: Record<string, JsonValue>;
|
|
1026
|
+
role: string;
|
|
1027
|
+
text: string;
|
|
1028
|
+
};
|
|
1029
|
+
|
|
1030
|
+
type QoderCliTranscript = {
|
|
1031
|
+
entries: QoderTranscriptEntry[];
|
|
1032
|
+
model: string | null;
|
|
1033
|
+
path: string | null;
|
|
1034
|
+
};
|
|
1035
|
+
|
|
1036
|
+
const getCliWorkspaceDirectoryName = (worktree: string): string => {
|
|
1037
|
+
return worktree.replace(/[\\/]+/gu, '-');
|
|
1038
|
+
};
|
|
1039
|
+
|
|
1040
|
+
const getCliTranscriptCandidates = (projectsDir: string, record: QoderSessionRecord): string[] => {
|
|
1041
|
+
return [
|
|
1042
|
+
path.join(projectsDir, `${record.sessionId}.jsonl`),
|
|
1043
|
+
path.join(projectsDir, getCliWorkspaceDirectoryName(record.worktree), `${record.sessionId}.jsonl`),
|
|
1044
|
+
];
|
|
1045
|
+
};
|
|
1046
|
+
|
|
1047
|
+
const locateCliTranscriptPath = async (projectsDir: string, record: QoderSessionRecord): Promise<string | null> => {
|
|
1048
|
+
for (const candidate of getCliTranscriptCandidates(projectsDir, record)) {
|
|
1049
|
+
if (await pathExists(candidate)) {
|
|
1050
|
+
return candidate;
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
return null;
|
|
1055
|
+
};
|
|
1056
|
+
|
|
1057
|
+
const stringifyCliValue = (value: JsonValue | undefined): string | null => {
|
|
1058
|
+
if (typeof value === 'string') {
|
|
1059
|
+
return value;
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
if (typeof value === 'number' || typeof value === 'boolean') {
|
|
1063
|
+
return String(value);
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
if (value === null || value === undefined) {
|
|
1067
|
+
return null;
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
return JSON.stringify(value, null, 2);
|
|
1071
|
+
};
|
|
1072
|
+
|
|
1073
|
+
const getCliTextValue = (value: JsonValue | undefined): string | null => {
|
|
1074
|
+
if (Array.isArray(value)) {
|
|
1075
|
+
const text = value
|
|
1076
|
+
.map((item) => getCliTextValue(item))
|
|
1077
|
+
.filter((item): item is string => Boolean(item?.trim()))
|
|
1078
|
+
.join('\n');
|
|
1079
|
+
return text ? text : null;
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
const objectValue = asObject(value ?? null);
|
|
1083
|
+
if (objectValue) {
|
|
1084
|
+
return (
|
|
1085
|
+
getCliTextValue(objectValue.text) ??
|
|
1086
|
+
getCliTextValue(objectValue.content) ??
|
|
1087
|
+
getCliTextValue(objectValue.result) ??
|
|
1088
|
+
stringifyCliValue(objectValue)
|
|
1089
|
+
);
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
return stringifyCliValue(value);
|
|
1093
|
+
};
|
|
1094
|
+
|
|
1095
|
+
const getCliPartData = (part: Record<string, JsonValue>): Record<string, JsonValue> => {
|
|
1096
|
+
return asObject(part.data ?? null) ?? part;
|
|
1097
|
+
};
|
|
1098
|
+
|
|
1099
|
+
const getCliToolName = (part: Record<string, JsonValue>, data: Record<string, JsonValue>): string => {
|
|
1100
|
+
return asString(data.name ?? null) ?? asString(part.name ?? null) ?? 'qoder_tool';
|
|
1101
|
+
};
|
|
1102
|
+
|
|
1103
|
+
const formatCliToolCall = (part: Record<string, JsonValue>, data: Record<string, JsonValue>): string | null => {
|
|
1104
|
+
const name = getCliToolName(part, data);
|
|
1105
|
+
const input = getCliTextValue(data.input ?? part.input);
|
|
1106
|
+
const text = [name, input].filter((value): value is string => Boolean(value?.trim())).join('\n');
|
|
1107
|
+
return text || null;
|
|
1108
|
+
};
|
|
1109
|
+
|
|
1110
|
+
const cliTextPartToTranscriptPart = (part: Record<string, JsonValue>, role: string): QoderCliPart | null => {
|
|
1111
|
+
const data = getCliPartData(part);
|
|
1112
|
+
const text = getCliTextValue(data.text ?? part.text);
|
|
1113
|
+
return text ? { entryType: 'message', raw: part, role, text } : null;
|
|
1114
|
+
};
|
|
1115
|
+
|
|
1116
|
+
const cliReasoningPartToTranscriptPart = (part: Record<string, JsonValue>, type: string): QoderCliPart | null => {
|
|
1117
|
+
const data = getCliPartData(part);
|
|
1118
|
+
const text = getCliTextValue(data.thinking ?? data.signature ?? part.thinking ?? part.text);
|
|
1119
|
+
return text ? { entryType: 'message', raw: { ...part, sourceType: type }, role: 'assistant', text } : null;
|
|
1120
|
+
};
|
|
1121
|
+
|
|
1122
|
+
const cliToolCallPartToTranscriptPart = (part: Record<string, JsonValue>): QoderCliPart | null => {
|
|
1123
|
+
const data = getCliPartData(part);
|
|
1124
|
+
const text = formatCliToolCall(part, data);
|
|
1125
|
+
return text
|
|
1126
|
+
? {
|
|
1127
|
+
entryType: 'tool_call',
|
|
1128
|
+
raw: {
|
|
1129
|
+
...part,
|
|
1130
|
+
command: text,
|
|
1131
|
+
toolName: getCliToolName(part, data),
|
|
1132
|
+
},
|
|
1133
|
+
role: 'tool',
|
|
1134
|
+
text,
|
|
1135
|
+
}
|
|
1136
|
+
: null;
|
|
1137
|
+
};
|
|
1138
|
+
|
|
1139
|
+
const cliToolOutputPartToTranscriptPart = (part: Record<string, JsonValue>): QoderCliPart | null => {
|
|
1140
|
+
const data = getCliPartData(part);
|
|
1141
|
+
const text = getCliTextValue(data.content ?? data.output ?? part.content);
|
|
1142
|
+
return text
|
|
1143
|
+
? {
|
|
1144
|
+
entryType: 'tool_output',
|
|
1145
|
+
raw: {
|
|
1146
|
+
...part,
|
|
1147
|
+
toolCallId: asString(data.tool_use_id ?? part.tool_use_id ?? null),
|
|
1148
|
+
toolName: getCliToolName(part, data),
|
|
1149
|
+
},
|
|
1150
|
+
role: 'tool',
|
|
1151
|
+
text,
|
|
1152
|
+
}
|
|
1153
|
+
: null;
|
|
1154
|
+
};
|
|
1155
|
+
|
|
1156
|
+
const cliPartToTranscriptPart = (part: Record<string, JsonValue>, role: string): QoderCliPart | null => {
|
|
1157
|
+
const type = asString(part.type ?? null);
|
|
1158
|
+
switch (type) {
|
|
1159
|
+
case 'text':
|
|
1160
|
+
return cliTextPartToTranscriptPart(part, role);
|
|
1161
|
+
case 'reasoning':
|
|
1162
|
+
case 'thinking':
|
|
1163
|
+
return cliReasoningPartToTranscriptPart(part, type);
|
|
1164
|
+
case 'tool_call':
|
|
1165
|
+
case 'tool_use':
|
|
1166
|
+
return cliToolCallPartToTranscriptPart(part);
|
|
1167
|
+
case 'tool_result':
|
|
1168
|
+
case 'tool_output':
|
|
1169
|
+
return cliToolOutputPartToTranscriptPart(part);
|
|
1170
|
+
default:
|
|
1171
|
+
return null;
|
|
1172
|
+
}
|
|
1173
|
+
};
|
|
1174
|
+
|
|
1175
|
+
const getCliLineParts = (raw: Record<string, JsonValue>): Record<string, JsonValue>[] => {
|
|
1176
|
+
const parts = Array.isArray(raw.parts) ? raw.parts : asObject(raw.message ?? null)?.content;
|
|
1177
|
+
return Array.isArray(parts)
|
|
1178
|
+
? parts.map((part) => asObject(part)).filter((part): part is Record<string, JsonValue> => Boolean(part))
|
|
1179
|
+
: [];
|
|
1180
|
+
};
|
|
1181
|
+
|
|
1182
|
+
const getCliLineRole = (raw: Record<string, JsonValue>): string => {
|
|
1183
|
+
return (
|
|
1184
|
+
asString(raw.role ?? null) ??
|
|
1185
|
+
asString(asObject(raw.message ?? null)?.role ?? null) ??
|
|
1186
|
+
asString(raw.type ?? null) ??
|
|
1187
|
+
'unknown'
|
|
1188
|
+
);
|
|
1189
|
+
};
|
|
1190
|
+
|
|
1191
|
+
const normalizeCliModel = (model: string | null, modelFallback: string | null): string | null => {
|
|
1192
|
+
if (!model) {
|
|
1193
|
+
return null;
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
if (model === 'auto') {
|
|
1197
|
+
return modelFallback;
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
return normalizeQoderModelLabel(model);
|
|
1201
|
+
};
|
|
1202
|
+
|
|
1203
|
+
const parseCliTranscriptLine = (
|
|
1204
|
+
raw: Record<string, JsonValue>,
|
|
1205
|
+
lineIndex: number,
|
|
1206
|
+
sourcePath: string,
|
|
1207
|
+
): QoderTranscriptEntry[] => {
|
|
1208
|
+
const role = getCliLineRole(raw);
|
|
1209
|
+
const timestamp = toIso(parseTimestampMs(raw.created_at ?? raw.timestamp ?? raw.updated_at));
|
|
1210
|
+
const parentId = asString(raw.id ?? raw.uuid ?? null) ?? `${sourcePath}:${lineIndex}`;
|
|
1211
|
+
return getCliLineParts(raw).flatMap((part, partIndex) => {
|
|
1212
|
+
const parsed = cliPartToTranscriptPart(part, role);
|
|
1213
|
+
if (!parsed) {
|
|
1214
|
+
return [];
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
return [
|
|
1218
|
+
{
|
|
1219
|
+
entryId: `${parentId}:${partIndex}`,
|
|
1220
|
+
entryType: parsed.entryType,
|
|
1221
|
+
parts: [
|
|
1222
|
+
parseTextPart(
|
|
1223
|
+
{
|
|
1224
|
+
...parsed.raw,
|
|
1225
|
+
source: 'qoderCliTranscript',
|
|
1226
|
+
sourcePath,
|
|
1227
|
+
},
|
|
1228
|
+
parsed.text,
|
|
1229
|
+
),
|
|
1230
|
+
],
|
|
1231
|
+
raw,
|
|
1232
|
+
requestId: asString(raw.request_set_id ?? raw.requestSetId ?? null),
|
|
1233
|
+
role: parsed.role,
|
|
1234
|
+
timestamp,
|
|
1235
|
+
},
|
|
1236
|
+
];
|
|
1237
|
+
});
|
|
1238
|
+
};
|
|
1239
|
+
|
|
1240
|
+
const readCliTranscriptEntries = async (
|
|
1241
|
+
projectsDir: string,
|
|
1242
|
+
record: QoderSessionRecord,
|
|
1243
|
+
modelFallback: string | null,
|
|
1244
|
+
): Promise<QoderCliTranscript> => {
|
|
1245
|
+
const transcriptPath = await locateCliTranscriptPath(projectsDir, record);
|
|
1246
|
+
if (!transcriptPath) {
|
|
1247
|
+
return { entries: [], model: null, path: null };
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
const text = await Bun.file(transcriptPath)
|
|
1251
|
+
.text()
|
|
1252
|
+
.catch(() => '');
|
|
1253
|
+
let model: string | null = null;
|
|
1254
|
+
const entries = text.split(/\r?\n/u).flatMap((line, lineIndex) => {
|
|
1255
|
+
if (!line.trim()) {
|
|
1256
|
+
return [];
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
const raw = asJsonObject(parseJsonValue(line));
|
|
1260
|
+
model ??= normalizeCliModel(asString(raw?.model ?? null), modelFallback);
|
|
1261
|
+
return raw ? parseCliTranscriptLine(raw, lineIndex, transcriptPath) : [];
|
|
1262
|
+
});
|
|
1263
|
+
|
|
1264
|
+
return { entries, model, path: transcriptPath };
|
|
1265
|
+
};
|
|
1266
|
+
|
|
1267
|
+
const getRawStringValue = (raw: Record<string, JsonValue>, keys: string[]): string | null => {
|
|
1268
|
+
for (const key of keys) {
|
|
1269
|
+
const value = asString(raw[key] ?? null);
|
|
1270
|
+
if (value?.trim()) {
|
|
1271
|
+
return value;
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
return null;
|
|
1276
|
+
};
|
|
1277
|
+
|
|
1278
|
+
const stringifyAcpValue = (value: JsonValue | undefined): string | null => {
|
|
1279
|
+
if (typeof value === 'string') {
|
|
1280
|
+
return value;
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
if (typeof value === 'number' || typeof value === 'boolean') {
|
|
1284
|
+
return String(value);
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
if (value === null || value === undefined) {
|
|
1288
|
+
return null;
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
return JSON.stringify(value, null, 2);
|
|
1292
|
+
};
|
|
1293
|
+
|
|
1294
|
+
const getAcpContentText = (update: Record<string, JsonValue>): string | null => {
|
|
1295
|
+
const content = asObject(update.content ?? null);
|
|
1296
|
+
const data = asObject(update.data ?? null);
|
|
1297
|
+
return (
|
|
1298
|
+
getRawStringValue(content ?? {}, ['text', 'content', 'thinking']) ??
|
|
1299
|
+
getRawStringValue(data ?? {}, ['text', 'content', 'thinking', 'output']) ??
|
|
1300
|
+
getRawStringValue(update, ['text', 'content', 'thinking', 'message', 'delta'])
|
|
1301
|
+
);
|
|
1302
|
+
};
|
|
1303
|
+
|
|
1304
|
+
const getAcpTimestamp = (update: Record<string, JsonValue>): string | null => {
|
|
1305
|
+
return toIso(parseTimestampMs(update.timestamp ?? update.created_at ?? update.createdAt ?? update.updated_at));
|
|
1306
|
+
};
|
|
1307
|
+
|
|
1308
|
+
const buildAcpMessageEntry = (
|
|
1309
|
+
event: QoderAcpSessionUpdate,
|
|
1310
|
+
index: number,
|
|
1311
|
+
role: 'assistant' | 'user',
|
|
1312
|
+
): QoderTranscriptEntry | null => {
|
|
1313
|
+
const text = getAcpContentText(event.update);
|
|
1314
|
+
if (!text?.trim()) {
|
|
1315
|
+
return null;
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
return {
|
|
1319
|
+
entryId: `qoder-acp:${event.sessionId}:${index}`,
|
|
1320
|
+
entryType: 'message',
|
|
1321
|
+
parts: [
|
|
1322
|
+
parseTextPart(
|
|
1323
|
+
{
|
|
1324
|
+
requestId: event.requestId,
|
|
1325
|
+
sessionUpdate: event.update.sessionUpdate ?? null,
|
|
1326
|
+
source: 'qoderAcpSessionLoad',
|
|
1327
|
+
},
|
|
1328
|
+
text,
|
|
1329
|
+
),
|
|
1330
|
+
],
|
|
1331
|
+
raw: event.update,
|
|
1332
|
+
requestId: event.requestId,
|
|
1333
|
+
role,
|
|
1334
|
+
timestamp: getAcpTimestamp(event.update),
|
|
1335
|
+
};
|
|
1336
|
+
};
|
|
1337
|
+
|
|
1338
|
+
const getAcpToolId = (update: Record<string, JsonValue>, index: number): string => {
|
|
1339
|
+
return (
|
|
1340
|
+
getRawStringValue(update, ['toolCallId', 'tool_call_id', 'callId', 'id']) ??
|
|
1341
|
+
getRawStringValue(asObject(update.toolCall ?? null) ?? {}, ['id', 'toolCallId']) ??
|
|
1342
|
+
`tool:${index}`
|
|
1343
|
+
);
|
|
1344
|
+
};
|
|
1345
|
+
|
|
1346
|
+
const getAcpToolName = (update: Record<string, JsonValue>): string => {
|
|
1347
|
+
return (
|
|
1348
|
+
getRawStringValue(update, ['toolName', 'name', 'title', 'kind']) ??
|
|
1349
|
+
getRawStringValue(asObject(update.toolCall ?? null) ?? {}, ['toolName', 'name', 'title', 'kind']) ??
|
|
1350
|
+
'qoder_tool'
|
|
1351
|
+
);
|
|
1352
|
+
};
|
|
1353
|
+
|
|
1354
|
+
const buildAcpToolCallText = (update: Record<string, JsonValue>): string | null => {
|
|
1355
|
+
const toolCall = asObject(update.toolCall ?? null);
|
|
1356
|
+
const name = getAcpToolName(update);
|
|
1357
|
+
const input =
|
|
1358
|
+
stringifyAcpValue(update.input) ??
|
|
1359
|
+
stringifyAcpValue(update.arguments) ??
|
|
1360
|
+
stringifyAcpValue(update.rawInput) ??
|
|
1361
|
+
stringifyAcpValue(toolCall?.input) ??
|
|
1362
|
+
stringifyAcpValue(toolCall?.arguments);
|
|
1363
|
+
return [name, input].filter((value): value is string => Boolean(value?.trim())).join('\n') || null;
|
|
1364
|
+
};
|
|
1365
|
+
|
|
1366
|
+
const buildAcpToolOutputText = (update: Record<string, JsonValue>): string | null => {
|
|
1367
|
+
const text =
|
|
1368
|
+
getAcpContentText(update) ??
|
|
1369
|
+
stringifyAcpValue(update.output) ??
|
|
1370
|
+
stringifyAcpValue(update.result) ??
|
|
1371
|
+
stringifyAcpValue(update.rawOutput);
|
|
1372
|
+
return text?.trim() ? text : null;
|
|
1373
|
+
};
|
|
1374
|
+
|
|
1375
|
+
const buildAcpToolEntry = (
|
|
1376
|
+
event: QoderAcpSessionUpdate,
|
|
1377
|
+
index: number,
|
|
1378
|
+
entryType: 'tool_call' | 'tool_output',
|
|
1379
|
+
): QoderTranscriptEntry | null => {
|
|
1380
|
+
const text = entryType === 'tool_call' ? buildAcpToolCallText(event.update) : buildAcpToolOutputText(event.update);
|
|
1381
|
+
if (!text) {
|
|
1382
|
+
return null;
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1385
|
+
const toolCallId = getAcpToolId(event.update, index);
|
|
1386
|
+
const toolName = getAcpToolName(event.update);
|
|
1387
|
+
return {
|
|
1388
|
+
entryId: `qoder-acp:${event.sessionId}:${toolCallId}:${index}`,
|
|
1389
|
+
entryType,
|
|
1390
|
+
parts: [
|
|
1391
|
+
parseTextPart(
|
|
1392
|
+
{
|
|
1393
|
+
requestId: event.requestId,
|
|
1394
|
+
sessionUpdate: event.update.sessionUpdate ?? null,
|
|
1395
|
+
source: 'qoderAcpSessionLoad',
|
|
1396
|
+
toolCallId,
|
|
1397
|
+
toolName,
|
|
1398
|
+
},
|
|
1399
|
+
text,
|
|
1400
|
+
),
|
|
1401
|
+
],
|
|
1402
|
+
raw: event.update,
|
|
1403
|
+
requestId: event.requestId,
|
|
1404
|
+
role: 'tool',
|
|
1405
|
+
timestamp: getAcpTimestamp(event.update),
|
|
1406
|
+
};
|
|
1407
|
+
};
|
|
1408
|
+
|
|
1409
|
+
const acpUpdateToEntry = (event: QoderAcpSessionUpdate, index: number): QoderTranscriptEntry | null => {
|
|
1410
|
+
switch (event.update.sessionUpdate) {
|
|
1411
|
+
case 'user_message_chunk':
|
|
1412
|
+
return buildAcpMessageEntry(event, index, 'user');
|
|
1413
|
+
case 'agent_thought_chunk':
|
|
1414
|
+
case 'agent_message_chunk':
|
|
1415
|
+
return buildAcpMessageEntry(event, index, 'assistant');
|
|
1416
|
+
case 'tool_call':
|
|
1417
|
+
return buildAcpToolEntry(event, index, 'tool_call');
|
|
1418
|
+
case 'tool_call_update':
|
|
1419
|
+
return buildAcpToolEntry(event, index, 'tool_output');
|
|
1420
|
+
default:
|
|
1421
|
+
return null;
|
|
1422
|
+
}
|
|
1423
|
+
};
|
|
1424
|
+
|
|
1425
|
+
const getAcpModel = (events: QoderAcpSessionUpdate[]): string | null => {
|
|
1426
|
+
for (const event of [...events].reverse()) {
|
|
1427
|
+
if (event.update.sessionUpdate !== 'current_model_update') {
|
|
1428
|
+
continue;
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1431
|
+
const model = normalizeQoderModelLabel(getRawStringValue(event.update, ['modelId', 'model', 'modelName']));
|
|
1432
|
+
if (model) {
|
|
1433
|
+
return model;
|
|
1434
|
+
}
|
|
1435
|
+
}
|
|
1436
|
+
|
|
1437
|
+
return null;
|
|
1438
|
+
};
|
|
1439
|
+
|
|
1440
|
+
const getTaskIdForAcpLoad = (record: QoderSessionRecord): string | null => {
|
|
1441
|
+
return record.task?.id ?? (record.sessionId.replace(/\.session\.execution$/u, '') || null);
|
|
1442
|
+
};
|
|
1443
|
+
|
|
1444
|
+
const shouldUseAcp = (
|
|
1445
|
+
record: QoderSessionRecord,
|
|
1446
|
+
state: QoderStateData,
|
|
1447
|
+
cliTranscript: QoderCliTranscript,
|
|
1448
|
+
options: QoderTranscriptReadOptions,
|
|
1449
|
+
globalStateDb: string,
|
|
1450
|
+
workspaceStorageDir: string,
|
|
1451
|
+
): boolean => {
|
|
1452
|
+
if (options.enableAcp === false) {
|
|
1453
|
+
return false;
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1456
|
+
if (cliTranscript.entries.some((entry) => entry.role === 'assistant')) {
|
|
1457
|
+
return false;
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1460
|
+
if (options.acpSocketPath) {
|
|
1461
|
+
return true;
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1464
|
+
if (globalStateDb !== resolveQoderGlobalStateDb() || workspaceStorageDir !== resolveQoderWorkspaceStorageDir()) {
|
|
1465
|
+
return false;
|
|
1466
|
+
}
|
|
1467
|
+
|
|
1468
|
+
const lastActiveAtMs = getLastActiveAtMs(record, state);
|
|
1469
|
+
return lastActiveAtMs !== null && Date.now() - lastActiveAtMs <= ACP_RECENT_SESSION_WINDOW_MS;
|
|
1470
|
+
};
|
|
1471
|
+
|
|
1472
|
+
const readAcpTranscriptEntries = async (
|
|
1473
|
+
record: QoderSessionRecord,
|
|
1474
|
+
state: QoderStateData,
|
|
1475
|
+
cliTranscript: QoderCliTranscript,
|
|
1476
|
+
options: QoderTranscriptReadOptions,
|
|
1477
|
+
globalStateDb: string,
|
|
1478
|
+
workspaceStorageDir: string,
|
|
1479
|
+
): Promise<{ entries: QoderTranscriptEntry[]; model: string | null; socketPath: string | null }> => {
|
|
1480
|
+
if (!shouldUseAcp(record, state, cliTranscript, options, globalStateDb, workspaceStorageDir)) {
|
|
1481
|
+
return { entries: [], model: null, socketPath: null };
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1484
|
+
const loaded = await loadQoderAcpSession({
|
|
1485
|
+
cwd: record.worktree,
|
|
1486
|
+
drainMs: options.acpDrainMs,
|
|
1487
|
+
sessionId: record.sessionId,
|
|
1488
|
+
socketPath: options.acpSocketPath ?? resolveQoderAcpSocketPath(),
|
|
1489
|
+
taskId: getTaskIdForAcpLoad(record),
|
|
1490
|
+
timeoutMs: options.acpTimeoutMs,
|
|
1491
|
+
});
|
|
1492
|
+
if (!loaded) {
|
|
1493
|
+
return { entries: [], model: null, socketPath: null };
|
|
1494
|
+
}
|
|
1495
|
+
|
|
1496
|
+
return {
|
|
1497
|
+
entries: loaded.events
|
|
1498
|
+
.map((event, index) => acpUpdateToEntry(event, index))
|
|
1499
|
+
.filter((entry): entry is QoderTranscriptEntry => Boolean(entry)),
|
|
1500
|
+
model: getAcpModel(loaded.events),
|
|
1501
|
+
socketPath: loaded.socketPath,
|
|
1502
|
+
};
|
|
1503
|
+
};
|
|
1504
|
+
|
|
1505
|
+
const buildLocalTranscriptEntryGroups = (
|
|
1506
|
+
record: QoderSessionRecord,
|
|
1507
|
+
state: QoderStateData,
|
|
1508
|
+
): { historyEntries: QoderTranscriptEntry[]; operationEntries: QoderTranscriptEntry[] } => {
|
|
1509
|
+
const historyEntries = record.histories.map(buildHistoryEntry);
|
|
1510
|
+
const timeline = asObject(state.rawState?.timeline ?? null);
|
|
1511
|
+
const operations = Array.isArray(timeline?.operations) ? timeline.operations : [];
|
|
1512
|
+
const operationEntries = operations.flatMap((operation, index) => {
|
|
1513
|
+
const raw = asObject(operation);
|
|
1514
|
+
return raw ? [buildOperationEntry(raw, index, state.statePath)] : [];
|
|
1515
|
+
});
|
|
1516
|
+
return { historyEntries, operationEntries };
|
|
1517
|
+
};
|
|
1518
|
+
|
|
1519
|
+
const buildLocalTranscriptEntries = (record: QoderSessionRecord, state: QoderStateData): QoderTranscriptEntry[] => {
|
|
1520
|
+
const { historyEntries, operationEntries } = buildLocalTranscriptEntryGroups(record, state);
|
|
1521
|
+
return [...historyEntries, ...operationEntries];
|
|
1522
|
+
};
|
|
1523
|
+
|
|
1524
|
+
const buildTranscriptEntries = async (
|
|
1525
|
+
record: QoderSessionRecord,
|
|
1526
|
+
state: QoderStateData,
|
|
1527
|
+
cliProjectsDir: string,
|
|
1528
|
+
modelFallback: string | null,
|
|
1529
|
+
options: QoderTranscriptReadOptions,
|
|
1530
|
+
globalStateDb: string,
|
|
1531
|
+
workspaceStorageDir: string,
|
|
1532
|
+
): Promise<{
|
|
1533
|
+
acpSocketPath: string | null;
|
|
1534
|
+
cliTranscriptPath: string | null;
|
|
1535
|
+
entries: QoderTranscriptEntry[];
|
|
1536
|
+
model: string | null;
|
|
1537
|
+
}> => {
|
|
1538
|
+
const { historyEntries, operationEntries } = buildLocalTranscriptEntryGroups(record, state);
|
|
1539
|
+
const cliTranscript = await readCliTranscriptEntries(cliProjectsDir, record, modelFallback);
|
|
1540
|
+
const acpTranscript = await readAcpTranscriptEntries(
|
|
1541
|
+
record,
|
|
1542
|
+
state,
|
|
1543
|
+
cliTranscript,
|
|
1544
|
+
options,
|
|
1545
|
+
globalStateDb,
|
|
1546
|
+
workspaceStorageDir,
|
|
1547
|
+
);
|
|
1548
|
+
const transcriptEntries = cliTranscript.entries.some((entry) => entry.role === 'assistant')
|
|
1549
|
+
? cliTranscript.entries
|
|
1550
|
+
: acpTranscript.entries;
|
|
1551
|
+
const shouldIncludeHistory = !transcriptEntries.some((entry) => entry.role === 'user');
|
|
1552
|
+
|
|
1553
|
+
return {
|
|
1554
|
+
acpSocketPath: acpTranscript.socketPath,
|
|
1555
|
+
cliTranscriptPath: cliTranscript.path,
|
|
1556
|
+
entries: [...(shouldIncludeHistory ? historyEntries : []), ...transcriptEntries, ...operationEntries],
|
|
1557
|
+
model: acpTranscript.model ?? cliTranscript.model ?? modelFallback,
|
|
1558
|
+
};
|
|
1559
|
+
};
|
|
1560
|
+
|
|
1561
|
+
export const readQoderSessionTranscript = async (
|
|
1562
|
+
globalStateDb: string,
|
|
1563
|
+
workspaceStorageDir: string,
|
|
1564
|
+
sessionId: string,
|
|
1565
|
+
cliProjectsDir = resolveQoderCliProjectsDir(),
|
|
1566
|
+
options: QoderTranscriptReadOptions = {},
|
|
1567
|
+
): Promise<QoderSessionTranscript | null> => {
|
|
1568
|
+
const { modelConfig, records, workspaceStorageIds } = await loadRecords(globalStateDb, workspaceStorageDir);
|
|
1569
|
+
const record = records.find((candidate) => candidate.sessionId === sessionId);
|
|
1570
|
+
if (!record) {
|
|
1571
|
+
return null;
|
|
1572
|
+
}
|
|
1573
|
+
|
|
1574
|
+
const state = await readStateData(workspaceStorageDir, workspaceStorageIds, record);
|
|
1575
|
+
const modelFallback = getModelFallback(modelConfig);
|
|
1576
|
+
const { acpSocketPath, cliTranscriptPath, entries, model } = await buildTranscriptEntries(
|
|
1577
|
+
record,
|
|
1578
|
+
state,
|
|
1579
|
+
cliProjectsDir,
|
|
1580
|
+
modelFallback,
|
|
1581
|
+
options,
|
|
1582
|
+
globalStateDb,
|
|
1583
|
+
workspaceStorageDir,
|
|
1584
|
+
);
|
|
1585
|
+
const stats = createStatsFromEntries(entries, state.snapshotFileCount);
|
|
1586
|
+
|
|
1587
|
+
return {
|
|
1588
|
+
entries,
|
|
1589
|
+
rawSession: {
|
|
1590
|
+
histories: record.histories.map((history) => history.raw),
|
|
1591
|
+
sourceAcpSocketPath: acpSocketPath,
|
|
1592
|
+
sourceCliTranscriptPath: cliTranscriptPath,
|
|
1593
|
+
sourceStatePath: state.statePath,
|
|
1594
|
+
state: state.rawState,
|
|
1595
|
+
task: record.task?.raw ?? null,
|
|
1596
|
+
workspaceStorageId: record.workspaceStorageId,
|
|
1597
|
+
},
|
|
1598
|
+
renderablePartCount: stats.renderablePartCount,
|
|
1599
|
+
session: toSessionSummary(record, state, stats, model),
|
|
1600
|
+
};
|
|
1601
|
+
};
|