spiracha 2.0.0 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +27 -12
- package/README.md +42 -19
- package/apps/ui/AGENTS.md +18 -10
- package/apps/ui/README.md +36 -11
- package/apps/ui/src/components/analytics-breakdowns.tsx +106 -0
- package/apps/ui/src/components/antigravity-conversations-table.tsx +51 -11
- package/apps/ui/src/components/antigravity-workspaces-table.tsx +1 -1
- package/apps/ui/src/components/app-shell.tsx +46 -3
- 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 +23 -5
- package/apps/ui/src/components/delete-confirm-dialog.tsx +4 -3
- package/apps/ui/src/components/export-dialog.tsx +79 -74
- 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/projects-table.tsx +4 -1
- 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/route-error-panel.tsx +13 -0
- package/apps/ui/src/components/selection-actions-toolbar.tsx +80 -0
- package/apps/ui/src/components/thread-goals-panel.tsx +50 -0
- package/apps/ui/src/components/thread-tools-panel.tsx +142 -0
- package/apps/ui/src/components/threads-table.tsx +13 -47
- 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 +209 -11
- package/apps/ui/src/lib/antigravity-transcript-events.ts +57 -73
- package/apps/ui/src/lib/claude-code-queries.ts +8 -0
- package/apps/ui/src/lib/claude-code-server.ts +185 -7
- package/apps/ui/src/lib/claude-code-transcript-events.ts +8 -1
- package/apps/ui/src/lib/codex-queries.ts +16 -0
- package/apps/ui/src/lib/codex-server.ts +134 -52
- package/apps/ui/src/lib/cursor-server.ts +68 -53
- package/apps/ui/src/lib/cursor-transcript-events.ts +11 -86
- package/apps/ui/src/lib/delete-batch.ts +52 -0
- package/apps/ui/src/lib/error-presentation.ts +43 -0
- package/apps/ui/src/lib/export-mutation.ts +20 -0
- package/apps/ui/src/lib/export-options.ts +15 -0
- 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 +168 -0
- package/apps/ui/src/lib/grok-transcript-events.ts +149 -0
- package/apps/ui/src/lib/kiro-server.ts +97 -7
- package/apps/ui/src/lib/mutation-error.ts +15 -0
- package/apps/ui/src/lib/opencode-server.ts +96 -7
- package/apps/ui/src/lib/path-utils.ts +1 -1
- package/apps/ui/src/lib/qoder-queries.ts +15 -0
- package/apps/ui/src/lib/qoder-server.ts +67 -11
- package/apps/ui/src/lib/qoder-transcript-events.ts +1 -1
- package/apps/ui/src/lib/route-search.ts +114 -0
- package/apps/ui/src/lib/route-state-reset.tsx +10 -0
- package/apps/ui/src/lib/settings-server.ts +33 -0
- package/apps/ui/src/lib/settings-store.tsx +82 -38
- package/apps/ui/src/lib/settings.ts +65 -0
- package/apps/ui/src/lib/source-session-export-server.ts +86 -3
- package/apps/ui/src/lib/thread-metadata.ts +22 -0
- package/apps/ui/src/lib/thread-transcript-load.ts +15 -0
- package/apps/ui/src/lib/thread-transcript-stats.ts +10 -1
- package/apps/ui/src/lib/workspace-delete-navigation.ts +20 -0
- package/apps/ui/src/routeTree.gen.ts +341 -234
- package/apps/ui/src/routes/__root.tsx +12 -15
- package/apps/ui/src/routes/analytics.tsx +10 -61
- package/apps/ui/src/routes/antigravity-conversations.$conversationId.tsx +149 -29
- package/apps/ui/src/routes/antigravity.$workspaceKey.tsx +262 -42
- package/apps/ui/src/routes/antigravity.index.tsx +2 -2
- 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 +168 -28
- package/apps/ui/src/routes/claude-code.$workspaceKey.tsx +179 -45
- package/apps/ui/src/routes/claude-code.index.tsx +2 -2
- package/apps/ui/src/routes/codex.$project.tsx +33 -31
- package/apps/ui/src/routes/codex.index.tsx +5 -10
- package/apps/ui/src/routes/cursor-threads.$composerId.tsx +31 -18
- package/apps/ui/src/routes/cursor.$workspaceKey.tsx +44 -28
- package/apps/ui/src/routes/cursor.index.tsx +5 -2
- package/apps/ui/src/routes/grok-sessions.$sessionId.tsx +401 -0
- package/apps/ui/src/routes/grok.$workspaceKey.tsx +295 -0
- package/apps/ui/src/routes/grok.index.tsx +48 -0
- package/apps/ui/src/routes/index.tsx +2 -18
- package/apps/ui/src/routes/kiro-sessions.$sessionId.tsx +91 -27
- package/apps/ui/src/routes/kiro.$workspaceKey.tsx +213 -51
- package/apps/ui/src/routes/kiro.index.tsx +2 -2
- package/apps/ui/src/routes/opencode-sessions.$sessionId.tsx +108 -29
- package/apps/ui/src/routes/opencode.$workspaceKey.tsx +231 -55
- package/apps/ui/src/routes/opencode.index.tsx +6 -2
- package/apps/ui/src/routes/qoder-sessions.$sessionId.tsx +19 -15
- package/apps/ui/src/routes/qoder.$workspaceKey.tsx +89 -48
- package/apps/ui/src/routes/qoder.index.tsx +2 -2
- package/apps/ui/src/routes/threads.$threadId.tsx +620 -105
- package/apps/ui/vite.config.ts +22 -12
- package/bin/spiracha.ts +28 -1
- package/package.json +37 -27
- package/src/client.ts +183 -5
- package/src/lib/antigravity-db.ts +424 -94
- package/src/lib/antigravity-exporter-types.ts +3 -0
- package/src/lib/antigravity-keychain.ts +7 -8
- package/src/lib/antigravity-projects.ts +201 -0
- package/src/lib/antigravity-transcript-phase.ts +47 -0
- package/src/lib/claude-code-db.ts +534 -42
- 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-analytics.ts +32 -5
- package/src/lib/codex-browser-db.ts +540 -137
- package/src/lib/codex-browser-export.ts +18 -27
- package/src/lib/codex-browser-types.ts +21 -1
- package/src/lib/codex-thread-cache.ts +106 -23
- package/src/lib/codex-thread-parser.ts +149 -55
- package/src/lib/codex-thread-recovery.ts +38 -11
- package/src/lib/codex-transcript-filter.ts +31 -0
- package/src/lib/codex-transcript-renderer.ts +87 -79
- package/src/lib/concurrency.ts +56 -3
- package/src/lib/conversation-api.ts +410 -27
- package/src/lib/conversation-data/adapter-helpers.ts +25 -3
- package/src/lib/conversation-data/antigravity-adapter.ts +70 -26
- package/src/lib/conversation-data/claude-code-adapter.ts +69 -12
- package/src/lib/conversation-data/codex-adapter.ts +108 -73
- package/src/lib/conversation-data/cursor-adapter.ts +77 -29
- package/src/lib/conversation-data/grok-adapter.ts +223 -0
- package/src/lib/conversation-data/index.ts +105 -11
- package/src/lib/conversation-data/kiro-adapter.ts +59 -12
- package/src/lib/conversation-data/message-selector.ts +2 -5
- package/src/lib/conversation-data/opencode-adapter.ts +67 -17
- package/src/lib/conversation-data/path-match.ts +1 -13
- package/src/lib/conversation-data/qoder-adapter.ts +67 -37
- package/src/lib/conversation-data/types.ts +43 -0
- package/src/lib/conversation-zip-export.ts +75 -0
- package/src/lib/cursor-db.ts +68 -28
- package/src/lib/cursor-id.ts +17 -0
- package/src/lib/cursor-recovery.ts +146 -35
- package/src/lib/cursor-transcript-phase.ts +40 -0
- package/src/lib/cursor-transcript.ts +21 -5
- package/src/lib/grok-db.ts +1068 -0
- package/src/lib/grok-exporter-types.ts +110 -0
- package/src/lib/grok-transcript-phase.ts +61 -0
- package/src/lib/grok-transcript.ts +167 -0
- package/src/lib/kiro-db.ts +187 -43
- package/src/lib/kiro-transcript.ts +0 -4
- package/src/lib/model-label.ts +11 -3
- package/src/lib/opencode-db.ts +599 -60
- package/src/lib/opencode-think-tags.ts +17 -3
- package/src/lib/opencode-transcript.ts +8 -5
- package/src/lib/portable-path.ts +9 -0
- package/src/lib/qoder-acp-client.ts +11 -2
- package/src/lib/qoder-db.ts +51 -17
- package/src/lib/qoder-transcript-phase.ts +64 -0
- package/src/lib/qoder-transcript.ts +30 -2
- package/src/lib/shared.ts +63 -15
- package/src/lib/sqlite-error.ts +31 -5
- package/src/lib/transcript-load-limiter.ts +87 -0
- package/src/lib/ui-cache.ts +185 -24
- package/src/lib/ui-export-archive.ts +58 -39
- package/src/lib/ui-export-files.ts +75 -15
- package/src/lib/ui-export-zip.ts +40 -0
- package/apps/ui/package.json +0 -65
|
@@ -1,37 +1,37 @@
|
|
|
1
1
|
import type { ClaudeCodeSessionTranscript } from '@spiracha/lib/claude-code-exporter-types';
|
|
2
2
|
import type { ThreadEvent, ThreadTranscriptStats } from '@spiracha/lib/codex-browser-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, useQuery, useQueryClient, useSuspenseQuery } from '@tanstack/react-query';
|
|
4
|
+
import { createFileRoute, Link, useNavigate } from '@tanstack/react-router';
|
|
5
|
+
import { Download, Trash2 } from 'lucide-react';
|
|
6
6
|
import { useMemo, useState } from 'react';
|
|
7
7
|
import { Breadcrumbs } from '#/components/breadcrumbs';
|
|
8
|
+
import { DeleteConfirmDialog } from '#/components/delete-confirm-dialog';
|
|
8
9
|
import { ExportDialog } from '#/components/export-dialog';
|
|
9
10
|
import { JsonPanel } from '#/components/json-panel';
|
|
10
11
|
import { LoadingPanel } from '#/components/loading-panel';
|
|
11
12
|
import { MetadataSection } from '#/components/metadata-section';
|
|
12
13
|
import { MetricCard } from '#/components/metric-card';
|
|
13
14
|
import { PageHeader } from '#/components/page-header';
|
|
14
|
-
import {
|
|
15
|
-
import { TranscriptView } from '#/components/transcript-view';
|
|
15
|
+
import { RouteErrorPanel } from '#/components/route-error-panel';
|
|
16
|
+
import { DEFAULT_SHOW_USER_MESSAGES, TranscriptView } from '#/components/transcript-view';
|
|
16
17
|
import { Button } from '#/components/ui/button';
|
|
17
18
|
import { Checkbox } from '#/components/ui/checkbox';
|
|
18
19
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '#/components/ui/tabs';
|
|
19
|
-
import {
|
|
20
|
-
|
|
20
|
+
import {
|
|
21
|
+
claudeCodeSessionDetailQueryOptions,
|
|
22
|
+
claudeCodeSessionTranscriptQueryOptions,
|
|
23
|
+
claudeCodeWorkspacesQueryOptions,
|
|
24
|
+
} from '#/lib/claude-code-queries';
|
|
25
|
+
import { deleteClaudeCodeSessionFn, exportClaudeCodeSessionFn } from '#/lib/claude-code-server';
|
|
21
26
|
import {
|
|
22
27
|
claudeCodeTranscriptToThreadEvents,
|
|
23
28
|
getClaudeCodeThreadTranscriptStats,
|
|
24
29
|
} from '#/lib/claude-code-transcript-events';
|
|
25
30
|
import { downloadTextFile, downloadUrlFile } from '#/lib/download';
|
|
31
|
+
import type { ExportDialogOptions } from '#/lib/export-options';
|
|
26
32
|
import { formatDateTime, formatList, formatNumber, formatTokens } from '#/lib/formatters';
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
includeCommentary: boolean;
|
|
30
|
-
includeMetadata: boolean;
|
|
31
|
-
includeTools: boolean;
|
|
32
|
-
outputFormat: 'md' | 'txt';
|
|
33
|
-
zipArchive: boolean;
|
|
34
|
-
};
|
|
33
|
+
import { RouteStateResetBoundary } from '#/lib/route-state-reset';
|
|
34
|
+
import { shouldNavigateToSourceIndexAfterDelete } from '#/lib/workspace-delete-navigation';
|
|
35
35
|
|
|
36
36
|
type TranscriptControlsProps = {
|
|
37
37
|
rawJsonDisabled?: boolean;
|
|
@@ -48,7 +48,14 @@ type TranscriptControlsProps = {
|
|
|
48
48
|
};
|
|
49
49
|
|
|
50
50
|
export const Route = createFileRoute('/claude-code-sessions/$sessionId')({
|
|
51
|
-
component:
|
|
51
|
+
component: () => {
|
|
52
|
+
const { sessionId } = Route.useParams();
|
|
53
|
+
return (
|
|
54
|
+
<RouteStateResetBoundary routeKey={sessionId}>
|
|
55
|
+
<ClaudeCodeSessionDetailPage />
|
|
56
|
+
</RouteStateResetBoundary>
|
|
57
|
+
);
|
|
58
|
+
},
|
|
52
59
|
errorComponent: ClaudeCodeSessionDetailErrorComponent,
|
|
53
60
|
loader: ({ context, params }) =>
|
|
54
61
|
context.queryClient.ensureQueryData(claudeCodeSessionDetailQueryOptions(params.sessionId)),
|
|
@@ -61,7 +68,7 @@ export const Route = createFileRoute('/claude-code-sessions/$sessionId')({
|
|
|
61
68
|
});
|
|
62
69
|
|
|
63
70
|
function ClaudeCodeSessionDetailErrorComponent({ error }: { error: Error }) {
|
|
64
|
-
return <
|
|
71
|
+
return <RouteErrorPanel error={error} title="Failed to load Claude Code session" />;
|
|
65
72
|
}
|
|
66
73
|
|
|
67
74
|
const buildSessionMetadata = (detail: ClaudeCodeSessionTranscript) => [
|
|
@@ -169,6 +176,20 @@ const ClaudeCodeTranscriptControls = ({
|
|
|
169
176
|
};
|
|
170
177
|
|
|
171
178
|
function ClaudeCodeRawPanels({ detail, events }: { detail: ClaudeCodeSessionTranscript; events: ThreadEvent[] }) {
|
|
179
|
+
if (detail.rawPayloadsOmitted) {
|
|
180
|
+
return (
|
|
181
|
+
<section className="rounded-[1.6rem] border border-[var(--border)] bg-[var(--panel)] p-5 shadow-[var(--panel-shadow)]">
|
|
182
|
+
<h3 className="font-semibold text-[var(--muted-foreground)] text-sm uppercase tracking-[0.18em]">
|
|
183
|
+
Raw payloads omitted
|
|
184
|
+
</h3>
|
|
185
|
+
<p className="mt-4 text-[var(--muted-foreground)] text-sm">
|
|
186
|
+
This Claude Code session is large, so raw JSON payload copies were omitted from the browser detail
|
|
187
|
+
response. Export still reads the full source session from disk.
|
|
188
|
+
</p>
|
|
189
|
+
</section>
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
|
|
172
193
|
return (
|
|
173
194
|
<div className="space-y-4">
|
|
174
195
|
<JsonPanel title="Session summary" value={detail.session} />
|
|
@@ -179,14 +200,57 @@ function ClaudeCodeRawPanels({ detail, events }: { detail: ClaudeCodeSessionTran
|
|
|
179
200
|
);
|
|
180
201
|
}
|
|
181
202
|
|
|
203
|
+
const ClaudeCodeTranscriptPreviewNotice = ({
|
|
204
|
+
fullTranscriptLoaded,
|
|
205
|
+
omittedEntryCount,
|
|
206
|
+
pending,
|
|
207
|
+
onLoad,
|
|
208
|
+
}: {
|
|
209
|
+
fullTranscriptLoaded: boolean;
|
|
210
|
+
omittedEntryCount: number;
|
|
211
|
+
pending: boolean;
|
|
212
|
+
onLoad: () => void;
|
|
213
|
+
}) => {
|
|
214
|
+
const buttonLabel = fullTranscriptLoaded
|
|
215
|
+
? 'Full transcript loaded'
|
|
216
|
+
: pending
|
|
217
|
+
? 'Loading full transcript...'
|
|
218
|
+
: 'Load Full Transcript';
|
|
219
|
+
|
|
220
|
+
return (
|
|
221
|
+
<section className="rounded-[1.6rem] border border-[var(--border)] bg-[var(--panel)] p-5 shadow-[var(--panel-shadow)]">
|
|
222
|
+
<h3 className="font-semibold text-base">Showing a compact transcript preview</h3>
|
|
223
|
+
<p className="mt-2 text-[var(--muted-foreground)] text-sm leading-6">
|
|
224
|
+
Spiracha omitted {formatNumber(omittedEntryCount)} internal transcript entries from the initial page
|
|
225
|
+
load. The preview keeps the beginning and latest activity.
|
|
226
|
+
</p>
|
|
227
|
+
<div className="mt-4">
|
|
228
|
+
<Button disabled={pending || fullTranscriptLoaded} variant="outline" onClick={onLoad}>
|
|
229
|
+
{buttonLabel}
|
|
230
|
+
</Button>
|
|
231
|
+
</div>
|
|
232
|
+
</section>
|
|
233
|
+
);
|
|
234
|
+
};
|
|
235
|
+
|
|
182
236
|
function ClaudeCodeSessionDetailPage() {
|
|
183
|
-
const
|
|
237
|
+
const navigate = useNavigate();
|
|
238
|
+
const queryClient = useQueryClient();
|
|
239
|
+
const params = Route.useParams();
|
|
240
|
+
const initialDetail = useSuspenseQuery(claudeCodeSessionDetailQueryOptions(params.sessionId)).data;
|
|
241
|
+
const [shouldLoadFullTranscript, setShouldLoadFullTranscript] = useState(false);
|
|
242
|
+
const fullTranscriptQuery = useQuery({
|
|
243
|
+
...claudeCodeSessionTranscriptQueryOptions(params.sessionId),
|
|
244
|
+
enabled: shouldLoadFullTranscript,
|
|
245
|
+
});
|
|
246
|
+
const detail = fullTranscriptQuery.data ?? initialDetail;
|
|
247
|
+
const [deleteOpen, setDeleteOpen] = useState(false);
|
|
184
248
|
const [pendingExport, setPendingExport] = useState(false);
|
|
185
249
|
const [showToolCalls, setShowToolCalls] = useState(false);
|
|
186
250
|
const [showCommentary, setShowCommentary] = useState(false);
|
|
187
251
|
const [showExtraEvents, setShowExtraEvents] = useState(false);
|
|
188
252
|
const [showRawJson, setShowRawJson] = useState(false);
|
|
189
|
-
const [showUserMessages, setShowUserMessages] = useState(
|
|
253
|
+
const [showUserMessages, setShowUserMessages] = useState(DEFAULT_SHOW_USER_MESSAGES);
|
|
190
254
|
const transcriptEvents = useMemo(() => claudeCodeTranscriptToThreadEvents(detail), [detail]);
|
|
191
255
|
const transcriptStats = useMemo(() => getClaudeCodeThreadTranscriptStats(transcriptEvents), [transcriptEvents]);
|
|
192
256
|
|
|
@@ -214,19 +278,58 @@ function ClaudeCodeSessionDetailPage() {
|
|
|
214
278
|
},
|
|
215
279
|
});
|
|
216
280
|
|
|
281
|
+
const deleteSessionMutation = useMutation({
|
|
282
|
+
mutationFn: () => deleteClaudeCodeSessionFn({ data: { sessionId: detail.session.sessionId } }),
|
|
283
|
+
onSuccess: async () => {
|
|
284
|
+
await Promise.all([
|
|
285
|
+
queryClient.invalidateQueries({ queryKey: ['claude-code-workspaces'] }),
|
|
286
|
+
queryClient.invalidateQueries({ queryKey: ['claude-code-sessions', detail.session.workspaceKey] }),
|
|
287
|
+
queryClient.invalidateQueries({ queryKey: ['claude-code-session', detail.session.sessionId] }),
|
|
288
|
+
queryClient.invalidateQueries({ queryKey: ['claude-code-session', params.sessionId] }),
|
|
289
|
+
queryClient.invalidateQueries({ queryKey: ['claude-code-session-transcript', params.sessionId] }),
|
|
290
|
+
]);
|
|
291
|
+
const workspaces = await queryClient.fetchQuery(claudeCodeWorkspacesQueryOptions());
|
|
292
|
+
if (
|
|
293
|
+
shouldNavigateToSourceIndexAfterDelete(
|
|
294
|
+
workspaces,
|
|
295
|
+
detail.session.workspaceKey,
|
|
296
|
+
(workspace) => workspace.key,
|
|
297
|
+
)
|
|
298
|
+
) {
|
|
299
|
+
navigate({ to: '/claude-code' });
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
navigate({
|
|
303
|
+
params: { workspaceKey: detail.session.workspaceKey },
|
|
304
|
+
to: '/claude-code/$workspaceKey',
|
|
305
|
+
});
|
|
306
|
+
},
|
|
307
|
+
});
|
|
308
|
+
|
|
217
309
|
return (
|
|
218
310
|
<div className="space-y-6">
|
|
219
311
|
<PageHeader
|
|
220
312
|
actions={
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
313
|
+
<>
|
|
314
|
+
<Button
|
|
315
|
+
className="rounded-full"
|
|
316
|
+
type="button"
|
|
317
|
+
variant="outline"
|
|
318
|
+
onClick={() => setPendingExport(true)}
|
|
319
|
+
>
|
|
320
|
+
<Download className="mr-2 size-4" />
|
|
321
|
+
Export
|
|
322
|
+
</Button>
|
|
323
|
+
<Button
|
|
324
|
+
className="rounded-full border-[var(--destructive)]/20 text-[var(--destructive)]"
|
|
325
|
+
type="button"
|
|
326
|
+
variant="outline"
|
|
327
|
+
onClick={() => setDeleteOpen(true)}
|
|
328
|
+
>
|
|
329
|
+
<Trash2 className="mr-2 size-4" />
|
|
330
|
+
Delete
|
|
331
|
+
</Button>
|
|
332
|
+
</>
|
|
230
333
|
}
|
|
231
334
|
breadcrumb={
|
|
232
335
|
<Breadcrumbs
|
|
@@ -280,6 +383,22 @@ function ClaudeCodeSessionDetailPage() {
|
|
|
280
383
|
onShowToolCallsChange={setShowToolCalls}
|
|
281
384
|
onShowUserMessagesChange={setShowUserMessages}
|
|
282
385
|
/>
|
|
386
|
+
{initialDetail.isPartial ? (
|
|
387
|
+
<ClaudeCodeTranscriptPreviewNotice
|
|
388
|
+
fullTranscriptLoaded={Boolean(fullTranscriptQuery.data)}
|
|
389
|
+
omittedEntryCount={initialDetail.omittedEntryCount ?? 0}
|
|
390
|
+
pending={fullTranscriptQuery.isFetching}
|
|
391
|
+
onLoad={() => setShouldLoadFullTranscript(true)}
|
|
392
|
+
/>
|
|
393
|
+
) : null}
|
|
394
|
+
{fullTranscriptQuery.isError ? (
|
|
395
|
+
<p className="text-[var(--destructive)] text-sm">
|
|
396
|
+
Failed to load the full transcript:{' '}
|
|
397
|
+
{fullTranscriptQuery.error instanceof Error
|
|
398
|
+
? fullTranscriptQuery.error.message
|
|
399
|
+
: 'Unknown error'}
|
|
400
|
+
</p>
|
|
401
|
+
) : null}
|
|
283
402
|
{transcriptEvents.length > 0 ? (
|
|
284
403
|
<TranscriptView
|
|
285
404
|
assistantModel={detail.session.model}
|
|
@@ -337,6 +456,27 @@ function ClaudeCodeSessionDetailPage() {
|
|
|
337
456
|
}
|
|
338
457
|
}}
|
|
339
458
|
/>
|
|
459
|
+
|
|
460
|
+
<DeleteConfirmDialog
|
|
461
|
+
confirmLabel={deleteSessionMutation.isPending ? 'Deleting...' : 'Delete session'}
|
|
462
|
+
description="Permanently delete this Claude Code session from disk. This removes the session JSONL file."
|
|
463
|
+
errorMessage={
|
|
464
|
+
deleteSessionMutation.isError
|
|
465
|
+
? deleteSessionMutation.error instanceof Error
|
|
466
|
+
? deleteSessionMutation.error.message
|
|
467
|
+
: 'Session delete failed'
|
|
468
|
+
: null
|
|
469
|
+
}
|
|
470
|
+
open={deleteOpen}
|
|
471
|
+
title="Delete this Claude Code session?"
|
|
472
|
+
onConfirm={() => deleteSessionMutation.mutate()}
|
|
473
|
+
onOpenChange={(open) => {
|
|
474
|
+
setDeleteOpen(open);
|
|
475
|
+
if (!open) {
|
|
476
|
+
deleteSessionMutation.reset();
|
|
477
|
+
}
|
|
478
|
+
}}
|
|
479
|
+
/>
|
|
340
480
|
</div>
|
|
341
481
|
);
|
|
342
482
|
}
|
|
@@ -1,24 +1,33 @@
|
|
|
1
1
|
import type { ClaudeCodeSessionSummary, ClaudeCodeWorkspaceGroup } from '@spiracha/lib/claude-code-exporter-types';
|
|
2
|
-
import { useMutation, useSuspenseQuery } from '@tanstack/react-query';
|
|
3
|
-
import { createFileRoute } from '@tanstack/react-router';
|
|
4
|
-
import { useDeferredValue, useState } from 'react';
|
|
2
|
+
import { useMutation, useQueryClient, useSuspenseQuery } from '@tanstack/react-query';
|
|
3
|
+
import { createFileRoute, useNavigate } from '@tanstack/react-router';
|
|
4
|
+
import { useDeferredValue, useMemo, useState } from 'react';
|
|
5
5
|
import { ClaudeCodeSessionsTable } from '#/components/claude-code-sessions-table';
|
|
6
|
+
import { DeleteConfirmDialog } from '#/components/delete-confirm-dialog';
|
|
6
7
|
import { ExportDialog } from '#/components/export-dialog';
|
|
7
8
|
import { ListSearchInput } from '#/components/list-search-input';
|
|
8
9
|
import { LoadingPanel } from '#/components/loading-panel';
|
|
9
10
|
import { PageHeader } from '#/components/page-header';
|
|
10
|
-
import {
|
|
11
|
+
import { RouteErrorPanel } from '#/components/route-error-panel';
|
|
11
12
|
import { claudeCodeSessionsQueryOptions, claudeCodeWorkspacesQueryOptions } from '#/lib/claude-code-queries';
|
|
12
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
deleteClaudeCodeSessionFn,
|
|
15
|
+
deleteClaudeCodeSessionsFn,
|
|
16
|
+
exportClaudeCodeSessionFn,
|
|
17
|
+
exportClaudeCodeSessionsFn,
|
|
18
|
+
} from '#/lib/claude-code-server';
|
|
13
19
|
import { downloadTextFile, downloadUrlFile } from '#/lib/download';
|
|
20
|
+
import { createExportSelectionMutationInput, type ExportSelectionMutationInput } from '#/lib/export-mutation';
|
|
14
21
|
import { matchesTextQuery } from '#/lib/text-filter';
|
|
22
|
+
import { isWorkspaceEmptiedByDelete } from '#/lib/workspace-delete-navigation';
|
|
15
23
|
|
|
16
|
-
type
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
24
|
+
type PendingSessionDelete = {
|
|
25
|
+
sessions: ClaudeCodeSessionSummary[];
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
type PendingSessionExport = {
|
|
29
|
+
label: string;
|
|
30
|
+
sessionIds: string[];
|
|
22
31
|
};
|
|
23
32
|
|
|
24
33
|
const findWorkspaceOrThrow = (workspaces: ClaudeCodeWorkspaceGroup[], workspaceKey: string) => {
|
|
@@ -30,6 +39,36 @@ const findWorkspaceOrThrow = (workspaces: ClaudeCodeWorkspaceGroup[], workspaceK
|
|
|
30
39
|
return workspace;
|
|
31
40
|
};
|
|
32
41
|
|
|
42
|
+
const buildSessionExport = (selectedSessions: ClaudeCodeSessionSummary[]): PendingSessionExport => ({
|
|
43
|
+
label: selectedSessions.length === 1 ? selectedSessions[0]!.title : `${selectedSessions.length} selected sessions`,
|
|
44
|
+
sessionIds: selectedSessions.map((session) => session.sessionId),
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
const getDeleteConfirmLabel = (pendingDelete: PendingSessionDelete | null, isPending: boolean) => {
|
|
48
|
+
if (isPending) {
|
|
49
|
+
return 'Deleting...';
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return pendingDelete && pendingDelete.sessions.length > 1 ? 'Delete sessions' : 'Delete session';
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const getDeleteDescription = (pendingDelete: PendingSessionDelete | null) => {
|
|
56
|
+
if (!pendingDelete) {
|
|
57
|
+
return 'Permanently delete the selected Claude Code sessions from disk.';
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (pendingDelete.sessions.length === 1) {
|
|
61
|
+
return `Permanently delete "${pendingDelete.sessions[0]!.title}" from Claude Code history. This removes the session JSONL file from disk.`;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return `Permanently delete ${pendingDelete.sessions.length} selected Claude Code sessions from disk.`;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const getDeleteTitle = (pendingDelete: PendingSessionDelete | null) =>
|
|
68
|
+
pendingDelete && pendingDelete.sessions.length > 1
|
|
69
|
+
? `Delete ${pendingDelete.sessions.length} Claude Code sessions?`
|
|
70
|
+
: 'Delete this Claude Code session?';
|
|
71
|
+
|
|
33
72
|
export const Route = createFileRoute('/claude-code/$workspaceKey')({
|
|
34
73
|
component: ClaudeCodeWorkspacePage,
|
|
35
74
|
errorComponent: ClaudeCodeWorkspaceErrorComponent,
|
|
@@ -44,34 +83,45 @@ export const Route = createFileRoute('/claude-code/$workspaceKey')({
|
|
|
44
83
|
});
|
|
45
84
|
|
|
46
85
|
function ClaudeCodeWorkspaceErrorComponent({ error }: { error: Error }) {
|
|
47
|
-
return <
|
|
86
|
+
return <RouteErrorPanel error={error} title="Failed to load Claude Code workspace" />;
|
|
48
87
|
}
|
|
49
88
|
|
|
50
89
|
function ClaudeCodeWorkspacePage() {
|
|
90
|
+
const navigate = useNavigate({ from: Route.fullPath });
|
|
51
91
|
const params = Route.useParams();
|
|
92
|
+
const queryClient = useQueryClient();
|
|
52
93
|
const workspaces = useSuspenseQuery(claudeCodeWorkspacesQueryOptions()).data;
|
|
53
94
|
const workspace = findWorkspaceOrThrow(workspaces, params.workspaceKey);
|
|
54
95
|
const sessions = useSuspenseQuery(claudeCodeSessionsQueryOptions(workspace.key)).data;
|
|
55
96
|
const [searchInput, setSearchInput] = useState('');
|
|
56
|
-
const [
|
|
97
|
+
const [pendingDelete, setPendingDelete] = useState<PendingSessionDelete | null>(null);
|
|
98
|
+
const [pendingExport, setPendingExport] = useState<PendingSessionExport | null>(null);
|
|
57
99
|
const deferredSearch = useDeferredValue(searchInput);
|
|
58
100
|
|
|
59
101
|
const exportMutation = useMutation({
|
|
60
|
-
mutationFn: async (options:
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
102
|
+
mutationFn: async ({ ids, options }: ExportSelectionMutationInput) => {
|
|
103
|
+
const download =
|
|
104
|
+
ids.length === 1
|
|
105
|
+
? await exportClaudeCodeSessionFn({
|
|
106
|
+
data: {
|
|
107
|
+
includeCommentary: options.includeCommentary,
|
|
108
|
+
includeMetadata: options.includeMetadata,
|
|
109
|
+
includeTools: options.includeTools,
|
|
110
|
+
outputFormat: options.outputFormat,
|
|
111
|
+
sessionId: ids[0]!,
|
|
112
|
+
zipArchive: options.zipArchive,
|
|
113
|
+
},
|
|
114
|
+
})
|
|
115
|
+
: await exportClaudeCodeSessionsFn({
|
|
116
|
+
data: {
|
|
117
|
+
includeCommentary: options.includeCommentary,
|
|
118
|
+
includeMetadata: options.includeMetadata,
|
|
119
|
+
includeTools: options.includeTools,
|
|
120
|
+
outputFormat: options.outputFormat,
|
|
121
|
+
sessionIds: [...ids],
|
|
122
|
+
zipArchive: options.zipArchive,
|
|
123
|
+
},
|
|
124
|
+
});
|
|
75
125
|
if (download.mode === 'download') {
|
|
76
126
|
downloadTextFile(download.fileName, download.content, download.mimeType);
|
|
77
127
|
return;
|
|
@@ -84,16 +134,63 @@ function ClaudeCodeWorkspacePage() {
|
|
|
84
134
|
},
|
|
85
135
|
});
|
|
86
136
|
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
137
|
+
const deleteMutation = useMutation({
|
|
138
|
+
mutationFn: async (sessionIds: string[]) =>
|
|
139
|
+
sessionIds.length === 1
|
|
140
|
+
? deleteClaudeCodeSessionFn({ data: { sessionId: sessionIds[0]! } })
|
|
141
|
+
: deleteClaudeCodeSessionsFn({ data: { sessionIds } }),
|
|
142
|
+
onSettled: async (_result, _error, sessionIds) => {
|
|
143
|
+
await Promise.all([
|
|
144
|
+
queryClient.invalidateQueries({ queryKey: ['claude-code-workspaces'] }),
|
|
145
|
+
queryClient.invalidateQueries({ queryKey: ['claude-code-sessions', workspace.key] }),
|
|
146
|
+
...sessionIds.map((sessionId) =>
|
|
147
|
+
queryClient.invalidateQueries({ queryKey: ['claude-code-session', sessionId] }),
|
|
148
|
+
),
|
|
149
|
+
]);
|
|
150
|
+
},
|
|
151
|
+
onSuccess: async (_result, sessionIds) => {
|
|
152
|
+
const workspaceEmptied = isWorkspaceEmptiedByDelete(sessions, sessionIds, (session) => session.sessionId);
|
|
153
|
+
setPendingDelete(null);
|
|
154
|
+
if (workspaceEmptied) {
|
|
155
|
+
await navigate({ to: '/claude-code' });
|
|
156
|
+
}
|
|
157
|
+
},
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
const visibleSessions = useMemo(
|
|
161
|
+
() =>
|
|
162
|
+
sessions.filter((session) =>
|
|
163
|
+
matchesTextQuery(deferredSearch, [
|
|
164
|
+
session.title,
|
|
165
|
+
session.sessionId,
|
|
166
|
+
session.model,
|
|
167
|
+
session.version,
|
|
168
|
+
session.gitBranch,
|
|
169
|
+
session.filePath,
|
|
170
|
+
]),
|
|
171
|
+
),
|
|
172
|
+
[deferredSearch, sessions],
|
|
173
|
+
);
|
|
174
|
+
const visibleSessionsById = useMemo(
|
|
175
|
+
() => new Map(visibleSessions.map((session) => [session.sessionId, session])),
|
|
176
|
+
[visibleSessions],
|
|
96
177
|
);
|
|
178
|
+
const lookupSelectedSessions = (sessionIds: string[]) =>
|
|
179
|
+
sessionIds
|
|
180
|
+
.map((sessionId) => visibleSessionsById.get(sessionId) ?? null)
|
|
181
|
+
.filter((session): session is ClaudeCodeSessionSummary => session !== null);
|
|
182
|
+
const openExportForSessions = (selectedSessions: ClaudeCodeSessionSummary[]) => {
|
|
183
|
+
if (selectedSessions.length === 0) {
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
setPendingExport(buildSessionExport(selectedSessions));
|
|
188
|
+
};
|
|
189
|
+
const openDeleteForSessions = (selectedSessions: ClaudeCodeSessionSummary[]) => {
|
|
190
|
+
if (selectedSessions.length > 0) {
|
|
191
|
+
setPendingDelete({ sessions: selectedSessions });
|
|
192
|
+
}
|
|
193
|
+
};
|
|
97
194
|
|
|
98
195
|
return (
|
|
99
196
|
<div className="space-y-6">
|
|
@@ -110,13 +207,31 @@ function ClaudeCodeWorkspacePage() {
|
|
|
110
207
|
title={workspace.label}
|
|
111
208
|
/>
|
|
112
209
|
|
|
113
|
-
<ClaudeCodeSessionsTable
|
|
210
|
+
<ClaudeCodeSessionsTable
|
|
211
|
+
sessions={visibleSessions}
|
|
212
|
+
onDeleteSession={(session) => openDeleteForSessions([session])}
|
|
213
|
+
onDeleteSessions={(sessionIds) => openDeleteForSessions(lookupSelectedSessions(sessionIds))}
|
|
214
|
+
onExportSession={(session) => openExportForSessions([session])}
|
|
215
|
+
onExportSessions={(sessionIds) => openExportForSessions(lookupSelectedSessions(sessionIds))}
|
|
216
|
+
/>
|
|
114
217
|
|
|
115
218
|
<ExportDialog
|
|
219
|
+
errorMessage={
|
|
220
|
+
exportMutation.isError
|
|
221
|
+
? exportMutation.error instanceof Error
|
|
222
|
+
? exportMutation.error.message
|
|
223
|
+
: 'Session export failed'
|
|
224
|
+
: null
|
|
225
|
+
}
|
|
226
|
+
forceZipArchive={pendingExport ? pendingExport.sessionIds.length > 1 : false}
|
|
116
227
|
open={pendingExport !== null}
|
|
117
228
|
pending={exportMutation.isPending}
|
|
118
|
-
title={pendingExport ? `Export ${pendingExport.
|
|
119
|
-
onExport={(options) =>
|
|
229
|
+
title={pendingExport ? `Export ${pendingExport.label}` : 'Export session'}
|
|
230
|
+
onExport={(options) => {
|
|
231
|
+
if (pendingExport) {
|
|
232
|
+
exportMutation.mutate(createExportSelectionMutationInput(pendingExport.sessionIds, options));
|
|
233
|
+
}
|
|
234
|
+
}}
|
|
120
235
|
onOpenChange={(open) => {
|
|
121
236
|
if (!open) {
|
|
122
237
|
setPendingExport(null);
|
|
@@ -125,11 +240,30 @@ function ClaudeCodeWorkspacePage() {
|
|
|
125
240
|
}}
|
|
126
241
|
/>
|
|
127
242
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
243
|
+
<DeleteConfirmDialog
|
|
244
|
+
confirmLabel={getDeleteConfirmLabel(pendingDelete, deleteMutation.isPending)}
|
|
245
|
+
description={getDeleteDescription(pendingDelete)}
|
|
246
|
+
errorMessage={
|
|
247
|
+
deleteMutation.isError
|
|
248
|
+
? deleteMutation.error instanceof Error
|
|
249
|
+
? deleteMutation.error.message
|
|
250
|
+
: 'Session delete failed'
|
|
251
|
+
: null
|
|
252
|
+
}
|
|
253
|
+
open={pendingDelete !== null}
|
|
254
|
+
title={getDeleteTitle(pendingDelete)}
|
|
255
|
+
onConfirm={() => {
|
|
256
|
+
if (pendingDelete) {
|
|
257
|
+
deleteMutation.mutate(pendingDelete.sessions.map((session) => session.sessionId));
|
|
258
|
+
}
|
|
259
|
+
}}
|
|
260
|
+
onOpenChange={(open) => {
|
|
261
|
+
if (!open) {
|
|
262
|
+
setPendingDelete(null);
|
|
263
|
+
deleteMutation.reset();
|
|
264
|
+
}
|
|
265
|
+
}}
|
|
266
|
+
/>
|
|
133
267
|
</div>
|
|
134
268
|
);
|
|
135
269
|
}
|
|
@@ -4,7 +4,7 @@ import { useDeferredValue, useState } from 'react';
|
|
|
4
4
|
import { ClaudeCodeWorkspacesTable } from '#/components/claude-code-workspaces-table';
|
|
5
5
|
import { ListSearchInput } from '#/components/list-search-input';
|
|
6
6
|
import { PageHeader } from '#/components/page-header';
|
|
7
|
-
import {
|
|
7
|
+
import { RouteErrorPanel } from '#/components/route-error-panel';
|
|
8
8
|
import { claudeCodeWorkspacesQueryOptions } from '#/lib/claude-code-queries';
|
|
9
9
|
import { matchesTextQuery } from '#/lib/text-filter';
|
|
10
10
|
|
|
@@ -15,7 +15,7 @@ export const Route = createFileRoute('/claude-code/')({
|
|
|
15
15
|
});
|
|
16
16
|
|
|
17
17
|
function ClaudeCodeErrorComponent({ error }: { error: Error }) {
|
|
18
|
-
return <
|
|
18
|
+
return <RouteErrorPanel error={error} title="Failed to load Claude Code workspaces" />;
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
function ClaudeCodePage() {
|