spiracha 2.0.0 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -1
- package/apps/ui/README.md +20 -2
- package/apps/ui/package.json +2 -2
- package/apps/ui/src/components/antigravity-conversations-table.tsx +50 -11
- package/apps/ui/src/components/antigravity-workspaces-table.tsx +1 -1
- package/apps/ui/src/components/app-shell.tsx +1 -0
- package/apps/ui/src/components/breadcrumbs.tsx +21 -3
- package/apps/ui/src/components/claude-code-sessions-table.tsx +42 -6
- package/apps/ui/src/components/claude-code-workspaces-table.tsx +3 -1
- package/apps/ui/src/components/cursor-threads-table.tsx +10 -47
- package/apps/ui/src/components/data-table.tsx +7 -5
- package/apps/ui/src/components/export-dialog.tsx +2 -12
- package/apps/ui/src/components/grok-sessions-table.tsx +146 -0
- package/apps/ui/src/components/grok-workspaces-table.tsx +53 -0
- package/apps/ui/src/components/kiro-sessions-table.tsx +40 -4
- package/apps/ui/src/components/opencode-sessions-table.tsx +40 -4
- package/apps/ui/src/components/page-header.tsx +15 -9
- package/apps/ui/src/components/qoder-sessions-table.tsx +23 -2
- package/apps/ui/src/components/qoder-workspaces-table.tsx +5 -1
- package/apps/ui/src/components/selection-actions-toolbar.tsx +80 -0
- package/apps/ui/src/components/threads-table.tsx +9 -46
- package/apps/ui/src/components/transcript-view.tsx +148 -52
- package/apps/ui/src/components/ui/select.tsx +1 -1
- package/apps/ui/src/lib/antigravity-conversation-state.ts +17 -4
- package/apps/ui/src/lib/antigravity-server.ts +152 -7
- package/apps/ui/src/lib/antigravity-transcript-events.ts +11 -3
- package/apps/ui/src/lib/claude-code-queries.ts +8 -0
- package/apps/ui/src/lib/claude-code-server.ts +175 -7
- package/apps/ui/src/lib/claude-code-transcript-events.ts +8 -1
- package/apps/ui/src/lib/codex-queries.ts +19 -3
- package/apps/ui/src/lib/codex-server.ts +135 -53
- package/apps/ui/src/lib/cursor-server.ts +48 -5
- package/apps/ui/src/lib/formatters.ts +3 -5
- package/apps/ui/src/lib/grok-queries.ts +22 -0
- package/apps/ui/src/lib/grok-server.ts +169 -0
- package/apps/ui/src/lib/grok-transcript-events.ts +149 -0
- package/apps/ui/src/lib/kiro-server.ts +85 -7
- package/apps/ui/src/lib/opencode-server.ts +85 -7
- package/apps/ui/src/lib/qoder-server.ts +67 -11
- package/apps/ui/src/lib/route-search.ts +114 -0
- package/apps/ui/src/lib/source-session-export-server.ts +86 -3
- package/apps/ui/src/lib/thread-transcript-load.ts +15 -0
- package/apps/ui/src/lib/workspace-delete-navigation.ts +12 -0
- package/apps/ui/src/routeTree.gen.ts +107 -0
- package/apps/ui/src/routes/antigravity-conversations.$conversationId.tsx +76 -10
- package/apps/ui/src/routes/antigravity.$workspaceKey.tsx +269 -31
- package/apps/ui/src/routes/api.v1.conversations.$source.$id.ts +4 -0
- package/apps/ui/src/routes/api.v1.conversations.delete.ts +12 -0
- package/apps/ui/src/routes/api.v1.conversations.export.ts +12 -0
- package/apps/ui/src/routes/claude-code-sessions.$sessionId.tsx +143 -17
- package/apps/ui/src/routes/claude-code.$workspaceKey.tsx +165 -26
- package/apps/ui/src/routes/cursor-threads.$composerId.tsx +2 -2
- package/apps/ui/src/routes/cursor.$workspaceKey.tsx +11 -1
- package/apps/ui/src/routes/grok-sessions.$sessionId.tsx +388 -0
- package/apps/ui/src/routes/grok.$workspaceKey.tsx +301 -0
- package/apps/ui/src/routes/grok.index.tsx +48 -0
- package/apps/ui/src/routes/kiro-sessions.$sessionId.tsx +66 -15
- package/apps/ui/src/routes/kiro.$workspaceKey.tsx +199 -32
- package/apps/ui/src/routes/opencode-sessions.$sessionId.tsx +105 -19
- package/apps/ui/src/routes/opencode.$workspaceKey.tsx +236 -44
- package/apps/ui/src/routes/opencode.index.tsx +16 -3
- package/apps/ui/src/routes/qoder-sessions.$sessionId.tsx +7 -4
- package/apps/ui/src/routes/qoder.$workspaceKey.tsx +75 -28
- package/apps/ui/src/routes/threads.$threadId.tsx +548 -64
- package/package.json +19 -18
- package/src/client.ts +134 -1
- package/src/lib/antigravity-db.ts +208 -32
- package/src/lib/antigravity-exporter-types.ts +3 -0
- package/src/lib/antigravity-projects.ts +201 -0
- package/src/lib/claude-code-db.ts +498 -32
- package/src/lib/claude-code-exporter-types.ts +5 -0
- package/src/lib/claude-code-transcript-phase.ts +60 -1
- package/src/lib/claude-code-transcript.ts +8 -5
- package/src/lib/codex-browser-export.ts +23 -27
- package/src/lib/codex-thread-cache.ts +41 -13
- package/src/lib/codex-thread-parser.ts +86 -35
- package/src/lib/codex-transcript-filter.ts +31 -0
- package/src/lib/codex-transcript-renderer.ts +39 -19
- package/src/lib/concurrency.ts +41 -0
- package/src/lib/conversation-api.ts +399 -19
- package/src/lib/conversation-data/antigravity-adapter.ts +21 -2
- package/src/lib/conversation-data/claude-code-adapter.ts +37 -6
- package/src/lib/conversation-data/codex-adapter.ts +28 -4
- package/src/lib/conversation-data/cursor-adapter.ts +35 -1
- package/src/lib/conversation-data/grok-adapter.ts +210 -0
- package/src/lib/conversation-data/index.ts +76 -1
- package/src/lib/conversation-data/kiro-adapter.ts +41 -7
- package/src/lib/conversation-data/opencode-adapter.ts +24 -2
- package/src/lib/conversation-data/qoder-adapter.ts +44 -19
- package/src/lib/conversation-data/types.ts +43 -0
- package/src/lib/conversation-zip-export.ts +57 -0
- package/src/lib/cursor-db.ts +34 -5
- package/src/lib/grok-db.ts +1026 -0
- package/src/lib/grok-exporter-types.ts +110 -0
- package/src/lib/grok-transcript-phase.ts +52 -0
- package/src/lib/grok-transcript.ts +154 -0
- package/src/lib/kiro-db.ts +52 -1
- package/src/lib/model-label.ts +11 -3
- package/src/lib/opencode-db.ts +598 -55
- package/src/lib/opencode-transcript.ts +8 -5
- package/src/lib/shared.ts +12 -0
- package/src/lib/transcript-load-limiter.ts +82 -0
- package/src/lib/ui-cache.ts +43 -17
- package/src/lib/ui-export-archive.ts +60 -0
|
@@ -11,11 +11,9 @@ type DateTimeFormatterSet = {
|
|
|
11
11
|
};
|
|
12
12
|
|
|
13
13
|
const DATE_TIME_FORMATTERS = new Map<string, DateTimeFormatterSet>();
|
|
14
|
-
const DEFAULT_TIME_ZONE = 'UTC';
|
|
15
14
|
|
|
16
15
|
const getDateTimeFormatters = (timeZone?: string): DateTimeFormatterSet => {
|
|
17
|
-
const
|
|
18
|
-
const cacheKey = resolvedTimeZone;
|
|
16
|
+
const cacheKey = timeZone ?? 'local';
|
|
19
17
|
const cached = DATE_TIME_FORMATTERS.get(cacheKey);
|
|
20
18
|
if (cached) {
|
|
21
19
|
return cached;
|
|
@@ -25,7 +23,7 @@ const getDateTimeFormatters = (timeZone?: string): DateTimeFormatterSet => {
|
|
|
25
23
|
dayKeyFormatter: new Intl.DateTimeFormat('en-CA', {
|
|
26
24
|
day: '2-digit',
|
|
27
25
|
month: '2-digit',
|
|
28
|
-
timeZone
|
|
26
|
+
timeZone,
|
|
29
27
|
year: 'numeric',
|
|
30
28
|
}),
|
|
31
29
|
timePartsFormatter: new Intl.DateTimeFormat('en-US', {
|
|
@@ -34,7 +32,7 @@ const getDateTimeFormatters = (timeZone?: string): DateTimeFormatterSet => {
|
|
|
34
32
|
hour12: true,
|
|
35
33
|
minute: '2-digit',
|
|
36
34
|
month: 'short',
|
|
37
|
-
timeZone
|
|
35
|
+
timeZone,
|
|
38
36
|
year: 'numeric',
|
|
39
37
|
}),
|
|
40
38
|
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { queryOptions } from '@tanstack/react-query';
|
|
2
|
+
import { getGrokSessionDetailFn, listGrokSessionsFn, listGrokWorkspacesFn } from './grok-server';
|
|
3
|
+
|
|
4
|
+
export const grokWorkspacesQueryOptions = () =>
|
|
5
|
+
queryOptions({
|
|
6
|
+
queryFn: () => listGrokWorkspacesFn(),
|
|
7
|
+
queryKey: ['grok-workspaces'],
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
export const grokSessionsQueryOptions = (workspaceKey: string | null) =>
|
|
11
|
+
queryOptions({
|
|
12
|
+
enabled: workspaceKey !== null,
|
|
13
|
+
queryFn: () => listGrokSessionsFn({ data: { workspaceKey: workspaceKey ?? '' } }),
|
|
14
|
+
queryKey: ['grok-sessions', workspaceKey ?? 'none'],
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
export const grokSessionDetailQueryOptions = (sessionId: string | null) =>
|
|
18
|
+
queryOptions({
|
|
19
|
+
enabled: sessionId !== null,
|
|
20
|
+
queryFn: () => getGrokSessionDetailFn({ data: { sessionId: sessionId ?? '' } }),
|
|
21
|
+
queryKey: ['grok-session', sessionId ?? 'none'],
|
|
22
|
+
});
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { createServerFn } from '@tanstack/react-start';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { renderSourceSessionDownload, renderSourceSessionsDownload } from './source-session-export-server';
|
|
4
|
+
|
|
5
|
+
const workspaceSchema = z.object({
|
|
6
|
+
workspaceKey: z.string().min(1),
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
const sessionSchema = z.object({
|
|
10
|
+
sessionId: z.string().min(1),
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
const exportSessionSchema = z.object({
|
|
14
|
+
includeCommentary: z.boolean().default(true),
|
|
15
|
+
includeMetadata: z.boolean().default(true),
|
|
16
|
+
includeTools: z.boolean().default(true),
|
|
17
|
+
outputFormat: z.enum(['md', 'txt']).default('md'),
|
|
18
|
+
sessionId: z.string().min(1),
|
|
19
|
+
zipArchive: z.boolean().default(false),
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
const exportSessionsSchema = z.object({
|
|
23
|
+
includeCommentary: z.boolean().default(true),
|
|
24
|
+
includeMetadata: z.boolean().default(true),
|
|
25
|
+
includeTools: z.boolean().default(true),
|
|
26
|
+
outputFormat: z.enum(['md', 'txt']).default('md'),
|
|
27
|
+
sessionIds: z.array(z.string().min(1)).min(1),
|
|
28
|
+
zipArchive: z.boolean().default(true),
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const deleteSessionsSchema = z.object({
|
|
32
|
+
sessionIds: z.array(z.string().min(1)).min(1),
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
export const listGrokWorkspacesFn = createServerFn({ method: 'GET' }).handler(async () => {
|
|
36
|
+
const { listGrokWorkspaceGroups } = await import('@spiracha/lib/grok-db');
|
|
37
|
+
return listGrokWorkspaceGroups();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
export const listGrokSessionsFn = createServerFn({ method: 'GET' })
|
|
41
|
+
.validator(workspaceSchema)
|
|
42
|
+
.handler(async ({ data }) => {
|
|
43
|
+
const { listGrokSessionsForGroup } = await import('@spiracha/lib/grok-db');
|
|
44
|
+
return listGrokSessionsForGroup(data.workspaceKey);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
const loadGrokSessionTranscript = async (sessionId: string) => {
|
|
48
|
+
const { runWithTranscriptLoadLimit } = await import('@spiracha/lib/transcript-load-limiter');
|
|
49
|
+
const { readGrokSessionTranscript, resolveGrokSessionsDir } = await import('@spiracha/lib/grok-db');
|
|
50
|
+
const sessionsDir = resolveGrokSessionsDir();
|
|
51
|
+
return runWithTranscriptLoadLimit(
|
|
52
|
+
async () => {
|
|
53
|
+
const transcript = await readGrokSessionTranscript(sessionsDir, sessionId, {
|
|
54
|
+
includeRawPayloads: false,
|
|
55
|
+
});
|
|
56
|
+
if (!transcript) {
|
|
57
|
+
throw new Error(`Grok session not found: ${sessionId}`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return transcript;
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
id: sessionId,
|
|
64
|
+
path: sessionsDir,
|
|
65
|
+
source: 'grok-ui',
|
|
66
|
+
},
|
|
67
|
+
);
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
export const getGrokSessionDetailFn = createServerFn({ method: 'GET' })
|
|
71
|
+
.validator(sessionSchema)
|
|
72
|
+
.handler(async ({ data }) => {
|
|
73
|
+
return loadGrokSessionTranscript(data.sessionId);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
export const exportGrokSessionFn = createServerFn({ method: 'POST' })
|
|
77
|
+
.validator(exportSessionSchema)
|
|
78
|
+
.handler(async ({ data }) => {
|
|
79
|
+
const { renderGrokTranscript } = await import('@spiracha/lib/grok-transcript');
|
|
80
|
+
const transcript = await loadGrokSessionTranscript(data.sessionId);
|
|
81
|
+
const content = renderGrokTranscript(transcript, {
|
|
82
|
+
includeCommentary: data.includeCommentary,
|
|
83
|
+
includeMetadata: data.includeMetadata,
|
|
84
|
+
includeTools: data.includeTools,
|
|
85
|
+
outputFormat: data.outputFormat,
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
if (!content) {
|
|
89
|
+
throw new Error(`Grok session has no exportable content: ${data.sessionId}`);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return renderSourceSessionDownload({
|
|
93
|
+
content,
|
|
94
|
+
cwd: transcript.session.cwd ?? transcript.session.worktree,
|
|
95
|
+
fallbackBaseName: 'grok-session',
|
|
96
|
+
outputFormat: data.outputFormat,
|
|
97
|
+
sessionId: transcript.session.sessionId,
|
|
98
|
+
updatedAtMs: transcript.session.lastActiveAtMs,
|
|
99
|
+
zipArchive: data.zipArchive,
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
export const exportGrokSessionsFn = createServerFn({ method: 'POST' })
|
|
104
|
+
.validator(exportSessionsSchema)
|
|
105
|
+
.handler(async ({ data }) => {
|
|
106
|
+
const { renderGrokTranscript } = await import('@spiracha/lib/grok-transcript');
|
|
107
|
+
const entries = await Promise.all(
|
|
108
|
+
data.sessionIds.map(async (sessionId) => {
|
|
109
|
+
const transcript = await loadGrokSessionTranscript(sessionId);
|
|
110
|
+
const content = renderGrokTranscript(transcript, {
|
|
111
|
+
includeCommentary: data.includeCommentary,
|
|
112
|
+
includeMetadata: data.includeMetadata,
|
|
113
|
+
includeTools: data.includeTools,
|
|
114
|
+
outputFormat: data.outputFormat,
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
if (!content) {
|
|
118
|
+
throw new Error(`Grok session has no exportable content: ${sessionId}`);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return {
|
|
122
|
+
content,
|
|
123
|
+
cwd: transcript.session.cwd ?? transcript.session.worktree,
|
|
124
|
+
fallbackBaseName: 'grok-session',
|
|
125
|
+
fileBaseName: transcript.session.title || transcript.session.sessionId,
|
|
126
|
+
sessionId: transcript.session.sessionId,
|
|
127
|
+
updatedAtMs: transcript.session.lastActiveAtMs,
|
|
128
|
+
};
|
|
129
|
+
}),
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
return renderSourceSessionsDownload({
|
|
133
|
+
entries,
|
|
134
|
+
fallbackBaseName: 'grok-sessions',
|
|
135
|
+
outputFormat: data.outputFormat,
|
|
136
|
+
zipArchive: data.zipArchive,
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
export const deleteGrokSessionFn = createServerFn({ method: 'POST' })
|
|
141
|
+
.validator(sessionSchema)
|
|
142
|
+
.handler(async ({ data }) => {
|
|
143
|
+
const { deleteGrokSession, resolveGrokSessionsDir } = await import('@spiracha/lib/grok-db');
|
|
144
|
+
const result = await deleteGrokSession(resolveGrokSessionsDir(), data.sessionId);
|
|
145
|
+
if (result.deletedSessionIds.length === 0) {
|
|
146
|
+
throw new Error(`Grok session not found: ${data.sessionId}`);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return result;
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
export const deleteGrokSessionsFn = createServerFn({ method: 'POST' })
|
|
153
|
+
.validator(deleteSessionsSchema)
|
|
154
|
+
.handler(async ({ data }) => {
|
|
155
|
+
const { deleteGrokSession, resolveGrokSessionsDir } = await import('@spiracha/lib/grok-db');
|
|
156
|
+
const sessionsDir = resolveGrokSessionsDir();
|
|
157
|
+
const results = await Promise.all(
|
|
158
|
+
data.sessionIds.map((sessionId) => deleteGrokSession(sessionsDir, sessionId)),
|
|
159
|
+
);
|
|
160
|
+
const deletedSessionIds = results.flatMap((result) => result.deletedSessionIds);
|
|
161
|
+
if (deletedSessionIds.length === 0) {
|
|
162
|
+
throw new Error('No Grok sessions were deleted');
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return {
|
|
166
|
+
deletedFiles: results.flatMap((result) => result.deletedFiles),
|
|
167
|
+
deletedSessionIds,
|
|
168
|
+
};
|
|
169
|
+
});
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import type { ThreadEvent, ThreadTranscriptStats } from '@spiracha/lib/codex-browser-types';
|
|
2
|
+
import type { GrokSessionTranscript, GrokTranscriptEntry, GrokTranscriptPart } from '@spiracha/lib/grok-exporter-types';
|
|
3
|
+
import { getFinalGrokAssistantTextPartIds, getGrokTextPartPhase } from '@spiracha/lib/grok-transcript-phase';
|
|
4
|
+
import type { JsonValue } from '@spiracha/lib/shared';
|
|
5
|
+
import { getThreadTranscriptStats } from './thread-transcript-stats';
|
|
6
|
+
|
|
7
|
+
const buildRaw = (
|
|
8
|
+
entry: GrokTranscriptEntry,
|
|
9
|
+
part: GrokTranscriptPart,
|
|
10
|
+
eventType: string,
|
|
11
|
+
): Record<string, JsonValue> => ({
|
|
12
|
+
entryId: entry.entryId,
|
|
13
|
+
eventType,
|
|
14
|
+
role: entry.role,
|
|
15
|
+
source: 'grok_local_session',
|
|
16
|
+
type: part.type,
|
|
17
|
+
...part.raw,
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
const buildMessageEvent = (
|
|
21
|
+
entry: GrokTranscriptEntry,
|
|
22
|
+
part: GrokTranscriptPart,
|
|
23
|
+
sequence: number,
|
|
24
|
+
text: string,
|
|
25
|
+
finalTextPartIds: Set<string>,
|
|
26
|
+
): ThreadEvent => ({
|
|
27
|
+
isHiddenByDefault: entry.role !== 'assistant' && entry.role !== 'user',
|
|
28
|
+
kind: 'message',
|
|
29
|
+
memoryCitation: null,
|
|
30
|
+
model: entry.modelId ?? null,
|
|
31
|
+
phase: getGrokTextPartPhase(entry, part, finalTextPartIds),
|
|
32
|
+
raw: buildRaw(entry, part, 'message'),
|
|
33
|
+
role: entry.role,
|
|
34
|
+
sequence,
|
|
35
|
+
text,
|
|
36
|
+
timestamp: entry.timestamp,
|
|
37
|
+
variant: entry.role === 'user' ? 'user_message' : entry.role === 'assistant' ? 'agent_message' : 'message',
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const buildReasoningEvent = (
|
|
41
|
+
entry: GrokTranscriptEntry,
|
|
42
|
+
part: GrokTranscriptPart,
|
|
43
|
+
sequence: number,
|
|
44
|
+
): ThreadEvent | null => {
|
|
45
|
+
const text = part.text?.trim();
|
|
46
|
+
if (!text) {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
content: text,
|
|
52
|
+
hasEncryptedContent: false,
|
|
53
|
+
kind: 'reasoning',
|
|
54
|
+
raw: buildRaw(entry, part, 'reasoning'),
|
|
55
|
+
sequence,
|
|
56
|
+
summary: [text],
|
|
57
|
+
timestamp: entry.timestamp,
|
|
58
|
+
};
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const buildToolCallCommand = (part: GrokTranscriptPart): string => {
|
|
62
|
+
const toolName = part.toolName ?? 'unknown';
|
|
63
|
+
if (!part.argumentsText?.trim()) {
|
|
64
|
+
return toolName;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return `${toolName}\n${part.argumentsText}`;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const buildToolCallEvent = (entry: GrokTranscriptEntry, part: GrokTranscriptPart, sequence: number): ThreadEvent => ({
|
|
71
|
+
argumentsParseFailed: false,
|
|
72
|
+
argumentsText: part.argumentsText ?? null,
|
|
73
|
+
callId: part.toolCallId ?? null,
|
|
74
|
+
command: buildToolCallCommand(part),
|
|
75
|
+
kind: 'tool_call',
|
|
76
|
+
name: part.toolName ?? 'unknown',
|
|
77
|
+
raw: buildRaw(entry, part, 'tool_call'),
|
|
78
|
+
sequence,
|
|
79
|
+
timestamp: entry.timestamp,
|
|
80
|
+
workdir: null,
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
const buildToolOutputEvent = (
|
|
84
|
+
entry: GrokTranscriptEntry,
|
|
85
|
+
part: GrokTranscriptPart,
|
|
86
|
+
sequence: number,
|
|
87
|
+
): ThreadEvent | null => {
|
|
88
|
+
const outputText = part.outputText?.trim();
|
|
89
|
+
if (!outputText) {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
callId: part.toolCallId ?? null,
|
|
95
|
+
exitCode: null,
|
|
96
|
+
kind: 'tool_output',
|
|
97
|
+
outputText,
|
|
98
|
+
raw: buildRaw(entry, part, 'tool_output'),
|
|
99
|
+
sequence,
|
|
100
|
+
summary: outputText,
|
|
101
|
+
timestamp: entry.timestamp,
|
|
102
|
+
wallTime: null,
|
|
103
|
+
};
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const partToEvents = (
|
|
107
|
+
entry: GrokTranscriptEntry,
|
|
108
|
+
part: GrokTranscriptPart,
|
|
109
|
+
sequence: number,
|
|
110
|
+
finalTextPartIds: Set<string>,
|
|
111
|
+
): ThreadEvent[] => {
|
|
112
|
+
if (part.type === 'text') {
|
|
113
|
+
const text = part.text?.trim();
|
|
114
|
+
return text ? [buildMessageEvent(entry, part, sequence, text, finalTextPartIds)] : [];
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (part.type === 'reasoning') {
|
|
118
|
+
const event = buildReasoningEvent(entry, part, sequence);
|
|
119
|
+
return event ? [event] : [];
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (part.type === 'tool_call') {
|
|
123
|
+
return [buildToolCallEvent(entry, part, sequence)];
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (part.type === 'tool_result') {
|
|
127
|
+
const event = buildToolOutputEvent(entry, part, sequence);
|
|
128
|
+
return event ? [event] : [];
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return [];
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
export const grokTranscriptToThreadEvents = (transcript: GrokSessionTranscript): ThreadEvent[] => {
|
|
135
|
+
const finalTextPartIds = getFinalGrokAssistantTextPartIds(transcript.entries);
|
|
136
|
+
const events: ThreadEvent[] = [];
|
|
137
|
+
let sequence = 0;
|
|
138
|
+
for (const entry of transcript.entries) {
|
|
139
|
+
for (const part of entry.parts) {
|
|
140
|
+
events.push(...partToEvents(entry, part, sequence, finalTextPartIds));
|
|
141
|
+
sequence += 1;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return events;
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
export const getGrokThreadTranscriptStats = (events: ThreadEvent[]): ThreadTranscriptStats => {
|
|
148
|
+
return getThreadTranscriptStats(events);
|
|
149
|
+
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createServerFn } from '@tanstack/react-start';
|
|
2
2
|
import { z } from 'zod';
|
|
3
|
-
import { renderSourceSessionDownload } from './source-session-export-server';
|
|
3
|
+
import { renderSourceSessionDownload, renderSourceSessionsDownload } from './source-session-export-server';
|
|
4
4
|
|
|
5
5
|
const workspaceSchema = z.object({
|
|
6
6
|
workspaceKey: z.string().min(1),
|
|
@@ -19,6 +19,19 @@ const exportSessionSchema = z.object({
|
|
|
19
19
|
zipArchive: z.boolean().default(false),
|
|
20
20
|
});
|
|
21
21
|
|
|
22
|
+
const exportSessionsSchema = z.object({
|
|
23
|
+
includeCommentary: z.boolean().default(true),
|
|
24
|
+
includeMetadata: z.boolean().default(true),
|
|
25
|
+
includeTools: z.boolean().default(true),
|
|
26
|
+
outputFormat: z.enum(['md', 'txt']).default('md'),
|
|
27
|
+
sessionIds: z.array(z.string().min(1)).min(1),
|
|
28
|
+
zipArchive: z.boolean().default(true),
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const deleteSessionsSchema = z.object({
|
|
32
|
+
sessionIds: z.array(z.string().min(1)).min(1),
|
|
33
|
+
});
|
|
34
|
+
|
|
22
35
|
export const listKiroWorkspacesFn = createServerFn({ method: 'GET' }).handler(async () => {
|
|
23
36
|
const { listKiroWorkspaceGroups } = await import('@spiracha/lib/kiro-db');
|
|
24
37
|
return listKiroWorkspaceGroups();
|
|
@@ -32,13 +45,24 @@ export const listKiroSessionsFn = createServerFn({ method: 'GET' })
|
|
|
32
45
|
});
|
|
33
46
|
|
|
34
47
|
const loadKiroSessionTranscript = async (sessionId: string) => {
|
|
48
|
+
const { runWithTranscriptLoadLimit } = await import('@spiracha/lib/transcript-load-limiter');
|
|
35
49
|
const { readKiroSessionTranscript, resolveKiroWorkspaceSessionsDir } = await import('@spiracha/lib/kiro-db');
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
50
|
+
const sessionsDir = resolveKiroWorkspaceSessionsDir();
|
|
51
|
+
return runWithTranscriptLoadLimit(
|
|
52
|
+
async () => {
|
|
53
|
+
const transcript = await readKiroSessionTranscript(sessionsDir, sessionId);
|
|
54
|
+
if (!transcript) {
|
|
55
|
+
throw new Error(`Kiro session not found: ${sessionId}`);
|
|
56
|
+
}
|
|
40
57
|
|
|
41
|
-
|
|
58
|
+
return transcript;
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
id: sessionId,
|
|
62
|
+
path: sessionsDir,
|
|
63
|
+
source: 'kiro-ui',
|
|
64
|
+
},
|
|
65
|
+
);
|
|
42
66
|
};
|
|
43
67
|
|
|
44
68
|
export const getKiroSessionDetailFn = createServerFn({ method: 'GET' })
|
|
@@ -65,9 +89,63 @@ export const exportKiroSessionFn = createServerFn({ method: 'POST' })
|
|
|
65
89
|
|
|
66
90
|
return renderSourceSessionDownload({
|
|
67
91
|
content,
|
|
92
|
+
cwd: transcript.session.workspacePath ?? transcript.session.worktree,
|
|
68
93
|
fallbackBaseName: 'kiro-session',
|
|
69
|
-
fileBaseName: transcript.session.title || transcript.session.sessionId,
|
|
70
94
|
outputFormat: data.outputFormat,
|
|
95
|
+
sessionId: transcript.session.sessionId,
|
|
96
|
+
updatedAtMs: transcript.session.lastActiveAtMs,
|
|
71
97
|
zipArchive: data.zipArchive,
|
|
72
98
|
});
|
|
73
99
|
});
|
|
100
|
+
|
|
101
|
+
export const exportKiroSessionsFn = createServerFn({ method: 'POST' })
|
|
102
|
+
.validator(exportSessionsSchema)
|
|
103
|
+
.handler(async ({ data }) => {
|
|
104
|
+
const { renderKiroTranscript } = await import('@spiracha/lib/kiro-transcript');
|
|
105
|
+
const entries = await Promise.all(
|
|
106
|
+
data.sessionIds.map(async (sessionId) => {
|
|
107
|
+
const transcript = await loadKiroSessionTranscript(sessionId);
|
|
108
|
+
const content = renderKiroTranscript(transcript, {
|
|
109
|
+
includeCommentary: data.includeCommentary,
|
|
110
|
+
includeMetadata: data.includeMetadata,
|
|
111
|
+
includeTools: data.includeTools,
|
|
112
|
+
outputFormat: data.outputFormat,
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
if (!content) {
|
|
116
|
+
throw new Error(`Kiro session has no exportable content: ${sessionId}`);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
content,
|
|
121
|
+
cwd: transcript.session.workspacePath ?? transcript.session.worktree,
|
|
122
|
+
fallbackBaseName: 'kiro-session',
|
|
123
|
+
fileBaseName: transcript.session.title || transcript.session.sessionId,
|
|
124
|
+
sessionId: transcript.session.sessionId,
|
|
125
|
+
updatedAtMs: transcript.session.lastActiveAtMs,
|
|
126
|
+
};
|
|
127
|
+
}),
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
return renderSourceSessionsDownload({
|
|
131
|
+
entries,
|
|
132
|
+
fallbackBaseName: 'kiro-sessions',
|
|
133
|
+
outputFormat: data.outputFormat,
|
|
134
|
+
zipArchive: data.zipArchive,
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
export const deleteKiroSessionFn = createServerFn({ method: 'POST' })
|
|
139
|
+
.validator(sessionSchema)
|
|
140
|
+
.handler(async ({ data }) => {
|
|
141
|
+
const { deleteKiroSession, resolveKiroWorkspaceSessionsDir } = await import('@spiracha/lib/kiro-db');
|
|
142
|
+
return deleteKiroSession(resolveKiroWorkspaceSessionsDir(), data.sessionId);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
export const deleteKiroSessionsFn = createServerFn({ method: 'POST' })
|
|
146
|
+
.validator(deleteSessionsSchema)
|
|
147
|
+
.handler(async ({ data }) => {
|
|
148
|
+
const { deleteKiroSession, resolveKiroWorkspaceSessionsDir } = await import('@spiracha/lib/kiro-db');
|
|
149
|
+
const sessionsDir = resolveKiroWorkspaceSessionsDir();
|
|
150
|
+
return Promise.all(data.sessionIds.map((sessionId) => deleteKiroSession(sessionsDir, sessionId)));
|
|
151
|
+
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createServerFn } from '@tanstack/react-start';
|
|
2
2
|
import { z } from 'zod';
|
|
3
|
-
import { renderSourceSessionDownload } from './source-session-export-server';
|
|
3
|
+
import { renderSourceSessionDownload, renderSourceSessionsDownload } from './source-session-export-server';
|
|
4
4
|
|
|
5
5
|
const workspaceSchema = z.object({
|
|
6
6
|
workspaceKey: z.string().min(1),
|
|
@@ -19,6 +19,19 @@ const exportSessionSchema = z.object({
|
|
|
19
19
|
zipArchive: z.boolean().default(false),
|
|
20
20
|
});
|
|
21
21
|
|
|
22
|
+
const exportSessionsSchema = z.object({
|
|
23
|
+
includeCommentary: z.boolean().default(true),
|
|
24
|
+
includeMetadata: z.boolean().default(true),
|
|
25
|
+
includeTools: z.boolean().default(true),
|
|
26
|
+
outputFormat: z.enum(['md', 'txt']).default('md'),
|
|
27
|
+
sessionIds: z.array(z.string().min(1)).min(1),
|
|
28
|
+
zipArchive: z.boolean().default(true),
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const deleteSessionsSchema = z.object({
|
|
32
|
+
sessionIds: z.array(z.string().min(1)).min(1),
|
|
33
|
+
});
|
|
34
|
+
|
|
22
35
|
export const listOpenCodeWorkspacesFn = createServerFn({ method: 'GET' }).handler(async () => {
|
|
23
36
|
const { listOpenCodeWorkspaceGroups } = await import('@spiracha/lib/opencode-db');
|
|
24
37
|
return listOpenCodeWorkspaceGroups();
|
|
@@ -32,13 +45,24 @@ export const listOpenCodeSessionsFn = createServerFn({ method: 'GET' })
|
|
|
32
45
|
});
|
|
33
46
|
|
|
34
47
|
const loadOpenCodeSessionTranscript = async (sessionId: string) => {
|
|
48
|
+
const { runWithTranscriptLoadLimit } = await import('@spiracha/lib/transcript-load-limiter');
|
|
35
49
|
const { readOpenCodeSessionTranscript, resolveOpenCodeDbPath } = await import('@spiracha/lib/opencode-db');
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
50
|
+
const dbPath = resolveOpenCodeDbPath();
|
|
51
|
+
return runWithTranscriptLoadLimit(
|
|
52
|
+
async () => {
|
|
53
|
+
const transcript = await readOpenCodeSessionTranscript(dbPath, sessionId);
|
|
54
|
+
if (!transcript) {
|
|
55
|
+
throw new Error(`OpenCode session not found: ${sessionId}`);
|
|
56
|
+
}
|
|
40
57
|
|
|
41
|
-
|
|
58
|
+
return transcript;
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
id: sessionId,
|
|
62
|
+
path: dbPath,
|
|
63
|
+
source: 'opencode-ui',
|
|
64
|
+
},
|
|
65
|
+
);
|
|
42
66
|
};
|
|
43
67
|
|
|
44
68
|
export const getOpenCodeSessionDetailFn = createServerFn({ method: 'GET' })
|
|
@@ -65,9 +89,63 @@ export const exportOpenCodeSessionFn = createServerFn({ method: 'POST' })
|
|
|
65
89
|
|
|
66
90
|
return renderSourceSessionDownload({
|
|
67
91
|
content,
|
|
92
|
+
cwd: transcript.session.worktree,
|
|
68
93
|
fallbackBaseName: 'opencode-session',
|
|
69
|
-
fileBaseName: transcript.session.title || transcript.session.slug || transcript.session.sessionId,
|
|
70
94
|
outputFormat: data.outputFormat,
|
|
95
|
+
sessionId: transcript.session.sessionId,
|
|
96
|
+
updatedAtMs: transcript.session.lastUpdatedAtMs,
|
|
71
97
|
zipArchive: data.zipArchive,
|
|
72
98
|
});
|
|
73
99
|
});
|
|
100
|
+
|
|
101
|
+
export const exportOpenCodeSessionsFn = createServerFn({ method: 'POST' })
|
|
102
|
+
.validator(exportSessionsSchema)
|
|
103
|
+
.handler(async ({ data }) => {
|
|
104
|
+
const { renderOpenCodeTranscript } = await import('@spiracha/lib/opencode-transcript');
|
|
105
|
+
const entries = await Promise.all(
|
|
106
|
+
data.sessionIds.map(async (sessionId) => {
|
|
107
|
+
const transcript = await loadOpenCodeSessionTranscript(sessionId);
|
|
108
|
+
const content = renderOpenCodeTranscript(transcript, {
|
|
109
|
+
includeCommentary: data.includeCommentary,
|
|
110
|
+
includeMetadata: data.includeMetadata,
|
|
111
|
+
includeTools: data.includeTools,
|
|
112
|
+
outputFormat: data.outputFormat,
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
if (!content) {
|
|
116
|
+
throw new Error(`OpenCode session has no exportable content: ${sessionId}`);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
content,
|
|
121
|
+
cwd: transcript.session.worktree,
|
|
122
|
+
fallbackBaseName: 'opencode-session',
|
|
123
|
+
fileBaseName: transcript.session.title || transcript.session.slug || transcript.session.sessionId,
|
|
124
|
+
sessionId: transcript.session.sessionId,
|
|
125
|
+
updatedAtMs: transcript.session.lastUpdatedAtMs,
|
|
126
|
+
};
|
|
127
|
+
}),
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
return renderSourceSessionsDownload({
|
|
131
|
+
entries,
|
|
132
|
+
fallbackBaseName: 'opencode-sessions',
|
|
133
|
+
outputFormat: data.outputFormat,
|
|
134
|
+
zipArchive: data.zipArchive,
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
export const deleteOpenCodeSessionFn = createServerFn({ method: 'POST' })
|
|
139
|
+
.validator(sessionSchema)
|
|
140
|
+
.handler(async ({ data }) => {
|
|
141
|
+
const { deleteOpenCodeSession, resolveOpenCodeDbPath } = await import('@spiracha/lib/opencode-db');
|
|
142
|
+
return deleteOpenCodeSession(resolveOpenCodeDbPath(), data.sessionId);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
export const deleteOpenCodeSessionsFn = createServerFn({ method: 'POST' })
|
|
146
|
+
.validator(deleteSessionsSchema)
|
|
147
|
+
.handler(async ({ data }) => {
|
|
148
|
+
const { deleteOpenCodeSession, resolveOpenCodeDbPath } = await import('@spiracha/lib/opencode-db');
|
|
149
|
+
const dbPath = resolveOpenCodeDbPath();
|
|
150
|
+
return Promise.all(data.sessionIds.map((sessionId) => deleteOpenCodeSession(dbPath, sessionId)));
|
|
151
|
+
});
|