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 { OpenCodeSessionSummary, OpenCodeWorkspaceGroup } from '@spiracha/lib/opencode-exporter-types';
|
|
2
|
-
import { useMutation,
|
|
3
|
-
import { createFileRoute } from '@tanstack/react-router';
|
|
4
|
-
import {
|
|
2
|
+
import { useMutation, useQuery, useQueryClient } 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 { ListSearchInput } from '#/components/list-search-input';
|
|
7
9
|
import { LoadingPanel } from '#/components/loading-panel';
|
|
8
10
|
import { OpenCodeSessionsTable } from '#/components/opencode-sessions-table';
|
|
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 { openCodeSessionsQueryOptions, openCodeWorkspacesQueryOptions } from '#/lib/opencode-queries';
|
|
13
|
-
import {
|
|
16
|
+
import {
|
|
17
|
+
deleteOpenCodeSessionFn,
|
|
18
|
+
deleteOpenCodeSessionsFn,
|
|
19
|
+
exportOpenCodeSessionFn,
|
|
20
|
+
exportOpenCodeSessionsFn,
|
|
21
|
+
} from '#/lib/opencode-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,23 +30,62 @@ type ExportDialogOptions = {
|
|
|
21
30
|
zipArchive: boolean;
|
|
22
31
|
};
|
|
23
32
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
33
|
+
type PendingSessionDelete = {
|
|
34
|
+
scope: 'all' | 'selected';
|
|
35
|
+
sessions: OpenCodeSessionSummary[];
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
type PendingSessionExport = {
|
|
39
|
+
label: string;
|
|
40
|
+
sessionIds: string[];
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const buildSessionExport = (selectedSessions: OpenCodeSessionSummary[]): PendingSessionExport => ({
|
|
44
|
+
label: selectedSessions.length === 1 ? selectedSessions[0]!.title : `${selectedSessions.length} selected sessions`,
|
|
45
|
+
sessionIds: selectedSessions.map((session) => session.sessionId),
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const getDeleteConfirmLabel = (pendingDelete: PendingSessionDelete | null, isPending: boolean) => {
|
|
49
|
+
if (isPending) {
|
|
50
|
+
return 'Deleting...';
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (pendingDelete?.scope === 'all') {
|
|
54
|
+
return 'Delete all';
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return pendingDelete && pendingDelete.sessions.length > 1 ? 'Delete sessions' : 'Delete session';
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const getDeleteDescription = (pendingDelete: PendingSessionDelete | null) => {
|
|
61
|
+
if (!pendingDelete) {
|
|
62
|
+
return 'Permanently delete the selected OpenCode sessions from the database.';
|
|
28
63
|
}
|
|
29
64
|
|
|
30
|
-
|
|
65
|
+
if (pendingDelete.scope === 'all') {
|
|
66
|
+
return `Permanently delete all ${pendingDelete.sessions.length} OpenCode sessions in this workspace from the database, including child sessions, messages, and parts.`;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (pendingDelete.sessions.length === 1) {
|
|
70
|
+
return `Permanently delete "${pendingDelete.sessions[0]!.title}" from OpenCode history. This removes the session, child sessions, messages, and parts from the OpenCode database.`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return `Permanently delete ${pendingDelete.sessions.length} selected OpenCode sessions from the database, including child sessions, messages, and parts.`;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const getDeleteTitle = (pendingDelete: PendingSessionDelete | null) => {
|
|
77
|
+
if (pendingDelete?.scope === 'all') {
|
|
78
|
+
return `Delete all ${pendingDelete.sessions.length} OpenCode sessions?`;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return pendingDelete && pendingDelete.sessions.length > 1
|
|
82
|
+
? `Delete ${pendingDelete.sessions.length} OpenCode sessions?`
|
|
83
|
+
: 'Delete this OpenCode session?';
|
|
31
84
|
};
|
|
32
85
|
|
|
33
86
|
export const Route = createFileRoute('/opencode/$workspaceKey')({
|
|
34
87
|
component: OpenCodeWorkspacePage,
|
|
35
88
|
errorComponent: OpenCodeWorkspaceErrorComponent,
|
|
36
|
-
loader: async ({ context, params }) => {
|
|
37
|
-
const workspaces = await context.queryClient.ensureQueryData(openCodeWorkspacesQueryOptions());
|
|
38
|
-
findWorkspaceOrThrow(workspaces, params.workspaceKey);
|
|
39
|
-
await context.queryClient.ensureQueryData(openCodeSessionsQueryOptions(params.workspaceKey));
|
|
40
|
-
},
|
|
41
89
|
pendingComponent: () => (
|
|
42
90
|
<LoadingPanel description="Loading OpenCode sessions and transcript metadata." title="Loading workspace" />
|
|
43
91
|
),
|
|
@@ -47,13 +95,52 @@ function OpenCodeWorkspaceErrorComponent({ error }: { error: Error }) {
|
|
|
47
95
|
return <ReloadErrorPanel description={error.message} title="Failed to load OpenCode workspace" />;
|
|
48
96
|
}
|
|
49
97
|
|
|
98
|
+
const toError = (error: unknown) => (error instanceof Error ? error : new Error(String(error)));
|
|
99
|
+
|
|
50
100
|
function OpenCodeWorkspacePage() {
|
|
51
101
|
const params = Route.useParams();
|
|
52
|
-
const
|
|
53
|
-
const
|
|
54
|
-
const
|
|
102
|
+
const workspacesQuery = useQuery(openCodeWorkspacesQueryOptions());
|
|
103
|
+
const workspaces = workspacesQuery.data ?? [];
|
|
104
|
+
const workspace = workspaces.find((candidate) => candidate.key === params.workspaceKey) ?? null;
|
|
105
|
+
const sessionsQuery = useQuery(openCodeSessionsQueryOptions(workspace?.key ?? null));
|
|
106
|
+
|
|
107
|
+
if (workspacesQuery.isLoading || (workspace && sessionsQuery.isLoading)) {
|
|
108
|
+
return (
|
|
109
|
+
<LoadingPanel description="Loading OpenCode sessions and transcript metadata." title="Loading workspace" />
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (workspacesQuery.isError) {
|
|
114
|
+
return <OpenCodeWorkspaceErrorComponent error={toError(workspacesQuery.error)} />;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (!workspace) {
|
|
118
|
+
return (
|
|
119
|
+
<OpenCodeWorkspaceErrorComponent
|
|
120
|
+
error={new Error(`OpenCode workspace not found: ${params.workspaceKey}`)}
|
|
121
|
+
/>
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (sessionsQuery.isError) {
|
|
126
|
+
return <OpenCodeWorkspaceErrorComponent error={toError(sessionsQuery.error)} />;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return <OpenCodeWorkspaceContent sessions={sessionsQuery.data ?? []} workspace={workspace} />;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function OpenCodeWorkspaceContent({
|
|
133
|
+
sessions,
|
|
134
|
+
workspace,
|
|
135
|
+
}: {
|
|
136
|
+
sessions: OpenCodeSessionSummary[];
|
|
137
|
+
workspace: OpenCodeWorkspaceGroup;
|
|
138
|
+
}) {
|
|
139
|
+
const navigate = useNavigate({ from: Route.fullPath });
|
|
140
|
+
const queryClient = useQueryClient();
|
|
55
141
|
const [searchInput, setSearchInput] = useState('');
|
|
56
|
-
const [
|
|
142
|
+
const [pendingDelete, setPendingDelete] = useState<PendingSessionDelete | null>(null);
|
|
143
|
+
const [pendingExport, setPendingExport] = useState<PendingSessionExport | null>(null);
|
|
57
144
|
const deferredSearch = useDeferredValue(searchInput);
|
|
58
145
|
|
|
59
146
|
const exportMutation = useMutation({
|
|
@@ -62,16 +149,28 @@ function OpenCodeWorkspacePage() {
|
|
|
62
149
|
throw new Error('No OpenCode session selected for export');
|
|
63
150
|
}
|
|
64
151
|
|
|
65
|
-
const download =
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
152
|
+
const download =
|
|
153
|
+
pendingExport.sessionIds.length === 1
|
|
154
|
+
? await exportOpenCodeSessionFn({
|
|
155
|
+
data: {
|
|
156
|
+
includeCommentary: options.includeCommentary,
|
|
157
|
+
includeMetadata: options.includeMetadata,
|
|
158
|
+
includeTools: options.includeTools,
|
|
159
|
+
outputFormat: options.outputFormat,
|
|
160
|
+
sessionId: pendingExport.sessionIds[0]!,
|
|
161
|
+
zipArchive: options.zipArchive,
|
|
162
|
+
},
|
|
163
|
+
})
|
|
164
|
+
: await exportOpenCodeSessionsFn({
|
|
165
|
+
data: {
|
|
166
|
+
includeCommentary: options.includeCommentary,
|
|
167
|
+
includeMetadata: options.includeMetadata,
|
|
168
|
+
includeTools: options.includeTools,
|
|
169
|
+
outputFormat: options.outputFormat,
|
|
170
|
+
sessionIds: pendingExport.sessionIds,
|
|
171
|
+
zipArchive: options.zipArchive,
|
|
172
|
+
},
|
|
173
|
+
});
|
|
75
174
|
if (download.mode === 'download') {
|
|
76
175
|
downloadTextFile(download.fileName, download.content, download.mimeType);
|
|
77
176
|
return;
|
|
@@ -84,38 +183,106 @@ function OpenCodeWorkspacePage() {
|
|
|
84
183
|
},
|
|
85
184
|
});
|
|
86
185
|
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
session.
|
|
94
|
-
|
|
95
|
-
|
|
186
|
+
const deleteMutation = useMutation({
|
|
187
|
+
mutationFn: async (sessionIds: string[]) =>
|
|
188
|
+
sessionIds.length === 1
|
|
189
|
+
? deleteOpenCodeSessionFn({ data: { sessionId: sessionIds[0]! } })
|
|
190
|
+
: deleteOpenCodeSessionsFn({ data: { sessionIds } }),
|
|
191
|
+
onSuccess: async (_result, sessionIds) => {
|
|
192
|
+
const workspaceEmptied = isWorkspaceEmptiedByDelete(sessions, sessionIds, (session) => session.sessionId);
|
|
193
|
+
setPendingDelete(null);
|
|
194
|
+
if (workspaceEmptied) {
|
|
195
|
+
await navigate({ to: '/opencode' });
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
await Promise.all([
|
|
199
|
+
queryClient.invalidateQueries({ queryKey: ['opencode-workspaces'] }),
|
|
200
|
+
queryClient.invalidateQueries({ queryKey: ['opencode-sessions', workspace.key] }),
|
|
201
|
+
...sessionIds.map((sessionId) =>
|
|
202
|
+
queryClient.invalidateQueries({ queryKey: ['opencode-session', sessionId] }),
|
|
203
|
+
),
|
|
204
|
+
]);
|
|
205
|
+
},
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
const visibleSessions = useMemo(
|
|
209
|
+
() =>
|
|
210
|
+
sessions.filter((session) =>
|
|
211
|
+
matchesTextQuery(deferredSearch, [
|
|
212
|
+
session.title,
|
|
213
|
+
session.sessionId,
|
|
214
|
+
session.slug,
|
|
215
|
+
session.agent,
|
|
216
|
+
session.modelLabel,
|
|
217
|
+
session.directory,
|
|
218
|
+
]),
|
|
219
|
+
),
|
|
220
|
+
[deferredSearch, sessions],
|
|
221
|
+
);
|
|
222
|
+
const visibleSessionsById = useMemo(
|
|
223
|
+
() => new Map(visibleSessions.map((session) => [session.sessionId, session])),
|
|
224
|
+
[visibleSessions],
|
|
96
225
|
);
|
|
226
|
+
const lookupSelectedSessions = (sessionIds: string[]) =>
|
|
227
|
+
sessionIds
|
|
228
|
+
.map((sessionId) => visibleSessionsById.get(sessionId) ?? null)
|
|
229
|
+
.filter((session): session is OpenCodeSessionSummary => session !== null);
|
|
230
|
+
const openExportForSessions = (selectedSessions: OpenCodeSessionSummary[]) => {
|
|
231
|
+
if (selectedSessions.length === 0) {
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
setPendingExport(buildSessionExport(selectedSessions));
|
|
236
|
+
};
|
|
237
|
+
const openDeleteForSessions = (
|
|
238
|
+
selectedSessions: OpenCodeSessionSummary[],
|
|
239
|
+
scope: PendingSessionDelete['scope'],
|
|
240
|
+
) => {
|
|
241
|
+
if (selectedSessions.length > 0) {
|
|
242
|
+
setPendingDelete({ scope, sessions: selectedSessions });
|
|
243
|
+
}
|
|
244
|
+
};
|
|
97
245
|
|
|
98
246
|
return (
|
|
99
247
|
<div className="space-y-6">
|
|
100
248
|
<PageHeader
|
|
101
249
|
actions={
|
|
102
|
-
<
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
250
|
+
<div className="flex flex-col gap-2 sm:flex-row">
|
|
251
|
+
<Button
|
|
252
|
+
className="rounded-full"
|
|
253
|
+
disabled={deleteMutation.isPending || sessions.length === 0}
|
|
254
|
+
type="button"
|
|
255
|
+
variant="destructive"
|
|
256
|
+
onClick={() => openDeleteForSessions(sessions, 'all')}
|
|
257
|
+
>
|
|
258
|
+
<Trash2 className="size-4" />
|
|
259
|
+
Delete all
|
|
260
|
+
</Button>
|
|
261
|
+
<ListSearchInput
|
|
262
|
+
placeholder="Search session title, id, model, or agent"
|
|
263
|
+
value={searchInput}
|
|
264
|
+
onValueChange={setSearchInput}
|
|
265
|
+
/>
|
|
266
|
+
</div>
|
|
107
267
|
}
|
|
108
268
|
eyebrow="OpenCode workspace"
|
|
109
269
|
subtitle="Inspect local OpenCode sessions, transcript parts, tool calls, reasoning, token totals, and exportable conversation text."
|
|
110
270
|
title={workspace.label}
|
|
111
271
|
/>
|
|
112
272
|
|
|
113
|
-
<OpenCodeSessionsTable
|
|
273
|
+
<OpenCodeSessionsTable
|
|
274
|
+
sessions={visibleSessions}
|
|
275
|
+
onDeleteSession={(session) => openDeleteForSessions([session], 'selected')}
|
|
276
|
+
onDeleteSessions={(sessionIds) => openDeleteForSessions(lookupSelectedSessions(sessionIds), 'selected')}
|
|
277
|
+
onExportSession={(session) => openExportForSessions([session])}
|
|
278
|
+
onExportSessions={(sessionIds) => openExportForSessions(lookupSelectedSessions(sessionIds))}
|
|
279
|
+
/>
|
|
114
280
|
|
|
115
281
|
<ExportDialog
|
|
282
|
+
forceZipArchive={pendingExport ? pendingExport.sessionIds.length > 1 : false}
|
|
116
283
|
open={pendingExport !== null}
|
|
117
284
|
pending={exportMutation.isPending}
|
|
118
|
-
title={pendingExport ? `Export ${pendingExport.
|
|
285
|
+
title={pendingExport ? `Export ${pendingExport.label}` : 'Export session'}
|
|
119
286
|
onExport={(options) => exportMutation.mutate(options)}
|
|
120
287
|
onOpenChange={(open) => {
|
|
121
288
|
if (!open) {
|
|
@@ -130,6 +297,31 @@ function OpenCodeWorkspacePage() {
|
|
|
130
297
|
{exportMutation.error instanceof Error ? exportMutation.error.message : 'Session export failed'}
|
|
131
298
|
</p>
|
|
132
299
|
) : null}
|
|
300
|
+
|
|
301
|
+
<DeleteConfirmDialog
|
|
302
|
+
confirmLabel={getDeleteConfirmLabel(pendingDelete, deleteMutation.isPending)}
|
|
303
|
+
description={getDeleteDescription(pendingDelete)}
|
|
304
|
+
errorMessage={
|
|
305
|
+
deleteMutation.isError
|
|
306
|
+
? deleteMutation.error instanceof Error
|
|
307
|
+
? deleteMutation.error.message
|
|
308
|
+
: 'Session delete failed'
|
|
309
|
+
: null
|
|
310
|
+
}
|
|
311
|
+
open={pendingDelete !== null}
|
|
312
|
+
title={getDeleteTitle(pendingDelete)}
|
|
313
|
+
onConfirm={() => {
|
|
314
|
+
if (pendingDelete) {
|
|
315
|
+
deleteMutation.mutate(pendingDelete.sessions.map((session) => session.sessionId));
|
|
316
|
+
}
|
|
317
|
+
}}
|
|
318
|
+
onOpenChange={(open) => {
|
|
319
|
+
if (!open) {
|
|
320
|
+
setPendingDelete(null);
|
|
321
|
+
deleteMutation.reset();
|
|
322
|
+
}
|
|
323
|
+
}}
|
|
324
|
+
/>
|
|
133
325
|
</div>
|
|
134
326
|
);
|
|
135
327
|
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { useQuery } from '@tanstack/react-query';
|
|
2
2
|
import { createFileRoute } from '@tanstack/react-router';
|
|
3
3
|
import { useDeferredValue, useState } from 'react';
|
|
4
4
|
import { ListSearchInput } from '#/components/list-search-input';
|
|
5
|
+
import { LoadingPanel } from '#/components/loading-panel';
|
|
5
6
|
import { OpenCodeWorkspacesTable } from '#/components/opencode-workspaces-table';
|
|
6
7
|
import { PageHeader } from '#/components/page-header';
|
|
7
8
|
import { ReloadErrorPanel } from '#/components/reload-error-panel';
|
|
@@ -12,15 +13,28 @@ const OpenCodeErrorComponent = ({ error }: { error: Error }) => {
|
|
|
12
13
|
return <ReloadErrorPanel description={error.message} title="Failed to load OpenCode workspaces" />;
|
|
13
14
|
};
|
|
14
15
|
|
|
16
|
+
const toError = (error: unknown) => (error instanceof Error ? error : new Error(String(error)));
|
|
17
|
+
|
|
15
18
|
const OpenCodePage = () => {
|
|
16
|
-
const
|
|
19
|
+
const workspacesQuery = useQuery(openCodeWorkspacesQueryOptions());
|
|
17
20
|
const [searchInput, setSearchInput] = useState('');
|
|
18
21
|
const deferredSearch = useDeferredValue(searchInput);
|
|
22
|
+
const workspaces = workspacesQuery.data ?? [];
|
|
19
23
|
|
|
20
24
|
const visibleWorkspaces = workspaces.filter((workspace) =>
|
|
21
25
|
matchesTextQuery(deferredSearch, [workspace.label, workspace.worktree, workspace.key, workspace.projectId]),
|
|
22
26
|
);
|
|
23
27
|
|
|
28
|
+
if (workspacesQuery.isLoading) {
|
|
29
|
+
return (
|
|
30
|
+
<LoadingPanel description="Loading OpenCode workspaces and database metadata." title="Loading OpenCode" />
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (workspacesQuery.isError) {
|
|
35
|
+
return <OpenCodeErrorComponent error={toError(workspacesQuery.error)} />;
|
|
36
|
+
}
|
|
37
|
+
|
|
24
38
|
return (
|
|
25
39
|
<div className="space-y-6">
|
|
26
40
|
<PageHeader
|
|
@@ -44,5 +58,4 @@ const OpenCodePage = () => {
|
|
|
44
58
|
export const Route = createFileRoute('/opencode/')({
|
|
45
59
|
component: OpenCodePage,
|
|
46
60
|
errorComponent: OpenCodeErrorComponent,
|
|
47
|
-
loader: ({ context }) => context.queryClient.ensureQueryData(openCodeWorkspacesQueryOptions()),
|
|
48
61
|
});
|
|
@@ -12,7 +12,7 @@ import { MetadataSection } from '#/components/metadata-section';
|
|
|
12
12
|
import { MetricCard } from '#/components/metric-card';
|
|
13
13
|
import { PageHeader } from '#/components/page-header';
|
|
14
14
|
import { ReloadErrorPanel } from '#/components/reload-error-panel';
|
|
15
|
-
import { TranscriptView } from '#/components/transcript-view';
|
|
15
|
+
import { DEFAULT_SHOW_USER_MESSAGES, TranscriptView } from '#/components/transcript-view';
|
|
16
16
|
import { Button } from '#/components/ui/button';
|
|
17
17
|
import { Checkbox } from '#/components/ui/checkbox';
|
|
18
18
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '#/components/ui/tabs';
|
|
@@ -71,10 +71,13 @@ const buildSessionMetadata = (detail: QoderSessionTranscript) => [
|
|
|
71
71
|
{ label: 'Model', value: detail.session.model ?? 'unknown' },
|
|
72
72
|
{ label: 'Execution mode', value: detail.session.executionMode ?? 'unknown' },
|
|
73
73
|
{ label: 'Agent class', value: detail.session.agentClass ?? 'unknown' },
|
|
74
|
-
{
|
|
74
|
+
{
|
|
75
|
+
label: 'Created',
|
|
76
|
+
value: <span suppressHydrationWarning>{formatDateTime(detail.session.createdAtMs)}</span>,
|
|
77
|
+
},
|
|
75
78
|
{
|
|
76
79
|
label: 'Last active',
|
|
77
|
-
value: formatDateTime(detail.session.lastActiveAtMs)
|
|
80
|
+
value: <span suppressHydrationWarning>{formatDateTime(detail.session.lastActiveAtMs)}</span>,
|
|
78
81
|
},
|
|
79
82
|
];
|
|
80
83
|
|
|
@@ -171,7 +174,7 @@ const QoderSessionDetailPage = () => {
|
|
|
171
174
|
const [showCommentary, setShowCommentary] = useState(false);
|
|
172
175
|
const [showExtraEvents, setShowExtraEvents] = useState(false);
|
|
173
176
|
const [showRawJson, setShowRawJson] = useState(false);
|
|
174
|
-
const [showUserMessages, setShowUserMessages] = useState(
|
|
177
|
+
const [showUserMessages, setShowUserMessages] = useState(DEFAULT_SHOW_USER_MESSAGES);
|
|
175
178
|
const transcriptEvents = useMemo(() => qoderTranscriptToThreadEvents(detail), [detail]);
|
|
176
179
|
const transcriptStats = useMemo(() => getQoderThreadTranscriptStats(transcriptEvents), [transcriptEvents]);
|
|
177
180
|
const modelLabel = detail.session.model ?? 'Qoder';
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { QoderSessionSummary, QoderWorkspaceGroup } from '@spiracha/lib/qoder-exporter-types';
|
|
2
2
|
import { useMutation, useSuspenseQuery } from '@tanstack/react-query';
|
|
3
3
|
import { createFileRoute } from '@tanstack/react-router';
|
|
4
|
-
import { useDeferredValue, useState } from 'react';
|
|
4
|
+
import { useDeferredValue, useMemo, useState } from 'react';
|
|
5
5
|
import { ExportDialog } from '#/components/export-dialog';
|
|
6
6
|
import { ListSearchInput } from '#/components/list-search-input';
|
|
7
7
|
import { LoadingPanel } from '#/components/loading-panel';
|
|
@@ -10,7 +10,7 @@ import { QoderSessionsTable } from '#/components/qoder-sessions-table';
|
|
|
10
10
|
import { ReloadErrorPanel } from '#/components/reload-error-panel';
|
|
11
11
|
import { downloadTextFile, downloadUrlFile } from '#/lib/download';
|
|
12
12
|
import { qoderSessionsQueryOptions, qoderWorkspacesQueryOptions } from '#/lib/qoder-queries';
|
|
13
|
-
import { exportQoderSessionFn } from '#/lib/qoder-server';
|
|
13
|
+
import { exportQoderSessionFn, exportQoderSessionsFn } from '#/lib/qoder-server';
|
|
14
14
|
import { matchesTextQuery } from '#/lib/text-filter';
|
|
15
15
|
|
|
16
16
|
type ExportDialogOptions = {
|
|
@@ -21,6 +21,11 @@ type ExportDialogOptions = {
|
|
|
21
21
|
zipArchive: boolean;
|
|
22
22
|
};
|
|
23
23
|
|
|
24
|
+
type PendingSessionExport = {
|
|
25
|
+
label: string;
|
|
26
|
+
sessionIds: string[];
|
|
27
|
+
};
|
|
28
|
+
|
|
24
29
|
const findWorkspaceOrThrow = (workspaces: QoderWorkspaceGroup[], workspaceKey: string) => {
|
|
25
30
|
const workspace = workspaces.find((candidate) => candidate.key === workspaceKey);
|
|
26
31
|
if (!workspace) {
|
|
@@ -40,7 +45,7 @@ const QoderWorkspacePage = () => {
|
|
|
40
45
|
const workspace = findWorkspaceOrThrow(workspaces, params.workspaceKey);
|
|
41
46
|
const sessions = useSuspenseQuery(qoderSessionsQueryOptions(workspace.key)).data;
|
|
42
47
|
const [searchInput, setSearchInput] = useState('');
|
|
43
|
-
const [pendingExport, setPendingExport] = useState<
|
|
48
|
+
const [pendingExport, setPendingExport] = useState<PendingSessionExport | null>(null);
|
|
44
49
|
const deferredSearch = useDeferredValue(searchInput);
|
|
45
50
|
|
|
46
51
|
const exportMutation = useMutation({
|
|
@@ -49,16 +54,28 @@ const QoderWorkspacePage = () => {
|
|
|
49
54
|
throw new Error('No Qoder session selected for export');
|
|
50
55
|
}
|
|
51
56
|
|
|
52
|
-
const download =
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
57
|
+
const download =
|
|
58
|
+
pendingExport.sessionIds.length === 1
|
|
59
|
+
? await exportQoderSessionFn({
|
|
60
|
+
data: {
|
|
61
|
+
includeCommentary: options.includeCommentary,
|
|
62
|
+
includeMetadata: options.includeMetadata,
|
|
63
|
+
includeTools: options.includeTools,
|
|
64
|
+
outputFormat: options.outputFormat,
|
|
65
|
+
sessionId: pendingExport.sessionIds[0]!,
|
|
66
|
+
zipArchive: options.zipArchive,
|
|
67
|
+
},
|
|
68
|
+
})
|
|
69
|
+
: await exportQoderSessionsFn({
|
|
70
|
+
data: {
|
|
71
|
+
includeCommentary: options.includeCommentary,
|
|
72
|
+
includeMetadata: options.includeMetadata,
|
|
73
|
+
includeTools: options.includeTools,
|
|
74
|
+
outputFormat: options.outputFormat,
|
|
75
|
+
sessionIds: pendingExport.sessionIds,
|
|
76
|
+
zipArchive: options.zipArchive,
|
|
77
|
+
},
|
|
78
|
+
});
|
|
62
79
|
if (download.mode === 'download') {
|
|
63
80
|
downloadTextFile(download.fileName, download.content, download.mimeType);
|
|
64
81
|
return;
|
|
@@ -71,20 +88,45 @@ const QoderWorkspacePage = () => {
|
|
|
71
88
|
},
|
|
72
89
|
});
|
|
73
90
|
|
|
74
|
-
const visibleSessions =
|
|
75
|
-
|
|
76
|
-
session
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
91
|
+
const visibleSessions = useMemo(
|
|
92
|
+
() =>
|
|
93
|
+
sessions.filter((session) =>
|
|
94
|
+
matchesTextQuery(deferredSearch, [
|
|
95
|
+
session.title,
|
|
96
|
+
session.sessionId,
|
|
97
|
+
session.taskId,
|
|
98
|
+
session.requestId,
|
|
99
|
+
session.model,
|
|
100
|
+
session.status,
|
|
101
|
+
session.executionMode,
|
|
102
|
+
session.agentClass,
|
|
103
|
+
session.query,
|
|
104
|
+
session.sourceStatePath,
|
|
105
|
+
]),
|
|
106
|
+
),
|
|
107
|
+
[deferredSearch, sessions],
|
|
108
|
+
);
|
|
109
|
+
const visibleSessionsById = useMemo(
|
|
110
|
+
() => new Map(visibleSessions.map((session) => [session.sessionId, session])),
|
|
111
|
+
[visibleSessions],
|
|
87
112
|
);
|
|
113
|
+
const lookupSelectedSessions = (sessionIds: string[]) =>
|
|
114
|
+
sessionIds
|
|
115
|
+
.map((sessionId) => visibleSessionsById.get(sessionId) ?? null)
|
|
116
|
+
.filter((session): session is QoderSessionSummary => session !== null);
|
|
117
|
+
const openExportForSessions = (selectedSessions: QoderSessionSummary[]) => {
|
|
118
|
+
if (selectedSessions.length === 0) {
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
setPendingExport({
|
|
123
|
+
label:
|
|
124
|
+
selectedSessions.length === 1
|
|
125
|
+
? selectedSessions[0]!.title
|
|
126
|
+
: `${selectedSessions.length} selected sessions`,
|
|
127
|
+
sessionIds: selectedSessions.map((session) => session.sessionId),
|
|
128
|
+
});
|
|
129
|
+
};
|
|
88
130
|
|
|
89
131
|
return (
|
|
90
132
|
<div className="space-y-6">
|
|
@@ -101,12 +143,17 @@ const QoderWorkspacePage = () => {
|
|
|
101
143
|
title={workspace.label}
|
|
102
144
|
/>
|
|
103
145
|
|
|
104
|
-
<QoderSessionsTable
|
|
146
|
+
<QoderSessionsTable
|
|
147
|
+
sessions={visibleSessions}
|
|
148
|
+
onExportSession={(session) => openExportForSessions([session])}
|
|
149
|
+
onExportSessions={(sessionIds) => openExportForSessions(lookupSelectedSessions(sessionIds))}
|
|
150
|
+
/>
|
|
105
151
|
|
|
106
152
|
<ExportDialog
|
|
153
|
+
forceZipArchive={pendingExport ? pendingExport.sessionIds.length > 1 : false}
|
|
107
154
|
open={pendingExport !== null}
|
|
108
155
|
pending={exportMutation.isPending}
|
|
109
|
-
title={pendingExport ? `Export ${pendingExport.
|
|
156
|
+
title={pendingExport ? `Export ${pendingExport.label}` : 'Export session'}
|
|
110
157
|
onExport={(options) => exportMutation.mutate(options)}
|
|
111
158
|
onOpenChange={(open) => {
|
|
112
159
|
if (!open) {
|