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.
- package/README.md +6 -1
- package/apps/ui/README.md +20 -2
- package/apps/ui/package.json +2 -2
- package/apps/ui/src/components/antigravity-conversations-table.tsx +50 -11
- package/apps/ui/src/components/antigravity-workspaces-table.tsx +1 -1
- package/apps/ui/src/components/app-shell.tsx +1 -0
- package/apps/ui/src/components/breadcrumbs.tsx +21 -3
- package/apps/ui/src/components/claude-code-sessions-table.tsx +42 -6
- package/apps/ui/src/components/claude-code-workspaces-table.tsx +3 -1
- package/apps/ui/src/components/cursor-threads-table.tsx +10 -47
- package/apps/ui/src/components/data-table.tsx +7 -5
- package/apps/ui/src/components/export-dialog.tsx +2 -12
- package/apps/ui/src/components/grok-sessions-table.tsx +146 -0
- package/apps/ui/src/components/grok-workspaces-table.tsx +53 -0
- package/apps/ui/src/components/kiro-sessions-table.tsx +40 -4
- package/apps/ui/src/components/opencode-sessions-table.tsx +40 -4
- package/apps/ui/src/components/page-header.tsx +15 -9
- package/apps/ui/src/components/qoder-sessions-table.tsx +23 -2
- package/apps/ui/src/components/qoder-workspaces-table.tsx +5 -1
- package/apps/ui/src/components/selection-actions-toolbar.tsx +80 -0
- package/apps/ui/src/components/threads-table.tsx +9 -46
- package/apps/ui/src/components/transcript-view.tsx +148 -52
- package/apps/ui/src/components/ui/select.tsx +1 -1
- package/apps/ui/src/lib/antigravity-conversation-state.ts +17 -4
- package/apps/ui/src/lib/antigravity-server.ts +152 -7
- package/apps/ui/src/lib/antigravity-transcript-events.ts +11 -3
- package/apps/ui/src/lib/claude-code-queries.ts +8 -0
- package/apps/ui/src/lib/claude-code-server.ts +175 -7
- package/apps/ui/src/lib/claude-code-transcript-events.ts +8 -1
- package/apps/ui/src/lib/codex-queries.ts +19 -3
- package/apps/ui/src/lib/codex-server.ts +135 -53
- package/apps/ui/src/lib/cursor-server.ts +48 -5
- package/apps/ui/src/lib/formatters.ts +3 -5
- package/apps/ui/src/lib/grok-queries.ts +22 -0
- package/apps/ui/src/lib/grok-server.ts +169 -0
- package/apps/ui/src/lib/grok-transcript-events.ts +149 -0
- package/apps/ui/src/lib/kiro-server.ts +85 -7
- package/apps/ui/src/lib/opencode-server.ts +85 -7
- package/apps/ui/src/lib/qoder-server.ts +67 -11
- package/apps/ui/src/lib/route-search.ts +114 -0
- package/apps/ui/src/lib/source-session-export-server.ts +86 -3
- package/apps/ui/src/lib/thread-transcript-load.ts +15 -0
- package/apps/ui/src/lib/workspace-delete-navigation.ts +12 -0
- package/apps/ui/src/routeTree.gen.ts +107 -0
- package/apps/ui/src/routes/antigravity-conversations.$conversationId.tsx +76 -10
- package/apps/ui/src/routes/antigravity.$workspaceKey.tsx +269 -31
- package/apps/ui/src/routes/api.v1.conversations.$source.$id.ts +4 -0
- package/apps/ui/src/routes/api.v1.conversations.delete.ts +12 -0
- package/apps/ui/src/routes/api.v1.conversations.export.ts +12 -0
- package/apps/ui/src/routes/claude-code-sessions.$sessionId.tsx +143 -17
- package/apps/ui/src/routes/claude-code.$workspaceKey.tsx +165 -26
- package/apps/ui/src/routes/cursor-threads.$composerId.tsx +2 -2
- package/apps/ui/src/routes/cursor.$workspaceKey.tsx +11 -1
- package/apps/ui/src/routes/grok-sessions.$sessionId.tsx +388 -0
- package/apps/ui/src/routes/grok.$workspaceKey.tsx +301 -0
- package/apps/ui/src/routes/grok.index.tsx +48 -0
- package/apps/ui/src/routes/kiro-sessions.$sessionId.tsx +66 -15
- package/apps/ui/src/routes/kiro.$workspaceKey.tsx +199 -32
- package/apps/ui/src/routes/opencode-sessions.$sessionId.tsx +105 -19
- package/apps/ui/src/routes/opencode.$workspaceKey.tsx +236 -44
- package/apps/ui/src/routes/opencode.index.tsx +16 -3
- package/apps/ui/src/routes/qoder-sessions.$sessionId.tsx +7 -4
- package/apps/ui/src/routes/qoder.$workspaceKey.tsx +75 -28
- package/apps/ui/src/routes/threads.$threadId.tsx +548 -64
- package/package.json +19 -18
- package/src/client.ts +134 -1
- package/src/lib/antigravity-db.ts +208 -32
- package/src/lib/antigravity-exporter-types.ts +3 -0
- package/src/lib/antigravity-projects.ts +201 -0
- package/src/lib/claude-code-db.ts +498 -32
- package/src/lib/claude-code-exporter-types.ts +5 -0
- package/src/lib/claude-code-transcript-phase.ts +60 -1
- package/src/lib/claude-code-transcript.ts +8 -5
- package/src/lib/codex-browser-export.ts +23 -27
- package/src/lib/codex-thread-cache.ts +41 -13
- package/src/lib/codex-thread-parser.ts +86 -35
- package/src/lib/codex-transcript-filter.ts +31 -0
- package/src/lib/codex-transcript-renderer.ts +39 -19
- package/src/lib/concurrency.ts +41 -0
- package/src/lib/conversation-api.ts +399 -19
- package/src/lib/conversation-data/antigravity-adapter.ts +21 -2
- package/src/lib/conversation-data/claude-code-adapter.ts +37 -6
- package/src/lib/conversation-data/codex-adapter.ts +28 -4
- package/src/lib/conversation-data/cursor-adapter.ts +35 -1
- package/src/lib/conversation-data/grok-adapter.ts +210 -0
- package/src/lib/conversation-data/index.ts +76 -1
- package/src/lib/conversation-data/kiro-adapter.ts +41 -7
- package/src/lib/conversation-data/opencode-adapter.ts +24 -2
- package/src/lib/conversation-data/qoder-adapter.ts +44 -19
- package/src/lib/conversation-data/types.ts +43 -0
- package/src/lib/conversation-zip-export.ts +57 -0
- package/src/lib/cursor-db.ts +34 -5
- package/src/lib/grok-db.ts +1026 -0
- package/src/lib/grok-exporter-types.ts +110 -0
- package/src/lib/grok-transcript-phase.ts +52 -0
- package/src/lib/grok-transcript.ts +154 -0
- package/src/lib/kiro-db.ts +52 -1
- package/src/lib/model-label.ts +11 -3
- package/src/lib/opencode-db.ts +598 -55
- package/src/lib/opencode-transcript.ts +8 -5
- package/src/lib/shared.ts +12 -0
- package/src/lib/transcript-load-limiter.ts +82 -0
- package/src/lib/ui-cache.ts +43 -17
- 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(
|
|
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
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
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
|
};
|