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.
Files changed (162) hide show
  1. package/AGENTS.md +27 -12
  2. package/README.md +42 -19
  3. package/apps/ui/AGENTS.md +18 -10
  4. package/apps/ui/README.md +36 -11
  5. package/apps/ui/src/components/analytics-breakdowns.tsx +106 -0
  6. package/apps/ui/src/components/antigravity-conversations-table.tsx +51 -11
  7. package/apps/ui/src/components/antigravity-workspaces-table.tsx +1 -1
  8. package/apps/ui/src/components/app-shell.tsx +46 -3
  9. package/apps/ui/src/components/breadcrumbs.tsx +21 -3
  10. package/apps/ui/src/components/claude-code-sessions-table.tsx +42 -6
  11. package/apps/ui/src/components/claude-code-workspaces-table.tsx +3 -1
  12. package/apps/ui/src/components/cursor-threads-table.tsx +10 -47
  13. package/apps/ui/src/components/data-table.tsx +23 -5
  14. package/apps/ui/src/components/delete-confirm-dialog.tsx +4 -3
  15. package/apps/ui/src/components/export-dialog.tsx +79 -74
  16. package/apps/ui/src/components/grok-sessions-table.tsx +146 -0
  17. package/apps/ui/src/components/grok-workspaces-table.tsx +53 -0
  18. package/apps/ui/src/components/kiro-sessions-table.tsx +40 -4
  19. package/apps/ui/src/components/opencode-sessions-table.tsx +40 -4
  20. package/apps/ui/src/components/page-header.tsx +15 -9
  21. package/apps/ui/src/components/projects-table.tsx +4 -1
  22. package/apps/ui/src/components/qoder-sessions-table.tsx +23 -2
  23. package/apps/ui/src/components/qoder-workspaces-table.tsx +5 -1
  24. package/apps/ui/src/components/route-error-panel.tsx +13 -0
  25. package/apps/ui/src/components/selection-actions-toolbar.tsx +80 -0
  26. package/apps/ui/src/components/thread-goals-panel.tsx +50 -0
  27. package/apps/ui/src/components/thread-tools-panel.tsx +142 -0
  28. package/apps/ui/src/components/threads-table.tsx +13 -47
  29. package/apps/ui/src/components/transcript-view.tsx +148 -52
  30. package/apps/ui/src/components/ui/select.tsx +1 -1
  31. package/apps/ui/src/lib/antigravity-conversation-state.ts +17 -4
  32. package/apps/ui/src/lib/antigravity-server.ts +209 -11
  33. package/apps/ui/src/lib/antigravity-transcript-events.ts +57 -73
  34. package/apps/ui/src/lib/claude-code-queries.ts +8 -0
  35. package/apps/ui/src/lib/claude-code-server.ts +185 -7
  36. package/apps/ui/src/lib/claude-code-transcript-events.ts +8 -1
  37. package/apps/ui/src/lib/codex-queries.ts +16 -0
  38. package/apps/ui/src/lib/codex-server.ts +134 -52
  39. package/apps/ui/src/lib/cursor-server.ts +68 -53
  40. package/apps/ui/src/lib/cursor-transcript-events.ts +11 -86
  41. package/apps/ui/src/lib/delete-batch.ts +52 -0
  42. package/apps/ui/src/lib/error-presentation.ts +43 -0
  43. package/apps/ui/src/lib/export-mutation.ts +20 -0
  44. package/apps/ui/src/lib/export-options.ts +15 -0
  45. package/apps/ui/src/lib/formatters.ts +3 -5
  46. package/apps/ui/src/lib/grok-queries.ts +22 -0
  47. package/apps/ui/src/lib/grok-server.ts +168 -0
  48. package/apps/ui/src/lib/grok-transcript-events.ts +149 -0
  49. package/apps/ui/src/lib/kiro-server.ts +97 -7
  50. package/apps/ui/src/lib/mutation-error.ts +15 -0
  51. package/apps/ui/src/lib/opencode-server.ts +96 -7
  52. package/apps/ui/src/lib/path-utils.ts +1 -1
  53. package/apps/ui/src/lib/qoder-queries.ts +15 -0
  54. package/apps/ui/src/lib/qoder-server.ts +67 -11
  55. package/apps/ui/src/lib/qoder-transcript-events.ts +1 -1
  56. package/apps/ui/src/lib/route-search.ts +114 -0
  57. package/apps/ui/src/lib/route-state-reset.tsx +10 -0
  58. package/apps/ui/src/lib/settings-server.ts +33 -0
  59. package/apps/ui/src/lib/settings-store.tsx +82 -38
  60. package/apps/ui/src/lib/settings.ts +65 -0
  61. package/apps/ui/src/lib/source-session-export-server.ts +86 -3
  62. package/apps/ui/src/lib/thread-metadata.ts +22 -0
  63. package/apps/ui/src/lib/thread-transcript-load.ts +15 -0
  64. package/apps/ui/src/lib/thread-transcript-stats.ts +10 -1
  65. package/apps/ui/src/lib/workspace-delete-navigation.ts +20 -0
  66. package/apps/ui/src/routeTree.gen.ts +341 -234
  67. package/apps/ui/src/routes/__root.tsx +12 -15
  68. package/apps/ui/src/routes/analytics.tsx +10 -61
  69. package/apps/ui/src/routes/antigravity-conversations.$conversationId.tsx +149 -29
  70. package/apps/ui/src/routes/antigravity.$workspaceKey.tsx +262 -42
  71. package/apps/ui/src/routes/antigravity.index.tsx +2 -2
  72. package/apps/ui/src/routes/api.v1.conversations.$source.$id.ts +4 -0
  73. package/apps/ui/src/routes/api.v1.conversations.delete.ts +12 -0
  74. package/apps/ui/src/routes/api.v1.conversations.export.ts +12 -0
  75. package/apps/ui/src/routes/claude-code-sessions.$sessionId.tsx +168 -28
  76. package/apps/ui/src/routes/claude-code.$workspaceKey.tsx +179 -45
  77. package/apps/ui/src/routes/claude-code.index.tsx +2 -2
  78. package/apps/ui/src/routes/codex.$project.tsx +33 -31
  79. package/apps/ui/src/routes/codex.index.tsx +5 -10
  80. package/apps/ui/src/routes/cursor-threads.$composerId.tsx +31 -18
  81. package/apps/ui/src/routes/cursor.$workspaceKey.tsx +44 -28
  82. package/apps/ui/src/routes/cursor.index.tsx +5 -2
  83. package/apps/ui/src/routes/grok-sessions.$sessionId.tsx +401 -0
  84. package/apps/ui/src/routes/grok.$workspaceKey.tsx +295 -0
  85. package/apps/ui/src/routes/grok.index.tsx +48 -0
  86. package/apps/ui/src/routes/index.tsx +2 -18
  87. package/apps/ui/src/routes/kiro-sessions.$sessionId.tsx +91 -27
  88. package/apps/ui/src/routes/kiro.$workspaceKey.tsx +213 -51
  89. package/apps/ui/src/routes/kiro.index.tsx +2 -2
  90. package/apps/ui/src/routes/opencode-sessions.$sessionId.tsx +108 -29
  91. package/apps/ui/src/routes/opencode.$workspaceKey.tsx +231 -55
  92. package/apps/ui/src/routes/opencode.index.tsx +6 -2
  93. package/apps/ui/src/routes/qoder-sessions.$sessionId.tsx +19 -15
  94. package/apps/ui/src/routes/qoder.$workspaceKey.tsx +89 -48
  95. package/apps/ui/src/routes/qoder.index.tsx +2 -2
  96. package/apps/ui/src/routes/threads.$threadId.tsx +620 -105
  97. package/apps/ui/vite.config.ts +22 -12
  98. package/bin/spiracha.ts +28 -1
  99. package/package.json +37 -27
  100. package/src/client.ts +183 -5
  101. package/src/lib/antigravity-db.ts +424 -94
  102. package/src/lib/antigravity-exporter-types.ts +3 -0
  103. package/src/lib/antigravity-keychain.ts +7 -8
  104. package/src/lib/antigravity-projects.ts +201 -0
  105. package/src/lib/antigravity-transcript-phase.ts +47 -0
  106. package/src/lib/claude-code-db.ts +534 -42
  107. package/src/lib/claude-code-exporter-types.ts +5 -0
  108. package/src/lib/claude-code-transcript-phase.ts +60 -1
  109. package/src/lib/claude-code-transcript.ts +8 -5
  110. package/src/lib/codex-analytics.ts +32 -5
  111. package/src/lib/codex-browser-db.ts +540 -137
  112. package/src/lib/codex-browser-export.ts +18 -27
  113. package/src/lib/codex-browser-types.ts +21 -1
  114. package/src/lib/codex-thread-cache.ts +106 -23
  115. package/src/lib/codex-thread-parser.ts +149 -55
  116. package/src/lib/codex-thread-recovery.ts +38 -11
  117. package/src/lib/codex-transcript-filter.ts +31 -0
  118. package/src/lib/codex-transcript-renderer.ts +87 -79
  119. package/src/lib/concurrency.ts +56 -3
  120. package/src/lib/conversation-api.ts +410 -27
  121. package/src/lib/conversation-data/adapter-helpers.ts +25 -3
  122. package/src/lib/conversation-data/antigravity-adapter.ts +70 -26
  123. package/src/lib/conversation-data/claude-code-adapter.ts +69 -12
  124. package/src/lib/conversation-data/codex-adapter.ts +108 -73
  125. package/src/lib/conversation-data/cursor-adapter.ts +77 -29
  126. package/src/lib/conversation-data/grok-adapter.ts +223 -0
  127. package/src/lib/conversation-data/index.ts +105 -11
  128. package/src/lib/conversation-data/kiro-adapter.ts +59 -12
  129. package/src/lib/conversation-data/message-selector.ts +2 -5
  130. package/src/lib/conversation-data/opencode-adapter.ts +67 -17
  131. package/src/lib/conversation-data/path-match.ts +1 -13
  132. package/src/lib/conversation-data/qoder-adapter.ts +67 -37
  133. package/src/lib/conversation-data/types.ts +43 -0
  134. package/src/lib/conversation-zip-export.ts +75 -0
  135. package/src/lib/cursor-db.ts +68 -28
  136. package/src/lib/cursor-id.ts +17 -0
  137. package/src/lib/cursor-recovery.ts +146 -35
  138. package/src/lib/cursor-transcript-phase.ts +40 -0
  139. package/src/lib/cursor-transcript.ts +21 -5
  140. package/src/lib/grok-db.ts +1068 -0
  141. package/src/lib/grok-exporter-types.ts +110 -0
  142. package/src/lib/grok-transcript-phase.ts +61 -0
  143. package/src/lib/grok-transcript.ts +167 -0
  144. package/src/lib/kiro-db.ts +187 -43
  145. package/src/lib/kiro-transcript.ts +0 -4
  146. package/src/lib/model-label.ts +11 -3
  147. package/src/lib/opencode-db.ts +599 -60
  148. package/src/lib/opencode-think-tags.ts +17 -3
  149. package/src/lib/opencode-transcript.ts +8 -5
  150. package/src/lib/portable-path.ts +9 -0
  151. package/src/lib/qoder-acp-client.ts +11 -2
  152. package/src/lib/qoder-db.ts +51 -17
  153. package/src/lib/qoder-transcript-phase.ts +64 -0
  154. package/src/lib/qoder-transcript.ts +30 -2
  155. package/src/lib/shared.ts +63 -15
  156. package/src/lib/sqlite-error.ts +31 -5
  157. package/src/lib/transcript-load-limiter.ts +87 -0
  158. package/src/lib/ui-cache.ts +185 -24
  159. package/src/lib/ui-export-archive.ts +58 -39
  160. package/src/lib/ui-export-files.ts +75 -15
  161. package/src/lib/ui-export-zip.ts +40 -0
  162. package/apps/ui/package.json +0 -65
@@ -1,21 +1,43 @@
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 { useDeferredValue, useState } from 'react';
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
- import { ReloadErrorPanel } from '#/components/reload-error-panel';
13
+ import { RouteErrorPanel } from '#/components/route-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 { exportAntigravityArtifactsFn, exportAntigravityConversationFn } from '#/lib/antigravity-server';
17
- import { downloadTextFile } from '#/lib/download';
20
+ import {
21
+ deleteAntigravityConversationFn,
22
+ deleteAntigravityConversationsFn,
23
+ exportAntigravityArtifactsFn,
24
+ exportAntigravityConversationsFn,
25
+ } from '#/lib/antigravity-server';
26
+ import { downloadTextFile, downloadUrlFile } from '#/lib/download';
27
+ import { createExportSelectionMutationInput, type ExportSelectionMutationInput } from '#/lib/export-mutation';
18
28
  import { matchesTextQuery } from '#/lib/text-filter';
29
+ import { isWorkspaceEmptiedByDelete } from '#/lib/workspace-delete-navigation';
30
+
31
+ type PendingConversationDelete = {
32
+ conversations: AntigravityConversation[];
33
+ scope: 'all' | 'selected';
34
+ };
35
+
36
+ type PendingConversationExport = {
37
+ conversationIds: string[];
38
+ label: string;
39
+ supportsTranscriptFilters: boolean;
40
+ };
19
41
 
20
42
  const findWorkspaceOrThrow = (workspaces: AntigravityWorkspaceGroup[], workspaceKey: string) => {
21
43
  const workspace = workspaces.find((candidate) => candidate.key === workspaceKey);
@@ -26,6 +48,63 @@ const findWorkspaceOrThrow = (workspaces: AntigravityWorkspaceGroup[], workspace
26
48
  return workspace;
27
49
  };
28
50
 
51
+ const buildConversationExport = (selectedConversations: AntigravityConversation[]): PendingConversationExport => ({
52
+ conversationIds: selectedConversations.map((conversation) => conversation.conversationId),
53
+ label:
54
+ selectedConversations.length === 1
55
+ ? selectedConversations[0]!.title
56
+ : `${selectedConversations.length} selected conversations`,
57
+ supportsTranscriptFilters: selectedConversations.every(
58
+ (conversation) => conversation.transcriptSource !== 'safe-storage',
59
+ ),
60
+ });
61
+
62
+ const getDeleteConfirmLabel = (pendingDelete: PendingConversationDelete | null, isPending: boolean) => {
63
+ if (isPending) {
64
+ return 'Deleting...';
65
+ }
66
+
67
+ if (pendingDelete?.scope === 'all') {
68
+ return 'Delete all';
69
+ }
70
+
71
+ return pendingDelete && pendingDelete.conversations.length > 1 ? 'Delete conversations' : 'Delete conversation';
72
+ };
73
+
74
+ const getDeleteDescription = (pendingDelete: PendingConversationDelete | null) => {
75
+ if (!pendingDelete) {
76
+ return 'Permanently delete the selected Antigravity conversations from disk.';
77
+ }
78
+
79
+ if (pendingDelete.scope === 'all') {
80
+ 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.`;
81
+ }
82
+
83
+ if (pendingDelete.conversations.length === 1) {
84
+ 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.`;
85
+ }
86
+
87
+ return `Permanently delete ${pendingDelete.conversations.length} selected Antigravity conversations from disk. This removes their summaries, conversation files, transcript logs, and generated artifacts.`;
88
+ };
89
+
90
+ const getDeleteTitle = (pendingDelete: PendingConversationDelete | null) => {
91
+ if (pendingDelete?.scope === 'all') {
92
+ return `Delete all ${pendingDelete.conversations.length} Antigravity conversations?`;
93
+ }
94
+
95
+ return pendingDelete && pendingDelete.conversations.length > 1
96
+ ? `Delete ${pendingDelete.conversations.length} Antigravity conversations?`
97
+ : 'Delete this Antigravity conversation?';
98
+ };
99
+
100
+ const AntigravityWorkspaceErrors = ({ artifactError }: { artifactError: Error | null }) => {
101
+ if (!artifactError) {
102
+ return null;
103
+ }
104
+
105
+ return <p className="text-[var(--destructive)] text-sm">{artifactError.message}</p>;
106
+ };
107
+
29
108
  export const Route = createFileRoute('/antigravity/$workspaceKey')({
30
109
  component: AntigravityWorkspacePage,
31
110
  errorComponent: AntigravityWorkspaceErrorComponent,
@@ -46,52 +125,142 @@ export const Route = createFileRoute('/antigravity/$workspaceKey')({
46
125
  });
47
126
 
48
127
  function AntigravityWorkspaceErrorComponent({ error }: { error: Error }) {
49
- return <ReloadErrorPanel description={error.message} title="Failed to load Antigravity workspace" />;
128
+ return <RouteErrorPanel error={error} title="Failed to load Antigravity workspace" />;
50
129
  }
51
130
 
52
131
  function AntigravityWorkspacePage() {
132
+ const navigate = useNavigate({ from: Route.fullPath });
53
133
  const params = Route.useParams();
134
+ const queryClient = useQueryClient();
54
135
  const workspaces = useSuspenseQuery(antigravityWorkspacesQueryOptions()).data;
55
136
  const workspace = findWorkspaceOrThrow(workspaces, params.workspaceKey);
56
137
  const conversations = useSuspenseQuery(antigravityConversationsQueryOptions(workspace.key)).data;
57
138
  const decryptionState = useSuspenseQuery(antigravityDecryptionQueryOptions()).data ?? null;
58
139
  const [searchInput, setSearchInput] = useState('');
140
+ const [pendingDelete, setPendingDelete] = useState<PendingConversationDelete | null>(null);
141
+ const [pendingExport, setPendingExport] = useState<PendingConversationExport | null>(null);
59
142
  const deferredSearch = useDeferredValue(searchInput);
60
143
 
61
- const exportConversationMutation = useMutation({
144
+ const exportArtifactsMutation = useMutation({
62
145
  mutationFn: (conversation: AntigravityConversation) =>
63
- exportAntigravityConversationFn({ data: { conversationId: conversation.conversationId } }),
146
+ exportAntigravityArtifactsFn({ data: { conversationId: conversation.conversationId } }),
64
147
  onSuccess: (result) => {
65
148
  downloadTextFile(result.filename, result.content, 'text/markdown; charset=utf-8');
66
149
  },
67
150
  });
68
151
 
69
- const exportArtifactsMutation = useMutation({
70
- mutationFn: (conversation: AntigravityConversation) =>
71
- exportAntigravityArtifactsFn({ data: { conversationId: conversation.conversationId } }),
72
- onSuccess: (result) => {
73
- downloadTextFile(result.filename, result.content, 'text/markdown; charset=utf-8');
152
+ const exportConversationsMutation = useMutation({
153
+ mutationFn: async ({ ids, options }: ExportSelectionMutationInput) => {
154
+ const download = await exportAntigravityConversationsFn({
155
+ data: {
156
+ conversationIds: [...ids],
157
+ includeCommentary: options.includeCommentary,
158
+ includeMetadata: options.includeMetadata,
159
+ includeTools: options.includeTools,
160
+ outputFormat: options.outputFormat,
161
+ zipArchive: options.zipArchive,
162
+ },
163
+ });
164
+
165
+ if (download.mode === 'download') {
166
+ downloadTextFile(download.fileName, download.content, download.mimeType);
167
+ return;
168
+ }
169
+
170
+ await downloadUrlFile(download.fileName, download.downloadUrl);
171
+ },
172
+ onSuccess: () => {
173
+ setPendingExport(null);
74
174
  },
75
175
  });
76
176
 
77
- const visibleConversations = conversations.filter((conversation) =>
78
- matchesTextQuery(deferredSearch, [
79
- conversation.title,
80
- conversation.conversationId,
81
- conversation.transcriptSource,
82
- conversation.workspaceLabel,
83
- ]),
177
+ const deleteMutation = useMutation({
178
+ mutationFn: async (conversationIds: string[]) =>
179
+ conversationIds.length === 1
180
+ ? deleteAntigravityConversationFn({ data: { conversationId: conversationIds[0]! } })
181
+ : deleteAntigravityConversationsFn({ data: { conversationIds } }),
182
+ onSettled: async (_result, _error, conversationIds) => {
183
+ await Promise.all([
184
+ queryClient.invalidateQueries({ queryKey: ['antigravity-workspaces'] }),
185
+ queryClient.invalidateQueries({ queryKey: ['antigravity-conversations', workspace.key] }),
186
+ ...conversationIds.map((conversationId) =>
187
+ queryClient.invalidateQueries({
188
+ queryKey: ['antigravity-conversation', conversationId],
189
+ }),
190
+ ),
191
+ ]);
192
+ },
193
+ onSuccess: async (result) => {
194
+ const conversationIds = result.deletedConversationIds;
195
+ const workspaceEmptied = isWorkspaceEmptiedByDelete(
196
+ conversations,
197
+ conversationIds,
198
+ (conversation) => conversation.conversationId,
199
+ );
200
+ setPendingDelete(null);
201
+ if (workspaceEmptied) {
202
+ await navigate({ to: '/antigravity' });
203
+ }
204
+ },
205
+ });
206
+
207
+ const visibleConversations = useMemo(
208
+ () =>
209
+ conversations.filter((conversation) =>
210
+ matchesTextQuery(deferredSearch, [
211
+ conversation.title,
212
+ conversation.conversationId,
213
+ conversation.transcriptSource,
214
+ conversation.workspaceLabel,
215
+ ]),
216
+ ),
217
+ [conversations, deferredSearch],
84
218
  );
219
+ const visibleConversationsById = useMemo(
220
+ () => new Map(visibleConversations.map((conversation) => [conversation.conversationId, conversation])),
221
+ [visibleConversations],
222
+ );
223
+ const lookupSelectedConversations = (conversationIds: string[]) =>
224
+ conversationIds
225
+ .map((conversationId) => visibleConversationsById.get(conversationId) ?? null)
226
+ .filter((conversation): conversation is AntigravityConversation => conversation !== null);
227
+ const openExportForConversations = (selectedConversations: AntigravityConversation[]) => {
228
+ if (selectedConversations.length === 0) {
229
+ return;
230
+ }
231
+
232
+ setPendingExport(buildConversationExport(selectedConversations));
233
+ };
234
+ const openDeleteForConversations = (
235
+ selectedConversations: AntigravityConversation[],
236
+ scope: PendingConversationDelete['scope'],
237
+ ) => {
238
+ if (selectedConversations.length > 0) {
239
+ setPendingDelete({ conversations: selectedConversations, scope });
240
+ }
241
+ };
85
242
 
86
243
  return (
87
244
  <div className="space-y-6">
88
245
  <PageHeader
89
246
  actions={
90
- <ListSearchInput
91
- placeholder="Search title, id, or transcript source"
92
- value={searchInput}
93
- onValueChange={setSearchInput}
94
- />
247
+ <div className="flex flex-col gap-2 sm:flex-row">
248
+ <Button
249
+ className="rounded-full"
250
+ disabled={deleteMutation.isPending || conversations.length === 0}
251
+ type="button"
252
+ variant="destructive"
253
+ onClick={() => openDeleteForConversations(conversations, 'all')}
254
+ >
255
+ <Trash2 className="size-4" />
256
+ Delete all
257
+ </Button>
258
+ <ListSearchInput
259
+ placeholder="Search title, id, or transcript source"
260
+ value={searchInput}
261
+ onValueChange={setSearchInput}
262
+ />
263
+ </div>
95
264
  }
96
265
  eyebrow="Antigravity workspace"
97
266
  subtitle="Inspect conversation coverage across Antigravity transcripts, raw payloads, and generated artifacts."
@@ -103,25 +272,76 @@ function AntigravityWorkspacePage() {
103
272
  <AntigravityConversationsTable
104
273
  conversations={visibleConversations}
105
274
  decryptionState={decryptionState}
275
+ onDeleteConversation={(conversation) => openDeleteForConversations([conversation], 'selected')}
276
+ onDeleteConversations={(conversationIds) =>
277
+ openDeleteForConversations(lookupSelectedConversations(conversationIds), 'selected')
278
+ }
106
279
  onExportArtifacts={(conversation) => exportArtifactsMutation.mutate(conversation)}
107
- onExportConversation={(conversation) => exportConversationMutation.mutate(conversation)}
280
+ onExportConversation={(conversation) => openExportForConversations([conversation])}
281
+ onExportConversations={(conversationIds) =>
282
+ openExportForConversations(lookupSelectedConversations(conversationIds))
283
+ }
108
284
  />
109
285
 
110
- {exportConversationMutation.isError ? (
111
- <p className="text-[var(--destructive)] text-sm">
112
- {exportConversationMutation.error instanceof Error
113
- ? exportConversationMutation.error.message
114
- : 'Conversation export failed'}
115
- </p>
116
- ) : null}
117
-
118
- {exportArtifactsMutation.isError ? (
119
- <p className="text-[var(--destructive)] text-sm">
120
- {exportArtifactsMutation.error instanceof Error
121
- ? exportArtifactsMutation.error.message
122
- : 'Artifact export failed'}
123
- </p>
124
- ) : null}
286
+ <AntigravityWorkspaceErrors
287
+ artifactError={exportArtifactsMutation.isError ? exportArtifactsMutation.error : null}
288
+ />
289
+
290
+ <ExportDialog
291
+ errorMessage={
292
+ exportConversationsMutation.isError
293
+ ? exportConversationsMutation.error instanceof Error
294
+ ? exportConversationsMutation.error.message
295
+ : 'Conversation export failed'
296
+ : null
297
+ }
298
+ forceZipArchive={pendingExport ? pendingExport.conversationIds.length > 1 : false}
299
+ open={pendingExport !== null}
300
+ pending={exportConversationsMutation.isPending}
301
+ showCommentaryOption={pendingExport?.supportsTranscriptFilters ?? true}
302
+ showToolsOption={pendingExport?.supportsTranscriptFilters ?? true}
303
+ title={pendingExport ? `Export ${pendingExport.label}` : 'Export conversation'}
304
+ onExport={(options) => {
305
+ if (pendingExport) {
306
+ exportConversationsMutation.mutate(
307
+ createExportSelectionMutationInput(pendingExport.conversationIds, options),
308
+ );
309
+ }
310
+ }}
311
+ onOpenChange={(open) => {
312
+ if (!open) {
313
+ setPendingExport(null);
314
+ exportConversationsMutation.reset();
315
+ }
316
+ }}
317
+ />
318
+
319
+ <DeleteConfirmDialog
320
+ confirmLabel={getDeleteConfirmLabel(pendingDelete, deleteMutation.isPending)}
321
+ description={getDeleteDescription(pendingDelete)}
322
+ errorMessage={
323
+ deleteMutation.isError
324
+ ? deleteMutation.error instanceof Error
325
+ ? deleteMutation.error.message
326
+ : 'Conversation delete failed'
327
+ : null
328
+ }
329
+ open={pendingDelete !== null}
330
+ title={getDeleteTitle(pendingDelete)}
331
+ onConfirm={() => {
332
+ if (pendingDelete) {
333
+ deleteMutation.mutate(
334
+ pendingDelete.conversations.map((conversation) => conversation.conversationId),
335
+ );
336
+ }
337
+ }}
338
+ onOpenChange={(open) => {
339
+ if (!open) {
340
+ setPendingDelete(null);
341
+ deleteMutation.reset();
342
+ }
343
+ }}
344
+ />
125
345
  </div>
126
346
  );
127
347
  }
@@ -5,7 +5,7 @@ import { AntigravityKeychainPanel } from '#/components/antigravity-keychain-pane
5
5
  import { AntigravityWorkspacesTable } from '#/components/antigravity-workspaces-table';
6
6
  import { ListSearchInput } from '#/components/list-search-input';
7
7
  import { PageHeader } from '#/components/page-header';
8
- import { ReloadErrorPanel } from '#/components/reload-error-panel';
8
+ import { RouteErrorPanel } from '#/components/route-error-panel';
9
9
  import { antigravityWorkspacesQueryOptions } from '#/lib/antigravity-queries';
10
10
  import { matchesTextQuery } from '#/lib/text-filter';
11
11
 
@@ -16,7 +16,7 @@ export const Route = createFileRoute('/antigravity/')({
16
16
  });
17
17
 
18
18
  function AntigravityErrorComponent({ error }: { error: Error }) {
19
- return <ReloadErrorPanel description={error.message} title="Failed to load Antigravity workspaces" />;
19
+ return <RouteErrorPanel error={error} title="Failed to load Antigravity workspaces" />;
20
20
  }
21
21
 
22
22
  function AntigravityPage() {
@@ -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
+ });