spiracha 2.0.0 → 2.1.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/README.md +6 -1
- package/apps/ui/README.md +20 -2
- package/apps/ui/package.json +2 -2
- package/apps/ui/src/components/antigravity-conversations-table.tsx +50 -11
- package/apps/ui/src/components/antigravity-workspaces-table.tsx +1 -1
- package/apps/ui/src/components/app-shell.tsx +1 -0
- 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 +7 -5
- package/apps/ui/src/components/export-dialog.tsx +2 -12
- 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/qoder-sessions-table.tsx +23 -2
- package/apps/ui/src/components/qoder-workspaces-table.tsx +5 -1
- package/apps/ui/src/components/selection-actions-toolbar.tsx +80 -0
- package/apps/ui/src/components/threads-table.tsx +9 -46
- 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 +152 -7
- package/apps/ui/src/lib/antigravity-transcript-events.ts +11 -3
- package/apps/ui/src/lib/claude-code-queries.ts +8 -0
- package/apps/ui/src/lib/claude-code-server.ts +175 -7
- package/apps/ui/src/lib/claude-code-transcript-events.ts +8 -1
- package/apps/ui/src/lib/codex-queries.ts +19 -3
- package/apps/ui/src/lib/codex-server.ts +135 -53
- package/apps/ui/src/lib/cursor-server.ts +48 -5
- 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 +169 -0
- package/apps/ui/src/lib/grok-transcript-events.ts +149 -0
- package/apps/ui/src/lib/kiro-server.ts +85 -7
- package/apps/ui/src/lib/opencode-server.ts +85 -7
- package/apps/ui/src/lib/qoder-server.ts +67 -11
- package/apps/ui/src/lib/route-search.ts +114 -0
- package/apps/ui/src/lib/source-session-export-server.ts +86 -3
- package/apps/ui/src/lib/thread-transcript-load.ts +15 -0
- package/apps/ui/src/lib/workspace-delete-navigation.ts +12 -0
- package/apps/ui/src/routeTree.gen.ts +107 -0
- package/apps/ui/src/routes/antigravity-conversations.$conversationId.tsx +76 -10
- package/apps/ui/src/routes/antigravity.$workspaceKey.tsx +269 -31
- 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 +143 -17
- package/apps/ui/src/routes/claude-code.$workspaceKey.tsx +165 -26
- package/apps/ui/src/routes/cursor-threads.$composerId.tsx +2 -2
- package/apps/ui/src/routes/cursor.$workspaceKey.tsx +11 -1
- package/apps/ui/src/routes/grok-sessions.$sessionId.tsx +388 -0
- package/apps/ui/src/routes/grok.$workspaceKey.tsx +301 -0
- package/apps/ui/src/routes/grok.index.tsx +48 -0
- package/apps/ui/src/routes/kiro-sessions.$sessionId.tsx +66 -15
- package/apps/ui/src/routes/kiro.$workspaceKey.tsx +199 -32
- package/apps/ui/src/routes/opencode-sessions.$sessionId.tsx +105 -19
- package/apps/ui/src/routes/opencode.$workspaceKey.tsx +236 -44
- package/apps/ui/src/routes/opencode.index.tsx +16 -3
- package/apps/ui/src/routes/qoder-sessions.$sessionId.tsx +7 -4
- package/apps/ui/src/routes/qoder.$workspaceKey.tsx +75 -28
- package/apps/ui/src/routes/threads.$threadId.tsx +548 -64
- package/package.json +19 -18
- package/src/client.ts +134 -1
- package/src/lib/antigravity-db.ts +208 -32
- package/src/lib/antigravity-exporter-types.ts +3 -0
- package/src/lib/antigravity-projects.ts +201 -0
- package/src/lib/claude-code-db.ts +498 -32
- 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-browser-export.ts +23 -27
- package/src/lib/codex-thread-cache.ts +41 -13
- package/src/lib/codex-thread-parser.ts +86 -35
- package/src/lib/codex-transcript-filter.ts +31 -0
- package/src/lib/codex-transcript-renderer.ts +39 -19
- package/src/lib/concurrency.ts +41 -0
- package/src/lib/conversation-api.ts +399 -19
- package/src/lib/conversation-data/antigravity-adapter.ts +21 -2
- package/src/lib/conversation-data/claude-code-adapter.ts +37 -6
- package/src/lib/conversation-data/codex-adapter.ts +28 -4
- package/src/lib/conversation-data/cursor-adapter.ts +35 -1
- package/src/lib/conversation-data/grok-adapter.ts +210 -0
- package/src/lib/conversation-data/index.ts +76 -1
- package/src/lib/conversation-data/kiro-adapter.ts +41 -7
- package/src/lib/conversation-data/opencode-adapter.ts +24 -2
- package/src/lib/conversation-data/qoder-adapter.ts +44 -19
- package/src/lib/conversation-data/types.ts +43 -0
- package/src/lib/conversation-zip-export.ts +57 -0
- package/src/lib/cursor-db.ts +34 -5
- package/src/lib/grok-db.ts +1026 -0
- package/src/lib/grok-exporter-types.ts +110 -0
- package/src/lib/grok-transcript-phase.ts +52 -0
- package/src/lib/grok-transcript.ts +154 -0
- package/src/lib/kiro-db.ts +52 -1
- package/src/lib/model-label.ts +11 -3
- package/src/lib/opencode-db.ts +598 -55
- package/src/lib/opencode-transcript.ts +8 -5
- package/src/lib/shared.ts +12 -0
- package/src/lib/transcript-load-limiter.ts +82 -0
- package/src/lib/ui-cache.ts +43 -17
- package/src/lib/ui-export-archive.ts +60 -0
|
@@ -1,21 +1,50 @@
|
|
|
1
1
|
import type { AntigravityConversation, AntigravityWorkspaceGroup } from '@spiracha/lib/antigravity-exporter-types';
|
|
2
|
-
import { useMutation, useSuspenseQuery } from '@tanstack/react-query';
|
|
3
|
-
import { createFileRoute } from '@tanstack/react-router';
|
|
4
|
-
import {
|
|
2
|
+
import { useMutation, useQueryClient, useSuspenseQuery } from '@tanstack/react-query';
|
|
3
|
+
import { createFileRoute, useNavigate } from '@tanstack/react-router';
|
|
4
|
+
import { Trash2 } from 'lucide-react';
|
|
5
|
+
import { useDeferredValue, useMemo, useState } from 'react';
|
|
5
6
|
import { AntigravityConversationsTable } from '#/components/antigravity-conversations-table';
|
|
6
7
|
import { AntigravityKeychainPanel } from '#/components/antigravity-keychain-panel';
|
|
8
|
+
import { DeleteConfirmDialog } from '#/components/delete-confirm-dialog';
|
|
9
|
+
import { ExportDialog } from '#/components/export-dialog';
|
|
7
10
|
import { ListSearchInput } from '#/components/list-search-input';
|
|
8
11
|
import { LoadingPanel } from '#/components/loading-panel';
|
|
9
12
|
import { PageHeader } from '#/components/page-header';
|
|
10
13
|
import { ReloadErrorPanel } from '#/components/reload-error-panel';
|
|
14
|
+
import { Button } from '#/components/ui/button';
|
|
11
15
|
import {
|
|
12
16
|
antigravityConversationsQueryOptions,
|
|
13
17
|
antigravityDecryptionQueryOptions,
|
|
14
18
|
antigravityWorkspacesQueryOptions,
|
|
15
19
|
} from '#/lib/antigravity-queries';
|
|
16
|
-
import {
|
|
17
|
-
|
|
20
|
+
import {
|
|
21
|
+
deleteAntigravityConversationFn,
|
|
22
|
+
deleteAntigravityConversationsFn,
|
|
23
|
+
exportAntigravityArtifactsFn,
|
|
24
|
+
exportAntigravityConversationFn,
|
|
25
|
+
exportAntigravityConversationsFn,
|
|
26
|
+
} from '#/lib/antigravity-server';
|
|
27
|
+
import { downloadTextFile, downloadUrlFile } from '#/lib/download';
|
|
18
28
|
import { matchesTextQuery } from '#/lib/text-filter';
|
|
29
|
+
import { isWorkspaceEmptiedByDelete } from '#/lib/workspace-delete-navigation';
|
|
30
|
+
|
|
31
|
+
type ExportDialogOptions = {
|
|
32
|
+
includeCommentary: boolean;
|
|
33
|
+
includeMetadata: boolean;
|
|
34
|
+
includeTools: boolean;
|
|
35
|
+
outputFormat: 'md' | 'txt';
|
|
36
|
+
zipArchive: boolean;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
type PendingConversationDelete = {
|
|
40
|
+
conversations: AntigravityConversation[];
|
|
41
|
+
scope: 'all' | 'selected';
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
type PendingConversationExport = {
|
|
45
|
+
conversationIds: string[];
|
|
46
|
+
label: string;
|
|
47
|
+
};
|
|
19
48
|
|
|
20
49
|
const findWorkspaceOrThrow = (workspaces: AntigravityWorkspaceGroup[], workspaceKey: string) => {
|
|
21
50
|
const workspace = workspaces.find((candidate) => candidate.key === workspaceKey);
|
|
@@ -26,6 +55,80 @@ const findWorkspaceOrThrow = (workspaces: AntigravityWorkspaceGroup[], workspace
|
|
|
26
55
|
return workspace;
|
|
27
56
|
};
|
|
28
57
|
|
|
58
|
+
const buildConversationExport = (selectedConversations: AntigravityConversation[]): PendingConversationExport => ({
|
|
59
|
+
conversationIds: selectedConversations.map((conversation) => conversation.conversationId),
|
|
60
|
+
label:
|
|
61
|
+
selectedConversations.length === 1
|
|
62
|
+
? selectedConversations[0]!.title
|
|
63
|
+
: `${selectedConversations.length} selected conversations`,
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
const getDeleteConfirmLabel = (pendingDelete: PendingConversationDelete | null, isPending: boolean) => {
|
|
67
|
+
if (isPending) {
|
|
68
|
+
return 'Deleting...';
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (pendingDelete?.scope === 'all') {
|
|
72
|
+
return 'Delete all';
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return pendingDelete && pendingDelete.conversations.length > 1 ? 'Delete conversations' : 'Delete conversation';
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const getDeleteDescription = (pendingDelete: PendingConversationDelete | null) => {
|
|
79
|
+
if (!pendingDelete) {
|
|
80
|
+
return 'Permanently delete the selected Antigravity conversations from disk.';
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (pendingDelete.scope === 'all') {
|
|
84
|
+
return `Permanently delete all ${pendingDelete.conversations.length} Antigravity conversations in this workspace from disk. This removes their summaries, conversation files, transcript logs, and generated artifacts.`;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (pendingDelete.conversations.length === 1) {
|
|
88
|
+
return `Permanently delete "${pendingDelete.conversations[0]!.title}" from Antigravity history. This removes the summary entry, conversation file, transcript logs, and generated artifacts that belong to this conversation.`;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return `Permanently delete ${pendingDelete.conversations.length} selected Antigravity conversations from disk. This removes their summaries, conversation files, transcript logs, and generated artifacts.`;
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const getDeleteTitle = (pendingDelete: PendingConversationDelete | null) => {
|
|
95
|
+
if (pendingDelete?.scope === 'all') {
|
|
96
|
+
return `Delete all ${pendingDelete.conversations.length} Antigravity conversations?`;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return pendingDelete && pendingDelete.conversations.length > 1
|
|
100
|
+
? `Delete ${pendingDelete.conversations.length} Antigravity conversations?`
|
|
101
|
+
: 'Delete this Antigravity conversation?';
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const AntigravityWorkspaceErrors = ({
|
|
105
|
+
artifactError,
|
|
106
|
+
batchExportError,
|
|
107
|
+
conversationError,
|
|
108
|
+
}: {
|
|
109
|
+
artifactError: Error | null;
|
|
110
|
+
batchExportError: Error | null;
|
|
111
|
+
conversationError: Error | null;
|
|
112
|
+
}) => {
|
|
113
|
+
const messages = [conversationError?.message, artifactError?.message, batchExportError?.message].filter(
|
|
114
|
+
(message): message is string => Boolean(message),
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
if (messages.length === 0) {
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return (
|
|
122
|
+
<div className="space-y-1">
|
|
123
|
+
{messages.map((message) => (
|
|
124
|
+
<p className="text-[var(--destructive)] text-sm" key={message}>
|
|
125
|
+
{message}
|
|
126
|
+
</p>
|
|
127
|
+
))}
|
|
128
|
+
</div>
|
|
129
|
+
);
|
|
130
|
+
};
|
|
131
|
+
|
|
29
132
|
export const Route = createFileRoute('/antigravity/$workspaceKey')({
|
|
30
133
|
component: AntigravityWorkspacePage,
|
|
31
134
|
errorComponent: AntigravityWorkspaceErrorComponent,
|
|
@@ -50,12 +153,16 @@ function AntigravityWorkspaceErrorComponent({ error }: { error: Error }) {
|
|
|
50
153
|
}
|
|
51
154
|
|
|
52
155
|
function AntigravityWorkspacePage() {
|
|
156
|
+
const navigate = useNavigate({ from: Route.fullPath });
|
|
53
157
|
const params = Route.useParams();
|
|
158
|
+
const queryClient = useQueryClient();
|
|
54
159
|
const workspaces = useSuspenseQuery(antigravityWorkspacesQueryOptions()).data;
|
|
55
160
|
const workspace = findWorkspaceOrThrow(workspaces, params.workspaceKey);
|
|
56
161
|
const conversations = useSuspenseQuery(antigravityConversationsQueryOptions(workspace.key)).data;
|
|
57
162
|
const decryptionState = useSuspenseQuery(antigravityDecryptionQueryOptions()).data ?? null;
|
|
58
163
|
const [searchInput, setSearchInput] = useState('');
|
|
164
|
+
const [pendingDelete, setPendingDelete] = useState<PendingConversationDelete | null>(null);
|
|
165
|
+
const [pendingExport, setPendingExport] = useState<PendingConversationExport | null>(null);
|
|
59
166
|
const deferredSearch = useDeferredValue(searchInput);
|
|
60
167
|
|
|
61
168
|
const exportConversationMutation = useMutation({
|
|
@@ -74,24 +181,117 @@ function AntigravityWorkspacePage() {
|
|
|
74
181
|
},
|
|
75
182
|
});
|
|
76
183
|
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
184
|
+
const exportConversationsMutation = useMutation({
|
|
185
|
+
mutationFn: async (options: ExportDialogOptions) => {
|
|
186
|
+
if (!pendingExport) {
|
|
187
|
+
throw new Error('No Antigravity conversation selected for export');
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const download = await exportAntigravityConversationsFn({
|
|
191
|
+
data: {
|
|
192
|
+
conversationIds: pendingExport.conversationIds,
|
|
193
|
+
outputFormat: options.outputFormat,
|
|
194
|
+
zipArchive: options.zipArchive,
|
|
195
|
+
},
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
if (download.mode === 'download') {
|
|
199
|
+
downloadTextFile(download.fileName, download.content, download.mimeType);
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
await downloadUrlFile(download.fileName, download.downloadUrl);
|
|
204
|
+
},
|
|
205
|
+
onSuccess: () => {
|
|
206
|
+
setPendingExport(null);
|
|
207
|
+
},
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
const deleteMutation = useMutation({
|
|
211
|
+
mutationFn: async (conversationIds: string[]) =>
|
|
212
|
+
conversationIds.length === 1
|
|
213
|
+
? deleteAntigravityConversationFn({ data: { conversationId: conversationIds[0]! } })
|
|
214
|
+
: deleteAntigravityConversationsFn({ data: { conversationIds } }),
|
|
215
|
+
onSuccess: async (result) => {
|
|
216
|
+
const conversationIds = result.deletedConversationIds;
|
|
217
|
+
const workspaceEmptied = isWorkspaceEmptiedByDelete(
|
|
218
|
+
conversations,
|
|
219
|
+
conversationIds,
|
|
220
|
+
(conversation) => conversation.conversationId,
|
|
221
|
+
);
|
|
222
|
+
setPendingDelete(null);
|
|
223
|
+
await Promise.all([
|
|
224
|
+
queryClient.invalidateQueries({ queryKey: ['antigravity-workspaces'] }),
|
|
225
|
+
queryClient.invalidateQueries({ queryKey: ['antigravity-conversations', workspace.key] }),
|
|
226
|
+
...conversationIds.map((conversationId) =>
|
|
227
|
+
queryClient.invalidateQueries({
|
|
228
|
+
queryKey: ['antigravity-conversation', conversationId],
|
|
229
|
+
}),
|
|
230
|
+
),
|
|
231
|
+
]);
|
|
232
|
+
if (workspaceEmptied) {
|
|
233
|
+
await navigate({ to: '/antigravity' });
|
|
234
|
+
}
|
|
235
|
+
},
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
const visibleConversations = useMemo(
|
|
239
|
+
() =>
|
|
240
|
+
conversations.filter((conversation) =>
|
|
241
|
+
matchesTextQuery(deferredSearch, [
|
|
242
|
+
conversation.title,
|
|
243
|
+
conversation.conversationId,
|
|
244
|
+
conversation.transcriptSource,
|
|
245
|
+
conversation.workspaceLabel,
|
|
246
|
+
]),
|
|
247
|
+
),
|
|
248
|
+
[conversations, deferredSearch],
|
|
249
|
+
);
|
|
250
|
+
const visibleConversationsById = useMemo(
|
|
251
|
+
() => new Map(visibleConversations.map((conversation) => [conversation.conversationId, conversation])),
|
|
252
|
+
[visibleConversations],
|
|
84
253
|
);
|
|
254
|
+
const lookupSelectedConversations = (conversationIds: string[]) =>
|
|
255
|
+
conversationIds
|
|
256
|
+
.map((conversationId) => visibleConversationsById.get(conversationId) ?? null)
|
|
257
|
+
.filter((conversation): conversation is AntigravityConversation => conversation !== null);
|
|
258
|
+
const openExportForConversations = (selectedConversations: AntigravityConversation[]) => {
|
|
259
|
+
if (selectedConversations.length === 0) {
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
setPendingExport(buildConversationExport(selectedConversations));
|
|
264
|
+
};
|
|
265
|
+
const openDeleteForConversations = (
|
|
266
|
+
selectedConversations: AntigravityConversation[],
|
|
267
|
+
scope: PendingConversationDelete['scope'],
|
|
268
|
+
) => {
|
|
269
|
+
if (selectedConversations.length > 0) {
|
|
270
|
+
setPendingDelete({ conversations: selectedConversations, scope });
|
|
271
|
+
}
|
|
272
|
+
};
|
|
85
273
|
|
|
86
274
|
return (
|
|
87
275
|
<div className="space-y-6">
|
|
88
276
|
<PageHeader
|
|
89
277
|
actions={
|
|
90
|
-
<
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
278
|
+
<div className="flex flex-col gap-2 sm:flex-row">
|
|
279
|
+
<Button
|
|
280
|
+
className="rounded-full"
|
|
281
|
+
disabled={deleteMutation.isPending || conversations.length === 0}
|
|
282
|
+
type="button"
|
|
283
|
+
variant="destructive"
|
|
284
|
+
onClick={() => openDeleteForConversations(conversations, 'all')}
|
|
285
|
+
>
|
|
286
|
+
<Trash2 className="size-4" />
|
|
287
|
+
Delete all
|
|
288
|
+
</Button>
|
|
289
|
+
<ListSearchInput
|
|
290
|
+
placeholder="Search title, id, or transcript source"
|
|
291
|
+
value={searchInput}
|
|
292
|
+
onValueChange={setSearchInput}
|
|
293
|
+
/>
|
|
294
|
+
</div>
|
|
95
295
|
}
|
|
96
296
|
eyebrow="Antigravity workspace"
|
|
97
297
|
subtitle="Inspect conversation coverage across Antigravity transcripts, raw payloads, and generated artifacts."
|
|
@@ -103,25 +303,63 @@ function AntigravityWorkspacePage() {
|
|
|
103
303
|
<AntigravityConversationsTable
|
|
104
304
|
conversations={visibleConversations}
|
|
105
305
|
decryptionState={decryptionState}
|
|
306
|
+
onDeleteConversation={(conversation) => openDeleteForConversations([conversation], 'selected')}
|
|
307
|
+
onDeleteConversations={(conversationIds) =>
|
|
308
|
+
openDeleteForConversations(lookupSelectedConversations(conversationIds), 'selected')
|
|
309
|
+
}
|
|
106
310
|
onExportArtifacts={(conversation) => exportArtifactsMutation.mutate(conversation)}
|
|
107
311
|
onExportConversation={(conversation) => exportConversationMutation.mutate(conversation)}
|
|
312
|
+
onExportConversations={(conversationIds) =>
|
|
313
|
+
openExportForConversations(lookupSelectedConversations(conversationIds))
|
|
314
|
+
}
|
|
108
315
|
/>
|
|
109
316
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
</p>
|
|
116
|
-
) : null}
|
|
317
|
+
<AntigravityWorkspaceErrors
|
|
318
|
+
artifactError={exportArtifactsMutation.isError ? exportArtifactsMutation.error : null}
|
|
319
|
+
batchExportError={exportConversationsMutation.isError ? exportConversationsMutation.error : null}
|
|
320
|
+
conversationError={exportConversationMutation.isError ? exportConversationMutation.error : null}
|
|
321
|
+
/>
|
|
117
322
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
323
|
+
<ExportDialog
|
|
324
|
+
forceZipArchive={pendingExport ? pendingExport.conversationIds.length > 1 : false}
|
|
325
|
+
open={pendingExport !== null}
|
|
326
|
+
pending={exportConversationsMutation.isPending}
|
|
327
|
+
title={pendingExport ? `Export ${pendingExport.label}` : 'Export conversation'}
|
|
328
|
+
onExport={(options) => exportConversationsMutation.mutate(options)}
|
|
329
|
+
onOpenChange={(open) => {
|
|
330
|
+
if (!open) {
|
|
331
|
+
setPendingExport(null);
|
|
332
|
+
exportConversationsMutation.reset();
|
|
333
|
+
}
|
|
334
|
+
}}
|
|
335
|
+
/>
|
|
336
|
+
|
|
337
|
+
<DeleteConfirmDialog
|
|
338
|
+
confirmLabel={getDeleteConfirmLabel(pendingDelete, deleteMutation.isPending)}
|
|
339
|
+
description={getDeleteDescription(pendingDelete)}
|
|
340
|
+
errorMessage={
|
|
341
|
+
deleteMutation.isError
|
|
342
|
+
? deleteMutation.error instanceof Error
|
|
343
|
+
? deleteMutation.error.message
|
|
344
|
+
: 'Conversation delete failed'
|
|
345
|
+
: null
|
|
346
|
+
}
|
|
347
|
+
open={pendingDelete !== null}
|
|
348
|
+
title={getDeleteTitle(pendingDelete)}
|
|
349
|
+
onConfirm={() => {
|
|
350
|
+
if (pendingDelete) {
|
|
351
|
+
deleteMutation.mutate(
|
|
352
|
+
pendingDelete.conversations.map((conversation) => conversation.conversationId),
|
|
353
|
+
);
|
|
354
|
+
}
|
|
355
|
+
}}
|
|
356
|
+
onOpenChange={(open) => {
|
|
357
|
+
if (!open) {
|
|
358
|
+
setPendingDelete(null);
|
|
359
|
+
deleteMutation.reset();
|
|
360
|
+
}
|
|
361
|
+
}}
|
|
362
|
+
/>
|
|
125
363
|
</div>
|
|
126
364
|
);
|
|
127
365
|
}
|
|
@@ -3,6 +3,10 @@ import { createFileRoute } from '@tanstack/react-router';
|
|
|
3
3
|
export const Route = createFileRoute('/api/v1/conversations/$source/$id')({
|
|
4
4
|
server: {
|
|
5
5
|
handlers: {
|
|
6
|
+
DELETE: async ({ request }) => {
|
|
7
|
+
const { handleConversationApiRequest } = await import('@spiracha/lib/conversation-api');
|
|
8
|
+
return handleConversationApiRequest(request);
|
|
9
|
+
},
|
|
6
10
|
GET: async ({ request }) => {
|
|
7
11
|
const { handleConversationApiRequest } = await import('@spiracha/lib/conversation-api');
|
|
8
12
|
return handleConversationApiRequest(request);
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { createFileRoute } from '@tanstack/react-router';
|
|
2
|
+
|
|
3
|
+
export const Route = createFileRoute('/api/v1/conversations/delete')({
|
|
4
|
+
server: {
|
|
5
|
+
handlers: {
|
|
6
|
+
POST: async ({ request }) => {
|
|
7
|
+
const { handleConversationApiRequest } = await import('@spiracha/lib/conversation-api');
|
|
8
|
+
return handleConversationApiRequest(request);
|
|
9
|
+
},
|
|
10
|
+
},
|
|
11
|
+
},
|
|
12
|
+
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { createFileRoute } from '@tanstack/react-router';
|
|
2
|
+
|
|
3
|
+
export const Route = createFileRoute('/api/v1/conversations/export')({
|
|
4
|
+
server: {
|
|
5
|
+
handlers: {
|
|
6
|
+
POST: async ({ request }) => {
|
|
7
|
+
const { handleConversationApiRequest } = await import('@spiracha/lib/conversation-api');
|
|
8
|
+
return handleConversationApiRequest(request);
|
|
9
|
+
},
|
|
10
|
+
},
|
|
11
|
+
},
|
|
12
|
+
});
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import type { ClaudeCodeSessionTranscript } from '@spiracha/lib/claude-code-exporter-types';
|
|
2
2
|
import type { ThreadEvent, ThreadTranscriptStats } from '@spiracha/lib/codex-browser-types';
|
|
3
|
-
import { useMutation, useSuspenseQuery } from '@tanstack/react-query';
|
|
4
|
-
import { createFileRoute, Link } from '@tanstack/react-router';
|
|
5
|
-
import { Download } from 'lucide-react';
|
|
3
|
+
import { useMutation, useQuery, useQueryClient, useSuspenseQuery } from '@tanstack/react-query';
|
|
4
|
+
import { createFileRoute, Link, useNavigate } from '@tanstack/react-router';
|
|
5
|
+
import { Download, Trash2 } from 'lucide-react';
|
|
6
6
|
import { useMemo, useState } from 'react';
|
|
7
7
|
import { Breadcrumbs } from '#/components/breadcrumbs';
|
|
8
|
+
import { DeleteConfirmDialog } from '#/components/delete-confirm-dialog';
|
|
8
9
|
import { ExportDialog } from '#/components/export-dialog';
|
|
9
10
|
import { JsonPanel } from '#/components/json-panel';
|
|
10
11
|
import { LoadingPanel } from '#/components/loading-panel';
|
|
@@ -12,12 +13,15 @@ import { MetadataSection } from '#/components/metadata-section';
|
|
|
12
13
|
import { MetricCard } from '#/components/metric-card';
|
|
13
14
|
import { PageHeader } from '#/components/page-header';
|
|
14
15
|
import { ReloadErrorPanel } from '#/components/reload-error-panel';
|
|
15
|
-
import { TranscriptView } from '#/components/transcript-view';
|
|
16
|
+
import { DEFAULT_SHOW_USER_MESSAGES, TranscriptView } from '#/components/transcript-view';
|
|
16
17
|
import { Button } from '#/components/ui/button';
|
|
17
18
|
import { Checkbox } from '#/components/ui/checkbox';
|
|
18
19
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '#/components/ui/tabs';
|
|
19
|
-
import {
|
|
20
|
-
|
|
20
|
+
import {
|
|
21
|
+
claudeCodeSessionDetailQueryOptions,
|
|
22
|
+
claudeCodeSessionTranscriptQueryOptions,
|
|
23
|
+
} from '#/lib/claude-code-queries';
|
|
24
|
+
import { deleteClaudeCodeSessionFn, exportClaudeCodeSessionFn } from '#/lib/claude-code-server';
|
|
21
25
|
import {
|
|
22
26
|
claudeCodeTranscriptToThreadEvents,
|
|
23
27
|
getClaudeCodeThreadTranscriptStats,
|
|
@@ -169,6 +173,20 @@ const ClaudeCodeTranscriptControls = ({
|
|
|
169
173
|
};
|
|
170
174
|
|
|
171
175
|
function ClaudeCodeRawPanels({ detail, events }: { detail: ClaudeCodeSessionTranscript; events: ThreadEvent[] }) {
|
|
176
|
+
if (detail.rawPayloadsOmitted) {
|
|
177
|
+
return (
|
|
178
|
+
<section className="rounded-[1.6rem] border border-[var(--border)] bg-[var(--panel)] p-5 shadow-[var(--panel-shadow)]">
|
|
179
|
+
<h3 className="font-semibold text-[var(--muted-foreground)] text-sm uppercase tracking-[0.18em]">
|
|
180
|
+
Raw payloads omitted
|
|
181
|
+
</h3>
|
|
182
|
+
<p className="mt-4 text-[var(--muted-foreground)] text-sm">
|
|
183
|
+
This Claude Code session is large, so raw JSON payload copies were omitted from the browser detail
|
|
184
|
+
response. Export still reads the full source session from disk.
|
|
185
|
+
</p>
|
|
186
|
+
</section>
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
|
|
172
190
|
return (
|
|
173
191
|
<div className="space-y-4">
|
|
174
192
|
<JsonPanel title="Session summary" value={detail.session} />
|
|
@@ -179,14 +197,57 @@ function ClaudeCodeRawPanels({ detail, events }: { detail: ClaudeCodeSessionTran
|
|
|
179
197
|
);
|
|
180
198
|
}
|
|
181
199
|
|
|
200
|
+
const ClaudeCodeTranscriptPreviewNotice = ({
|
|
201
|
+
fullTranscriptLoaded,
|
|
202
|
+
omittedEntryCount,
|
|
203
|
+
pending,
|
|
204
|
+
onLoad,
|
|
205
|
+
}: {
|
|
206
|
+
fullTranscriptLoaded: boolean;
|
|
207
|
+
omittedEntryCount: number;
|
|
208
|
+
pending: boolean;
|
|
209
|
+
onLoad: () => void;
|
|
210
|
+
}) => {
|
|
211
|
+
const buttonLabel = fullTranscriptLoaded
|
|
212
|
+
? 'Full transcript loaded'
|
|
213
|
+
: pending
|
|
214
|
+
? 'Loading full transcript...'
|
|
215
|
+
: 'Load Full Transcript';
|
|
216
|
+
|
|
217
|
+
return (
|
|
218
|
+
<section className="rounded-[1.6rem] border border-[var(--border)] bg-[var(--panel)] p-5 shadow-[var(--panel-shadow)]">
|
|
219
|
+
<h3 className="font-semibold text-base">Showing a compact transcript preview</h3>
|
|
220
|
+
<p className="mt-2 text-[var(--muted-foreground)] text-sm leading-6">
|
|
221
|
+
Spiracha omitted {formatNumber(omittedEntryCount)} internal transcript entries from the initial page
|
|
222
|
+
load. The preview keeps the beginning and latest activity.
|
|
223
|
+
</p>
|
|
224
|
+
<div className="mt-4">
|
|
225
|
+
<Button disabled={pending || fullTranscriptLoaded} variant="outline" onClick={onLoad}>
|
|
226
|
+
{buttonLabel}
|
|
227
|
+
</Button>
|
|
228
|
+
</div>
|
|
229
|
+
</section>
|
|
230
|
+
);
|
|
231
|
+
};
|
|
232
|
+
|
|
182
233
|
function ClaudeCodeSessionDetailPage() {
|
|
183
|
-
const
|
|
234
|
+
const navigate = useNavigate();
|
|
235
|
+
const queryClient = useQueryClient();
|
|
236
|
+
const params = Route.useParams();
|
|
237
|
+
const initialDetail = useSuspenseQuery(claudeCodeSessionDetailQueryOptions(params.sessionId)).data;
|
|
238
|
+
const [shouldLoadFullTranscript, setShouldLoadFullTranscript] = useState(false);
|
|
239
|
+
const fullTranscriptQuery = useQuery({
|
|
240
|
+
...claudeCodeSessionTranscriptQueryOptions(params.sessionId),
|
|
241
|
+
enabled: shouldLoadFullTranscript,
|
|
242
|
+
});
|
|
243
|
+
const detail = fullTranscriptQuery.data ?? initialDetail;
|
|
244
|
+
const [deleteOpen, setDeleteOpen] = useState(false);
|
|
184
245
|
const [pendingExport, setPendingExport] = useState(false);
|
|
185
246
|
const [showToolCalls, setShowToolCalls] = useState(false);
|
|
186
247
|
const [showCommentary, setShowCommentary] = useState(false);
|
|
187
248
|
const [showExtraEvents, setShowExtraEvents] = useState(false);
|
|
188
249
|
const [showRawJson, setShowRawJson] = useState(false);
|
|
189
|
-
const [showUserMessages, setShowUserMessages] = useState(
|
|
250
|
+
const [showUserMessages, setShowUserMessages] = useState(DEFAULT_SHOW_USER_MESSAGES);
|
|
190
251
|
const transcriptEvents = useMemo(() => claudeCodeTranscriptToThreadEvents(detail), [detail]);
|
|
191
252
|
const transcriptStats = useMemo(() => getClaudeCodeThreadTranscriptStats(transcriptEvents), [transcriptEvents]);
|
|
192
253
|
|
|
@@ -214,19 +275,47 @@ function ClaudeCodeSessionDetailPage() {
|
|
|
214
275
|
},
|
|
215
276
|
});
|
|
216
277
|
|
|
278
|
+
const deleteSessionMutation = useMutation({
|
|
279
|
+
mutationFn: () => deleteClaudeCodeSessionFn({ data: { sessionId: detail.session.sessionId } }),
|
|
280
|
+
onSuccess: async () => {
|
|
281
|
+
await Promise.all([
|
|
282
|
+
queryClient.invalidateQueries({ queryKey: ['claude-code-workspaces'] }),
|
|
283
|
+
queryClient.invalidateQueries({ queryKey: ['claude-code-sessions', detail.session.workspaceKey] }),
|
|
284
|
+
queryClient.invalidateQueries({ queryKey: ['claude-code-session', detail.session.sessionId] }),
|
|
285
|
+
queryClient.invalidateQueries({ queryKey: ['claude-code-session', params.sessionId] }),
|
|
286
|
+
queryClient.invalidateQueries({ queryKey: ['claude-code-session-transcript', params.sessionId] }),
|
|
287
|
+
]);
|
|
288
|
+
navigate({
|
|
289
|
+
params: { workspaceKey: detail.session.workspaceKey },
|
|
290
|
+
to: '/claude-code/$workspaceKey',
|
|
291
|
+
});
|
|
292
|
+
},
|
|
293
|
+
});
|
|
294
|
+
|
|
217
295
|
return (
|
|
218
296
|
<div className="space-y-6">
|
|
219
297
|
<PageHeader
|
|
220
298
|
actions={
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
299
|
+
<>
|
|
300
|
+
<Button
|
|
301
|
+
className="rounded-full"
|
|
302
|
+
type="button"
|
|
303
|
+
variant="outline"
|
|
304
|
+
onClick={() => setPendingExport(true)}
|
|
305
|
+
>
|
|
306
|
+
<Download className="mr-2 size-4" />
|
|
307
|
+
Export
|
|
308
|
+
</Button>
|
|
309
|
+
<Button
|
|
310
|
+
className="rounded-full border-[var(--destructive)]/20 text-[var(--destructive)]"
|
|
311
|
+
type="button"
|
|
312
|
+
variant="outline"
|
|
313
|
+
onClick={() => setDeleteOpen(true)}
|
|
314
|
+
>
|
|
315
|
+
<Trash2 className="mr-2 size-4" />
|
|
316
|
+
Delete
|
|
317
|
+
</Button>
|
|
318
|
+
</>
|
|
230
319
|
}
|
|
231
320
|
breadcrumb={
|
|
232
321
|
<Breadcrumbs
|
|
@@ -280,6 +369,22 @@ function ClaudeCodeSessionDetailPage() {
|
|
|
280
369
|
onShowToolCallsChange={setShowToolCalls}
|
|
281
370
|
onShowUserMessagesChange={setShowUserMessages}
|
|
282
371
|
/>
|
|
372
|
+
{initialDetail.isPartial ? (
|
|
373
|
+
<ClaudeCodeTranscriptPreviewNotice
|
|
374
|
+
fullTranscriptLoaded={Boolean(fullTranscriptQuery.data)}
|
|
375
|
+
omittedEntryCount={initialDetail.omittedEntryCount ?? 0}
|
|
376
|
+
pending={fullTranscriptQuery.isFetching}
|
|
377
|
+
onLoad={() => setShouldLoadFullTranscript(true)}
|
|
378
|
+
/>
|
|
379
|
+
) : null}
|
|
380
|
+
{fullTranscriptQuery.isError ? (
|
|
381
|
+
<p className="text-[var(--destructive)] text-sm">
|
|
382
|
+
Failed to load the full transcript:{' '}
|
|
383
|
+
{fullTranscriptQuery.error instanceof Error
|
|
384
|
+
? fullTranscriptQuery.error.message
|
|
385
|
+
: 'Unknown error'}
|
|
386
|
+
</p>
|
|
387
|
+
) : null}
|
|
283
388
|
{transcriptEvents.length > 0 ? (
|
|
284
389
|
<TranscriptView
|
|
285
390
|
assistantModel={detail.session.model}
|
|
@@ -337,6 +442,27 @@ function ClaudeCodeSessionDetailPage() {
|
|
|
337
442
|
}
|
|
338
443
|
}}
|
|
339
444
|
/>
|
|
445
|
+
|
|
446
|
+
<DeleteConfirmDialog
|
|
447
|
+
confirmLabel={deleteSessionMutation.isPending ? 'Deleting...' : 'Delete session'}
|
|
448
|
+
description="Permanently delete this Claude Code session from disk. This removes the session JSONL file."
|
|
449
|
+
errorMessage={
|
|
450
|
+
deleteSessionMutation.isError
|
|
451
|
+
? deleteSessionMutation.error instanceof Error
|
|
452
|
+
? deleteSessionMutation.error.message
|
|
453
|
+
: 'Session delete failed'
|
|
454
|
+
: null
|
|
455
|
+
}
|
|
456
|
+
open={deleteOpen}
|
|
457
|
+
title="Delete this Claude Code session?"
|
|
458
|
+
onConfirm={() => deleteSessionMutation.mutate()}
|
|
459
|
+
onOpenChange={(open) => {
|
|
460
|
+
setDeleteOpen(open);
|
|
461
|
+
if (!open) {
|
|
462
|
+
deleteSessionMutation.reset();
|
|
463
|
+
}
|
|
464
|
+
}}
|
|
465
|
+
/>
|
|
340
466
|
</div>
|
|
341
467
|
);
|
|
342
468
|
}
|