spiracha 2.1.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 +37 -19
- package/apps/ui/AGENTS.md +18 -10
- package/apps/ui/README.md +17 -10
- package/apps/ui/src/components/analytics-breakdowns.tsx +106 -0
- package/apps/ui/src/components/antigravity-conversations-table.tsx +1 -0
- package/apps/ui/src/components/app-shell.tsx +45 -3
- package/apps/ui/src/components/data-table.tsx +17 -1
- package/apps/ui/src/components/delete-confirm-dialog.tsx +4 -3
- package/apps/ui/src/components/export-dialog.tsx +77 -62
- package/apps/ui/src/components/projects-table.tsx +4 -1
- package/apps/ui/src/components/route-error-panel.tsx +13 -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 +4 -1
- package/apps/ui/src/lib/antigravity-server.ts +70 -17
- package/apps/ui/src/lib/antigravity-transcript-events.ts +46 -70
- package/apps/ui/src/lib/claude-code-server.ts +14 -4
- package/apps/ui/src/lib/codex-queries.ts +3 -3
- package/apps/ui/src/lib/codex-server.ts +3 -3
- package/apps/ui/src/lib/cursor-server.ts +41 -69
- 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/grok-server.ts +2 -3
- package/apps/ui/src/lib/kiro-server.ts +14 -2
- package/apps/ui/src/lib/mutation-error.ts +15 -0
- package/apps/ui/src/lib/opencode-server.ts +13 -2
- 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-transcript-events.ts +1 -1
- 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 +1 -1
- package/apps/ui/src/lib/thread-metadata.ts +22 -0
- package/apps/ui/src/lib/thread-transcript-stats.ts +10 -1
- package/apps/ui/src/lib/workspace-delete-navigation.ts +8 -0
- package/apps/ui/src/routeTree.gen.ts +282 -282
- 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 +73 -19
- package/apps/ui/src/routes/antigravity.$workspaceKey.tsx +42 -60
- package/apps/ui/src/routes/antigravity.index.tsx +2 -2
- package/apps/ui/src/routes/claude-code-sessions.$sessionId.tsx +25 -11
- package/apps/ui/src/routes/claude-code.$workspaceKey.tsx +27 -32
- 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 +29 -16
- package/apps/ui/src/routes/cursor.$workspaceKey.tsx +33 -27
- package/apps/ui/src/routes/cursor.index.tsx +5 -2
- package/apps/ui/src/routes/grok-sessions.$sessionId.tsx +25 -12
- package/apps/ui/src/routes/grok.$workspaceKey.tsx +20 -26
- package/apps/ui/src/routes/grok.index.tsx +2 -2
- package/apps/ui/src/routes/index.tsx +2 -18
- package/apps/ui/src/routes/kiro-sessions.$sessionId.tsx +25 -12
- package/apps/ui/src/routes/kiro.$workspaceKey.tsx +27 -32
- package/apps/ui/src/routes/kiro.index.tsx +2 -2
- package/apps/ui/src/routes/opencode-sessions.$sessionId.tsx +29 -36
- package/apps/ui/src/routes/opencode.$workspaceKey.tsx +45 -61
- package/apps/ui/src/routes/opencode.index.tsx +8 -17
- package/apps/ui/src/routes/qoder-sessions.$sessionId.tsx +12 -11
- package/apps/ui/src/routes/qoder.$workspaceKey.tsx +19 -25
- package/apps/ui/src/routes/qoder.index.tsx +2 -2
- package/apps/ui/src/routes/threads.$threadId.tsx +87 -56
- package/apps/ui/vite.config.ts +22 -12
- package/bin/spiracha.ts +28 -1
- package/package.json +26 -17
- package/src/client.ts +61 -16
- package/src/lib/antigravity-db.ts +236 -82
- package/src/lib/antigravity-keychain.ts +7 -8
- package/src/lib/antigravity-transcript-phase.ts +47 -0
- package/src/lib/claude-code-db.ts +45 -19
- package/src/lib/codex-analytics.ts +32 -5
- package/src/lib/codex-browser-db.ts +540 -137
- package/src/lib/codex-browser-export.ts +2 -7
- package/src/lib/codex-browser-types.ts +21 -1
- package/src/lib/codex-thread-cache.ts +88 -33
- package/src/lib/codex-thread-parser.ts +78 -35
- package/src/lib/codex-thread-recovery.ts +38 -11
- package/src/lib/codex-transcript-renderer.ts +49 -61
- package/src/lib/concurrency.ts +15 -3
- package/src/lib/conversation-api.ts +34 -31
- package/src/lib/conversation-data/adapter-helpers.ts +25 -3
- package/src/lib/conversation-data/antigravity-adapter.ts +54 -29
- package/src/lib/conversation-data/claude-code-adapter.ts +32 -6
- package/src/lib/conversation-data/codex-adapter.ts +91 -80
- package/src/lib/conversation-data/cursor-adapter.ts +48 -34
- package/src/lib/conversation-data/grok-adapter.ts +19 -6
- package/src/lib/conversation-data/index.ts +29 -10
- package/src/lib/conversation-data/kiro-adapter.ts +18 -5
- package/src/lib/conversation-data/message-selector.ts +2 -5
- package/src/lib/conversation-data/opencode-adapter.ts +43 -15
- package/src/lib/conversation-data/path-match.ts +1 -13
- package/src/lib/conversation-data/qoder-adapter.ts +23 -18
- package/src/lib/conversation-zip-export.ts +20 -2
- package/src/lib/cursor-db.ts +34 -23
- 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 +76 -34
- package/src/lib/grok-transcript-phase.ts +11 -2
- package/src/lib/grok-transcript.ts +15 -2
- package/src/lib/kiro-db.ts +161 -68
- package/src/lib/kiro-transcript.ts +0 -4
- package/src/lib/opencode-db.ts +4 -8
- package/src/lib/opencode-think-tags.ts +17 -3
- 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 +51 -15
- package/src/lib/sqlite-error.ts +31 -5
- package/src/lib/transcript-load-limiter.ts +8 -3
- package/src/lib/ui-cache.ts +152 -17
- package/src/lib/ui-export-archive.ts +14 -55
- 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
|
@@ -12,24 +12,19 @@ import { LoadingPanel } from '#/components/loading-panel';
|
|
|
12
12
|
import { MetadataSection } from '#/components/metadata-section';
|
|
13
13
|
import { MetricCard } from '#/components/metric-card';
|
|
14
14
|
import { PageHeader } from '#/components/page-header';
|
|
15
|
-
import {
|
|
15
|
+
import { RouteErrorPanel } from '#/components/route-error-panel';
|
|
16
16
|
import { DEFAULT_SHOW_USER_MESSAGES, TranscriptView } from '#/components/transcript-view';
|
|
17
17
|
import { Button } from '#/components/ui/button';
|
|
18
18
|
import { Checkbox } from '#/components/ui/checkbox';
|
|
19
19
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '#/components/ui/tabs';
|
|
20
20
|
import { downloadTextFile, downloadUrlFile } from '#/lib/download';
|
|
21
|
+
import type { ExportDialogOptions } from '#/lib/export-options';
|
|
21
22
|
import { formatDateTime, formatList, formatNumber } from '#/lib/formatters';
|
|
22
|
-
import { grokSessionDetailQueryOptions } from '#/lib/grok-queries';
|
|
23
|
+
import { grokSessionDetailQueryOptions, grokWorkspacesQueryOptions } from '#/lib/grok-queries';
|
|
23
24
|
import { deleteGrokSessionFn, exportGrokSessionFn } from '#/lib/grok-server';
|
|
24
25
|
import { getGrokThreadTranscriptStats, grokTranscriptToThreadEvents } from '#/lib/grok-transcript-events';
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
includeCommentary: boolean;
|
|
28
|
-
includeMetadata: boolean;
|
|
29
|
-
includeTools: boolean;
|
|
30
|
-
outputFormat: 'md' | 'txt';
|
|
31
|
-
zipArchive: boolean;
|
|
32
|
-
};
|
|
26
|
+
import { RouteStateResetBoundary } from '#/lib/route-state-reset';
|
|
27
|
+
import { shouldNavigateToSourceIndexAfterDelete } from '#/lib/workspace-delete-navigation';
|
|
33
28
|
|
|
34
29
|
type TranscriptControlsProps = {
|
|
35
30
|
rawJsonDisabled?: boolean;
|
|
@@ -46,7 +41,7 @@ type TranscriptControlsProps = {
|
|
|
46
41
|
};
|
|
47
42
|
|
|
48
43
|
const GrokSessionDetailErrorComponent = ({ error }: { error: Error }) => {
|
|
49
|
-
return <
|
|
44
|
+
return <RouteErrorPanel error={error} title="Failed to load Grok session" />;
|
|
50
45
|
};
|
|
51
46
|
|
|
52
47
|
const buildSessionMetadata = (detail: GrokSessionTranscript) => [
|
|
@@ -205,6 +200,17 @@ const GrokSessionDetailPage = () => {
|
|
|
205
200
|
queryClient.invalidateQueries({ queryKey: ['grok-sessions', detail.session.workspaceKey] }),
|
|
206
201
|
queryClient.invalidateQueries({ queryKey: ['grok-session', detail.session.sessionId] }),
|
|
207
202
|
]);
|
|
203
|
+
const workspaces = await queryClient.fetchQuery(grokWorkspacesQueryOptions());
|
|
204
|
+
if (
|
|
205
|
+
shouldNavigateToSourceIndexAfterDelete(
|
|
206
|
+
workspaces,
|
|
207
|
+
detail.session.workspaceKey,
|
|
208
|
+
(workspace) => workspace.key,
|
|
209
|
+
)
|
|
210
|
+
) {
|
|
211
|
+
navigate({ to: '/grok' });
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
208
214
|
navigate({
|
|
209
215
|
params: { workspaceKey: detail.session.workspaceKey },
|
|
210
216
|
to: '/grok/$workspaceKey',
|
|
@@ -378,7 +384,14 @@ const GrokSessionDetailPage = () => {
|
|
|
378
384
|
};
|
|
379
385
|
|
|
380
386
|
export const Route = createFileRoute('/grok-sessions/$sessionId')({
|
|
381
|
-
component:
|
|
387
|
+
component: () => {
|
|
388
|
+
const { sessionId } = Route.useParams();
|
|
389
|
+
return (
|
|
390
|
+
<RouteStateResetBoundary routeKey={sessionId}>
|
|
391
|
+
<GrokSessionDetailPage />
|
|
392
|
+
</RouteStateResetBoundary>
|
|
393
|
+
);
|
|
394
|
+
},
|
|
382
395
|
errorComponent: GrokSessionDetailErrorComponent,
|
|
383
396
|
loader: ({ context, params }) =>
|
|
384
397
|
context.queryClient.ensureQueryData(grokSessionDetailQueryOptions(params.sessionId)),
|
|
@@ -9,9 +9,10 @@ import { GrokSessionsTable } from '#/components/grok-sessions-table';
|
|
|
9
9
|
import { ListSearchInput } from '#/components/list-search-input';
|
|
10
10
|
import { LoadingPanel } from '#/components/loading-panel';
|
|
11
11
|
import { PageHeader } from '#/components/page-header';
|
|
12
|
-
import {
|
|
12
|
+
import { RouteErrorPanel } from '#/components/route-error-panel';
|
|
13
13
|
import { Button } from '#/components/ui/button';
|
|
14
14
|
import { downloadTextFile, downloadUrlFile } from '#/lib/download';
|
|
15
|
+
import { createExportSelectionMutationInput, type ExportSelectionMutationInput } from '#/lib/export-mutation';
|
|
15
16
|
import { grokSessionsQueryOptions, grokWorkspacesQueryOptions } from '#/lib/grok-queries';
|
|
16
17
|
import {
|
|
17
18
|
deleteGrokSessionFn,
|
|
@@ -22,14 +23,6 @@ import {
|
|
|
22
23
|
import { matchesTextQuery } from '#/lib/text-filter';
|
|
23
24
|
import { isWorkspaceEmptiedByDelete } from '#/lib/workspace-delete-navigation';
|
|
24
25
|
|
|
25
|
-
type ExportDialogOptions = {
|
|
26
|
-
includeCommentary: boolean;
|
|
27
|
-
includeMetadata: boolean;
|
|
28
|
-
includeTools: boolean;
|
|
29
|
-
outputFormat: 'md' | 'txt';
|
|
30
|
-
zipArchive: boolean;
|
|
31
|
-
};
|
|
32
|
-
|
|
33
26
|
type PendingSessionDelete = {
|
|
34
27
|
scope: 'all' | 'selected';
|
|
35
28
|
sessions: GrokSessionSummary[];
|
|
@@ -104,7 +97,7 @@ export const Route = createFileRoute('/grok/$workspaceKey')({
|
|
|
104
97
|
});
|
|
105
98
|
|
|
106
99
|
function GrokWorkspaceErrorComponent({ error }: { error: Error }) {
|
|
107
|
-
return <
|
|
100
|
+
return <RouteErrorPanel error={error} title="Failed to load Grok workspace" />;
|
|
108
101
|
}
|
|
109
102
|
|
|
110
103
|
function GrokWorkspacePage() {
|
|
@@ -120,20 +113,16 @@ function GrokWorkspacePage() {
|
|
|
120
113
|
const deferredSearch = useDeferredValue(searchInput);
|
|
121
114
|
|
|
122
115
|
const exportMutation = useMutation({
|
|
123
|
-
mutationFn: async (options:
|
|
124
|
-
if (!pendingExport) {
|
|
125
|
-
throw new Error('No Grok session selected for export');
|
|
126
|
-
}
|
|
127
|
-
|
|
116
|
+
mutationFn: async ({ ids, options }: ExportSelectionMutationInput) => {
|
|
128
117
|
const download =
|
|
129
|
-
|
|
118
|
+
ids.length === 1
|
|
130
119
|
? await exportGrokSessionFn({
|
|
131
120
|
data: {
|
|
132
121
|
includeCommentary: options.includeCommentary,
|
|
133
122
|
includeMetadata: options.includeMetadata,
|
|
134
123
|
includeTools: options.includeTools,
|
|
135
124
|
outputFormat: options.outputFormat,
|
|
136
|
-
sessionId:
|
|
125
|
+
sessionId: ids[0]!,
|
|
137
126
|
zipArchive: options.zipArchive,
|
|
138
127
|
},
|
|
139
128
|
})
|
|
@@ -143,7 +132,7 @@ function GrokWorkspacePage() {
|
|
|
143
132
|
includeMetadata: options.includeMetadata,
|
|
144
133
|
includeTools: options.includeTools,
|
|
145
134
|
outputFormat: options.outputFormat,
|
|
146
|
-
sessionIds:
|
|
135
|
+
sessionIds: [...ids],
|
|
147
136
|
zipArchive: options.zipArchive,
|
|
148
137
|
},
|
|
149
138
|
});
|
|
@@ -164,13 +153,7 @@ function GrokWorkspacePage() {
|
|
|
164
153
|
sessionIds.length === 1
|
|
165
154
|
? deleteGrokSessionFn({ data: { sessionId: sessionIds[0]! } })
|
|
166
155
|
: deleteGrokSessionsFn({ data: { sessionIds } }),
|
|
167
|
-
|
|
168
|
-
const workspaceEmptied = isWorkspaceEmptiedByDelete(sessions, sessionIds, (session) => session.sessionId);
|
|
169
|
-
setPendingDelete(null);
|
|
170
|
-
if (workspaceEmptied) {
|
|
171
|
-
await navigate({ to: '/grok' });
|
|
172
|
-
}
|
|
173
|
-
|
|
156
|
+
onSettled: async (_result, _error, sessionIds) => {
|
|
174
157
|
await Promise.all([
|
|
175
158
|
queryClient.invalidateQueries({ queryKey: ['grok-workspaces'] }),
|
|
176
159
|
queryClient.invalidateQueries({ queryKey: ['grok-sessions', workspace.key] }),
|
|
@@ -179,6 +162,13 @@ function GrokWorkspacePage() {
|
|
|
179
162
|
),
|
|
180
163
|
]);
|
|
181
164
|
},
|
|
165
|
+
onSuccess: async (_result, sessionIds) => {
|
|
166
|
+
const workspaceEmptied = isWorkspaceEmptiedByDelete(sessions, sessionIds, (session) => session.sessionId);
|
|
167
|
+
setPendingDelete(null);
|
|
168
|
+
if (workspaceEmptied) {
|
|
169
|
+
await navigate({ to: '/grok' });
|
|
170
|
+
}
|
|
171
|
+
},
|
|
182
172
|
});
|
|
183
173
|
|
|
184
174
|
const visibleSessions = useMemo(
|
|
@@ -263,7 +253,11 @@ function GrokWorkspacePage() {
|
|
|
263
253
|
open={pendingExport !== null}
|
|
264
254
|
pending={exportMutation.isPending}
|
|
265
255
|
title={pendingExport ? `Export ${pendingExport.label}` : 'Export session'}
|
|
266
|
-
onExport={(options) =>
|
|
256
|
+
onExport={(options) => {
|
|
257
|
+
if (pendingExport) {
|
|
258
|
+
exportMutation.mutate(createExportSelectionMutationInput(pendingExport.sessionIds, options));
|
|
259
|
+
}
|
|
260
|
+
}}
|
|
267
261
|
onOpenChange={(open) => {
|
|
268
262
|
if (!open) {
|
|
269
263
|
setPendingExport(null);
|
|
@@ -4,12 +4,12 @@ import { useDeferredValue, useState } from 'react';
|
|
|
4
4
|
import { GrokWorkspacesTable } from '#/components/grok-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 { grokWorkspacesQueryOptions } from '#/lib/grok-queries';
|
|
9
9
|
import { matchesTextQuery } from '#/lib/text-filter';
|
|
10
10
|
|
|
11
11
|
const GrokErrorComponent = ({ error }: { error: Error }) => {
|
|
12
|
-
return <
|
|
12
|
+
return <RouteErrorPanel error={error} title="Failed to load Grok workspaces" />;
|
|
13
13
|
};
|
|
14
14
|
|
|
15
15
|
const GrokPage = () => {
|
|
@@ -3,6 +3,7 @@ import { createFileRoute, Link } from '@tanstack/react-router';
|
|
|
3
3
|
import { MetricCard } from '#/components/metric-card';
|
|
4
4
|
import { PageHeader } from '#/components/page-header';
|
|
5
5
|
import { RecentThreadsList } from '#/components/recent-threads-list';
|
|
6
|
+
import { RouteErrorPanel } from '#/components/route-error-panel';
|
|
6
7
|
import { dashboardQueryOptions } from '#/lib/codex-queries';
|
|
7
8
|
import { formatNumber, formatTokens } from '#/lib/formatters';
|
|
8
9
|
|
|
@@ -12,24 +13,7 @@ export const Route = createFileRoute('/')({
|
|
|
12
13
|
});
|
|
13
14
|
|
|
14
15
|
function DashboardErrorComponent({ error }: { error: Error }) {
|
|
15
|
-
|
|
16
|
-
return (
|
|
17
|
-
<div className="rounded-xl border border-[var(--border)] bg-[var(--panel)] px-6 py-10 text-center">
|
|
18
|
-
<p className="font-medium text-[var(--destructive)] text-sm">
|
|
19
|
-
{isSqlite ? 'Database unavailable' : 'Failed to load dashboard'}
|
|
20
|
-
</p>
|
|
21
|
-
<p className="mt-2 text-[var(--muted-foreground)] text-sm">
|
|
22
|
-
{isSqlite ? 'Codex may have an exclusive lock on the database. Reload to retry.' : error.message}
|
|
23
|
-
</p>
|
|
24
|
-
<button
|
|
25
|
-
className="mt-4 text-[var(--accent)] text-sm underline-offset-2 hover:underline"
|
|
26
|
-
type="button"
|
|
27
|
-
onClick={() => window.location.reload()}
|
|
28
|
-
>
|
|
29
|
-
Reload
|
|
30
|
-
</button>
|
|
31
|
-
</div>
|
|
32
|
-
);
|
|
16
|
+
return <RouteErrorPanel error={error} title="Failed to load dashboard" />;
|
|
33
17
|
}
|
|
34
18
|
|
|
35
19
|
Route.update({ errorComponent: DashboardErrorComponent });
|
|
@@ -12,24 +12,19 @@ import { LoadingPanel } from '#/components/loading-panel';
|
|
|
12
12
|
import { MetadataSection } from '#/components/metadata-section';
|
|
13
13
|
import { MetricCard } from '#/components/metric-card';
|
|
14
14
|
import { PageHeader } from '#/components/page-header';
|
|
15
|
-
import {
|
|
15
|
+
import { RouteErrorPanel } from '#/components/route-error-panel';
|
|
16
16
|
import { DEFAULT_SHOW_USER_MESSAGES, TranscriptView } from '#/components/transcript-view';
|
|
17
17
|
import { Button } from '#/components/ui/button';
|
|
18
18
|
import { Checkbox } from '#/components/ui/checkbox';
|
|
19
19
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '#/components/ui/tabs';
|
|
20
20
|
import { downloadTextFile, downloadUrlFile } from '#/lib/download';
|
|
21
|
+
import type { ExportDialogOptions } from '#/lib/export-options';
|
|
21
22
|
import { formatDateTime, formatList, formatNumber } from '#/lib/formatters';
|
|
22
|
-
import { kiroSessionDetailQueryOptions } from '#/lib/kiro-queries';
|
|
23
|
+
import { kiroSessionDetailQueryOptions, kiroWorkspacesQueryOptions } from '#/lib/kiro-queries';
|
|
23
24
|
import { deleteKiroSessionFn, exportKiroSessionFn } from '#/lib/kiro-server';
|
|
24
25
|
import { getKiroThreadTranscriptStats, kiroTranscriptToThreadEvents } from '#/lib/kiro-transcript-events';
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
includeCommentary: boolean;
|
|
28
|
-
includeMetadata: boolean;
|
|
29
|
-
includeTools: boolean;
|
|
30
|
-
outputFormat: 'md' | 'txt';
|
|
31
|
-
zipArchive: boolean;
|
|
32
|
-
};
|
|
26
|
+
import { RouteStateResetBoundary } from '#/lib/route-state-reset';
|
|
27
|
+
import { shouldNavigateToSourceIndexAfterDelete } from '#/lib/workspace-delete-navigation';
|
|
33
28
|
|
|
34
29
|
type TranscriptControlsProps = {
|
|
35
30
|
rawJsonDisabled?: boolean;
|
|
@@ -46,7 +41,7 @@ type TranscriptControlsProps = {
|
|
|
46
41
|
};
|
|
47
42
|
|
|
48
43
|
const KiroSessionDetailErrorComponent = ({ error }: { error: Error }) => {
|
|
49
|
-
return <
|
|
44
|
+
return <RouteErrorPanel error={error} title="Failed to load Kiro session" />;
|
|
50
45
|
};
|
|
51
46
|
|
|
52
47
|
const getKiroModelLabel = (detail: KiroSessionTranscript): string => {
|
|
@@ -216,6 +211,17 @@ const KiroSessionDetailPage = () => {
|
|
|
216
211
|
queryClient.invalidateQueries({ queryKey: ['kiro-sessions', detail.session.workspaceKey] }),
|
|
217
212
|
queryClient.invalidateQueries({ queryKey: ['kiro-session', detail.session.sessionId] }),
|
|
218
213
|
]);
|
|
214
|
+
const workspaces = await queryClient.fetchQuery(kiroWorkspacesQueryOptions());
|
|
215
|
+
if (
|
|
216
|
+
shouldNavigateToSourceIndexAfterDelete(
|
|
217
|
+
workspaces,
|
|
218
|
+
detail.session.workspaceKey,
|
|
219
|
+
(workspace) => workspace.key,
|
|
220
|
+
)
|
|
221
|
+
) {
|
|
222
|
+
navigate({ to: '/kiro' });
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
219
225
|
navigate({
|
|
220
226
|
params: { workspaceKey: detail.session.workspaceKey },
|
|
221
227
|
to: '/kiro/$workspaceKey',
|
|
@@ -383,7 +389,14 @@ const KiroSessionDetailPage = () => {
|
|
|
383
389
|
};
|
|
384
390
|
|
|
385
391
|
export const Route = createFileRoute('/kiro-sessions/$sessionId')({
|
|
386
|
-
component:
|
|
392
|
+
component: () => {
|
|
393
|
+
const { sessionId } = Route.useParams();
|
|
394
|
+
return (
|
|
395
|
+
<RouteStateResetBoundary routeKey={sessionId}>
|
|
396
|
+
<KiroSessionDetailPage />
|
|
397
|
+
</RouteStateResetBoundary>
|
|
398
|
+
);
|
|
399
|
+
},
|
|
387
400
|
errorComponent: KiroSessionDetailErrorComponent,
|
|
388
401
|
loader: ({ context, params }) =>
|
|
389
402
|
context.queryClient.ensureQueryData(kiroSessionDetailQueryOptions(params.sessionId)),
|
|
@@ -9,9 +9,10 @@ import { KiroSessionsTable } from '#/components/kiro-sessions-table';
|
|
|
9
9
|
import { ListSearchInput } from '#/components/list-search-input';
|
|
10
10
|
import { LoadingPanel } from '#/components/loading-panel';
|
|
11
11
|
import { PageHeader } from '#/components/page-header';
|
|
12
|
-
import {
|
|
12
|
+
import { RouteErrorPanel } from '#/components/route-error-panel';
|
|
13
13
|
import { Button } from '#/components/ui/button';
|
|
14
14
|
import { downloadTextFile, downloadUrlFile } from '#/lib/download';
|
|
15
|
+
import { createExportSelectionMutationInput, type ExportSelectionMutationInput } from '#/lib/export-mutation';
|
|
15
16
|
import { kiroSessionsQueryOptions, kiroWorkspacesQueryOptions } from '#/lib/kiro-queries';
|
|
16
17
|
import {
|
|
17
18
|
deleteKiroSessionFn,
|
|
@@ -22,14 +23,6 @@ import {
|
|
|
22
23
|
import { matchesTextQuery } from '#/lib/text-filter';
|
|
23
24
|
import { isWorkspaceEmptiedByDelete } from '#/lib/workspace-delete-navigation';
|
|
24
25
|
|
|
25
|
-
type ExportDialogOptions = {
|
|
26
|
-
includeCommentary: boolean;
|
|
27
|
-
includeMetadata: boolean;
|
|
28
|
-
includeTools: boolean;
|
|
29
|
-
outputFormat: 'md' | 'txt';
|
|
30
|
-
zipArchive: boolean;
|
|
31
|
-
};
|
|
32
|
-
|
|
33
26
|
type PendingSessionDelete = {
|
|
34
27
|
scope: 'all' | 'selected';
|
|
35
28
|
sessions: KiroSessionSummary[];
|
|
@@ -93,7 +86,7 @@ const getDeleteTitle = (pendingDelete: PendingSessionDelete | null) => {
|
|
|
93
86
|
};
|
|
94
87
|
|
|
95
88
|
const KiroWorkspaceErrorComponent = ({ error }: { error: Error }) => {
|
|
96
|
-
return <
|
|
89
|
+
return <RouteErrorPanel error={error} title="Failed to load Kiro workspace" />;
|
|
97
90
|
};
|
|
98
91
|
|
|
99
92
|
const KiroWorkspacePage = () => {
|
|
@@ -109,20 +102,16 @@ const KiroWorkspacePage = () => {
|
|
|
109
102
|
const deferredSearch = useDeferredValue(searchInput);
|
|
110
103
|
|
|
111
104
|
const exportMutation = useMutation({
|
|
112
|
-
mutationFn: async (options:
|
|
113
|
-
if (!pendingExport) {
|
|
114
|
-
throw new Error('No Kiro session selected for export');
|
|
115
|
-
}
|
|
116
|
-
|
|
105
|
+
mutationFn: async ({ ids, options }: ExportSelectionMutationInput) => {
|
|
117
106
|
const download =
|
|
118
|
-
|
|
107
|
+
ids.length === 1
|
|
119
108
|
? await exportKiroSessionFn({
|
|
120
109
|
data: {
|
|
121
110
|
includeCommentary: options.includeCommentary,
|
|
122
111
|
includeMetadata: options.includeMetadata,
|
|
123
112
|
includeTools: options.includeTools,
|
|
124
113
|
outputFormat: options.outputFormat,
|
|
125
|
-
sessionId:
|
|
114
|
+
sessionId: ids[0]!,
|
|
126
115
|
zipArchive: options.zipArchive,
|
|
127
116
|
},
|
|
128
117
|
})
|
|
@@ -132,7 +121,7 @@ const KiroWorkspacePage = () => {
|
|
|
132
121
|
includeMetadata: options.includeMetadata,
|
|
133
122
|
includeTools: options.includeTools,
|
|
134
123
|
outputFormat: options.outputFormat,
|
|
135
|
-
sessionIds:
|
|
124
|
+
sessionIds: [...ids],
|
|
136
125
|
zipArchive: options.zipArchive,
|
|
137
126
|
},
|
|
138
127
|
});
|
|
@@ -153,13 +142,7 @@ const KiroWorkspacePage = () => {
|
|
|
153
142
|
sessionIds.length === 1
|
|
154
143
|
? deleteKiroSessionFn({ data: { sessionId: sessionIds[0]! } })
|
|
155
144
|
: deleteKiroSessionsFn({ data: { sessionIds } }),
|
|
156
|
-
|
|
157
|
-
const workspaceEmptied = isWorkspaceEmptiedByDelete(sessions, sessionIds, (session) => session.sessionId);
|
|
158
|
-
setPendingDelete(null);
|
|
159
|
-
if (workspaceEmptied) {
|
|
160
|
-
await navigate({ to: '/kiro' });
|
|
161
|
-
}
|
|
162
|
-
|
|
145
|
+
onSettled: async (_result, _error, sessionIds) => {
|
|
163
146
|
await Promise.all([
|
|
164
147
|
queryClient.invalidateQueries({ queryKey: ['kiro-workspaces'] }),
|
|
165
148
|
queryClient.invalidateQueries({ queryKey: ['kiro-sessions', workspace.key] }),
|
|
@@ -168,6 +151,13 @@ const KiroWorkspacePage = () => {
|
|
|
168
151
|
),
|
|
169
152
|
]);
|
|
170
153
|
},
|
|
154
|
+
onSuccess: async (_result, sessionIds) => {
|
|
155
|
+
const workspaceEmptied = isWorkspaceEmptiedByDelete(sessions, sessionIds, (session) => session.sessionId);
|
|
156
|
+
setPendingDelete(null);
|
|
157
|
+
if (workspaceEmptied) {
|
|
158
|
+
await navigate({ to: '/kiro' });
|
|
159
|
+
}
|
|
160
|
+
},
|
|
171
161
|
});
|
|
172
162
|
|
|
173
163
|
const visibleSessions = useMemo(
|
|
@@ -242,11 +232,22 @@ const KiroWorkspacePage = () => {
|
|
|
242
232
|
/>
|
|
243
233
|
|
|
244
234
|
<ExportDialog
|
|
235
|
+
errorMessage={
|
|
236
|
+
exportMutation.isError
|
|
237
|
+
? exportMutation.error instanceof Error
|
|
238
|
+
? exportMutation.error.message
|
|
239
|
+
: 'Session export failed'
|
|
240
|
+
: null
|
|
241
|
+
}
|
|
245
242
|
forceZipArchive={pendingExport ? pendingExport.sessionIds.length > 1 : false}
|
|
246
243
|
open={pendingExport !== null}
|
|
247
244
|
pending={exportMutation.isPending}
|
|
248
245
|
title={pendingExport ? `Export ${pendingExport.label}` : 'Export session'}
|
|
249
|
-
onExport={(options) =>
|
|
246
|
+
onExport={(options) => {
|
|
247
|
+
if (pendingExport) {
|
|
248
|
+
exportMutation.mutate(createExportSelectionMutationInput(pendingExport.sessionIds, options));
|
|
249
|
+
}
|
|
250
|
+
}}
|
|
250
251
|
onOpenChange={(open) => {
|
|
251
252
|
if (!open) {
|
|
252
253
|
setPendingExport(null);
|
|
@@ -255,12 +256,6 @@ const KiroWorkspacePage = () => {
|
|
|
255
256
|
}}
|
|
256
257
|
/>
|
|
257
258
|
|
|
258
|
-
{exportMutation.isError ? (
|
|
259
|
-
<p className="text-[var(--destructive)] text-sm">
|
|
260
|
-
{exportMutation.error instanceof Error ? exportMutation.error.message : 'Session export failed'}
|
|
261
|
-
</p>
|
|
262
|
-
) : null}
|
|
263
|
-
|
|
264
259
|
<DeleteConfirmDialog
|
|
265
260
|
confirmLabel={getDeleteConfirmLabel(pendingDelete, deleteMutation.isPending)}
|
|
266
261
|
description={getDeleteDescription(pendingDelete)}
|
|
@@ -4,12 +4,12 @@ import { useDeferredValue, useState } from 'react';
|
|
|
4
4
|
import { KiroWorkspacesTable } from '#/components/kiro-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 { kiroWorkspacesQueryOptions } from '#/lib/kiro-queries';
|
|
9
9
|
import { matchesTextQuery } from '#/lib/text-filter';
|
|
10
10
|
|
|
11
11
|
const KiroErrorComponent = ({ error }: { error: Error }) => {
|
|
12
|
-
return <
|
|
12
|
+
return <RouteErrorPanel error={error} title="Failed to load Kiro workspaces" />;
|
|
13
13
|
};
|
|
14
14
|
|
|
15
15
|
const KiroPage = () => {
|
|
@@ -1,6 +1,6 @@
|
|
|
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,
|
|
3
|
+
import { useMutation, useQueryClient, useSuspenseQuery } from '@tanstack/react-query';
|
|
4
4
|
import { createFileRoute, Link, useNavigate } from '@tanstack/react-router';
|
|
5
5
|
import { Download, Trash2 } from 'lucide-react';
|
|
6
6
|
import { useMemo, useState } from 'react';
|
|
@@ -12,24 +12,19 @@ import { LoadingPanel } from '#/components/loading-panel';
|
|
|
12
12
|
import { MetadataSection } from '#/components/metadata-section';
|
|
13
13
|
import { MetricCard } from '#/components/metric-card';
|
|
14
14
|
import { PageHeader } from '#/components/page-header';
|
|
15
|
-
import {
|
|
15
|
+
import { RouteErrorPanel } from '#/components/route-error-panel';
|
|
16
16
|
import { DEFAULT_SHOW_USER_MESSAGES, TranscriptView } from '#/components/transcript-view';
|
|
17
17
|
import { Button } from '#/components/ui/button';
|
|
18
18
|
import { Checkbox } from '#/components/ui/checkbox';
|
|
19
19
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '#/components/ui/tabs';
|
|
20
20
|
import { downloadTextFile, downloadUrlFile } from '#/lib/download';
|
|
21
|
+
import type { ExportDialogOptions } from '#/lib/export-options';
|
|
21
22
|
import { formatDateTime, formatList, formatNumber, formatTokens } from '#/lib/formatters';
|
|
22
|
-
import { openCodeSessionDetailQueryOptions } from '#/lib/opencode-queries';
|
|
23
|
+
import { openCodeSessionDetailQueryOptions, openCodeWorkspacesQueryOptions } from '#/lib/opencode-queries';
|
|
23
24
|
import { deleteOpenCodeSessionFn, exportOpenCodeSessionFn } from '#/lib/opencode-server';
|
|
24
25
|
import { getOpenCodeThreadTranscriptStats, openCodeTranscriptToThreadEvents } from '#/lib/opencode-transcript-events';
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
includeCommentary: boolean;
|
|
28
|
-
includeMetadata: boolean;
|
|
29
|
-
includeTools: boolean;
|
|
30
|
-
outputFormat: 'md' | 'txt';
|
|
31
|
-
zipArchive: boolean;
|
|
32
|
-
};
|
|
26
|
+
import { RouteStateResetBoundary } from '#/lib/route-state-reset';
|
|
27
|
+
import { shouldNavigateToSourceIndexAfterDelete } from '#/lib/workspace-delete-navigation';
|
|
33
28
|
|
|
34
29
|
type TranscriptControlsProps = {
|
|
35
30
|
rawJsonDisabled?: boolean;
|
|
@@ -46,11 +41,9 @@ type TranscriptControlsProps = {
|
|
|
46
41
|
};
|
|
47
42
|
|
|
48
43
|
const OpenCodeSessionDetailErrorComponent = ({ error }: { error: Error }) => {
|
|
49
|
-
return <
|
|
44
|
+
return <RouteErrorPanel error={error} title="Failed to load OpenCode session" />;
|
|
50
45
|
};
|
|
51
46
|
|
|
52
|
-
const toError = (error: unknown) => (error instanceof Error ? error : new Error(String(error)));
|
|
53
|
-
|
|
54
47
|
const buildSessionMetadata = (detail: OpenCodeSessionTranscript) => [
|
|
55
48
|
{ label: 'Session ID', value: <span data-mono="true">{detail.session.sessionId}</span> },
|
|
56
49
|
{
|
|
@@ -165,8 +158,7 @@ const OpenCodeSessionDetailPage = () => {
|
|
|
165
158
|
const navigate = useNavigate();
|
|
166
159
|
const queryClient = useQueryClient();
|
|
167
160
|
const params = Route.useParams();
|
|
168
|
-
const
|
|
169
|
-
const detail = detailQuery.data ?? null;
|
|
161
|
+
const detail = useSuspenseQuery(openCodeSessionDetailQueryOptions(params.sessionId)).data;
|
|
170
162
|
const [deleteOpen, setDeleteOpen] = useState(false);
|
|
171
163
|
const [pendingExport, setPendingExport] = useState(false);
|
|
172
164
|
const [showToolCalls, setShowToolCalls] = useState(false);
|
|
@@ -223,6 +215,17 @@ const OpenCodeSessionDetailPage = () => {
|
|
|
223
215
|
queryClient.invalidateQueries({ queryKey: ['opencode-sessions', detail.session.workspaceKey] }),
|
|
224
216
|
queryClient.invalidateQueries({ queryKey: ['opencode-session', detail.session.sessionId] }),
|
|
225
217
|
]);
|
|
218
|
+
const workspaces = await queryClient.fetchQuery(openCodeWorkspacesQueryOptions());
|
|
219
|
+
if (
|
|
220
|
+
shouldNavigateToSourceIndexAfterDelete(
|
|
221
|
+
workspaces,
|
|
222
|
+
detail.session.workspaceKey,
|
|
223
|
+
(workspace) => workspace.key,
|
|
224
|
+
)
|
|
225
|
+
) {
|
|
226
|
+
navigate({ to: '/opencode' });
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
226
229
|
navigate({
|
|
227
230
|
params: { workspaceKey: detail.session.workspaceKey },
|
|
228
231
|
to: '/opencode/$workspaceKey',
|
|
@@ -230,25 +233,6 @@ const OpenCodeSessionDetailPage = () => {
|
|
|
230
233
|
},
|
|
231
234
|
});
|
|
232
235
|
|
|
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
|
-
|
|
252
236
|
return (
|
|
253
237
|
<div className="space-y-6">
|
|
254
238
|
<PageHeader
|
|
@@ -409,8 +393,17 @@ const OpenCodeSessionDetailPage = () => {
|
|
|
409
393
|
};
|
|
410
394
|
|
|
411
395
|
export const Route = createFileRoute('/opencode-sessions/$sessionId')({
|
|
412
|
-
component:
|
|
396
|
+
component: () => {
|
|
397
|
+
const { sessionId } = Route.useParams();
|
|
398
|
+
return (
|
|
399
|
+
<RouteStateResetBoundary routeKey={sessionId}>
|
|
400
|
+
<OpenCodeSessionDetailPage />
|
|
401
|
+
</RouteStateResetBoundary>
|
|
402
|
+
);
|
|
403
|
+
},
|
|
413
404
|
errorComponent: OpenCodeSessionDetailErrorComponent,
|
|
405
|
+
loader: ({ context, params }) =>
|
|
406
|
+
context.queryClient.ensureQueryData(openCodeSessionDetailQueryOptions(params.sessionId)),
|
|
414
407
|
pendingComponent: () => (
|
|
415
408
|
<LoadingPanel
|
|
416
409
|
description="Loading the OpenCode transcript, parts, and session metadata."
|