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
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
import type { GrokSessionSummary, GrokWorkspaceGroup } from '@spiracha/lib/grok-exporter-types';
|
|
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';
|
|
7
|
+
import { ExportDialog } from '#/components/export-dialog';
|
|
8
|
+
import { GrokSessionsTable } from '#/components/grok-sessions-table';
|
|
9
|
+
import { ListSearchInput } from '#/components/list-search-input';
|
|
10
|
+
import { LoadingPanel } from '#/components/loading-panel';
|
|
11
|
+
import { PageHeader } from '#/components/page-header';
|
|
12
|
+
import { RouteErrorPanel } from '#/components/route-error-panel';
|
|
13
|
+
import { Button } from '#/components/ui/button';
|
|
14
|
+
import { downloadTextFile, downloadUrlFile } from '#/lib/download';
|
|
15
|
+
import { createExportSelectionMutationInput, type ExportSelectionMutationInput } from '#/lib/export-mutation';
|
|
16
|
+
import { grokSessionsQueryOptions, grokWorkspacesQueryOptions } from '#/lib/grok-queries';
|
|
17
|
+
import {
|
|
18
|
+
deleteGrokSessionFn,
|
|
19
|
+
deleteGrokSessionsFn,
|
|
20
|
+
exportGrokSessionFn,
|
|
21
|
+
exportGrokSessionsFn,
|
|
22
|
+
} from '#/lib/grok-server';
|
|
23
|
+
import { matchesTextQuery } from '#/lib/text-filter';
|
|
24
|
+
import { isWorkspaceEmptiedByDelete } from '#/lib/workspace-delete-navigation';
|
|
25
|
+
|
|
26
|
+
type PendingSessionDelete = {
|
|
27
|
+
scope: 'all' | 'selected';
|
|
28
|
+
sessions: GrokSessionSummary[];
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
type PendingSessionExport = {
|
|
32
|
+
label: string;
|
|
33
|
+
sessionIds: string[];
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const findWorkspaceOrThrow = (workspaces: GrokWorkspaceGroup[], workspaceKey: string) => {
|
|
37
|
+
const workspace = workspaces.find((candidate) => candidate.key === workspaceKey);
|
|
38
|
+
if (!workspace) {
|
|
39
|
+
throw new Error(`Grok workspace not found: ${workspaceKey}`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return workspace;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const buildSessionExport = (selectedSessions: GrokSessionSummary[]): 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 Grok sessions from local history.';
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (pendingDelete.scope === 'all') {
|
|
68
|
+
return `Permanently delete all ${pendingDelete.sessions.length} Grok sessions in this workspace from local history. This removes their session directories and transcript files under ~/.grok/sessions.`;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (pendingDelete.sessions.length === 1) {
|
|
72
|
+
return `Permanently delete "${pendingDelete.sessions[0]!.title}" from Grok history. This removes the session directory and transcript files under ~/.grok/sessions.`;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return `Permanently delete ${pendingDelete.sessions.length} selected Grok sessions from local history. This removes their session directories and transcript files under ~/.grok/sessions.`;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const getDeleteTitle = (pendingDelete: PendingSessionDelete | null) => {
|
|
79
|
+
if (pendingDelete?.scope === 'all') {
|
|
80
|
+
return `Delete all ${pendingDelete.sessions.length} Grok sessions?`;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return pendingDelete && pendingDelete.sessions.length > 1
|
|
84
|
+
? `Delete ${pendingDelete.sessions.length} Grok sessions?`
|
|
85
|
+
: 'Delete this Grok session?';
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
export const Route = createFileRoute('/grok/$workspaceKey')({
|
|
89
|
+
component: GrokWorkspacePage,
|
|
90
|
+
errorComponent: GrokWorkspaceErrorComponent,
|
|
91
|
+
loader: async ({ context, params }) => {
|
|
92
|
+
const workspaces = await context.queryClient.ensureQueryData(grokWorkspacesQueryOptions());
|
|
93
|
+
findWorkspaceOrThrow(workspaces, params.workspaceKey);
|
|
94
|
+
await context.queryClient.ensureQueryData(grokSessionsQueryOptions(params.workspaceKey));
|
|
95
|
+
},
|
|
96
|
+
pendingComponent: () => <LoadingPanel description="Loading Grok sessions." title="Loading workspace" />,
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
function GrokWorkspaceErrorComponent({ error }: { error: Error }) {
|
|
100
|
+
return <RouteErrorPanel error={error} title="Failed to load Grok workspace" />;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function GrokWorkspacePage() {
|
|
104
|
+
const navigate = useNavigate({ from: Route.fullPath });
|
|
105
|
+
const params = Route.useParams();
|
|
106
|
+
const queryClient = useQueryClient();
|
|
107
|
+
const workspaces = useSuspenseQuery(grokWorkspacesQueryOptions()).data;
|
|
108
|
+
const workspace = findWorkspaceOrThrow(workspaces, params.workspaceKey);
|
|
109
|
+
const sessions = useSuspenseQuery(grokSessionsQueryOptions(workspace.key)).data;
|
|
110
|
+
const [searchInput, setSearchInput] = useState('');
|
|
111
|
+
const [pendingDelete, setPendingDelete] = useState<PendingSessionDelete | null>(null);
|
|
112
|
+
const [pendingExport, setPendingExport] = useState<PendingSessionExport | null>(null);
|
|
113
|
+
const deferredSearch = useDeferredValue(searchInput);
|
|
114
|
+
|
|
115
|
+
const exportMutation = useMutation({
|
|
116
|
+
mutationFn: async ({ ids, options }: ExportSelectionMutationInput) => {
|
|
117
|
+
const download =
|
|
118
|
+
ids.length === 1
|
|
119
|
+
? await exportGrokSessionFn({
|
|
120
|
+
data: {
|
|
121
|
+
includeCommentary: options.includeCommentary,
|
|
122
|
+
includeMetadata: options.includeMetadata,
|
|
123
|
+
includeTools: options.includeTools,
|
|
124
|
+
outputFormat: options.outputFormat,
|
|
125
|
+
sessionId: ids[0]!,
|
|
126
|
+
zipArchive: options.zipArchive,
|
|
127
|
+
},
|
|
128
|
+
})
|
|
129
|
+
: await exportGrokSessionsFn({
|
|
130
|
+
data: {
|
|
131
|
+
includeCommentary: options.includeCommentary,
|
|
132
|
+
includeMetadata: options.includeMetadata,
|
|
133
|
+
includeTools: options.includeTools,
|
|
134
|
+
outputFormat: options.outputFormat,
|
|
135
|
+
sessionIds: [...ids],
|
|
136
|
+
zipArchive: options.zipArchive,
|
|
137
|
+
},
|
|
138
|
+
});
|
|
139
|
+
if (download.mode === 'download') {
|
|
140
|
+
downloadTextFile(download.fileName, download.content, download.mimeType);
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
await downloadUrlFile(download.fileName, download.downloadUrl);
|
|
145
|
+
},
|
|
146
|
+
onSuccess: () => {
|
|
147
|
+
setPendingExport(null);
|
|
148
|
+
},
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
const deleteMutation = useMutation({
|
|
152
|
+
mutationFn: async (sessionIds: string[]) =>
|
|
153
|
+
sessionIds.length === 1
|
|
154
|
+
? deleteGrokSessionFn({ data: { sessionId: sessionIds[0]! } })
|
|
155
|
+
: deleteGrokSessionsFn({ data: { sessionIds } }),
|
|
156
|
+
onSettled: async (_result, _error, sessionIds) => {
|
|
157
|
+
await Promise.all([
|
|
158
|
+
queryClient.invalidateQueries({ queryKey: ['grok-workspaces'] }),
|
|
159
|
+
queryClient.invalidateQueries({ queryKey: ['grok-sessions', workspace.key] }),
|
|
160
|
+
...sessionIds.map((sessionId) =>
|
|
161
|
+
queryClient.invalidateQueries({ queryKey: ['grok-session', sessionId] }),
|
|
162
|
+
),
|
|
163
|
+
]);
|
|
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
|
+
},
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
const visibleSessions = useMemo(
|
|
175
|
+
() =>
|
|
176
|
+
sessions.filter((session) =>
|
|
177
|
+
matchesTextQuery(deferredSearch, [
|
|
178
|
+
session.title,
|
|
179
|
+
session.sessionId,
|
|
180
|
+
session.agentName,
|
|
181
|
+
session.currentModelId,
|
|
182
|
+
session.modelLabel,
|
|
183
|
+
session.gitBranch,
|
|
184
|
+
]),
|
|
185
|
+
),
|
|
186
|
+
[deferredSearch, sessions],
|
|
187
|
+
);
|
|
188
|
+
const visibleSessionsById = useMemo(
|
|
189
|
+
() => new Map(visibleSessions.map((session) => [session.sessionId, session])),
|
|
190
|
+
[visibleSessions],
|
|
191
|
+
);
|
|
192
|
+
const lookupSelectedSessions = (sessionIds: string[]) =>
|
|
193
|
+
sessionIds
|
|
194
|
+
.map((sessionId) => visibleSessionsById.get(sessionId) ?? null)
|
|
195
|
+
.filter((session): session is GrokSessionSummary => session !== null);
|
|
196
|
+
const openExportForSessions = (selectedSessions: GrokSessionSummary[]) => {
|
|
197
|
+
if (selectedSessions.length === 0) {
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
setPendingExport(buildSessionExport(selectedSessions));
|
|
202
|
+
};
|
|
203
|
+
const openDeleteForSessions = (selectedSessions: GrokSessionSummary[], scope: PendingSessionDelete['scope']) => {
|
|
204
|
+
if (selectedSessions.length > 0) {
|
|
205
|
+
setPendingDelete({ scope, sessions: selectedSessions });
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
return (
|
|
210
|
+
<div className="space-y-6">
|
|
211
|
+
<PageHeader
|
|
212
|
+
actions={
|
|
213
|
+
<div className="flex flex-col gap-2 sm:flex-row">
|
|
214
|
+
<Button
|
|
215
|
+
className="rounded-full"
|
|
216
|
+
disabled={deleteMutation.isPending || sessions.length === 0}
|
|
217
|
+
type="button"
|
|
218
|
+
variant="destructive"
|
|
219
|
+
onClick={() => openDeleteForSessions(sessions, 'all')}
|
|
220
|
+
>
|
|
221
|
+
<Trash2 className="size-4" />
|
|
222
|
+
Delete all
|
|
223
|
+
</Button>
|
|
224
|
+
<ListSearchInput
|
|
225
|
+
placeholder="Search session title, id, model, or branch"
|
|
226
|
+
value={searchInput}
|
|
227
|
+
onValueChange={setSearchInput}
|
|
228
|
+
/>
|
|
229
|
+
</div>
|
|
230
|
+
}
|
|
231
|
+
eyebrow="Grok workspace"
|
|
232
|
+
subtitle="Inspect local Grok CLI sessions, reasoning summaries, tool calls, and exportable conversation text."
|
|
233
|
+
title={workspace.label}
|
|
234
|
+
/>
|
|
235
|
+
|
|
236
|
+
<GrokSessionsTable
|
|
237
|
+
sessions={visibleSessions}
|
|
238
|
+
onDeleteSession={(session) => openDeleteForSessions([session], 'selected')}
|
|
239
|
+
onDeleteSessions={(sessionIds) => openDeleteForSessions(lookupSelectedSessions(sessionIds), 'selected')}
|
|
240
|
+
onExportSession={(session) => openExportForSessions([session])}
|
|
241
|
+
onExportSessions={(sessionIds) => openExportForSessions(lookupSelectedSessions(sessionIds))}
|
|
242
|
+
/>
|
|
243
|
+
|
|
244
|
+
<ExportDialog
|
|
245
|
+
errorMessage={
|
|
246
|
+
exportMutation.isError
|
|
247
|
+
? exportMutation.error instanceof Error
|
|
248
|
+
? exportMutation.error.message
|
|
249
|
+
: 'Session export failed'
|
|
250
|
+
: null
|
|
251
|
+
}
|
|
252
|
+
forceZipArchive={pendingExport ? pendingExport.sessionIds.length > 1 : false}
|
|
253
|
+
open={pendingExport !== null}
|
|
254
|
+
pending={exportMutation.isPending}
|
|
255
|
+
title={pendingExport ? `Export ${pendingExport.label}` : 'Export session'}
|
|
256
|
+
onExport={(options) => {
|
|
257
|
+
if (pendingExport) {
|
|
258
|
+
exportMutation.mutate(createExportSelectionMutationInput(pendingExport.sessionIds, options));
|
|
259
|
+
}
|
|
260
|
+
}}
|
|
261
|
+
onOpenChange={(open) => {
|
|
262
|
+
if (!open) {
|
|
263
|
+
setPendingExport(null);
|
|
264
|
+
exportMutation.reset();
|
|
265
|
+
}
|
|
266
|
+
}}
|
|
267
|
+
/>
|
|
268
|
+
|
|
269
|
+
<DeleteConfirmDialog
|
|
270
|
+
confirmLabel={getDeleteConfirmLabel(pendingDelete, deleteMutation.isPending)}
|
|
271
|
+
description={getDeleteDescription(pendingDelete)}
|
|
272
|
+
errorMessage={
|
|
273
|
+
deleteMutation.isError
|
|
274
|
+
? deleteMutation.error instanceof Error
|
|
275
|
+
? deleteMutation.error.message
|
|
276
|
+
: 'Session delete failed'
|
|
277
|
+
: null
|
|
278
|
+
}
|
|
279
|
+
open={pendingDelete !== null}
|
|
280
|
+
title={getDeleteTitle(pendingDelete)}
|
|
281
|
+
onConfirm={() => {
|
|
282
|
+
if (pendingDelete) {
|
|
283
|
+
deleteMutation.mutate(pendingDelete.sessions.map((session) => session.sessionId));
|
|
284
|
+
}
|
|
285
|
+
}}
|
|
286
|
+
onOpenChange={(open) => {
|
|
287
|
+
if (!open) {
|
|
288
|
+
setPendingDelete(null);
|
|
289
|
+
deleteMutation.reset();
|
|
290
|
+
}
|
|
291
|
+
}}
|
|
292
|
+
/>
|
|
293
|
+
</div>
|
|
294
|
+
);
|
|
295
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { useSuspenseQuery } from '@tanstack/react-query';
|
|
2
|
+
import { createFileRoute } from '@tanstack/react-router';
|
|
3
|
+
import { useDeferredValue, useState } from 'react';
|
|
4
|
+
import { GrokWorkspacesTable } from '#/components/grok-workspaces-table';
|
|
5
|
+
import { ListSearchInput } from '#/components/list-search-input';
|
|
6
|
+
import { PageHeader } from '#/components/page-header';
|
|
7
|
+
import { RouteErrorPanel } from '#/components/route-error-panel';
|
|
8
|
+
import { grokWorkspacesQueryOptions } from '#/lib/grok-queries';
|
|
9
|
+
import { matchesTextQuery } from '#/lib/text-filter';
|
|
10
|
+
|
|
11
|
+
const GrokErrorComponent = ({ error }: { error: Error }) => {
|
|
12
|
+
return <RouteErrorPanel error={error} title="Failed to load Grok workspaces" />;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const GrokPage = () => {
|
|
16
|
+
const workspaces = useSuspenseQuery(grokWorkspacesQueryOptions()).data;
|
|
17
|
+
const [searchInput, setSearchInput] = useState('');
|
|
18
|
+
const deferredSearch = useDeferredValue(searchInput);
|
|
19
|
+
|
|
20
|
+
const visibleWorkspaces = workspaces.filter((workspace) =>
|
|
21
|
+
matchesTextQuery(deferredSearch, [workspace.label, workspace.worktree, workspace.key]),
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<div className="space-y-6">
|
|
26
|
+
<PageHeader
|
|
27
|
+
actions={
|
|
28
|
+
<ListSearchInput
|
|
29
|
+
placeholder="Search workspace name or path"
|
|
30
|
+
value={searchInput}
|
|
31
|
+
onValueChange={setSearchInput}
|
|
32
|
+
/>
|
|
33
|
+
}
|
|
34
|
+
eyebrow="Inventory"
|
|
35
|
+
subtitle="Workspace groups are derived from local Grok CLI session directories under ~/.grok/sessions."
|
|
36
|
+
title="Grok"
|
|
37
|
+
/>
|
|
38
|
+
|
|
39
|
+
<GrokWorkspacesTable workspaces={visibleWorkspaces} />
|
|
40
|
+
</div>
|
|
41
|
+
);
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export const Route = createFileRoute('/grok/')({
|
|
45
|
+
component: GrokPage,
|
|
46
|
+
errorComponent: GrokErrorComponent,
|
|
47
|
+
loader: ({ context }) => context.queryClient.ensureQueryData(grokWorkspacesQueryOptions()),
|
|
48
|
+
});
|
|
@@ -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 });
|
|
@@ -1,34 +1,30 @@
|
|
|
1
1
|
import type { ThreadEvent, ThreadTranscriptStats } from '@spiracha/lib/codex-browser-types';
|
|
2
2
|
import type { KiroSessionTranscript } from '@spiracha/lib/kiro-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 } from '#/lib/formatters';
|
|
21
|
-
import { kiroSessionDetailQueryOptions } from '#/lib/kiro-queries';
|
|
22
|
-
import { exportKiroSessionFn } from '#/lib/kiro-server';
|
|
23
|
+
import { kiroSessionDetailQueryOptions, kiroWorkspacesQueryOptions } from '#/lib/kiro-queries';
|
|
24
|
+
import { deleteKiroSessionFn, exportKiroSessionFn } from '#/lib/kiro-server';
|
|
23
25
|
import { getKiroThreadTranscriptStats, kiroTranscriptToThreadEvents } from '#/lib/kiro-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 KiroSessionDetailErrorComponent = ({ error }: { error: Error }) => {
|
|
48
|
-
return <
|
|
44
|
+
return <RouteErrorPanel error={error} title="Failed to load Kiro session" />;
|
|
49
45
|
};
|
|
50
46
|
|
|
51
47
|
const getKiroModelLabel = (detail: KiroSessionTranscript): string => {
|
|
@@ -169,13 +165,16 @@ const KiroRawPanels = ({ detail, events }: { detail: KiroSessionTranscript; even
|
|
|
169
165
|
};
|
|
170
166
|
|
|
171
167
|
const KiroSessionDetailPage = () => {
|
|
168
|
+
const navigate = useNavigate();
|
|
169
|
+
const queryClient = useQueryClient();
|
|
172
170
|
const detail = useSuspenseQuery(kiroSessionDetailQueryOptions(Route.useParams().sessionId)).data;
|
|
171
|
+
const [deleteOpen, setDeleteOpen] = useState(false);
|
|
173
172
|
const [pendingExport, setPendingExport] = useState(false);
|
|
174
173
|
const [showToolCalls, setShowToolCalls] = useState(false);
|
|
175
174
|
const [showCommentary, setShowCommentary] = useState(false);
|
|
176
175
|
const [showExtraEvents, setShowExtraEvents] = useState(false);
|
|
177
176
|
const [showRawJson, setShowRawJson] = useState(false);
|
|
178
|
-
const [showUserMessages, setShowUserMessages] = useState(
|
|
177
|
+
const [showUserMessages, setShowUserMessages] = useState(DEFAULT_SHOW_USER_MESSAGES);
|
|
179
178
|
const transcriptEvents = useMemo(() => kiroTranscriptToThreadEvents(detail), [detail]);
|
|
180
179
|
const transcriptStats = useMemo(() => getKiroThreadTranscriptStats(transcriptEvents), [transcriptEvents]);
|
|
181
180
|
const modelLabel = getKiroModelLabel(detail);
|
|
@@ -204,19 +203,56 @@ const KiroSessionDetailPage = () => {
|
|
|
204
203
|
},
|
|
205
204
|
});
|
|
206
205
|
|
|
206
|
+
const deleteSessionMutation = useMutation({
|
|
207
|
+
mutationFn: () => deleteKiroSessionFn({ data: { sessionId: detail.session.sessionId } }),
|
|
208
|
+
onSuccess: async () => {
|
|
209
|
+
await Promise.all([
|
|
210
|
+
queryClient.invalidateQueries({ queryKey: ['kiro-workspaces'] }),
|
|
211
|
+
queryClient.invalidateQueries({ queryKey: ['kiro-sessions', detail.session.workspaceKey] }),
|
|
212
|
+
queryClient.invalidateQueries({ queryKey: ['kiro-session', detail.session.sessionId] }),
|
|
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
|
+
}
|
|
225
|
+
navigate({
|
|
226
|
+
params: { workspaceKey: detail.session.workspaceKey },
|
|
227
|
+
to: '/kiro/$workspaceKey',
|
|
228
|
+
});
|
|
229
|
+
},
|
|
230
|
+
});
|
|
231
|
+
|
|
207
232
|
return (
|
|
208
233
|
<div className="space-y-6">
|
|
209
234
|
<PageHeader
|
|
210
235
|
actions={
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
236
|
+
<>
|
|
237
|
+
<Button
|
|
238
|
+
className="rounded-full"
|
|
239
|
+
type="button"
|
|
240
|
+
variant="outline"
|
|
241
|
+
onClick={() => setPendingExport(true)}
|
|
242
|
+
>
|
|
243
|
+
<Download className="mr-2 size-4" />
|
|
244
|
+
Export
|
|
245
|
+
</Button>
|
|
246
|
+
<Button
|
|
247
|
+
className="rounded-full border-[var(--destructive)]/20 text-[var(--destructive)]"
|
|
248
|
+
type="button"
|
|
249
|
+
variant="outline"
|
|
250
|
+
onClick={() => setDeleteOpen(true)}
|
|
251
|
+
>
|
|
252
|
+
<Trash2 className="mr-2 size-4" />
|
|
253
|
+
Delete
|
|
254
|
+
</Button>
|
|
255
|
+
</>
|
|
220
256
|
}
|
|
221
257
|
breadcrumb={
|
|
222
258
|
<Breadcrumbs
|
|
@@ -327,12 +363,40 @@ const KiroSessionDetailPage = () => {
|
|
|
327
363
|
}
|
|
328
364
|
}}
|
|
329
365
|
/>
|
|
366
|
+
|
|
367
|
+
<DeleteConfirmDialog
|
|
368
|
+
confirmLabel={deleteSessionMutation.isPending ? 'Deleting...' : 'Delete session'}
|
|
369
|
+
description="Permanently delete this Kiro session from disk. This removes the session JSON file and matching execution files."
|
|
370
|
+
errorMessage={
|
|
371
|
+
deleteSessionMutation.isError
|
|
372
|
+
? deleteSessionMutation.error instanceof Error
|
|
373
|
+
? deleteSessionMutation.error.message
|
|
374
|
+
: 'Session delete failed'
|
|
375
|
+
: null
|
|
376
|
+
}
|
|
377
|
+
open={deleteOpen}
|
|
378
|
+
title="Delete this Kiro session?"
|
|
379
|
+
onConfirm={() => deleteSessionMutation.mutate()}
|
|
380
|
+
onOpenChange={(open) => {
|
|
381
|
+
setDeleteOpen(open);
|
|
382
|
+
if (!open) {
|
|
383
|
+
deleteSessionMutation.reset();
|
|
384
|
+
}
|
|
385
|
+
}}
|
|
386
|
+
/>
|
|
330
387
|
</div>
|
|
331
388
|
);
|
|
332
389
|
};
|
|
333
390
|
|
|
334
391
|
export const Route = createFileRoute('/kiro-sessions/$sessionId')({
|
|
335
|
-
component:
|
|
392
|
+
component: () => {
|
|
393
|
+
const { sessionId } = Route.useParams();
|
|
394
|
+
return (
|
|
395
|
+
<RouteStateResetBoundary routeKey={sessionId}>
|
|
396
|
+
<KiroSessionDetailPage />
|
|
397
|
+
</RouteStateResetBoundary>
|
|
398
|
+
);
|
|
399
|
+
},
|
|
336
400
|
errorComponent: KiroSessionDetailErrorComponent,
|
|
337
401
|
loader: ({ context, params }) =>
|
|
338
402
|
context.queryClient.ensureQueryData(kiroSessionDetailQueryOptions(params.sessionId)),
|