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
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
isClaudeCodeSyntheticTranscriptEntry,
|
|
16
16
|
} from './claude-code-transcript-phase';
|
|
17
17
|
import { mapWithConcurrency } from './concurrency';
|
|
18
|
+
import { getPortablePathBasename } from './portable-path';
|
|
18
19
|
import {
|
|
19
20
|
asBoolean,
|
|
20
21
|
asNumber,
|
|
@@ -22,9 +23,9 @@ import {
|
|
|
22
23
|
asString,
|
|
23
24
|
cleanExtractedText,
|
|
24
25
|
cleanInlineTitle,
|
|
25
|
-
getPortablePathBasename,
|
|
26
26
|
isWorkspacePathQuery,
|
|
27
27
|
type JsonValue,
|
|
28
|
+
readDirectoryEntriesIfExists,
|
|
28
29
|
readJsonlObjects,
|
|
29
30
|
workspacePathMatchesQuery,
|
|
30
31
|
} from './shared';
|
|
@@ -579,13 +580,14 @@ const markLatestAssistantAsCommentary = (entries: ClaudeCodeTranscriptEntry[]):
|
|
|
579
580
|
}
|
|
580
581
|
};
|
|
581
582
|
|
|
582
|
-
const
|
|
583
|
-
|
|
584
|
-
raw.type === 'queue-operation'
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
583
|
+
const isTaskNotification = (raw: Record<string, JsonValue>): boolean => {
|
|
584
|
+
const content =
|
|
585
|
+
raw.type === 'queue-operation'
|
|
586
|
+
? asString(raw.content ?? null)
|
|
587
|
+
: raw.type === 'user'
|
|
588
|
+
? textFromContentValue(asObject(raw.message ?? null)?.content)
|
|
589
|
+
: null;
|
|
590
|
+
return content?.trimStart().startsWith('<task-notification>') === true;
|
|
589
591
|
};
|
|
590
592
|
|
|
591
593
|
const buildTranscriptFromRawEvents = (
|
|
@@ -612,7 +614,7 @@ const buildTranscriptFromRawEvents = (
|
|
|
612
614
|
updateTimeline(timeline, raw);
|
|
613
615
|
updateIdentityFromRaw(identity, raw);
|
|
614
616
|
|
|
615
|
-
if (
|
|
617
|
+
if (isTaskNotification(raw)) {
|
|
616
618
|
markLatestAssistantAsCommentary(entries);
|
|
617
619
|
}
|
|
618
620
|
|
|
@@ -669,7 +671,7 @@ const readTranscriptFile = async (
|
|
|
669
671
|
|
|
670
672
|
const listTranscriptFilesForProject = async (projectsDir: string, directoryName: string): Promise<TranscriptFile[]> => {
|
|
671
673
|
const projectDir = path.join(projectsDir, directoryName);
|
|
672
|
-
const entries = await
|
|
674
|
+
const entries = await readDirectoryEntriesIfExists(projectDir);
|
|
673
675
|
return entries
|
|
674
676
|
.filter((entry) => entry.isFile() && entry.name.endsWith('.jsonl'))
|
|
675
677
|
.map((entry) => ({
|
|
@@ -928,7 +930,11 @@ const compareNullableMsDesc = (left: number | null, right: number | null): numbe
|
|
|
928
930
|
};
|
|
929
931
|
|
|
930
932
|
const toWorkspaceGroup = (directoryName: string, sessions: ClaudeCodeSessionSummary[]): ClaudeCodeWorkspaceGroup => {
|
|
931
|
-
const
|
|
933
|
+
const decodedWorktree = decodeWorktreeFromDirectoryName(directoryName);
|
|
934
|
+
const worktree =
|
|
935
|
+
sessions.find((session) => session.worktree !== decodedWorktree)?.worktree ??
|
|
936
|
+
sessions[0]?.worktree ??
|
|
937
|
+
decodedWorktree;
|
|
932
938
|
const lastActiveAtMs = sessions.reduce<number | null>((latest, session) => {
|
|
933
939
|
if (session.lastActiveAtMs === null) {
|
|
934
940
|
return latest;
|
|
@@ -1014,17 +1020,17 @@ export const findClaudeCodeWorkspaceGroups = (
|
|
|
1014
1020
|
return groups.filter((group) => claudeCodeWorkspaceMatchesQuery(group, query));
|
|
1015
1021
|
};
|
|
1016
1022
|
|
|
1023
|
+
const compareSessions = (left: ClaudeCodeSessionSummary, right: ClaudeCodeSessionSummary) =>
|
|
1024
|
+
compareNullableMsDesc(left.lastActiveAtMs, right.lastActiveAtMs) || left.title.localeCompare(right.title);
|
|
1025
|
+
|
|
1017
1026
|
const sortSessions = (sessions: ClaudeCodeSessionSummary[]): ClaudeCodeSessionSummary[] => {
|
|
1018
|
-
return [...sessions].sort(
|
|
1019
|
-
(left, right) =>
|
|
1020
|
-
compareNullableMsDesc(left.lastActiveAtMs, right.lastActiveAtMs) || left.title.localeCompare(right.title),
|
|
1021
|
-
);
|
|
1027
|
+
return [...sessions].sort(compareSessions);
|
|
1022
1028
|
};
|
|
1023
1029
|
|
|
1024
|
-
export const
|
|
1030
|
+
export const listClaudeCodeSessionTranscriptsForGroup = async (
|
|
1025
1031
|
workspaceKey: string,
|
|
1026
1032
|
projectsDir = resolveClaudeCodeProjectsDir(),
|
|
1027
|
-
): Promise<
|
|
1033
|
+
): Promise<ClaudeCodeSessionTranscript[]> => {
|
|
1028
1034
|
const directoryName = getDirectoryNameFromWorkspaceKey(workspaceKey);
|
|
1029
1035
|
if (!directoryName || !(await pathExists(projectsDir))) {
|
|
1030
1036
|
return [];
|
|
@@ -1032,12 +1038,32 @@ export const listClaudeCodeSessionsForGroup = async (
|
|
|
1032
1038
|
|
|
1033
1039
|
const files = await listTranscriptFilesForProject(projectsDir, directoryName);
|
|
1034
1040
|
const transcripts = coalesceTranscriptLineages(await readTranscriptFiles(files));
|
|
1035
|
-
return
|
|
1041
|
+
return transcripts.filter(hasSessionContent).sort((left, right) => compareSessions(left.session, right.session));
|
|
1042
|
+
};
|
|
1043
|
+
|
|
1044
|
+
export const listClaudeCodeSessionsForGroup = async (
|
|
1045
|
+
workspaceKey: string,
|
|
1046
|
+
projectsDir = resolveClaudeCodeProjectsDir(),
|
|
1047
|
+
): Promise<ClaudeCodeSessionSummary[]> => {
|
|
1048
|
+
return sortSessions(
|
|
1049
|
+
(await listClaudeCodeSessionTranscriptsForGroup(workspaceKey, projectsDir)).map(
|
|
1050
|
+
(transcript) => transcript.session,
|
|
1051
|
+
),
|
|
1052
|
+
);
|
|
1036
1053
|
};
|
|
1037
1054
|
|
|
1038
1055
|
const locateSessionFile = async (projectsDir: string, sessionId: string): Promise<TranscriptFile | null> => {
|
|
1039
1056
|
const files = await listTranscriptFiles(projectsDir);
|
|
1040
|
-
|
|
1057
|
+
const filenameMatch = files.find((file) => path.basename(file.filePath, '.jsonl') === sessionId);
|
|
1058
|
+
if (filenameMatch) {
|
|
1059
|
+
return filenameMatch;
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
const bodyMatches = await mapWithConcurrency(files, READ_CONCURRENCY, async (file) => {
|
|
1063
|
+
const parsed = await readTranscriptFile(file, { includeRawPayloads: false });
|
|
1064
|
+
return parsed?.transcript.session.sessionId === sessionId ? file : null;
|
|
1065
|
+
});
|
|
1066
|
+
return bodyMatches.find((file): file is TranscriptFile => file !== null) ?? null;
|
|
1041
1067
|
};
|
|
1042
1068
|
|
|
1043
1069
|
export const readClaudeCodeSessionTranscript = async (
|
|
@@ -2,8 +2,9 @@ import { listScopedThreads } from './codex-browser-db';
|
|
|
2
2
|
import type { CodexAnalytics, DistributionItem, ModelTokenSummary } from './codex-browser-types';
|
|
3
3
|
import type { ThreadRow } from './codex-thread-types';
|
|
4
4
|
import { mapWithConcurrency } from './concurrency';
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
5
|
+
import { getPortablePathBasename } from './portable-path';
|
|
6
|
+
import { asObject, asString, readJsonlObjects } from './shared';
|
|
7
|
+
import { hashCacheKeyPartsIterable, withCachedJson } from './ui-cache';
|
|
7
8
|
|
|
8
9
|
export type CodexAnalyticsInput = {
|
|
9
10
|
dbPath: string;
|
|
@@ -38,6 +39,20 @@ const roundToTwoDecimals = (value: number) => {
|
|
|
38
39
|
return Number(value.toFixed(2));
|
|
39
40
|
};
|
|
40
41
|
|
|
42
|
+
const median = (values: number[]) => {
|
|
43
|
+
if (values.length === 0) {
|
|
44
|
+
return 0;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const sorted = [...values].sort((left, right) => left - right);
|
|
48
|
+
const midpoint = Math.floor(sorted.length / 2);
|
|
49
|
+
if (sorted.length % 2 === 1) {
|
|
50
|
+
return sorted[midpoint]!;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return roundToTwoDecimals((sorted[midpoint - 1]! + sorted[midpoint]!) / 2);
|
|
54
|
+
};
|
|
55
|
+
|
|
41
56
|
const incrementCount = (counts: Map<string, number>, key: string) => {
|
|
42
57
|
counts.set(key, (counts.get(key) ?? 0) + 1);
|
|
43
58
|
};
|
|
@@ -90,6 +105,8 @@ const threadMetadataCacheKeyParts = (thread: ThreadRow) => [
|
|
|
90
105
|
String(thread.archived_at ?? ''),
|
|
91
106
|
thread.cwd,
|
|
92
107
|
thread.model ?? '',
|
|
108
|
+
thread.reasoning_effort ?? '',
|
|
109
|
+
thread.source,
|
|
93
110
|
thread.model_provider,
|
|
94
111
|
thread.cli_version,
|
|
95
112
|
thread.title,
|
|
@@ -98,7 +115,7 @@ const threadMetadataCacheKeyParts = (thread: ThreadRow) => [
|
|
|
98
115
|
|
|
99
116
|
export const buildCodexAnalyticsCacheKey = (dbPath: string, threads: ThreadRow[], project: string | null) => {
|
|
100
117
|
const parts = (function* () {
|
|
101
|
-
yield '
|
|
118
|
+
yield 'v3';
|
|
102
119
|
yield dbPath;
|
|
103
120
|
yield project ?? 'all';
|
|
104
121
|
yield String(threads.length);
|
|
@@ -111,7 +128,7 @@ export const buildCodexAnalyticsCacheKey = (dbPath: string, threads: ThreadRow[]
|
|
|
111
128
|
};
|
|
112
129
|
|
|
113
130
|
const buildThreadAnalyticsCacheKey = (thread: ThreadRow) => {
|
|
114
|
-
return `thread-analytics-${
|
|
131
|
+
return `thread-analytics-${hashCacheKeyPartsIterable(['v1', ...threadMetadataCacheKeyParts(thread)])}`;
|
|
115
132
|
};
|
|
116
133
|
|
|
117
134
|
const parseThreadAnalyticsFile = async (sessionFile: string): Promise<ThreadAnalyticsSummary> => {
|
|
@@ -129,7 +146,7 @@ const parseThreadAnalyticsFile = async (sessionFile: string): Promise<ThreadAnal
|
|
|
129
146
|
}
|
|
130
147
|
|
|
131
148
|
const payloadType = asString(payload.type);
|
|
132
|
-
if (payloadType === 'function_call') {
|
|
149
|
+
if (payloadType === 'function_call' || payloadType === 'custom_tool_call') {
|
|
133
150
|
toolNames.push(asString(payload.name) ?? 'unknown');
|
|
134
151
|
continue;
|
|
135
152
|
}
|
|
@@ -155,6 +172,8 @@ export const computeCodexAnalyticsFromThreads = async (
|
|
|
155
172
|
): Promise<CodexAnalytics> => {
|
|
156
173
|
const totalTokens = threads.reduce((sum, thread) => sum + thread.tokens_used, 0);
|
|
157
174
|
const projectNames = new Set(threads.map((thread) => getPortablePathBasename(thread.cwd)).filter(Boolean));
|
|
175
|
+
const reasoningEfforts = new Map<string, number>();
|
|
176
|
+
const sources = new Map<string, number>();
|
|
158
177
|
const toolUsage = new Map<string, number>();
|
|
159
178
|
let threadsWithWebSearch = 0;
|
|
160
179
|
const loadThreadAnalytics = options.loadThreadAnalytics ?? getCachedThreadAnalytics;
|
|
@@ -163,6 +182,11 @@ export const computeCodexAnalyticsFromThreads = async (
|
|
|
163
182
|
loadThreadAnalytics(thread),
|
|
164
183
|
);
|
|
165
184
|
|
|
185
|
+
for (const thread of threads) {
|
|
186
|
+
incrementCount(reasoningEfforts, thread.reasoning_effort?.trim() || 'unspecified');
|
|
187
|
+
incrementCount(sources, thread.source.trim() || 'unknown');
|
|
188
|
+
}
|
|
189
|
+
|
|
166
190
|
for (const analytics of threadAnalytics) {
|
|
167
191
|
if (analytics.hasWebSearch) {
|
|
168
192
|
threadsWithWebSearch += 1;
|
|
@@ -175,10 +199,13 @@ export const computeCodexAnalyticsFromThreads = async (
|
|
|
175
199
|
|
|
176
200
|
return {
|
|
177
201
|
modelsByTokens: buildModelsByTokens(threads),
|
|
202
|
+
reasoningEfforts: toDistribution(reasoningEfforts),
|
|
203
|
+
sources: toDistribution(sources),
|
|
178
204
|
summary: {
|
|
179
205
|
archivedThreads: threads.filter((thread) => Boolean(thread.archived)).length,
|
|
180
206
|
averageTokensPerThread: threads.length === 0 ? 0 : roundToTwoDecimals(totalTokens / threads.length),
|
|
181
207
|
distinctToolNames: toolUsage.size,
|
|
208
|
+
medianTokensPerThread: median(threads.map((thread) => thread.tokens_used)),
|
|
182
209
|
threadsWithWebSearch,
|
|
183
210
|
totalProjects: projectNames.size,
|
|
184
211
|
totalThreads: threads.length,
|