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.
Files changed (104) hide show
  1. package/README.md +6 -1
  2. package/apps/ui/README.md +20 -2
  3. package/apps/ui/package.json +2 -2
  4. package/apps/ui/src/components/antigravity-conversations-table.tsx +50 -11
  5. package/apps/ui/src/components/antigravity-workspaces-table.tsx +1 -1
  6. package/apps/ui/src/components/app-shell.tsx +1 -0
  7. package/apps/ui/src/components/breadcrumbs.tsx +21 -3
  8. package/apps/ui/src/components/claude-code-sessions-table.tsx +42 -6
  9. package/apps/ui/src/components/claude-code-workspaces-table.tsx +3 -1
  10. package/apps/ui/src/components/cursor-threads-table.tsx +10 -47
  11. package/apps/ui/src/components/data-table.tsx +7 -5
  12. package/apps/ui/src/components/export-dialog.tsx +2 -12
  13. package/apps/ui/src/components/grok-sessions-table.tsx +146 -0
  14. package/apps/ui/src/components/grok-workspaces-table.tsx +53 -0
  15. package/apps/ui/src/components/kiro-sessions-table.tsx +40 -4
  16. package/apps/ui/src/components/opencode-sessions-table.tsx +40 -4
  17. package/apps/ui/src/components/page-header.tsx +15 -9
  18. package/apps/ui/src/components/qoder-sessions-table.tsx +23 -2
  19. package/apps/ui/src/components/qoder-workspaces-table.tsx +5 -1
  20. package/apps/ui/src/components/selection-actions-toolbar.tsx +80 -0
  21. package/apps/ui/src/components/threads-table.tsx +9 -46
  22. package/apps/ui/src/components/transcript-view.tsx +148 -52
  23. package/apps/ui/src/components/ui/select.tsx +1 -1
  24. package/apps/ui/src/lib/antigravity-conversation-state.ts +17 -4
  25. package/apps/ui/src/lib/antigravity-server.ts +152 -7
  26. package/apps/ui/src/lib/antigravity-transcript-events.ts +11 -3
  27. package/apps/ui/src/lib/claude-code-queries.ts +8 -0
  28. package/apps/ui/src/lib/claude-code-server.ts +175 -7
  29. package/apps/ui/src/lib/claude-code-transcript-events.ts +8 -1
  30. package/apps/ui/src/lib/codex-queries.ts +19 -3
  31. package/apps/ui/src/lib/codex-server.ts +135 -53
  32. package/apps/ui/src/lib/cursor-server.ts +48 -5
  33. package/apps/ui/src/lib/formatters.ts +3 -5
  34. package/apps/ui/src/lib/grok-queries.ts +22 -0
  35. package/apps/ui/src/lib/grok-server.ts +169 -0
  36. package/apps/ui/src/lib/grok-transcript-events.ts +149 -0
  37. package/apps/ui/src/lib/kiro-server.ts +85 -7
  38. package/apps/ui/src/lib/opencode-server.ts +85 -7
  39. package/apps/ui/src/lib/qoder-server.ts +67 -11
  40. package/apps/ui/src/lib/route-search.ts +114 -0
  41. package/apps/ui/src/lib/source-session-export-server.ts +86 -3
  42. package/apps/ui/src/lib/thread-transcript-load.ts +15 -0
  43. package/apps/ui/src/lib/workspace-delete-navigation.ts +12 -0
  44. package/apps/ui/src/routeTree.gen.ts +107 -0
  45. package/apps/ui/src/routes/antigravity-conversations.$conversationId.tsx +76 -10
  46. package/apps/ui/src/routes/antigravity.$workspaceKey.tsx +269 -31
  47. package/apps/ui/src/routes/api.v1.conversations.$source.$id.ts +4 -0
  48. package/apps/ui/src/routes/api.v1.conversations.delete.ts +12 -0
  49. package/apps/ui/src/routes/api.v1.conversations.export.ts +12 -0
  50. package/apps/ui/src/routes/claude-code-sessions.$sessionId.tsx +143 -17
  51. package/apps/ui/src/routes/claude-code.$workspaceKey.tsx +165 -26
  52. package/apps/ui/src/routes/cursor-threads.$composerId.tsx +2 -2
  53. package/apps/ui/src/routes/cursor.$workspaceKey.tsx +11 -1
  54. package/apps/ui/src/routes/grok-sessions.$sessionId.tsx +388 -0
  55. package/apps/ui/src/routes/grok.$workspaceKey.tsx +301 -0
  56. package/apps/ui/src/routes/grok.index.tsx +48 -0
  57. package/apps/ui/src/routes/kiro-sessions.$sessionId.tsx +66 -15
  58. package/apps/ui/src/routes/kiro.$workspaceKey.tsx +199 -32
  59. package/apps/ui/src/routes/opencode-sessions.$sessionId.tsx +105 -19
  60. package/apps/ui/src/routes/opencode.$workspaceKey.tsx +236 -44
  61. package/apps/ui/src/routes/opencode.index.tsx +16 -3
  62. package/apps/ui/src/routes/qoder-sessions.$sessionId.tsx +7 -4
  63. package/apps/ui/src/routes/qoder.$workspaceKey.tsx +75 -28
  64. package/apps/ui/src/routes/threads.$threadId.tsx +548 -64
  65. package/package.json +19 -18
  66. package/src/client.ts +134 -1
  67. package/src/lib/antigravity-db.ts +208 -32
  68. package/src/lib/antigravity-exporter-types.ts +3 -0
  69. package/src/lib/antigravity-projects.ts +201 -0
  70. package/src/lib/claude-code-db.ts +498 -32
  71. package/src/lib/claude-code-exporter-types.ts +5 -0
  72. package/src/lib/claude-code-transcript-phase.ts +60 -1
  73. package/src/lib/claude-code-transcript.ts +8 -5
  74. package/src/lib/codex-browser-export.ts +23 -27
  75. package/src/lib/codex-thread-cache.ts +41 -13
  76. package/src/lib/codex-thread-parser.ts +86 -35
  77. package/src/lib/codex-transcript-filter.ts +31 -0
  78. package/src/lib/codex-transcript-renderer.ts +39 -19
  79. package/src/lib/concurrency.ts +41 -0
  80. package/src/lib/conversation-api.ts +399 -19
  81. package/src/lib/conversation-data/antigravity-adapter.ts +21 -2
  82. package/src/lib/conversation-data/claude-code-adapter.ts +37 -6
  83. package/src/lib/conversation-data/codex-adapter.ts +28 -4
  84. package/src/lib/conversation-data/cursor-adapter.ts +35 -1
  85. package/src/lib/conversation-data/grok-adapter.ts +210 -0
  86. package/src/lib/conversation-data/index.ts +76 -1
  87. package/src/lib/conversation-data/kiro-adapter.ts +41 -7
  88. package/src/lib/conversation-data/opencode-adapter.ts +24 -2
  89. package/src/lib/conversation-data/qoder-adapter.ts +44 -19
  90. package/src/lib/conversation-data/types.ts +43 -0
  91. package/src/lib/conversation-zip-export.ts +57 -0
  92. package/src/lib/cursor-db.ts +34 -5
  93. package/src/lib/grok-db.ts +1026 -0
  94. package/src/lib/grok-exporter-types.ts +110 -0
  95. package/src/lib/grok-transcript-phase.ts +52 -0
  96. package/src/lib/grok-transcript.ts +154 -0
  97. package/src/lib/kiro-db.ts +52 -1
  98. package/src/lib/model-label.ts +11 -3
  99. package/src/lib/opencode-db.ts +598 -55
  100. package/src/lib/opencode-transcript.ts +8 -5
  101. package/src/lib/shared.ts +12 -0
  102. package/src/lib/transcript-load-limiter.ts +82 -0
  103. package/src/lib/ui-cache.ts +43 -17
  104. package/src/lib/ui-export-archive.ts +60 -0
@@ -1,8 +1,14 @@
1
- import { getThreadBrowseData, listScopedThreads, resolveCodexThreadDbPath } from '../codex-browser-db';
1
+ import {
2
+ deleteCodexThread,
3
+ getThreadBrowseData,
4
+ listScopedThreads,
5
+ resolveCodexThreadDbPath,
6
+ } from '../codex-browser-db';
2
7
  import type { MessageEvent, ThreadBrowseData, ThreadEvent } from '../codex-browser-types';
3
8
  import { parseCodexTranscriptFile } from '../codex-thread-parser';
4
9
  import type { ThreadRow } from '../codex-thread-types';
5
10
  import { cleanInlineTitle } from '../shared';
11
+ import { runWithTranscriptLoadLimit } from '../transcript-load-limiter';
6
12
  import { selectConversationMessages } from './message-selector';
7
13
  import { getConversationPathMatch } from './path-match';
8
14
  import type {
@@ -12,6 +18,7 @@ import type {
12
18
  ConversationMessagePhase,
13
19
  ConversationMessageRole,
14
20
  ConversationPathMatch,
21
+ DeleteConversationOptions,
15
22
  GetConversationOptions,
16
23
  ListConversationsForPathOptions,
17
24
  } from './types';
@@ -142,9 +149,17 @@ const toConversationMessage = (event: ThreadEvent): ConversationMessage | null =
142
149
  };
143
150
 
144
151
  const readCodexMessages = async (thread: ThreadRow): Promise<ConversationMessage[]> => {
145
- const transcript = await parseCodexTranscriptFile(thread.rollout_path, {
146
- includeRaw: false,
147
- });
152
+ const transcript = await runWithTranscriptLoadLimit(
153
+ () =>
154
+ parseCodexTranscriptFile(thread.rollout_path, {
155
+ includeRaw: false,
156
+ }),
157
+ {
158
+ id: thread.id,
159
+ path: thread.rollout_path,
160
+ source: 'codex-api',
161
+ },
162
+ );
148
163
 
149
164
  return transcript.events.flatMap((event) => {
150
165
  const message = toConversationMessage(event);
@@ -238,7 +253,16 @@ const getCodexConversation = async (options: GetConversationOptions): Promise<Co
238
253
  });
239
254
  };
240
255
 
256
+ const deleteCodexConversation = async (options: DeleteConversationOptions) => {
257
+ const result = await deleteCodexThread(getCodexDbPath(options), options.id, { deleteSessionFiles: true });
258
+ return {
259
+ deletedFiles: result.deletedSessionFiles,
260
+ deletedIds: result.deletedThreadIds,
261
+ };
262
+ };
263
+
241
264
  export const codexConversationAdapter: ConversationAdapter = {
265
+ deleteConversation: deleteCodexConversation,
242
266
  getConversation: getCodexConversation,
243
267
  listConversationsForPath: listCodexConversationsForPath,
244
268
  source: 'codex',
@@ -11,7 +11,9 @@ import type {
11
11
  CursorWorkspaceGroup,
12
12
  } from '../cursor-exporter-types';
13
13
  import { getCursorGlobalDbPath, resolveCursorUserDir } from '../cursor-exporter-types';
14
+ import { collectCursorThreadsForDeletion, isCursorRunning, pruneCursorThreads } from '../cursor-recovery';
14
15
  import { cleanInlineTitle } from '../shared';
16
+ import { runWithTranscriptLoadLimit } from '../transcript-load-limiter';
15
17
  import { createDeepLinks, createTextMessage, finalizeMessages } from './adapter-helpers';
16
18
  import { selectConversationMessages } from './message-selector';
17
19
  import { getFirstConversationPathMatch } from './path-match';
@@ -20,6 +22,7 @@ import type {
20
22
  ConversationDetail,
21
23
  ConversationMessage,
22
24
  ConversationPathMatch,
25
+ DeleteConversationOptions,
23
26
  GetConversationOptions,
24
27
  ListConversationsForPathOptions,
25
28
  } from './types';
@@ -90,8 +93,16 @@ const buildConversation = async (
90
93
  matches: ConversationPathMatch[],
91
94
  options: Pick<ListConversationsForPathOptions, 'includeMessages' | 'messageSelector'>,
92
95
  ): Promise<ConversationDetail> => {
96
+ const globalDbPath = getCursorGlobalDbPath(userDir);
93
97
  const transcript = options.includeMessages
94
- ? await readCursorThreadTranscriptWithAgentFiles(getCursorGlobalDbPath(userDir), thread.composerId, userDir)
98
+ ? await runWithTranscriptLoadLimit(
99
+ () => readCursorThreadTranscriptWithAgentFiles(globalDbPath, thread.composerId, userDir),
100
+ {
101
+ id: thread.composerId,
102
+ path: thread.transcriptDirs[0] ?? globalDbPath,
103
+ source: 'cursor-api',
104
+ },
105
+ )
95
106
  : null;
96
107
  const allMessages = transcript ? transcriptToMessages(transcript) : [];
97
108
  const messages = options.includeMessages
@@ -164,7 +175,30 @@ const getCursorConversation = async (options: GetConversationOptions): Promise<C
164
175
  return null;
165
176
  };
166
177
 
178
+ const deleteCursorConversation = async (options: DeleteConversationOptions) => {
179
+ const userDir = getUserDir(options);
180
+ if (!options.locations?.cursorUserDir && (await isCursorRunning())) {
181
+ throw new Error(
182
+ 'Quit Cursor before deleting. It rewrites chat history on exit, which can resurrect deleted threads.',
183
+ );
184
+ }
185
+
186
+ const existing = await getCursorConversation(options);
187
+ if (!existing) {
188
+ return { deletedFiles: [], deletedIds: [] };
189
+ }
190
+
191
+ const threads = await collectCursorThreadsForDeletion([options.id], userDir);
192
+ const deletedFiles = threads.flatMap((thread) => thread.transcriptDirs);
193
+ const result = await pruneCursorThreads(threads, true, userDir);
194
+ return {
195
+ deletedFiles,
196
+ deletedIds: result.composerIds,
197
+ };
198
+ };
199
+
167
200
  export const cursorConversationAdapter: ConversationAdapter = {
201
+ deleteConversation: deleteCursorConversation,
168
202
  getConversation: getCursorConversation,
169
203
  listConversationsForPath: listCursorConversationsForPath,
170
204
  source: 'cursor',
@@ -0,0 +1,210 @@
1
+ import {
2
+ deleteGrokSession,
3
+ listGrokSessionsForGroup,
4
+ listGrokWorkspaceGroups,
5
+ readGrokSessionTranscript,
6
+ resolveGrokSessionsDir,
7
+ } from '../grok-db';
8
+ import type {
9
+ GrokSessionSummary,
10
+ GrokSessionTranscript,
11
+ GrokTranscriptEntry,
12
+ GrokTranscriptPart,
13
+ } from '../grok-exporter-types';
14
+ import { getFinalGrokAssistantTextPartIds, getGrokTextPartPhase } from '../grok-transcript-phase';
15
+ import { cleanInlineTitle } from '../shared';
16
+ import { runWithTranscriptLoadLimit } from '../transcript-load-limiter';
17
+ import {
18
+ createDeepLinks,
19
+ createTextMessage,
20
+ finalizeMessages,
21
+ normalizeAssistantPhase,
22
+ normalizeRole,
23
+ } from './adapter-helpers';
24
+ import { selectConversationMessages } from './message-selector';
25
+ import { getConversationPathMatch } from './path-match';
26
+ import type {
27
+ ConversationAdapter,
28
+ ConversationDetail,
29
+ ConversationMessage,
30
+ ConversationPathMatch,
31
+ DeleteConversationOptions,
32
+ GetConversationOptions,
33
+ ListConversationsForPathOptions,
34
+ } from './types';
35
+
36
+ const getSessionsDir = (options: { locations?: { grokSessionsDir?: string } }) =>
37
+ options.locations?.grokSessionsDir ?? resolveGrokSessionsDir();
38
+
39
+ const partToMessages = (
40
+ entry: GrokTranscriptEntry,
41
+ part: GrokTranscriptPart,
42
+ finalTextPartIds: Set<string>,
43
+ order: number,
44
+ ): ConversationMessage[] => {
45
+ if (part.type === 'text') {
46
+ return createTextMessage({
47
+ createdAtMs: entry.createdAtMs,
48
+ id: part.partId,
49
+ metadata: { modelFingerprint: entry.modelFingerprint, modelId: entry.modelId },
50
+ order,
51
+ phase: normalizeAssistantPhase(getGrokTextPartPhase(entry, part, finalTextPartIds), 'unknown'),
52
+ role: normalizeRole(entry.role),
53
+ text: part.text,
54
+ });
55
+ }
56
+
57
+ if (part.type === 'reasoning') {
58
+ return createTextMessage({
59
+ createdAtMs: entry.createdAtMs,
60
+ id: part.partId,
61
+ order,
62
+ phase: 'reasoning',
63
+ role: 'assistant',
64
+ text: part.text,
65
+ });
66
+ }
67
+
68
+ if (part.type === 'tool_call') {
69
+ return createTextMessage({
70
+ createdAtMs: entry.createdAtMs,
71
+ id: part.partId,
72
+ metadata: { toolCallId: part.toolCallId, toolName: part.toolName },
73
+ order,
74
+ phase: 'tool_call',
75
+ role: 'tool',
76
+ text: [part.toolName, part.argumentsText].filter(Boolean).join('\n'),
77
+ });
78
+ }
79
+
80
+ if (part.type === 'tool_result') {
81
+ return createTextMessage({
82
+ createdAtMs: entry.createdAtMs,
83
+ id: part.partId,
84
+ metadata: { toolCallId: part.toolCallId },
85
+ order,
86
+ phase: 'tool_output',
87
+ role: 'tool',
88
+ text: part.outputText,
89
+ });
90
+ }
91
+
92
+ return [];
93
+ };
94
+
95
+ const transcriptToMessages = (transcript: GrokSessionTranscript): ConversationMessage[] => {
96
+ const finalTextPartIds = getFinalGrokAssistantTextPartIds(transcript.entries);
97
+ return finalizeMessages(
98
+ transcript.entries.flatMap((entry, entryIndex) =>
99
+ entry.parts.flatMap((part, partIndex) =>
100
+ partToMessages(entry, part, finalTextPartIds, entryIndex + partIndex),
101
+ ),
102
+ ),
103
+ );
104
+ };
105
+
106
+ const buildConversation = async (
107
+ session: GrokSessionSummary,
108
+ sessionsDir: string,
109
+ matches: ConversationPathMatch[],
110
+ options: Pick<ListConversationsForPathOptions, 'includeMessages' | 'messageSelector'>,
111
+ loadedTranscript: GrokSessionTranscript | null = null,
112
+ ): Promise<ConversationDetail> => {
113
+ const transcript =
114
+ loadedTranscript ??
115
+ (options.includeMessages
116
+ ? await runWithTranscriptLoadLimit(
117
+ () => readGrokSessionTranscript(sessionsDir, session.sessionId, { includeRawPayloads: false }),
118
+ {
119
+ id: session.sessionId,
120
+ path: sessionsDir,
121
+ source: 'grok-api',
122
+ },
123
+ )
124
+ : null);
125
+ const allMessages = transcript ? transcriptToMessages(transcript) : [];
126
+ const messages = options.includeMessages
127
+ ? selectConversationMessages(allMessages, options.messageSelector ?? 'last_final_answer')
128
+ : [];
129
+
130
+ return {
131
+ createdAtMs: session.createdAtMs,
132
+ deepLinks: createDeepLinks('grok', session.sessionId, `/grok-sessions/${session.sessionId}`),
133
+ id: session.sessionId,
134
+ matches,
135
+ messageCount: options.includeMessages ? allMessages.length : session.messageCount,
136
+ messages,
137
+ metadata: {
138
+ agentName: session.agentName,
139
+ currentModelId: session.currentModelId,
140
+ gitBranch: session.gitBranch,
141
+ headCommit: session.headCommit,
142
+ modelLabel: session.modelLabel,
143
+ renderablePartCount: session.renderablePartCount,
144
+ sandboxProfile: session.sandboxProfile,
145
+ },
146
+ source: 'grok',
147
+ title: cleanInlineTitle(session.title),
148
+ updatedAtMs: session.lastActiveAtMs,
149
+ workspaceKey: session.workspaceKey,
150
+ workspacePath: session.worktree,
151
+ };
152
+ };
153
+
154
+ const listGrokConversationsForPath = async (options: ListConversationsForPathOptions) => {
155
+ const sessionsDir = getSessionsDir(options);
156
+ const groups = await listGrokWorkspaceGroups(sessionsDir);
157
+ const conversations: ConversationDetail[] = [];
158
+
159
+ for (const group of groups) {
160
+ const match = await getConversationPathMatch(options.cwd, group.worktree);
161
+ if (!match) {
162
+ continue;
163
+ }
164
+ const sessions = await listGrokSessionsForGroup(group.key, sessionsDir);
165
+ for (const session of sessions) {
166
+ conversations.push(await buildConversation(session, sessionsDir, [match], options));
167
+ }
168
+ }
169
+
170
+ return conversations;
171
+ };
172
+
173
+ const getGrokConversation = async (options: GetConversationOptions): Promise<ConversationDetail | null> => {
174
+ const sessionsDir = getSessionsDir(options);
175
+ const transcript = await runWithTranscriptLoadLimit(
176
+ () => readGrokSessionTranscript(sessionsDir, options.id, { includeRawPayloads: false }),
177
+ {
178
+ id: options.id,
179
+ path: sessionsDir,
180
+ source: 'grok-api',
181
+ },
182
+ );
183
+ return transcript
184
+ ? buildConversation(
185
+ transcript.session,
186
+ sessionsDir,
187
+ [],
188
+ {
189
+ includeMessages: true,
190
+ messageSelector: options.messageSelector ?? 'all',
191
+ },
192
+ transcript,
193
+ )
194
+ : null;
195
+ };
196
+
197
+ const deleteGrokConversation = async (options: DeleteConversationOptions) => {
198
+ const result = await deleteGrokSession(getSessionsDir(options), options.id);
199
+ return {
200
+ deletedFiles: result.deletedFiles,
201
+ deletedIds: result.deletedSessionIds,
202
+ };
203
+ };
204
+
205
+ export const grokConversationAdapter: ConversationAdapter = {
206
+ deleteConversation: deleteGrokConversation,
207
+ getConversation: getGrokConversation,
208
+ listConversationsForPath: listGrokConversationsForPath,
209
+ source: 'grok',
210
+ };
@@ -1,7 +1,9 @@
1
+ import { mapWithConcurrency } from '../concurrency';
1
2
  import { antigravityConversationAdapter } from './antigravity-adapter';
2
3
  import { claudeCodeConversationAdapter } from './claude-code-adapter';
3
4
  import { codexConversationAdapter } from './codex-adapter';
4
5
  import { cursorConversationAdapter } from './cursor-adapter';
6
+ import { grokConversationAdapter } from './grok-adapter';
5
7
  import { kiroConversationAdapter } from './kiro-adapter';
6
8
  import { selectConversationMessages } from './message-selector';
7
9
  import { opencodeConversationAdapter } from './opencode-adapter';
@@ -14,6 +16,11 @@ import {
14
16
  type ConversationPage,
15
17
  type ConversationSource,
16
18
  type ConversationSourceInfo,
19
+ type DeleteConversationItemResult,
20
+ type DeleteConversationOptions,
21
+ type DeleteConversationResult,
22
+ type DeleteConversationsOptions,
23
+ type DeleteConversationsResult,
17
24
  type GetConversationOptions,
18
25
  type ListConversationsForPathOptions,
19
26
  type ResolvedConversationRef,
@@ -27,6 +34,7 @@ export {
27
34
  type ConversationDataLocations,
28
35
  type ConversationDeepLinks,
29
36
  type ConversationDetail,
37
+ type ConversationIdSetOptions,
30
38
  type ConversationMessage,
31
39
  type ConversationMessagePhase,
32
40
  type ConversationMessageRole,
@@ -35,6 +43,13 @@ export {
35
43
  type ConversationPathMatch,
36
44
  type ConversationSource,
37
45
  type ConversationSourceInfo,
46
+ type ConversationZipDownload,
47
+ type DeleteConversationItemResult,
48
+ type DeleteConversationOptions,
49
+ type DeleteConversationResult,
50
+ type DeleteConversationsOptions,
51
+ type DeleteConversationsResult,
52
+ type ExportConversationsZipOptions,
38
53
  type GetConversationOptions,
39
54
  type ListConversationsForPathOptions,
40
55
  type ResolvedConversationRef,
@@ -45,6 +60,7 @@ const SOURCE_LABELS: Record<ConversationSource, string> = {
45
60
  'claude-code': 'Claude Code',
46
61
  codex: 'Codex',
47
62
  cursor: 'Cursor',
63
+ grok: 'Grok',
48
64
  kiro: 'Kiro',
49
65
  opencode: 'OpenCode',
50
66
  qoder: 'Qoder',
@@ -64,6 +80,7 @@ const ADAPTERS: Partial<Record<ConversationSource, ConversationAdapter>> = {
64
80
  'claude-code': claudeCodeConversationAdapter,
65
81
  codex: codexConversationAdapter,
66
82
  cursor: cursorConversationAdapter,
83
+ grok: grokConversationAdapter,
67
84
  kiro: kiroConversationAdapter,
68
85
  opencode: opencodeConversationAdapter,
69
86
  qoder: qoderConversationAdapter,
@@ -71,6 +88,16 @@ const ADAPTERS: Partial<Record<ConversationSource, ConversationAdapter>> = {
71
88
 
72
89
  const MAX_LIMIT = 200;
73
90
  const DEFAULT_LIMIT = 100;
91
+ const DELETE_CONCURRENCY_BY_SOURCE: Record<ConversationSource, number> = {
92
+ antigravity: 1,
93
+ 'claude-code': 4,
94
+ codex: 1,
95
+ cursor: 1,
96
+ grok: 1,
97
+ kiro: 1,
98
+ opencode: 2,
99
+ qoder: 1,
100
+ };
74
101
 
75
102
  const getEnabledSources = (sources: ListConversationsForPathOptions['sources']): ConversationSource[] => {
76
103
  if (!sources || sources === 'all') {
@@ -181,10 +208,56 @@ export const getConversation = async (options: GetConversationOptions) => {
181
208
  return getAdapter(options.source)?.getConversation(options) ?? null;
182
209
  };
183
210
 
211
+ export const deleteConversation = async (
212
+ options: DeleteConversationOptions,
213
+ ): Promise<DeleteConversationResult | null> => {
214
+ return (await getAdapter(options.source)?.deleteConversation?.(options)) ?? null;
215
+ };
216
+
217
+ export const deleteConversations = async (
218
+ options: DeleteConversationsOptions,
219
+ ): Promise<DeleteConversationsResult | null> => {
220
+ const adapter = getAdapter(options.source);
221
+ if (!adapter?.deleteConversation) {
222
+ return null;
223
+ }
224
+ const deleteAdapterConversation = adapter.deleteConversation;
225
+
226
+ const rawResults = await mapWithConcurrency(
227
+ options.ids,
228
+ DELETE_CONCURRENCY_BY_SOURCE[options.source],
229
+ async (id) => ({
230
+ id,
231
+ result: await deleteAdapterConversation({
232
+ id,
233
+ locations: options.locations,
234
+ source: options.source,
235
+ }),
236
+ }),
237
+ );
238
+ const deletedIdSet = new Set(rawResults.flatMap(({ result }) => result.deletedIds));
239
+ const results: DeleteConversationItemResult[] = rawResults.map(({ id, result }) => ({
240
+ deleted: result.deletedIds.length > 0 || deletedIdSet.has(id),
241
+ deletedFiles: result.deletedFiles,
242
+ deletedIds: result.deletedIds,
243
+ id,
244
+ }));
245
+
246
+ return {
247
+ deletedFiles: [...new Set(results.flatMap((result) => result.deletedFiles))],
248
+ deletedIds: [...deletedIdSet],
249
+ missingIds: results.filter((result) => !result.deleted).map((result) => result.id),
250
+ results,
251
+ };
252
+ };
253
+
184
254
  const sourceFromSessionRoute = (segment: string): ConversationSource | null => {
185
255
  if (segment === 'claude-code-sessions') {
186
256
  return 'claude-code';
187
257
  }
258
+ if (segment === 'grok-sessions') {
259
+ return 'grok';
260
+ }
188
261
  if (segment === 'kiro-sessions') {
189
262
  return 'kiro';
190
263
  }
@@ -287,7 +360,9 @@ export const renderConversationMarkdown = (
287
360
  messages: ConversationMessage[];
288
361
  title: string | null;
289
362
  },
290
- options: { messageSelector?: ConversationMessageSelector } = {},
363
+ options: {
364
+ messageSelector?: ConversationMessageSelector;
365
+ } = {},
291
366
  ) => {
292
367
  const selectedMessages = options.messageSelector
293
368
  ? selectConversationMessages(conversation.messages, options.messageSelector)
@@ -1,4 +1,9 @@
1
- import { listKiroSessionsForGroup, listKiroWorkspaceGroups, readKiroSessionTranscript } from '../kiro-db';
1
+ import {
2
+ deleteKiroSession,
3
+ listKiroSessionsForGroup,
4
+ listKiroWorkspaceGroups,
5
+ readKiroSessionTranscript,
6
+ } from '../kiro-db';
2
7
  import type {
3
8
  KiroSessionSummary,
4
9
  KiroSessionTranscript,
@@ -8,6 +13,7 @@ import type {
8
13
  import { resolveKiroWorkspaceSessionsDir } from '../kiro-exporter-types';
9
14
  import { getFinalKiroAssistantMessageEntryIds, getKiroMessagePhase } from '../kiro-transcript-phase';
10
15
  import { cleanInlineTitle } from '../shared';
16
+ import { runWithTranscriptLoadLimit } from '../transcript-load-limiter';
11
17
  import {
12
18
  createDeepLinks,
13
19
  createTextMessage,
@@ -23,6 +29,7 @@ import type {
23
29
  ConversationDetail,
24
30
  ConversationMessage,
25
31
  ConversationPathMatch,
32
+ DeleteConversationOptions,
26
33
  GetConversationOptions,
27
34
  ListConversationsForPathOptions,
28
35
  } from './types';
@@ -74,8 +81,16 @@ const buildConversation = async (
74
81
  sessionsDir: string,
75
82
  matches: ConversationPathMatch[],
76
83
  options: Pick<ListConversationsForPathOptions, 'includeMessages' | 'messageSelector'>,
84
+ loadedTranscript: KiroSessionTranscript | null = null,
77
85
  ): Promise<ConversationDetail> => {
78
- const transcript = options.includeMessages ? await readKiroSessionTranscript(sessionsDir, session.sessionId) : null;
86
+ const transcript = options.includeMessages
87
+ ? (loadedTranscript ??
88
+ (await runWithTranscriptLoadLimit(() => readKiroSessionTranscript(sessionsDir, session.sessionId), {
89
+ id: session.sessionId,
90
+ path: session.filePath,
91
+ source: 'kiro-api',
92
+ })))
93
+ : null;
79
94
  const allMessages = transcript ? transcriptToMessages(transcript) : [];
80
95
  const messages = options.includeMessages
81
96
  ? selectConversationMessages(allMessages, options.messageSelector ?? 'last_final_answer')
@@ -124,16 +139,35 @@ const listKiroConversationsForPath = async (options: ListConversationsForPathOpt
124
139
 
125
140
  const getKiroConversation = async (options: GetConversationOptions): Promise<ConversationDetail | null> => {
126
141
  const sessionsDir = getSessionsDir(options);
127
- const transcript = await readKiroSessionTranscript(sessionsDir, options.id);
142
+ const transcript = await runWithTranscriptLoadLimit(() => readKiroSessionTranscript(sessionsDir, options.id), {
143
+ id: options.id,
144
+ path: sessionsDir,
145
+ source: 'kiro-api',
146
+ });
128
147
  return transcript
129
- ? buildConversation(transcript.session, sessionsDir, [], {
130
- includeMessages: true,
131
- messageSelector: options.messageSelector ?? 'all',
132
- })
148
+ ? buildConversation(
149
+ transcript.session,
150
+ sessionsDir,
151
+ [],
152
+ {
153
+ includeMessages: true,
154
+ messageSelector: options.messageSelector ?? 'all',
155
+ },
156
+ transcript,
157
+ )
133
158
  : null;
134
159
  };
135
160
 
161
+ const deleteKiroConversation = async (options: DeleteConversationOptions) => {
162
+ const result = await deleteKiroSession(getSessionsDir(options), options.id);
163
+ return {
164
+ deletedFiles: result.deletedFiles,
165
+ deletedIds: result.deletedSessionIds,
166
+ };
167
+ };
168
+
136
169
  export const kiroConversationAdapter: ConversationAdapter = {
170
+ deleteConversation: deleteKiroConversation,
137
171
  getConversation: getKiroConversation,
138
172
  listConversationsForPath: listKiroConversationsForPath,
139
173
  source: 'kiro',
@@ -1,4 +1,5 @@
1
1
  import {
2
+ deleteOpenCodeSession,
2
3
  listOpenCodeSessionsForGroup,
3
4
  listOpenCodeWorkspaceGroups,
4
5
  readOpenCodeSessionTranscript,
@@ -12,6 +13,7 @@ import { resolveOpenCodeDbPath } from '../opencode-exporter-types';
12
13
  import { splitOpenCodeThinkTaggedText } from '../opencode-think-tags';
13
14
  import { getFinalOpenCodeAssistantTextPartIds, getOpenCodeTextPartPhase } from '../opencode-transcript-phase';
14
15
  import { cleanInlineTitle } from '../shared';
16
+ import { runWithTranscriptLoadLimit } from '../transcript-load-limiter';
15
17
  import {
16
18
  createDeepLinks,
17
19
  createTextMessage,
@@ -26,6 +28,7 @@ import type {
26
28
  ConversationDetail,
27
29
  ConversationMessage,
28
30
  ConversationPathMatch,
31
+ DeleteConversationOptions,
29
32
  GetConversationOptions,
30
33
  ListConversationsForPathOptions,
31
34
  } from './types';
@@ -109,7 +112,13 @@ const buildConversation = async (
109
112
  ): Promise<ConversationDetail> => {
110
113
  const transcript =
111
114
  loadedTranscript ??
112
- (options.includeMessages ? await readOpenCodeSessionTranscript(dbPath, session.sessionId) : null);
115
+ (options.includeMessages
116
+ ? await runWithTranscriptLoadLimit(() => readOpenCodeSessionTranscript(dbPath, session.sessionId), {
117
+ id: session.sessionId,
118
+ path: dbPath,
119
+ source: 'opencode-api',
120
+ })
121
+ : null);
113
122
  const allMessages = transcript ? transcriptToMessages(transcript) : [];
114
123
  const messages = options.includeMessages
115
124
  ? selectConversationMessages(allMessages, options.messageSelector ?? 'last_final_answer')
@@ -158,7 +167,11 @@ const listOpenCodeConversationsForPath = async (options: ListConversationsForPat
158
167
 
159
168
  const getOpenCodeConversation = async (options: GetConversationOptions): Promise<ConversationDetail | null> => {
160
169
  const dbPath = getDbPath(options);
161
- const transcript = await readOpenCodeSessionTranscript(dbPath, options.id);
170
+ const transcript = await runWithTranscriptLoadLimit(() => readOpenCodeSessionTranscript(dbPath, options.id), {
171
+ id: options.id,
172
+ path: dbPath,
173
+ source: 'opencode-api',
174
+ });
162
175
  return transcript
163
176
  ? buildConversation(
164
177
  transcript.session,
@@ -173,7 +186,16 @@ const getOpenCodeConversation = async (options: GetConversationOptions): Promise
173
186
  : null;
174
187
  };
175
188
 
189
+ const deleteOpenCodeConversation = async (options: DeleteConversationOptions) => {
190
+ const result = await deleteOpenCodeSession(getDbPath(options), options.id);
191
+ return {
192
+ deletedFiles: [],
193
+ deletedIds: result.deletedSessionIds,
194
+ };
195
+ };
196
+
176
197
  export const opencodeConversationAdapter: ConversationAdapter = {
198
+ deleteConversation: deleteOpenCodeConversation,
177
199
  getConversation: getOpenCodeConversation,
178
200
  listConversationsForPath: listOpenCodeConversationsForPath,
179
201
  source: 'opencode',