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
@@ -3,6 +3,8 @@ import { createRootRouteWithContext, HeadContent, Outlet, Scripts } from '@tanst
3
3
  import type { ReactNode } from 'react';
4
4
  import { AppShell } from '#/components/app-shell';
5
5
  import { TooltipProvider } from '#/components/ui/tooltip';
6
+ import { getErrorPresentation } from '#/lib/error-presentation';
7
+ import { getInitialSettingsFn } from '#/lib/settings-server';
6
8
  import { SettingsProvider } from '#/lib/settings-store';
7
9
  import appCss from '#/styles.css?url';
8
10
 
@@ -23,26 +25,18 @@ const themeInitScript = `
23
25
  `;
24
26
 
25
27
  function RootErrorComponent({ error }: { error: Error }) {
26
- const isSqliteError =
27
- error.message.includes('unable to open database') ||
28
- error.message.includes('database is locked') ||
29
- error.message.includes('SQLITE_');
28
+ const presentation = getErrorPresentation(error, { fallbackTitle: 'Something went wrong' });
30
29
 
31
30
  return (
32
31
  <div className="flex min-h-screen items-center justify-center bg-[#101418] px-4 text-[#eef3f7]">
33
32
  <div className="max-w-[30rem] text-center">
34
- <h1 className="mb-3 font-semibold text-base">
35
- {isSqliteError ? 'Database unavailable' : 'Something went wrong'}
36
- </h1>
37
- {isSqliteError ? (
38
- <p className="my-2 text-[#99a3af] text-[0.875rem] leading-6">
39
- Spiracha could not open the Codex SQLite database. Codex may have an exclusive lock on the file,
40
- or the database does not exist yet. Close Codex or wait a moment, then reload.
41
- </p>
33
+ <h1 className="mb-3 font-semibold text-base">{presentation.title}</h1>
34
+ {presentation.isDatabaseError ? (
35
+ <p className="my-2 text-[#99a3af] text-[0.875rem] leading-6">{presentation.description}</p>
42
36
  ) : (
43
37
  <p className="my-2 text-[#99a3af] text-[0.875rem] leading-6">
44
38
  <code className="rounded border border-white/10 bg-[#12181e] px-1.5 py-1 text-[0.8em]">
45
- {error.message}
39
+ {presentation.description}
46
40
  </code>
47
41
  </p>
48
42
  )}
@@ -87,7 +81,7 @@ export const Route = createRootRouteWithContext<RouterContext>()({
87
81
  },
88
82
  {
89
83
  content:
90
- 'Browse local Codex, Claude Code, Kiro, Qoder, Cursor, Antigravity, and OpenCode history through a compact workspace UI.',
84
+ 'Browse local Codex, Claude Code, Grok, Kiro, Qoder, Cursor, Antigravity, and OpenCode history through a compact workspace UI.',
91
85
  name: 'description',
92
86
  },
93
87
  {
@@ -95,12 +89,15 @@ export const Route = createRootRouteWithContext<RouterContext>()({
95
89
  },
96
90
  ],
97
91
  }),
92
+ loader: () => getInitialSettingsFn(),
98
93
  });
99
94
 
100
95
  function RootComponent() {
96
+ const initialSettings = Route.useLoaderData();
97
+
101
98
  return (
102
99
  <RootDocument>
103
- <SettingsProvider>
100
+ <SettingsProvider initialSettings={initialSettings}>
104
101
  <TooltipProvider>
105
102
  <AppShell>
106
103
  <Outlet />
@@ -1,9 +1,7 @@
1
- import type { ModelTokenSummary, ToolUsageSummary } from '@spiracha/lib/codex-browser-types';
2
1
  import { useSuspenseQuery } from '@tanstack/react-query';
3
2
  import { createFileRoute, useNavigate } from '@tanstack/react-router';
4
- import { createColumnHelper } from '@tanstack/react-table';
5
3
  import { startTransition } from 'react';
6
- import { DataTable } from '#/components/data-table';
4
+ import { AnalyticsBreakdowns } from '#/components/analytics-breakdowns';
7
5
  import { MetricCard } from '#/components/metric-card';
8
6
  import { PageHeader } from '#/components/page-header';
9
7
  import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '#/components/ui/select';
@@ -16,34 +14,6 @@ import {
16
14
  withAnalyticsProjectSearch,
17
15
  } from '#/lib/route-search';
18
16
 
19
- const toolUsageColumnHelper = createColumnHelper<ToolUsageSummary>();
20
- const toolUsageColumns = [
21
- toolUsageColumnHelper.accessor('name', {
22
- cell: (info) => <span className="font-mono text-sm">{info.getValue()}</span>,
23
- header: 'Tool',
24
- }),
25
- toolUsageColumnHelper.accessor('count', {
26
- cell: (info) => <span className="font-mono text-sm">{formatNumber(info.getValue())}</span>,
27
- header: 'Calls',
28
- }),
29
- ] as const;
30
-
31
- const modelColumnHelper = createColumnHelper<ModelTokenSummary>();
32
- const modelColumns = [
33
- modelColumnHelper.accessor('model', {
34
- cell: (info) => <span className="font-mono text-sm">{info.getValue()}</span>,
35
- header: 'Model',
36
- }),
37
- modelColumnHelper.accessor('threadCount', {
38
- cell: (info) => <span className="font-mono text-sm">{formatNumber(info.getValue())}</span>,
39
- header: 'Threads',
40
- }),
41
- modelColumnHelper.accessor('totalTokens', {
42
- cell: (info) => <span className="font-mono text-sm">{formatTokens(info.getValue())}</span>,
43
- header: 'Tokens',
44
- }),
45
- ] as const;
46
-
47
17
  export const Route = createFileRoute('/analytics')({
48
18
  component: AnalyticsPage,
49
19
  loader: ({ context, deps }) => {
@@ -98,7 +68,7 @@ function AnalyticsPage() {
98
68
  title="Analytics"
99
69
  />
100
70
 
101
- <div className="grid gap-4 md:grid-cols-2 xl:grid-cols-5">
71
+ <div className="grid gap-4 md:grid-cols-2 xl:grid-cols-4">
102
72
  <MetricCard label="Threads" value={formatNumber(analytics.summary.totalThreads)} />
103
73
  <MetricCard label="Projects" value={formatNumber(analytics.summary.totalProjects)} />
104
74
  <MetricCard label="Tokens" value={formatTokens(analytics.summary.totalTokens)} />
@@ -106,38 +76,17 @@ function AnalyticsPage() {
106
76
  label="Average per thread"
107
77
  value={formatTokens(Math.round(analytics.summary.averageTokensPerThread))}
108
78
  />
79
+ <MetricCard label="Median per thread" value={formatTokens(analytics.summary.medianTokensPerThread)} />
109
80
  <MetricCard label="Web search threads" value={formatNumber(analytics.summary.threadsWithWebSearch)} />
81
+ <MetricCard label="Archived threads" value={formatNumber(analytics.summary.archivedThreads)} />
110
82
  </div>
111
83
 
112
- <div className="grid gap-4 xl:grid-cols-2">
113
- <section className="space-y-4">
114
- <div>
115
- <h3 className="font-semibold text-sm">Most frequent tool calls</h3>
116
- <p className="mt-1 text-[var(--muted-foreground)] text-sm">
117
- Useful for future prompt and tool optimization work.
118
- </p>
119
- </div>
120
- <DataTable
121
- columns={toolUsageColumns}
122
- data={analytics.toolUsage}
123
- emptyMessage="No tool calls recorded."
124
- />
125
- </section>
126
-
127
- <section className="space-y-4">
128
- <div>
129
- <h3 className="font-semibold text-sm">Model token breakdown</h3>
130
- <p className="mt-1 text-[var(--muted-foreground)] text-sm">
131
- Compare model usage and token concentration within the current project scope.
132
- </p>
133
- </div>
134
- <DataTable
135
- columns={modelColumns}
136
- data={analytics.modelsByTokens}
137
- emptyMessage="No model usage recorded."
138
- />
139
- </section>
140
- </div>
84
+ <AnalyticsBreakdowns
85
+ modelsByTokens={analytics.modelsByTokens}
86
+ reasoningEfforts={analytics.reasoningEfforts}
87
+ sources={analytics.sources}
88
+ toolUsage={analytics.toolUsage}
89
+ />
141
90
  </div>
142
91
  );
143
92
  }
@@ -1,17 +1,19 @@
1
- import { useMutation, useSuspenseQuery } from '@tanstack/react-query';
2
- import { createFileRoute, Link } from '@tanstack/react-router';
3
- import { Download, ScrollText } from 'lucide-react';
1
+ import { useMutation, useQueryClient, useSuspenseQuery } from '@tanstack/react-query';
2
+ import { createFileRoute, Link, useNavigate } from '@tanstack/react-router';
3
+ import { Download, ScrollText, Trash2 } from 'lucide-react';
4
4
  import { useMemo, useState } from 'react';
5
5
  import { AntigravityKeychainPanel } from '#/components/antigravity-keychain-panel';
6
6
  import { Breadcrumbs } from '#/components/breadcrumbs';
7
+ import { DeleteConfirmDialog } from '#/components/delete-confirm-dialog';
8
+ import { ExportDialog } from '#/components/export-dialog';
7
9
  import { JsonPanel } from '#/components/json-panel';
8
10
  import { LoadingPanel } from '#/components/loading-panel';
9
11
  import { MetadataSection } from '#/components/metadata-section';
10
12
  import { MetricCard } from '#/components/metric-card';
11
13
  import { PageHeader } from '#/components/page-header';
12
- import { ReloadErrorPanel } from '#/components/reload-error-panel';
14
+ import { RouteErrorPanel } from '#/components/route-error-panel';
13
15
  import { TextDocumentPanel } from '#/components/text-document-panel';
14
- import { TranscriptView } from '#/components/transcript-view';
16
+ import { DEFAULT_SHOW_USER_MESSAGES, TranscriptView } from '#/components/transcript-view';
15
17
  import { Button } from '#/components/ui/button';
16
18
  import { Checkbox } from '#/components/ui/checkbox';
17
19
  import { Tabs, TabsContent, TabsList, TabsTrigger } from '#/components/ui/tabs';
@@ -19,8 +21,10 @@ import { canExportAntigravityConversation } from '#/lib/antigravity-conversation
19
21
  import {
20
22
  antigravityConversationDetailQueryOptions,
21
23
  antigravityDecryptionQueryOptions,
24
+ antigravityWorkspacesQueryOptions,
22
25
  } from '#/lib/antigravity-queries';
23
26
  import {
27
+ deleteAntigravityConversationFn,
24
28
  exportAntigravityArtifactsFn,
25
29
  exportAntigravityConversationFn,
26
30
  type getAntigravityConversationDetailFn,
@@ -29,8 +33,11 @@ import {
29
33
  antigravityMarkdownToThreadEvents,
30
34
  getAntigravityThreadTranscriptStats,
31
35
  } from '#/lib/antigravity-transcript-events';
32
- import { downloadTextFile } from '#/lib/download';
33
- import { formatBytes, formatDateTime, formatNumber } from '#/lib/formatters';
36
+ import { downloadTextFile, downloadUrlFile } from '#/lib/download';
37
+ import type { ExportDialogOptions } from '#/lib/export-options';
38
+ import { formatBytes, formatDateTime, formatList, formatNumber } from '#/lib/formatters';
39
+ import { RouteStateResetBoundary } from '#/lib/route-state-reset';
40
+ import { shouldNavigateToSourceIndexAfterDelete } from '#/lib/workspace-delete-navigation';
34
41
 
35
42
  type AntigravityConversationDetail = Awaited<ReturnType<typeof getAntigravityConversationDetailFn>>;
36
43
 
@@ -56,10 +63,10 @@ const buildConversationMetadata = (detail: AntigravityConversationDetail) => {
56
63
  value: (
57
64
  <Link
58
65
  className="text-[var(--accent)]"
59
- params={{ workspaceKey: detail.conversation.workspaceKey }}
66
+ params={{ workspaceKey: detail.conversationGroup.key }}
60
67
  to="/antigravity/$workspaceKey"
61
68
  >
62
- {detail.conversation.workspaceLabel}
69
+ {detail.conversationGroup.label}
63
70
  </Link>
64
71
  ),
65
72
  },
@@ -95,7 +102,7 @@ const buildTranscriptStatsItems = (
95
102
 
96
103
  const stats = getAntigravityThreadTranscriptStats(events);
97
104
  return [
98
- { label: 'Event kinds', value: [...new Set(events.map((event) => event.kind))].join(', ') || 'n/a' },
105
+ { label: 'Event kinds', value: formatList([...new Set(events.map((event) => event.kind))]) },
99
106
  { label: 'Messages', value: formatNumber(stats.messageCount) },
100
107
  { label: 'User messages', value: formatNumber(stats.userMessageCount) },
101
108
  { label: 'Assistant messages', value: formatNumber(stats.assistantMessageCount) },
@@ -106,8 +113,23 @@ const buildTranscriptStatsItems = (
106
113
  ];
107
114
  };
108
115
 
116
+ const getConversationSizeLabel = (detail: AntigravityConversationDetail): string => {
117
+ if (detail.conversation.totalBytes > 0) {
118
+ return formatBytes(detail.conversation.totalBytes);
119
+ }
120
+
121
+ return detail.conversation.summaryPath ? 'Summary' : formatBytes(detail.conversation.totalBytes);
122
+ };
123
+
109
124
  export const Route = createFileRoute('/antigravity-conversations/$conversationId')({
110
- component: AntigravityConversationDetailPage,
125
+ component: () => {
126
+ const { conversationId } = Route.useParams();
127
+ return (
128
+ <RouteStateResetBoundary routeKey={conversationId}>
129
+ <AntigravityConversationDetailPage />
130
+ </RouteStateResetBoundary>
131
+ );
132
+ },
111
133
  errorComponent: AntigravityConversationDetailErrorComponent,
112
134
  loader: async ({ context, params }) => {
113
135
  await Promise.all([
@@ -124,7 +146,7 @@ export const Route = createFileRoute('/antigravity-conversations/$conversationId
124
146
  });
125
147
 
126
148
  function AntigravityConversationDetailErrorComponent({ error }: { error: Error }) {
127
- return <ReloadErrorPanel description={error.message} title="Failed to load Antigravity conversation" />;
149
+ return <RouteErrorPanel error={error} title="Failed to load Antigravity conversation" />;
128
150
  }
129
151
 
130
152
  function AntigravityConversationHeaderActions({
@@ -133,6 +155,7 @@ function AntigravityConversationHeaderActions({
133
155
  exportArtifactsPending,
134
156
  exportConversationPending,
135
157
  showConversationExport,
158
+ onDeleteConversation,
136
159
  onExportArtifacts,
137
160
  onExportConversation,
138
161
  }: {
@@ -141,6 +164,7 @@ function AntigravityConversationHeaderActions({
141
164
  exportArtifactsPending: boolean;
142
165
  exportConversationPending: boolean;
143
166
  showConversationExport: boolean;
167
+ onDeleteConversation: () => void;
144
168
  onExportArtifacts: () => void;
145
169
  onExportConversation: () => void;
146
170
  }) {
@@ -170,6 +194,15 @@ function AntigravityConversationHeaderActions({
170
194
  Export artifacts
171
195
  </Button>
172
196
  ) : null}
197
+ <Button
198
+ className="rounded-full border-[var(--destructive)]/20 text-[var(--destructive)]"
199
+ type="button"
200
+ variant="outline"
201
+ onClick={onDeleteConversation}
202
+ >
203
+ <Trash2 className="mr-2 size-4" />
204
+ Delete
205
+ </Button>
173
206
  </div>
174
207
  );
175
208
  }
@@ -281,13 +314,17 @@ function AntigravityRawPanels({
281
314
  }
282
315
 
283
316
  function AntigravityConversationDetailPage() {
317
+ const navigate = useNavigate();
318
+ const queryClient = useQueryClient();
284
319
  const decryptionState = useSuspenseQuery(antigravityDecryptionQueryOptions()).data;
285
320
  const detail = useSuspenseQuery(antigravityConversationDetailQueryOptions(Route.useParams().conversationId)).data;
321
+ const [deleteOpen, setDeleteOpen] = useState(false);
322
+ const [exportOpen, setExportOpen] = useState(false);
286
323
  const [showToolCalls, setShowToolCalls] = useState(false);
287
324
  const [showCommentary, setShowCommentary] = useState(false);
288
325
  const [showExtraEvents, setShowExtraEvents] = useState(false);
289
326
  const [showRawJson, setShowRawJson] = useState(false);
290
- const [showUserMessages, setShowUserMessages] = useState(true);
327
+ const [showUserMessages, setShowUserMessages] = useState(DEFAULT_SHOW_USER_MESSAGES);
291
328
  const transcriptEvents = useMemo(
292
329
  () => antigravityMarkdownToThreadEvents(detail.conversationMarkdown),
293
330
  [detail.conversationMarkdown],
@@ -300,10 +337,26 @@ function AntigravityConversationDetailPage() {
300
337
  const showConversationExport = canExportConversation || detail.transcriptLocked;
301
338
 
302
339
  const exportConversationMutation = useMutation({
303
- mutationFn: () =>
304
- exportAntigravityConversationFn({ data: { conversationId: detail.conversation.conversationId } }),
305
- onSuccess: (result) => {
306
- downloadTextFile(result.filename, result.content, 'text/markdown; charset=utf-8');
340
+ mutationFn: async (options: ExportDialogOptions) => {
341
+ const download = await exportAntigravityConversationFn({
342
+ data: {
343
+ conversationId: detail.conversation.conversationId,
344
+ includeCommentary: options.includeCommentary,
345
+ includeMetadata: options.includeMetadata,
346
+ includeTools: options.includeTools,
347
+ outputFormat: options.outputFormat,
348
+ zipArchive: options.zipArchive,
349
+ },
350
+ });
351
+ if (download.mode === 'download') {
352
+ downloadTextFile(download.fileName, download.content, download.mimeType);
353
+ return;
354
+ }
355
+
356
+ await downloadUrlFile(download.fileName, download.downloadUrl);
357
+ },
358
+ onSuccess: () => {
359
+ setExportOpen(false);
307
360
  },
308
361
  });
309
362
 
@@ -315,6 +368,37 @@ function AntigravityConversationDetailPage() {
315
368
  },
316
369
  });
317
370
 
371
+ const deleteConversationMutation = useMutation({
372
+ mutationFn: () =>
373
+ deleteAntigravityConversationFn({ data: { conversationId: detail.conversation.conversationId } }),
374
+ onSuccess: async () => {
375
+ await Promise.all([
376
+ queryClient.invalidateQueries({ queryKey: ['antigravity-workspaces'] }),
377
+ queryClient.invalidateQueries({
378
+ queryKey: ['antigravity-conversations', detail.conversationGroup.key],
379
+ }),
380
+ queryClient.invalidateQueries({
381
+ queryKey: ['antigravity-conversation', detail.conversation.conversationId],
382
+ }),
383
+ ]);
384
+ const workspaces = await queryClient.fetchQuery(antigravityWorkspacesQueryOptions());
385
+ if (
386
+ shouldNavigateToSourceIndexAfterDelete(
387
+ workspaces,
388
+ detail.conversationGroup.key,
389
+ (workspace) => workspace.key,
390
+ )
391
+ ) {
392
+ navigate({ to: '/antigravity' });
393
+ return;
394
+ }
395
+ navigate({
396
+ params: { workspaceKey: detail.conversationGroup.key },
397
+ to: '/antigravity/$workspaceKey',
398
+ });
399
+ },
400
+ });
401
+
318
402
  return (
319
403
  <div className="space-y-6">
320
404
  <PageHeader
@@ -325,8 +409,9 @@ function AntigravityConversationDetailPage() {
325
409
  exportArtifactsPending={exportArtifactsMutation.isPending}
326
410
  exportConversationPending={exportConversationMutation.isPending}
327
411
  showConversationExport={showConversationExport}
412
+ onDeleteConversation={() => setDeleteOpen(true)}
328
413
  onExportArtifacts={() => exportArtifactsMutation.mutate()}
329
- onExportConversation={() => exportConversationMutation.mutate()}
414
+ onExportConversation={() => setExportOpen(true)}
330
415
  />
331
416
  }
332
417
  breadcrumb={
@@ -334,8 +419,8 @@ function AntigravityConversationDetailPage() {
334
419
  items={[
335
420
  { label: 'Antigravity', to: '/antigravity' },
336
421
  {
337
- label: detail.conversation.workspaceLabel,
338
- params: { workspaceKey: detail.conversation.workspaceKey },
422
+ label: detail.conversationGroup.label,
423
+ params: { workspaceKey: detail.conversationGroup.key },
339
424
  to: '/antigravity/$workspaceKey',
340
425
  },
341
426
  { label: detail.conversation.title },
@@ -352,7 +437,7 @@ function AntigravityConversationDetailPage() {
352
437
  <div className="grid gap-3 md:grid-cols-2 xl:grid-cols-4">
353
438
  <MetricCard label="Transcript entries" value={formatNumber(detail.conversation.transcriptEntryCount)} />
354
439
  <MetricCard label="Artifacts" value={formatNumber(detail.conversation.artifactCount)} />
355
- <MetricCard label="Size" value={formatBytes(detail.conversation.conversationBytes)} />
440
+ <MetricCard label="Size" value={getConversationSizeLabel(detail)} />
356
441
  <MetricCard
357
442
  helper={detail.conversation.transcriptSource ?? 'summary'}
358
443
  label="Indexed items"
@@ -418,14 +503,6 @@ function AntigravityConversationDetailPage() {
418
503
  </TabsContent>
419
504
  </Tabs>
420
505
 
421
- {exportConversationMutation.isError ? (
422
- <p className="text-[var(--destructive)] text-sm">
423
- {exportConversationMutation.error instanceof Error
424
- ? exportConversationMutation.error.message
425
- : 'Conversation export failed'}
426
- </p>
427
- ) : null}
428
-
429
506
  {exportArtifactsMutation.isError ? (
430
507
  <p className="text-[var(--destructive)] text-sm">
431
508
  {exportArtifactsMutation.error instanceof Error
@@ -433,6 +510,49 @@ function AntigravityConversationDetailPage() {
433
510
  : 'Artifact export failed'}
434
511
  </p>
435
512
  ) : null}
513
+
514
+ <ExportDialog
515
+ errorMessage={
516
+ exportConversationMutation.isError
517
+ ? exportConversationMutation.error instanceof Error
518
+ ? exportConversationMutation.error.message
519
+ : 'Conversation export failed'
520
+ : null
521
+ }
522
+ open={exportOpen}
523
+ pending={exportConversationMutation.isPending}
524
+ showCommentaryOption={detail.conversation.transcriptSource !== 'safe-storage'}
525
+ showToolsOption={detail.conversation.transcriptSource !== 'safe-storage'}
526
+ title={`Export ${detail.conversation.title}`}
527
+ onExport={(options) => exportConversationMutation.mutate(options)}
528
+ onOpenChange={(open) => {
529
+ setExportOpen(open);
530
+ if (!open) {
531
+ exportConversationMutation.reset();
532
+ }
533
+ }}
534
+ />
535
+
536
+ <DeleteConfirmDialog
537
+ confirmLabel={deleteConversationMutation.isPending ? 'Deleting...' : 'Delete conversation'}
538
+ description="Permanently delete this Antigravity conversation from disk. This removes its summary entry, conversation file, transcript logs, and generated artifacts."
539
+ errorMessage={
540
+ deleteConversationMutation.isError
541
+ ? deleteConversationMutation.error instanceof Error
542
+ ? deleteConversationMutation.error.message
543
+ : 'Conversation delete failed'
544
+ : null
545
+ }
546
+ open={deleteOpen}
547
+ title="Delete this Antigravity conversation?"
548
+ onConfirm={() => deleteConversationMutation.mutate()}
549
+ onOpenChange={(open) => {
550
+ setDeleteOpen(open);
551
+ if (!open) {
552
+ deleteConversationMutation.reset();
553
+ }
554
+ }}
555
+ />
436
556
  </div>
437
557
  );
438
558
  }