spiracha 2.0.0 → 2.1.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 (104) hide show
  1. package/README.md +6 -1
  2. package/apps/ui/README.md +20 -2
  3. package/apps/ui/package.json +2 -2
  4. package/apps/ui/src/components/antigravity-conversations-table.tsx +50 -11
  5. package/apps/ui/src/components/antigravity-workspaces-table.tsx +1 -1
  6. package/apps/ui/src/components/app-shell.tsx +1 -0
  7. package/apps/ui/src/components/breadcrumbs.tsx +21 -3
  8. package/apps/ui/src/components/claude-code-sessions-table.tsx +42 -6
  9. package/apps/ui/src/components/claude-code-workspaces-table.tsx +3 -1
  10. package/apps/ui/src/components/cursor-threads-table.tsx +10 -47
  11. package/apps/ui/src/components/data-table.tsx +7 -5
  12. package/apps/ui/src/components/export-dialog.tsx +2 -12
  13. package/apps/ui/src/components/grok-sessions-table.tsx +146 -0
  14. package/apps/ui/src/components/grok-workspaces-table.tsx +53 -0
  15. package/apps/ui/src/components/kiro-sessions-table.tsx +40 -4
  16. package/apps/ui/src/components/opencode-sessions-table.tsx +40 -4
  17. package/apps/ui/src/components/page-header.tsx +15 -9
  18. package/apps/ui/src/components/qoder-sessions-table.tsx +23 -2
  19. package/apps/ui/src/components/qoder-workspaces-table.tsx +5 -1
  20. package/apps/ui/src/components/selection-actions-toolbar.tsx +80 -0
  21. package/apps/ui/src/components/threads-table.tsx +9 -46
  22. package/apps/ui/src/components/transcript-view.tsx +148 -52
  23. package/apps/ui/src/components/ui/select.tsx +1 -1
  24. package/apps/ui/src/lib/antigravity-conversation-state.ts +17 -4
  25. package/apps/ui/src/lib/antigravity-server.ts +152 -7
  26. package/apps/ui/src/lib/antigravity-transcript-events.ts +11 -3
  27. package/apps/ui/src/lib/claude-code-queries.ts +8 -0
  28. package/apps/ui/src/lib/claude-code-server.ts +175 -7
  29. package/apps/ui/src/lib/claude-code-transcript-events.ts +8 -1
  30. package/apps/ui/src/lib/codex-queries.ts +19 -3
  31. package/apps/ui/src/lib/codex-server.ts +135 -53
  32. package/apps/ui/src/lib/cursor-server.ts +48 -5
  33. package/apps/ui/src/lib/formatters.ts +3 -5
  34. package/apps/ui/src/lib/grok-queries.ts +22 -0
  35. package/apps/ui/src/lib/grok-server.ts +169 -0
  36. package/apps/ui/src/lib/grok-transcript-events.ts +149 -0
  37. package/apps/ui/src/lib/kiro-server.ts +85 -7
  38. package/apps/ui/src/lib/opencode-server.ts +85 -7
  39. package/apps/ui/src/lib/qoder-server.ts +67 -11
  40. package/apps/ui/src/lib/route-search.ts +114 -0
  41. package/apps/ui/src/lib/source-session-export-server.ts +86 -3
  42. package/apps/ui/src/lib/thread-transcript-load.ts +15 -0
  43. package/apps/ui/src/lib/workspace-delete-navigation.ts +12 -0
  44. package/apps/ui/src/routeTree.gen.ts +107 -0
  45. package/apps/ui/src/routes/antigravity-conversations.$conversationId.tsx +76 -10
  46. package/apps/ui/src/routes/antigravity.$workspaceKey.tsx +269 -31
  47. package/apps/ui/src/routes/api.v1.conversations.$source.$id.ts +4 -0
  48. package/apps/ui/src/routes/api.v1.conversations.delete.ts +12 -0
  49. package/apps/ui/src/routes/api.v1.conversations.export.ts +12 -0
  50. package/apps/ui/src/routes/claude-code-sessions.$sessionId.tsx +143 -17
  51. package/apps/ui/src/routes/claude-code.$workspaceKey.tsx +165 -26
  52. package/apps/ui/src/routes/cursor-threads.$composerId.tsx +2 -2
  53. package/apps/ui/src/routes/cursor.$workspaceKey.tsx +11 -1
  54. package/apps/ui/src/routes/grok-sessions.$sessionId.tsx +388 -0
  55. package/apps/ui/src/routes/grok.$workspaceKey.tsx +301 -0
  56. package/apps/ui/src/routes/grok.index.tsx +48 -0
  57. package/apps/ui/src/routes/kiro-sessions.$sessionId.tsx +66 -15
  58. package/apps/ui/src/routes/kiro.$workspaceKey.tsx +199 -32
  59. package/apps/ui/src/routes/opencode-sessions.$sessionId.tsx +105 -19
  60. package/apps/ui/src/routes/opencode.$workspaceKey.tsx +236 -44
  61. package/apps/ui/src/routes/opencode.index.tsx +16 -3
  62. package/apps/ui/src/routes/qoder-sessions.$sessionId.tsx +7 -4
  63. package/apps/ui/src/routes/qoder.$workspaceKey.tsx +75 -28
  64. package/apps/ui/src/routes/threads.$threadId.tsx +548 -64
  65. package/package.json +19 -18
  66. package/src/client.ts +134 -1
  67. package/src/lib/antigravity-db.ts +208 -32
  68. package/src/lib/antigravity-exporter-types.ts +3 -0
  69. package/src/lib/antigravity-projects.ts +201 -0
  70. package/src/lib/claude-code-db.ts +498 -32
  71. package/src/lib/claude-code-exporter-types.ts +5 -0
  72. package/src/lib/claude-code-transcript-phase.ts +60 -1
  73. package/src/lib/claude-code-transcript.ts +8 -5
  74. package/src/lib/codex-browser-export.ts +23 -27
  75. package/src/lib/codex-thread-cache.ts +41 -13
  76. package/src/lib/codex-thread-parser.ts +86 -35
  77. package/src/lib/codex-transcript-filter.ts +31 -0
  78. package/src/lib/codex-transcript-renderer.ts +39 -19
  79. package/src/lib/concurrency.ts +41 -0
  80. package/src/lib/conversation-api.ts +399 -19
  81. package/src/lib/conversation-data/antigravity-adapter.ts +21 -2
  82. package/src/lib/conversation-data/claude-code-adapter.ts +37 -6
  83. package/src/lib/conversation-data/codex-adapter.ts +28 -4
  84. package/src/lib/conversation-data/cursor-adapter.ts +35 -1
  85. package/src/lib/conversation-data/grok-adapter.ts +210 -0
  86. package/src/lib/conversation-data/index.ts +76 -1
  87. package/src/lib/conversation-data/kiro-adapter.ts +41 -7
  88. package/src/lib/conversation-data/opencode-adapter.ts +24 -2
  89. package/src/lib/conversation-data/qoder-adapter.ts +44 -19
  90. package/src/lib/conversation-data/types.ts +43 -0
  91. package/src/lib/conversation-zip-export.ts +57 -0
  92. package/src/lib/cursor-db.ts +34 -5
  93. package/src/lib/grok-db.ts +1026 -0
  94. package/src/lib/grok-exporter-types.ts +110 -0
  95. package/src/lib/grok-transcript-phase.ts +52 -0
  96. package/src/lib/grok-transcript.ts +154 -0
  97. package/src/lib/kiro-db.ts +52 -1
  98. package/src/lib/model-label.ts +11 -3
  99. package/src/lib/opencode-db.ts +598 -55
  100. package/src/lib/opencode-transcript.ts +8 -5
  101. package/src/lib/shared.ts +12 -0
  102. package/src/lib/transcript-load-limiter.ts +82 -0
  103. package/src/lib/ui-cache.ts +43 -17
  104. package/src/lib/ui-export-archive.ts +60 -0
@@ -0,0 +1,301 @@
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 { ReloadErrorPanel } from '#/components/reload-error-panel';
13
+ import { Button } from '#/components/ui/button';
14
+ import { downloadTextFile, downloadUrlFile } from '#/lib/download';
15
+ import { grokSessionsQueryOptions, grokWorkspacesQueryOptions } from '#/lib/grok-queries';
16
+ import {
17
+ deleteGrokSessionFn,
18
+ deleteGrokSessionsFn,
19
+ exportGrokSessionFn,
20
+ exportGrokSessionsFn,
21
+ } from '#/lib/grok-server';
22
+ import { matchesTextQuery } from '#/lib/text-filter';
23
+ import { isWorkspaceEmptiedByDelete } from '#/lib/workspace-delete-navigation';
24
+
25
+ type ExportDialogOptions = {
26
+ includeCommentary: boolean;
27
+ includeMetadata: boolean;
28
+ includeTools: boolean;
29
+ outputFormat: 'md' | 'txt';
30
+ zipArchive: boolean;
31
+ };
32
+
33
+ type PendingSessionDelete = {
34
+ scope: 'all' | 'selected';
35
+ sessions: GrokSessionSummary[];
36
+ };
37
+
38
+ type PendingSessionExport = {
39
+ label: string;
40
+ sessionIds: string[];
41
+ };
42
+
43
+ const findWorkspaceOrThrow = (workspaces: GrokWorkspaceGroup[], workspaceKey: string) => {
44
+ const workspace = workspaces.find((candidate) => candidate.key === workspaceKey);
45
+ if (!workspace) {
46
+ throw new Error(`Grok workspace not found: ${workspaceKey}`);
47
+ }
48
+
49
+ return workspace;
50
+ };
51
+
52
+ const buildSessionExport = (selectedSessions: GrokSessionSummary[]): PendingSessionExport => ({
53
+ label: selectedSessions.length === 1 ? selectedSessions[0]!.title : `${selectedSessions.length} selected sessions`,
54
+ sessionIds: selectedSessions.map((session) => session.sessionId),
55
+ });
56
+
57
+ const getDeleteConfirmLabel = (pendingDelete: PendingSessionDelete | null, isPending: boolean) => {
58
+ if (isPending) {
59
+ return 'Deleting...';
60
+ }
61
+
62
+ if (pendingDelete?.scope === 'all') {
63
+ return 'Delete all';
64
+ }
65
+
66
+ return pendingDelete && pendingDelete.sessions.length > 1 ? 'Delete sessions' : 'Delete session';
67
+ };
68
+
69
+ const getDeleteDescription = (pendingDelete: PendingSessionDelete | null) => {
70
+ if (!pendingDelete) {
71
+ return 'Permanently delete the selected Grok sessions from local history.';
72
+ }
73
+
74
+ if (pendingDelete.scope === 'all') {
75
+ 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.`;
76
+ }
77
+
78
+ if (pendingDelete.sessions.length === 1) {
79
+ return `Permanently delete "${pendingDelete.sessions[0]!.title}" from Grok history. This removes the session directory and transcript files under ~/.grok/sessions.`;
80
+ }
81
+
82
+ return `Permanently delete ${pendingDelete.sessions.length} selected Grok sessions from local history. This removes their session directories and transcript files under ~/.grok/sessions.`;
83
+ };
84
+
85
+ const getDeleteTitle = (pendingDelete: PendingSessionDelete | null) => {
86
+ if (pendingDelete?.scope === 'all') {
87
+ return `Delete all ${pendingDelete.sessions.length} Grok sessions?`;
88
+ }
89
+
90
+ return pendingDelete && pendingDelete.sessions.length > 1
91
+ ? `Delete ${pendingDelete.sessions.length} Grok sessions?`
92
+ : 'Delete this Grok session?';
93
+ };
94
+
95
+ export const Route = createFileRoute('/grok/$workspaceKey')({
96
+ component: GrokWorkspacePage,
97
+ errorComponent: GrokWorkspaceErrorComponent,
98
+ loader: async ({ context, params }) => {
99
+ const workspaces = await context.queryClient.ensureQueryData(grokWorkspacesQueryOptions());
100
+ findWorkspaceOrThrow(workspaces, params.workspaceKey);
101
+ await context.queryClient.ensureQueryData(grokSessionsQueryOptions(params.workspaceKey));
102
+ },
103
+ pendingComponent: () => <LoadingPanel description="Loading Grok sessions." title="Loading workspace" />,
104
+ });
105
+
106
+ function GrokWorkspaceErrorComponent({ error }: { error: Error }) {
107
+ return <ReloadErrorPanel description={error.message} title="Failed to load Grok workspace" />;
108
+ }
109
+
110
+ function GrokWorkspacePage() {
111
+ const navigate = useNavigate({ from: Route.fullPath });
112
+ const params = Route.useParams();
113
+ const queryClient = useQueryClient();
114
+ const workspaces = useSuspenseQuery(grokWorkspacesQueryOptions()).data;
115
+ const workspace = findWorkspaceOrThrow(workspaces, params.workspaceKey);
116
+ const sessions = useSuspenseQuery(grokSessionsQueryOptions(workspace.key)).data;
117
+ const [searchInput, setSearchInput] = useState('');
118
+ const [pendingDelete, setPendingDelete] = useState<PendingSessionDelete | null>(null);
119
+ const [pendingExport, setPendingExport] = useState<PendingSessionExport | null>(null);
120
+ const deferredSearch = useDeferredValue(searchInput);
121
+
122
+ const exportMutation = useMutation({
123
+ mutationFn: async (options: ExportDialogOptions) => {
124
+ if (!pendingExport) {
125
+ throw new Error('No Grok session selected for export');
126
+ }
127
+
128
+ const download =
129
+ pendingExport.sessionIds.length === 1
130
+ ? await exportGrokSessionFn({
131
+ data: {
132
+ includeCommentary: options.includeCommentary,
133
+ includeMetadata: options.includeMetadata,
134
+ includeTools: options.includeTools,
135
+ outputFormat: options.outputFormat,
136
+ sessionId: pendingExport.sessionIds[0]!,
137
+ zipArchive: options.zipArchive,
138
+ },
139
+ })
140
+ : await exportGrokSessionsFn({
141
+ data: {
142
+ includeCommentary: options.includeCommentary,
143
+ includeMetadata: options.includeMetadata,
144
+ includeTools: options.includeTools,
145
+ outputFormat: options.outputFormat,
146
+ sessionIds: pendingExport.sessionIds,
147
+ zipArchive: options.zipArchive,
148
+ },
149
+ });
150
+ if (download.mode === 'download') {
151
+ downloadTextFile(download.fileName, download.content, download.mimeType);
152
+ return;
153
+ }
154
+
155
+ await downloadUrlFile(download.fileName, download.downloadUrl);
156
+ },
157
+ onSuccess: () => {
158
+ setPendingExport(null);
159
+ },
160
+ });
161
+
162
+ const deleteMutation = useMutation({
163
+ mutationFn: async (sessionIds: string[]) =>
164
+ sessionIds.length === 1
165
+ ? deleteGrokSessionFn({ data: { sessionId: sessionIds[0]! } })
166
+ : deleteGrokSessionsFn({ data: { sessionIds } }),
167
+ onSuccess: async (_result, sessionIds) => {
168
+ const workspaceEmptied = isWorkspaceEmptiedByDelete(sessions, sessionIds, (session) => session.sessionId);
169
+ setPendingDelete(null);
170
+ if (workspaceEmptied) {
171
+ await navigate({ to: '/grok' });
172
+ }
173
+
174
+ await Promise.all([
175
+ queryClient.invalidateQueries({ queryKey: ['grok-workspaces'] }),
176
+ queryClient.invalidateQueries({ queryKey: ['grok-sessions', workspace.key] }),
177
+ ...sessionIds.map((sessionId) =>
178
+ queryClient.invalidateQueries({ queryKey: ['grok-session', sessionId] }),
179
+ ),
180
+ ]);
181
+ },
182
+ });
183
+
184
+ const visibleSessions = useMemo(
185
+ () =>
186
+ sessions.filter((session) =>
187
+ matchesTextQuery(deferredSearch, [
188
+ session.title,
189
+ session.sessionId,
190
+ session.agentName,
191
+ session.currentModelId,
192
+ session.modelLabel,
193
+ session.gitBranch,
194
+ ]),
195
+ ),
196
+ [deferredSearch, sessions],
197
+ );
198
+ const visibleSessionsById = useMemo(
199
+ () => new Map(visibleSessions.map((session) => [session.sessionId, session])),
200
+ [visibleSessions],
201
+ );
202
+ const lookupSelectedSessions = (sessionIds: string[]) =>
203
+ sessionIds
204
+ .map((sessionId) => visibleSessionsById.get(sessionId) ?? null)
205
+ .filter((session): session is GrokSessionSummary => session !== null);
206
+ const openExportForSessions = (selectedSessions: GrokSessionSummary[]) => {
207
+ if (selectedSessions.length === 0) {
208
+ return;
209
+ }
210
+
211
+ setPendingExport(buildSessionExport(selectedSessions));
212
+ };
213
+ const openDeleteForSessions = (selectedSessions: GrokSessionSummary[], scope: PendingSessionDelete['scope']) => {
214
+ if (selectedSessions.length > 0) {
215
+ setPendingDelete({ scope, sessions: selectedSessions });
216
+ }
217
+ };
218
+
219
+ return (
220
+ <div className="space-y-6">
221
+ <PageHeader
222
+ actions={
223
+ <div className="flex flex-col gap-2 sm:flex-row">
224
+ <Button
225
+ className="rounded-full"
226
+ disabled={deleteMutation.isPending || sessions.length === 0}
227
+ type="button"
228
+ variant="destructive"
229
+ onClick={() => openDeleteForSessions(sessions, 'all')}
230
+ >
231
+ <Trash2 className="size-4" />
232
+ Delete all
233
+ </Button>
234
+ <ListSearchInput
235
+ placeholder="Search session title, id, model, or branch"
236
+ value={searchInput}
237
+ onValueChange={setSearchInput}
238
+ />
239
+ </div>
240
+ }
241
+ eyebrow="Grok workspace"
242
+ subtitle="Inspect local Grok CLI sessions, reasoning summaries, tool calls, and exportable conversation text."
243
+ title={workspace.label}
244
+ />
245
+
246
+ <GrokSessionsTable
247
+ sessions={visibleSessions}
248
+ onDeleteSession={(session) => openDeleteForSessions([session], 'selected')}
249
+ onDeleteSessions={(sessionIds) => openDeleteForSessions(lookupSelectedSessions(sessionIds), 'selected')}
250
+ onExportSession={(session) => openExportForSessions([session])}
251
+ onExportSessions={(sessionIds) => openExportForSessions(lookupSelectedSessions(sessionIds))}
252
+ />
253
+
254
+ <ExportDialog
255
+ errorMessage={
256
+ exportMutation.isError
257
+ ? exportMutation.error instanceof Error
258
+ ? exportMutation.error.message
259
+ : 'Session export failed'
260
+ : null
261
+ }
262
+ forceZipArchive={pendingExport ? pendingExport.sessionIds.length > 1 : false}
263
+ open={pendingExport !== null}
264
+ pending={exportMutation.isPending}
265
+ title={pendingExport ? `Export ${pendingExport.label}` : 'Export session'}
266
+ onExport={(options) => exportMutation.mutate(options)}
267
+ onOpenChange={(open) => {
268
+ if (!open) {
269
+ setPendingExport(null);
270
+ exportMutation.reset();
271
+ }
272
+ }}
273
+ />
274
+
275
+ <DeleteConfirmDialog
276
+ confirmLabel={getDeleteConfirmLabel(pendingDelete, deleteMutation.isPending)}
277
+ description={getDeleteDescription(pendingDelete)}
278
+ errorMessage={
279
+ deleteMutation.isError
280
+ ? deleteMutation.error instanceof Error
281
+ ? deleteMutation.error.message
282
+ : 'Session delete failed'
283
+ : null
284
+ }
285
+ open={pendingDelete !== null}
286
+ title={getDeleteTitle(pendingDelete)}
287
+ onConfirm={() => {
288
+ if (pendingDelete) {
289
+ deleteMutation.mutate(pendingDelete.sessions.map((session) => session.sessionId));
290
+ }
291
+ }}
292
+ onOpenChange={(open) => {
293
+ if (!open) {
294
+ setPendingDelete(null);
295
+ deleteMutation.reset();
296
+ }
297
+ }}
298
+ />
299
+ </div>
300
+ );
301
+ }
@@ -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 { ReloadErrorPanel } from '#/components/reload-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 <ReloadErrorPanel description={error.message} 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
+ });
@@ -1,10 +1,11 @@
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';
@@ -12,14 +13,14 @@ import { MetadataSection } from '#/components/metadata-section';
12
13
  import { MetricCard } from '#/components/metric-card';
13
14
  import { PageHeader } from '#/components/page-header';
14
15
  import { ReloadErrorPanel } from '#/components/reload-error-panel';
15
- import { TranscriptView } from '#/components/transcript-view';
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';
20
21
  import { formatDateTime, formatList, formatNumber } from '#/lib/formatters';
21
22
  import { kiroSessionDetailQueryOptions } from '#/lib/kiro-queries';
22
- import { exportKiroSessionFn } from '#/lib/kiro-server';
23
+ import { deleteKiroSessionFn, exportKiroSessionFn } from '#/lib/kiro-server';
23
24
  import { getKiroThreadTranscriptStats, kiroTranscriptToThreadEvents } from '#/lib/kiro-transcript-events';
24
25
 
25
26
  type ExportDialogOptions = {
@@ -169,13 +170,16 @@ const KiroRawPanels = ({ detail, events }: { detail: KiroSessionTranscript; even
169
170
  };
170
171
 
171
172
  const KiroSessionDetailPage = () => {
173
+ const navigate = useNavigate();
174
+ const queryClient = useQueryClient();
172
175
  const detail = useSuspenseQuery(kiroSessionDetailQueryOptions(Route.useParams().sessionId)).data;
176
+ const [deleteOpen, setDeleteOpen] = useState(false);
173
177
  const [pendingExport, setPendingExport] = useState(false);
174
178
  const [showToolCalls, setShowToolCalls] = useState(false);
175
179
  const [showCommentary, setShowCommentary] = useState(false);
176
180
  const [showExtraEvents, setShowExtraEvents] = useState(false);
177
181
  const [showRawJson, setShowRawJson] = useState(false);
178
- const [showUserMessages, setShowUserMessages] = useState(true);
182
+ const [showUserMessages, setShowUserMessages] = useState(DEFAULT_SHOW_USER_MESSAGES);
179
183
  const transcriptEvents = useMemo(() => kiroTranscriptToThreadEvents(detail), [detail]);
180
184
  const transcriptStats = useMemo(() => getKiroThreadTranscriptStats(transcriptEvents), [transcriptEvents]);
181
185
  const modelLabel = getKiroModelLabel(detail);
@@ -204,19 +208,45 @@ const KiroSessionDetailPage = () => {
204
208
  },
205
209
  });
206
210
 
211
+ const deleteSessionMutation = useMutation({
212
+ mutationFn: () => deleteKiroSessionFn({ data: { sessionId: detail.session.sessionId } }),
213
+ onSuccess: async () => {
214
+ await Promise.all([
215
+ queryClient.invalidateQueries({ queryKey: ['kiro-workspaces'] }),
216
+ queryClient.invalidateQueries({ queryKey: ['kiro-sessions', detail.session.workspaceKey] }),
217
+ queryClient.invalidateQueries({ queryKey: ['kiro-session', detail.session.sessionId] }),
218
+ ]);
219
+ navigate({
220
+ params: { workspaceKey: detail.session.workspaceKey },
221
+ to: '/kiro/$workspaceKey',
222
+ });
223
+ },
224
+ });
225
+
207
226
  return (
208
227
  <div className="space-y-6">
209
228
  <PageHeader
210
229
  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>
230
+ <>
231
+ <Button
232
+ className="rounded-full"
233
+ type="button"
234
+ variant="outline"
235
+ onClick={() => setPendingExport(true)}
236
+ >
237
+ <Download className="mr-2 size-4" />
238
+ Export
239
+ </Button>
240
+ <Button
241
+ className="rounded-full border-[var(--destructive)]/20 text-[var(--destructive)]"
242
+ type="button"
243
+ variant="outline"
244
+ onClick={() => setDeleteOpen(true)}
245
+ >
246
+ <Trash2 className="mr-2 size-4" />
247
+ Delete
248
+ </Button>
249
+ </>
220
250
  }
221
251
  breadcrumb={
222
252
  <Breadcrumbs
@@ -327,6 +357,27 @@ const KiroSessionDetailPage = () => {
327
357
  }
328
358
  }}
329
359
  />
360
+
361
+ <DeleteConfirmDialog
362
+ confirmLabel={deleteSessionMutation.isPending ? 'Deleting...' : 'Delete session'}
363
+ description="Permanently delete this Kiro session from disk. This removes the session JSON file and matching execution files."
364
+ errorMessage={
365
+ deleteSessionMutation.isError
366
+ ? deleteSessionMutation.error instanceof Error
367
+ ? deleteSessionMutation.error.message
368
+ : 'Session delete failed'
369
+ : null
370
+ }
371
+ open={deleteOpen}
372
+ title="Delete this Kiro session?"
373
+ onConfirm={() => deleteSessionMutation.mutate()}
374
+ onOpenChange={(open) => {
375
+ setDeleteOpen(open);
376
+ if (!open) {
377
+ deleteSessionMutation.reset();
378
+ }
379
+ }}
380
+ />
330
381
  </div>
331
382
  );
332
383
  };