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