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,23 +1,18 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { mkdtemp, rm } from 'node:fs/promises';
|
|
3
|
-
import os from 'node:os';
|
|
4
|
-
import path from 'node:path';
|
|
5
|
-
import {
|
|
6
|
-
getExportMimeType,
|
|
7
|
-
resolveUniqueExportFileBaseName,
|
|
8
|
-
sanitizeExportFileName,
|
|
9
|
-
zipExportDirectory,
|
|
10
|
-
} from '@spiracha/lib/ui-export-archive';
|
|
11
|
-
import { buildUiExportDownloadUrl, ensureUiExportDir } from '@spiracha/lib/ui-export-files';
|
|
1
|
+
import { isSafeCursorComposerId } from '@spiracha/lib/cursor-id';
|
|
12
2
|
import { createServerFn } from '@tanstack/react-start';
|
|
13
3
|
import { z } from 'zod';
|
|
4
|
+
import { renderSourceSessionDownload, renderSourceSessionsDownload } from './source-session-export-server';
|
|
14
5
|
|
|
15
6
|
const workspaceSchema = z.object({
|
|
16
7
|
workspaceKey: z.string().min(1),
|
|
17
8
|
});
|
|
18
9
|
|
|
10
|
+
const composerIdSchema = z.string().refine(isSafeCursorComposerId, {
|
|
11
|
+
message: 'Invalid Cursor composer id.',
|
|
12
|
+
});
|
|
13
|
+
|
|
19
14
|
const threadSchema = z.object({
|
|
20
|
-
composerId:
|
|
15
|
+
composerId: composerIdSchema,
|
|
21
16
|
});
|
|
22
17
|
|
|
23
18
|
const recoverSchema = z.object({
|
|
@@ -26,7 +21,7 @@ const recoverSchema = z.object({
|
|
|
26
21
|
});
|
|
27
22
|
|
|
28
23
|
const exportSchema = z.object({
|
|
29
|
-
composerId:
|
|
24
|
+
composerId: composerIdSchema,
|
|
30
25
|
includeCommentary: z.boolean().default(true),
|
|
31
26
|
includeMetadata: z.boolean().default(true),
|
|
32
27
|
includeTools: z.boolean().default(true),
|
|
@@ -35,7 +30,7 @@ const exportSchema = z.object({
|
|
|
35
30
|
});
|
|
36
31
|
|
|
37
32
|
const exportThreadsSchema = z.object({
|
|
38
|
-
composerIds: z.array(
|
|
33
|
+
composerIds: z.array(composerIdSchema).min(1),
|
|
39
34
|
includeCommentary: z.boolean().default(true),
|
|
40
35
|
includeMetadata: z.boolean().default(true),
|
|
41
36
|
includeTools: z.boolean().default(true),
|
|
@@ -44,7 +39,7 @@ const exportThreadsSchema = z.object({
|
|
|
44
39
|
});
|
|
45
40
|
|
|
46
41
|
const deleteThreadsSchema = z.object({
|
|
47
|
-
composerIds: z.array(
|
|
42
|
+
composerIds: z.array(composerIdSchema).min(1),
|
|
48
43
|
});
|
|
49
44
|
|
|
50
45
|
const ensureCursorClosedForWrite = async () => {
|
|
@@ -67,39 +62,29 @@ const findGroupByKey = async (workspaceKey: string) => {
|
|
|
67
62
|
return group;
|
|
68
63
|
};
|
|
69
64
|
|
|
70
|
-
const toSafeExportName = (value: string) => {
|
|
71
|
-
return sanitizeExportFileName(value) || 'cursor-thread';
|
|
72
|
-
};
|
|
73
|
-
|
|
74
65
|
const renderCursorZipDownload = async (
|
|
75
|
-
rendered: Array<{
|
|
66
|
+
rendered: Array<{
|
|
67
|
+
composerId: string;
|
|
68
|
+
content: string;
|
|
69
|
+
cwd: string | null;
|
|
70
|
+
fileBaseName: string;
|
|
71
|
+
updatedAtMs: number | null;
|
|
72
|
+
}>,
|
|
76
73
|
outputFormat: 'md' | 'txt',
|
|
77
74
|
) => {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
await zipExportDirectory(workspaceDir, zipPath);
|
|
93
|
-
} finally {
|
|
94
|
-
await rm(workspaceDir, { force: true, recursive: true });
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
return {
|
|
98
|
-
downloadUrl: buildUiExportDownloadUrl(zipPath),
|
|
99
|
-
fileName: `${exportBaseName}.zip`,
|
|
100
|
-
mimeType: 'application/zip',
|
|
101
|
-
mode: 'download_url' as const,
|
|
102
|
-
};
|
|
75
|
+
return renderSourceSessionsDownload({
|
|
76
|
+
entries: rendered.map((entry) => ({
|
|
77
|
+
content: entry.content,
|
|
78
|
+
cwd: entry.cwd,
|
|
79
|
+
fallbackBaseName: 'cursor-thread',
|
|
80
|
+
fileBaseName: entry.fileBaseName,
|
|
81
|
+
sessionId: entry.composerId,
|
|
82
|
+
updatedAtMs: entry.updatedAtMs,
|
|
83
|
+
})),
|
|
84
|
+
fallbackBaseName: 'cursor',
|
|
85
|
+
outputFormat,
|
|
86
|
+
zipArchive: true,
|
|
87
|
+
});
|
|
103
88
|
};
|
|
104
89
|
|
|
105
90
|
export const findCursorThreadByComposerId = async (composerId: string) => {
|
|
@@ -123,13 +108,23 @@ const renderCursorDownload = async (input: {
|
|
|
123
108
|
outputFormat: 'md' | 'txt';
|
|
124
109
|
zipArchive: boolean;
|
|
125
110
|
}) => {
|
|
111
|
+
const { runWithTranscriptLoadLimit } = await import('@spiracha/lib/transcript-load-limiter');
|
|
126
112
|
const { readCursorThreadTranscriptWithAgentFiles } = await import('@spiracha/lib/cursor-db');
|
|
127
113
|
const { getCursorGlobalDbPath } = await import('@spiracha/lib/cursor-exporter-types');
|
|
128
114
|
const { renderCursorTranscript } = await import('@spiracha/lib/cursor-transcript');
|
|
129
115
|
const globalDbPath = getCursorGlobalDbPath();
|
|
116
|
+
const { listCursorWorkspaceGroups } = await import('@spiracha/lib/cursor-db');
|
|
117
|
+
const workspaceGroups = await listCursorWorkspaceGroups();
|
|
130
118
|
const rendered = await Promise.all(
|
|
131
119
|
input.composerIds.map(async (composerId) => {
|
|
132
|
-
const transcript = await
|
|
120
|
+
const transcript = await runWithTranscriptLoadLimit(
|
|
121
|
+
() => readCursorThreadTranscriptWithAgentFiles(globalDbPath, composerId),
|
|
122
|
+
{
|
|
123
|
+
id: composerId,
|
|
124
|
+
path: globalDbPath,
|
|
125
|
+
source: 'cursor-ui-export',
|
|
126
|
+
},
|
|
127
|
+
);
|
|
133
128
|
if (!transcript) {
|
|
134
129
|
throw new Error(`No transcript found for thread: ${composerId}`);
|
|
135
130
|
}
|
|
@@ -145,9 +140,17 @@ const renderCursorDownload = async (input: {
|
|
|
145
140
|
throw new Error(`Thread has no exportable content: ${composerId}`);
|
|
146
141
|
}
|
|
147
142
|
|
|
143
|
+
const workspace =
|
|
144
|
+
workspaceGroups.find((candidate) =>
|
|
145
|
+
candidate.buckets.some((bucket) => bucket.threadComposerIds.includes(composerId)),
|
|
146
|
+
) ?? null;
|
|
147
|
+
|
|
148
148
|
return {
|
|
149
149
|
composerId,
|
|
150
150
|
content,
|
|
151
|
+
cwd: workspace?.folders[0] ?? null,
|
|
152
|
+
fileBaseName: transcript.head.name || composerId,
|
|
153
|
+
updatedAtMs: transcript.head.lastUpdatedAtMs,
|
|
151
154
|
};
|
|
152
155
|
}),
|
|
153
156
|
);
|
|
@@ -157,12 +160,16 @@ const renderCursorDownload = async (input: {
|
|
|
157
160
|
}
|
|
158
161
|
|
|
159
162
|
if (rendered.length === 1) {
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
163
|
+
const entry = rendered[0]!;
|
|
164
|
+
return renderSourceSessionDownload({
|
|
165
|
+
content: entry.content,
|
|
166
|
+
cwd: entry.cwd,
|
|
167
|
+
fallbackBaseName: 'cursor-thread',
|
|
168
|
+
outputFormat: input.outputFormat,
|
|
169
|
+
sessionId: entry.composerId,
|
|
170
|
+
updatedAtMs: entry.updatedAtMs,
|
|
171
|
+
zipArchive: false,
|
|
172
|
+
});
|
|
166
173
|
}
|
|
167
174
|
|
|
168
175
|
throw new Error('No Cursor threads selected for export');
|
|
@@ -184,6 +191,7 @@ export const listCursorThreadsFn = createServerFn({ method: 'GET' })
|
|
|
184
191
|
export const getCursorThreadDetailFn = createServerFn({ method: 'GET' })
|
|
185
192
|
.validator(threadSchema)
|
|
186
193
|
.handler(async ({ data }) => {
|
|
194
|
+
const { runWithTranscriptLoadLimit } = await import('@spiracha/lib/transcript-load-limiter');
|
|
187
195
|
const { readCursorThreadTranscriptWithAgentFiles } = await import('@spiracha/lib/cursor-db');
|
|
188
196
|
const { getCursorGlobalDbPath } = await import('@spiracha/lib/cursor-exporter-types');
|
|
189
197
|
const thread = await findCursorThreadByComposerId(data.composerId);
|
|
@@ -191,7 +199,14 @@ export const getCursorThreadDetailFn = createServerFn({ method: 'GET' })
|
|
|
191
199
|
throw new Error(`Cursor thread not found: ${data.composerId}`);
|
|
192
200
|
}
|
|
193
201
|
|
|
194
|
-
const transcript = await
|
|
202
|
+
const transcript = await runWithTranscriptLoadLimit(
|
|
203
|
+
() => readCursorThreadTranscriptWithAgentFiles(getCursorGlobalDbPath(), data.composerId),
|
|
204
|
+
{
|
|
205
|
+
id: data.composerId,
|
|
206
|
+
path: getCursorGlobalDbPath(),
|
|
207
|
+
source: 'cursor-ui-detail',
|
|
208
|
+
},
|
|
209
|
+
);
|
|
195
210
|
return {
|
|
196
211
|
thread,
|
|
197
212
|
transcript,
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { ThreadEvent, ThreadTranscriptStats } from '@spiracha/lib/codex-browser-types';
|
|
2
2
|
import type { CursorBubble, CursorThreadTranscript, CursorToolCall } from '@spiracha/lib/cursor-exporter-types';
|
|
3
|
+
import { getCursorTextBubblePhase, getFinalCursorAssistantTextBubbleIds } from '@spiracha/lib/cursor-transcript-phase';
|
|
3
4
|
import type { JsonValue } from '@spiracha/lib/shared';
|
|
5
|
+
import { getThreadTranscriptStats } from './thread-transcript-stats';
|
|
4
6
|
|
|
5
7
|
const toTimestamp = (value: number | null): string | null => {
|
|
6
8
|
if (value === null || !Number.isFinite(value)) {
|
|
@@ -110,36 +112,10 @@ const buildToolOutputEvent = (bubble: CursorBubble, sequence: number, toolCall:
|
|
|
110
112
|
};
|
|
111
113
|
};
|
|
112
114
|
|
|
113
|
-
const getAssistantFinalTextIndexes = (bubbles: CursorBubble[]) => {
|
|
114
|
-
const finalTextIndexes = new Set<number>();
|
|
115
|
-
let currentAssistantTextIndex: number | null = null;
|
|
116
|
-
|
|
117
|
-
const flushAssistantRun = () => {
|
|
118
|
-
if (currentAssistantTextIndex !== null) {
|
|
119
|
-
finalTextIndexes.add(currentAssistantTextIndex);
|
|
120
|
-
currentAssistantTextIndex = null;
|
|
121
|
-
}
|
|
122
|
-
};
|
|
123
|
-
|
|
124
|
-
bubbles.forEach((bubble, index) => {
|
|
125
|
-
if (bubble.kind !== 'assistant') {
|
|
126
|
-
flushAssistantRun();
|
|
127
|
-
return;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
if (bubble.text.trim()) {
|
|
131
|
-
currentAssistantTextIndex = index;
|
|
132
|
-
}
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
flushAssistantRun();
|
|
136
|
-
return finalTextIndexes;
|
|
137
|
-
};
|
|
138
|
-
|
|
139
115
|
const cursorBubbleToThreadEvents = (
|
|
140
116
|
bubble: CursorBubble,
|
|
141
117
|
bubbleIndex: number,
|
|
142
|
-
|
|
118
|
+
finalAssistantTextBubbleIds: Set<string>,
|
|
143
119
|
): ThreadEvent[] => {
|
|
144
120
|
if (bubble.kind !== 'assistant' && bubble.kind !== 'user') {
|
|
145
121
|
return [];
|
|
@@ -152,9 +128,8 @@ const cursorBubbleToThreadEvents = (
|
|
|
152
128
|
}
|
|
153
129
|
|
|
154
130
|
if (bubble.text.trim()) {
|
|
155
|
-
const phase =
|
|
156
|
-
|
|
157
|
-
events.push(buildMessageEvent(bubble, baseSequence + 1, bubble.kind, bubble.text, phase));
|
|
131
|
+
const phase = getCursorTextBubblePhase(bubble, finalAssistantTextBubbleIds);
|
|
132
|
+
events.push(buildMessageEvent(bubble, baseSequence + 1, bubble.kind, bubble.text.trim(), phase));
|
|
158
133
|
}
|
|
159
134
|
|
|
160
135
|
if (bubble.kind === 'assistant' && bubble.toolCall) {
|
|
@@ -169,61 +144,11 @@ const cursorBubbleToThreadEvents = (
|
|
|
169
144
|
};
|
|
170
145
|
|
|
171
146
|
export const cursorTranscriptToThreadEvents = (transcript: CursorThreadTranscript): ThreadEvent[] => {
|
|
172
|
-
const
|
|
173
|
-
return transcript.bubbles.flatMap((bubble, index) =>
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
const updateMessageStats = (stats: ThreadTranscriptStats, event: Extract<ThreadEvent, { kind: 'message' }>) => {
|
|
177
|
-
stats.messageCount += 1;
|
|
178
|
-
if (event.role === 'assistant') {
|
|
179
|
-
stats.assistantMessageCount += 1;
|
|
180
|
-
}
|
|
181
|
-
if (event.role === 'user') {
|
|
182
|
-
stats.userMessageCount += 1;
|
|
183
|
-
}
|
|
184
|
-
if (event.phase === 'commentary') {
|
|
185
|
-
stats.commentaryCount += 1;
|
|
186
|
-
}
|
|
187
|
-
if (event.phase === 'final_answer') {
|
|
188
|
-
stats.finalAnswerCount += 1;
|
|
189
|
-
}
|
|
190
|
-
};
|
|
191
|
-
|
|
192
|
-
const updateCursorStats = (stats: ThreadTranscriptStats, event: ThreadEvent) => {
|
|
193
|
-
switch (event.kind) {
|
|
194
|
-
case 'message':
|
|
195
|
-
updateMessageStats(stats, event);
|
|
196
|
-
break;
|
|
197
|
-
case 'tool_call':
|
|
198
|
-
stats.toolCallCount += 1;
|
|
199
|
-
break;
|
|
200
|
-
case 'tool_output':
|
|
201
|
-
stats.toolOutputCount += 1;
|
|
202
|
-
break;
|
|
203
|
-
case 'web_search':
|
|
204
|
-
stats.webSearchEventCount += 1;
|
|
205
|
-
break;
|
|
206
|
-
default:
|
|
207
|
-
break;
|
|
208
|
-
}
|
|
147
|
+
const finalAssistantTextBubbleIds = getFinalCursorAssistantTextBubbleIds(transcript.bubbles);
|
|
148
|
+
return transcript.bubbles.flatMap((bubble, index) =>
|
|
149
|
+
cursorBubbleToThreadEvents(bubble, index, finalAssistantTextBubbleIds),
|
|
150
|
+
);
|
|
209
151
|
};
|
|
210
152
|
|
|
211
|
-
export const getCursorThreadTranscriptStats = (events: ThreadEvent[]): ThreadTranscriptStats =>
|
|
212
|
-
|
|
213
|
-
assistantMessageCount: 0,
|
|
214
|
-
commentaryCount: 0,
|
|
215
|
-
execCommandCount: 0,
|
|
216
|
-
finalAnswerCount: 0,
|
|
217
|
-
messageCount: 0,
|
|
218
|
-
toolCallCount: 0,
|
|
219
|
-
toolOutputCount: 0,
|
|
220
|
-
userMessageCount: 0,
|
|
221
|
-
webSearchEventCount: 0,
|
|
222
|
-
};
|
|
223
|
-
|
|
224
|
-
for (const event of events) {
|
|
225
|
-
updateCursorStats(stats, event);
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
return stats;
|
|
229
|
-
};
|
|
153
|
+
export const getCursorThreadTranscriptStats = (events: ThreadEvent[]): ThreadTranscriptStats =>
|
|
154
|
+
getThreadTranscriptStats(events);
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
export class DeleteBatchError<TId, TResult> extends AggregateError {
|
|
2
|
+
readonly failedIds: TId[];
|
|
3
|
+
readonly successfulIds: TId[];
|
|
4
|
+
readonly successfulResults: TResult[];
|
|
5
|
+
|
|
6
|
+
constructor(
|
|
7
|
+
failures: Array<{ id: TId; reason: unknown }>,
|
|
8
|
+
successes: Array<{ id: TId; result: TResult }>,
|
|
9
|
+
totalCount: number,
|
|
10
|
+
) {
|
|
11
|
+
const visibleFailedIds = failures.slice(0, 5).map((failure) => String(failure.id));
|
|
12
|
+
const omittedFailedIdCount = failures.length - visibleFailedIds.length;
|
|
13
|
+
const failedIdSummary = `${visibleFailedIds.join(', ')}${omittedFailedIdCount > 0 ? ` (+${omittedFailedIdCount} more)` : ''}`;
|
|
14
|
+
super(
|
|
15
|
+
failures.map((failure) => failure.reason),
|
|
16
|
+
`${failures.length} of ${totalCount} deletions failed: ${failedIdSummary}`,
|
|
17
|
+
);
|
|
18
|
+
this.name = 'DeleteBatchError';
|
|
19
|
+
this.failedIds = failures.map((failure) => failure.id);
|
|
20
|
+
this.successfulIds = successes.map((success) => success.id);
|
|
21
|
+
this.successfulResults = successes.map((success) => success.result);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const runDeleteBatch = async <TId, TResult>(
|
|
26
|
+
ids: TId[],
|
|
27
|
+
deleteOne: (id: TId) => Promise<TResult>,
|
|
28
|
+
): Promise<TResult[]> => {
|
|
29
|
+
const settled = await Promise.allSettled(ids.map((id) => deleteOne(id)));
|
|
30
|
+
const failures: Array<{ id: TId; reason: unknown }> = [];
|
|
31
|
+
const successes: Array<{ id: TId; result: TResult }> = [];
|
|
32
|
+
for (const [index, result] of settled.entries()) {
|
|
33
|
+
const id = ids[index]!;
|
|
34
|
+
if (result.status === 'rejected') {
|
|
35
|
+
failures.push({ id, reason: result.reason });
|
|
36
|
+
} else {
|
|
37
|
+
successes.push({ id, result: result.value });
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
if (failures.length > 0) {
|
|
41
|
+
throw new DeleteBatchError(failures, successes, ids.length);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return successes.map((success) => success.result);
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export const requireDeletedItems = <T>(deletedIds: T[], label: string, id: string): T[] => {
|
|
48
|
+
if (deletedIds.length === 0) {
|
|
49
|
+
throw new Error(`${label} not found: ${id}`);
|
|
50
|
+
}
|
|
51
|
+
return deletedIds;
|
|
52
|
+
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { isSqliteDatabaseError } from '@spiracha/lib/sqlite-error';
|
|
2
|
+
|
|
3
|
+
export type ErrorPresentation = {
|
|
4
|
+
description: string;
|
|
5
|
+
isDatabaseError: boolean;
|
|
6
|
+
title: string;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
type ErrorPresentationOptions = {
|
|
10
|
+
fallbackTitle: string;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const DATABASE_ERROR_DESCRIPTION =
|
|
14
|
+
'Spiracha could not open a local conversation database. The source application may have an exclusive lock on the file, or the database may not exist yet. Close the relevant application or wait a moment, then reload.';
|
|
15
|
+
|
|
16
|
+
const redactLocalPaths = (message: string): string => {
|
|
17
|
+
return message
|
|
18
|
+
.split(/(https?:\/\/[^\s]+)/gu)
|
|
19
|
+
.map((part, index) =>
|
|
20
|
+
index % 2 === 1
|
|
21
|
+
? part
|
|
22
|
+
: part
|
|
23
|
+
.replace(/\/(?:[^/\s]+\/)+[^/\s]+/gu, '[local path]')
|
|
24
|
+
.replace(/[A-Z]:\\(?:[^\\\s]+\\)+[^\\\s]+/gu, '[local path]'),
|
|
25
|
+
)
|
|
26
|
+
.join('');
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export const getErrorPresentation = (error: Error, options: ErrorPresentationOptions): ErrorPresentation => {
|
|
30
|
+
if (isSqliteDatabaseError(error)) {
|
|
31
|
+
return {
|
|
32
|
+
description: DATABASE_ERROR_DESCRIPTION,
|
|
33
|
+
isDatabaseError: true,
|
|
34
|
+
title: 'Database unavailable',
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
description: redactLocalPaths(error.message),
|
|
40
|
+
isDatabaseError: false,
|
|
41
|
+
title: options.fallbackTitle,
|
|
42
|
+
};
|
|
43
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { ExportDialogOptions } from '#/lib/export-options';
|
|
2
|
+
|
|
3
|
+
export type ExportSelectionMutationInput = Readonly<{
|
|
4
|
+
ids: readonly string[];
|
|
5
|
+
options: Readonly<ExportDialogOptions>;
|
|
6
|
+
}>;
|
|
7
|
+
|
|
8
|
+
export const createExportSelectionMutationInput = (
|
|
9
|
+
ids: readonly string[],
|
|
10
|
+
options: ExportDialogOptions,
|
|
11
|
+
): ExportSelectionMutationInput => {
|
|
12
|
+
if (ids.length === 0) {
|
|
13
|
+
throw new Error('No conversations selected for export');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return {
|
|
17
|
+
ids: [...ids],
|
|
18
|
+
options: { ...options },
|
|
19
|
+
};
|
|
20
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export type ExportDialogOptions = {
|
|
2
|
+
includeCommentary: boolean;
|
|
3
|
+
includeMetadata: boolean;
|
|
4
|
+
includeTools: boolean;
|
|
5
|
+
outputFormat: 'md' | 'txt';
|
|
6
|
+
zipArchive: boolean;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export const DEFAULT_EXPORT_DIALOG_OPTIONS: ExportDialogOptions = {
|
|
10
|
+
includeCommentary: false,
|
|
11
|
+
includeMetadata: true,
|
|
12
|
+
includeTools: true,
|
|
13
|
+
outputFormat: 'md',
|
|
14
|
+
zipArchive: false,
|
|
15
|
+
};
|
|
@@ -11,11 +11,9 @@ type DateTimeFormatterSet = {
|
|
|
11
11
|
};
|
|
12
12
|
|
|
13
13
|
const DATE_TIME_FORMATTERS = new Map<string, DateTimeFormatterSet>();
|
|
14
|
-
const DEFAULT_TIME_ZONE = 'UTC';
|
|
15
14
|
|
|
16
15
|
const getDateTimeFormatters = (timeZone?: string): DateTimeFormatterSet => {
|
|
17
|
-
const
|
|
18
|
-
const cacheKey = resolvedTimeZone;
|
|
16
|
+
const cacheKey = timeZone ?? 'local';
|
|
19
17
|
const cached = DATE_TIME_FORMATTERS.get(cacheKey);
|
|
20
18
|
if (cached) {
|
|
21
19
|
return cached;
|
|
@@ -25,7 +23,7 @@ const getDateTimeFormatters = (timeZone?: string): DateTimeFormatterSet => {
|
|
|
25
23
|
dayKeyFormatter: new Intl.DateTimeFormat('en-CA', {
|
|
26
24
|
day: '2-digit',
|
|
27
25
|
month: '2-digit',
|
|
28
|
-
timeZone
|
|
26
|
+
timeZone,
|
|
29
27
|
year: 'numeric',
|
|
30
28
|
}),
|
|
31
29
|
timePartsFormatter: new Intl.DateTimeFormat('en-US', {
|
|
@@ -34,7 +32,7 @@ const getDateTimeFormatters = (timeZone?: string): DateTimeFormatterSet => {
|
|
|
34
32
|
hour12: true,
|
|
35
33
|
minute: '2-digit',
|
|
36
34
|
month: 'short',
|
|
37
|
-
timeZone
|
|
35
|
+
timeZone,
|
|
38
36
|
year: 'numeric',
|
|
39
37
|
}),
|
|
40
38
|
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { queryOptions } from '@tanstack/react-query';
|
|
2
|
+
import { getGrokSessionDetailFn, listGrokSessionsFn, listGrokWorkspacesFn } from './grok-server';
|
|
3
|
+
|
|
4
|
+
export const grokWorkspacesQueryOptions = () =>
|
|
5
|
+
queryOptions({
|
|
6
|
+
queryFn: () => listGrokWorkspacesFn(),
|
|
7
|
+
queryKey: ['grok-workspaces'],
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
export const grokSessionsQueryOptions = (workspaceKey: string | null) =>
|
|
11
|
+
queryOptions({
|
|
12
|
+
enabled: workspaceKey !== null,
|
|
13
|
+
queryFn: () => listGrokSessionsFn({ data: { workspaceKey: workspaceKey ?? '' } }),
|
|
14
|
+
queryKey: ['grok-sessions', workspaceKey ?? 'none'],
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
export const grokSessionDetailQueryOptions = (sessionId: string | null) =>
|
|
18
|
+
queryOptions({
|
|
19
|
+
enabled: sessionId !== null,
|
|
20
|
+
queryFn: () => getGrokSessionDetailFn({ data: { sessionId: sessionId ?? '' } }),
|
|
21
|
+
queryKey: ['grok-session', sessionId ?? 'none'],
|
|
22
|
+
});
|