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
|
@@ -3,6 +3,8 @@ import { createRootRouteWithContext, HeadContent, Outlet, Scripts } from '@tanst
|
|
|
3
3
|
import type { ReactNode } from 'react';
|
|
4
4
|
import { AppShell } from '#/components/app-shell';
|
|
5
5
|
import { TooltipProvider } from '#/components/ui/tooltip';
|
|
6
|
+
import { getErrorPresentation } from '#/lib/error-presentation';
|
|
7
|
+
import { getInitialSettingsFn } from '#/lib/settings-server';
|
|
6
8
|
import { SettingsProvider } from '#/lib/settings-store';
|
|
7
9
|
import appCss from '#/styles.css?url';
|
|
8
10
|
|
|
@@ -23,26 +25,18 @@ const themeInitScript = `
|
|
|
23
25
|
`;
|
|
24
26
|
|
|
25
27
|
function RootErrorComponent({ error }: { error: Error }) {
|
|
26
|
-
const
|
|
27
|
-
error.message.includes('unable to open database') ||
|
|
28
|
-
error.message.includes('database is locked') ||
|
|
29
|
-
error.message.includes('SQLITE_');
|
|
28
|
+
const presentation = getErrorPresentation(error, { fallbackTitle: 'Something went wrong' });
|
|
30
29
|
|
|
31
30
|
return (
|
|
32
31
|
<div className="flex min-h-screen items-center justify-center bg-[#101418] px-4 text-[#eef3f7]">
|
|
33
32
|
<div className="max-w-[30rem] text-center">
|
|
34
|
-
<h1 className="mb-3 font-semibold text-base">
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
{isSqliteError ? (
|
|
38
|
-
<p className="my-2 text-[#99a3af] text-[0.875rem] leading-6">
|
|
39
|
-
Spiracha could not open the Codex SQLite database. Codex may have an exclusive lock on the file,
|
|
40
|
-
or the database does not exist yet. Close Codex or wait a moment, then reload.
|
|
41
|
-
</p>
|
|
33
|
+
<h1 className="mb-3 font-semibold text-base">{presentation.title}</h1>
|
|
34
|
+
{presentation.isDatabaseError ? (
|
|
35
|
+
<p className="my-2 text-[#99a3af] text-[0.875rem] leading-6">{presentation.description}</p>
|
|
42
36
|
) : (
|
|
43
37
|
<p className="my-2 text-[#99a3af] text-[0.875rem] leading-6">
|
|
44
38
|
<code className="rounded border border-white/10 bg-[#12181e] px-1.5 py-1 text-[0.8em]">
|
|
45
|
-
{
|
|
39
|
+
{presentation.description}
|
|
46
40
|
</code>
|
|
47
41
|
</p>
|
|
48
42
|
)}
|
|
@@ -87,7 +81,7 @@ export const Route = createRootRouteWithContext<RouterContext>()({
|
|
|
87
81
|
},
|
|
88
82
|
{
|
|
89
83
|
content:
|
|
90
|
-
'Browse local Codex, Claude Code, Kiro, Qoder, Cursor, Antigravity, and OpenCode history through a compact workspace UI.',
|
|
84
|
+
'Browse local Codex, Claude Code, Grok, Kiro, Qoder, Cursor, Antigravity, and OpenCode history through a compact workspace UI.',
|
|
91
85
|
name: 'description',
|
|
92
86
|
},
|
|
93
87
|
{
|
|
@@ -95,12 +89,15 @@ export const Route = createRootRouteWithContext<RouterContext>()({
|
|
|
95
89
|
},
|
|
96
90
|
],
|
|
97
91
|
}),
|
|
92
|
+
loader: () => getInitialSettingsFn(),
|
|
98
93
|
});
|
|
99
94
|
|
|
100
95
|
function RootComponent() {
|
|
96
|
+
const initialSettings = Route.useLoaderData();
|
|
97
|
+
|
|
101
98
|
return (
|
|
102
99
|
<RootDocument>
|
|
103
|
-
<SettingsProvider>
|
|
100
|
+
<SettingsProvider initialSettings={initialSettings}>
|
|
104
101
|
<TooltipProvider>
|
|
105
102
|
<AppShell>
|
|
106
103
|
<Outlet />
|
|
@@ -1,9 +1,7 @@
|
|
|
1
|
-
import type { ModelTokenSummary, ToolUsageSummary } from '@spiracha/lib/codex-browser-types';
|
|
2
1
|
import { useSuspenseQuery } from '@tanstack/react-query';
|
|
3
2
|
import { createFileRoute, useNavigate } from '@tanstack/react-router';
|
|
4
|
-
import { createColumnHelper } from '@tanstack/react-table';
|
|
5
3
|
import { startTransition } from 'react';
|
|
6
|
-
import {
|
|
4
|
+
import { AnalyticsBreakdowns } from '#/components/analytics-breakdowns';
|
|
7
5
|
import { MetricCard } from '#/components/metric-card';
|
|
8
6
|
import { PageHeader } from '#/components/page-header';
|
|
9
7
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '#/components/ui/select';
|
|
@@ -16,34 +14,6 @@ import {
|
|
|
16
14
|
withAnalyticsProjectSearch,
|
|
17
15
|
} from '#/lib/route-search';
|
|
18
16
|
|
|
19
|
-
const toolUsageColumnHelper = createColumnHelper<ToolUsageSummary>();
|
|
20
|
-
const toolUsageColumns = [
|
|
21
|
-
toolUsageColumnHelper.accessor('name', {
|
|
22
|
-
cell: (info) => <span className="font-mono text-sm">{info.getValue()}</span>,
|
|
23
|
-
header: 'Tool',
|
|
24
|
-
}),
|
|
25
|
-
toolUsageColumnHelper.accessor('count', {
|
|
26
|
-
cell: (info) => <span className="font-mono text-sm">{formatNumber(info.getValue())}</span>,
|
|
27
|
-
header: 'Calls',
|
|
28
|
-
}),
|
|
29
|
-
] as const;
|
|
30
|
-
|
|
31
|
-
const modelColumnHelper = createColumnHelper<ModelTokenSummary>();
|
|
32
|
-
const modelColumns = [
|
|
33
|
-
modelColumnHelper.accessor('model', {
|
|
34
|
-
cell: (info) => <span className="font-mono text-sm">{info.getValue()}</span>,
|
|
35
|
-
header: 'Model',
|
|
36
|
-
}),
|
|
37
|
-
modelColumnHelper.accessor('threadCount', {
|
|
38
|
-
cell: (info) => <span className="font-mono text-sm">{formatNumber(info.getValue())}</span>,
|
|
39
|
-
header: 'Threads',
|
|
40
|
-
}),
|
|
41
|
-
modelColumnHelper.accessor('totalTokens', {
|
|
42
|
-
cell: (info) => <span className="font-mono text-sm">{formatTokens(info.getValue())}</span>,
|
|
43
|
-
header: 'Tokens',
|
|
44
|
-
}),
|
|
45
|
-
] as const;
|
|
46
|
-
|
|
47
17
|
export const Route = createFileRoute('/analytics')({
|
|
48
18
|
component: AnalyticsPage,
|
|
49
19
|
loader: ({ context, deps }) => {
|
|
@@ -98,7 +68,7 @@ function AnalyticsPage() {
|
|
|
98
68
|
title="Analytics"
|
|
99
69
|
/>
|
|
100
70
|
|
|
101
|
-
<div className="grid gap-4 md:grid-cols-2 xl:grid-cols-
|
|
71
|
+
<div className="grid gap-4 md:grid-cols-2 xl:grid-cols-4">
|
|
102
72
|
<MetricCard label="Threads" value={formatNumber(analytics.summary.totalThreads)} />
|
|
103
73
|
<MetricCard label="Projects" value={formatNumber(analytics.summary.totalProjects)} />
|
|
104
74
|
<MetricCard label="Tokens" value={formatTokens(analytics.summary.totalTokens)} />
|
|
@@ -106,38 +76,17 @@ function AnalyticsPage() {
|
|
|
106
76
|
label="Average per thread"
|
|
107
77
|
value={formatTokens(Math.round(analytics.summary.averageTokensPerThread))}
|
|
108
78
|
/>
|
|
79
|
+
<MetricCard label="Median per thread" value={formatTokens(analytics.summary.medianTokensPerThread)} />
|
|
109
80
|
<MetricCard label="Web search threads" value={formatNumber(analytics.summary.threadsWithWebSearch)} />
|
|
81
|
+
<MetricCard label="Archived threads" value={formatNumber(analytics.summary.archivedThreads)} />
|
|
110
82
|
</div>
|
|
111
83
|
|
|
112
|
-
<
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
</p>
|
|
119
|
-
</div>
|
|
120
|
-
<DataTable
|
|
121
|
-
columns={toolUsageColumns}
|
|
122
|
-
data={analytics.toolUsage}
|
|
123
|
-
emptyMessage="No tool calls recorded."
|
|
124
|
-
/>
|
|
125
|
-
</section>
|
|
126
|
-
|
|
127
|
-
<section className="space-y-4">
|
|
128
|
-
<div>
|
|
129
|
-
<h3 className="font-semibold text-sm">Model token breakdown</h3>
|
|
130
|
-
<p className="mt-1 text-[var(--muted-foreground)] text-sm">
|
|
131
|
-
Compare model usage and token concentration within the current project scope.
|
|
132
|
-
</p>
|
|
133
|
-
</div>
|
|
134
|
-
<DataTable
|
|
135
|
-
columns={modelColumns}
|
|
136
|
-
data={analytics.modelsByTokens}
|
|
137
|
-
emptyMessage="No model usage recorded."
|
|
138
|
-
/>
|
|
139
|
-
</section>
|
|
140
|
-
</div>
|
|
84
|
+
<AnalyticsBreakdowns
|
|
85
|
+
modelsByTokens={analytics.modelsByTokens}
|
|
86
|
+
reasoningEfforts={analytics.reasoningEfforts}
|
|
87
|
+
sources={analytics.sources}
|
|
88
|
+
toolUsage={analytics.toolUsage}
|
|
89
|
+
/>
|
|
141
90
|
</div>
|
|
142
91
|
);
|
|
143
92
|
}
|
|
@@ -5,12 +5,13 @@ import { useMemo, useState } from 'react';
|
|
|
5
5
|
import { AntigravityKeychainPanel } from '#/components/antigravity-keychain-panel';
|
|
6
6
|
import { Breadcrumbs } from '#/components/breadcrumbs';
|
|
7
7
|
import { DeleteConfirmDialog } from '#/components/delete-confirm-dialog';
|
|
8
|
+
import { ExportDialog } from '#/components/export-dialog';
|
|
8
9
|
import { JsonPanel } from '#/components/json-panel';
|
|
9
10
|
import { LoadingPanel } from '#/components/loading-panel';
|
|
10
11
|
import { MetadataSection } from '#/components/metadata-section';
|
|
11
12
|
import { MetricCard } from '#/components/metric-card';
|
|
12
13
|
import { PageHeader } from '#/components/page-header';
|
|
13
|
-
import {
|
|
14
|
+
import { RouteErrorPanel } from '#/components/route-error-panel';
|
|
14
15
|
import { TextDocumentPanel } from '#/components/text-document-panel';
|
|
15
16
|
import { DEFAULT_SHOW_USER_MESSAGES, TranscriptView } from '#/components/transcript-view';
|
|
16
17
|
import { Button } from '#/components/ui/button';
|
|
@@ -20,6 +21,7 @@ import { canExportAntigravityConversation } from '#/lib/antigravity-conversation
|
|
|
20
21
|
import {
|
|
21
22
|
antigravityConversationDetailQueryOptions,
|
|
22
23
|
antigravityDecryptionQueryOptions,
|
|
24
|
+
antigravityWorkspacesQueryOptions,
|
|
23
25
|
} from '#/lib/antigravity-queries';
|
|
24
26
|
import {
|
|
25
27
|
deleteAntigravityConversationFn,
|
|
@@ -31,8 +33,11 @@ import {
|
|
|
31
33
|
antigravityMarkdownToThreadEvents,
|
|
32
34
|
getAntigravityThreadTranscriptStats,
|
|
33
35
|
} from '#/lib/antigravity-transcript-events';
|
|
34
|
-
import { downloadTextFile } from '#/lib/download';
|
|
35
|
-
import {
|
|
36
|
+
import { downloadTextFile, downloadUrlFile } from '#/lib/download';
|
|
37
|
+
import type { ExportDialogOptions } from '#/lib/export-options';
|
|
38
|
+
import { formatBytes, formatDateTime, formatList, formatNumber } from '#/lib/formatters';
|
|
39
|
+
import { RouteStateResetBoundary } from '#/lib/route-state-reset';
|
|
40
|
+
import { shouldNavigateToSourceIndexAfterDelete } from '#/lib/workspace-delete-navigation';
|
|
36
41
|
|
|
37
42
|
type AntigravityConversationDetail = Awaited<ReturnType<typeof getAntigravityConversationDetailFn>>;
|
|
38
43
|
|
|
@@ -97,7 +102,7 @@ const buildTranscriptStatsItems = (
|
|
|
97
102
|
|
|
98
103
|
const stats = getAntigravityThreadTranscriptStats(events);
|
|
99
104
|
return [
|
|
100
|
-
{ label: 'Event kinds', value: [...new Set(events.map((event) => event.kind))]
|
|
105
|
+
{ label: 'Event kinds', value: formatList([...new Set(events.map((event) => event.kind))]) },
|
|
101
106
|
{ label: 'Messages', value: formatNumber(stats.messageCount) },
|
|
102
107
|
{ label: 'User messages', value: formatNumber(stats.userMessageCount) },
|
|
103
108
|
{ label: 'Assistant messages', value: formatNumber(stats.assistantMessageCount) },
|
|
@@ -117,7 +122,14 @@ const getConversationSizeLabel = (detail: AntigravityConversationDetail): string
|
|
|
117
122
|
};
|
|
118
123
|
|
|
119
124
|
export const Route = createFileRoute('/antigravity-conversations/$conversationId')({
|
|
120
|
-
component:
|
|
125
|
+
component: () => {
|
|
126
|
+
const { conversationId } = Route.useParams();
|
|
127
|
+
return (
|
|
128
|
+
<RouteStateResetBoundary routeKey={conversationId}>
|
|
129
|
+
<AntigravityConversationDetailPage />
|
|
130
|
+
</RouteStateResetBoundary>
|
|
131
|
+
);
|
|
132
|
+
},
|
|
121
133
|
errorComponent: AntigravityConversationDetailErrorComponent,
|
|
122
134
|
loader: async ({ context, params }) => {
|
|
123
135
|
await Promise.all([
|
|
@@ -134,7 +146,7 @@ export const Route = createFileRoute('/antigravity-conversations/$conversationId
|
|
|
134
146
|
});
|
|
135
147
|
|
|
136
148
|
function AntigravityConversationDetailErrorComponent({ error }: { error: Error }) {
|
|
137
|
-
return <
|
|
149
|
+
return <RouteErrorPanel error={error} title="Failed to load Antigravity conversation" />;
|
|
138
150
|
}
|
|
139
151
|
|
|
140
152
|
function AntigravityConversationHeaderActions({
|
|
@@ -307,6 +319,7 @@ function AntigravityConversationDetailPage() {
|
|
|
307
319
|
const decryptionState = useSuspenseQuery(antigravityDecryptionQueryOptions()).data;
|
|
308
320
|
const detail = useSuspenseQuery(antigravityConversationDetailQueryOptions(Route.useParams().conversationId)).data;
|
|
309
321
|
const [deleteOpen, setDeleteOpen] = useState(false);
|
|
322
|
+
const [exportOpen, setExportOpen] = useState(false);
|
|
310
323
|
const [showToolCalls, setShowToolCalls] = useState(false);
|
|
311
324
|
const [showCommentary, setShowCommentary] = useState(false);
|
|
312
325
|
const [showExtraEvents, setShowExtraEvents] = useState(false);
|
|
@@ -324,10 +337,26 @@ function AntigravityConversationDetailPage() {
|
|
|
324
337
|
const showConversationExport = canExportConversation || detail.transcriptLocked;
|
|
325
338
|
|
|
326
339
|
const exportConversationMutation = useMutation({
|
|
327
|
-
mutationFn: () =>
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
340
|
+
mutationFn: async (options: ExportDialogOptions) => {
|
|
341
|
+
const download = await exportAntigravityConversationFn({
|
|
342
|
+
data: {
|
|
343
|
+
conversationId: detail.conversation.conversationId,
|
|
344
|
+
includeCommentary: options.includeCommentary,
|
|
345
|
+
includeMetadata: options.includeMetadata,
|
|
346
|
+
includeTools: options.includeTools,
|
|
347
|
+
outputFormat: options.outputFormat,
|
|
348
|
+
zipArchive: options.zipArchive,
|
|
349
|
+
},
|
|
350
|
+
});
|
|
351
|
+
if (download.mode === 'download') {
|
|
352
|
+
downloadTextFile(download.fileName, download.content, download.mimeType);
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
await downloadUrlFile(download.fileName, download.downloadUrl);
|
|
357
|
+
},
|
|
358
|
+
onSuccess: () => {
|
|
359
|
+
setExportOpen(false);
|
|
331
360
|
},
|
|
332
361
|
});
|
|
333
362
|
|
|
@@ -352,6 +381,17 @@ function AntigravityConversationDetailPage() {
|
|
|
352
381
|
queryKey: ['antigravity-conversation', detail.conversation.conversationId],
|
|
353
382
|
}),
|
|
354
383
|
]);
|
|
384
|
+
const workspaces = await queryClient.fetchQuery(antigravityWorkspacesQueryOptions());
|
|
385
|
+
if (
|
|
386
|
+
shouldNavigateToSourceIndexAfterDelete(
|
|
387
|
+
workspaces,
|
|
388
|
+
detail.conversationGroup.key,
|
|
389
|
+
(workspace) => workspace.key,
|
|
390
|
+
)
|
|
391
|
+
) {
|
|
392
|
+
navigate({ to: '/antigravity' });
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
355
395
|
navigate({
|
|
356
396
|
params: { workspaceKey: detail.conversationGroup.key },
|
|
357
397
|
to: '/antigravity/$workspaceKey',
|
|
@@ -371,7 +411,7 @@ function AntigravityConversationDetailPage() {
|
|
|
371
411
|
showConversationExport={showConversationExport}
|
|
372
412
|
onDeleteConversation={() => setDeleteOpen(true)}
|
|
373
413
|
onExportArtifacts={() => exportArtifactsMutation.mutate()}
|
|
374
|
-
onExportConversation={() =>
|
|
414
|
+
onExportConversation={() => setExportOpen(true)}
|
|
375
415
|
/>
|
|
376
416
|
}
|
|
377
417
|
breadcrumb={
|
|
@@ -463,14 +503,6 @@ function AntigravityConversationDetailPage() {
|
|
|
463
503
|
</TabsContent>
|
|
464
504
|
</Tabs>
|
|
465
505
|
|
|
466
|
-
{exportConversationMutation.isError ? (
|
|
467
|
-
<p className="text-[var(--destructive)] text-sm">
|
|
468
|
-
{exportConversationMutation.error instanceof Error
|
|
469
|
-
? exportConversationMutation.error.message
|
|
470
|
-
: 'Conversation export failed'}
|
|
471
|
-
</p>
|
|
472
|
-
) : null}
|
|
473
|
-
|
|
474
506
|
{exportArtifactsMutation.isError ? (
|
|
475
507
|
<p className="text-[var(--destructive)] text-sm">
|
|
476
508
|
{exportArtifactsMutation.error instanceof Error
|
|
@@ -479,6 +511,28 @@ function AntigravityConversationDetailPage() {
|
|
|
479
511
|
</p>
|
|
480
512
|
) : null}
|
|
481
513
|
|
|
514
|
+
<ExportDialog
|
|
515
|
+
errorMessage={
|
|
516
|
+
exportConversationMutation.isError
|
|
517
|
+
? exportConversationMutation.error instanceof Error
|
|
518
|
+
? exportConversationMutation.error.message
|
|
519
|
+
: 'Conversation export failed'
|
|
520
|
+
: null
|
|
521
|
+
}
|
|
522
|
+
open={exportOpen}
|
|
523
|
+
pending={exportConversationMutation.isPending}
|
|
524
|
+
showCommentaryOption={detail.conversation.transcriptSource !== 'safe-storage'}
|
|
525
|
+
showToolsOption={detail.conversation.transcriptSource !== 'safe-storage'}
|
|
526
|
+
title={`Export ${detail.conversation.title}`}
|
|
527
|
+
onExport={(options) => exportConversationMutation.mutate(options)}
|
|
528
|
+
onOpenChange={(open) => {
|
|
529
|
+
setExportOpen(open);
|
|
530
|
+
if (!open) {
|
|
531
|
+
exportConversationMutation.reset();
|
|
532
|
+
}
|
|
533
|
+
}}
|
|
534
|
+
/>
|
|
535
|
+
|
|
482
536
|
<DeleteConfirmDialog
|
|
483
537
|
confirmLabel={deleteConversationMutation.isPending ? 'Deleting...' : 'Delete conversation'}
|
|
484
538
|
description="Permanently delete this Antigravity conversation from disk. This removes its summary entry, conversation file, transcript logs, and generated artifacts."
|
|
@@ -10,7 +10,7 @@ import { ExportDialog } from '#/components/export-dialog';
|
|
|
10
10
|
import { ListSearchInput } from '#/components/list-search-input';
|
|
11
11
|
import { LoadingPanel } from '#/components/loading-panel';
|
|
12
12
|
import { PageHeader } from '#/components/page-header';
|
|
13
|
-
import {
|
|
13
|
+
import { RouteErrorPanel } from '#/components/route-error-panel';
|
|
14
14
|
import { Button } from '#/components/ui/button';
|
|
15
15
|
import {
|
|
16
16
|
antigravityConversationsQueryOptions,
|
|
@@ -21,21 +21,13 @@ import {
|
|
|
21
21
|
deleteAntigravityConversationFn,
|
|
22
22
|
deleteAntigravityConversationsFn,
|
|
23
23
|
exportAntigravityArtifactsFn,
|
|
24
|
-
exportAntigravityConversationFn,
|
|
25
24
|
exportAntigravityConversationsFn,
|
|
26
25
|
} from '#/lib/antigravity-server';
|
|
27
26
|
import { downloadTextFile, downloadUrlFile } from '#/lib/download';
|
|
27
|
+
import { createExportSelectionMutationInput, type ExportSelectionMutationInput } from '#/lib/export-mutation';
|
|
28
28
|
import { matchesTextQuery } from '#/lib/text-filter';
|
|
29
29
|
import { isWorkspaceEmptiedByDelete } from '#/lib/workspace-delete-navigation';
|
|
30
30
|
|
|
31
|
-
type ExportDialogOptions = {
|
|
32
|
-
includeCommentary: boolean;
|
|
33
|
-
includeMetadata: boolean;
|
|
34
|
-
includeTools: boolean;
|
|
35
|
-
outputFormat: 'md' | 'txt';
|
|
36
|
-
zipArchive: boolean;
|
|
37
|
-
};
|
|
38
|
-
|
|
39
31
|
type PendingConversationDelete = {
|
|
40
32
|
conversations: AntigravityConversation[];
|
|
41
33
|
scope: 'all' | 'selected';
|
|
@@ -44,6 +36,7 @@ type PendingConversationDelete = {
|
|
|
44
36
|
type PendingConversationExport = {
|
|
45
37
|
conversationIds: string[];
|
|
46
38
|
label: string;
|
|
39
|
+
supportsTranscriptFilters: boolean;
|
|
47
40
|
};
|
|
48
41
|
|
|
49
42
|
const findWorkspaceOrThrow = (workspaces: AntigravityWorkspaceGroup[], workspaceKey: string) => {
|
|
@@ -61,6 +54,9 @@ const buildConversationExport = (selectedConversations: AntigravityConversation[
|
|
|
61
54
|
selectedConversations.length === 1
|
|
62
55
|
? selectedConversations[0]!.title
|
|
63
56
|
: `${selectedConversations.length} selected conversations`,
|
|
57
|
+
supportsTranscriptFilters: selectedConversations.every(
|
|
58
|
+
(conversation) => conversation.transcriptSource !== 'safe-storage',
|
|
59
|
+
),
|
|
64
60
|
});
|
|
65
61
|
|
|
66
62
|
const getDeleteConfirmLabel = (pendingDelete: PendingConversationDelete | null, isPending: boolean) => {
|
|
@@ -101,32 +97,12 @@ const getDeleteTitle = (pendingDelete: PendingConversationDelete | null) => {
|
|
|
101
97
|
: 'Delete this Antigravity conversation?';
|
|
102
98
|
};
|
|
103
99
|
|
|
104
|
-
const AntigravityWorkspaceErrors = ({
|
|
105
|
-
artifactError
|
|
106
|
-
batchExportError,
|
|
107
|
-
conversationError,
|
|
108
|
-
}: {
|
|
109
|
-
artifactError: Error | null;
|
|
110
|
-
batchExportError: Error | null;
|
|
111
|
-
conversationError: Error | null;
|
|
112
|
-
}) => {
|
|
113
|
-
const messages = [conversationError?.message, artifactError?.message, batchExportError?.message].filter(
|
|
114
|
-
(message): message is string => Boolean(message),
|
|
115
|
-
);
|
|
116
|
-
|
|
117
|
-
if (messages.length === 0) {
|
|
100
|
+
const AntigravityWorkspaceErrors = ({ artifactError }: { artifactError: Error | null }) => {
|
|
101
|
+
if (!artifactError) {
|
|
118
102
|
return null;
|
|
119
103
|
}
|
|
120
104
|
|
|
121
|
-
return (
|
|
122
|
-
<div className="space-y-1">
|
|
123
|
-
{messages.map((message) => (
|
|
124
|
-
<p className="text-[var(--destructive)] text-sm" key={message}>
|
|
125
|
-
{message}
|
|
126
|
-
</p>
|
|
127
|
-
))}
|
|
128
|
-
</div>
|
|
129
|
-
);
|
|
105
|
+
return <p className="text-[var(--destructive)] text-sm">{artifactError.message}</p>;
|
|
130
106
|
};
|
|
131
107
|
|
|
132
108
|
export const Route = createFileRoute('/antigravity/$workspaceKey')({
|
|
@@ -149,7 +125,7 @@ export const Route = createFileRoute('/antigravity/$workspaceKey')({
|
|
|
149
125
|
});
|
|
150
126
|
|
|
151
127
|
function AntigravityWorkspaceErrorComponent({ error }: { error: Error }) {
|
|
152
|
-
return <
|
|
128
|
+
return <RouteErrorPanel error={error} title="Failed to load Antigravity workspace" />;
|
|
153
129
|
}
|
|
154
130
|
|
|
155
131
|
function AntigravityWorkspacePage() {
|
|
@@ -165,14 +141,6 @@ function AntigravityWorkspacePage() {
|
|
|
165
141
|
const [pendingExport, setPendingExport] = useState<PendingConversationExport | null>(null);
|
|
166
142
|
const deferredSearch = useDeferredValue(searchInput);
|
|
167
143
|
|
|
168
|
-
const exportConversationMutation = useMutation({
|
|
169
|
-
mutationFn: (conversation: AntigravityConversation) =>
|
|
170
|
-
exportAntigravityConversationFn({ data: { conversationId: conversation.conversationId } }),
|
|
171
|
-
onSuccess: (result) => {
|
|
172
|
-
downloadTextFile(result.filename, result.content, 'text/markdown; charset=utf-8');
|
|
173
|
-
},
|
|
174
|
-
});
|
|
175
|
-
|
|
176
144
|
const exportArtifactsMutation = useMutation({
|
|
177
145
|
mutationFn: (conversation: AntigravityConversation) =>
|
|
178
146
|
exportAntigravityArtifactsFn({ data: { conversationId: conversation.conversationId } }),
|
|
@@ -182,14 +150,13 @@ function AntigravityWorkspacePage() {
|
|
|
182
150
|
});
|
|
183
151
|
|
|
184
152
|
const exportConversationsMutation = useMutation({
|
|
185
|
-
mutationFn: async (options:
|
|
186
|
-
if (!pendingExport) {
|
|
187
|
-
throw new Error('No Antigravity conversation selected for export');
|
|
188
|
-
}
|
|
189
|
-
|
|
153
|
+
mutationFn: async ({ ids, options }: ExportSelectionMutationInput) => {
|
|
190
154
|
const download = await exportAntigravityConversationsFn({
|
|
191
155
|
data: {
|
|
192
|
-
conversationIds:
|
|
156
|
+
conversationIds: [...ids],
|
|
157
|
+
includeCommentary: options.includeCommentary,
|
|
158
|
+
includeMetadata: options.includeMetadata,
|
|
159
|
+
includeTools: options.includeTools,
|
|
193
160
|
outputFormat: options.outputFormat,
|
|
194
161
|
zipArchive: options.zipArchive,
|
|
195
162
|
},
|
|
@@ -212,14 +179,7 @@ function AntigravityWorkspacePage() {
|
|
|
212
179
|
conversationIds.length === 1
|
|
213
180
|
? deleteAntigravityConversationFn({ data: { conversationId: conversationIds[0]! } })
|
|
214
181
|
: deleteAntigravityConversationsFn({ data: { conversationIds } }),
|
|
215
|
-
|
|
216
|
-
const conversationIds = result.deletedConversationIds;
|
|
217
|
-
const workspaceEmptied = isWorkspaceEmptiedByDelete(
|
|
218
|
-
conversations,
|
|
219
|
-
conversationIds,
|
|
220
|
-
(conversation) => conversation.conversationId,
|
|
221
|
-
);
|
|
222
|
-
setPendingDelete(null);
|
|
182
|
+
onSettled: async (_result, _error, conversationIds) => {
|
|
223
183
|
await Promise.all([
|
|
224
184
|
queryClient.invalidateQueries({ queryKey: ['antigravity-workspaces'] }),
|
|
225
185
|
queryClient.invalidateQueries({ queryKey: ['antigravity-conversations', workspace.key] }),
|
|
@@ -229,6 +189,15 @@ function AntigravityWorkspacePage() {
|
|
|
229
189
|
}),
|
|
230
190
|
),
|
|
231
191
|
]);
|
|
192
|
+
},
|
|
193
|
+
onSuccess: async (result) => {
|
|
194
|
+
const conversationIds = result.deletedConversationIds;
|
|
195
|
+
const workspaceEmptied = isWorkspaceEmptiedByDelete(
|
|
196
|
+
conversations,
|
|
197
|
+
conversationIds,
|
|
198
|
+
(conversation) => conversation.conversationId,
|
|
199
|
+
);
|
|
200
|
+
setPendingDelete(null);
|
|
232
201
|
if (workspaceEmptied) {
|
|
233
202
|
await navigate({ to: '/antigravity' });
|
|
234
203
|
}
|
|
@@ -308,7 +277,7 @@ function AntigravityWorkspacePage() {
|
|
|
308
277
|
openDeleteForConversations(lookupSelectedConversations(conversationIds), 'selected')
|
|
309
278
|
}
|
|
310
279
|
onExportArtifacts={(conversation) => exportArtifactsMutation.mutate(conversation)}
|
|
311
|
-
onExportConversation={(conversation) =>
|
|
280
|
+
onExportConversation={(conversation) => openExportForConversations([conversation])}
|
|
312
281
|
onExportConversations={(conversationIds) =>
|
|
313
282
|
openExportForConversations(lookupSelectedConversations(conversationIds))
|
|
314
283
|
}
|
|
@@ -316,16 +285,29 @@ function AntigravityWorkspacePage() {
|
|
|
316
285
|
|
|
317
286
|
<AntigravityWorkspaceErrors
|
|
318
287
|
artifactError={exportArtifactsMutation.isError ? exportArtifactsMutation.error : null}
|
|
319
|
-
batchExportError={exportConversationsMutation.isError ? exportConversationsMutation.error : null}
|
|
320
|
-
conversationError={exportConversationMutation.isError ? exportConversationMutation.error : null}
|
|
321
288
|
/>
|
|
322
289
|
|
|
323
290
|
<ExportDialog
|
|
291
|
+
errorMessage={
|
|
292
|
+
exportConversationsMutation.isError
|
|
293
|
+
? exportConversationsMutation.error instanceof Error
|
|
294
|
+
? exportConversationsMutation.error.message
|
|
295
|
+
: 'Conversation export failed'
|
|
296
|
+
: null
|
|
297
|
+
}
|
|
324
298
|
forceZipArchive={pendingExport ? pendingExport.conversationIds.length > 1 : false}
|
|
325
299
|
open={pendingExport !== null}
|
|
326
300
|
pending={exportConversationsMutation.isPending}
|
|
301
|
+
showCommentaryOption={pendingExport?.supportsTranscriptFilters ?? true}
|
|
302
|
+
showToolsOption={pendingExport?.supportsTranscriptFilters ?? true}
|
|
327
303
|
title={pendingExport ? `Export ${pendingExport.label}` : 'Export conversation'}
|
|
328
|
-
onExport={(options) =>
|
|
304
|
+
onExport={(options) => {
|
|
305
|
+
if (pendingExport) {
|
|
306
|
+
exportConversationsMutation.mutate(
|
|
307
|
+
createExportSelectionMutationInput(pendingExport.conversationIds, options),
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
}}
|
|
329
311
|
onOpenChange={(open) => {
|
|
330
312
|
if (!open) {
|
|
331
313
|
setPendingExport(null);
|
|
@@ -5,7 +5,7 @@ import { AntigravityKeychainPanel } from '#/components/antigravity-keychain-pane
|
|
|
5
5
|
import { AntigravityWorkspacesTable } from '#/components/antigravity-workspaces-table';
|
|
6
6
|
import { ListSearchInput } from '#/components/list-search-input';
|
|
7
7
|
import { PageHeader } from '#/components/page-header';
|
|
8
|
-
import {
|
|
8
|
+
import { RouteErrorPanel } from '#/components/route-error-panel';
|
|
9
9
|
import { antigravityWorkspacesQueryOptions } from '#/lib/antigravity-queries';
|
|
10
10
|
import { matchesTextQuery } from '#/lib/text-filter';
|
|
11
11
|
|
|
@@ -16,7 +16,7 @@ export const Route = createFileRoute('/antigravity/')({
|
|
|
16
16
|
});
|
|
17
17
|
|
|
18
18
|
function AntigravityErrorComponent({ error }: { error: Error }) {
|
|
19
|
-
return <
|
|
19
|
+
return <RouteErrorPanel error={error} title="Failed to load Antigravity workspaces" />;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
function AntigravityPage() {
|
|
@@ -12,7 +12,7 @@ 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';
|
|
@@ -20,6 +20,7 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from '#/components/ui/tabs';
|
|
|
20
20
|
import {
|
|
21
21
|
claudeCodeSessionDetailQueryOptions,
|
|
22
22
|
claudeCodeSessionTranscriptQueryOptions,
|
|
23
|
+
claudeCodeWorkspacesQueryOptions,
|
|
23
24
|
} from '#/lib/claude-code-queries';
|
|
24
25
|
import { deleteClaudeCodeSessionFn, exportClaudeCodeSessionFn } from '#/lib/claude-code-server';
|
|
25
26
|
import {
|
|
@@ -27,15 +28,10 @@ import {
|
|
|
27
28
|
getClaudeCodeThreadTranscriptStats,
|
|
28
29
|
} from '#/lib/claude-code-transcript-events';
|
|
29
30
|
import { downloadTextFile, downloadUrlFile } from '#/lib/download';
|
|
31
|
+
import type { ExportDialogOptions } from '#/lib/export-options';
|
|
30
32
|
import { formatDateTime, formatList, formatNumber, formatTokens } from '#/lib/formatters';
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
includeCommentary: boolean;
|
|
34
|
-
includeMetadata: boolean;
|
|
35
|
-
includeTools: boolean;
|
|
36
|
-
outputFormat: 'md' | 'txt';
|
|
37
|
-
zipArchive: boolean;
|
|
38
|
-
};
|
|
33
|
+
import { RouteStateResetBoundary } from '#/lib/route-state-reset';
|
|
34
|
+
import { shouldNavigateToSourceIndexAfterDelete } from '#/lib/workspace-delete-navigation';
|
|
39
35
|
|
|
40
36
|
type TranscriptControlsProps = {
|
|
41
37
|
rawJsonDisabled?: boolean;
|
|
@@ -52,7 +48,14 @@ type TranscriptControlsProps = {
|
|
|
52
48
|
};
|
|
53
49
|
|
|
54
50
|
export const Route = createFileRoute('/claude-code-sessions/$sessionId')({
|
|
55
|
-
component:
|
|
51
|
+
component: () => {
|
|
52
|
+
const { sessionId } = Route.useParams();
|
|
53
|
+
return (
|
|
54
|
+
<RouteStateResetBoundary routeKey={sessionId}>
|
|
55
|
+
<ClaudeCodeSessionDetailPage />
|
|
56
|
+
</RouteStateResetBoundary>
|
|
57
|
+
);
|
|
58
|
+
},
|
|
56
59
|
errorComponent: ClaudeCodeSessionDetailErrorComponent,
|
|
57
60
|
loader: ({ context, params }) =>
|
|
58
61
|
context.queryClient.ensureQueryData(claudeCodeSessionDetailQueryOptions(params.sessionId)),
|
|
@@ -65,7 +68,7 @@ export const Route = createFileRoute('/claude-code-sessions/$sessionId')({
|
|
|
65
68
|
});
|
|
66
69
|
|
|
67
70
|
function ClaudeCodeSessionDetailErrorComponent({ error }: { error: Error }) {
|
|
68
|
-
return <
|
|
71
|
+
return <RouteErrorPanel error={error} title="Failed to load Claude Code session" />;
|
|
69
72
|
}
|
|
70
73
|
|
|
71
74
|
const buildSessionMetadata = (detail: ClaudeCodeSessionTranscript) => [
|
|
@@ -285,6 +288,17 @@ function ClaudeCodeSessionDetailPage() {
|
|
|
285
288
|
queryClient.invalidateQueries({ queryKey: ['claude-code-session', params.sessionId] }),
|
|
286
289
|
queryClient.invalidateQueries({ queryKey: ['claude-code-session-transcript', params.sessionId] }),
|
|
287
290
|
]);
|
|
291
|
+
const workspaces = await queryClient.fetchQuery(claudeCodeWorkspacesQueryOptions());
|
|
292
|
+
if (
|
|
293
|
+
shouldNavigateToSourceIndexAfterDelete(
|
|
294
|
+
workspaces,
|
|
295
|
+
detail.session.workspaceKey,
|
|
296
|
+
(workspace) => workspace.key,
|
|
297
|
+
)
|
|
298
|
+
) {
|
|
299
|
+
navigate({ to: '/claude-code' });
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
288
302
|
navigate({
|
|
289
303
|
params: { workspaceKey: detail.session.workspaceKey },
|
|
290
304
|
to: '/claude-code/$workspaceKey',
|