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
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
import type { CursorThreadSummary, CursorWorkspaceGroup } from '@spiracha/lib/cursor-exporter-types';
|
|
2
|
+
import { useMutation, useQueryClient, useSuspenseQuery } from '@tanstack/react-query';
|
|
3
|
+
import { createFileRoute, useNavigate } from '@tanstack/react-router';
|
|
4
|
+
import { RefreshCcw, Trash2 } from 'lucide-react';
|
|
5
|
+
import { useDeferredValue, useState } from 'react';
|
|
6
|
+
import { CursorThreadsTable } from '#/components/cursor-threads-table';
|
|
7
|
+
import { DeleteConfirmDialog } from '#/components/delete-confirm-dialog';
|
|
8
|
+
import { ExportDialog } from '#/components/export-dialog';
|
|
9
|
+
import { ListSearchInput } from '#/components/list-search-input';
|
|
10
|
+
import { LoadingPanel } from '#/components/loading-panel';
|
|
11
|
+
import { PageHeader } from '#/components/page-header';
|
|
12
|
+
import { ReloadErrorPanel } from '#/components/reload-error-panel';
|
|
13
|
+
import { Button } from '#/components/ui/button';
|
|
14
|
+
import { cursorThreadsQueryOptions, cursorWorkspacesQueryOptions } from '#/lib/cursor-queries';
|
|
15
|
+
import {
|
|
16
|
+
deleteCursorThreadsFn,
|
|
17
|
+
deleteCursorWorkspaceFn,
|
|
18
|
+
exportCursorThreadFn,
|
|
19
|
+
exportCursorThreadsFn,
|
|
20
|
+
recoverCursorWorkspaceFn,
|
|
21
|
+
} from '#/lib/cursor-server';
|
|
22
|
+
import { downloadTextFile, downloadUrlFile } from '#/lib/download';
|
|
23
|
+
import { matchesTextQuery } from '#/lib/text-filter';
|
|
24
|
+
|
|
25
|
+
type PendingCursorDelete =
|
|
26
|
+
| { kind: 'threads'; threads: CursorThreadSummary[] }
|
|
27
|
+
| { kind: 'workspace'; workspace: CursorWorkspaceGroup };
|
|
28
|
+
|
|
29
|
+
type PendingCursorExport = {
|
|
30
|
+
composerIds: string[];
|
|
31
|
+
label: string;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
type ExportDialogOptions = {
|
|
35
|
+
includeCommentary: boolean;
|
|
36
|
+
includeMetadata: boolean;
|
|
37
|
+
includeTools: boolean;
|
|
38
|
+
outputFormat: 'md' | 'txt';
|
|
39
|
+
zipArchive: boolean;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const findWorkspaceOrThrow = (workspaces: CursorWorkspaceGroup[], workspaceKey: string) => {
|
|
43
|
+
const workspace = workspaces.find((candidate) => candidate.key === workspaceKey);
|
|
44
|
+
if (!workspace) {
|
|
45
|
+
throw new Error(`Cursor workspace not found: ${workspaceKey}`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return workspace;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const getSelectedThreads = (threads: CursorThreadSummary[], composerIds: string[]) => {
|
|
52
|
+
const composerIdSet = new Set(composerIds);
|
|
53
|
+
return threads.filter((thread) => composerIdSet.has(thread.composerId));
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const buildPendingCursorDelete = (threads: CursorThreadSummary[]): PendingCursorDelete | null => {
|
|
57
|
+
if (threads.length === 0) {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return { kind: 'threads', threads };
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const buildPendingCursorExport = (threads: CursorThreadSummary[]): PendingCursorExport | null => {
|
|
65
|
+
if (threads.length === 0) {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
composerIds: threads.map((thread) => thread.composerId),
|
|
71
|
+
label: threads.length === 1 ? threads[0]!.name : `${threads.length} selected threads`,
|
|
72
|
+
};
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const getCursorDeleteConfirmLabel = (pendingDelete: PendingCursorDelete | null, pending: boolean) => {
|
|
76
|
+
if (pending) {
|
|
77
|
+
return 'Deleting...';
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (pendingDelete?.kind === 'workspace') {
|
|
81
|
+
return 'Delete workspace';
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return pendingDelete && pendingDelete.threads.length > 1 ? 'Delete threads' : 'Delete thread';
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const getCursorDeleteDescription = (pendingDelete: PendingCursorDelete | null) => {
|
|
88
|
+
if (!pendingDelete) {
|
|
89
|
+
return '';
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (pendingDelete.kind === 'workspace') {
|
|
93
|
+
return `Permanently delete every thread for "${pendingDelete.workspace.label}" from Cursor's database and remove any on-disk transcript directories. Quit Cursor first. This cannot be undone.`;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (pendingDelete.threads.length === 1) {
|
|
97
|
+
return `Permanently delete "${pendingDelete.threads[0]!.name}" from Cursor's database and remove any on-disk transcript directories. Quit Cursor first. This cannot be undone.`;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return `Permanently delete ${pendingDelete.threads.length} selected Cursor threads and remove any on-disk transcript directories. Quit Cursor first. This cannot be undone.`;
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const getCursorDeleteTitle = (pendingDelete: PendingCursorDelete | null) => {
|
|
104
|
+
if (pendingDelete?.kind === 'workspace') {
|
|
105
|
+
return 'Delete Cursor workspace?';
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (pendingDelete && pendingDelete.threads.length > 1) {
|
|
109
|
+
return `Delete ${pendingDelete.threads.length} Cursor threads?`;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return 'Delete Cursor thread?';
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const CursorWorkspaceErrorComponent = ({ error }: { error: Error }) => {
|
|
116
|
+
return <ReloadErrorPanel description={error.message} title="Failed to load Cursor workspace" />;
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const CursorWorkspacePage = () => {
|
|
120
|
+
const navigate = useNavigate();
|
|
121
|
+
const params = Route.useParams();
|
|
122
|
+
const queryClient = useQueryClient();
|
|
123
|
+
const workspaces = useSuspenseQuery(cursorWorkspacesQueryOptions()).data;
|
|
124
|
+
const workspace = findWorkspaceOrThrow(workspaces, params.workspaceKey);
|
|
125
|
+
const threads = useSuspenseQuery(cursorThreadsQueryOptions(workspace.key)).data;
|
|
126
|
+
const [searchInput, setSearchInput] = useState('');
|
|
127
|
+
const [pendingDelete, setPendingDelete] = useState<PendingCursorDelete | null>(null);
|
|
128
|
+
const [pendingExport, setPendingExport] = useState<PendingCursorExport | null>(null);
|
|
129
|
+
const deferredSearch = useDeferredValue(searchInput);
|
|
130
|
+
|
|
131
|
+
const invalidateWorkspaceQueries = async () => {
|
|
132
|
+
await Promise.all([
|
|
133
|
+
queryClient.invalidateQueries({ queryKey: ['cursor-workspaces'] }),
|
|
134
|
+
queryClient.invalidateQueries({ queryKey: ['cursor-threads', workspace.key] }),
|
|
135
|
+
]);
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
const recoverWorkspaceMutation = useMutation({
|
|
139
|
+
mutationFn: () => recoverCursorWorkspaceFn({ data: { apply: true, workspaceKey: workspace.key } }),
|
|
140
|
+
onSuccess: invalidateWorkspaceQueries,
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
const deleteMutation = useMutation({
|
|
144
|
+
mutationFn: (target: PendingCursorDelete) =>
|
|
145
|
+
target.kind === 'workspace'
|
|
146
|
+
? deleteCursorWorkspaceFn({ data: { workspaceKey: target.workspace.key } })
|
|
147
|
+
: deleteCursorThreadsFn({ data: { composerIds: target.threads.map((thread) => thread.composerId) } }),
|
|
148
|
+
onSuccess: async (_result, target) => {
|
|
149
|
+
if (target.kind === 'workspace') {
|
|
150
|
+
await navigate({ to: '/cursor' });
|
|
151
|
+
await queryClient.invalidateQueries({ queryKey: ['cursor-workspaces'] });
|
|
152
|
+
setPendingDelete(null);
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
await invalidateWorkspaceQueries();
|
|
157
|
+
setPendingDelete(null);
|
|
158
|
+
},
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
const exportMutation = useMutation({
|
|
162
|
+
mutationFn: async (options: ExportDialogOptions) => {
|
|
163
|
+
if (!pendingExport) {
|
|
164
|
+
throw new Error('No thread selected for export');
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const download =
|
|
168
|
+
pendingExport.composerIds.length === 1
|
|
169
|
+
? await exportCursorThreadFn({
|
|
170
|
+
data: {
|
|
171
|
+
...options,
|
|
172
|
+
composerId: pendingExport.composerIds[0]!,
|
|
173
|
+
},
|
|
174
|
+
})
|
|
175
|
+
: await exportCursorThreadsFn({
|
|
176
|
+
data: {
|
|
177
|
+
...options,
|
|
178
|
+
composerIds: pendingExport.composerIds,
|
|
179
|
+
},
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
if (download.mode === 'download') {
|
|
183
|
+
downloadTextFile(download.fileName, download.content, download.mimeType);
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
await downloadUrlFile(download.fileName, download.downloadUrl);
|
|
188
|
+
},
|
|
189
|
+
onSuccess: () => {
|
|
190
|
+
setPendingExport(null);
|
|
191
|
+
},
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
const visibleThreads = threads.filter((thread) =>
|
|
195
|
+
matchesTextQuery(deferredSearch, [thread.name, thread.composerId, thread.mode, thread.workspaceLabel]),
|
|
196
|
+
);
|
|
197
|
+
const openDeleteForSelectedThreads = (composerIds: string[]) => {
|
|
198
|
+
const nextPendingDelete = buildPendingCursorDelete(getSelectedThreads(visibleThreads, composerIds));
|
|
199
|
+
if (nextPendingDelete) {
|
|
200
|
+
setPendingDelete(nextPendingDelete);
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
const openExportForSelectedThreads = (composerIds: string[]) => {
|
|
204
|
+
const nextPendingExport = buildPendingCursorExport(getSelectedThreads(visibleThreads, composerIds));
|
|
205
|
+
if (nextPendingExport) {
|
|
206
|
+
setPendingExport(nextPendingExport);
|
|
207
|
+
}
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
return (
|
|
211
|
+
<div className="space-y-6">
|
|
212
|
+
<PageHeader
|
|
213
|
+
actions={
|
|
214
|
+
<CursorWorkspaceHeaderActions
|
|
215
|
+
deletePending={deleteMutation.isPending}
|
|
216
|
+
recoverPending={recoverWorkspaceMutation.isPending}
|
|
217
|
+
searchInput={searchInput}
|
|
218
|
+
workspace={workspace}
|
|
219
|
+
onDeleteWorkspace={() => setPendingDelete({ kind: 'workspace', workspace })}
|
|
220
|
+
onRecoverWorkspace={() => recoverWorkspaceMutation.mutate()}
|
|
221
|
+
onSearchInputChange={setSearchInput}
|
|
222
|
+
/>
|
|
223
|
+
}
|
|
224
|
+
eyebrow="Cursor workspace"
|
|
225
|
+
subtitle="Sort by any column, export thread transcripts, delete stale records, or repair split storage buckets for this workspace."
|
|
226
|
+
title={workspace.label}
|
|
227
|
+
/>
|
|
228
|
+
|
|
229
|
+
<CursorWorkspaceRecoveryNotice workspace={workspace} />
|
|
230
|
+
|
|
231
|
+
<CursorThreadsTable
|
|
232
|
+
onDeleteThread={(thread) => setPendingDelete({ kind: 'threads', threads: [thread] })}
|
|
233
|
+
onDeleteThreads={openDeleteForSelectedThreads}
|
|
234
|
+
onExportThread={(thread) =>
|
|
235
|
+
setPendingExport({
|
|
236
|
+
composerIds: [thread.composerId],
|
|
237
|
+
label: thread.name,
|
|
238
|
+
})
|
|
239
|
+
}
|
|
240
|
+
onExportThreads={openExportForSelectedThreads}
|
|
241
|
+
threads={visibleThreads}
|
|
242
|
+
/>
|
|
243
|
+
|
|
244
|
+
<CursorWorkspaceErrors
|
|
245
|
+
deleteError={deleteMutation.isError ? deleteMutation.error : null}
|
|
246
|
+
exportError={exportMutation.isError ? exportMutation.error : null}
|
|
247
|
+
recoverError={recoverWorkspaceMutation.isError ? recoverWorkspaceMutation.error : null}
|
|
248
|
+
/>
|
|
249
|
+
|
|
250
|
+
<CursorWorkspaceDeleteDialog
|
|
251
|
+
pending={deleteMutation.isPending}
|
|
252
|
+
pendingDelete={pendingDelete}
|
|
253
|
+
onConfirm={() => {
|
|
254
|
+
if (!pendingDelete) {
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
deleteMutation.mutate(pendingDelete);
|
|
259
|
+
}}
|
|
260
|
+
onOpenChange={(open) => {
|
|
261
|
+
if (!open) {
|
|
262
|
+
setPendingDelete(null);
|
|
263
|
+
}
|
|
264
|
+
}}
|
|
265
|
+
/>
|
|
266
|
+
|
|
267
|
+
<ExportDialog
|
|
268
|
+
forceZipArchive={pendingExport ? pendingExport.composerIds.length > 1 : false}
|
|
269
|
+
open={pendingExport !== null}
|
|
270
|
+
pending={exportMutation.isPending}
|
|
271
|
+
title={pendingExport ? `Export ${pendingExport.label}` : 'Export thread'}
|
|
272
|
+
onExport={(options) => exportMutation.mutate(options)}
|
|
273
|
+
onOpenChange={(open) => {
|
|
274
|
+
if (!open) {
|
|
275
|
+
setPendingExport(null);
|
|
276
|
+
}
|
|
277
|
+
}}
|
|
278
|
+
/>
|
|
279
|
+
</div>
|
|
280
|
+
);
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
const CursorWorkspaceHeaderActions = ({
|
|
284
|
+
deletePending,
|
|
285
|
+
recoverPending,
|
|
286
|
+
searchInput,
|
|
287
|
+
workspace,
|
|
288
|
+
onDeleteWorkspace,
|
|
289
|
+
onRecoverWorkspace,
|
|
290
|
+
onSearchInputChange,
|
|
291
|
+
}: {
|
|
292
|
+
deletePending: boolean;
|
|
293
|
+
recoverPending: boolean;
|
|
294
|
+
searchInput: string;
|
|
295
|
+
workspace: CursorWorkspaceGroup;
|
|
296
|
+
onDeleteWorkspace: () => void;
|
|
297
|
+
onRecoverWorkspace: () => void;
|
|
298
|
+
onSearchInputChange: (value: string) => void;
|
|
299
|
+
}) => {
|
|
300
|
+
return (
|
|
301
|
+
<div className="flex flex-col gap-2 sm:flex-row">
|
|
302
|
+
{workspace.needsRecovery ? (
|
|
303
|
+
<Button
|
|
304
|
+
className="rounded-full"
|
|
305
|
+
disabled={recoverPending}
|
|
306
|
+
type="button"
|
|
307
|
+
variant="outline"
|
|
308
|
+
onClick={onRecoverWorkspace}
|
|
309
|
+
>
|
|
310
|
+
<RefreshCcw className="mr-2 size-4" />
|
|
311
|
+
{recoverPending ? 'Recovering...' : 'Recover'}
|
|
312
|
+
</Button>
|
|
313
|
+
) : null}
|
|
314
|
+
<Button
|
|
315
|
+
className="rounded-full"
|
|
316
|
+
disabled={deletePending}
|
|
317
|
+
type="button"
|
|
318
|
+
variant="outline"
|
|
319
|
+
onClick={onDeleteWorkspace}
|
|
320
|
+
>
|
|
321
|
+
<Trash2 className="mr-2 size-4" />
|
|
322
|
+
Delete workspace
|
|
323
|
+
</Button>
|
|
324
|
+
<ListSearchInput
|
|
325
|
+
placeholder="Search thread name, id, or mode"
|
|
326
|
+
value={searchInput}
|
|
327
|
+
onValueChange={onSearchInputChange}
|
|
328
|
+
/>
|
|
329
|
+
</div>
|
|
330
|
+
);
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
const CursorWorkspaceRecoveryNotice = ({ workspace }: { workspace: CursorWorkspaceGroup }) => {
|
|
334
|
+
if (!workspace.needsRecovery) {
|
|
335
|
+
return null;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
return (
|
|
339
|
+
<div className="rounded-xl border border-[var(--border)] bg-[var(--panel)] px-4 py-4">
|
|
340
|
+
<p className="font-medium text-sm">This workspace has threads in an older storage bucket.</p>
|
|
341
|
+
<p className="mt-1 text-[var(--muted-foreground)] text-xs">
|
|
342
|
+
Recover merges those threads into the active bucket Cursor reads now so they reappear in Chat History.
|
|
343
|
+
Quit Cursor before running it.
|
|
344
|
+
</p>
|
|
345
|
+
</div>
|
|
346
|
+
);
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
const CursorWorkspaceErrors = ({
|
|
350
|
+
deleteError,
|
|
351
|
+
exportError,
|
|
352
|
+
recoverError,
|
|
353
|
+
}: {
|
|
354
|
+
deleteError: Error | null;
|
|
355
|
+
exportError: Error | null;
|
|
356
|
+
recoverError: Error | null;
|
|
357
|
+
}) => {
|
|
358
|
+
const entries = [
|
|
359
|
+
recoverError ? recoverError.message : null,
|
|
360
|
+
deleteError ? deleteError.message : null,
|
|
361
|
+
exportError ? exportError.message : null,
|
|
362
|
+
].filter(Boolean);
|
|
363
|
+
|
|
364
|
+
if (entries.length === 0) {
|
|
365
|
+
return null;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
return (
|
|
369
|
+
<div className="space-y-1">
|
|
370
|
+
{entries.map((message) => (
|
|
371
|
+
<p className="text-[var(--destructive)] text-sm" key={message}>
|
|
372
|
+
{message}
|
|
373
|
+
</p>
|
|
374
|
+
))}
|
|
375
|
+
</div>
|
|
376
|
+
);
|
|
377
|
+
};
|
|
378
|
+
|
|
379
|
+
const CursorWorkspaceDeleteDialog = ({
|
|
380
|
+
pending,
|
|
381
|
+
pendingDelete,
|
|
382
|
+
onConfirm,
|
|
383
|
+
onOpenChange,
|
|
384
|
+
}: {
|
|
385
|
+
pending: boolean;
|
|
386
|
+
pendingDelete: PendingCursorDelete | null;
|
|
387
|
+
onConfirm: () => void;
|
|
388
|
+
onOpenChange: (open: boolean) => void;
|
|
389
|
+
}) => {
|
|
390
|
+
return (
|
|
391
|
+
<DeleteConfirmDialog
|
|
392
|
+
confirmLabel={getCursorDeleteConfirmLabel(pendingDelete, pending)}
|
|
393
|
+
description={getCursorDeleteDescription(pendingDelete)}
|
|
394
|
+
open={pendingDelete !== null}
|
|
395
|
+
title={getCursorDeleteTitle(pendingDelete)}
|
|
396
|
+
onConfirm={onConfirm}
|
|
397
|
+
onOpenChange={onOpenChange}
|
|
398
|
+
/>
|
|
399
|
+
);
|
|
400
|
+
};
|
|
401
|
+
|
|
402
|
+
export const Route = createFileRoute('/cursor/$workspaceKey')({
|
|
403
|
+
component: CursorWorkspacePage,
|
|
404
|
+
errorComponent: CursorWorkspaceErrorComponent,
|
|
405
|
+
loader: async ({ context, params }) => {
|
|
406
|
+
const workspaces = await context.queryClient.ensureQueryData(cursorWorkspacesQueryOptions());
|
|
407
|
+
findWorkspaceOrThrow(workspaces, params.workspaceKey);
|
|
408
|
+
await context.queryClient.ensureQueryData(cursorThreadsQueryOptions(params.workspaceKey));
|
|
409
|
+
},
|
|
410
|
+
pendingComponent: () => (
|
|
411
|
+
<LoadingPanel
|
|
412
|
+
description="Loading Cursor threads and workspace metadata. Larger workspaces can take a moment."
|
|
413
|
+
title="Loading workspace"
|
|
414
|
+
/>
|
|
415
|
+
),
|
|
416
|
+
});
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import type { CursorWorkspaceGroup } from '@spiracha/lib/cursor-exporter-types';
|
|
2
|
+
import { useMutation, useQueryClient, useSuspenseQuery } from '@tanstack/react-query';
|
|
3
|
+
import { createFileRoute } from '@tanstack/react-router';
|
|
4
|
+
import { useDeferredValue, useState } from 'react';
|
|
5
|
+
import { CursorWorkspacesTable } from '#/components/cursor-workspaces-table';
|
|
6
|
+
import { DeleteConfirmDialog } from '#/components/delete-confirm-dialog';
|
|
7
|
+
import { ListSearchInput } from '#/components/list-search-input';
|
|
8
|
+
import { PageHeader } from '#/components/page-header';
|
|
9
|
+
import { ReloadErrorPanel } from '#/components/reload-error-panel';
|
|
10
|
+
import { cursorWorkspacesQueryOptions } from '#/lib/cursor-queries';
|
|
11
|
+
import { deleteCursorWorkspaceFn, recoverCursorWorkspaceFn } from '#/lib/cursor-server';
|
|
12
|
+
import { matchesTextQuery } from '#/lib/text-filter';
|
|
13
|
+
|
|
14
|
+
const CursorErrorComponent = ({ error }: { error: Error }) => {
|
|
15
|
+
return <ReloadErrorPanel description={error.message} title="Failed to load Cursor workspaces" />;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const CursorPage = () => {
|
|
19
|
+
const queryClient = useQueryClient();
|
|
20
|
+
const workspaces = useSuspenseQuery(cursorWorkspacesQueryOptions()).data;
|
|
21
|
+
const [searchInput, setSearchInput] = useState('');
|
|
22
|
+
const [pendingDelete, setPendingDelete] = useState<CursorWorkspaceGroup | null>(null);
|
|
23
|
+
const deferredSearch = useDeferredValue(searchInput);
|
|
24
|
+
|
|
25
|
+
const invalidateCursorQueries = async () => {
|
|
26
|
+
await queryClient.invalidateQueries({ queryKey: ['cursor-workspaces'] });
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const recoverWorkspaceMutation = useMutation({
|
|
30
|
+
mutationFn: (workspace: CursorWorkspaceGroup) =>
|
|
31
|
+
recoverCursorWorkspaceFn({ data: { apply: true, workspaceKey: workspace.key } }),
|
|
32
|
+
onSuccess: invalidateCursorQueries,
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
const deleteWorkspaceMutation = useMutation({
|
|
36
|
+
mutationFn: (workspace: CursorWorkspaceGroup) =>
|
|
37
|
+
deleteCursorWorkspaceFn({ data: { workspaceKey: workspace.key } }),
|
|
38
|
+
onSuccess: async () => {
|
|
39
|
+
await invalidateCursorQueries();
|
|
40
|
+
setPendingDelete(null);
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const visibleWorkspaces = workspaces.filter((workspace) =>
|
|
45
|
+
matchesTextQuery(deferredSearch, [
|
|
46
|
+
workspace.label,
|
|
47
|
+
workspace.uri,
|
|
48
|
+
workspace.folders.join('\n'),
|
|
49
|
+
workspace.kind,
|
|
50
|
+
]),
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<div className="space-y-6">
|
|
55
|
+
<PageHeader
|
|
56
|
+
actions={
|
|
57
|
+
<ListSearchInput
|
|
58
|
+
placeholder="Search workspace name or path"
|
|
59
|
+
value={searchInput}
|
|
60
|
+
onValueChange={setSearchInput}
|
|
61
|
+
/>
|
|
62
|
+
}
|
|
63
|
+
eyebrow="Inventory"
|
|
64
|
+
subtitle="Workspace groups are derived from Cursor storage buckets and activity metadata. Open a workspace to inspect, export, recover, or delete its threads."
|
|
65
|
+
title="Cursor"
|
|
66
|
+
/>
|
|
67
|
+
|
|
68
|
+
<CursorWorkspacesTable
|
|
69
|
+
onDeleteWorkspace={setPendingDelete}
|
|
70
|
+
onRecoverWorkspace={(workspace) => recoverWorkspaceMutation.mutate(workspace)}
|
|
71
|
+
workspaces={visibleWorkspaces}
|
|
72
|
+
/>
|
|
73
|
+
|
|
74
|
+
{recoverWorkspaceMutation.isError ? (
|
|
75
|
+
<p className="text-[var(--destructive)] text-sm">
|
|
76
|
+
{recoverWorkspaceMutation.error instanceof Error
|
|
77
|
+
? recoverWorkspaceMutation.error.message
|
|
78
|
+
: 'Workspace recovery failed'}
|
|
79
|
+
</p>
|
|
80
|
+
) : null}
|
|
81
|
+
|
|
82
|
+
{deleteWorkspaceMutation.isError ? (
|
|
83
|
+
<p className="text-[var(--destructive)] text-sm">
|
|
84
|
+
{deleteWorkspaceMutation.error instanceof Error
|
|
85
|
+
? deleteWorkspaceMutation.error.message
|
|
86
|
+
: 'Workspace deletion failed'}
|
|
87
|
+
</p>
|
|
88
|
+
) : null}
|
|
89
|
+
|
|
90
|
+
<DeleteConfirmDialog
|
|
91
|
+
confirmLabel={deleteWorkspaceMutation.isPending ? 'Deleting...' : 'Delete workspace'}
|
|
92
|
+
description={
|
|
93
|
+
pendingDelete
|
|
94
|
+
? `Permanently delete every thread for "${pendingDelete.label}" from Cursor's database and remove any on-disk transcript directories. Quit Cursor first. This cannot be undone.`
|
|
95
|
+
: ''
|
|
96
|
+
}
|
|
97
|
+
open={pendingDelete !== null}
|
|
98
|
+
title="Delete Cursor workspace?"
|
|
99
|
+
onConfirm={() => {
|
|
100
|
+
if (!pendingDelete) {
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
deleteWorkspaceMutation.mutate(pendingDelete);
|
|
105
|
+
}}
|
|
106
|
+
onOpenChange={(open) => {
|
|
107
|
+
if (!open) {
|
|
108
|
+
setPendingDelete(null);
|
|
109
|
+
}
|
|
110
|
+
}}
|
|
111
|
+
/>
|
|
112
|
+
</div>
|
|
113
|
+
);
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
export const Route = createFileRoute('/cursor/')({
|
|
117
|
+
component: CursorPage,
|
|
118
|
+
errorComponent: CursorErrorComponent,
|
|
119
|
+
loader: ({ context }) => context.queryClient.ensureQueryData(cursorWorkspacesQueryOptions()),
|
|
120
|
+
});
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { useSuspenseQuery } from '@tanstack/react-query';
|
|
2
|
+
import { createFileRoute, Link } from '@tanstack/react-router';
|
|
3
|
+
import { MetricCard } from '#/components/metric-card';
|
|
4
|
+
import { PageHeader } from '#/components/page-header';
|
|
5
|
+
import { RecentThreadsList } from '#/components/recent-threads-list';
|
|
6
|
+
import { dashboardQueryOptions } from '#/lib/codex-queries';
|
|
7
|
+
import { formatNumber, formatTokens } from '#/lib/formatters';
|
|
8
|
+
|
|
9
|
+
export const Route = createFileRoute('/')({
|
|
10
|
+
component: DashboardPage,
|
|
11
|
+
loader: ({ context }) => context.queryClient.ensureQueryData(dashboardQueryOptions()),
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
function DashboardErrorComponent({ error }: { error: Error }) {
|
|
15
|
+
const isSqlite = error.message.includes('unable to open database') || error.message.includes('database is locked');
|
|
16
|
+
return (
|
|
17
|
+
<div className="rounded-xl border border-[var(--border)] bg-[var(--panel)] px-6 py-10 text-center">
|
|
18
|
+
<p className="font-medium text-[var(--destructive)] text-sm">
|
|
19
|
+
{isSqlite ? 'Database unavailable' : 'Failed to load dashboard'}
|
|
20
|
+
</p>
|
|
21
|
+
<p className="mt-2 text-[var(--muted-foreground)] text-sm">
|
|
22
|
+
{isSqlite ? 'Codex may have an exclusive lock on the database. Reload to retry.' : error.message}
|
|
23
|
+
</p>
|
|
24
|
+
<button
|
|
25
|
+
className="mt-4 text-[var(--accent)] text-sm underline-offset-2 hover:underline"
|
|
26
|
+
type="button"
|
|
27
|
+
onClick={() => window.location.reload()}
|
|
28
|
+
>
|
|
29
|
+
Reload
|
|
30
|
+
</button>
|
|
31
|
+
</div>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
Route.update({ errorComponent: DashboardErrorComponent });
|
|
36
|
+
|
|
37
|
+
function DashboardPage() {
|
|
38
|
+
const dashboard = useSuspenseQuery(dashboardQueryOptions()).data;
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<div className="space-y-6">
|
|
42
|
+
<PageHeader
|
|
43
|
+
actions={
|
|
44
|
+
<img
|
|
45
|
+
alt="Spiracha icon"
|
|
46
|
+
className="size-16 shrink-0 rounded-2xl border border-[var(--border)] bg-white/95 p-2 shadow-[var(--panel-shadow)]"
|
|
47
|
+
src="/icon.svg"
|
|
48
|
+
/>
|
|
49
|
+
}
|
|
50
|
+
eyebrow="Overview"
|
|
51
|
+
subtitle="A compact readout of local Codex activity, project distribution, and recent thread movement."
|
|
52
|
+
title="Dashboard"
|
|
53
|
+
/>
|
|
54
|
+
|
|
55
|
+
<div className="grid gap-3 md:grid-cols-2 xl:grid-cols-4">
|
|
56
|
+
<MetricCard label="Threads" value={formatNumber(dashboard.totalThreads)} />
|
|
57
|
+
<MetricCard label="Projects" value={formatNumber(dashboard.totalProjects)} />
|
|
58
|
+
<MetricCard label="Tokens" value={formatTokens(dashboard.totalTokens)} />
|
|
59
|
+
<MetricCard
|
|
60
|
+
helper={`${formatNumber(dashboard.archivedThreads)} archived`}
|
|
61
|
+
label="Active"
|
|
62
|
+
value={formatNumber(dashboard.activeThreads)}
|
|
63
|
+
/>
|
|
64
|
+
</div>
|
|
65
|
+
|
|
66
|
+
<div className="grid min-w-0 gap-4 xl:grid-cols-[1.4fr_1fr]">
|
|
67
|
+
<section className="min-w-0 overflow-hidden rounded-[1.8rem] border border-[var(--border)] bg-[var(--panel)] p-5 shadow-[var(--panel-shadow)]">
|
|
68
|
+
<div className="flex items-center justify-between gap-3">
|
|
69
|
+
<div className="min-w-0">
|
|
70
|
+
<p className="font-semibold text-sm">Recent threads</p>
|
|
71
|
+
<p className="mt-0.5 text-[var(--muted-foreground)] text-xs">
|
|
72
|
+
Most recently updated threads across the local Codex database.
|
|
73
|
+
</p>
|
|
74
|
+
</div>
|
|
75
|
+
<Link className="shrink-0 font-medium text-[var(--accent)] text-sm" to="/codex">
|
|
76
|
+
View Codex
|
|
77
|
+
</Link>
|
|
78
|
+
</div>
|
|
79
|
+
<RecentThreadsList threads={dashboard.recentThreads} />
|
|
80
|
+
</section>
|
|
81
|
+
|
|
82
|
+
<div className="grid min-w-0 gap-4">
|
|
83
|
+
<section className="min-w-0 overflow-hidden rounded-[1.8rem] border border-[var(--border)] bg-[var(--panel)] p-5 shadow-[var(--panel-shadow)]">
|
|
84
|
+
<p className="font-semibold text-sm">Top projects by thread count</p>
|
|
85
|
+
<div className="mt-3 space-y-2">
|
|
86
|
+
{dashboard.topProjectsByThreadCount.map((project) => (
|
|
87
|
+
<Link
|
|
88
|
+
key={project.name}
|
|
89
|
+
className="flex items-center justify-between gap-3 rounded-lg border border-[var(--border)] bg-[var(--panel-secondary)] px-3.5 py-2.5 text-sm"
|
|
90
|
+
params={{ project: project.name }}
|
|
91
|
+
to="/codex/$project"
|
|
92
|
+
>
|
|
93
|
+
<span className="truncate">{project.name}</span>
|
|
94
|
+
<span className="shrink-0 font-mono text-[var(--muted-foreground)]">
|
|
95
|
+
{formatNumber(project.threadCount)}
|
|
96
|
+
</span>
|
|
97
|
+
</Link>
|
|
98
|
+
))}
|
|
99
|
+
</div>
|
|
100
|
+
</section>
|
|
101
|
+
|
|
102
|
+
<section className="min-w-0 overflow-hidden rounded-[1.8rem] border border-[var(--border)] bg-[var(--panel)] p-5 shadow-[var(--panel-shadow)]">
|
|
103
|
+
<p className="font-semibold text-sm">Top projects by tokens</p>
|
|
104
|
+
<div className="mt-3 space-y-2">
|
|
105
|
+
{dashboard.topProjectsByTokens.map((project) => (
|
|
106
|
+
<Link
|
|
107
|
+
key={project.name}
|
|
108
|
+
className="flex items-center justify-between gap-3 rounded-lg border border-[var(--border)] bg-[var(--panel-secondary)] px-3.5 py-2.5 text-sm"
|
|
109
|
+
params={{ project: project.name }}
|
|
110
|
+
to="/codex/$project"
|
|
111
|
+
>
|
|
112
|
+
<span className="truncate">{project.name}</span>
|
|
113
|
+
<span className="shrink-0 font-mono text-[var(--muted-foreground)]">
|
|
114
|
+
{formatTokens(project.totalTokens)}
|
|
115
|
+
</span>
|
|
116
|
+
</Link>
|
|
117
|
+
))}
|
|
118
|
+
</div>
|
|
119
|
+
</section>
|
|
120
|
+
</div>
|
|
121
|
+
</div>
|
|
122
|
+
</div>
|
|
123
|
+
);
|
|
124
|
+
}
|