spiracha 2.0.0 → 2.2.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 +27 -12
- package/README.md +42 -19
- package/apps/ui/AGENTS.md +18 -10
- package/apps/ui/README.md +36 -11
- package/apps/ui/src/components/analytics-breakdowns.tsx +106 -0
- package/apps/ui/src/components/antigravity-conversations-table.tsx +51 -11
- package/apps/ui/src/components/antigravity-workspaces-table.tsx +1 -1
- package/apps/ui/src/components/app-shell.tsx +46 -3
- package/apps/ui/src/components/breadcrumbs.tsx +21 -3
- package/apps/ui/src/components/claude-code-sessions-table.tsx +42 -6
- package/apps/ui/src/components/claude-code-workspaces-table.tsx +3 -1
- package/apps/ui/src/components/cursor-threads-table.tsx +10 -47
- package/apps/ui/src/components/data-table.tsx +23 -5
- package/apps/ui/src/components/delete-confirm-dialog.tsx +4 -3
- package/apps/ui/src/components/export-dialog.tsx +79 -74
- package/apps/ui/src/components/grok-sessions-table.tsx +146 -0
- package/apps/ui/src/components/grok-workspaces-table.tsx +53 -0
- package/apps/ui/src/components/kiro-sessions-table.tsx +40 -4
- package/apps/ui/src/components/opencode-sessions-table.tsx +40 -4
- package/apps/ui/src/components/page-header.tsx +15 -9
- package/apps/ui/src/components/projects-table.tsx +4 -1
- package/apps/ui/src/components/qoder-sessions-table.tsx +23 -2
- package/apps/ui/src/components/qoder-workspaces-table.tsx +5 -1
- package/apps/ui/src/components/route-error-panel.tsx +13 -0
- package/apps/ui/src/components/selection-actions-toolbar.tsx +80 -0
- package/apps/ui/src/components/thread-goals-panel.tsx +50 -0
- package/apps/ui/src/components/thread-tools-panel.tsx +142 -0
- package/apps/ui/src/components/threads-table.tsx +13 -47
- package/apps/ui/src/components/transcript-view.tsx +148 -52
- package/apps/ui/src/components/ui/select.tsx +1 -1
- package/apps/ui/src/lib/antigravity-conversation-state.ts +17 -4
- package/apps/ui/src/lib/antigravity-server.ts +209 -11
- package/apps/ui/src/lib/antigravity-transcript-events.ts +57 -73
- package/apps/ui/src/lib/claude-code-queries.ts +8 -0
- package/apps/ui/src/lib/claude-code-server.ts +185 -7
- package/apps/ui/src/lib/claude-code-transcript-events.ts +8 -1
- package/apps/ui/src/lib/codex-queries.ts +16 -0
- package/apps/ui/src/lib/codex-server.ts +134 -52
- package/apps/ui/src/lib/cursor-server.ts +68 -53
- package/apps/ui/src/lib/cursor-transcript-events.ts +11 -86
- package/apps/ui/src/lib/delete-batch.ts +52 -0
- package/apps/ui/src/lib/error-presentation.ts +43 -0
- package/apps/ui/src/lib/export-mutation.ts +20 -0
- package/apps/ui/src/lib/export-options.ts +15 -0
- package/apps/ui/src/lib/formatters.ts +3 -5
- package/apps/ui/src/lib/grok-queries.ts +22 -0
- package/apps/ui/src/lib/grok-server.ts +168 -0
- package/apps/ui/src/lib/grok-transcript-events.ts +149 -0
- package/apps/ui/src/lib/kiro-server.ts +97 -7
- package/apps/ui/src/lib/mutation-error.ts +15 -0
- package/apps/ui/src/lib/opencode-server.ts +96 -7
- package/apps/ui/src/lib/path-utils.ts +1 -1
- package/apps/ui/src/lib/qoder-queries.ts +15 -0
- package/apps/ui/src/lib/qoder-server.ts +67 -11
- package/apps/ui/src/lib/qoder-transcript-events.ts +1 -1
- package/apps/ui/src/lib/route-search.ts +114 -0
- package/apps/ui/src/lib/route-state-reset.tsx +10 -0
- package/apps/ui/src/lib/settings-server.ts +33 -0
- package/apps/ui/src/lib/settings-store.tsx +82 -38
- package/apps/ui/src/lib/settings.ts +65 -0
- package/apps/ui/src/lib/source-session-export-server.ts +86 -3
- package/apps/ui/src/lib/thread-metadata.ts +22 -0
- package/apps/ui/src/lib/thread-transcript-load.ts +15 -0
- package/apps/ui/src/lib/thread-transcript-stats.ts +10 -1
- package/apps/ui/src/lib/workspace-delete-navigation.ts +20 -0
- package/apps/ui/src/routeTree.gen.ts +341 -234
- package/apps/ui/src/routes/__root.tsx +12 -15
- package/apps/ui/src/routes/analytics.tsx +10 -61
- package/apps/ui/src/routes/antigravity-conversations.$conversationId.tsx +149 -29
- package/apps/ui/src/routes/antigravity.$workspaceKey.tsx +262 -42
- package/apps/ui/src/routes/antigravity.index.tsx +2 -2
- package/apps/ui/src/routes/api.v1.conversations.$source.$id.ts +4 -0
- package/apps/ui/src/routes/api.v1.conversations.delete.ts +12 -0
- package/apps/ui/src/routes/api.v1.conversations.export.ts +12 -0
- package/apps/ui/src/routes/claude-code-sessions.$sessionId.tsx +168 -28
- package/apps/ui/src/routes/claude-code.$workspaceKey.tsx +179 -45
- package/apps/ui/src/routes/claude-code.index.tsx +2 -2
- package/apps/ui/src/routes/codex.$project.tsx +33 -31
- package/apps/ui/src/routes/codex.index.tsx +5 -10
- package/apps/ui/src/routes/cursor-threads.$composerId.tsx +31 -18
- package/apps/ui/src/routes/cursor.$workspaceKey.tsx +44 -28
- package/apps/ui/src/routes/cursor.index.tsx +5 -2
- package/apps/ui/src/routes/grok-sessions.$sessionId.tsx +401 -0
- package/apps/ui/src/routes/grok.$workspaceKey.tsx +295 -0
- package/apps/ui/src/routes/grok.index.tsx +48 -0
- package/apps/ui/src/routes/index.tsx +2 -18
- package/apps/ui/src/routes/kiro-sessions.$sessionId.tsx +91 -27
- package/apps/ui/src/routes/kiro.$workspaceKey.tsx +213 -51
- package/apps/ui/src/routes/kiro.index.tsx +2 -2
- package/apps/ui/src/routes/opencode-sessions.$sessionId.tsx +108 -29
- package/apps/ui/src/routes/opencode.$workspaceKey.tsx +231 -55
- package/apps/ui/src/routes/opencode.index.tsx +6 -2
- package/apps/ui/src/routes/qoder-sessions.$sessionId.tsx +19 -15
- package/apps/ui/src/routes/qoder.$workspaceKey.tsx +89 -48
- package/apps/ui/src/routes/qoder.index.tsx +2 -2
- package/apps/ui/src/routes/threads.$threadId.tsx +620 -105
- package/apps/ui/vite.config.ts +22 -12
- package/bin/spiracha.ts +28 -1
- package/package.json +37 -27
- package/src/client.ts +183 -5
- package/src/lib/antigravity-db.ts +424 -94
- package/src/lib/antigravity-exporter-types.ts +3 -0
- package/src/lib/antigravity-keychain.ts +7 -8
- package/src/lib/antigravity-projects.ts +201 -0
- package/src/lib/antigravity-transcript-phase.ts +47 -0
- package/src/lib/claude-code-db.ts +534 -42
- package/src/lib/claude-code-exporter-types.ts +5 -0
- package/src/lib/claude-code-transcript-phase.ts +60 -1
- package/src/lib/claude-code-transcript.ts +8 -5
- package/src/lib/codex-analytics.ts +32 -5
- package/src/lib/codex-browser-db.ts +540 -137
- package/src/lib/codex-browser-export.ts +18 -27
- package/src/lib/codex-browser-types.ts +21 -1
- package/src/lib/codex-thread-cache.ts +106 -23
- package/src/lib/codex-thread-parser.ts +149 -55
- package/src/lib/codex-thread-recovery.ts +38 -11
- package/src/lib/codex-transcript-filter.ts +31 -0
- package/src/lib/codex-transcript-renderer.ts +87 -79
- package/src/lib/concurrency.ts +56 -3
- package/src/lib/conversation-api.ts +410 -27
- package/src/lib/conversation-data/adapter-helpers.ts +25 -3
- package/src/lib/conversation-data/antigravity-adapter.ts +70 -26
- package/src/lib/conversation-data/claude-code-adapter.ts +69 -12
- package/src/lib/conversation-data/codex-adapter.ts +108 -73
- package/src/lib/conversation-data/cursor-adapter.ts +77 -29
- package/src/lib/conversation-data/grok-adapter.ts +223 -0
- package/src/lib/conversation-data/index.ts +105 -11
- package/src/lib/conversation-data/kiro-adapter.ts +59 -12
- package/src/lib/conversation-data/message-selector.ts +2 -5
- package/src/lib/conversation-data/opencode-adapter.ts +67 -17
- package/src/lib/conversation-data/path-match.ts +1 -13
- package/src/lib/conversation-data/qoder-adapter.ts +67 -37
- package/src/lib/conversation-data/types.ts +43 -0
- package/src/lib/conversation-zip-export.ts +75 -0
- package/src/lib/cursor-db.ts +68 -28
- package/src/lib/cursor-id.ts +17 -0
- package/src/lib/cursor-recovery.ts +146 -35
- package/src/lib/cursor-transcript-phase.ts +40 -0
- package/src/lib/cursor-transcript.ts +21 -5
- package/src/lib/grok-db.ts +1068 -0
- package/src/lib/grok-exporter-types.ts +110 -0
- package/src/lib/grok-transcript-phase.ts +61 -0
- package/src/lib/grok-transcript.ts +167 -0
- package/src/lib/kiro-db.ts +187 -43
- package/src/lib/kiro-transcript.ts +0 -4
- package/src/lib/model-label.ts +11 -3
- package/src/lib/opencode-db.ts +599 -60
- package/src/lib/opencode-think-tags.ts +17 -3
- package/src/lib/opencode-transcript.ts +8 -5
- package/src/lib/portable-path.ts +9 -0
- package/src/lib/qoder-acp-client.ts +11 -2
- package/src/lib/qoder-db.ts +51 -17
- package/src/lib/qoder-transcript-phase.ts +64 -0
- package/src/lib/qoder-transcript.ts +30 -2
- package/src/lib/shared.ts +63 -15
- package/src/lib/sqlite-error.ts +31 -5
- package/src/lib/transcript-load-limiter.ts +87 -0
- package/src/lib/ui-cache.ts +185 -24
- package/src/lib/ui-export-archive.ts +58 -39
- package/src/lib/ui-export-files.ts +75 -15
- package/src/lib/ui-export-zip.ts +40 -0
- package/apps/ui/package.json +0 -65
|
@@ -8,7 +8,7 @@ import { ExportDialog } from '#/components/export-dialog';
|
|
|
8
8
|
import { ListSearchInput } from '#/components/list-search-input';
|
|
9
9
|
import { LoadingPanel } from '#/components/loading-panel';
|
|
10
10
|
import { PageHeader } from '#/components/page-header';
|
|
11
|
-
import {
|
|
11
|
+
import { RouteErrorPanel } from '#/components/route-error-panel';
|
|
12
12
|
import { ThreadsTable } from '#/components/threads-table';
|
|
13
13
|
import { Button } from '#/components/ui/button';
|
|
14
14
|
import { projectThreadsQueryOptions } from '#/lib/codex-queries';
|
|
@@ -20,6 +20,8 @@ import {
|
|
|
20
20
|
recoverProjectThreadsFn,
|
|
21
21
|
} from '#/lib/codex-server';
|
|
22
22
|
import { downloadTextFile, downloadUrlFile } from '#/lib/download';
|
|
23
|
+
import { createExportSelectionMutationInput, type ExportSelectionMutationInput } from '#/lib/export-mutation';
|
|
24
|
+
import { getMutationErrorMessage } from '#/lib/mutation-error';
|
|
23
25
|
import { parseTextQuerySearch, withTextQuerySearch } from '#/lib/route-search';
|
|
24
26
|
import { useSettings } from '#/lib/settings-store';
|
|
25
27
|
import { matchesTextQuery } from '#/lib/text-filter';
|
|
@@ -84,15 +86,7 @@ export const Route = createFileRoute('/codex/$project')({
|
|
|
84
86
|
});
|
|
85
87
|
|
|
86
88
|
function ProjectDetailErrorComponent({ error }: { error: Error }) {
|
|
87
|
-
|
|
88
|
-
return (
|
|
89
|
-
<ReloadErrorPanel
|
|
90
|
-
description={
|
|
91
|
-
isSqlite ? 'Codex may have an exclusive lock on the database. Reload to retry.' : error.message
|
|
92
|
-
}
|
|
93
|
-
title={isSqlite ? 'Database unavailable' : 'Failed to load Codex project'}
|
|
94
|
-
/>
|
|
95
|
-
);
|
|
89
|
+
return <RouteErrorPanel error={error} title="Failed to load Codex project" />;
|
|
96
90
|
}
|
|
97
91
|
|
|
98
92
|
function ProjectThreadsLoadingState({ project }: { project: string }) {
|
|
@@ -112,6 +106,14 @@ function ProjectThreadsLoadingState({ project }: { project: string }) {
|
|
|
112
106
|
|
|
113
107
|
const toError = (error: unknown) => (error instanceof Error ? error : new Error(String(error)));
|
|
114
108
|
|
|
109
|
+
const getThreadExportErrorMessage = (error: unknown): string | null => {
|
|
110
|
+
if (!error) {
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return error instanceof Error ? error.message : 'Thread export failed';
|
|
115
|
+
};
|
|
116
|
+
|
|
115
117
|
function ProjectDetailPage() {
|
|
116
118
|
const navigate = useNavigate({ from: Route.fullPath });
|
|
117
119
|
const params = Route.useParams();
|
|
@@ -168,39 +170,29 @@ function ProjectDetailPage() {
|
|
|
168
170
|
});
|
|
169
171
|
|
|
170
172
|
const exportThreadMutation = useMutation({
|
|
171
|
-
mutationFn: async (options: {
|
|
172
|
-
includeCommentary: boolean;
|
|
173
|
-
includeTools: boolean;
|
|
174
|
-
includeMetadata: boolean;
|
|
175
|
-
outputFormat: 'md' | 'txt';
|
|
176
|
-
zipArchive: boolean;
|
|
177
|
-
}) => {
|
|
178
|
-
if (!pendingExport) {
|
|
179
|
-
throw new Error('No thread selected for export');
|
|
180
|
-
}
|
|
181
|
-
|
|
173
|
+
mutationFn: async ({ ids, options }: ExportSelectionMutationInput) => {
|
|
182
174
|
console.info('[spiracha:export-ui] request', {
|
|
183
175
|
outputFormat: options.outputFormat,
|
|
184
176
|
project,
|
|
185
|
-
selectedThreadCount:
|
|
186
|
-
selectedThreadIds:
|
|
177
|
+
selectedThreadCount: ids.length,
|
|
178
|
+
selectedThreadIds: ids,
|
|
187
179
|
zipArchive: options.zipArchive,
|
|
188
180
|
});
|
|
189
181
|
|
|
190
182
|
const download =
|
|
191
|
-
|
|
183
|
+
ids.length === 1
|
|
192
184
|
? await exportThreadFn({
|
|
193
185
|
data: {
|
|
194
186
|
...options,
|
|
195
187
|
...settings,
|
|
196
|
-
threadId:
|
|
188
|
+
threadId: ids[0]!,
|
|
197
189
|
},
|
|
198
190
|
})
|
|
199
191
|
: await exportThreadsFn({
|
|
200
192
|
data: {
|
|
201
193
|
...options,
|
|
202
194
|
...settings,
|
|
203
|
-
threadIds:
|
|
195
|
+
threadIds: [...ids],
|
|
204
196
|
},
|
|
205
197
|
});
|
|
206
198
|
|
|
@@ -209,7 +201,7 @@ function ProjectDetailPage() {
|
|
|
209
201
|
fileName: download.fileName,
|
|
210
202
|
mode: download.mode,
|
|
211
203
|
project,
|
|
212
|
-
selectedThreadCount:
|
|
204
|
+
selectedThreadCount: ids.length,
|
|
213
205
|
});
|
|
214
206
|
|
|
215
207
|
if (download.mode === 'download') {
|
|
@@ -219,12 +211,12 @@ function ProjectDetailPage() {
|
|
|
219
211
|
|
|
220
212
|
await downloadUrlFile(download.fileName, download.downloadUrl);
|
|
221
213
|
},
|
|
222
|
-
onError: (error) => {
|
|
214
|
+
onError: (error, variables) => {
|
|
223
215
|
console.error('[spiracha:export-ui] failed', {
|
|
224
216
|
error: error instanceof Error ? error.message : String(error),
|
|
225
217
|
project,
|
|
226
|
-
selectedThreadCount:
|
|
227
|
-
selectedThreadIds:
|
|
218
|
+
selectedThreadCount: variables.ids.length,
|
|
219
|
+
selectedThreadIds: variables.ids,
|
|
228
220
|
});
|
|
229
221
|
},
|
|
230
222
|
onSuccess: () => {
|
|
@@ -354,6 +346,7 @@ function ProjectDetailPage() {
|
|
|
354
346
|
confirmLabel={getDeleteConfirmLabel(pendingDelete, deleteThreadMutation.isPending)}
|
|
355
347
|
defaultDeleteSessionFiles
|
|
356
348
|
description={getDeleteDescription(pendingDelete)}
|
|
349
|
+
errorMessage={getMutationErrorMessage(deleteThreadMutation.error, 'Thread delete failed')}
|
|
357
350
|
open={pendingDelete !== null}
|
|
358
351
|
showDeleteSessionFilesOption
|
|
359
352
|
title={getDeleteTitle(pendingDelete)}
|
|
@@ -369,19 +362,28 @@ function ProjectDetailPage() {
|
|
|
369
362
|
onOpenChange={(open) => {
|
|
370
363
|
if (!open) {
|
|
371
364
|
setPendingDelete(null);
|
|
365
|
+
deleteThreadMutation.reset();
|
|
372
366
|
}
|
|
373
367
|
}}
|
|
374
368
|
/>
|
|
375
369
|
|
|
376
370
|
<ExportDialog
|
|
371
|
+
errorMessage={getThreadExportErrorMessage(exportThreadMutation.error)}
|
|
377
372
|
forceZipArchive={shouldForceZipArchive(pendingExport)}
|
|
378
373
|
open={pendingExport !== null}
|
|
379
374
|
pending={exportThreadMutation.isPending}
|
|
380
375
|
title={pendingExport ? `Export ${pendingExport.threadLabel}` : 'Export thread'}
|
|
381
|
-
onExport={(options) =>
|
|
376
|
+
onExport={(options) => {
|
|
377
|
+
if (pendingExport) {
|
|
378
|
+
exportThreadMutation.mutate(
|
|
379
|
+
createExportSelectionMutationInput(pendingExport.threadIds, options),
|
|
380
|
+
);
|
|
381
|
+
}
|
|
382
|
+
}}
|
|
382
383
|
onOpenChange={(open) => {
|
|
383
384
|
if (!open) {
|
|
384
385
|
setPendingExport(null);
|
|
386
|
+
exportThreadMutation.reset();
|
|
385
387
|
}
|
|
386
388
|
}}
|
|
387
389
|
/>
|
|
@@ -7,9 +7,10 @@ import { ListSearchInput } from '#/components/list-search-input';
|
|
|
7
7
|
import { PageHeader } from '#/components/page-header';
|
|
8
8
|
import { ProjectsLoadingState } from '#/components/projects-loading-state';
|
|
9
9
|
import { ProjectsTable } from '#/components/projects-table';
|
|
10
|
-
import {
|
|
10
|
+
import { RouteErrorPanel } from '#/components/route-error-panel';
|
|
11
11
|
import { projectsQueryOptions } from '#/lib/codex-queries';
|
|
12
12
|
import { deleteProjectFn } from '#/lib/codex-server';
|
|
13
|
+
import { getMutationErrorMessage } from '#/lib/mutation-error';
|
|
13
14
|
import { parseTextQuerySearch, withTextQuerySearch } from '#/lib/route-search';
|
|
14
15
|
import { matchesTextQuery } from '#/lib/text-filter';
|
|
15
16
|
|
|
@@ -22,15 +23,7 @@ export const Route = createFileRoute('/codex/')({
|
|
|
22
23
|
});
|
|
23
24
|
|
|
24
25
|
function ProjectsErrorComponent({ error }: { error: Error }) {
|
|
25
|
-
|
|
26
|
-
return (
|
|
27
|
-
<ReloadErrorPanel
|
|
28
|
-
description={
|
|
29
|
-
isSqlite ? 'Codex may have an exclusive lock on the database. Reload to retry.' : error.message
|
|
30
|
-
}
|
|
31
|
-
title={isSqlite ? 'Database unavailable' : 'Failed to load Codex inventory'}
|
|
32
|
-
/>
|
|
33
|
-
);
|
|
26
|
+
return <RouteErrorPanel error={error} title="Failed to load Codex inventory" />;
|
|
34
27
|
}
|
|
35
28
|
|
|
36
29
|
function ProjectsPage() {
|
|
@@ -93,6 +86,7 @@ function ProjectsPage() {
|
|
|
93
86
|
? `Delete ${pendingDelete.threadCount} thread records for the derived project "${pendingDelete.name}" from the Codex database. Enable Delete Session files to remove the rollout JSONL files too.`
|
|
94
87
|
: ''
|
|
95
88
|
}
|
|
89
|
+
errorMessage={getMutationErrorMessage(deleteProjectMutation.error, 'Project delete failed')}
|
|
96
90
|
open={pendingDelete !== null}
|
|
97
91
|
showDeleteSessionFilesOption
|
|
98
92
|
title="Delete Codex project?"
|
|
@@ -108,6 +102,7 @@ function ProjectsPage() {
|
|
|
108
102
|
onOpenChange={(open) => {
|
|
109
103
|
if (!open) {
|
|
110
104
|
setPendingDelete(null);
|
|
105
|
+
deleteProjectMutation.reset();
|
|
111
106
|
}
|
|
112
107
|
}}
|
|
113
108
|
/>
|
|
@@ -11,27 +11,22 @@ import { LoadingPanel } from '#/components/loading-panel';
|
|
|
11
11
|
import { MetadataSection } from '#/components/metadata-section';
|
|
12
12
|
import { MetricCard } from '#/components/metric-card';
|
|
13
13
|
import { PageHeader } from '#/components/page-header';
|
|
14
|
-
import {
|
|
15
|
-
import { TranscriptView } from '#/components/transcript-view';
|
|
14
|
+
import { RouteErrorPanel } from '#/components/route-error-panel';
|
|
15
|
+
import { DEFAULT_SHOW_USER_MESSAGES, TranscriptView } from '#/components/transcript-view';
|
|
16
16
|
import { Button } from '#/components/ui/button';
|
|
17
17
|
import { Checkbox } from '#/components/ui/checkbox';
|
|
18
18
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '#/components/ui/tabs';
|
|
19
|
-
import { cursorThreadDetailQueryOptions } from '#/lib/cursor-queries';
|
|
19
|
+
import { cursorThreadDetailQueryOptions, cursorWorkspacesQueryOptions } from '#/lib/cursor-queries';
|
|
20
20
|
import { deleteCursorThreadsFn, exportCursorThreadFn, type getCursorThreadDetailFn } from '#/lib/cursor-server';
|
|
21
21
|
import { cursorTranscriptToThreadEvents, getCursorThreadTranscriptStats } from '#/lib/cursor-transcript-events';
|
|
22
22
|
import { downloadTextFile, downloadUrlFile } from '#/lib/download';
|
|
23
|
+
import type { ExportDialogOptions } from '#/lib/export-options';
|
|
23
24
|
import { formatBytes, formatDateTime, formatList, formatNumber } from '#/lib/formatters';
|
|
25
|
+
import { RouteStateResetBoundary } from '#/lib/route-state-reset';
|
|
26
|
+
import { shouldNavigateToSourceIndexAfterDelete } from '#/lib/workspace-delete-navigation';
|
|
24
27
|
|
|
25
28
|
type CursorThreadDetail = Awaited<ReturnType<typeof getCursorThreadDetailFn>>;
|
|
26
29
|
|
|
27
|
-
type ExportDialogOptions = {
|
|
28
|
-
includeCommentary: boolean;
|
|
29
|
-
includeMetadata: boolean;
|
|
30
|
-
includeTools: boolean;
|
|
31
|
-
outputFormat: 'md' | 'txt';
|
|
32
|
-
zipArchive: boolean;
|
|
33
|
-
};
|
|
34
|
-
|
|
35
30
|
type TranscriptControlsProps = {
|
|
36
31
|
rawJsonDisabled?: boolean;
|
|
37
32
|
showCommentary: boolean;
|
|
@@ -208,7 +203,7 @@ const CursorThreadRawPanels = ({ detail, events }: { detail: CursorThreadDetail;
|
|
|
208
203
|
};
|
|
209
204
|
|
|
210
205
|
const CursorThreadDetailErrorComponent = ({ error }: { error: Error }) => {
|
|
211
|
-
return <
|
|
206
|
+
return <RouteErrorPanel error={error} title="Failed to load Cursor thread" />;
|
|
212
207
|
};
|
|
213
208
|
|
|
214
209
|
const CursorThreadDetailPage = () => {
|
|
@@ -221,7 +216,7 @@ const CursorThreadDetailPage = () => {
|
|
|
221
216
|
const [showCommentary, setShowCommentary] = useState(false);
|
|
222
217
|
const [showExtraEvents, setShowExtraEvents] = useState(false);
|
|
223
218
|
const [showRawJson, setShowRawJson] = useState(false);
|
|
224
|
-
const [showUserMessages, setShowUserMessages] = useState(
|
|
219
|
+
const [showUserMessages, setShowUserMessages] = useState(DEFAULT_SHOW_USER_MESSAGES);
|
|
225
220
|
const transcriptEvents = useMemo(
|
|
226
221
|
() => (detail.transcript ? cursorTranscriptToThreadEvents(detail.transcript) : []),
|
|
227
222
|
[detail.transcript],
|
|
@@ -231,15 +226,26 @@ const CursorThreadDetailPage = () => {
|
|
|
231
226
|
const deleteThreadMutation = useMutation({
|
|
232
227
|
mutationFn: () => deleteCursorThreadsFn({ data: { composerIds: [detail.thread.composerId] } }),
|
|
233
228
|
onSuccess: async () => {
|
|
234
|
-
await navigate({
|
|
235
|
-
params: { workspaceKey: detail.thread.workspaceKey },
|
|
236
|
-
to: '/cursor/$workspaceKey',
|
|
237
|
-
});
|
|
238
229
|
await Promise.all([
|
|
239
230
|
queryClient.invalidateQueries({ queryKey: ['cursor-thread', detail.thread.composerId] }),
|
|
240
231
|
queryClient.invalidateQueries({ queryKey: ['cursor-threads', detail.thread.workspaceKey] }),
|
|
241
232
|
queryClient.invalidateQueries({ queryKey: ['cursor-workspaces'] }),
|
|
242
233
|
]);
|
|
234
|
+
const workspaces = await queryClient.fetchQuery(cursorWorkspacesQueryOptions());
|
|
235
|
+
if (
|
|
236
|
+
shouldNavigateToSourceIndexAfterDelete(
|
|
237
|
+
workspaces,
|
|
238
|
+
detail.thread.workspaceKey,
|
|
239
|
+
(workspace) => workspace.key,
|
|
240
|
+
)
|
|
241
|
+
) {
|
|
242
|
+
await navigate({ to: '/cursor' });
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
await navigate({
|
|
246
|
+
params: { workspaceKey: detail.thread.workspaceKey },
|
|
247
|
+
to: '/cursor/$workspaceKey',
|
|
248
|
+
});
|
|
243
249
|
},
|
|
244
250
|
});
|
|
245
251
|
|
|
@@ -422,7 +428,14 @@ const CursorThreadDetailPage = () => {
|
|
|
422
428
|
};
|
|
423
429
|
|
|
424
430
|
export const Route = createFileRoute('/cursor-threads/$composerId')({
|
|
425
|
-
component:
|
|
431
|
+
component: () => {
|
|
432
|
+
const { composerId } = Route.useParams();
|
|
433
|
+
return (
|
|
434
|
+
<RouteStateResetBoundary routeKey={composerId}>
|
|
435
|
+
<CursorThreadDetailPage />
|
|
436
|
+
</RouteStateResetBoundary>
|
|
437
|
+
);
|
|
438
|
+
},
|
|
426
439
|
errorComponent: CursorThreadDetailErrorComponent,
|
|
427
440
|
loader: ({ context, params }) =>
|
|
428
441
|
context.queryClient.ensureQueryData(cursorThreadDetailQueryOptions(params.composerId)),
|
|
@@ -9,7 +9,7 @@ import { ExportDialog } from '#/components/export-dialog';
|
|
|
9
9
|
import { ListSearchInput } from '#/components/list-search-input';
|
|
10
10
|
import { LoadingPanel } from '#/components/loading-panel';
|
|
11
11
|
import { PageHeader } from '#/components/page-header';
|
|
12
|
-
import {
|
|
12
|
+
import { RouteErrorPanel } from '#/components/route-error-panel';
|
|
13
13
|
import { Button } from '#/components/ui/button';
|
|
14
14
|
import { cursorThreadsQueryOptions, cursorWorkspacesQueryOptions } from '#/lib/cursor-queries';
|
|
15
15
|
import {
|
|
@@ -20,7 +20,10 @@ import {
|
|
|
20
20
|
recoverCursorWorkspaceFn,
|
|
21
21
|
} from '#/lib/cursor-server';
|
|
22
22
|
import { downloadTextFile, downloadUrlFile } from '#/lib/download';
|
|
23
|
+
import { createExportSelectionMutationInput, type ExportSelectionMutationInput } from '#/lib/export-mutation';
|
|
24
|
+
import { getMutationErrorMessage } from '#/lib/mutation-error';
|
|
23
25
|
import { matchesTextQuery } from '#/lib/text-filter';
|
|
26
|
+
import { isWorkspaceEmptiedByDelete } from '#/lib/workspace-delete-navigation';
|
|
24
27
|
|
|
25
28
|
type PendingCursorDelete =
|
|
26
29
|
| { kind: 'threads'; threads: CursorThreadSummary[] }
|
|
@@ -31,14 +34,6 @@ type PendingCursorExport = {
|
|
|
31
34
|
label: string;
|
|
32
35
|
};
|
|
33
36
|
|
|
34
|
-
type ExportDialogOptions = {
|
|
35
|
-
includeCommentary: boolean;
|
|
36
|
-
includeMetadata: boolean;
|
|
37
|
-
includeTools: boolean;
|
|
38
|
-
outputFormat: 'md' | 'txt';
|
|
39
|
-
zipArchive: boolean;
|
|
40
|
-
};
|
|
41
|
-
|
|
42
37
|
const findWorkspaceOrThrow = (workspaces: CursorWorkspaceGroup[], workspaceKey: string) => {
|
|
43
38
|
const workspace = workspaces.find((candidate) => candidate.key === workspaceKey);
|
|
44
39
|
if (!workspace) {
|
|
@@ -113,7 +108,7 @@ const getCursorDeleteTitle = (pendingDelete: PendingCursorDelete | null) => {
|
|
|
113
108
|
};
|
|
114
109
|
|
|
115
110
|
const CursorWorkspaceErrorComponent = ({ error }: { error: Error }) => {
|
|
116
|
-
return <
|
|
111
|
+
return <RouteErrorPanel error={error} title="Failed to load Cursor workspace" />;
|
|
117
112
|
};
|
|
118
113
|
|
|
119
114
|
const CursorWorkspacePage = () => {
|
|
@@ -148,34 +143,43 @@ const CursorWorkspacePage = () => {
|
|
|
148
143
|
onSuccess: async (_result, target) => {
|
|
149
144
|
if (target.kind === 'workspace') {
|
|
150
145
|
await navigate({ to: '/cursor' });
|
|
146
|
+
queryClient.removeQueries({ queryKey: ['cursor-thread'] });
|
|
151
147
|
await queryClient.invalidateQueries({ queryKey: ['cursor-workspaces'] });
|
|
152
148
|
setPendingDelete(null);
|
|
153
149
|
return;
|
|
154
150
|
}
|
|
155
151
|
|
|
156
|
-
|
|
152
|
+
const workspaceEmptied = isWorkspaceEmptiedByDelete(
|
|
153
|
+
threads,
|
|
154
|
+
target.threads.map((thread) => thread.composerId),
|
|
155
|
+
(thread) => thread.composerId,
|
|
156
|
+
);
|
|
157
|
+
for (const thread of target.threads) {
|
|
158
|
+
queryClient.removeQueries({ queryKey: ['cursor-thread', thread.composerId] });
|
|
159
|
+
}
|
|
157
160
|
setPendingDelete(null);
|
|
161
|
+
if (workspaceEmptied) {
|
|
162
|
+
await navigate({ to: '/cursor' });
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
await invalidateWorkspaceQueries();
|
|
158
166
|
},
|
|
159
167
|
});
|
|
160
168
|
|
|
161
169
|
const exportMutation = useMutation({
|
|
162
|
-
mutationFn: async (options:
|
|
163
|
-
if (!pendingExport) {
|
|
164
|
-
throw new Error('No thread selected for export');
|
|
165
|
-
}
|
|
166
|
-
|
|
170
|
+
mutationFn: async ({ ids, options }: ExportSelectionMutationInput) => {
|
|
167
171
|
const download =
|
|
168
|
-
|
|
172
|
+
ids.length === 1
|
|
169
173
|
? await exportCursorThreadFn({
|
|
170
174
|
data: {
|
|
171
175
|
...options,
|
|
172
|
-
composerId:
|
|
176
|
+
composerId: ids[0]!,
|
|
173
177
|
},
|
|
174
178
|
})
|
|
175
179
|
: await exportCursorThreadsFn({
|
|
176
180
|
data: {
|
|
177
181
|
...options,
|
|
178
|
-
composerIds:
|
|
182
|
+
composerIds: [...ids],
|
|
179
183
|
},
|
|
180
184
|
});
|
|
181
185
|
|
|
@@ -243,11 +247,11 @@ const CursorWorkspacePage = () => {
|
|
|
243
247
|
|
|
244
248
|
<CursorWorkspaceErrors
|
|
245
249
|
deleteError={deleteMutation.isError ? deleteMutation.error : null}
|
|
246
|
-
exportError={exportMutation.isError ? exportMutation.error : null}
|
|
247
250
|
recoverError={recoverWorkspaceMutation.isError ? recoverWorkspaceMutation.error : null}
|
|
248
251
|
/>
|
|
249
252
|
|
|
250
253
|
<CursorWorkspaceDeleteDialog
|
|
254
|
+
errorMessage={getMutationErrorMessage(deleteMutation.error, 'Delete failed')}
|
|
251
255
|
pending={deleteMutation.isPending}
|
|
252
256
|
pendingDelete={pendingDelete}
|
|
253
257
|
onConfirm={() => {
|
|
@@ -260,19 +264,32 @@ const CursorWorkspacePage = () => {
|
|
|
260
264
|
onOpenChange={(open) => {
|
|
261
265
|
if (!open) {
|
|
262
266
|
setPendingDelete(null);
|
|
267
|
+
deleteMutation.reset();
|
|
263
268
|
}
|
|
264
269
|
}}
|
|
265
270
|
/>
|
|
266
271
|
|
|
267
272
|
<ExportDialog
|
|
273
|
+
errorMessage={
|
|
274
|
+
exportMutation.isError
|
|
275
|
+
? exportMutation.error instanceof Error
|
|
276
|
+
? exportMutation.error.message
|
|
277
|
+
: 'Thread export failed'
|
|
278
|
+
: null
|
|
279
|
+
}
|
|
268
280
|
forceZipArchive={pendingExport ? pendingExport.composerIds.length > 1 : false}
|
|
269
281
|
open={pendingExport !== null}
|
|
270
282
|
pending={exportMutation.isPending}
|
|
271
283
|
title={pendingExport ? `Export ${pendingExport.label}` : 'Export thread'}
|
|
272
|
-
onExport={(options) =>
|
|
284
|
+
onExport={(options) => {
|
|
285
|
+
if (pendingExport) {
|
|
286
|
+
exportMutation.mutate(createExportSelectionMutationInput(pendingExport.composerIds, options));
|
|
287
|
+
}
|
|
288
|
+
}}
|
|
273
289
|
onOpenChange={(open) => {
|
|
274
290
|
if (!open) {
|
|
275
291
|
setPendingExport(null);
|
|
292
|
+
exportMutation.reset();
|
|
276
293
|
}
|
|
277
294
|
}}
|
|
278
295
|
/>
|
|
@@ -348,18 +365,14 @@ const CursorWorkspaceRecoveryNotice = ({ workspace }: { workspace: CursorWorkspa
|
|
|
348
365
|
|
|
349
366
|
const CursorWorkspaceErrors = ({
|
|
350
367
|
deleteError,
|
|
351
|
-
exportError,
|
|
352
368
|
recoverError,
|
|
353
369
|
}: {
|
|
354
370
|
deleteError: Error | null;
|
|
355
|
-
exportError: Error | null;
|
|
356
371
|
recoverError: Error | null;
|
|
357
372
|
}) => {
|
|
358
|
-
const entries = [
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
exportError ? exportError.message : null,
|
|
362
|
-
].filter(Boolean);
|
|
373
|
+
const entries = [recoverError ? recoverError.message : null, deleteError ? deleteError.message : null].filter(
|
|
374
|
+
Boolean,
|
|
375
|
+
);
|
|
363
376
|
|
|
364
377
|
if (entries.length === 0) {
|
|
365
378
|
return null;
|
|
@@ -377,11 +390,13 @@ const CursorWorkspaceErrors = ({
|
|
|
377
390
|
};
|
|
378
391
|
|
|
379
392
|
const CursorWorkspaceDeleteDialog = ({
|
|
393
|
+
errorMessage,
|
|
380
394
|
pending,
|
|
381
395
|
pendingDelete,
|
|
382
396
|
onConfirm,
|
|
383
397
|
onOpenChange,
|
|
384
398
|
}: {
|
|
399
|
+
errorMessage: string | null;
|
|
385
400
|
pending: boolean;
|
|
386
401
|
pendingDelete: PendingCursorDelete | null;
|
|
387
402
|
onConfirm: () => void;
|
|
@@ -391,6 +406,7 @@ const CursorWorkspaceDeleteDialog = ({
|
|
|
391
406
|
<DeleteConfirmDialog
|
|
392
407
|
confirmLabel={getCursorDeleteConfirmLabel(pendingDelete, pending)}
|
|
393
408
|
description={getCursorDeleteDescription(pendingDelete)}
|
|
409
|
+
errorMessage={errorMessage}
|
|
394
410
|
open={pendingDelete !== null}
|
|
395
411
|
title={getCursorDeleteTitle(pendingDelete)}
|
|
396
412
|
onConfirm={onConfirm}
|
|
@@ -6,13 +6,14 @@ import { CursorWorkspacesTable } from '#/components/cursor-workspaces-table';
|
|
|
6
6
|
import { DeleteConfirmDialog } from '#/components/delete-confirm-dialog';
|
|
7
7
|
import { ListSearchInput } from '#/components/list-search-input';
|
|
8
8
|
import { PageHeader } from '#/components/page-header';
|
|
9
|
-
import {
|
|
9
|
+
import { RouteErrorPanel } from '#/components/route-error-panel';
|
|
10
10
|
import { cursorWorkspacesQueryOptions } from '#/lib/cursor-queries';
|
|
11
11
|
import { deleteCursorWorkspaceFn, recoverCursorWorkspaceFn } from '#/lib/cursor-server';
|
|
12
|
+
import { getMutationErrorMessage } from '#/lib/mutation-error';
|
|
12
13
|
import { matchesTextQuery } from '#/lib/text-filter';
|
|
13
14
|
|
|
14
15
|
const CursorErrorComponent = ({ error }: { error: Error }) => {
|
|
15
|
-
return <
|
|
16
|
+
return <RouteErrorPanel error={error} title="Failed to load Cursor workspaces" />;
|
|
16
17
|
};
|
|
17
18
|
|
|
18
19
|
const CursorPage = () => {
|
|
@@ -94,6 +95,7 @@ const CursorPage = () => {
|
|
|
94
95
|
? `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
|
: ''
|
|
96
97
|
}
|
|
98
|
+
errorMessage={getMutationErrorMessage(deleteWorkspaceMutation.error, 'Workspace deletion failed')}
|
|
97
99
|
open={pendingDelete !== null}
|
|
98
100
|
title="Delete Cursor workspace?"
|
|
99
101
|
onConfirm={() => {
|
|
@@ -106,6 +108,7 @@ const CursorPage = () => {
|
|
|
106
108
|
onOpenChange={(open) => {
|
|
107
109
|
if (!open) {
|
|
108
110
|
setPendingDelete(null);
|
|
111
|
+
deleteWorkspaceMutation.reset();
|
|
109
112
|
}
|
|
110
113
|
}}
|
|
111
114
|
/>
|