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.
Files changed (125) hide show
  1. package/AGENTS.md +27 -12
  2. package/README.md +37 -19
  3. package/apps/ui/AGENTS.md +18 -10
  4. package/apps/ui/README.md +17 -10
  5. package/apps/ui/src/components/analytics-breakdowns.tsx +106 -0
  6. package/apps/ui/src/components/antigravity-conversations-table.tsx +1 -0
  7. package/apps/ui/src/components/app-shell.tsx +45 -3
  8. package/apps/ui/src/components/data-table.tsx +17 -1
  9. package/apps/ui/src/components/delete-confirm-dialog.tsx +4 -3
  10. package/apps/ui/src/components/export-dialog.tsx +77 -62
  11. package/apps/ui/src/components/projects-table.tsx +4 -1
  12. package/apps/ui/src/components/route-error-panel.tsx +13 -0
  13. package/apps/ui/src/components/thread-goals-panel.tsx +50 -0
  14. package/apps/ui/src/components/thread-tools-panel.tsx +142 -0
  15. package/apps/ui/src/components/threads-table.tsx +4 -1
  16. package/apps/ui/src/lib/antigravity-server.ts +70 -17
  17. package/apps/ui/src/lib/antigravity-transcript-events.ts +46 -70
  18. package/apps/ui/src/lib/claude-code-server.ts +14 -4
  19. package/apps/ui/src/lib/codex-queries.ts +3 -3
  20. package/apps/ui/src/lib/codex-server.ts +3 -3
  21. package/apps/ui/src/lib/cursor-server.ts +41 -69
  22. package/apps/ui/src/lib/cursor-transcript-events.ts +11 -86
  23. package/apps/ui/src/lib/delete-batch.ts +52 -0
  24. package/apps/ui/src/lib/error-presentation.ts +43 -0
  25. package/apps/ui/src/lib/export-mutation.ts +20 -0
  26. package/apps/ui/src/lib/export-options.ts +15 -0
  27. package/apps/ui/src/lib/grok-server.ts +2 -3
  28. package/apps/ui/src/lib/kiro-server.ts +14 -2
  29. package/apps/ui/src/lib/mutation-error.ts +15 -0
  30. package/apps/ui/src/lib/opencode-server.ts +13 -2
  31. package/apps/ui/src/lib/path-utils.ts +1 -1
  32. package/apps/ui/src/lib/qoder-queries.ts +15 -0
  33. package/apps/ui/src/lib/qoder-transcript-events.ts +1 -1
  34. package/apps/ui/src/lib/route-state-reset.tsx +10 -0
  35. package/apps/ui/src/lib/settings-server.ts +33 -0
  36. package/apps/ui/src/lib/settings-store.tsx +82 -38
  37. package/apps/ui/src/lib/settings.ts +65 -0
  38. package/apps/ui/src/lib/source-session-export-server.ts +1 -1
  39. package/apps/ui/src/lib/thread-metadata.ts +22 -0
  40. package/apps/ui/src/lib/thread-transcript-stats.ts +10 -1
  41. package/apps/ui/src/lib/workspace-delete-navigation.ts +8 -0
  42. package/apps/ui/src/routeTree.gen.ts +282 -282
  43. package/apps/ui/src/routes/__root.tsx +12 -15
  44. package/apps/ui/src/routes/analytics.tsx +10 -61
  45. package/apps/ui/src/routes/antigravity-conversations.$conversationId.tsx +73 -19
  46. package/apps/ui/src/routes/antigravity.$workspaceKey.tsx +42 -60
  47. package/apps/ui/src/routes/antigravity.index.tsx +2 -2
  48. package/apps/ui/src/routes/claude-code-sessions.$sessionId.tsx +25 -11
  49. package/apps/ui/src/routes/claude-code.$workspaceKey.tsx +27 -32
  50. package/apps/ui/src/routes/claude-code.index.tsx +2 -2
  51. package/apps/ui/src/routes/codex.$project.tsx +33 -31
  52. package/apps/ui/src/routes/codex.index.tsx +5 -10
  53. package/apps/ui/src/routes/cursor-threads.$composerId.tsx +29 -16
  54. package/apps/ui/src/routes/cursor.$workspaceKey.tsx +33 -27
  55. package/apps/ui/src/routes/cursor.index.tsx +5 -2
  56. package/apps/ui/src/routes/grok-sessions.$sessionId.tsx +25 -12
  57. package/apps/ui/src/routes/grok.$workspaceKey.tsx +20 -26
  58. package/apps/ui/src/routes/grok.index.tsx +2 -2
  59. package/apps/ui/src/routes/index.tsx +2 -18
  60. package/apps/ui/src/routes/kiro-sessions.$sessionId.tsx +25 -12
  61. package/apps/ui/src/routes/kiro.$workspaceKey.tsx +27 -32
  62. package/apps/ui/src/routes/kiro.index.tsx +2 -2
  63. package/apps/ui/src/routes/opencode-sessions.$sessionId.tsx +29 -36
  64. package/apps/ui/src/routes/opencode.$workspaceKey.tsx +45 -61
  65. package/apps/ui/src/routes/opencode.index.tsx +8 -17
  66. package/apps/ui/src/routes/qoder-sessions.$sessionId.tsx +12 -11
  67. package/apps/ui/src/routes/qoder.$workspaceKey.tsx +19 -25
  68. package/apps/ui/src/routes/qoder.index.tsx +2 -2
  69. package/apps/ui/src/routes/threads.$threadId.tsx +87 -56
  70. package/apps/ui/vite.config.ts +22 -12
  71. package/bin/spiracha.ts +28 -1
  72. package/package.json +26 -17
  73. package/src/client.ts +61 -16
  74. package/src/lib/antigravity-db.ts +236 -82
  75. package/src/lib/antigravity-keychain.ts +7 -8
  76. package/src/lib/antigravity-transcript-phase.ts +47 -0
  77. package/src/lib/claude-code-db.ts +45 -19
  78. package/src/lib/codex-analytics.ts +32 -5
  79. package/src/lib/codex-browser-db.ts +540 -137
  80. package/src/lib/codex-browser-export.ts +2 -7
  81. package/src/lib/codex-browser-types.ts +21 -1
  82. package/src/lib/codex-thread-cache.ts +88 -33
  83. package/src/lib/codex-thread-parser.ts +78 -35
  84. package/src/lib/codex-thread-recovery.ts +38 -11
  85. package/src/lib/codex-transcript-renderer.ts +49 -61
  86. package/src/lib/concurrency.ts +15 -3
  87. package/src/lib/conversation-api.ts +34 -31
  88. package/src/lib/conversation-data/adapter-helpers.ts +25 -3
  89. package/src/lib/conversation-data/antigravity-adapter.ts +54 -29
  90. package/src/lib/conversation-data/claude-code-adapter.ts +32 -6
  91. package/src/lib/conversation-data/codex-adapter.ts +91 -80
  92. package/src/lib/conversation-data/cursor-adapter.ts +48 -34
  93. package/src/lib/conversation-data/grok-adapter.ts +19 -6
  94. package/src/lib/conversation-data/index.ts +29 -10
  95. package/src/lib/conversation-data/kiro-adapter.ts +18 -5
  96. package/src/lib/conversation-data/message-selector.ts +2 -5
  97. package/src/lib/conversation-data/opencode-adapter.ts +43 -15
  98. package/src/lib/conversation-data/path-match.ts +1 -13
  99. package/src/lib/conversation-data/qoder-adapter.ts +23 -18
  100. package/src/lib/conversation-zip-export.ts +20 -2
  101. package/src/lib/cursor-db.ts +34 -23
  102. package/src/lib/cursor-id.ts +17 -0
  103. package/src/lib/cursor-recovery.ts +146 -35
  104. package/src/lib/cursor-transcript-phase.ts +40 -0
  105. package/src/lib/cursor-transcript.ts +21 -5
  106. package/src/lib/grok-db.ts +76 -34
  107. package/src/lib/grok-transcript-phase.ts +11 -2
  108. package/src/lib/grok-transcript.ts +15 -2
  109. package/src/lib/kiro-db.ts +161 -68
  110. package/src/lib/kiro-transcript.ts +0 -4
  111. package/src/lib/opencode-db.ts +4 -8
  112. package/src/lib/opencode-think-tags.ts +17 -3
  113. package/src/lib/portable-path.ts +9 -0
  114. package/src/lib/qoder-acp-client.ts +11 -2
  115. package/src/lib/qoder-db.ts +51 -17
  116. package/src/lib/qoder-transcript-phase.ts +64 -0
  117. package/src/lib/qoder-transcript.ts +30 -2
  118. package/src/lib/shared.ts +51 -15
  119. package/src/lib/sqlite-error.ts +31 -5
  120. package/src/lib/transcript-load-limiter.ts +8 -3
  121. package/src/lib/ui-cache.ts +152 -17
  122. package/src/lib/ui-export-archive.ts +14 -55
  123. package/src/lib/ui-export-files.ts +75 -15
  124. package/src/lib/ui-export-zip.ts +40 -0
  125. 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 isSubagentTaskNotification = (raw: Record<string, JsonValue>): boolean => {
583
- return (
584
- raw.type === 'queue-operation' &&
585
- asString(raw.content ?? null)
586
- ?.trimStart()
587
- .startsWith('<task-notification>') === true
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 (isSubagentTaskNotification(raw)) {
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 readdir(projectDir, { withFileTypes: true }).catch(() => []);
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 worktree = sessions[0]?.worktree ?? decodeWorktreeFromDirectoryName(directoryName);
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 listClaudeCodeSessionsForGroup = async (
1030
+ export const listClaudeCodeSessionTranscriptsForGroup = async (
1025
1031
  workspaceKey: string,
1026
1032
  projectsDir = resolveClaudeCodeProjectsDir(),
1027
- ): Promise<ClaudeCodeSessionSummary[]> => {
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 sortSessions(transcripts.filter(hasSessionContent).map((transcript) => transcript.session));
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
- return files.find((file) => path.basename(file.filePath, '.jsonl') === sessionId) ?? null;
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 { asObject, asString, getPortablePathBasename, readJsonlObjects } from './shared';
6
- import { hashCacheKeyParts, hashCacheKeyPartsIterable, withCachedJson } from './ui-cache';
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 'v2';
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-${hashCacheKeyParts('v1', ...threadMetadataCacheKeyParts(thread))}`;
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,