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,295 @@
1
+ import type { GrokSessionSummary, GrokWorkspaceGroup } from '@spiracha/lib/grok-exporter-types';
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';
6
+ import { DeleteConfirmDialog } from '#/components/delete-confirm-dialog';
7
+ import { ExportDialog } from '#/components/export-dialog';
8
+ import { GrokSessionsTable } from '#/components/grok-sessions-table';
9
+ import { ListSearchInput } from '#/components/list-search-input';
10
+ import { LoadingPanel } from '#/components/loading-panel';
11
+ import { PageHeader } from '#/components/page-header';
12
+ import { RouteErrorPanel } from '#/components/route-error-panel';
13
+ import { Button } from '#/components/ui/button';
14
+ import { downloadTextFile, downloadUrlFile } from '#/lib/download';
15
+ import { createExportSelectionMutationInput, type ExportSelectionMutationInput } from '#/lib/export-mutation';
16
+ import { grokSessionsQueryOptions, grokWorkspacesQueryOptions } from '#/lib/grok-queries';
17
+ import {
18
+ deleteGrokSessionFn,
19
+ deleteGrokSessionsFn,
20
+ exportGrokSessionFn,
21
+ exportGrokSessionsFn,
22
+ } from '#/lib/grok-server';
23
+ import { matchesTextQuery } from '#/lib/text-filter';
24
+ import { isWorkspaceEmptiedByDelete } from '#/lib/workspace-delete-navigation';
25
+
26
+ type PendingSessionDelete = {
27
+ scope: 'all' | 'selected';
28
+ sessions: GrokSessionSummary[];
29
+ };
30
+
31
+ type PendingSessionExport = {
32
+ label: string;
33
+ sessionIds: string[];
34
+ };
35
+
36
+ const findWorkspaceOrThrow = (workspaces: GrokWorkspaceGroup[], workspaceKey: string) => {
37
+ const workspace = workspaces.find((candidate) => candidate.key === workspaceKey);
38
+ if (!workspace) {
39
+ throw new Error(`Grok workspace not found: ${workspaceKey}`);
40
+ }
41
+
42
+ return workspace;
43
+ };
44
+
45
+ const buildSessionExport = (selectedSessions: GrokSessionSummary[]): PendingSessionExport => ({
46
+ label: selectedSessions.length === 1 ? selectedSessions[0]!.title : `${selectedSessions.length} selected sessions`,
47
+ sessionIds: selectedSessions.map((session) => session.sessionId),
48
+ });
49
+
50
+ const getDeleteConfirmLabel = (pendingDelete: PendingSessionDelete | null, isPending: boolean) => {
51
+ if (isPending) {
52
+ return 'Deleting...';
53
+ }
54
+
55
+ if (pendingDelete?.scope === 'all') {
56
+ return 'Delete all';
57
+ }
58
+
59
+ return pendingDelete && pendingDelete.sessions.length > 1 ? 'Delete sessions' : 'Delete session';
60
+ };
61
+
62
+ const getDeleteDescription = (pendingDelete: PendingSessionDelete | null) => {
63
+ if (!pendingDelete) {
64
+ return 'Permanently delete the selected Grok sessions from local history.';
65
+ }
66
+
67
+ if (pendingDelete.scope === 'all') {
68
+ return `Permanently delete all ${pendingDelete.sessions.length} Grok sessions in this workspace from local history. This removes their session directories and transcript files under ~/.grok/sessions.`;
69
+ }
70
+
71
+ if (pendingDelete.sessions.length === 1) {
72
+ return `Permanently delete "${pendingDelete.sessions[0]!.title}" from Grok history. This removes the session directory and transcript files under ~/.grok/sessions.`;
73
+ }
74
+
75
+ return `Permanently delete ${pendingDelete.sessions.length} selected Grok sessions from local history. This removes their session directories and transcript files under ~/.grok/sessions.`;
76
+ };
77
+
78
+ const getDeleteTitle = (pendingDelete: PendingSessionDelete | null) => {
79
+ if (pendingDelete?.scope === 'all') {
80
+ return `Delete all ${pendingDelete.sessions.length} Grok sessions?`;
81
+ }
82
+
83
+ return pendingDelete && pendingDelete.sessions.length > 1
84
+ ? `Delete ${pendingDelete.sessions.length} Grok sessions?`
85
+ : 'Delete this Grok session?';
86
+ };
87
+
88
+ export const Route = createFileRoute('/grok/$workspaceKey')({
89
+ component: GrokWorkspacePage,
90
+ errorComponent: GrokWorkspaceErrorComponent,
91
+ loader: async ({ context, params }) => {
92
+ const workspaces = await context.queryClient.ensureQueryData(grokWorkspacesQueryOptions());
93
+ findWorkspaceOrThrow(workspaces, params.workspaceKey);
94
+ await context.queryClient.ensureQueryData(grokSessionsQueryOptions(params.workspaceKey));
95
+ },
96
+ pendingComponent: () => <LoadingPanel description="Loading Grok sessions." title="Loading workspace" />,
97
+ });
98
+
99
+ function GrokWorkspaceErrorComponent({ error }: { error: Error }) {
100
+ return <RouteErrorPanel error={error} title="Failed to load Grok workspace" />;
101
+ }
102
+
103
+ function GrokWorkspacePage() {
104
+ const navigate = useNavigate({ from: Route.fullPath });
105
+ const params = Route.useParams();
106
+ const queryClient = useQueryClient();
107
+ const workspaces = useSuspenseQuery(grokWorkspacesQueryOptions()).data;
108
+ const workspace = findWorkspaceOrThrow(workspaces, params.workspaceKey);
109
+ const sessions = useSuspenseQuery(grokSessionsQueryOptions(workspace.key)).data;
110
+ const [searchInput, setSearchInput] = useState('');
111
+ const [pendingDelete, setPendingDelete] = useState<PendingSessionDelete | null>(null);
112
+ const [pendingExport, setPendingExport] = useState<PendingSessionExport | null>(null);
113
+ const deferredSearch = useDeferredValue(searchInput);
114
+
115
+ const exportMutation = useMutation({
116
+ mutationFn: async ({ ids, options }: ExportSelectionMutationInput) => {
117
+ const download =
118
+ ids.length === 1
119
+ ? await exportGrokSessionFn({
120
+ data: {
121
+ includeCommentary: options.includeCommentary,
122
+ includeMetadata: options.includeMetadata,
123
+ includeTools: options.includeTools,
124
+ outputFormat: options.outputFormat,
125
+ sessionId: ids[0]!,
126
+ zipArchive: options.zipArchive,
127
+ },
128
+ })
129
+ : await exportGrokSessionsFn({
130
+ data: {
131
+ includeCommentary: options.includeCommentary,
132
+ includeMetadata: options.includeMetadata,
133
+ includeTools: options.includeTools,
134
+ outputFormat: options.outputFormat,
135
+ sessionIds: [...ids],
136
+ zipArchive: options.zipArchive,
137
+ },
138
+ });
139
+ if (download.mode === 'download') {
140
+ downloadTextFile(download.fileName, download.content, download.mimeType);
141
+ return;
142
+ }
143
+
144
+ await downloadUrlFile(download.fileName, download.downloadUrl);
145
+ },
146
+ onSuccess: () => {
147
+ setPendingExport(null);
148
+ },
149
+ });
150
+
151
+ const deleteMutation = useMutation({
152
+ mutationFn: async (sessionIds: string[]) =>
153
+ sessionIds.length === 1
154
+ ? deleteGrokSessionFn({ data: { sessionId: sessionIds[0]! } })
155
+ : deleteGrokSessionsFn({ data: { sessionIds } }),
156
+ onSettled: async (_result, _error, sessionIds) => {
157
+ await Promise.all([
158
+ queryClient.invalidateQueries({ queryKey: ['grok-workspaces'] }),
159
+ queryClient.invalidateQueries({ queryKey: ['grok-sessions', workspace.key] }),
160
+ ...sessionIds.map((sessionId) =>
161
+ queryClient.invalidateQueries({ queryKey: ['grok-session', sessionId] }),
162
+ ),
163
+ ]);
164
+ },
165
+ onSuccess: async (_result, sessionIds) => {
166
+ const workspaceEmptied = isWorkspaceEmptiedByDelete(sessions, sessionIds, (session) => session.sessionId);
167
+ setPendingDelete(null);
168
+ if (workspaceEmptied) {
169
+ await navigate({ to: '/grok' });
170
+ }
171
+ },
172
+ });
173
+
174
+ const visibleSessions = useMemo(
175
+ () =>
176
+ sessions.filter((session) =>
177
+ matchesTextQuery(deferredSearch, [
178
+ session.title,
179
+ session.sessionId,
180
+ session.agentName,
181
+ session.currentModelId,
182
+ session.modelLabel,
183
+ session.gitBranch,
184
+ ]),
185
+ ),
186
+ [deferredSearch, sessions],
187
+ );
188
+ const visibleSessionsById = useMemo(
189
+ () => new Map(visibleSessions.map((session) => [session.sessionId, session])),
190
+ [visibleSessions],
191
+ );
192
+ const lookupSelectedSessions = (sessionIds: string[]) =>
193
+ sessionIds
194
+ .map((sessionId) => visibleSessionsById.get(sessionId) ?? null)
195
+ .filter((session): session is GrokSessionSummary => session !== null);
196
+ const openExportForSessions = (selectedSessions: GrokSessionSummary[]) => {
197
+ if (selectedSessions.length === 0) {
198
+ return;
199
+ }
200
+
201
+ setPendingExport(buildSessionExport(selectedSessions));
202
+ };
203
+ const openDeleteForSessions = (selectedSessions: GrokSessionSummary[], scope: PendingSessionDelete['scope']) => {
204
+ if (selectedSessions.length > 0) {
205
+ setPendingDelete({ scope, sessions: selectedSessions });
206
+ }
207
+ };
208
+
209
+ return (
210
+ <div className="space-y-6">
211
+ <PageHeader
212
+ actions={
213
+ <div className="flex flex-col gap-2 sm:flex-row">
214
+ <Button
215
+ className="rounded-full"
216
+ disabled={deleteMutation.isPending || sessions.length === 0}
217
+ type="button"
218
+ variant="destructive"
219
+ onClick={() => openDeleteForSessions(sessions, 'all')}
220
+ >
221
+ <Trash2 className="size-4" />
222
+ Delete all
223
+ </Button>
224
+ <ListSearchInput
225
+ placeholder="Search session title, id, model, or branch"
226
+ value={searchInput}
227
+ onValueChange={setSearchInput}
228
+ />
229
+ </div>
230
+ }
231
+ eyebrow="Grok workspace"
232
+ subtitle="Inspect local Grok CLI sessions, reasoning summaries, tool calls, and exportable conversation text."
233
+ title={workspace.label}
234
+ />
235
+
236
+ <GrokSessionsTable
237
+ sessions={visibleSessions}
238
+ onDeleteSession={(session) => openDeleteForSessions([session], 'selected')}
239
+ onDeleteSessions={(sessionIds) => openDeleteForSessions(lookupSelectedSessions(sessionIds), 'selected')}
240
+ onExportSession={(session) => openExportForSessions([session])}
241
+ onExportSessions={(sessionIds) => openExportForSessions(lookupSelectedSessions(sessionIds))}
242
+ />
243
+
244
+ <ExportDialog
245
+ errorMessage={
246
+ exportMutation.isError
247
+ ? exportMutation.error instanceof Error
248
+ ? exportMutation.error.message
249
+ : 'Session export failed'
250
+ : null
251
+ }
252
+ forceZipArchive={pendingExport ? pendingExport.sessionIds.length > 1 : false}
253
+ open={pendingExport !== null}
254
+ pending={exportMutation.isPending}
255
+ title={pendingExport ? `Export ${pendingExport.label}` : 'Export session'}
256
+ onExport={(options) => {
257
+ if (pendingExport) {
258
+ exportMutation.mutate(createExportSelectionMutationInput(pendingExport.sessionIds, options));
259
+ }
260
+ }}
261
+ onOpenChange={(open) => {
262
+ if (!open) {
263
+ setPendingExport(null);
264
+ exportMutation.reset();
265
+ }
266
+ }}
267
+ />
268
+
269
+ <DeleteConfirmDialog
270
+ confirmLabel={getDeleteConfirmLabel(pendingDelete, deleteMutation.isPending)}
271
+ description={getDeleteDescription(pendingDelete)}
272
+ errorMessage={
273
+ deleteMutation.isError
274
+ ? deleteMutation.error instanceof Error
275
+ ? deleteMutation.error.message
276
+ : 'Session delete failed'
277
+ : null
278
+ }
279
+ open={pendingDelete !== null}
280
+ title={getDeleteTitle(pendingDelete)}
281
+ onConfirm={() => {
282
+ if (pendingDelete) {
283
+ deleteMutation.mutate(pendingDelete.sessions.map((session) => session.sessionId));
284
+ }
285
+ }}
286
+ onOpenChange={(open) => {
287
+ if (!open) {
288
+ setPendingDelete(null);
289
+ deleteMutation.reset();
290
+ }
291
+ }}
292
+ />
293
+ </div>
294
+ );
295
+ }
@@ -0,0 +1,48 @@
1
+ import { useSuspenseQuery } from '@tanstack/react-query';
2
+ import { createFileRoute } from '@tanstack/react-router';
3
+ import { useDeferredValue, useState } from 'react';
4
+ import { GrokWorkspacesTable } from '#/components/grok-workspaces-table';
5
+ import { ListSearchInput } from '#/components/list-search-input';
6
+ import { PageHeader } from '#/components/page-header';
7
+ import { RouteErrorPanel } from '#/components/route-error-panel';
8
+ import { grokWorkspacesQueryOptions } from '#/lib/grok-queries';
9
+ import { matchesTextQuery } from '#/lib/text-filter';
10
+
11
+ const GrokErrorComponent = ({ error }: { error: Error }) => {
12
+ return <RouteErrorPanel error={error} title="Failed to load Grok workspaces" />;
13
+ };
14
+
15
+ const GrokPage = () => {
16
+ const workspaces = useSuspenseQuery(grokWorkspacesQueryOptions()).data;
17
+ const [searchInput, setSearchInput] = useState('');
18
+ const deferredSearch = useDeferredValue(searchInput);
19
+
20
+ const visibleWorkspaces = workspaces.filter((workspace) =>
21
+ matchesTextQuery(deferredSearch, [workspace.label, workspace.worktree, workspace.key]),
22
+ );
23
+
24
+ return (
25
+ <div className="space-y-6">
26
+ <PageHeader
27
+ actions={
28
+ <ListSearchInput
29
+ placeholder="Search workspace name or path"
30
+ value={searchInput}
31
+ onValueChange={setSearchInput}
32
+ />
33
+ }
34
+ eyebrow="Inventory"
35
+ subtitle="Workspace groups are derived from local Grok CLI session directories under ~/.grok/sessions."
36
+ title="Grok"
37
+ />
38
+
39
+ <GrokWorkspacesTable workspaces={visibleWorkspaces} />
40
+ </div>
41
+ );
42
+ };
43
+
44
+ export const Route = createFileRoute('/grok/')({
45
+ component: GrokPage,
46
+ errorComponent: GrokErrorComponent,
47
+ loader: ({ context }) => context.queryClient.ensureQueryData(grokWorkspacesQueryOptions()),
48
+ });
@@ -3,6 +3,7 @@ import { createFileRoute, Link } from '@tanstack/react-router';
3
3
  import { MetricCard } from '#/components/metric-card';
4
4
  import { PageHeader } from '#/components/page-header';
5
5
  import { RecentThreadsList } from '#/components/recent-threads-list';
6
+ import { RouteErrorPanel } from '#/components/route-error-panel';
6
7
  import { dashboardQueryOptions } from '#/lib/codex-queries';
7
8
  import { formatNumber, formatTokens } from '#/lib/formatters';
8
9
 
@@ -12,24 +13,7 @@ export const Route = createFileRoute('/')({
12
13
  });
13
14
 
14
15
  function DashboardErrorComponent({ error }: { error: Error }) {
15
- const isSqlite = error.message.includes('unable to open database') || error.message.includes('database is locked');
16
- return (
17
- <div className="rounded-xl border border-[var(--border)] bg-[var(--panel)] px-6 py-10 text-center">
18
- <p className="font-medium text-[var(--destructive)] text-sm">
19
- {isSqlite ? 'Database unavailable' : 'Failed to load dashboard'}
20
- </p>
21
- <p className="mt-2 text-[var(--muted-foreground)] text-sm">
22
- {isSqlite ? 'Codex may have an exclusive lock on the database. Reload to retry.' : error.message}
23
- </p>
24
- <button
25
- className="mt-4 text-[var(--accent)] text-sm underline-offset-2 hover:underline"
26
- type="button"
27
- onClick={() => window.location.reload()}
28
- >
29
- Reload
30
- </button>
31
- </div>
32
- );
16
+ return <RouteErrorPanel error={error} title="Failed to load dashboard" />;
33
17
  }
34
18
 
35
19
  Route.update({ errorComponent: DashboardErrorComponent });
@@ -1,34 +1,30 @@
1
1
  import type { ThreadEvent, ThreadTranscriptStats } from '@spiracha/lib/codex-browser-types';
2
2
  import type { KiroSessionTranscript } from '@spiracha/lib/kiro-exporter-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, 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
20
  import { downloadTextFile, downloadUrlFile } from '#/lib/download';
21
+ import type { ExportDialogOptions } from '#/lib/export-options';
20
22
  import { formatDateTime, formatList, formatNumber } from '#/lib/formatters';
21
- import { kiroSessionDetailQueryOptions } from '#/lib/kiro-queries';
22
- import { exportKiroSessionFn } from '#/lib/kiro-server';
23
+ import { kiroSessionDetailQueryOptions, kiroWorkspacesQueryOptions } from '#/lib/kiro-queries';
24
+ import { deleteKiroSessionFn, exportKiroSessionFn } from '#/lib/kiro-server';
23
25
  import { getKiroThreadTranscriptStats, kiroTranscriptToThreadEvents } from '#/lib/kiro-transcript-events';
24
-
25
- type ExportDialogOptions = {
26
- includeCommentary: boolean;
27
- includeMetadata: boolean;
28
- includeTools: boolean;
29
- outputFormat: 'md' | 'txt';
30
- zipArchive: boolean;
31
- };
26
+ import { RouteStateResetBoundary } from '#/lib/route-state-reset';
27
+ import { shouldNavigateToSourceIndexAfterDelete } from '#/lib/workspace-delete-navigation';
32
28
 
33
29
  type TranscriptControlsProps = {
34
30
  rawJsonDisabled?: boolean;
@@ -45,7 +41,7 @@ type TranscriptControlsProps = {
45
41
  };
46
42
 
47
43
  const KiroSessionDetailErrorComponent = ({ error }: { error: Error }) => {
48
- return <ReloadErrorPanel description={error.message} title="Failed to load Kiro session" />;
44
+ return <RouteErrorPanel error={error} title="Failed to load Kiro session" />;
49
45
  };
50
46
 
51
47
  const getKiroModelLabel = (detail: KiroSessionTranscript): string => {
@@ -169,13 +165,16 @@ const KiroRawPanels = ({ detail, events }: { detail: KiroSessionTranscript; even
169
165
  };
170
166
 
171
167
  const KiroSessionDetailPage = () => {
168
+ const navigate = useNavigate();
169
+ const queryClient = useQueryClient();
172
170
  const detail = useSuspenseQuery(kiroSessionDetailQueryOptions(Route.useParams().sessionId)).data;
171
+ const [deleteOpen, setDeleteOpen] = useState(false);
173
172
  const [pendingExport, setPendingExport] = useState(false);
174
173
  const [showToolCalls, setShowToolCalls] = useState(false);
175
174
  const [showCommentary, setShowCommentary] = useState(false);
176
175
  const [showExtraEvents, setShowExtraEvents] = useState(false);
177
176
  const [showRawJson, setShowRawJson] = useState(false);
178
- const [showUserMessages, setShowUserMessages] = useState(true);
177
+ const [showUserMessages, setShowUserMessages] = useState(DEFAULT_SHOW_USER_MESSAGES);
179
178
  const transcriptEvents = useMemo(() => kiroTranscriptToThreadEvents(detail), [detail]);
180
179
  const transcriptStats = useMemo(() => getKiroThreadTranscriptStats(transcriptEvents), [transcriptEvents]);
181
180
  const modelLabel = getKiroModelLabel(detail);
@@ -204,19 +203,56 @@ const KiroSessionDetailPage = () => {
204
203
  },
205
204
  });
206
205
 
206
+ const deleteSessionMutation = useMutation({
207
+ mutationFn: () => deleteKiroSessionFn({ data: { sessionId: detail.session.sessionId } }),
208
+ onSuccess: async () => {
209
+ await Promise.all([
210
+ queryClient.invalidateQueries({ queryKey: ['kiro-workspaces'] }),
211
+ queryClient.invalidateQueries({ queryKey: ['kiro-sessions', detail.session.workspaceKey] }),
212
+ queryClient.invalidateQueries({ queryKey: ['kiro-session', detail.session.sessionId] }),
213
+ ]);
214
+ const workspaces = await queryClient.fetchQuery(kiroWorkspacesQueryOptions());
215
+ if (
216
+ shouldNavigateToSourceIndexAfterDelete(
217
+ workspaces,
218
+ detail.session.workspaceKey,
219
+ (workspace) => workspace.key,
220
+ )
221
+ ) {
222
+ navigate({ to: '/kiro' });
223
+ return;
224
+ }
225
+ navigate({
226
+ params: { workspaceKey: detail.session.workspaceKey },
227
+ to: '/kiro/$workspaceKey',
228
+ });
229
+ },
230
+ });
231
+
207
232
  return (
208
233
  <div className="space-y-6">
209
234
  <PageHeader
210
235
  actions={
211
- <Button
212
- className="rounded-full"
213
- type="button"
214
- variant="outline"
215
- onClick={() => setPendingExport(true)}
216
- >
217
- <Download className="mr-2 size-4" />
218
- Export
219
- </Button>
236
+ <>
237
+ <Button
238
+ className="rounded-full"
239
+ type="button"
240
+ variant="outline"
241
+ onClick={() => setPendingExport(true)}
242
+ >
243
+ <Download className="mr-2 size-4" />
244
+ Export
245
+ </Button>
246
+ <Button
247
+ className="rounded-full border-[var(--destructive)]/20 text-[var(--destructive)]"
248
+ type="button"
249
+ variant="outline"
250
+ onClick={() => setDeleteOpen(true)}
251
+ >
252
+ <Trash2 className="mr-2 size-4" />
253
+ Delete
254
+ </Button>
255
+ </>
220
256
  }
221
257
  breadcrumb={
222
258
  <Breadcrumbs
@@ -327,12 +363,40 @@ const KiroSessionDetailPage = () => {
327
363
  }
328
364
  }}
329
365
  />
366
+
367
+ <DeleteConfirmDialog
368
+ confirmLabel={deleteSessionMutation.isPending ? 'Deleting...' : 'Delete session'}
369
+ description="Permanently delete this Kiro session from disk. This removes the session JSON file and matching execution files."
370
+ errorMessage={
371
+ deleteSessionMutation.isError
372
+ ? deleteSessionMutation.error instanceof Error
373
+ ? deleteSessionMutation.error.message
374
+ : 'Session delete failed'
375
+ : null
376
+ }
377
+ open={deleteOpen}
378
+ title="Delete this Kiro session?"
379
+ onConfirm={() => deleteSessionMutation.mutate()}
380
+ onOpenChange={(open) => {
381
+ setDeleteOpen(open);
382
+ if (!open) {
383
+ deleteSessionMutation.reset();
384
+ }
385
+ }}
386
+ />
330
387
  </div>
331
388
  );
332
389
  };
333
390
 
334
391
  export const Route = createFileRoute('/kiro-sessions/$sessionId')({
335
- component: KiroSessionDetailPage,
392
+ component: () => {
393
+ const { sessionId } = Route.useParams();
394
+ return (
395
+ <RouteStateResetBoundary routeKey={sessionId}>
396
+ <KiroSessionDetailPage />
397
+ </RouteStateResetBoundary>
398
+ );
399
+ },
336
400
  errorComponent: KiroSessionDetailErrorComponent,
337
401
  loader: ({ context, params }) =>
338
402
  context.queryClient.ensureQueryData(kiroSessionDetailQueryOptions(params.sessionId)),