spiracha 2.1.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 +37 -19
- package/apps/ui/AGENTS.md +18 -10
- package/apps/ui/README.md +17 -10
- package/apps/ui/src/components/analytics-breakdowns.tsx +106 -0
- package/apps/ui/src/components/antigravity-conversations-table.tsx +1 -0
- package/apps/ui/src/components/app-shell.tsx +45 -3
- package/apps/ui/src/components/data-table.tsx +17 -1
- package/apps/ui/src/components/delete-confirm-dialog.tsx +4 -3
- package/apps/ui/src/components/export-dialog.tsx +77 -62
- package/apps/ui/src/components/projects-table.tsx +4 -1
- package/apps/ui/src/components/route-error-panel.tsx +13 -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 +4 -1
- package/apps/ui/src/lib/antigravity-server.ts +70 -17
- package/apps/ui/src/lib/antigravity-transcript-events.ts +46 -70
- package/apps/ui/src/lib/claude-code-server.ts +14 -4
- package/apps/ui/src/lib/codex-queries.ts +3 -3
- package/apps/ui/src/lib/codex-server.ts +3 -3
- package/apps/ui/src/lib/cursor-server.ts +41 -69
- 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/grok-server.ts +2 -3
- package/apps/ui/src/lib/kiro-server.ts +14 -2
- package/apps/ui/src/lib/mutation-error.ts +15 -0
- package/apps/ui/src/lib/opencode-server.ts +13 -2
- 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-transcript-events.ts +1 -1
- 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 +1 -1
- package/apps/ui/src/lib/thread-metadata.ts +22 -0
- package/apps/ui/src/lib/thread-transcript-stats.ts +10 -1
- package/apps/ui/src/lib/workspace-delete-navigation.ts +8 -0
- package/apps/ui/src/routeTree.gen.ts +282 -282
- 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 +73 -19
- package/apps/ui/src/routes/antigravity.$workspaceKey.tsx +42 -60
- package/apps/ui/src/routes/antigravity.index.tsx +2 -2
- package/apps/ui/src/routes/claude-code-sessions.$sessionId.tsx +25 -11
- package/apps/ui/src/routes/claude-code.$workspaceKey.tsx +27 -32
- 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 +29 -16
- package/apps/ui/src/routes/cursor.$workspaceKey.tsx +33 -27
- package/apps/ui/src/routes/cursor.index.tsx +5 -2
- package/apps/ui/src/routes/grok-sessions.$sessionId.tsx +25 -12
- package/apps/ui/src/routes/grok.$workspaceKey.tsx +20 -26
- package/apps/ui/src/routes/grok.index.tsx +2 -2
- package/apps/ui/src/routes/index.tsx +2 -18
- package/apps/ui/src/routes/kiro-sessions.$sessionId.tsx +25 -12
- package/apps/ui/src/routes/kiro.$workspaceKey.tsx +27 -32
- package/apps/ui/src/routes/kiro.index.tsx +2 -2
- package/apps/ui/src/routes/opencode-sessions.$sessionId.tsx +29 -36
- package/apps/ui/src/routes/opencode.$workspaceKey.tsx +45 -61
- package/apps/ui/src/routes/opencode.index.tsx +8 -17
- package/apps/ui/src/routes/qoder-sessions.$sessionId.tsx +12 -11
- package/apps/ui/src/routes/qoder.$workspaceKey.tsx +19 -25
- package/apps/ui/src/routes/qoder.index.tsx +2 -2
- package/apps/ui/src/routes/threads.$threadId.tsx +87 -56
- package/apps/ui/vite.config.ts +22 -12
- package/bin/spiracha.ts +28 -1
- package/package.json +26 -17
- package/src/client.ts +61 -16
- package/src/lib/antigravity-db.ts +236 -82
- package/src/lib/antigravity-keychain.ts +7 -8
- package/src/lib/antigravity-transcript-phase.ts +47 -0
- package/src/lib/claude-code-db.ts +45 -19
- package/src/lib/codex-analytics.ts +32 -5
- package/src/lib/codex-browser-db.ts +540 -137
- package/src/lib/codex-browser-export.ts +2 -7
- package/src/lib/codex-browser-types.ts +21 -1
- package/src/lib/codex-thread-cache.ts +88 -33
- package/src/lib/codex-thread-parser.ts +78 -35
- package/src/lib/codex-thread-recovery.ts +38 -11
- package/src/lib/codex-transcript-renderer.ts +49 -61
- package/src/lib/concurrency.ts +15 -3
- package/src/lib/conversation-api.ts +34 -31
- package/src/lib/conversation-data/adapter-helpers.ts +25 -3
- package/src/lib/conversation-data/antigravity-adapter.ts +54 -29
- package/src/lib/conversation-data/claude-code-adapter.ts +32 -6
- package/src/lib/conversation-data/codex-adapter.ts +91 -80
- package/src/lib/conversation-data/cursor-adapter.ts +48 -34
- package/src/lib/conversation-data/grok-adapter.ts +19 -6
- package/src/lib/conversation-data/index.ts +29 -10
- package/src/lib/conversation-data/kiro-adapter.ts +18 -5
- package/src/lib/conversation-data/message-selector.ts +2 -5
- package/src/lib/conversation-data/opencode-adapter.ts +43 -15
- package/src/lib/conversation-data/path-match.ts +1 -13
- package/src/lib/conversation-data/qoder-adapter.ts +23 -18
- package/src/lib/conversation-zip-export.ts +20 -2
- package/src/lib/cursor-db.ts +34 -23
- 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 +76 -34
- package/src/lib/grok-transcript-phase.ts +11 -2
- package/src/lib/grok-transcript.ts +15 -2
- package/src/lib/kiro-db.ts +161 -68
- package/src/lib/kiro-transcript.ts +0 -4
- package/src/lib/opencode-db.ts +4 -8
- package/src/lib/opencode-think-tags.ts +17 -3
- 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 +51 -15
- package/src/lib/sqlite-error.ts +31 -5
- package/src/lib/transcript-load-limiter.ts +8 -3
- package/src/lib/ui-cache.ts +152 -17
- package/src/lib/ui-export-archive.ts +14 -55
- 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 { claudeCodeSessionsQueryOptions, claudeCodeWorkspacesQueryOptions } from '#/lib/claude-code-queries';
|
|
13
13
|
import {
|
|
14
14
|
deleteClaudeCodeSessionFn,
|
|
@@ -17,17 +17,10 @@ import {
|
|
|
17
17
|
exportClaudeCodeSessionsFn,
|
|
18
18
|
} from '#/lib/claude-code-server';
|
|
19
19
|
import { downloadTextFile, downloadUrlFile } from '#/lib/download';
|
|
20
|
+
import { createExportSelectionMutationInput, type ExportSelectionMutationInput } from '#/lib/export-mutation';
|
|
20
21
|
import { matchesTextQuery } from '#/lib/text-filter';
|
|
21
22
|
import { isWorkspaceEmptiedByDelete } from '#/lib/workspace-delete-navigation';
|
|
22
23
|
|
|
23
|
-
type ExportDialogOptions = {
|
|
24
|
-
includeCommentary: boolean;
|
|
25
|
-
includeMetadata: boolean;
|
|
26
|
-
includeTools: boolean;
|
|
27
|
-
outputFormat: 'md' | 'txt';
|
|
28
|
-
zipArchive: boolean;
|
|
29
|
-
};
|
|
30
|
-
|
|
31
24
|
type PendingSessionDelete = {
|
|
32
25
|
sessions: ClaudeCodeSessionSummary[];
|
|
33
26
|
};
|
|
@@ -90,7 +83,7 @@ export const Route = createFileRoute('/claude-code/$workspaceKey')({
|
|
|
90
83
|
});
|
|
91
84
|
|
|
92
85
|
function ClaudeCodeWorkspaceErrorComponent({ error }: { error: Error }) {
|
|
93
|
-
return <
|
|
86
|
+
return <RouteErrorPanel error={error} title="Failed to load Claude Code workspace" />;
|
|
94
87
|
}
|
|
95
88
|
|
|
96
89
|
function ClaudeCodeWorkspacePage() {
|
|
@@ -106,20 +99,16 @@ function ClaudeCodeWorkspacePage() {
|
|
|
106
99
|
const deferredSearch = useDeferredValue(searchInput);
|
|
107
100
|
|
|
108
101
|
const exportMutation = useMutation({
|
|
109
|
-
mutationFn: async (options:
|
|
110
|
-
if (!pendingExport) {
|
|
111
|
-
throw new Error('No Claude Code session selected for export');
|
|
112
|
-
}
|
|
113
|
-
|
|
102
|
+
mutationFn: async ({ ids, options }: ExportSelectionMutationInput) => {
|
|
114
103
|
const download =
|
|
115
|
-
|
|
104
|
+
ids.length === 1
|
|
116
105
|
? await exportClaudeCodeSessionFn({
|
|
117
106
|
data: {
|
|
118
107
|
includeCommentary: options.includeCommentary,
|
|
119
108
|
includeMetadata: options.includeMetadata,
|
|
120
109
|
includeTools: options.includeTools,
|
|
121
110
|
outputFormat: options.outputFormat,
|
|
122
|
-
sessionId:
|
|
111
|
+
sessionId: ids[0]!,
|
|
123
112
|
zipArchive: options.zipArchive,
|
|
124
113
|
},
|
|
125
114
|
})
|
|
@@ -129,7 +118,7 @@ function ClaudeCodeWorkspacePage() {
|
|
|
129
118
|
includeMetadata: options.includeMetadata,
|
|
130
119
|
includeTools: options.includeTools,
|
|
131
120
|
outputFormat: options.outputFormat,
|
|
132
|
-
sessionIds:
|
|
121
|
+
sessionIds: [...ids],
|
|
133
122
|
zipArchive: options.zipArchive,
|
|
134
123
|
},
|
|
135
124
|
});
|
|
@@ -150,13 +139,7 @@ function ClaudeCodeWorkspacePage() {
|
|
|
150
139
|
sessionIds.length === 1
|
|
151
140
|
? deleteClaudeCodeSessionFn({ data: { sessionId: sessionIds[0]! } })
|
|
152
141
|
: deleteClaudeCodeSessionsFn({ data: { sessionIds } }),
|
|
153
|
-
|
|
154
|
-
const workspaceEmptied = isWorkspaceEmptiedByDelete(sessions, sessionIds, (session) => session.sessionId);
|
|
155
|
-
setPendingDelete(null);
|
|
156
|
-
if (workspaceEmptied) {
|
|
157
|
-
await navigate({ to: '/claude-code' });
|
|
158
|
-
}
|
|
159
|
-
|
|
142
|
+
onSettled: async (_result, _error, sessionIds) => {
|
|
160
143
|
await Promise.all([
|
|
161
144
|
queryClient.invalidateQueries({ queryKey: ['claude-code-workspaces'] }),
|
|
162
145
|
queryClient.invalidateQueries({ queryKey: ['claude-code-sessions', workspace.key] }),
|
|
@@ -165,6 +148,13 @@ function ClaudeCodeWorkspacePage() {
|
|
|
165
148
|
),
|
|
166
149
|
]);
|
|
167
150
|
},
|
|
151
|
+
onSuccess: async (_result, sessionIds) => {
|
|
152
|
+
const workspaceEmptied = isWorkspaceEmptiedByDelete(sessions, sessionIds, (session) => session.sessionId);
|
|
153
|
+
setPendingDelete(null);
|
|
154
|
+
if (workspaceEmptied) {
|
|
155
|
+
await navigate({ to: '/claude-code' });
|
|
156
|
+
}
|
|
157
|
+
},
|
|
168
158
|
});
|
|
169
159
|
|
|
170
160
|
const visibleSessions = useMemo(
|
|
@@ -226,11 +216,22 @@ function ClaudeCodeWorkspacePage() {
|
|
|
226
216
|
/>
|
|
227
217
|
|
|
228
218
|
<ExportDialog
|
|
219
|
+
errorMessage={
|
|
220
|
+
exportMutation.isError
|
|
221
|
+
? exportMutation.error instanceof Error
|
|
222
|
+
? exportMutation.error.message
|
|
223
|
+
: 'Session export failed'
|
|
224
|
+
: null
|
|
225
|
+
}
|
|
229
226
|
forceZipArchive={pendingExport ? pendingExport.sessionIds.length > 1 : false}
|
|
230
227
|
open={pendingExport !== null}
|
|
231
228
|
pending={exportMutation.isPending}
|
|
232
229
|
title={pendingExport ? `Export ${pendingExport.label}` : 'Export session'}
|
|
233
|
-
onExport={(options) =>
|
|
230
|
+
onExport={(options) => {
|
|
231
|
+
if (pendingExport) {
|
|
232
|
+
exportMutation.mutate(createExportSelectionMutationInput(pendingExport.sessionIds, options));
|
|
233
|
+
}
|
|
234
|
+
}}
|
|
234
235
|
onOpenChange={(open) => {
|
|
235
236
|
if (!open) {
|
|
236
237
|
setPendingExport(null);
|
|
@@ -239,12 +240,6 @@ function ClaudeCodeWorkspacePage() {
|
|
|
239
240
|
}}
|
|
240
241
|
/>
|
|
241
242
|
|
|
242
|
-
{exportMutation.isError ? (
|
|
243
|
-
<p className="text-[var(--destructive)] text-sm">
|
|
244
|
-
{exportMutation.error instanceof Error ? exportMutation.error.message : 'Session export failed'}
|
|
245
|
-
</p>
|
|
246
|
-
) : null}
|
|
247
|
-
|
|
248
243
|
<DeleteConfirmDialog
|
|
249
244
|
confirmLabel={getDeleteConfirmLabel(pendingDelete, deleteMutation.isPending)}
|
|
250
245
|
description={getDeleteDescription(pendingDelete)}
|
|
@@ -4,7 +4,7 @@ import { useDeferredValue, useState } from 'react';
|
|
|
4
4
|
import { ClaudeCodeWorkspacesTable } from '#/components/claude-code-workspaces-table';
|
|
5
5
|
import { ListSearchInput } from '#/components/list-search-input';
|
|
6
6
|
import { PageHeader } from '#/components/page-header';
|
|
7
|
-
import {
|
|
7
|
+
import { RouteErrorPanel } from '#/components/route-error-panel';
|
|
8
8
|
import { claudeCodeWorkspacesQueryOptions } from '#/lib/claude-code-queries';
|
|
9
9
|
import { matchesTextQuery } from '#/lib/text-filter';
|
|
10
10
|
|
|
@@ -15,7 +15,7 @@ export const Route = createFileRoute('/claude-code/')({
|
|
|
15
15
|
});
|
|
16
16
|
|
|
17
17
|
function ClaudeCodeErrorComponent({ error }: { error: Error }) {
|
|
18
|
-
return <
|
|
18
|
+
return <RouteErrorPanel error={error} title="Failed to load Claude Code workspaces" />;
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
function ClaudeCodePage() {
|
|
@@ -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 {
|
|
14
|
+
import { RouteErrorPanel } from '#/components/route-error-panel';
|
|
15
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 = () => {
|
|
@@ -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,6 +20,8 @@ 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';
|
|
24
26
|
import { isWorkspaceEmptiedByDelete } from '#/lib/workspace-delete-navigation';
|
|
25
27
|
|
|
@@ -32,14 +34,6 @@ type PendingCursorExport = {
|
|
|
32
34
|
label: string;
|
|
33
35
|
};
|
|
34
36
|
|
|
35
|
-
type ExportDialogOptions = {
|
|
36
|
-
includeCommentary: boolean;
|
|
37
|
-
includeMetadata: boolean;
|
|
38
|
-
includeTools: boolean;
|
|
39
|
-
outputFormat: 'md' | 'txt';
|
|
40
|
-
zipArchive: boolean;
|
|
41
|
-
};
|
|
42
|
-
|
|
43
37
|
const findWorkspaceOrThrow = (workspaces: CursorWorkspaceGroup[], workspaceKey: string) => {
|
|
44
38
|
const workspace = workspaces.find((candidate) => candidate.key === workspaceKey);
|
|
45
39
|
if (!workspace) {
|
|
@@ -114,7 +108,7 @@ const getCursorDeleteTitle = (pendingDelete: PendingCursorDelete | null) => {
|
|
|
114
108
|
};
|
|
115
109
|
|
|
116
110
|
const CursorWorkspaceErrorComponent = ({ error }: { error: Error }) => {
|
|
117
|
-
return <
|
|
111
|
+
return <RouteErrorPanel error={error} title="Failed to load Cursor workspace" />;
|
|
118
112
|
};
|
|
119
113
|
|
|
120
114
|
const CursorWorkspacePage = () => {
|
|
@@ -149,6 +143,7 @@ const CursorWorkspacePage = () => {
|
|
|
149
143
|
onSuccess: async (_result, target) => {
|
|
150
144
|
if (target.kind === 'workspace') {
|
|
151
145
|
await navigate({ to: '/cursor' });
|
|
146
|
+
queryClient.removeQueries({ queryKey: ['cursor-thread'] });
|
|
152
147
|
await queryClient.invalidateQueries({ queryKey: ['cursor-workspaces'] });
|
|
153
148
|
setPendingDelete(null);
|
|
154
149
|
return;
|
|
@@ -159,6 +154,9 @@ const CursorWorkspacePage = () => {
|
|
|
159
154
|
target.threads.map((thread) => thread.composerId),
|
|
160
155
|
(thread) => thread.composerId,
|
|
161
156
|
);
|
|
157
|
+
for (const thread of target.threads) {
|
|
158
|
+
queryClient.removeQueries({ queryKey: ['cursor-thread', thread.composerId] });
|
|
159
|
+
}
|
|
162
160
|
setPendingDelete(null);
|
|
163
161
|
if (workspaceEmptied) {
|
|
164
162
|
await navigate({ to: '/cursor' });
|
|
@@ -169,23 +167,19 @@ const CursorWorkspacePage = () => {
|
|
|
169
167
|
});
|
|
170
168
|
|
|
171
169
|
const exportMutation = useMutation({
|
|
172
|
-
mutationFn: async (options:
|
|
173
|
-
if (!pendingExport) {
|
|
174
|
-
throw new Error('No thread selected for export');
|
|
175
|
-
}
|
|
176
|
-
|
|
170
|
+
mutationFn: async ({ ids, options }: ExportSelectionMutationInput) => {
|
|
177
171
|
const download =
|
|
178
|
-
|
|
172
|
+
ids.length === 1
|
|
179
173
|
? await exportCursorThreadFn({
|
|
180
174
|
data: {
|
|
181
175
|
...options,
|
|
182
|
-
composerId:
|
|
176
|
+
composerId: ids[0]!,
|
|
183
177
|
},
|
|
184
178
|
})
|
|
185
179
|
: await exportCursorThreadsFn({
|
|
186
180
|
data: {
|
|
187
181
|
...options,
|
|
188
|
-
composerIds:
|
|
182
|
+
composerIds: [...ids],
|
|
189
183
|
},
|
|
190
184
|
});
|
|
191
185
|
|
|
@@ -253,11 +247,11 @@ const CursorWorkspacePage = () => {
|
|
|
253
247
|
|
|
254
248
|
<CursorWorkspaceErrors
|
|
255
249
|
deleteError={deleteMutation.isError ? deleteMutation.error : null}
|
|
256
|
-
exportError={exportMutation.isError ? exportMutation.error : null}
|
|
257
250
|
recoverError={recoverWorkspaceMutation.isError ? recoverWorkspaceMutation.error : null}
|
|
258
251
|
/>
|
|
259
252
|
|
|
260
253
|
<CursorWorkspaceDeleteDialog
|
|
254
|
+
errorMessage={getMutationErrorMessage(deleteMutation.error, 'Delete failed')}
|
|
261
255
|
pending={deleteMutation.isPending}
|
|
262
256
|
pendingDelete={pendingDelete}
|
|
263
257
|
onConfirm={() => {
|
|
@@ -270,19 +264,32 @@ const CursorWorkspacePage = () => {
|
|
|
270
264
|
onOpenChange={(open) => {
|
|
271
265
|
if (!open) {
|
|
272
266
|
setPendingDelete(null);
|
|
267
|
+
deleteMutation.reset();
|
|
273
268
|
}
|
|
274
269
|
}}
|
|
275
270
|
/>
|
|
276
271
|
|
|
277
272
|
<ExportDialog
|
|
273
|
+
errorMessage={
|
|
274
|
+
exportMutation.isError
|
|
275
|
+
? exportMutation.error instanceof Error
|
|
276
|
+
? exportMutation.error.message
|
|
277
|
+
: 'Thread export failed'
|
|
278
|
+
: null
|
|
279
|
+
}
|
|
278
280
|
forceZipArchive={pendingExport ? pendingExport.composerIds.length > 1 : false}
|
|
279
281
|
open={pendingExport !== null}
|
|
280
282
|
pending={exportMutation.isPending}
|
|
281
283
|
title={pendingExport ? `Export ${pendingExport.label}` : 'Export thread'}
|
|
282
|
-
onExport={(options) =>
|
|
284
|
+
onExport={(options) => {
|
|
285
|
+
if (pendingExport) {
|
|
286
|
+
exportMutation.mutate(createExportSelectionMutationInput(pendingExport.composerIds, options));
|
|
287
|
+
}
|
|
288
|
+
}}
|
|
283
289
|
onOpenChange={(open) => {
|
|
284
290
|
if (!open) {
|
|
285
291
|
setPendingExport(null);
|
|
292
|
+
exportMutation.reset();
|
|
286
293
|
}
|
|
287
294
|
}}
|
|
288
295
|
/>
|
|
@@ -358,18 +365,14 @@ const CursorWorkspaceRecoveryNotice = ({ workspace }: { workspace: CursorWorkspa
|
|
|
358
365
|
|
|
359
366
|
const CursorWorkspaceErrors = ({
|
|
360
367
|
deleteError,
|
|
361
|
-
exportError,
|
|
362
368
|
recoverError,
|
|
363
369
|
}: {
|
|
364
370
|
deleteError: Error | null;
|
|
365
|
-
exportError: Error | null;
|
|
366
371
|
recoverError: Error | null;
|
|
367
372
|
}) => {
|
|
368
|
-
const entries = [
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
exportError ? exportError.message : null,
|
|
372
|
-
].filter(Boolean);
|
|
373
|
+
const entries = [recoverError ? recoverError.message : null, deleteError ? deleteError.message : null].filter(
|
|
374
|
+
Boolean,
|
|
375
|
+
);
|
|
373
376
|
|
|
374
377
|
if (entries.length === 0) {
|
|
375
378
|
return null;
|
|
@@ -387,11 +390,13 @@ const CursorWorkspaceErrors = ({
|
|
|
387
390
|
};
|
|
388
391
|
|
|
389
392
|
const CursorWorkspaceDeleteDialog = ({
|
|
393
|
+
errorMessage,
|
|
390
394
|
pending,
|
|
391
395
|
pendingDelete,
|
|
392
396
|
onConfirm,
|
|
393
397
|
onOpenChange,
|
|
394
398
|
}: {
|
|
399
|
+
errorMessage: string | null;
|
|
395
400
|
pending: boolean;
|
|
396
401
|
pendingDelete: PendingCursorDelete | null;
|
|
397
402
|
onConfirm: () => void;
|
|
@@ -401,6 +406,7 @@ const CursorWorkspaceDeleteDialog = ({
|
|
|
401
406
|
<DeleteConfirmDialog
|
|
402
407
|
confirmLabel={getCursorDeleteConfirmLabel(pendingDelete, pending)}
|
|
403
408
|
description={getCursorDeleteDescription(pendingDelete)}
|
|
409
|
+
errorMessage={errorMessage}
|
|
404
410
|
open={pendingDelete !== null}
|
|
405
411
|
title={getCursorDeleteTitle(pendingDelete)}
|
|
406
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
|
/>
|