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.
Files changed (162) hide show
  1. package/AGENTS.md +27 -12
  2. package/README.md +42 -19
  3. package/apps/ui/AGENTS.md +18 -10
  4. package/apps/ui/README.md +36 -11
  5. package/apps/ui/src/components/analytics-breakdowns.tsx +106 -0
  6. package/apps/ui/src/components/antigravity-conversations-table.tsx +51 -11
  7. package/apps/ui/src/components/antigravity-workspaces-table.tsx +1 -1
  8. package/apps/ui/src/components/app-shell.tsx +46 -3
  9. package/apps/ui/src/components/breadcrumbs.tsx +21 -3
  10. package/apps/ui/src/components/claude-code-sessions-table.tsx +42 -6
  11. package/apps/ui/src/components/claude-code-workspaces-table.tsx +3 -1
  12. package/apps/ui/src/components/cursor-threads-table.tsx +10 -47
  13. package/apps/ui/src/components/data-table.tsx +23 -5
  14. package/apps/ui/src/components/delete-confirm-dialog.tsx +4 -3
  15. package/apps/ui/src/components/export-dialog.tsx +79 -74
  16. package/apps/ui/src/components/grok-sessions-table.tsx +146 -0
  17. package/apps/ui/src/components/grok-workspaces-table.tsx +53 -0
  18. package/apps/ui/src/components/kiro-sessions-table.tsx +40 -4
  19. package/apps/ui/src/components/opencode-sessions-table.tsx +40 -4
  20. package/apps/ui/src/components/page-header.tsx +15 -9
  21. package/apps/ui/src/components/projects-table.tsx +4 -1
  22. package/apps/ui/src/components/qoder-sessions-table.tsx +23 -2
  23. package/apps/ui/src/components/qoder-workspaces-table.tsx +5 -1
  24. package/apps/ui/src/components/route-error-panel.tsx +13 -0
  25. package/apps/ui/src/components/selection-actions-toolbar.tsx +80 -0
  26. package/apps/ui/src/components/thread-goals-panel.tsx +50 -0
  27. package/apps/ui/src/components/thread-tools-panel.tsx +142 -0
  28. package/apps/ui/src/components/threads-table.tsx +13 -47
  29. package/apps/ui/src/components/transcript-view.tsx +148 -52
  30. package/apps/ui/src/components/ui/select.tsx +1 -1
  31. package/apps/ui/src/lib/antigravity-conversation-state.ts +17 -4
  32. package/apps/ui/src/lib/antigravity-server.ts +209 -11
  33. package/apps/ui/src/lib/antigravity-transcript-events.ts +57 -73
  34. package/apps/ui/src/lib/claude-code-queries.ts +8 -0
  35. package/apps/ui/src/lib/claude-code-server.ts +185 -7
  36. package/apps/ui/src/lib/claude-code-transcript-events.ts +8 -1
  37. package/apps/ui/src/lib/codex-queries.ts +16 -0
  38. package/apps/ui/src/lib/codex-server.ts +134 -52
  39. package/apps/ui/src/lib/cursor-server.ts +68 -53
  40. package/apps/ui/src/lib/cursor-transcript-events.ts +11 -86
  41. package/apps/ui/src/lib/delete-batch.ts +52 -0
  42. package/apps/ui/src/lib/error-presentation.ts +43 -0
  43. package/apps/ui/src/lib/export-mutation.ts +20 -0
  44. package/apps/ui/src/lib/export-options.ts +15 -0
  45. package/apps/ui/src/lib/formatters.ts +3 -5
  46. package/apps/ui/src/lib/grok-queries.ts +22 -0
  47. package/apps/ui/src/lib/grok-server.ts +168 -0
  48. package/apps/ui/src/lib/grok-transcript-events.ts +149 -0
  49. package/apps/ui/src/lib/kiro-server.ts +97 -7
  50. package/apps/ui/src/lib/mutation-error.ts +15 -0
  51. package/apps/ui/src/lib/opencode-server.ts +96 -7
  52. package/apps/ui/src/lib/path-utils.ts +1 -1
  53. package/apps/ui/src/lib/qoder-queries.ts +15 -0
  54. package/apps/ui/src/lib/qoder-server.ts +67 -11
  55. package/apps/ui/src/lib/qoder-transcript-events.ts +1 -1
  56. package/apps/ui/src/lib/route-search.ts +114 -0
  57. package/apps/ui/src/lib/route-state-reset.tsx +10 -0
  58. package/apps/ui/src/lib/settings-server.ts +33 -0
  59. package/apps/ui/src/lib/settings-store.tsx +82 -38
  60. package/apps/ui/src/lib/settings.ts +65 -0
  61. package/apps/ui/src/lib/source-session-export-server.ts +86 -3
  62. package/apps/ui/src/lib/thread-metadata.ts +22 -0
  63. package/apps/ui/src/lib/thread-transcript-load.ts +15 -0
  64. package/apps/ui/src/lib/thread-transcript-stats.ts +10 -1
  65. package/apps/ui/src/lib/workspace-delete-navigation.ts +20 -0
  66. package/apps/ui/src/routeTree.gen.ts +341 -234
  67. package/apps/ui/src/routes/__root.tsx +12 -15
  68. package/apps/ui/src/routes/analytics.tsx +10 -61
  69. package/apps/ui/src/routes/antigravity-conversations.$conversationId.tsx +149 -29
  70. package/apps/ui/src/routes/antigravity.$workspaceKey.tsx +262 -42
  71. package/apps/ui/src/routes/antigravity.index.tsx +2 -2
  72. package/apps/ui/src/routes/api.v1.conversations.$source.$id.ts +4 -0
  73. package/apps/ui/src/routes/api.v1.conversations.delete.ts +12 -0
  74. package/apps/ui/src/routes/api.v1.conversations.export.ts +12 -0
  75. package/apps/ui/src/routes/claude-code-sessions.$sessionId.tsx +168 -28
  76. package/apps/ui/src/routes/claude-code.$workspaceKey.tsx +179 -45
  77. package/apps/ui/src/routes/claude-code.index.tsx +2 -2
  78. package/apps/ui/src/routes/codex.$project.tsx +33 -31
  79. package/apps/ui/src/routes/codex.index.tsx +5 -10
  80. package/apps/ui/src/routes/cursor-threads.$composerId.tsx +31 -18
  81. package/apps/ui/src/routes/cursor.$workspaceKey.tsx +44 -28
  82. package/apps/ui/src/routes/cursor.index.tsx +5 -2
  83. package/apps/ui/src/routes/grok-sessions.$sessionId.tsx +401 -0
  84. package/apps/ui/src/routes/grok.$workspaceKey.tsx +295 -0
  85. package/apps/ui/src/routes/grok.index.tsx +48 -0
  86. package/apps/ui/src/routes/index.tsx +2 -18
  87. package/apps/ui/src/routes/kiro-sessions.$sessionId.tsx +91 -27
  88. package/apps/ui/src/routes/kiro.$workspaceKey.tsx +213 -51
  89. package/apps/ui/src/routes/kiro.index.tsx +2 -2
  90. package/apps/ui/src/routes/opencode-sessions.$sessionId.tsx +108 -29
  91. package/apps/ui/src/routes/opencode.$workspaceKey.tsx +231 -55
  92. package/apps/ui/src/routes/opencode.index.tsx +6 -2
  93. package/apps/ui/src/routes/qoder-sessions.$sessionId.tsx +19 -15
  94. package/apps/ui/src/routes/qoder.$workspaceKey.tsx +89 -48
  95. package/apps/ui/src/routes/qoder.index.tsx +2 -2
  96. package/apps/ui/src/routes/threads.$threadId.tsx +620 -105
  97. package/apps/ui/vite.config.ts +22 -12
  98. package/bin/spiracha.ts +28 -1
  99. package/package.json +37 -27
  100. package/src/client.ts +183 -5
  101. package/src/lib/antigravity-db.ts +424 -94
  102. package/src/lib/antigravity-exporter-types.ts +3 -0
  103. package/src/lib/antigravity-keychain.ts +7 -8
  104. package/src/lib/antigravity-projects.ts +201 -0
  105. package/src/lib/antigravity-transcript-phase.ts +47 -0
  106. package/src/lib/claude-code-db.ts +534 -42
  107. package/src/lib/claude-code-exporter-types.ts +5 -0
  108. package/src/lib/claude-code-transcript-phase.ts +60 -1
  109. package/src/lib/claude-code-transcript.ts +8 -5
  110. package/src/lib/codex-analytics.ts +32 -5
  111. package/src/lib/codex-browser-db.ts +540 -137
  112. package/src/lib/codex-browser-export.ts +18 -27
  113. package/src/lib/codex-browser-types.ts +21 -1
  114. package/src/lib/codex-thread-cache.ts +106 -23
  115. package/src/lib/codex-thread-parser.ts +149 -55
  116. package/src/lib/codex-thread-recovery.ts +38 -11
  117. package/src/lib/codex-transcript-filter.ts +31 -0
  118. package/src/lib/codex-transcript-renderer.ts +87 -79
  119. package/src/lib/concurrency.ts +56 -3
  120. package/src/lib/conversation-api.ts +410 -27
  121. package/src/lib/conversation-data/adapter-helpers.ts +25 -3
  122. package/src/lib/conversation-data/antigravity-adapter.ts +70 -26
  123. package/src/lib/conversation-data/claude-code-adapter.ts +69 -12
  124. package/src/lib/conversation-data/codex-adapter.ts +108 -73
  125. package/src/lib/conversation-data/cursor-adapter.ts +77 -29
  126. package/src/lib/conversation-data/grok-adapter.ts +223 -0
  127. package/src/lib/conversation-data/index.ts +105 -11
  128. package/src/lib/conversation-data/kiro-adapter.ts +59 -12
  129. package/src/lib/conversation-data/message-selector.ts +2 -5
  130. package/src/lib/conversation-data/opencode-adapter.ts +67 -17
  131. package/src/lib/conversation-data/path-match.ts +1 -13
  132. package/src/lib/conversation-data/qoder-adapter.ts +67 -37
  133. package/src/lib/conversation-data/types.ts +43 -0
  134. package/src/lib/conversation-zip-export.ts +75 -0
  135. package/src/lib/cursor-db.ts +68 -28
  136. package/src/lib/cursor-id.ts +17 -0
  137. package/src/lib/cursor-recovery.ts +146 -35
  138. package/src/lib/cursor-transcript-phase.ts +40 -0
  139. package/src/lib/cursor-transcript.ts +21 -5
  140. package/src/lib/grok-db.ts +1068 -0
  141. package/src/lib/grok-exporter-types.ts +110 -0
  142. package/src/lib/grok-transcript-phase.ts +61 -0
  143. package/src/lib/grok-transcript.ts +167 -0
  144. package/src/lib/kiro-db.ts +187 -43
  145. package/src/lib/kiro-transcript.ts +0 -4
  146. package/src/lib/model-label.ts +11 -3
  147. package/src/lib/opencode-db.ts +599 -60
  148. package/src/lib/opencode-think-tags.ts +17 -3
  149. package/src/lib/opencode-transcript.ts +8 -5
  150. package/src/lib/portable-path.ts +9 -0
  151. package/src/lib/qoder-acp-client.ts +11 -2
  152. package/src/lib/qoder-db.ts +51 -17
  153. package/src/lib/qoder-transcript-phase.ts +64 -0
  154. package/src/lib/qoder-transcript.ts +30 -2
  155. package/src/lib/shared.ts +63 -15
  156. package/src/lib/sqlite-error.ts +31 -5
  157. package/src/lib/transcript-load-limiter.ts +87 -0
  158. package/src/lib/ui-cache.ts +185 -24
  159. package/src/lib/ui-export-archive.ts +58 -39
  160. package/src/lib/ui-export-files.ts +75 -15
  161. package/src/lib/ui-export-zip.ts +40 -0
  162. package/apps/ui/package.json +0 -65
@@ -1,8 +1,8 @@
1
1
  import { Database } from 'bun:sqlite';
2
- import { copyFile, utimes } from 'node:fs/promises';
2
+ import { copyFile, readdir, rm, utimes } from 'node:fs/promises';
3
3
  import path from 'node:path';
4
4
  import type { RecoverProjectThreadsResult } from './codex-browser-types';
5
- import { getPortablePathBasename } from './shared';
5
+ import { getPortablePathBasename } from './portable-path';
6
6
  import { runWithSqliteRetry } from './sqlite-retry';
7
7
 
8
8
  type RecoveryThreadRow = {
@@ -18,6 +18,8 @@ type GlobalState = {
18
18
  'project-order'?: string[];
19
19
  };
20
20
 
21
+ const RECOVERY_BACKUP_RETENTION_COUNT = 5;
22
+
21
23
  const backupFile = async (filePath: string, label: string) => {
22
24
  const stamp = new Date()
23
25
  .toISOString()
@@ -26,6 +28,14 @@ const backupFile = async (filePath: string, label: string) => {
26
28
  .replace('T', '-');
27
29
  const backupPath = `${filePath}.bak-${label}-${stamp}`;
28
30
  await copyFile(filePath, backupPath);
31
+ const directory = path.dirname(filePath);
32
+ const prefix = `${path.basename(filePath)}.bak-${label}-`;
33
+ const backups = (await readdir(directory))
34
+ .filter((entry) => entry.startsWith(prefix))
35
+ .sort((left, right) => right.localeCompare(left));
36
+ await Promise.all(
37
+ backups.slice(RECOVERY_BACKUP_RETENTION_COUNT).map((entry) => rm(path.join(directory, entry), { force: true })),
38
+ );
29
39
  return backupPath;
30
40
  };
31
41
 
@@ -100,9 +110,9 @@ const refreshThreadRows = (db: Database, threadIds: string[]) => {
100
110
  return Number(result.changes);
101
111
  };
102
112
 
103
- const refreshSessionIndex = async (sessionIndexPath: string, threadIds: string[]) => {
113
+ const prepareSessionIndexRefresh = async (sessionIndexPath: string, threadIds: string[]) => {
104
114
  if (threadIds.length === 0) {
105
- return 0;
115
+ return { content: null, updated: 0 };
106
116
  }
107
117
 
108
118
  const now = new Date().toISOString().replace(/\.\d{3}Z$/, 'Z');
@@ -116,7 +126,19 @@ const refreshSessionIndex = async (sessionIndexPath: string, threadIds: string[]
116
126
  continue;
117
127
  }
118
128
 
119
- const parsed = JSON.parse(line) as { id?: string; updated_at?: string };
129
+ let parsed: { id?: string; updated_at?: string };
130
+ try {
131
+ const value: unknown = JSON.parse(line);
132
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
133
+ rewrittenLines.push(line);
134
+ continue;
135
+ }
136
+ parsed = value as { id?: string; updated_at?: string };
137
+ } catch {
138
+ rewrittenLines.push(line);
139
+ continue;
140
+ }
141
+
120
142
  if (parsed.id && threadIdSet.has(parsed.id)) {
121
143
  parsed.updated_at = now;
122
144
  updated += 1;
@@ -125,8 +147,10 @@ const refreshSessionIndex = async (sessionIndexPath: string, threadIds: string[]
125
147
  rewrittenLines.push(JSON.stringify(parsed));
126
148
  }
127
149
 
128
- await Bun.write(sessionIndexPath, `${rewrittenLines.join('\n')}\n`);
129
- return updated;
150
+ return {
151
+ content: `${rewrittenLines.join('\n')}\n`,
152
+ updated,
153
+ };
130
154
  };
131
155
 
132
156
  const touchRolloutFiles = async (codexDir: string, rolloutPaths: string[]) => {
@@ -177,12 +201,15 @@ export const recoverCodexProjectThreads = async (
177
201
  const topLevelThreads = getProjectTopLevelThreads(db, projectName);
178
202
  const projectCwds = [...new Set(topLevelThreads.map((thread) => thread.cwd))];
179
203
  const rootUpdateResult = updateGlobalRoots(globalState, projectCwds);
180
- await writeGlobalState(globalStatePath, rootUpdateResult.state);
181
-
182
204
  const threadIds = topLevelThreads.map((thread) => thread.id);
183
205
  const rolloutPaths = topLevelThreads.map((thread) => thread.rollout_path);
206
+ const sessionIndexRefresh = await prepareSessionIndexRefresh(sessionIndexPath, threadIds);
207
+
208
+ await writeGlobalState(globalStatePath, rootUpdateResult.state);
184
209
  const threadDbRowsUpdated = refreshThreadRows(db, threadIds);
185
- const sessionIndexRowsUpdated = await refreshSessionIndex(sessionIndexPath, threadIds);
210
+ if (sessionIndexRefresh.content !== null) {
211
+ await Bun.write(sessionIndexPath, sessionIndexRefresh.content);
212
+ }
186
213
  const rolloutFilesTouched = await touchRolloutFiles(codexDir, rolloutPaths);
187
214
 
188
215
  return {
@@ -192,7 +219,7 @@ export const recoverCodexProjectThreads = async (
192
219
  resolvedCwds: projectCwds,
193
220
  rolloutFilesTouched,
194
221
  savedRootsAdded: rootUpdateResult.savedRootsAdded,
195
- sessionIndexRowsUpdated,
222
+ sessionIndexRowsUpdated: sessionIndexRefresh.updated,
196
223
  threadDbRowsUpdated,
197
224
  topLevelThreadsFound: threadIds.length,
198
225
  };
@@ -0,0 +1,31 @@
1
+ import type { ThreadEvent } from './codex-browser-types';
2
+
3
+ const isCommentaryMessage = (event: ThreadEvent) =>
4
+ event.kind === 'message' && event.role === 'assistant' && event.phase === 'commentary';
5
+
6
+ export type CodexTranscriptEventFilters = {
7
+ showCommentary: boolean;
8
+ showExtraEvents: boolean;
9
+ showToolCalls: boolean;
10
+ showUserMessages: boolean;
11
+ };
12
+
13
+ export const shouldShowCodexTranscriptEvent = (event: ThreadEvent, filters: CodexTranscriptEventFilters) => {
14
+ if (isCommentaryMessage(event) && !filters.showCommentary) {
15
+ return false;
16
+ }
17
+
18
+ if (event.kind === 'message') {
19
+ if (event.role === 'user' && !filters.showUserMessages) {
20
+ return false;
21
+ }
22
+
23
+ return !event.isHiddenByDefault || filters.showExtraEvents;
24
+ }
25
+
26
+ if (event.kind === 'tool_call' || event.kind === 'tool_output') {
27
+ return filters.showToolCalls;
28
+ }
29
+
30
+ return filters.showExtraEvents;
31
+ };
@@ -1,7 +1,13 @@
1
1
  import { createReadStream } from 'node:fs';
2
2
  import { rm } from 'node:fs/promises';
3
3
  import path from 'node:path';
4
- import { finished } from 'node:stream/promises';
4
+ import type { Readable, Writable } from 'node:stream';
5
+ import { pipeline } from 'node:stream/promises';
6
+ import {
7
+ parseCodexTranscriptRecord,
8
+ shouldHideCodexTranscriptText,
9
+ stripCodexMemoryCitationBlocks,
10
+ } from './codex-thread-parser';
5
11
  import {
6
12
  type CodexTranscriptExportTarget,
7
13
  type CodexTranscriptRenderOptions,
@@ -23,10 +29,17 @@ import {
23
29
  type JsonValue,
24
30
  type MetadataEntry,
25
31
  readJsonlObjects,
32
+ renderCodeBlock,
26
33
  renderDocumentTitle,
27
34
  renderMetadataBlock,
28
35
  renderSection,
36
+ stripCodexAppDirectiveLines,
29
37
  } from './shared';
38
+ import { runWithTranscriptLoadLimit } from './transcript-load-limiter';
39
+
40
+ export const pipeCodexExportStream = async (source: Readable, destination: Writable) => {
41
+ await pipeline(source, destination, { end: false });
42
+ };
30
43
 
31
44
  export const renderCodexSessionFile = async (
32
45
  target: CodexTranscriptExportTarget,
@@ -35,7 +48,14 @@ export const renderCodexSessionFile = async (
35
48
  let transcriptState: CodexTranscriptState;
36
49
 
37
50
  try {
38
- transcriptState = await collectCodexTranscript(target.sessionFile, options, target.thread?.model ?? null);
51
+ transcriptState = await runWithTranscriptLoadLimit(
52
+ () => collectCodexTranscript(target.sessionFile, options, target.thread?.model ?? null),
53
+ {
54
+ id: target.thread?.id,
55
+ path: target.sessionFile,
56
+ source: 'codex-export-inline',
57
+ },
58
+ );
39
59
  } catch (error) {
40
60
  const message = error instanceof Error ? error.message : String(error);
41
61
  throw new Error(`Failed to read Codex transcript ${target.sessionFile}: ${message}`);
@@ -78,19 +98,28 @@ export const writeCodexSessionFileExport = async (
78
98
  let wroteSection = false;
79
99
 
80
100
  try {
81
- transcriptStream = await createExportWriteStream(transcriptOutputPath);
82
- for await (const parsed of readJsonlObjects(target.sessionFile)) {
83
- captureSessionMeta(parsed, state.sessionMeta);
84
- const block = renderCodexTranscriptRecord(parsed, options, state);
85
- if (!block) {
86
- continue;
87
- }
88
-
89
- transcriptStream.write(transform(wroteSection ? `${getSectionSeparator()}${block}` : block));
90
- wroteSection = true;
91
- }
92
- await finalizeExportWriteStream(transcriptStream);
93
- transcriptStream = null;
101
+ await runWithTranscriptLoadLimit(
102
+ async () => {
103
+ transcriptStream = await createExportWriteStream(transcriptOutputPath);
104
+ for await (const parsed of readJsonlObjects(target.sessionFile)) {
105
+ captureSessionMeta(parsed, state.sessionMeta);
106
+ const block = renderCodexTranscriptRecord(parsed, options, state);
107
+ if (!block) {
108
+ continue;
109
+ }
110
+
111
+ transcriptStream.write(transform(wroteSection ? `${getSectionSeparator()}${block}` : block));
112
+ wroteSection = true;
113
+ }
114
+ await finalizeExportWriteStream(transcriptStream);
115
+ transcriptStream = null;
116
+ },
117
+ {
118
+ id: target.thread?.id,
119
+ path: target.sessionFile,
120
+ source: 'codex-export-stream',
121
+ },
122
+ );
94
123
 
95
124
  if (!wroteSection) {
96
125
  return false;
@@ -104,8 +133,7 @@ export const writeCodexSessionFileExport = async (
104
133
  }
105
134
 
106
135
  const transcriptReadStream = createReadStream(transcriptOutputPath, { encoding: 'utf8' });
107
- transcriptReadStream.pipe(outputStream, { end: false });
108
- await finished(transcriptReadStream);
136
+ await pipeCodexExportStream(transcriptReadStream, outputStream);
109
137
  outputStream.write('\n');
110
138
  await finalizeExportWriteStream(outputStream);
111
139
  } catch (error) {
@@ -189,7 +217,7 @@ const processCodexMessageRecord = (
189
217
  options: CodexTranscriptRenderOptions,
190
218
  state: CodexTranscriptState,
191
219
  ) => {
192
- return renderMessageBlock(message, options.outputFormat, state.assistantModel, options.includeCommentary);
220
+ return renderMessageBlock(message, options.outputFormat, state, options.includeCommentary);
193
221
  };
194
222
 
195
223
  const buildStreamExportPrefix = (
@@ -494,45 +522,22 @@ const normalizeMessage = (value: Record<string, JsonValue>): MessageRecord | nul
494
522
  };
495
523
 
496
524
  const extractToolRecord = (parsed: Record<string, JsonValue>): ToolRecord | null => {
497
- if (parsed.type !== 'response_item') {
498
- return null;
499
- }
500
-
501
- const payload = asObject(parsed.payload);
502
- if (!payload) {
503
- return null;
504
- }
505
-
506
- if (payload.type === 'function_call') {
507
- const name = asString(payload.name);
508
- const argumentsText = asString(payload.arguments);
509
- const callId = asString(payload.call_id);
510
-
511
- if (name !== 'exec_command') {
512
- return null;
513
- }
514
-
525
+ const event = parseCodexTranscriptRecord(parsed);
526
+ if (event?.kind === 'tool_call') {
515
527
  return {
516
- argumentsText: argumentsText ?? undefined,
517
- callId,
528
+ argumentsText: event.argumentsText ?? undefined,
529
+ callId: event.callId,
518
530
  kind: 'call',
519
- name,
531
+ name: event.name,
520
532
  };
521
533
  }
522
534
 
523
- if (payload.type === 'function_call_output') {
524
- const callId = asString(payload.call_id);
525
- const outputText = asString(payload.output);
526
-
527
- if (!outputText?.includes('Command: ')) {
528
- return null;
529
- }
530
-
535
+ if (event?.kind === 'tool_output') {
531
536
  return {
532
- callId,
537
+ callId: event.callId,
533
538
  kind: 'output',
534
- name: 'function_call_output',
535
- outputText: outputText ?? undefined,
539
+ name: 'tool_output',
540
+ outputText: event.outputText,
536
541
  };
537
542
  }
538
543
 
@@ -542,7 +547,7 @@ const extractToolRecord = (parsed: Record<string, JsonValue>): ToolRecord | null
542
547
  const renderMessageBlock = (
543
548
  message: MessageRecord,
544
549
  outputFormat: ExportFormat,
545
- assistantModel: string | null,
550
+ state: CodexTranscriptState,
546
551
  includeCommentary: boolean,
547
552
  ): string => {
548
553
  if (message.role !== 'user' && message.role !== 'assistant') {
@@ -553,12 +558,15 @@ const renderMessageBlock = (
553
558
  return '';
554
559
  }
555
560
 
556
- const text = cleanExtractedText(stripPreviewBlock(extractText(message.content))).trim();
557
- if (!text || shouldSkipMessage(message.role, text)) {
561
+ const extractedText = extractText(message.content);
562
+ const text = stripCodexMemoryCitationBlocks(
563
+ stripCodexAppDirectiveLines(cleanExtractedText(stripPreviewBlock(extractedText))),
564
+ );
565
+ if (!text || shouldHideCodexTranscriptText(message.role, text)) {
558
566
  return '';
559
567
  }
560
568
 
561
- const title = message.role === 'user' ? 'User' : formatModelLabel(message.model ?? assistantModel);
569
+ const title = message.role === 'user' ? 'User' : formatModelLabel(message.model ?? state.assistantModel);
562
570
  const body = message.phase ? `Phase: ${message.phase}\n\n${text}` : text;
563
571
 
564
572
  return renderSection(title, body, outputFormat);
@@ -567,11 +575,18 @@ const renderMessageBlock = (
567
575
  const renderToolBlock = (tool: ToolRecord, outputFormat: ExportFormat): string => {
568
576
  if (tool.kind === 'call') {
569
577
  const details = formatToolCallDetails(tool, outputFormat);
570
- return details ? renderSection('Tool', details, outputFormat) : '';
578
+ return renderSection('Tool Call', details, outputFormat);
579
+ }
580
+
581
+ const outputText = tool.outputText ?? '';
582
+ const summary = formatToolOutputSummary(outputText, outputFormat);
583
+ if (!summary && !outputText.trim()) {
584
+ return '';
571
585
  }
572
586
 
573
- const summary = formatToolOutputSummary(tool.outputText ?? '', outputFormat);
574
- return summary ? renderSection('Tool Output', summary, outputFormat) : '';
587
+ const lines = tool.callId ? [`Call ID: ${tool.callId}`, ''] : [];
588
+ lines.push(summary || renderCodeBlock(outputText.trim(), outputFormat));
589
+ return renderSection('Tool Output', lines.join('\n'), outputFormat);
575
590
  };
576
591
 
577
592
  const stripPreviewBlock = (text: string): string => {
@@ -586,7 +601,7 @@ const stripPreviewBlock = (text: string): string => {
586
601
 
587
602
  const first = parts[0];
588
603
  const second = parts[1];
589
- const isTranscriptHeading = (value: string) => /^##\s+.+$/i.test(value);
604
+ const isTranscriptHeading = (value: string) => /^##\s+(?:assistant|user)$/i.test(value);
590
605
  const looksLikePreview =
591
606
  !/^([UA]):/i.test(first) &&
592
607
  !isTranscriptHeading(first) &&
@@ -600,33 +615,26 @@ const stripPreviewBlock = (text: string): string => {
600
615
  return parts.slice(1).join('\n\n');
601
616
  };
602
617
 
603
- const shouldSkipMessage = (role: string, text: string): boolean => {
604
- if (text.startsWith('<environment_context>')) {
605
- return true;
606
- }
607
-
608
- if (text.startsWith('AGENTS.md instructions for ')) {
609
- return true;
610
- }
611
-
612
- if (text.startsWith('# AGENTS.md instructions for ')) {
613
- return true;
618
+ const formatToolCallDetails = (tool: ToolRecord, outputFormat: ExportFormat): string => {
619
+ const lines = [`Tool: ${formatInlineLiteral(tool.name, outputFormat)}`];
620
+ if (tool.callId) {
621
+ lines.push(`Call ID: ${tool.callId}`);
614
622
  }
615
623
 
616
- if (role === 'user' && text.includes('<environment_context>')) {
617
- return true;
624
+ if (tool.name === 'exec_command') {
625
+ const details = parseExecCommandArguments(tool.argumentsText);
626
+ if (details.cmd) {
627
+ lines.push(`Command: ${formatInlineLiteral(details.cmd, outputFormat)}`);
628
+ return lines.join('\n');
629
+ }
618
630
  }
619
631
 
620
- return false;
621
- };
622
-
623
- const formatToolCallDetails = (tool: ToolRecord, outputFormat: ExportFormat): string => {
624
- if (tool.name !== 'exec_command') {
625
- return '';
632
+ const argumentsText = tool.argumentsText?.trim();
633
+ if (argumentsText) {
634
+ lines.push('', 'Input:', '', renderCodeBlock(argumentsText, outputFormat));
626
635
  }
627
636
 
628
- const details = parseExecCommandArguments(tool.argumentsText);
629
- return details.cmd ? `Command: ${formatInlineLiteral(details.cmd, outputFormat)}` : '';
637
+ return lines.join('\n');
630
638
  };
631
639
 
632
640
  const extractText = (content: JsonValue): string => {
@@ -11,15 +11,68 @@ export const mapWithConcurrency = async <T, TResult>(
11
11
  const workerLimit = Math.max(1, requestedLimit);
12
12
  const results = new Array<TResult>(values.length);
13
13
  let nextIndex = 0;
14
+ let failed = false;
14
15
 
15
16
  const worker = async () => {
16
- while (nextIndex < values.length) {
17
+ while (!failed && nextIndex < values.length) {
17
18
  const currentIndex = nextIndex;
18
19
  nextIndex += 1;
19
- results[currentIndex] = await mapper(values[currentIndex]!, currentIndex);
20
+ try {
21
+ results[currentIndex] = await mapper(values[currentIndex]!, currentIndex);
22
+ } catch (error) {
23
+ failed = true;
24
+ throw error;
25
+ }
20
26
  }
21
27
  };
22
28
 
23
- await Promise.all(Array.from({ length: Math.min(workerLimit, values.length) }, () => worker()));
29
+ const settledWorkers = await Promise.allSettled(
30
+ Array.from({ length: Math.min(workerLimit, values.length) }, () => worker()),
31
+ );
32
+ const failure = settledWorkers.find((result): result is PromiseRejectedResult => result.status === 'rejected');
33
+ if (failure) {
34
+ throw failure.reason;
35
+ }
24
36
  return results;
25
37
  };
38
+
39
+ export const createConcurrencyLimiter = (limit: number) => {
40
+ const requestedLimit = Number.isFinite(limit) ? Math.floor(limit) : 1;
41
+ const workerLimit = Math.max(1, requestedLimit);
42
+ const queue: Array<() => void> = [];
43
+ let activeCount = 0;
44
+
45
+ const drain = () => {
46
+ if (activeCount >= workerLimit) {
47
+ return;
48
+ }
49
+
50
+ const next = queue.shift();
51
+ if (next) {
52
+ next();
53
+ }
54
+ };
55
+
56
+ return async <T>(task: () => Promise<T>): Promise<T> => {
57
+ await new Promise<void>((resolve) => {
58
+ const start = () => {
59
+ activeCount += 1;
60
+ resolve();
61
+ };
62
+
63
+ if (activeCount < workerLimit) {
64
+ start();
65
+ return;
66
+ }
67
+
68
+ queue.push(start);
69
+ });
70
+
71
+ try {
72
+ return await task();
73
+ } finally {
74
+ activeCount -= 1;
75
+ drain();
76
+ }
77
+ };
78
+ };