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
|
@@ -1,17 +1,26 @@
|
|
|
1
1
|
import type { KiroSessionSummary, KiroWorkspaceGroup } from '@spiracha/lib/kiro-exporter-types';
|
|
2
|
-
import { useMutation, useSuspenseQuery } from '@tanstack/react-query';
|
|
3
|
-
import { createFileRoute } from '@tanstack/react-router';
|
|
4
|
-
import {
|
|
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';
|
|
5
7
|
import { ExportDialog } from '#/components/export-dialog';
|
|
6
8
|
import { KiroSessionsTable } from '#/components/kiro-sessions-table';
|
|
7
9
|
import { ListSearchInput } from '#/components/list-search-input';
|
|
8
10
|
import { LoadingPanel } from '#/components/loading-panel';
|
|
9
11
|
import { PageHeader } from '#/components/page-header';
|
|
10
12
|
import { ReloadErrorPanel } from '#/components/reload-error-panel';
|
|
13
|
+
import { Button } from '#/components/ui/button';
|
|
11
14
|
import { downloadTextFile, downloadUrlFile } from '#/lib/download';
|
|
12
15
|
import { kiroSessionsQueryOptions, kiroWorkspacesQueryOptions } from '#/lib/kiro-queries';
|
|
13
|
-
import {
|
|
16
|
+
import {
|
|
17
|
+
deleteKiroSessionFn,
|
|
18
|
+
deleteKiroSessionsFn,
|
|
19
|
+
exportKiroSessionFn,
|
|
20
|
+
exportKiroSessionsFn,
|
|
21
|
+
} from '#/lib/kiro-server';
|
|
14
22
|
import { matchesTextQuery } from '#/lib/text-filter';
|
|
23
|
+
import { isWorkspaceEmptiedByDelete } from '#/lib/workspace-delete-navigation';
|
|
15
24
|
|
|
16
25
|
type ExportDialogOptions = {
|
|
17
26
|
includeCommentary: boolean;
|
|
@@ -21,6 +30,16 @@ type ExportDialogOptions = {
|
|
|
21
30
|
zipArchive: boolean;
|
|
22
31
|
};
|
|
23
32
|
|
|
33
|
+
type PendingSessionDelete = {
|
|
34
|
+
scope: 'all' | 'selected';
|
|
35
|
+
sessions: KiroSessionSummary[];
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
type PendingSessionExport = {
|
|
39
|
+
label: string;
|
|
40
|
+
sessionIds: string[];
|
|
41
|
+
};
|
|
42
|
+
|
|
24
43
|
const findWorkspaceOrThrow = (workspaces: KiroWorkspaceGroup[], workspaceKey: string) => {
|
|
25
44
|
const workspace = workspaces.find((candidate) => candidate.key === workspaceKey);
|
|
26
45
|
if (!workspace) {
|
|
@@ -30,17 +49,63 @@ const findWorkspaceOrThrow = (workspaces: KiroWorkspaceGroup[], workspaceKey: st
|
|
|
30
49
|
return workspace;
|
|
31
50
|
};
|
|
32
51
|
|
|
52
|
+
const buildSessionExport = (selectedSessions: KiroSessionSummary[]): 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 Kiro sessions from disk.';
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (pendingDelete.scope === 'all') {
|
|
75
|
+
return `Permanently delete all ${pendingDelete.sessions.length} Kiro sessions in this workspace from disk. This removes session JSON files and matching execution files.`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (pendingDelete.sessions.length === 1) {
|
|
79
|
+
return `Permanently delete "${pendingDelete.sessions[0]!.title}" from Kiro history. This removes the session JSON file and matching execution files from disk.`;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return `Permanently delete ${pendingDelete.sessions.length} selected Kiro sessions from disk. This removes session JSON files and matching execution files.`;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const getDeleteTitle = (pendingDelete: PendingSessionDelete | null) => {
|
|
86
|
+
if (pendingDelete?.scope === 'all') {
|
|
87
|
+
return `Delete all ${pendingDelete.sessions.length} Kiro sessions?`;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return pendingDelete && pendingDelete.sessions.length > 1
|
|
91
|
+
? `Delete ${pendingDelete.sessions.length} Kiro sessions?`
|
|
92
|
+
: 'Delete this Kiro session?';
|
|
93
|
+
};
|
|
94
|
+
|
|
33
95
|
const KiroWorkspaceErrorComponent = ({ error }: { error: Error }) => {
|
|
34
96
|
return <ReloadErrorPanel description={error.message} title="Failed to load Kiro workspace" />;
|
|
35
97
|
};
|
|
36
98
|
|
|
37
99
|
const KiroWorkspacePage = () => {
|
|
100
|
+
const navigate = useNavigate({ from: Route.fullPath });
|
|
38
101
|
const params = Route.useParams();
|
|
102
|
+
const queryClient = useQueryClient();
|
|
39
103
|
const workspaces = useSuspenseQuery(kiroWorkspacesQueryOptions()).data;
|
|
40
104
|
const workspace = findWorkspaceOrThrow(workspaces, params.workspaceKey);
|
|
41
105
|
const sessions = useSuspenseQuery(kiroSessionsQueryOptions(workspace.key)).data;
|
|
42
106
|
const [searchInput, setSearchInput] = useState('');
|
|
43
|
-
const [
|
|
107
|
+
const [pendingDelete, setPendingDelete] = useState<PendingSessionDelete | null>(null);
|
|
108
|
+
const [pendingExport, setPendingExport] = useState<PendingSessionExport | null>(null);
|
|
44
109
|
const deferredSearch = useDeferredValue(searchInput);
|
|
45
110
|
|
|
46
111
|
const exportMutation = useMutation({
|
|
@@ -49,16 +114,28 @@ const KiroWorkspacePage = () => {
|
|
|
49
114
|
throw new Error('No Kiro session selected for export');
|
|
50
115
|
}
|
|
51
116
|
|
|
52
|
-
const download =
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
117
|
+
const download =
|
|
118
|
+
pendingExport.sessionIds.length === 1
|
|
119
|
+
? await exportKiroSessionFn({
|
|
120
|
+
data: {
|
|
121
|
+
includeCommentary: options.includeCommentary,
|
|
122
|
+
includeMetadata: options.includeMetadata,
|
|
123
|
+
includeTools: options.includeTools,
|
|
124
|
+
outputFormat: options.outputFormat,
|
|
125
|
+
sessionId: pendingExport.sessionIds[0]!,
|
|
126
|
+
zipArchive: options.zipArchive,
|
|
127
|
+
},
|
|
128
|
+
})
|
|
129
|
+
: await exportKiroSessionsFn({
|
|
130
|
+
data: {
|
|
131
|
+
includeCommentary: options.includeCommentary,
|
|
132
|
+
includeMetadata: options.includeMetadata,
|
|
133
|
+
includeTools: options.includeTools,
|
|
134
|
+
outputFormat: options.outputFormat,
|
|
135
|
+
sessionIds: pendingExport.sessionIds,
|
|
136
|
+
zipArchive: options.zipArchive,
|
|
137
|
+
},
|
|
138
|
+
});
|
|
62
139
|
if (download.mode === 'download') {
|
|
63
140
|
downloadTextFile(download.fileName, download.content, download.mimeType);
|
|
64
141
|
return;
|
|
@@ -71,39 +148,104 @@ const KiroWorkspacePage = () => {
|
|
|
71
148
|
},
|
|
72
149
|
});
|
|
73
150
|
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
session.
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
151
|
+
const deleteMutation = useMutation({
|
|
152
|
+
mutationFn: async (sessionIds: string[]) =>
|
|
153
|
+
sessionIds.length === 1
|
|
154
|
+
? deleteKiroSessionFn({ data: { sessionId: sessionIds[0]! } })
|
|
155
|
+
: deleteKiroSessionsFn({ data: { sessionIds } }),
|
|
156
|
+
onSuccess: async (_result, sessionIds) => {
|
|
157
|
+
const workspaceEmptied = isWorkspaceEmptiedByDelete(sessions, sessionIds, (session) => session.sessionId);
|
|
158
|
+
setPendingDelete(null);
|
|
159
|
+
if (workspaceEmptied) {
|
|
160
|
+
await navigate({ to: '/kiro' });
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
await Promise.all([
|
|
164
|
+
queryClient.invalidateQueries({ queryKey: ['kiro-workspaces'] }),
|
|
165
|
+
queryClient.invalidateQueries({ queryKey: ['kiro-sessions', workspace.key] }),
|
|
166
|
+
...sessionIds.map((sessionId) =>
|
|
167
|
+
queryClient.invalidateQueries({ queryKey: ['kiro-session', sessionId] }),
|
|
168
|
+
),
|
|
169
|
+
]);
|
|
170
|
+
},
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
const visibleSessions = useMemo(
|
|
174
|
+
() =>
|
|
175
|
+
sessions.filter((session) =>
|
|
176
|
+
matchesTextQuery(deferredSearch, [
|
|
177
|
+
session.title,
|
|
178
|
+
session.sessionId,
|
|
179
|
+
session.selectedModel,
|
|
180
|
+
session.defaultModelTitle,
|
|
181
|
+
session.selectedProfileId,
|
|
182
|
+
session.sessionType,
|
|
183
|
+
session.filePath,
|
|
184
|
+
]),
|
|
185
|
+
),
|
|
186
|
+
[deferredSearch, sessions],
|
|
84
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 KiroSessionSummary => session !== null);
|
|
196
|
+
const openExportForSessions = (selectedSessions: KiroSessionSummary[]) => {
|
|
197
|
+
if (selectedSessions.length === 0) {
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
setPendingExport(buildSessionExport(selectedSessions));
|
|
202
|
+
};
|
|
203
|
+
const openDeleteForSessions = (selectedSessions: KiroSessionSummary[], scope: PendingSessionDelete['scope']) => {
|
|
204
|
+
if (selectedSessions.length > 0) {
|
|
205
|
+
setPendingDelete({ scope, sessions: selectedSessions });
|
|
206
|
+
}
|
|
207
|
+
};
|
|
85
208
|
|
|
86
209
|
return (
|
|
87
210
|
<div className="space-y-6">
|
|
88
211
|
<PageHeader
|
|
89
212
|
actions={
|
|
90
|
-
<
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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 type"
|
|
226
|
+
value={searchInput}
|
|
227
|
+
onValueChange={setSearchInput}
|
|
228
|
+
/>
|
|
229
|
+
</div>
|
|
95
230
|
}
|
|
96
231
|
eyebrow="Kiro workspace"
|
|
97
232
|
subtitle="Inspect local Kiro sessions, user prompts, assistant responses, images, and prompt logs."
|
|
98
233
|
title={workspace.label}
|
|
99
234
|
/>
|
|
100
235
|
|
|
101
|
-
<KiroSessionsTable
|
|
236
|
+
<KiroSessionsTable
|
|
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
|
+
/>
|
|
102
243
|
|
|
103
244
|
<ExportDialog
|
|
245
|
+
forceZipArchive={pendingExport ? pendingExport.sessionIds.length > 1 : false}
|
|
104
246
|
open={pendingExport !== null}
|
|
105
247
|
pending={exportMutation.isPending}
|
|
106
|
-
title={pendingExport ? `Export ${pendingExport.
|
|
248
|
+
title={pendingExport ? `Export ${pendingExport.label}` : 'Export session'}
|
|
107
249
|
onExport={(options) => exportMutation.mutate(options)}
|
|
108
250
|
onOpenChange={(open) => {
|
|
109
251
|
if (!open) {
|
|
@@ -118,6 +260,31 @@ const KiroWorkspacePage = () => {
|
|
|
118
260
|
{exportMutation.error instanceof Error ? exportMutation.error.message : 'Session export failed'}
|
|
119
261
|
</p>
|
|
120
262
|
) : null}
|
|
263
|
+
|
|
264
|
+
<DeleteConfirmDialog
|
|
265
|
+
confirmLabel={getDeleteConfirmLabel(pendingDelete, deleteMutation.isPending)}
|
|
266
|
+
description={getDeleteDescription(pendingDelete)}
|
|
267
|
+
errorMessage={
|
|
268
|
+
deleteMutation.isError
|
|
269
|
+
? deleteMutation.error instanceof Error
|
|
270
|
+
? deleteMutation.error.message
|
|
271
|
+
: 'Session delete failed'
|
|
272
|
+
: null
|
|
273
|
+
}
|
|
274
|
+
open={pendingDelete !== null}
|
|
275
|
+
title={getDeleteTitle(pendingDelete)}
|
|
276
|
+
onConfirm={() => {
|
|
277
|
+
if (pendingDelete) {
|
|
278
|
+
deleteMutation.mutate(pendingDelete.sessions.map((session) => session.sessionId));
|
|
279
|
+
}
|
|
280
|
+
}}
|
|
281
|
+
onOpenChange={(open) => {
|
|
282
|
+
if (!open) {
|
|
283
|
+
setPendingDelete(null);
|
|
284
|
+
deleteMutation.reset();
|
|
285
|
+
}
|
|
286
|
+
}}
|
|
287
|
+
/>
|
|
121
288
|
</div>
|
|
122
289
|
);
|
|
123
290
|
};
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import type { ThreadEvent, ThreadTranscriptStats } from '@spiracha/lib/codex-browser-types';
|
|
2
2
|
import type { OpenCodeSessionTranscript } from '@spiracha/lib/opencode-exporter-types';
|
|
3
|
-
import { useMutation,
|
|
4
|
-
import { createFileRoute, Link } from '@tanstack/react-router';
|
|
5
|
-
import { Download } from 'lucide-react';
|
|
3
|
+
import { useMutation, useQuery, useQueryClient } 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, formatTokens } from '#/lib/formatters';
|
|
21
22
|
import { openCodeSessionDetailQueryOptions } from '#/lib/opencode-queries';
|
|
22
|
-
import { exportOpenCodeSessionFn } from '#/lib/opencode-server';
|
|
23
|
+
import { deleteOpenCodeSessionFn, exportOpenCodeSessionFn } from '#/lib/opencode-server';
|
|
23
24
|
import { getOpenCodeThreadTranscriptStats, openCodeTranscriptToThreadEvents } from '#/lib/opencode-transcript-events';
|
|
24
25
|
|
|
25
26
|
type ExportDialogOptions = {
|
|
@@ -48,6 +49,8 @@ const OpenCodeSessionDetailErrorComponent = ({ error }: { error: Error }) => {
|
|
|
48
49
|
return <ReloadErrorPanel description={error.message} title="Failed to load OpenCode session" />;
|
|
49
50
|
};
|
|
50
51
|
|
|
52
|
+
const toError = (error: unknown) => (error instanceof Error ? error : new Error(String(error)));
|
|
53
|
+
|
|
51
54
|
const buildSessionMetadata = (detail: OpenCodeSessionTranscript) => [
|
|
52
55
|
{ label: 'Session ID', value: <span data-mono="true">{detail.session.sessionId}</span> },
|
|
53
56
|
{
|
|
@@ -159,18 +162,27 @@ const OpenCodeRawPanels = ({ detail, events }: { detail: OpenCodeSessionTranscri
|
|
|
159
162
|
};
|
|
160
163
|
|
|
161
164
|
const OpenCodeSessionDetailPage = () => {
|
|
162
|
-
const
|
|
165
|
+
const navigate = useNavigate();
|
|
166
|
+
const queryClient = useQueryClient();
|
|
167
|
+
const params = Route.useParams();
|
|
168
|
+
const detailQuery = useQuery(openCodeSessionDetailQueryOptions(params.sessionId));
|
|
169
|
+
const detail = detailQuery.data ?? null;
|
|
170
|
+
const [deleteOpen, setDeleteOpen] = useState(false);
|
|
163
171
|
const [pendingExport, setPendingExport] = useState(false);
|
|
164
172
|
const [showToolCalls, setShowToolCalls] = useState(false);
|
|
165
173
|
const [showCommentary, setShowCommentary] = useState(false);
|
|
166
174
|
const [showExtraEvents, setShowExtraEvents] = useState(false);
|
|
167
175
|
const [showRawJson, setShowRawJson] = useState(false);
|
|
168
|
-
const [showUserMessages, setShowUserMessages] = useState(
|
|
169
|
-
const transcriptEvents = useMemo(() => openCodeTranscriptToThreadEvents(detail), [detail]);
|
|
176
|
+
const [showUserMessages, setShowUserMessages] = useState(DEFAULT_SHOW_USER_MESSAGES);
|
|
177
|
+
const transcriptEvents = useMemo(() => (detail ? openCodeTranscriptToThreadEvents(detail) : []), [detail]);
|
|
170
178
|
const transcriptStats = useMemo(() => getOpenCodeThreadTranscriptStats(transcriptEvents), [transcriptEvents]);
|
|
171
179
|
|
|
172
180
|
const exportSessionMutation = useMutation({
|
|
173
181
|
mutationFn: async (options: ExportDialogOptions) => {
|
|
182
|
+
if (!detail) {
|
|
183
|
+
throw new Error(`OpenCode session not found: ${params.sessionId}`);
|
|
184
|
+
}
|
|
185
|
+
|
|
174
186
|
const download = await exportOpenCodeSessionFn({
|
|
175
187
|
data: {
|
|
176
188
|
includeCommentary: options.includeCommentary,
|
|
@@ -193,19 +205,74 @@ const OpenCodeSessionDetailPage = () => {
|
|
|
193
205
|
},
|
|
194
206
|
});
|
|
195
207
|
|
|
208
|
+
const deleteSessionMutation = useMutation({
|
|
209
|
+
mutationFn: () => {
|
|
210
|
+
if (!detail) {
|
|
211
|
+
throw new Error(`OpenCode session not found: ${params.sessionId}`);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return deleteOpenCodeSessionFn({ data: { sessionId: detail.session.sessionId } });
|
|
215
|
+
},
|
|
216
|
+
onSuccess: async () => {
|
|
217
|
+
if (!detail) {
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
await Promise.all([
|
|
222
|
+
queryClient.invalidateQueries({ queryKey: ['opencode-workspaces'] }),
|
|
223
|
+
queryClient.invalidateQueries({ queryKey: ['opencode-sessions', detail.session.workspaceKey] }),
|
|
224
|
+
queryClient.invalidateQueries({ queryKey: ['opencode-session', detail.session.sessionId] }),
|
|
225
|
+
]);
|
|
226
|
+
navigate({
|
|
227
|
+
params: { workspaceKey: detail.session.workspaceKey },
|
|
228
|
+
to: '/opencode/$workspaceKey',
|
|
229
|
+
});
|
|
230
|
+
},
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
if (detailQuery.isLoading) {
|
|
234
|
+
return (
|
|
235
|
+
<LoadingPanel
|
|
236
|
+
description="Loading the OpenCode transcript, parts, and session metadata."
|
|
237
|
+
title="Loading session"
|
|
238
|
+
/>
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (detailQuery.isError) {
|
|
243
|
+
return <OpenCodeSessionDetailErrorComponent error={toError(detailQuery.error)} />;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (!detail) {
|
|
247
|
+
return (
|
|
248
|
+
<OpenCodeSessionDetailErrorComponent error={new Error(`OpenCode session not found: ${params.sessionId}`)} />
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
|
|
196
252
|
return (
|
|
197
253
|
<div className="space-y-6">
|
|
198
254
|
<PageHeader
|
|
199
255
|
actions={
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
256
|
+
<>
|
|
257
|
+
<Button
|
|
258
|
+
className="rounded-full"
|
|
259
|
+
type="button"
|
|
260
|
+
variant="outline"
|
|
261
|
+
onClick={() => setPendingExport(true)}
|
|
262
|
+
>
|
|
263
|
+
<Download className="mr-2 size-4" />
|
|
264
|
+
Export
|
|
265
|
+
</Button>
|
|
266
|
+
<Button
|
|
267
|
+
className="rounded-full border-[var(--destructive)]/20 text-[var(--destructive)]"
|
|
268
|
+
type="button"
|
|
269
|
+
variant="outline"
|
|
270
|
+
onClick={() => setDeleteOpen(true)}
|
|
271
|
+
>
|
|
272
|
+
<Trash2 className="mr-2 size-4" />
|
|
273
|
+
Delete
|
|
274
|
+
</Button>
|
|
275
|
+
</>
|
|
209
276
|
}
|
|
210
277
|
breadcrumb={
|
|
211
278
|
<Breadcrumbs
|
|
@@ -316,6 +383,27 @@ const OpenCodeSessionDetailPage = () => {
|
|
|
316
383
|
}
|
|
317
384
|
}}
|
|
318
385
|
/>
|
|
386
|
+
|
|
387
|
+
<DeleteConfirmDialog
|
|
388
|
+
confirmLabel={deleteSessionMutation.isPending ? 'Deleting...' : 'Delete session'}
|
|
389
|
+
description="Permanently delete this OpenCode session from the database. This removes the session, child sessions, messages, and parts."
|
|
390
|
+
errorMessage={
|
|
391
|
+
deleteSessionMutation.isError
|
|
392
|
+
? deleteSessionMutation.error instanceof Error
|
|
393
|
+
? deleteSessionMutation.error.message
|
|
394
|
+
: 'Session delete failed'
|
|
395
|
+
: null
|
|
396
|
+
}
|
|
397
|
+
open={deleteOpen}
|
|
398
|
+
title="Delete this OpenCode session?"
|
|
399
|
+
onConfirm={() => deleteSessionMutation.mutate()}
|
|
400
|
+
onOpenChange={(open) => {
|
|
401
|
+
setDeleteOpen(open);
|
|
402
|
+
if (!open) {
|
|
403
|
+
deleteSessionMutation.reset();
|
|
404
|
+
}
|
|
405
|
+
}}
|
|
406
|
+
/>
|
|
319
407
|
</div>
|
|
320
408
|
);
|
|
321
409
|
};
|
|
@@ -323,8 +411,6 @@ const OpenCodeSessionDetailPage = () => {
|
|
|
323
411
|
export const Route = createFileRoute('/opencode-sessions/$sessionId')({
|
|
324
412
|
component: OpenCodeSessionDetailPage,
|
|
325
413
|
errorComponent: OpenCodeSessionDetailErrorComponent,
|
|
326
|
-
loader: ({ context, params }) =>
|
|
327
|
-
context.queryClient.ensureQueryData(openCodeSessionDetailQueryOptions(params.sessionId)),
|
|
328
414
|
pendingComponent: () => (
|
|
329
415
|
<LoadingPanel
|
|
330
416
|
description="Loading the OpenCode transcript, parts, and session metadata."
|