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,37 +1,37 @@
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';
11
12
  import { MetadataSection } from '#/components/metadata-section';
12
13
  import { MetricCard } from '#/components/metric-card';
13
14
  import { PageHeader } from '#/components/page-header';
14
- import { ReloadErrorPanel } from '#/components/reload-error-panel';
15
- import { TranscriptView } from '#/components/transcript-view';
15
+ import { RouteErrorPanel } from '#/components/route-error-panel';
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 { claudeCodeSessionDetailQueryOptions } from '#/lib/claude-code-queries';
20
- import { exportClaudeCodeSessionFn } from '#/lib/claude-code-server';
20
+ import {
21
+ claudeCodeSessionDetailQueryOptions,
22
+ claudeCodeSessionTranscriptQueryOptions,
23
+ claudeCodeWorkspacesQueryOptions,
24
+ } from '#/lib/claude-code-queries';
25
+ import { deleteClaudeCodeSessionFn, exportClaudeCodeSessionFn } from '#/lib/claude-code-server';
21
26
  import {
22
27
  claudeCodeTranscriptToThreadEvents,
23
28
  getClaudeCodeThreadTranscriptStats,
24
29
  } from '#/lib/claude-code-transcript-events';
25
30
  import { downloadTextFile, downloadUrlFile } from '#/lib/download';
31
+ import type { ExportDialogOptions } from '#/lib/export-options';
26
32
  import { formatDateTime, formatList, formatNumber, formatTokens } from '#/lib/formatters';
27
-
28
- type ExportDialogOptions = {
29
- includeCommentary: boolean;
30
- includeMetadata: boolean;
31
- includeTools: boolean;
32
- outputFormat: 'md' | 'txt';
33
- zipArchive: boolean;
34
- };
33
+ import { RouteStateResetBoundary } from '#/lib/route-state-reset';
34
+ import { shouldNavigateToSourceIndexAfterDelete } from '#/lib/workspace-delete-navigation';
35
35
 
36
36
  type TranscriptControlsProps = {
37
37
  rawJsonDisabled?: boolean;
@@ -48,7 +48,14 @@ type TranscriptControlsProps = {
48
48
  };
49
49
 
50
50
  export const Route = createFileRoute('/claude-code-sessions/$sessionId')({
51
- component: ClaudeCodeSessionDetailPage,
51
+ component: () => {
52
+ const { sessionId } = Route.useParams();
53
+ return (
54
+ <RouteStateResetBoundary routeKey={sessionId}>
55
+ <ClaudeCodeSessionDetailPage />
56
+ </RouteStateResetBoundary>
57
+ );
58
+ },
52
59
  errorComponent: ClaudeCodeSessionDetailErrorComponent,
53
60
  loader: ({ context, params }) =>
54
61
  context.queryClient.ensureQueryData(claudeCodeSessionDetailQueryOptions(params.sessionId)),
@@ -61,7 +68,7 @@ export const Route = createFileRoute('/claude-code-sessions/$sessionId')({
61
68
  });
62
69
 
63
70
  function ClaudeCodeSessionDetailErrorComponent({ error }: { error: Error }) {
64
- return <ReloadErrorPanel description={error.message} title="Failed to load Claude Code session" />;
71
+ return <RouteErrorPanel error={error} title="Failed to load Claude Code session" />;
65
72
  }
66
73
 
67
74
  const buildSessionMetadata = (detail: ClaudeCodeSessionTranscript) => [
@@ -169,6 +176,20 @@ const ClaudeCodeTranscriptControls = ({
169
176
  };
170
177
 
171
178
  function ClaudeCodeRawPanels({ detail, events }: { detail: ClaudeCodeSessionTranscript; events: ThreadEvent[] }) {
179
+ if (detail.rawPayloadsOmitted) {
180
+ return (
181
+ <section className="rounded-[1.6rem] border border-[var(--border)] bg-[var(--panel)] p-5 shadow-[var(--panel-shadow)]">
182
+ <h3 className="font-semibold text-[var(--muted-foreground)] text-sm uppercase tracking-[0.18em]">
183
+ Raw payloads omitted
184
+ </h3>
185
+ <p className="mt-4 text-[var(--muted-foreground)] text-sm">
186
+ This Claude Code session is large, so raw JSON payload copies were omitted from the browser detail
187
+ response. Export still reads the full source session from disk.
188
+ </p>
189
+ </section>
190
+ );
191
+ }
192
+
172
193
  return (
173
194
  <div className="space-y-4">
174
195
  <JsonPanel title="Session summary" value={detail.session} />
@@ -179,14 +200,57 @@ function ClaudeCodeRawPanels({ detail, events }: { detail: ClaudeCodeSessionTran
179
200
  );
180
201
  }
181
202
 
203
+ const ClaudeCodeTranscriptPreviewNotice = ({
204
+ fullTranscriptLoaded,
205
+ omittedEntryCount,
206
+ pending,
207
+ onLoad,
208
+ }: {
209
+ fullTranscriptLoaded: boolean;
210
+ omittedEntryCount: number;
211
+ pending: boolean;
212
+ onLoad: () => void;
213
+ }) => {
214
+ const buttonLabel = fullTranscriptLoaded
215
+ ? 'Full transcript loaded'
216
+ : pending
217
+ ? 'Loading full transcript...'
218
+ : 'Load Full Transcript';
219
+
220
+ return (
221
+ <section className="rounded-[1.6rem] border border-[var(--border)] bg-[var(--panel)] p-5 shadow-[var(--panel-shadow)]">
222
+ <h3 className="font-semibold text-base">Showing a compact transcript preview</h3>
223
+ <p className="mt-2 text-[var(--muted-foreground)] text-sm leading-6">
224
+ Spiracha omitted {formatNumber(omittedEntryCount)} internal transcript entries from the initial page
225
+ load. The preview keeps the beginning and latest activity.
226
+ </p>
227
+ <div className="mt-4">
228
+ <Button disabled={pending || fullTranscriptLoaded} variant="outline" onClick={onLoad}>
229
+ {buttonLabel}
230
+ </Button>
231
+ </div>
232
+ </section>
233
+ );
234
+ };
235
+
182
236
  function ClaudeCodeSessionDetailPage() {
183
- const detail = useSuspenseQuery(claudeCodeSessionDetailQueryOptions(Route.useParams().sessionId)).data;
237
+ const navigate = useNavigate();
238
+ const queryClient = useQueryClient();
239
+ const params = Route.useParams();
240
+ const initialDetail = useSuspenseQuery(claudeCodeSessionDetailQueryOptions(params.sessionId)).data;
241
+ const [shouldLoadFullTranscript, setShouldLoadFullTranscript] = useState(false);
242
+ const fullTranscriptQuery = useQuery({
243
+ ...claudeCodeSessionTranscriptQueryOptions(params.sessionId),
244
+ enabled: shouldLoadFullTranscript,
245
+ });
246
+ const detail = fullTranscriptQuery.data ?? initialDetail;
247
+ const [deleteOpen, setDeleteOpen] = useState(false);
184
248
  const [pendingExport, setPendingExport] = useState(false);
185
249
  const [showToolCalls, setShowToolCalls] = useState(false);
186
250
  const [showCommentary, setShowCommentary] = useState(false);
187
251
  const [showExtraEvents, setShowExtraEvents] = useState(false);
188
252
  const [showRawJson, setShowRawJson] = useState(false);
189
- const [showUserMessages, setShowUserMessages] = useState(true);
253
+ const [showUserMessages, setShowUserMessages] = useState(DEFAULT_SHOW_USER_MESSAGES);
190
254
  const transcriptEvents = useMemo(() => claudeCodeTranscriptToThreadEvents(detail), [detail]);
191
255
  const transcriptStats = useMemo(() => getClaudeCodeThreadTranscriptStats(transcriptEvents), [transcriptEvents]);
192
256
 
@@ -214,19 +278,58 @@ function ClaudeCodeSessionDetailPage() {
214
278
  },
215
279
  });
216
280
 
281
+ const deleteSessionMutation = useMutation({
282
+ mutationFn: () => deleteClaudeCodeSessionFn({ data: { sessionId: detail.session.sessionId } }),
283
+ onSuccess: async () => {
284
+ await Promise.all([
285
+ queryClient.invalidateQueries({ queryKey: ['claude-code-workspaces'] }),
286
+ queryClient.invalidateQueries({ queryKey: ['claude-code-sessions', detail.session.workspaceKey] }),
287
+ queryClient.invalidateQueries({ queryKey: ['claude-code-session', detail.session.sessionId] }),
288
+ queryClient.invalidateQueries({ queryKey: ['claude-code-session', params.sessionId] }),
289
+ queryClient.invalidateQueries({ queryKey: ['claude-code-session-transcript', params.sessionId] }),
290
+ ]);
291
+ const workspaces = await queryClient.fetchQuery(claudeCodeWorkspacesQueryOptions());
292
+ if (
293
+ shouldNavigateToSourceIndexAfterDelete(
294
+ workspaces,
295
+ detail.session.workspaceKey,
296
+ (workspace) => workspace.key,
297
+ )
298
+ ) {
299
+ navigate({ to: '/claude-code' });
300
+ return;
301
+ }
302
+ navigate({
303
+ params: { workspaceKey: detail.session.workspaceKey },
304
+ to: '/claude-code/$workspaceKey',
305
+ });
306
+ },
307
+ });
308
+
217
309
  return (
218
310
  <div className="space-y-6">
219
311
  <PageHeader
220
312
  actions={
221
- <Button
222
- className="rounded-full"
223
- type="button"
224
- variant="outline"
225
- onClick={() => setPendingExport(true)}
226
- >
227
- <Download className="mr-2 size-4" />
228
- Export
229
- </Button>
313
+ <>
314
+ <Button
315
+ className="rounded-full"
316
+ type="button"
317
+ variant="outline"
318
+ onClick={() => setPendingExport(true)}
319
+ >
320
+ <Download className="mr-2 size-4" />
321
+ Export
322
+ </Button>
323
+ <Button
324
+ className="rounded-full border-[var(--destructive)]/20 text-[var(--destructive)]"
325
+ type="button"
326
+ variant="outline"
327
+ onClick={() => setDeleteOpen(true)}
328
+ >
329
+ <Trash2 className="mr-2 size-4" />
330
+ Delete
331
+ </Button>
332
+ </>
230
333
  }
231
334
  breadcrumb={
232
335
  <Breadcrumbs
@@ -280,6 +383,22 @@ function ClaudeCodeSessionDetailPage() {
280
383
  onShowToolCallsChange={setShowToolCalls}
281
384
  onShowUserMessagesChange={setShowUserMessages}
282
385
  />
386
+ {initialDetail.isPartial ? (
387
+ <ClaudeCodeTranscriptPreviewNotice
388
+ fullTranscriptLoaded={Boolean(fullTranscriptQuery.data)}
389
+ omittedEntryCount={initialDetail.omittedEntryCount ?? 0}
390
+ pending={fullTranscriptQuery.isFetching}
391
+ onLoad={() => setShouldLoadFullTranscript(true)}
392
+ />
393
+ ) : null}
394
+ {fullTranscriptQuery.isError ? (
395
+ <p className="text-[var(--destructive)] text-sm">
396
+ Failed to load the full transcript:{' '}
397
+ {fullTranscriptQuery.error instanceof Error
398
+ ? fullTranscriptQuery.error.message
399
+ : 'Unknown error'}
400
+ </p>
401
+ ) : null}
283
402
  {transcriptEvents.length > 0 ? (
284
403
  <TranscriptView
285
404
  assistantModel={detail.session.model}
@@ -337,6 +456,27 @@ function ClaudeCodeSessionDetailPage() {
337
456
  }
338
457
  }}
339
458
  />
459
+
460
+ <DeleteConfirmDialog
461
+ confirmLabel={deleteSessionMutation.isPending ? 'Deleting...' : 'Delete session'}
462
+ description="Permanently delete this Claude Code session from disk. This removes the session JSONL file."
463
+ errorMessage={
464
+ deleteSessionMutation.isError
465
+ ? deleteSessionMutation.error instanceof Error
466
+ ? deleteSessionMutation.error.message
467
+ : 'Session delete failed'
468
+ : null
469
+ }
470
+ open={deleteOpen}
471
+ title="Delete this Claude Code session?"
472
+ onConfirm={() => deleteSessionMutation.mutate()}
473
+ onOpenChange={(open) => {
474
+ setDeleteOpen(open);
475
+ if (!open) {
476
+ deleteSessionMutation.reset();
477
+ }
478
+ }}
479
+ />
340
480
  </div>
341
481
  );
342
482
  }
@@ -1,24 +1,33 @@
1
1
  import type { ClaudeCodeSessionSummary, ClaudeCodeWorkspaceGroup } from '@spiracha/lib/claude-code-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 { useDeferredValue, useMemo, useState } from 'react';
5
5
  import { ClaudeCodeSessionsTable } from '#/components/claude-code-sessions-table';
6
+ import { DeleteConfirmDialog } from '#/components/delete-confirm-dialog';
6
7
  import { ExportDialog } from '#/components/export-dialog';
7
8
  import { ListSearchInput } from '#/components/list-search-input';
8
9
  import { LoadingPanel } from '#/components/loading-panel';
9
10
  import { PageHeader } from '#/components/page-header';
10
- import { ReloadErrorPanel } from '#/components/reload-error-panel';
11
+ import { RouteErrorPanel } from '#/components/route-error-panel';
11
12
  import { claudeCodeSessionsQueryOptions, claudeCodeWorkspacesQueryOptions } from '#/lib/claude-code-queries';
12
- import { exportClaudeCodeSessionFn } from '#/lib/claude-code-server';
13
+ import {
14
+ deleteClaudeCodeSessionFn,
15
+ deleteClaudeCodeSessionsFn,
16
+ exportClaudeCodeSessionFn,
17
+ exportClaudeCodeSessionsFn,
18
+ } from '#/lib/claude-code-server';
13
19
  import { downloadTextFile, downloadUrlFile } from '#/lib/download';
20
+ import { createExportSelectionMutationInput, type ExportSelectionMutationInput } from '#/lib/export-mutation';
14
21
  import { matchesTextQuery } from '#/lib/text-filter';
22
+ import { isWorkspaceEmptiedByDelete } from '#/lib/workspace-delete-navigation';
15
23
 
16
- type ExportDialogOptions = {
17
- includeCommentary: boolean;
18
- includeMetadata: boolean;
19
- includeTools: boolean;
20
- outputFormat: 'md' | 'txt';
21
- zipArchive: boolean;
24
+ type PendingSessionDelete = {
25
+ sessions: ClaudeCodeSessionSummary[];
26
+ };
27
+
28
+ type PendingSessionExport = {
29
+ label: string;
30
+ sessionIds: string[];
22
31
  };
23
32
 
24
33
  const findWorkspaceOrThrow = (workspaces: ClaudeCodeWorkspaceGroup[], workspaceKey: string) => {
@@ -30,6 +39,36 @@ const findWorkspaceOrThrow = (workspaces: ClaudeCodeWorkspaceGroup[], workspaceK
30
39
  return workspace;
31
40
  };
32
41
 
42
+ const buildSessionExport = (selectedSessions: ClaudeCodeSessionSummary[]): PendingSessionExport => ({
43
+ label: selectedSessions.length === 1 ? selectedSessions[0]!.title : `${selectedSessions.length} selected sessions`,
44
+ sessionIds: selectedSessions.map((session) => session.sessionId),
45
+ });
46
+
47
+ const getDeleteConfirmLabel = (pendingDelete: PendingSessionDelete | null, isPending: boolean) => {
48
+ if (isPending) {
49
+ return 'Deleting...';
50
+ }
51
+
52
+ return pendingDelete && pendingDelete.sessions.length > 1 ? 'Delete sessions' : 'Delete session';
53
+ };
54
+
55
+ const getDeleteDescription = (pendingDelete: PendingSessionDelete | null) => {
56
+ if (!pendingDelete) {
57
+ return 'Permanently delete the selected Claude Code sessions from disk.';
58
+ }
59
+
60
+ if (pendingDelete.sessions.length === 1) {
61
+ return `Permanently delete "${pendingDelete.sessions[0]!.title}" from Claude Code history. This removes the session JSONL file from disk.`;
62
+ }
63
+
64
+ return `Permanently delete ${pendingDelete.sessions.length} selected Claude Code sessions from disk.`;
65
+ };
66
+
67
+ const getDeleteTitle = (pendingDelete: PendingSessionDelete | null) =>
68
+ pendingDelete && pendingDelete.sessions.length > 1
69
+ ? `Delete ${pendingDelete.sessions.length} Claude Code sessions?`
70
+ : 'Delete this Claude Code session?';
71
+
33
72
  export const Route = createFileRoute('/claude-code/$workspaceKey')({
34
73
  component: ClaudeCodeWorkspacePage,
35
74
  errorComponent: ClaudeCodeWorkspaceErrorComponent,
@@ -44,34 +83,45 @@ export const Route = createFileRoute('/claude-code/$workspaceKey')({
44
83
  });
45
84
 
46
85
  function ClaudeCodeWorkspaceErrorComponent({ error }: { error: Error }) {
47
- return <ReloadErrorPanel description={error.message} title="Failed to load Claude Code workspace" />;
86
+ return <RouteErrorPanel error={error} title="Failed to load Claude Code workspace" />;
48
87
  }
49
88
 
50
89
  function ClaudeCodeWorkspacePage() {
90
+ const navigate = useNavigate({ from: Route.fullPath });
51
91
  const params = Route.useParams();
92
+ const queryClient = useQueryClient();
52
93
  const workspaces = useSuspenseQuery(claudeCodeWorkspacesQueryOptions()).data;
53
94
  const workspace = findWorkspaceOrThrow(workspaces, params.workspaceKey);
54
95
  const sessions = useSuspenseQuery(claudeCodeSessionsQueryOptions(workspace.key)).data;
55
96
  const [searchInput, setSearchInput] = useState('');
56
- const [pendingExport, setPendingExport] = useState<ClaudeCodeSessionSummary | null>(null);
97
+ const [pendingDelete, setPendingDelete] = useState<PendingSessionDelete | null>(null);
98
+ const [pendingExport, setPendingExport] = useState<PendingSessionExport | null>(null);
57
99
  const deferredSearch = useDeferredValue(searchInput);
58
100
 
59
101
  const exportMutation = useMutation({
60
- mutationFn: async (options: ExportDialogOptions) => {
61
- if (!pendingExport) {
62
- throw new Error('No Claude Code session selected for export');
63
- }
64
-
65
- const download = await exportClaudeCodeSessionFn({
66
- data: {
67
- includeCommentary: options.includeCommentary,
68
- includeMetadata: options.includeMetadata,
69
- includeTools: options.includeTools,
70
- outputFormat: options.outputFormat,
71
- sessionId: pendingExport.sessionId,
72
- zipArchive: options.zipArchive,
73
- },
74
- });
102
+ mutationFn: async ({ ids, options }: ExportSelectionMutationInput) => {
103
+ const download =
104
+ ids.length === 1
105
+ ? await exportClaudeCodeSessionFn({
106
+ data: {
107
+ includeCommentary: options.includeCommentary,
108
+ includeMetadata: options.includeMetadata,
109
+ includeTools: options.includeTools,
110
+ outputFormat: options.outputFormat,
111
+ sessionId: ids[0]!,
112
+ zipArchive: options.zipArchive,
113
+ },
114
+ })
115
+ : await exportClaudeCodeSessionsFn({
116
+ data: {
117
+ includeCommentary: options.includeCommentary,
118
+ includeMetadata: options.includeMetadata,
119
+ includeTools: options.includeTools,
120
+ outputFormat: options.outputFormat,
121
+ sessionIds: [...ids],
122
+ zipArchive: options.zipArchive,
123
+ },
124
+ });
75
125
  if (download.mode === 'download') {
76
126
  downloadTextFile(download.fileName, download.content, download.mimeType);
77
127
  return;
@@ -84,16 +134,63 @@ function ClaudeCodeWorkspacePage() {
84
134
  },
85
135
  });
86
136
 
87
- const visibleSessions = sessions.filter((session) =>
88
- matchesTextQuery(deferredSearch, [
89
- session.title,
90
- session.sessionId,
91
- session.model,
92
- session.version,
93
- session.gitBranch,
94
- session.filePath,
95
- ]),
137
+ const deleteMutation = useMutation({
138
+ mutationFn: async (sessionIds: string[]) =>
139
+ sessionIds.length === 1
140
+ ? deleteClaudeCodeSessionFn({ data: { sessionId: sessionIds[0]! } })
141
+ : deleteClaudeCodeSessionsFn({ data: { sessionIds } }),
142
+ onSettled: async (_result, _error, sessionIds) => {
143
+ await Promise.all([
144
+ queryClient.invalidateQueries({ queryKey: ['claude-code-workspaces'] }),
145
+ queryClient.invalidateQueries({ queryKey: ['claude-code-sessions', workspace.key] }),
146
+ ...sessionIds.map((sessionId) =>
147
+ queryClient.invalidateQueries({ queryKey: ['claude-code-session', sessionId] }),
148
+ ),
149
+ ]);
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
+ },
158
+ });
159
+
160
+ const visibleSessions = useMemo(
161
+ () =>
162
+ sessions.filter((session) =>
163
+ matchesTextQuery(deferredSearch, [
164
+ session.title,
165
+ session.sessionId,
166
+ session.model,
167
+ session.version,
168
+ session.gitBranch,
169
+ session.filePath,
170
+ ]),
171
+ ),
172
+ [deferredSearch, sessions],
173
+ );
174
+ const visibleSessionsById = useMemo(
175
+ () => new Map(visibleSessions.map((session) => [session.sessionId, session])),
176
+ [visibleSessions],
96
177
  );
178
+ const lookupSelectedSessions = (sessionIds: string[]) =>
179
+ sessionIds
180
+ .map((sessionId) => visibleSessionsById.get(sessionId) ?? null)
181
+ .filter((session): session is ClaudeCodeSessionSummary => session !== null);
182
+ const openExportForSessions = (selectedSessions: ClaudeCodeSessionSummary[]) => {
183
+ if (selectedSessions.length === 0) {
184
+ return;
185
+ }
186
+
187
+ setPendingExport(buildSessionExport(selectedSessions));
188
+ };
189
+ const openDeleteForSessions = (selectedSessions: ClaudeCodeSessionSummary[]) => {
190
+ if (selectedSessions.length > 0) {
191
+ setPendingDelete({ sessions: selectedSessions });
192
+ }
193
+ };
97
194
 
98
195
  return (
99
196
  <div className="space-y-6">
@@ -110,13 +207,31 @@ function ClaudeCodeWorkspacePage() {
110
207
  title={workspace.label}
111
208
  />
112
209
 
113
- <ClaudeCodeSessionsTable sessions={visibleSessions} onExportSession={setPendingExport} />
210
+ <ClaudeCodeSessionsTable
211
+ sessions={visibleSessions}
212
+ onDeleteSession={(session) => openDeleteForSessions([session])}
213
+ onDeleteSessions={(sessionIds) => openDeleteForSessions(lookupSelectedSessions(sessionIds))}
214
+ onExportSession={(session) => openExportForSessions([session])}
215
+ onExportSessions={(sessionIds) => openExportForSessions(lookupSelectedSessions(sessionIds))}
216
+ />
114
217
 
115
218
  <ExportDialog
219
+ errorMessage={
220
+ exportMutation.isError
221
+ ? exportMutation.error instanceof Error
222
+ ? exportMutation.error.message
223
+ : 'Session export failed'
224
+ : null
225
+ }
226
+ forceZipArchive={pendingExport ? pendingExport.sessionIds.length > 1 : false}
116
227
  open={pendingExport !== null}
117
228
  pending={exportMutation.isPending}
118
- title={pendingExport ? `Export ${pendingExport.title}` : 'Export session'}
119
- onExport={(options) => exportMutation.mutate(options)}
229
+ title={pendingExport ? `Export ${pendingExport.label}` : 'Export session'}
230
+ onExport={(options) => {
231
+ if (pendingExport) {
232
+ exportMutation.mutate(createExportSelectionMutationInput(pendingExport.sessionIds, options));
233
+ }
234
+ }}
120
235
  onOpenChange={(open) => {
121
236
  if (!open) {
122
237
  setPendingExport(null);
@@ -125,11 +240,30 @@ function ClaudeCodeWorkspacePage() {
125
240
  }}
126
241
  />
127
242
 
128
- {exportMutation.isError ? (
129
- <p className="text-[var(--destructive)] text-sm">
130
- {exportMutation.error instanceof Error ? exportMutation.error.message : 'Session export failed'}
131
- </p>
132
- ) : null}
243
+ <DeleteConfirmDialog
244
+ confirmLabel={getDeleteConfirmLabel(pendingDelete, deleteMutation.isPending)}
245
+ description={getDeleteDescription(pendingDelete)}
246
+ errorMessage={
247
+ deleteMutation.isError
248
+ ? deleteMutation.error instanceof Error
249
+ ? deleteMutation.error.message
250
+ : 'Session delete failed'
251
+ : null
252
+ }
253
+ open={pendingDelete !== null}
254
+ title={getDeleteTitle(pendingDelete)}
255
+ onConfirm={() => {
256
+ if (pendingDelete) {
257
+ deleteMutation.mutate(pendingDelete.sessions.map((session) => session.sessionId));
258
+ }
259
+ }}
260
+ onOpenChange={(open) => {
261
+ if (!open) {
262
+ setPendingDelete(null);
263
+ deleteMutation.reset();
264
+ }
265
+ }}
266
+ />
133
267
  </div>
134
268
  );
135
269
  }
@@ -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 { ReloadErrorPanel } from '#/components/reload-error-panel';
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 <ReloadErrorPanel description={error.message} title="Failed to load Claude Code workspaces" />;
18
+ return <RouteErrorPanel error={error} title="Failed to load Claude Code workspaces" />;
19
19
  }
20
20
 
21
21
  function ClaudeCodePage() {