spiracha 2.0.0 → 2.1.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/README.md +6 -1
- package/apps/ui/README.md +20 -2
- package/apps/ui/package.json +2 -2
- package/apps/ui/src/components/antigravity-conversations-table.tsx +50 -11
- package/apps/ui/src/components/antigravity-workspaces-table.tsx +1 -1
- package/apps/ui/src/components/app-shell.tsx +1 -0
- 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 +7 -5
- package/apps/ui/src/components/export-dialog.tsx +2 -12
- 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/qoder-sessions-table.tsx +23 -2
- package/apps/ui/src/components/qoder-workspaces-table.tsx +5 -1
- package/apps/ui/src/components/selection-actions-toolbar.tsx +80 -0
- package/apps/ui/src/components/threads-table.tsx +9 -46
- 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 +152 -7
- package/apps/ui/src/lib/antigravity-transcript-events.ts +11 -3
- package/apps/ui/src/lib/claude-code-queries.ts +8 -0
- package/apps/ui/src/lib/claude-code-server.ts +175 -7
- package/apps/ui/src/lib/claude-code-transcript-events.ts +8 -1
- package/apps/ui/src/lib/codex-queries.ts +19 -3
- package/apps/ui/src/lib/codex-server.ts +135 -53
- package/apps/ui/src/lib/cursor-server.ts +48 -5
- 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 +169 -0
- package/apps/ui/src/lib/grok-transcript-events.ts +149 -0
- package/apps/ui/src/lib/kiro-server.ts +85 -7
- package/apps/ui/src/lib/opencode-server.ts +85 -7
- package/apps/ui/src/lib/qoder-server.ts +67 -11
- package/apps/ui/src/lib/route-search.ts +114 -0
- package/apps/ui/src/lib/source-session-export-server.ts +86 -3
- package/apps/ui/src/lib/thread-transcript-load.ts +15 -0
- package/apps/ui/src/lib/workspace-delete-navigation.ts +12 -0
- package/apps/ui/src/routeTree.gen.ts +107 -0
- package/apps/ui/src/routes/antigravity-conversations.$conversationId.tsx +76 -10
- package/apps/ui/src/routes/antigravity.$workspaceKey.tsx +269 -31
- 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 +143 -17
- package/apps/ui/src/routes/claude-code.$workspaceKey.tsx +165 -26
- package/apps/ui/src/routes/cursor-threads.$composerId.tsx +2 -2
- package/apps/ui/src/routes/cursor.$workspaceKey.tsx +11 -1
- package/apps/ui/src/routes/grok-sessions.$sessionId.tsx +388 -0
- package/apps/ui/src/routes/grok.$workspaceKey.tsx +301 -0
- package/apps/ui/src/routes/grok.index.tsx +48 -0
- package/apps/ui/src/routes/kiro-sessions.$sessionId.tsx +66 -15
- package/apps/ui/src/routes/kiro.$workspaceKey.tsx +199 -32
- package/apps/ui/src/routes/opencode-sessions.$sessionId.tsx +105 -19
- package/apps/ui/src/routes/opencode.$workspaceKey.tsx +236 -44
- package/apps/ui/src/routes/opencode.index.tsx +16 -3
- package/apps/ui/src/routes/qoder-sessions.$sessionId.tsx +7 -4
- package/apps/ui/src/routes/qoder.$workspaceKey.tsx +75 -28
- package/apps/ui/src/routes/threads.$threadId.tsx +548 -64
- package/package.json +19 -18
- package/src/client.ts +134 -1
- package/src/lib/antigravity-db.ts +208 -32
- package/src/lib/antigravity-exporter-types.ts +3 -0
- package/src/lib/antigravity-projects.ts +201 -0
- package/src/lib/claude-code-db.ts +498 -32
- 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-browser-export.ts +23 -27
- package/src/lib/codex-thread-cache.ts +41 -13
- package/src/lib/codex-thread-parser.ts +86 -35
- package/src/lib/codex-transcript-filter.ts +31 -0
- package/src/lib/codex-transcript-renderer.ts +39 -19
- package/src/lib/concurrency.ts +41 -0
- package/src/lib/conversation-api.ts +399 -19
- package/src/lib/conversation-data/antigravity-adapter.ts +21 -2
- package/src/lib/conversation-data/claude-code-adapter.ts +37 -6
- package/src/lib/conversation-data/codex-adapter.ts +28 -4
- package/src/lib/conversation-data/cursor-adapter.ts +35 -1
- package/src/lib/conversation-data/grok-adapter.ts +210 -0
- package/src/lib/conversation-data/index.ts +76 -1
- package/src/lib/conversation-data/kiro-adapter.ts +41 -7
- package/src/lib/conversation-data/opencode-adapter.ts +24 -2
- package/src/lib/conversation-data/qoder-adapter.ts +44 -19
- package/src/lib/conversation-data/types.ts +43 -0
- package/src/lib/conversation-zip-export.ts +57 -0
- package/src/lib/cursor-db.ts +34 -5
- package/src/lib/grok-db.ts +1026 -0
- package/src/lib/grok-exporter-types.ts +110 -0
- package/src/lib/grok-transcript-phase.ts +52 -0
- package/src/lib/grok-transcript.ts +154 -0
- package/src/lib/kiro-db.ts +52 -1
- package/src/lib/model-label.ts +11 -3
- package/src/lib/opencode-db.ts +598 -55
- package/src/lib/opencode-transcript.ts +8 -5
- package/src/lib/shared.ts +12 -0
- package/src/lib/transcript-load-limiter.ts +82 -0
- package/src/lib/ui-cache.ts +43 -17
- package/src/lib/ui-export-archive.ts +60 -0
|
@@ -88,10 +88,11 @@ const renderTextPart = (
|
|
|
88
88
|
options: OpenCodeExportOptions,
|
|
89
89
|
finalAssistantTextPartIds: Set<string>,
|
|
90
90
|
): string => {
|
|
91
|
+
const rawText = part.text ?? '';
|
|
91
92
|
const { reasoningBlocks, visibleText } =
|
|
92
93
|
part.role === 'assistant'
|
|
93
|
-
? splitOpenCodeThinkTaggedText(
|
|
94
|
-
: { reasoningBlocks: [], visibleText:
|
|
94
|
+
? splitOpenCodeThinkTaggedText(rawText)
|
|
95
|
+
: { reasoningBlocks: [], visibleText: rawText };
|
|
95
96
|
const sections: string[] = [];
|
|
96
97
|
|
|
97
98
|
if (options.includeCommentary) {
|
|
@@ -119,7 +120,8 @@ const renderReasoningPart = (part: OpenCodeTranscriptPart, options: OpenCodeExpo
|
|
|
119
120
|
return '';
|
|
120
121
|
}
|
|
121
122
|
|
|
122
|
-
const
|
|
123
|
+
const rawText = part.text ?? '';
|
|
124
|
+
const { reasoningBlocks, visibleText } = splitOpenCodeThinkTaggedText(rawText);
|
|
123
125
|
const text = cleanExtractedText([...reasoningBlocks, visibleText].filter(Boolean).join('\n\n')).trim();
|
|
124
126
|
return text ? renderSection('Reasoning', text, options.outputFormat) : '';
|
|
125
127
|
};
|
|
@@ -143,8 +145,9 @@ const renderToolPart = (part: OpenCodeTranscriptPart, options: OpenCodeExportOpt
|
|
|
143
145
|
if (part.argumentsText?.trim()) {
|
|
144
146
|
lines.push('', 'Input:', '', renderCodeBlock(part.argumentsText.trim(), options.outputFormat));
|
|
145
147
|
}
|
|
146
|
-
|
|
147
|
-
|
|
148
|
+
const outputText = part.outputText ?? '';
|
|
149
|
+
if (outputText.trim()) {
|
|
150
|
+
lines.push('', 'Output:', '', renderCodeBlock(truncateOutput(outputText.trim()), options.outputFormat));
|
|
148
151
|
}
|
|
149
152
|
|
|
150
153
|
return renderSection('Tool Call', lines.join('\n'), options.outputFormat);
|
package/src/lib/shared.ts
CHANGED
|
@@ -86,6 +86,18 @@ export const cleanExtractedText = (text: string): string => {
|
|
|
86
86
|
return text.replace(/^\s*<\/?image>\s*$/gm, '').replace(/\n{3,}/g, '\n\n');
|
|
87
87
|
};
|
|
88
88
|
|
|
89
|
+
const CODEX_APP_DIRECTIVE_PATTERN =
|
|
90
|
+
/^::(?:code-comment|created-thread|git-commit|git-create-branch|git-create-pr|git-push|git-stage)\{.*\}\s*$/u;
|
|
91
|
+
|
|
92
|
+
export const stripCodexAppDirectiveLines = (text: string): string => {
|
|
93
|
+
return text
|
|
94
|
+
.split('\n')
|
|
95
|
+
.filter((line) => !CODEX_APP_DIRECTIVE_PATTERN.test(line.trim()))
|
|
96
|
+
.join('\n')
|
|
97
|
+
.replace(/\n{3,}/g, '\n\n')
|
|
98
|
+
.trim();
|
|
99
|
+
};
|
|
100
|
+
|
|
89
101
|
export const formatModelLabel = formatSharedModelLabel;
|
|
90
102
|
|
|
91
103
|
export const asObject = (value: JsonValue): Record<string, JsonValue> | null => {
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { createConcurrencyLimiter } from './concurrency';
|
|
2
|
+
|
|
3
|
+
const DEFAULT_TRANSCRIPT_LOAD_CONCURRENCY = 3;
|
|
4
|
+
const MAX_TRANSCRIPT_LOAD_CONCURRENCY = 16;
|
|
5
|
+
type TranscriptLoadLogContext = {
|
|
6
|
+
id?: string;
|
|
7
|
+
path?: string;
|
|
8
|
+
source?: string;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
let nextTranscriptLoadId = 1;
|
|
12
|
+
let activeTranscriptLoads = 0;
|
|
13
|
+
let queuedTranscriptLoads = 0;
|
|
14
|
+
|
|
15
|
+
export const resolveTranscriptLoadConcurrency = (value = process.env.SPIRACHA_TRANSCRIPT_LOAD_CONCURRENCY): number => {
|
|
16
|
+
const parsed = Number.parseInt(value ?? '', 10);
|
|
17
|
+
return Number.isFinite(parsed) && parsed > 0
|
|
18
|
+
? Math.min(parsed, MAX_TRANSCRIPT_LOAD_CONCURRENCY)
|
|
19
|
+
: DEFAULT_TRANSCRIPT_LOAD_CONCURRENCY;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const transcriptLoadLimiter = createConcurrencyLimiter(resolveTranscriptLoadConcurrency());
|
|
23
|
+
|
|
24
|
+
const shouldLogTranscriptLoads = () => process.env.SPIRACHA_TRANSCRIPT_LOAD_LOGS !== '0';
|
|
25
|
+
|
|
26
|
+
const logTranscriptLoad = (event: string, details: Record<string, unknown>) => {
|
|
27
|
+
if (shouldLogTranscriptLoads()) {
|
|
28
|
+
console.info(`[spiracha:transcript-load] ${event}`, details);
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export const runWithTranscriptLoadLimit = async <T>(
|
|
33
|
+
loader: () => Promise<T>,
|
|
34
|
+
context: TranscriptLoadLogContext = {},
|
|
35
|
+
): Promise<T> => {
|
|
36
|
+
const loadId = nextTranscriptLoadId;
|
|
37
|
+
nextTranscriptLoadId += 1;
|
|
38
|
+
queuedTranscriptLoads += 1;
|
|
39
|
+
const queuedAt = Date.now();
|
|
40
|
+
|
|
41
|
+
return transcriptLoadLimiter(async () => {
|
|
42
|
+
queuedTranscriptLoads -= 1;
|
|
43
|
+
activeTranscriptLoads += 1;
|
|
44
|
+
const startedAt = Date.now();
|
|
45
|
+
logTranscriptLoad('start', {
|
|
46
|
+
active: activeTranscriptLoads,
|
|
47
|
+
id: context.id,
|
|
48
|
+
loadId,
|
|
49
|
+
path: context.path,
|
|
50
|
+
queued: queuedTranscriptLoads,
|
|
51
|
+
source: context.source,
|
|
52
|
+
waitMs: startedAt - queuedAt,
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
return await loader();
|
|
57
|
+
} catch (error) {
|
|
58
|
+
logTranscriptLoad('error', {
|
|
59
|
+
active: activeTranscriptLoads,
|
|
60
|
+
durationMs: Date.now() - startedAt,
|
|
61
|
+
error: error instanceof Error ? error.message : String(error),
|
|
62
|
+
id: context.id,
|
|
63
|
+
loadId,
|
|
64
|
+
path: context.path,
|
|
65
|
+
queued: queuedTranscriptLoads,
|
|
66
|
+
source: context.source,
|
|
67
|
+
});
|
|
68
|
+
throw error;
|
|
69
|
+
} finally {
|
|
70
|
+
activeTranscriptLoads -= 1;
|
|
71
|
+
logTranscriptLoad('finish', {
|
|
72
|
+
active: activeTranscriptLoads,
|
|
73
|
+
durationMs: Date.now() - startedAt,
|
|
74
|
+
id: context.id,
|
|
75
|
+
loadId,
|
|
76
|
+
path: context.path,
|
|
77
|
+
queued: queuedTranscriptLoads,
|
|
78
|
+
source: context.source,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
};
|
package/src/lib/ui-cache.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
-
import { mkdir, readdir, rename, rm, stat } from 'node:fs/promises';
|
|
2
|
+
import { chmod, mkdir, readdir, rename, rm, stat } from 'node:fs/promises';
|
|
3
3
|
import os from 'node:os';
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
|
|
@@ -11,8 +11,13 @@ type CacheEnvelope<T> = {
|
|
|
11
11
|
version: number;
|
|
12
12
|
};
|
|
13
13
|
|
|
14
|
+
type CacheReadResult<T> = { hit: true; value: T } | { hit: false };
|
|
15
|
+
|
|
16
|
+
const inFlightCacheLoads = new Map<string, Promise<unknown>>();
|
|
17
|
+
|
|
14
18
|
const ensureCacheDir = async () => {
|
|
15
|
-
await mkdir(CACHE_DIR, { recursive: true });
|
|
19
|
+
await mkdir(CACHE_DIR, { mode: 0o700, recursive: true });
|
|
20
|
+
await chmod(CACHE_DIR, 0o700);
|
|
16
21
|
};
|
|
17
22
|
|
|
18
23
|
const toCachePath = (key: string) => {
|
|
@@ -41,12 +46,12 @@ export const getFileFingerprint = async (filePath: string) => {
|
|
|
41
46
|
return `${filePath}:${metadata.size}:${metadata.mtimeMs}`;
|
|
42
47
|
};
|
|
43
48
|
|
|
44
|
-
|
|
49
|
+
const readCachedJson = async <T>(key: string): Promise<CacheReadResult<T>> => {
|
|
45
50
|
await ensureCacheDir();
|
|
46
51
|
const filePath = toCachePath(key);
|
|
47
52
|
const file = Bun.file(filePath);
|
|
48
53
|
if (!(await file.exists())) {
|
|
49
|
-
return
|
|
54
|
+
return { hit: false };
|
|
50
55
|
}
|
|
51
56
|
|
|
52
57
|
let parsed: CacheEnvelope<T> | T;
|
|
@@ -54,7 +59,7 @@ export const getCachedJson = async <T>(key: string): Promise<T | null> => {
|
|
|
54
59
|
parsed = (await file.json()) as CacheEnvelope<T> | T;
|
|
55
60
|
} catch {
|
|
56
61
|
await rm(filePath, { force: true });
|
|
57
|
-
return
|
|
62
|
+
return { hit: false };
|
|
58
63
|
}
|
|
59
64
|
|
|
60
65
|
if (
|
|
@@ -64,11 +69,16 @@ export const getCachedJson = async <T>(key: string): Promise<T | null> => {
|
|
|
64
69
|
(parsed as CacheEnvelope<T>).version === CACHE_ENVELOPE_VERSION &&
|
|
65
70
|
'value' in parsed
|
|
66
71
|
) {
|
|
67
|
-
return (parsed as CacheEnvelope<T>).value;
|
|
72
|
+
return { hit: true, value: (parsed as CacheEnvelope<T>).value };
|
|
68
73
|
}
|
|
69
74
|
|
|
70
75
|
await rm(filePath, { force: true });
|
|
71
|
-
return
|
|
76
|
+
return { hit: false };
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
export const getCachedJson = async <T>(key: string): Promise<T | null> => {
|
|
80
|
+
const cached = await readCachedJson<T>(key);
|
|
81
|
+
return cached.hit ? cached.value : null;
|
|
72
82
|
};
|
|
73
83
|
|
|
74
84
|
export const setCachedJson = async <T>(key: string, value: T) => {
|
|
@@ -80,21 +90,37 @@ export const setCachedJson = async <T>(key: string, value: T) => {
|
|
|
80
90
|
version: CACHE_ENVELOPE_VERSION,
|
|
81
91
|
};
|
|
82
92
|
|
|
83
|
-
|
|
84
|
-
|
|
93
|
+
try {
|
|
94
|
+
await Bun.write(tempPath, JSON.stringify(envelope));
|
|
95
|
+
await rename(tempPath, filePath);
|
|
96
|
+
} finally {
|
|
97
|
+
await rm(tempPath, { force: true });
|
|
98
|
+
}
|
|
85
99
|
};
|
|
86
100
|
|
|
87
101
|
export const withCachedJson = async <T>(key: string, loader: () => Promise<T>): Promise<T> => {
|
|
88
|
-
const
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
102
|
+
const cached = await readCachedJson<T>(key);
|
|
103
|
+
if (cached.hit) {
|
|
104
|
+
return cached.value;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const inFlight = inFlightCacheLoads.get(key);
|
|
108
|
+
if (inFlight) {
|
|
109
|
+
return (await inFlight) as T;
|
|
93
110
|
}
|
|
94
111
|
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
|
|
112
|
+
const load = (async () => {
|
|
113
|
+
const value = await loader();
|
|
114
|
+
await setCachedJson(key, value);
|
|
115
|
+
return value;
|
|
116
|
+
})();
|
|
117
|
+
inFlightCacheLoads.set(key, load);
|
|
118
|
+
|
|
119
|
+
try {
|
|
120
|
+
return await load;
|
|
121
|
+
} finally {
|
|
122
|
+
inFlightCacheLoads.delete(key);
|
|
123
|
+
}
|
|
98
124
|
};
|
|
99
125
|
|
|
100
126
|
export const invalidateCacheByPrefix = async (...prefixes: string[]) => {
|
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
import type { ExportFormat } from './shared';
|
|
2
2
|
|
|
3
|
+
type BatchExportNameEntry = {
|
|
4
|
+
cwd: string | null;
|
|
5
|
+
updatedAtMs: number | null;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
type ConversationExportNameEntry = BatchExportNameEntry & {
|
|
9
|
+
id: string;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const getPortablePathBasename = (value: string): string => {
|
|
13
|
+
const trimmed = value.replace(/[\\/]+$/u, '');
|
|
14
|
+
if (!trimmed) {
|
|
15
|
+
return '';
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const separatorIndex = Math.max(trimmed.lastIndexOf('/'), trimmed.lastIndexOf('\\'));
|
|
19
|
+
return separatorIndex === -1 ? trimmed : trimmed.slice(separatorIndex + 1);
|
|
20
|
+
};
|
|
21
|
+
|
|
3
22
|
export const sanitizeExportFileName = (value: string) => {
|
|
4
23
|
return value
|
|
5
24
|
.replace(/[<>:"/\\|?*\u0000-\u001f]/gu, ' ')
|
|
@@ -18,6 +37,47 @@ export const resolveUniqueExportFileBaseName = (baseName: string, usedCounts: Ma
|
|
|
18
37
|
return count === 1 ? baseName : `${baseName}-${count}`;
|
|
19
38
|
};
|
|
20
39
|
|
|
40
|
+
const formatBatchExportDate = (value: number) => {
|
|
41
|
+
const date = new Date(value);
|
|
42
|
+
const year = date.getUTCFullYear();
|
|
43
|
+
const month = String(date.getUTCMonth() + 1).padStart(2, '0');
|
|
44
|
+
const day = String(date.getUTCDate()).padStart(2, '0');
|
|
45
|
+
const hours = String(date.getUTCHours()).padStart(2, '0');
|
|
46
|
+
const minutes = String(date.getUTCMinutes()).padStart(2, '0');
|
|
47
|
+
return `${year}-${month}-${day}-${hours}${minutes}`;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const resolveExportProjectName = (cwd: string | null, fallbackProjectName: string) => {
|
|
51
|
+
return sanitizeExportFileName(getPortablePathBasename(cwd ?? '') || fallbackProjectName) || 'threads';
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export const buildConversationExportBaseName = (
|
|
55
|
+
{ cwd, id, updatedAtMs }: ConversationExportNameEntry,
|
|
56
|
+
fallbackProjectName: string,
|
|
57
|
+
) => {
|
|
58
|
+
const projectName = resolveExportProjectName(cwd, fallbackProjectName);
|
|
59
|
+
const shortId = sanitizeExportFileName(id).slice(0, 8) || 'conversation';
|
|
60
|
+
return Number.isFinite(updatedAtMs) && (updatedAtMs ?? 0) > 0
|
|
61
|
+
? `${projectName}-${formatBatchExportDate(updatedAtMs!)}-${shortId}`
|
|
62
|
+
: `${projectName}-${shortId}`;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
export const buildBatchExportBaseName = (entries: BatchExportNameEntry[], fallbackProjectName: string) => {
|
|
66
|
+
if (entries.length === 0) {
|
|
67
|
+
throw new Error('No conversations selected for export');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const firstCwd = entries.find((entry) => entry.cwd?.trim())?.cwd ?? null;
|
|
71
|
+
const projectName = resolveExportProjectName(firstCwd, fallbackProjectName);
|
|
72
|
+
const latestUpdatedAtMs = Math.max(
|
|
73
|
+
...entries.map((entry) => (Number.isFinite(entry.updatedAtMs) ? (entry.updatedAtMs ?? 0) : 0)),
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
return latestUpdatedAtMs > 0
|
|
77
|
+
? `${projectName}-${formatBatchExportDate(latestUpdatedAtMs)}-threads-${entries.length}`
|
|
78
|
+
: `${projectName}-threads-${entries.length}`;
|
|
79
|
+
};
|
|
80
|
+
|
|
21
81
|
const readPipeText = async (pipe: ReadableStream<Uint8Array> | number | undefined) => {
|
|
22
82
|
return pipe && typeof pipe !== 'number' ? new Response(pipe).text() : '';
|
|
23
83
|
};
|