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
@@ -0,0 +1,401 @@
1
+ import type { ThreadEvent, ThreadTranscriptStats } from '@spiracha/lib/codex-browser-types';
2
+ import type { GrokSessionTranscript } from '@spiracha/lib/grok-exporter-types';
3
+ import { useMutation, useQueryClient, useSuspenseQuery } from '@tanstack/react-query';
4
+ import { createFileRoute, Link, useNavigate } from '@tanstack/react-router';
5
+ import { Download, Trash2 } from 'lucide-react';
6
+ import { useMemo, useState } from 'react';
7
+ import { Breadcrumbs } from '#/components/breadcrumbs';
8
+ import { DeleteConfirmDialog } from '#/components/delete-confirm-dialog';
9
+ import { ExportDialog } from '#/components/export-dialog';
10
+ import { JsonPanel } from '#/components/json-panel';
11
+ import { LoadingPanel } from '#/components/loading-panel';
12
+ import { MetadataSection } from '#/components/metadata-section';
13
+ import { MetricCard } from '#/components/metric-card';
14
+ import { PageHeader } from '#/components/page-header';
15
+ import { RouteErrorPanel } from '#/components/route-error-panel';
16
+ import { DEFAULT_SHOW_USER_MESSAGES, TranscriptView } from '#/components/transcript-view';
17
+ import { Button } from '#/components/ui/button';
18
+ import { Checkbox } from '#/components/ui/checkbox';
19
+ import { Tabs, TabsContent, TabsList, TabsTrigger } from '#/components/ui/tabs';
20
+ import { downloadTextFile, downloadUrlFile } from '#/lib/download';
21
+ import type { ExportDialogOptions } from '#/lib/export-options';
22
+ import { formatDateTime, formatList, formatNumber } from '#/lib/formatters';
23
+ import { grokSessionDetailQueryOptions, grokWorkspacesQueryOptions } from '#/lib/grok-queries';
24
+ import { deleteGrokSessionFn, exportGrokSessionFn } from '#/lib/grok-server';
25
+ import { getGrokThreadTranscriptStats, grokTranscriptToThreadEvents } from '#/lib/grok-transcript-events';
26
+ import { RouteStateResetBoundary } from '#/lib/route-state-reset';
27
+ import { shouldNavigateToSourceIndexAfterDelete } from '#/lib/workspace-delete-navigation';
28
+
29
+ type TranscriptControlsProps = {
30
+ rawJsonDisabled?: boolean;
31
+ showCommentary: boolean;
32
+ showExtraEvents: boolean;
33
+ showRawJson: boolean;
34
+ showToolCalls: boolean;
35
+ showUserMessages: boolean;
36
+ onShowCommentaryChange: (checked: boolean) => void;
37
+ onShowExtraEventsChange: (checked: boolean) => void;
38
+ onShowRawJsonChange: (checked: boolean) => void;
39
+ onShowToolCallsChange: (checked: boolean) => void;
40
+ onShowUserMessagesChange: (checked: boolean) => void;
41
+ };
42
+
43
+ const GrokSessionDetailErrorComponent = ({ error }: { error: Error }) => {
44
+ return <RouteErrorPanel error={error} title="Failed to load Grok session" />;
45
+ };
46
+
47
+ const buildSessionMetadata = (detail: GrokSessionTranscript) => [
48
+ { label: 'Session ID', value: <span data-mono="true">{detail.session.sessionId}</span> },
49
+ {
50
+ label: 'Workspace',
51
+ value: (
52
+ <Link
53
+ className="text-[var(--accent)]"
54
+ params={{ workspaceKey: detail.session.workspaceKey }}
55
+ to="/grok/$workspaceKey"
56
+ >
57
+ {detail.session.workspaceLabel}
58
+ </Link>
59
+ ),
60
+ },
61
+ { label: 'Worktree', value: detail.session.worktree },
62
+ { label: 'Session directory', value: detail.session.sessionDir },
63
+ { label: 'Agent', value: detail.session.agentName ?? 'unknown' },
64
+ { label: 'Model', value: detail.session.modelLabel ?? detail.session.currentModelId ?? 'unknown' },
65
+ { label: 'Git branch', value: detail.session.gitBranch ?? 'unknown' },
66
+ { label: 'Head commit', value: detail.session.headCommit ?? 'unknown' },
67
+ { label: 'Created', value: <span suppressHydrationWarning>{formatDateTime(detail.session.createdAtMs)}</span> },
68
+ { label: 'Updated', value: <span suppressHydrationWarning>{formatDateTime(detail.session.lastActiveAtMs)}</span> },
69
+ ];
70
+
71
+ const buildTranscriptStatsItems = (
72
+ detail: GrokSessionTranscript,
73
+ events: ThreadEvent[],
74
+ stats: ThreadTranscriptStats,
75
+ ) => [
76
+ { label: 'Event kinds', value: formatList([...new Set(events.map((event) => event.kind))]) },
77
+ { label: 'Messages', value: formatNumber(stats.messageCount) },
78
+ { label: 'User messages', value: formatNumber(stats.userMessageCount) },
79
+ { label: 'Assistant messages', value: formatNumber(stats.assistantMessageCount) },
80
+ { label: 'Reasoning events', value: formatNumber(events.filter((event) => event.kind === 'reasoning').length) },
81
+ { label: 'Final answers', value: formatNumber(stats.finalAnswerCount) },
82
+ { label: 'Tool calls', value: formatNumber(stats.toolCallCount) },
83
+ { label: 'Tool outputs', value: formatNumber(stats.toolOutputCount) },
84
+ { label: 'Renderable parts', value: formatNumber(detail.renderablePartCount) },
85
+ ];
86
+
87
+ const GrokTranscriptControls = ({
88
+ rawJsonDisabled = false,
89
+ showCommentary,
90
+ showExtraEvents,
91
+ showRawJson,
92
+ showToolCalls,
93
+ showUserMessages,
94
+ onShowCommentaryChange,
95
+ onShowExtraEventsChange,
96
+ onShowRawJsonChange,
97
+ onShowToolCallsChange,
98
+ onShowUserMessagesChange,
99
+ }: TranscriptControlsProps) => {
100
+ return (
101
+ <div className="flex flex-wrap gap-4 rounded-xl border border-[var(--border)] bg-[var(--panel)] px-4 py-3 shadow-[var(--panel-shadow)]">
102
+ <div className="flex items-center gap-2 text-sm">
103
+ <Checkbox
104
+ checked={showToolCalls}
105
+ id="grok-transcript-show-tool-calls"
106
+ onCheckedChange={(checked) => onShowToolCallsChange(checked === true)}
107
+ />
108
+ <label htmlFor="grok-transcript-show-tool-calls">Show tool calls</label>
109
+ </div>
110
+ <div className="flex items-center gap-2 text-sm">
111
+ <Checkbox
112
+ checked={showCommentary}
113
+ id="grok-transcript-show-commentary"
114
+ onCheckedChange={(checked) => onShowCommentaryChange(checked === true)}
115
+ />
116
+ <label htmlFor="grok-transcript-show-commentary">Show commentary</label>
117
+ </div>
118
+ <div className="flex items-center gap-2 text-sm">
119
+ <Checkbox
120
+ checked={showExtraEvents}
121
+ id="grok-transcript-show-extra-events"
122
+ onCheckedChange={(checked) => onShowExtraEventsChange(checked === true)}
123
+ />
124
+ <label htmlFor="grok-transcript-show-extra-events">Show extra events</label>
125
+ </div>
126
+ <div className="flex items-center gap-2 text-sm">
127
+ <Checkbox
128
+ checked={showRawJson}
129
+ disabled={rawJsonDisabled}
130
+ id="grok-transcript-show-raw-json"
131
+ onCheckedChange={(checked) => onShowRawJsonChange(checked === true)}
132
+ />
133
+ <label htmlFor="grok-transcript-show-raw-json">Raw JSON</label>
134
+ </div>
135
+ <div className="flex items-center gap-2 text-sm">
136
+ <Checkbox
137
+ checked={showUserMessages}
138
+ id="grok-transcript-show-user-messages"
139
+ onCheckedChange={(checked) => onShowUserMessagesChange(checked === true)}
140
+ />
141
+ <label htmlFor="grok-transcript-show-user-messages">User</label>
142
+ </div>
143
+ </div>
144
+ );
145
+ };
146
+
147
+ const GrokRawPanels = ({ detail, events }: { detail: GrokSessionTranscript; events: ThreadEvent[] }) => {
148
+ return (
149
+ <div className="space-y-4">
150
+ <JsonPanel title="Session summary" value={detail.session} />
151
+ <JsonPanel title="Grok transcript entries" value={detail.entries} />
152
+ <JsonPanel title="Transcript events" value={events} />
153
+ </div>
154
+ );
155
+ };
156
+
157
+ const GrokSessionDetailPage = () => {
158
+ const navigate = useNavigate();
159
+ const queryClient = useQueryClient();
160
+ const detail = useSuspenseQuery(grokSessionDetailQueryOptions(Route.useParams().sessionId)).data;
161
+ const [deleteOpen, setDeleteOpen] = useState(false);
162
+ const [pendingExport, setPendingExport] = useState(false);
163
+ const [showToolCalls, setShowToolCalls] = useState(false);
164
+ const [showCommentary, setShowCommentary] = useState(false);
165
+ const [showExtraEvents, setShowExtraEvents] = useState(false);
166
+ const [showRawJson, setShowRawJson] = useState(false);
167
+ const [showUserMessages, setShowUserMessages] = useState(DEFAULT_SHOW_USER_MESSAGES);
168
+ const transcriptEvents = useMemo(() => grokTranscriptToThreadEvents(detail), [detail]);
169
+ const transcriptStats = useMemo(() => getGrokThreadTranscriptStats(transcriptEvents), [transcriptEvents]);
170
+
171
+ const exportSessionMutation = useMutation({
172
+ mutationFn: async (options: ExportDialogOptions) => {
173
+ const download = await exportGrokSessionFn({
174
+ data: {
175
+ includeCommentary: options.includeCommentary,
176
+ includeMetadata: options.includeMetadata,
177
+ includeTools: options.includeTools,
178
+ outputFormat: options.outputFormat,
179
+ sessionId: detail.session.sessionId,
180
+ zipArchive: options.zipArchive,
181
+ },
182
+ });
183
+ if (download.mode === 'download') {
184
+ downloadTextFile(download.fileName, download.content, download.mimeType);
185
+ return;
186
+ }
187
+
188
+ await downloadUrlFile(download.fileName, download.downloadUrl);
189
+ },
190
+ onSuccess: () => {
191
+ setPendingExport(false);
192
+ },
193
+ });
194
+
195
+ const deleteSessionMutation = useMutation({
196
+ mutationFn: () => deleteGrokSessionFn({ data: { sessionId: detail.session.sessionId } }),
197
+ onSuccess: async () => {
198
+ await Promise.all([
199
+ queryClient.invalidateQueries({ queryKey: ['grok-workspaces'] }),
200
+ queryClient.invalidateQueries({ queryKey: ['grok-sessions', detail.session.workspaceKey] }),
201
+ queryClient.invalidateQueries({ queryKey: ['grok-session', detail.session.sessionId] }),
202
+ ]);
203
+ const workspaces = await queryClient.fetchQuery(grokWorkspacesQueryOptions());
204
+ if (
205
+ shouldNavigateToSourceIndexAfterDelete(
206
+ workspaces,
207
+ detail.session.workspaceKey,
208
+ (workspace) => workspace.key,
209
+ )
210
+ ) {
211
+ navigate({ to: '/grok' });
212
+ return;
213
+ }
214
+ navigate({
215
+ params: { workspaceKey: detail.session.workspaceKey },
216
+ to: '/grok/$workspaceKey',
217
+ });
218
+ },
219
+ });
220
+
221
+ return (
222
+ <div className="space-y-6">
223
+ <PageHeader
224
+ actions={
225
+ <>
226
+ <Button
227
+ className="rounded-full"
228
+ type="button"
229
+ variant="outline"
230
+ onClick={() => setPendingExport(true)}
231
+ >
232
+ <Download className="mr-2 size-4" />
233
+ Export
234
+ </Button>
235
+ <Button
236
+ className="rounded-full border-[var(--destructive)]/20 text-[var(--destructive)]"
237
+ type="button"
238
+ variant="outline"
239
+ onClick={() => setDeleteOpen(true)}
240
+ >
241
+ <Trash2 className="mr-2 size-4" />
242
+ Delete
243
+ </Button>
244
+ </>
245
+ }
246
+ breadcrumb={
247
+ <Breadcrumbs
248
+ items={[
249
+ { label: 'Grok', to: '/grok' },
250
+ {
251
+ label: detail.session.workspaceLabel,
252
+ params: { workspaceKey: detail.session.workspaceKey },
253
+ to: '/grok/$workspaceKey',
254
+ },
255
+ { label: detail.session.title },
256
+ ]}
257
+ />
258
+ }
259
+ eyebrow="Grok session"
260
+ subtitle="Session detail for the selected local Grok CLI conversation."
261
+ title={detail.session.title}
262
+ />
263
+
264
+ <div className="grid gap-3 md:grid-cols-2 xl:grid-cols-4">
265
+ <MetricCard label="Messages" value={formatNumber(detail.session.messageCount)} />
266
+ <MetricCard label="Tool calls" value={formatNumber(detail.session.toolCallCount)} />
267
+ <MetricCard label="Reasoning" value={formatNumber(detail.session.reasoningCount)} />
268
+ <MetricCard label="Renderable parts" value={formatNumber(detail.renderablePartCount)} />
269
+ </div>
270
+
271
+ <Tabs className="space-y-4" defaultValue="transcript">
272
+ <TabsList className="grid w-fit min-w-[24rem] grid-cols-3 rounded-full border border-[var(--border)] bg-[var(--panel)] p-1">
273
+ <TabsTrigger className="rounded-full px-5 text-sm" value="transcript">
274
+ Transcript
275
+ </TabsTrigger>
276
+ <TabsTrigger className="rounded-full px-5 text-sm" value="metadata">
277
+ Metadata
278
+ </TabsTrigger>
279
+ <TabsTrigger className="rounded-full px-5 text-sm" value="raw">
280
+ Raw
281
+ </TabsTrigger>
282
+ </TabsList>
283
+
284
+ <TabsContent className="space-y-3" value="transcript">
285
+ <GrokTranscriptControls
286
+ rawJsonDisabled={Boolean(detail.rawPayloadsOmitted) || transcriptEvents.length === 0}
287
+ showCommentary={showCommentary}
288
+ showExtraEvents={showExtraEvents}
289
+ showRawJson={showRawJson}
290
+ showToolCalls={showToolCalls}
291
+ showUserMessages={showUserMessages}
292
+ onShowCommentaryChange={setShowCommentary}
293
+ onShowExtraEventsChange={setShowExtraEvents}
294
+ onShowRawJsonChange={setShowRawJson}
295
+ onShowToolCallsChange={setShowToolCalls}
296
+ onShowUserMessagesChange={setShowUserMessages}
297
+ />
298
+ {transcriptEvents.length > 0 ? (
299
+ <TranscriptView
300
+ assistantModel={detail.session.modelLabel ?? detail.session.currentModelId}
301
+ events={transcriptEvents}
302
+ projectPath={detail.session.worktree}
303
+ showCommentary={showCommentary}
304
+ showExtraEvents={showExtraEvents}
305
+ showRawJson={showRawJson && !detail.rawPayloadsOmitted}
306
+ showToolCalls={showToolCalls}
307
+ showUserMessages={showUserMessages}
308
+ />
309
+ ) : (
310
+ <section className="rounded-[1.6rem] border border-[var(--border)] bg-[var(--panel)] p-5 shadow-[var(--panel-shadow)]">
311
+ <h3 className="font-semibold text-[var(--muted-foreground)] text-sm uppercase tracking-[0.18em]">
312
+ Transcript
313
+ </h3>
314
+ <p className="mt-4 text-[var(--muted-foreground)] text-sm">
315
+ No renderable Grok transcript content was found for this session.
316
+ </p>
317
+ </section>
318
+ )}
319
+ </TabsContent>
320
+
321
+ <TabsContent value="metadata">
322
+ <div className="grid gap-4 xl:grid-cols-[0.95fr_1.05fr]">
323
+ <MetadataSection items={buildSessionMetadata(detail)} title="Session metadata" />
324
+ <MetadataSection
325
+ items={buildTranscriptStatsItems(detail, transcriptEvents, transcriptStats)}
326
+ title="Transcript stats"
327
+ />
328
+ </div>
329
+ </TabsContent>
330
+
331
+ <TabsContent value="raw">
332
+ {detail.rawPayloadsOmitted ? (
333
+ <section className="mb-4 rounded-xl border border-[var(--border)] bg-[var(--panel)] p-4 text-[var(--muted-foreground)] text-sm">
334
+ Raw Grok payloads are omitted from the default session view to keep large transcripts
335
+ reloadable.
336
+ </section>
337
+ ) : null}
338
+ <GrokRawPanels detail={detail} events={transcriptEvents} />
339
+ </TabsContent>
340
+ </Tabs>
341
+
342
+ <ExportDialog
343
+ errorMessage={
344
+ exportSessionMutation.isError
345
+ ? exportSessionMutation.error instanceof Error
346
+ ? exportSessionMutation.error.message
347
+ : 'Export failed'
348
+ : null
349
+ }
350
+ open={pendingExport}
351
+ pending={exportSessionMutation.isPending}
352
+ title={`Export ${detail.session.title}`}
353
+ onExport={(options) => exportSessionMutation.mutate(options)}
354
+ onOpenChange={(open) => {
355
+ setPendingExport(open);
356
+ if (!open) {
357
+ exportSessionMutation.reset();
358
+ }
359
+ }}
360
+ />
361
+
362
+ <DeleteConfirmDialog
363
+ confirmLabel={deleteSessionMutation.isPending ? 'Deleting...' : 'Delete session'}
364
+ description="Permanently delete this Grok session from local history. This removes the session directory and transcript files."
365
+ errorMessage={
366
+ deleteSessionMutation.isError
367
+ ? deleteSessionMutation.error instanceof Error
368
+ ? deleteSessionMutation.error.message
369
+ : 'Session delete failed'
370
+ : null
371
+ }
372
+ open={deleteOpen}
373
+ title="Delete this Grok session?"
374
+ onConfirm={() => deleteSessionMutation.mutate()}
375
+ onOpenChange={(open) => {
376
+ setDeleteOpen(open);
377
+ if (!open) {
378
+ deleteSessionMutation.reset();
379
+ }
380
+ }}
381
+ />
382
+ </div>
383
+ );
384
+ };
385
+
386
+ export const Route = createFileRoute('/grok-sessions/$sessionId')({
387
+ component: () => {
388
+ const { sessionId } = Route.useParams();
389
+ return (
390
+ <RouteStateResetBoundary routeKey={sessionId}>
391
+ <GrokSessionDetailPage />
392
+ </RouteStateResetBoundary>
393
+ );
394
+ },
395
+ errorComponent: GrokSessionDetailErrorComponent,
396
+ loader: ({ context, params }) =>
397
+ context.queryClient.ensureQueryData(grokSessionDetailQueryOptions(params.sessionId)),
398
+ pendingComponent: () => (
399
+ <LoadingPanel description="Loading the Grok transcript and session metadata." title="Loading session" />
400
+ ),
401
+ });