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
|
@@ -1,7 +1,14 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
DynamicToolDefinition,
|
|
3
|
+
MessageEvent,
|
|
4
|
+
ParsedCodexTranscript,
|
|
5
|
+
ThreadEvent,
|
|
6
|
+
} from '@spiracha/lib/codex-browser-types';
|
|
1
7
|
import { useMutation, useQuery, useQueryClient, useSuspenseQuery } from '@tanstack/react-query';
|
|
2
8
|
import { createFileRoute, Link, useNavigate } from '@tanstack/react-router';
|
|
3
|
-
import { Download, Trash2 } from 'lucide-react';
|
|
4
|
-
import {
|
|
9
|
+
import { ChevronDown, ChevronUp, Download, Search, Trash2 } from 'lucide-react';
|
|
10
|
+
import type { KeyboardEvent } from 'react';
|
|
11
|
+
import { startTransition, useEffect, useMemo, useState } from 'react';
|
|
5
12
|
import { Breadcrumbs } from '#/components/breadcrumbs';
|
|
6
13
|
import { DeleteConfirmDialog } from '#/components/delete-confirm-dialog';
|
|
7
14
|
import { ExportDialog } from '#/components/export-dialog';
|
|
@@ -10,18 +17,162 @@ import { LoadingPanel } from '#/components/loading-panel';
|
|
|
10
17
|
import { MetadataSection } from '#/components/metadata-section';
|
|
11
18
|
import { MetricCard } from '#/components/metric-card';
|
|
12
19
|
import { PageHeader } from '#/components/page-header';
|
|
13
|
-
import {
|
|
20
|
+
import {
|
|
21
|
+
getTranscriptEventKey,
|
|
22
|
+
shouldShowEvent,
|
|
23
|
+
type TranscriptSortOrder,
|
|
24
|
+
TranscriptView,
|
|
25
|
+
} from '#/components/transcript-view';
|
|
14
26
|
import { Badge } from '#/components/ui/badge';
|
|
15
27
|
import { Button } from '#/components/ui/button';
|
|
16
28
|
import { Checkbox } from '#/components/ui/checkbox';
|
|
29
|
+
import { Input } from '#/components/ui/input';
|
|
17
30
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '#/components/ui/tabs';
|
|
18
|
-
import {
|
|
31
|
+
import {
|
|
32
|
+
threadSnapshotQueryOptions,
|
|
33
|
+
threadTranscriptPreviewQueryOptions,
|
|
34
|
+
threadTranscriptQueryOptions,
|
|
35
|
+
} from '#/lib/codex-queries';
|
|
19
36
|
import { deleteThreadFn, exportThreadFn, type getThreadSnapshotFn } from '#/lib/codex-server';
|
|
20
37
|
import { downloadTextFile, downloadUrlFile } from '#/lib/download';
|
|
21
|
-
import {
|
|
38
|
+
import {
|
|
39
|
+
formatBooleanLabel,
|
|
40
|
+
formatBytes,
|
|
41
|
+
formatDateTime,
|
|
42
|
+
formatList,
|
|
43
|
+
formatModelLabel,
|
|
44
|
+
formatTokens,
|
|
45
|
+
} from '#/lib/formatters';
|
|
46
|
+
import { applyPathTransforms } from '#/lib/path-utils';
|
|
47
|
+
import {
|
|
48
|
+
parseThreadTranscriptSearch,
|
|
49
|
+
type ThreadTranscriptSearch,
|
|
50
|
+
withThreadTranscriptSearch,
|
|
51
|
+
} from '#/lib/route-search';
|
|
22
52
|
import { useSettings } from '#/lib/settings-store';
|
|
53
|
+
import { shouldLoadFullThreadTranscript } from '#/lib/thread-transcript-load';
|
|
23
54
|
|
|
24
|
-
type
|
|
55
|
+
type ThreadSnapshotResponse = Awaited<ReturnType<typeof getThreadSnapshotFn>>;
|
|
56
|
+
type ThreadTranscript = ParsedCodexTranscript;
|
|
57
|
+
type ThreadSnapshot = Omit<ThreadSnapshotResponse, 'availableTools' | 'transcript'> & {
|
|
58
|
+
availableTools: DynamicToolDefinition[];
|
|
59
|
+
transcript: ThreadTranscript | null;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
type TranscriptSearchResult = {
|
|
63
|
+
event: MessageEvent;
|
|
64
|
+
eventIndex: number;
|
|
65
|
+
eventKey: string;
|
|
66
|
+
messageNumber: number;
|
|
67
|
+
phase: string | null;
|
|
68
|
+
roleLabel: string;
|
|
69
|
+
snippet: string;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
type TranscriptSearchPanelProps = {
|
|
73
|
+
activeResultIndex: number;
|
|
74
|
+
query: string;
|
|
75
|
+
results: TranscriptSearchResult[];
|
|
76
|
+
onJumpToResult: (index: number) => void;
|
|
77
|
+
onQueryChange: (value: string) => void;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
type TranscriptSearchFilters = {
|
|
81
|
+
showCommentary: boolean;
|
|
82
|
+
showExtraEvents: boolean;
|
|
83
|
+
showToolCalls: boolean;
|
|
84
|
+
showUserMessages: boolean;
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const SEARCH_SNIPPET_RADIUS = 72;
|
|
88
|
+
|
|
89
|
+
const getTranscriptFiltersFromSearch = (search: ThreadTranscriptSearch) => ({
|
|
90
|
+
showCommentary: search.commentary === true,
|
|
91
|
+
showExtraEvents: search.extra === true,
|
|
92
|
+
showToolCalls: search.tools === true,
|
|
93
|
+
showUserMessages: search.user === true,
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
type ThreadTranscriptFilters = ReturnType<typeof getTranscriptFiltersFromSearch>;
|
|
97
|
+
|
|
98
|
+
const normalizeTranscriptSearchText = (value: string) => value.replace(/\s+/gu, ' ').trim();
|
|
99
|
+
|
|
100
|
+
const getTranscriptSearchRoleLabel = (event: MessageEvent, assistantModel: string | null) => {
|
|
101
|
+
if (event.role === 'assistant') {
|
|
102
|
+
return formatModelLabel(event.model ?? assistantModel);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return event.role === 'system' ? 'System' : 'User';
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const buildTranscriptSearchSnippet = (text: string, query: string) => {
|
|
109
|
+
const normalizedText = normalizeTranscriptSearchText(text);
|
|
110
|
+
const normalizedQuery = normalizeTranscriptSearchText(query).toLowerCase();
|
|
111
|
+
const matchIndex = normalizedText.toLowerCase().indexOf(normalizedQuery);
|
|
112
|
+
|
|
113
|
+
if (matchIndex < 0) {
|
|
114
|
+
return normalizedText.slice(0, SEARCH_SNIPPET_RADIUS * 2);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const start = Math.max(0, matchIndex - SEARCH_SNIPPET_RADIUS);
|
|
118
|
+
const end = Math.min(normalizedText.length, matchIndex + normalizedQuery.length + SEARCH_SNIPPET_RADIUS);
|
|
119
|
+
const prefix = start > 0 ? '...' : '';
|
|
120
|
+
const suffix = end < normalizedText.length ? '...' : '';
|
|
121
|
+
|
|
122
|
+
return `${prefix}${normalizedText.slice(start, end)}${suffix}`;
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
const buildTranscriptSearchResults = (
|
|
126
|
+
events: ThreadEvent[],
|
|
127
|
+
query: string,
|
|
128
|
+
assistantModel: string | null,
|
|
129
|
+
filters: TranscriptSearchFilters,
|
|
130
|
+
transform: (text: string) => string,
|
|
131
|
+
): TranscriptSearchResult[] => {
|
|
132
|
+
const normalizedQuery = normalizeTranscriptSearchText(query).toLowerCase();
|
|
133
|
+
if (!normalizedQuery) {
|
|
134
|
+
return [];
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const results: TranscriptSearchResult[] = [];
|
|
138
|
+
let messageNumber = 0;
|
|
139
|
+
|
|
140
|
+
events.forEach((event, index) => {
|
|
141
|
+
if (event.kind !== 'message') {
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (
|
|
146
|
+
!shouldShowEvent(
|
|
147
|
+
event,
|
|
148
|
+
filters.showToolCalls,
|
|
149
|
+
filters.showExtraEvents,
|
|
150
|
+
filters.showCommentary,
|
|
151
|
+
filters.showUserMessages,
|
|
152
|
+
)
|
|
153
|
+
) {
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
messageNumber += 1;
|
|
158
|
+
const searchText = normalizeTranscriptSearchText(transform(event.text));
|
|
159
|
+
if (!searchText.toLowerCase().includes(normalizedQuery)) {
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
results.push({
|
|
164
|
+
event,
|
|
165
|
+
eventIndex: index,
|
|
166
|
+
eventKey: getTranscriptEventKey(event, index),
|
|
167
|
+
messageNumber,
|
|
168
|
+
phase: event.phase,
|
|
169
|
+
roleLabel: getTranscriptSearchRoleLabel(event, assistantModel),
|
|
170
|
+
snippet: buildTranscriptSearchSnippet(searchText, query),
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
return results;
|
|
175
|
+
};
|
|
25
176
|
|
|
26
177
|
type TranscriptControlsProps = {
|
|
27
178
|
rawJsonDisabled?: boolean;
|
|
@@ -162,7 +313,7 @@ const buildTranscriptStatsItems = (snapshot: ThreadSnapshot) => {
|
|
|
162
313
|
return [
|
|
163
314
|
{ label: 'Transcript load', value: 'Deferred for oversized rollout' },
|
|
164
315
|
{ label: 'Rollout size', value: formatBytes(snapshot.rollout.fileSizeBytes) },
|
|
165
|
-
{ label: '
|
|
316
|
+
{ label: 'Deferred load', value: 'Load the transcript manually to inspect it.' },
|
|
166
317
|
];
|
|
167
318
|
}
|
|
168
319
|
|
|
@@ -240,6 +391,98 @@ function TranscriptControls({
|
|
|
240
391
|
);
|
|
241
392
|
}
|
|
242
393
|
|
|
394
|
+
function TranscriptSearchPanel({
|
|
395
|
+
activeResultIndex,
|
|
396
|
+
query,
|
|
397
|
+
results,
|
|
398
|
+
onJumpToResult,
|
|
399
|
+
onQueryChange,
|
|
400
|
+
}: TranscriptSearchPanelProps) {
|
|
401
|
+
const hasQuery = normalizeTranscriptSearchText(query).length > 0;
|
|
402
|
+
const hasResults = results.length > 0;
|
|
403
|
+
const statusLabel = hasQuery
|
|
404
|
+
? hasResults
|
|
405
|
+
? `${activeResultIndex + 1} / ${results.length}`
|
|
406
|
+
: 'No matches'
|
|
407
|
+
: 'Search';
|
|
408
|
+
|
|
409
|
+
const handleKeyDown = (event: KeyboardEvent<HTMLInputElement>) => {
|
|
410
|
+
if (event.key !== 'Enter' || !hasResults) {
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
event.preventDefault();
|
|
415
|
+
onJumpToResult(event.shiftKey ? activeResultIndex - 1 : activeResultIndex);
|
|
416
|
+
};
|
|
417
|
+
|
|
418
|
+
return (
|
|
419
|
+
<section className="rounded-xl border border-[var(--border)] bg-[var(--panel)] px-4 py-3 shadow-[var(--panel-shadow)]">
|
|
420
|
+
<div className="flex flex-col gap-3 lg:flex-row lg:items-center">
|
|
421
|
+
<div className="relative min-w-0 flex-1">
|
|
422
|
+
<Search className="pointer-events-none absolute top-1/2 left-3 size-4 -translate-y-1/2 text-[var(--muted-foreground)]" />
|
|
423
|
+
<Input
|
|
424
|
+
aria-label="Search transcript messages"
|
|
425
|
+
className="h-10 rounded-full border-[var(--border)] bg-[var(--panel-secondary)] pr-4 pl-9"
|
|
426
|
+
placeholder="Search transcript messages"
|
|
427
|
+
value={query}
|
|
428
|
+
onChange={(event) => onQueryChange(event.target.value)}
|
|
429
|
+
onKeyDown={handleKeyDown}
|
|
430
|
+
/>
|
|
431
|
+
</div>
|
|
432
|
+
|
|
433
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
434
|
+
<span className="min-w-20 text-right text-[var(--muted-foreground)] text-sm">{statusLabel}</span>
|
|
435
|
+
<Button
|
|
436
|
+
className="rounded-full"
|
|
437
|
+
disabled={!hasResults}
|
|
438
|
+
size="sm"
|
|
439
|
+
type="button"
|
|
440
|
+
variant="outline"
|
|
441
|
+
onClick={() => onJumpToResult(activeResultIndex - 1)}
|
|
442
|
+
>
|
|
443
|
+
<ChevronUp className="size-4" />
|
|
444
|
+
Prev
|
|
445
|
+
</Button>
|
|
446
|
+
<Button
|
|
447
|
+
className="rounded-full"
|
|
448
|
+
disabled={!hasResults}
|
|
449
|
+
size="sm"
|
|
450
|
+
type="button"
|
|
451
|
+
variant="outline"
|
|
452
|
+
onClick={() => onJumpToResult(activeResultIndex + 1)}
|
|
453
|
+
>
|
|
454
|
+
<ChevronDown className="size-4" />
|
|
455
|
+
Next
|
|
456
|
+
</Button>
|
|
457
|
+
</div>
|
|
458
|
+
</div>
|
|
459
|
+
|
|
460
|
+
{hasQuery && hasResults ? (
|
|
461
|
+
<div className="mt-3 max-h-72 overflow-auto rounded-xl border border-[var(--border)] bg-[var(--panel-secondary)]">
|
|
462
|
+
{results.map((result, index) => (
|
|
463
|
+
<button
|
|
464
|
+
key={result.eventKey}
|
|
465
|
+
aria-current={index === activeResultIndex ? 'true' : undefined}
|
|
466
|
+
className="block w-full border-[var(--border)] border-b px-3 py-2.5 text-left transition last:border-b-0 hover:bg-[var(--panel)] aria-current:bg-[var(--panel)]"
|
|
467
|
+
type="button"
|
|
468
|
+
onClick={() => onJumpToResult(index)}
|
|
469
|
+
>
|
|
470
|
+
<span className="flex flex-wrap items-center gap-2">
|
|
471
|
+
<span className="font-medium text-sm">Message {result.messageNumber}</span>
|
|
472
|
+
<Badge variant="outline">{result.roleLabel}</Badge>
|
|
473
|
+
{result.phase ? <Badge variant="outline">{result.phase}</Badge> : null}
|
|
474
|
+
</span>
|
|
475
|
+
<span className="mt-1 block min-w-0 break-words text-[var(--muted-foreground)] text-sm leading-5 [overflow-wrap:anywhere]">
|
|
476
|
+
{result.snippet}
|
|
477
|
+
</span>
|
|
478
|
+
</button>
|
|
479
|
+
))}
|
|
480
|
+
</div>
|
|
481
|
+
) : null}
|
|
482
|
+
</section>
|
|
483
|
+
);
|
|
484
|
+
}
|
|
485
|
+
|
|
243
486
|
function ThreadMetadataPanels({ snapshot }: ThreadMetadataProps) {
|
|
244
487
|
return (
|
|
245
488
|
<div className="grid gap-4 xl:grid-cols-[1.1fr_0.9fr]">
|
|
@@ -319,12 +562,12 @@ function DeferredTranscriptNotice({
|
|
|
319
562
|
<p className="mt-2 text-[var(--muted-foreground)] text-sm leading-6">
|
|
320
563
|
{missing
|
|
321
564
|
? 'The rollout JSONL referenced by this thread is no longer present on disk. Export may still work if the file is restored, but transcript browsing is unavailable right now.'
|
|
322
|
-
: `Spiracha skipped loading the transcript automatically because the rollout file is ${formatBytes(fileSizeBytes)}. Export still works immediately.
|
|
565
|
+
: `Spiracha skipped loading the transcript automatically because the rollout file is ${formatBytes(fileSizeBytes)}. Export still works immediately. Load the full transcript when you need to inspect it here.`}
|
|
323
566
|
</p>
|
|
324
567
|
{missing ? null : (
|
|
325
568
|
<div className="mt-4">
|
|
326
569
|
<Button disabled={pending} variant="outline" onClick={onLoad}>
|
|
327
|
-
{pending ? 'Loading
|
|
570
|
+
{pending ? 'Loading full thread...' : 'Load Full Thread'}
|
|
328
571
|
</Button>
|
|
329
572
|
</div>
|
|
330
573
|
)}
|
|
@@ -332,6 +575,39 @@ function DeferredTranscriptNotice({
|
|
|
332
575
|
);
|
|
333
576
|
}
|
|
334
577
|
|
|
578
|
+
function LargeThreadPreviewNotice({
|
|
579
|
+
fileSizeBytes,
|
|
580
|
+
fullTranscriptLoaded,
|
|
581
|
+
pending,
|
|
582
|
+
onLoad,
|
|
583
|
+
}: {
|
|
584
|
+
fileSizeBytes: number | null;
|
|
585
|
+
fullTranscriptLoaded: boolean;
|
|
586
|
+
pending: boolean;
|
|
587
|
+
onLoad: () => void;
|
|
588
|
+
}) {
|
|
589
|
+
const buttonLabel = fullTranscriptLoaded
|
|
590
|
+
? 'Full thread loaded'
|
|
591
|
+
: pending
|
|
592
|
+
? 'Loading full thread...'
|
|
593
|
+
: 'Load Full Thread';
|
|
594
|
+
|
|
595
|
+
return (
|
|
596
|
+
<section className="rounded-[1.6rem] border border-[var(--border)] bg-[var(--panel)] p-5 shadow-[var(--panel-shadow)]">
|
|
597
|
+
<h3 className="font-semibold text-base">Showing the latest matching messages</h3>
|
|
598
|
+
<p className="mt-2 text-[var(--muted-foreground)] text-sm leading-6">
|
|
599
|
+
This rollout is {formatBytes(fileSizeBytes)}, so Spiracha loaded a small window from the end of the
|
|
600
|
+
thread using the current transcript filters. Load the full thread when you need earlier messages.
|
|
601
|
+
</p>
|
|
602
|
+
<div className="mt-4">
|
|
603
|
+
<Button disabled={pending || fullTranscriptLoaded} variant="outline" onClick={onLoad}>
|
|
604
|
+
{buttonLabel}
|
|
605
|
+
</Button>
|
|
606
|
+
</div>
|
|
607
|
+
</section>
|
|
608
|
+
);
|
|
609
|
+
}
|
|
610
|
+
|
|
335
611
|
function ThreadErrorComponent({ error }: { error: Error }) {
|
|
336
612
|
const isSqlite = error.message.includes('unable to open database') || error.message.includes('database is locked');
|
|
337
613
|
return (
|
|
@@ -364,38 +640,263 @@ const getThreadExportErrorMessage = (transcriptMissing: boolean, error: unknown)
|
|
|
364
640
|
export const Route = createFileRoute('/threads/$threadId')({
|
|
365
641
|
component: ThreadDetailPage,
|
|
366
642
|
errorComponent: ThreadErrorComponent,
|
|
367
|
-
loader: ({ context, params }) =>
|
|
643
|
+
loader: ({ context, deps, params }) =>
|
|
644
|
+
context.queryClient.ensureQueryData(
|
|
645
|
+
threadSnapshotQueryOptions(
|
|
646
|
+
params.threadId,
|
|
647
|
+
(deps as { transcriptFilters: ThreadTranscriptFilters }).transcriptFilters,
|
|
648
|
+
),
|
|
649
|
+
),
|
|
650
|
+
loaderDeps: ({ search }) => ({
|
|
651
|
+
transcriptFilters: getTranscriptFiltersFromSearch(parseThreadTranscriptSearch(search)),
|
|
652
|
+
}),
|
|
368
653
|
pendingComponent: () => (
|
|
369
654
|
<LoadingPanel
|
|
370
655
|
description="Loading the transcript, metadata, and parsed event stream for this thread."
|
|
371
656
|
title="Loading thread"
|
|
372
657
|
/>
|
|
373
658
|
),
|
|
659
|
+
validateSearch: parseThreadTranscriptSearch,
|
|
374
660
|
});
|
|
375
661
|
|
|
662
|
+
const useThreadTranscriptRouteSearch = () => {
|
|
663
|
+
const navigate = useNavigate({ from: Route.fullPath });
|
|
664
|
+
const search = Route.useSearch();
|
|
665
|
+
const transcriptFilters = useMemo(() => getTranscriptFiltersFromSearch(search), [search]);
|
|
666
|
+
|
|
667
|
+
const updateTranscriptSearch = (patch: Partial<ThreadTranscriptSearch>) => {
|
|
668
|
+
startTransition(() => {
|
|
669
|
+
void navigate({
|
|
670
|
+
params: true,
|
|
671
|
+
replace: true,
|
|
672
|
+
search: (previous: Record<string, unknown>) => withThreadTranscriptSearch(previous, patch),
|
|
673
|
+
});
|
|
674
|
+
});
|
|
675
|
+
};
|
|
676
|
+
|
|
677
|
+
return {
|
|
678
|
+
navigate,
|
|
679
|
+
search,
|
|
680
|
+
transcriptFilters,
|
|
681
|
+
updateTranscriptSearch,
|
|
682
|
+
};
|
|
683
|
+
};
|
|
684
|
+
|
|
685
|
+
const useTranscriptSearchResults = ({
|
|
686
|
+
assistantModel,
|
|
687
|
+
filters,
|
|
688
|
+
projectPath,
|
|
689
|
+
query,
|
|
690
|
+
settings,
|
|
691
|
+
transcript,
|
|
692
|
+
}: {
|
|
693
|
+
assistantModel: string | null;
|
|
694
|
+
filters: TranscriptSearchFilters;
|
|
695
|
+
projectPath: string;
|
|
696
|
+
query: string;
|
|
697
|
+
settings: ReturnType<typeof useSettings>['settings'];
|
|
698
|
+
transcript: ThreadTranscript | null;
|
|
699
|
+
}) =>
|
|
700
|
+
useMemo(
|
|
701
|
+
() =>
|
|
702
|
+
transcript
|
|
703
|
+
? buildTranscriptSearchResults(transcript.events, query, assistantModel, filters, (text) =>
|
|
704
|
+
applyPathTransforms(text, settings, projectPath),
|
|
705
|
+
)
|
|
706
|
+
: [],
|
|
707
|
+
[assistantModel, filters, projectPath, query, settings, transcript],
|
|
708
|
+
);
|
|
709
|
+
|
|
710
|
+
function ThreadTranscriptTab({
|
|
711
|
+
activeEventJumpSignal,
|
|
712
|
+
activeSearchResultIndex,
|
|
713
|
+
activeTranscriptEventKey,
|
|
714
|
+
fullTranscriptLoaded,
|
|
715
|
+
loadError,
|
|
716
|
+
loadingFullTranscript,
|
|
717
|
+
searchInput,
|
|
718
|
+
searchResults,
|
|
719
|
+
showRawJson,
|
|
720
|
+
sortOrder,
|
|
721
|
+
snapshot,
|
|
722
|
+
transcript,
|
|
723
|
+
transcriptFilters,
|
|
724
|
+
onJumpToSearchResult,
|
|
725
|
+
onLoadFullThread,
|
|
726
|
+
onSearchInputChange,
|
|
727
|
+
onSortOrderChange,
|
|
728
|
+
onTranscriptFilterChange,
|
|
729
|
+
}: {
|
|
730
|
+
activeEventJumpSignal: number;
|
|
731
|
+
activeSearchResultIndex: number;
|
|
732
|
+
activeTranscriptEventKey: string | null;
|
|
733
|
+
fullTranscriptLoaded: boolean;
|
|
734
|
+
loadError: unknown;
|
|
735
|
+
loadingFullTranscript: boolean;
|
|
736
|
+
searchInput: string;
|
|
737
|
+
searchResults: TranscriptSearchResult[];
|
|
738
|
+
showRawJson: boolean;
|
|
739
|
+
sortOrder: TranscriptSortOrder;
|
|
740
|
+
snapshot: ThreadSnapshot;
|
|
741
|
+
transcript: ThreadTranscript | null;
|
|
742
|
+
transcriptFilters: ThreadTranscriptFilters;
|
|
743
|
+
onJumpToSearchResult: (index: number) => void;
|
|
744
|
+
onLoadFullThread: () => void;
|
|
745
|
+
onSearchInputChange: (value: string) => void;
|
|
746
|
+
onSortOrderChange: (value: TranscriptSortOrder) => void;
|
|
747
|
+
onTranscriptFilterChange: (patch: Partial<ThreadTranscriptSearch>) => void;
|
|
748
|
+
}) {
|
|
749
|
+
return (
|
|
750
|
+
<TabsContent className="space-y-3" value="transcript">
|
|
751
|
+
<TranscriptControls
|
|
752
|
+
rawJsonDisabled={!transcript?.rawIncluded}
|
|
753
|
+
showCommentary={transcriptFilters.showCommentary}
|
|
754
|
+
showExtraEvents={transcriptFilters.showExtraEvents}
|
|
755
|
+
showRawJson={showRawJson}
|
|
756
|
+
showToolCalls={transcriptFilters.showToolCalls}
|
|
757
|
+
showUserMessages={transcriptFilters.showUserMessages}
|
|
758
|
+
onShowCommentaryChange={(commentary) => onTranscriptFilterChange({ commentary })}
|
|
759
|
+
onShowExtraEventsChange={(extra) => onTranscriptFilterChange({ extra })}
|
|
760
|
+
onShowRawJsonChange={(raw) => onTranscriptFilterChange({ raw })}
|
|
761
|
+
onShowToolCallsChange={(tools) => onTranscriptFilterChange({ tools })}
|
|
762
|
+
onShowUserMessagesChange={(user) => onTranscriptFilterChange({ user })}
|
|
763
|
+
/>
|
|
764
|
+
{transcript?.isPartial && snapshot.rollout.shouldDeferTranscriptLoad ? (
|
|
765
|
+
<LargeThreadPreviewNotice
|
|
766
|
+
fileSizeBytes={snapshot.rollout.fileSizeBytes}
|
|
767
|
+
fullTranscriptLoaded={fullTranscriptLoaded}
|
|
768
|
+
pending={loadingFullTranscript}
|
|
769
|
+
onLoad={onLoadFullThread}
|
|
770
|
+
/>
|
|
771
|
+
) : null}
|
|
772
|
+
{transcript ? (
|
|
773
|
+
<TranscriptSearchPanel
|
|
774
|
+
activeResultIndex={activeSearchResultIndex}
|
|
775
|
+
query={searchInput}
|
|
776
|
+
results={searchResults}
|
|
777
|
+
onJumpToResult={onJumpToSearchResult}
|
|
778
|
+
onQueryChange={onSearchInputChange}
|
|
779
|
+
/>
|
|
780
|
+
) : null}
|
|
781
|
+
{transcript ? (
|
|
782
|
+
<TranscriptView
|
|
783
|
+
activeEventJumpSignal={activeEventJumpSignal}
|
|
784
|
+
activeEventKey={activeTranscriptEventKey}
|
|
785
|
+
assistantModel={snapshot.thread.model}
|
|
786
|
+
events={transcript.events}
|
|
787
|
+
projectPath={snapshot.thread.cwd}
|
|
788
|
+
showCommentary={transcriptFilters.showCommentary}
|
|
789
|
+
showExtraEvents={transcriptFilters.showExtraEvents}
|
|
790
|
+
showRawJson={showRawJson && transcript.rawIncluded}
|
|
791
|
+
showToolCalls={transcriptFilters.showToolCalls}
|
|
792
|
+
showUserMessages={transcriptFilters.showUserMessages}
|
|
793
|
+
sortOrder={sortOrder}
|
|
794
|
+
onSortOrderChange={onSortOrderChange}
|
|
795
|
+
/>
|
|
796
|
+
) : (
|
|
797
|
+
<DeferredTranscriptNotice
|
|
798
|
+
fileSizeBytes={snapshot.rollout.fileSizeBytes}
|
|
799
|
+
missing={snapshot.transcriptState === 'missing'}
|
|
800
|
+
pending={loadingFullTranscript}
|
|
801
|
+
onLoad={onLoadFullThread}
|
|
802
|
+
/>
|
|
803
|
+
)}
|
|
804
|
+
{loadError ? (
|
|
805
|
+
<p className="text-[var(--destructive)] text-sm">
|
|
806
|
+
Failed to load transcript preview:{' '}
|
|
807
|
+
{loadError instanceof Error ? loadError.message : 'Unknown error'}
|
|
808
|
+
</p>
|
|
809
|
+
) : null}
|
|
810
|
+
</TabsContent>
|
|
811
|
+
);
|
|
812
|
+
}
|
|
813
|
+
|
|
376
814
|
function ThreadDetailPage() {
|
|
377
|
-
const navigate =
|
|
815
|
+
const { navigate, search, transcriptFilters, updateTranscriptSearch } = useThreadTranscriptRouteSearch();
|
|
378
816
|
const queryClient = useQueryClient();
|
|
379
817
|
const params = Route.useParams();
|
|
380
|
-
const snapshot = useSuspenseQuery(threadSnapshotQueryOptions(params.threadId)).data;
|
|
818
|
+
const snapshot = useSuspenseQuery(threadSnapshotQueryOptions(params.threadId, transcriptFilters)).data;
|
|
381
819
|
const { settings } = useSettings();
|
|
382
820
|
const transcriptMissing = snapshot.transcriptState === 'missing';
|
|
383
821
|
const [shouldLoadTranscript, setShouldLoadTranscript] = useState(
|
|
384
822
|
!snapshot.rollout.shouldDeferTranscriptLoad && !transcriptMissing,
|
|
385
823
|
);
|
|
386
|
-
const
|
|
387
|
-
const
|
|
388
|
-
const
|
|
389
|
-
const [
|
|
390
|
-
const [
|
|
824
|
+
const showRawJson = search.raw === true;
|
|
825
|
+
const sortOrder: TranscriptSortOrder = search.sort ?? 'earliest';
|
|
826
|
+
const transcriptSearchInput = search.q ?? '';
|
|
827
|
+
const [activeSearchResultIndex, setActiveSearchResultIndex] = useState(0);
|
|
828
|
+
const [activeTranscriptEventKey, setActiveTranscriptEventKey] = useState<string | null>(null);
|
|
829
|
+
const [activeEventJumpSignal, setActiveEventJumpSignal] = useState(0);
|
|
391
830
|
const [exportOpen, setExportOpen] = useState(false);
|
|
392
831
|
const [deleteOpen, setDeleteOpen] = useState(false);
|
|
832
|
+
const transcriptPreviewQuery = useQuery({
|
|
833
|
+
...threadTranscriptPreviewQueryOptions(params.threadId, transcriptFilters),
|
|
834
|
+
enabled: snapshot.rollout.shouldDeferTranscriptLoad && !shouldLoadTranscript && !transcriptMissing,
|
|
835
|
+
});
|
|
393
836
|
const transcriptQuery = useQuery({
|
|
394
837
|
...threadTranscriptQueryOptions(params.threadId),
|
|
395
|
-
enabled:
|
|
838
|
+
enabled: shouldLoadFullThreadTranscript({
|
|
839
|
+
shouldLoadTranscript,
|
|
840
|
+
snapshotTranscript: snapshot.transcript,
|
|
841
|
+
transcriptMissing,
|
|
842
|
+
}),
|
|
843
|
+
});
|
|
844
|
+
const transcript = transcriptQuery.data ?? transcriptPreviewQuery.data ?? snapshot.transcript ?? null;
|
|
845
|
+
const viewSnapshot = {
|
|
846
|
+
...snapshot,
|
|
847
|
+
availableTools:
|
|
848
|
+
snapshot.availableTools.length > 0 ? snapshot.availableTools : (transcript?.sessionMeta.dynamicTools ?? []),
|
|
849
|
+
transcript,
|
|
850
|
+
};
|
|
851
|
+
const fullTranscriptLoaded = Boolean(transcriptQuery.data && !transcriptQuery.data.isPartial);
|
|
852
|
+
const transcriptSearchResults = useTranscriptSearchResults({
|
|
853
|
+
assistantModel: snapshot.thread.model,
|
|
854
|
+
filters: transcriptFilters,
|
|
855
|
+
projectPath: snapshot.thread.cwd,
|
|
856
|
+
query: transcriptSearchInput,
|
|
857
|
+
settings,
|
|
858
|
+
transcript,
|
|
396
859
|
});
|
|
397
|
-
|
|
398
|
-
|
|
860
|
+
|
|
861
|
+
useEffect(() => {
|
|
862
|
+
setActiveSearchResultIndex((current) =>
|
|
863
|
+
transcriptSearchResults.length === 0 ? 0 : Math.min(current, transcriptSearchResults.length - 1),
|
|
864
|
+
);
|
|
865
|
+
setActiveTranscriptEventKey((current) =>
|
|
866
|
+
current && transcriptSearchResults.some((result) => result.eventKey === current) ? current : null,
|
|
867
|
+
);
|
|
868
|
+
}, [transcriptSearchResults]);
|
|
869
|
+
|
|
870
|
+
const updateTranscriptSearchInput = (value: string) => {
|
|
871
|
+
setActiveSearchResultIndex(0);
|
|
872
|
+
setActiveTranscriptEventKey(null);
|
|
873
|
+
updateTranscriptSearch({ q: value });
|
|
874
|
+
};
|
|
875
|
+
|
|
876
|
+
const updateTranscriptFilter = (patch: Partial<ThreadTranscriptSearch>) => {
|
|
877
|
+
setActiveSearchResultIndex(0);
|
|
878
|
+
setActiveTranscriptEventKey(null);
|
|
879
|
+
updateTranscriptSearch(patch);
|
|
880
|
+
};
|
|
881
|
+
|
|
882
|
+
const updateSortOrder = (sort: TranscriptSortOrder) => {
|
|
883
|
+
updateTranscriptSearch({ sort });
|
|
884
|
+
};
|
|
885
|
+
|
|
886
|
+
const jumpToTranscriptSearchResult = (index: number) => {
|
|
887
|
+
if (transcriptSearchResults.length === 0) {
|
|
888
|
+
return;
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
const wrappedIndex =
|
|
892
|
+
((index % transcriptSearchResults.length) + transcriptSearchResults.length) %
|
|
893
|
+
transcriptSearchResults.length;
|
|
894
|
+
const result = transcriptSearchResults[wrappedIndex]!;
|
|
895
|
+
|
|
896
|
+
setActiveSearchResultIndex(wrappedIndex);
|
|
897
|
+
setActiveTranscriptEventKey(result.eventKey);
|
|
898
|
+
setActiveEventJumpSignal((current) => current + 1);
|
|
899
|
+
};
|
|
399
900
|
|
|
400
901
|
const exportThreadMutation = useMutation({
|
|
401
902
|
mutationFn: async (options: {
|
|
@@ -502,13 +1003,10 @@ function ThreadDetailPage() {
|
|
|
502
1003
|
params: { project: snapshot.project },
|
|
503
1004
|
to: '/codex/$project',
|
|
504
1005
|
},
|
|
505
|
-
{ label: snapshot.thread.title },
|
|
1006
|
+
{ label: snapshot.thread.title, title: snapshot.thread.title, truncate: true },
|
|
506
1007
|
]}
|
|
507
1008
|
/>
|
|
508
1009
|
}
|
|
509
|
-
eyebrow={snapshot.project}
|
|
510
|
-
subtitle={snapshot.thread.preview}
|
|
511
|
-
title={snapshot.thread.title}
|
|
512
1010
|
/>
|
|
513
1011
|
|
|
514
1012
|
<div className="grid gap-3 md:grid-cols-2 xl:grid-cols-4">
|
|
@@ -541,46 +1039,32 @@ function ThreadDetailPage() {
|
|
|
541
1039
|
</TabsTrigger>
|
|
542
1040
|
</TabsList>
|
|
543
1041
|
|
|
544
|
-
<
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
{
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
<DeferredTranscriptNotice
|
|
571
|
-
fileSizeBytes={snapshot.rollout.fileSizeBytes}
|
|
572
|
-
missing={snapshot.transcriptState === 'missing'}
|
|
573
|
-
pending={transcriptQuery.isFetching}
|
|
574
|
-
onLoad={() => setShouldLoadTranscript(true)}
|
|
575
|
-
/>
|
|
576
|
-
)}
|
|
577
|
-
{transcriptQuery.isError ? (
|
|
578
|
-
<p className="text-[var(--destructive)] text-sm">
|
|
579
|
-
Failed to load transcript preview:{' '}
|
|
580
|
-
{transcriptQuery.error instanceof Error ? transcriptQuery.error.message : 'Unknown error'}
|
|
581
|
-
</p>
|
|
582
|
-
) : null}
|
|
583
|
-
</TabsContent>
|
|
1042
|
+
<ThreadTranscriptTab
|
|
1043
|
+
activeEventJumpSignal={activeEventJumpSignal}
|
|
1044
|
+
activeSearchResultIndex={activeSearchResultIndex}
|
|
1045
|
+
activeTranscriptEventKey={activeTranscriptEventKey}
|
|
1046
|
+
fullTranscriptLoaded={fullTranscriptLoaded}
|
|
1047
|
+
loadError={
|
|
1048
|
+
transcriptQuery.isError
|
|
1049
|
+
? transcriptQuery.error
|
|
1050
|
+
: transcriptPreviewQuery.isError
|
|
1051
|
+
? transcriptPreviewQuery.error
|
|
1052
|
+
: null
|
|
1053
|
+
}
|
|
1054
|
+
loadingFullTranscript={transcriptQuery.isFetching}
|
|
1055
|
+
searchInput={transcriptSearchInput}
|
|
1056
|
+
searchResults={transcriptSearchResults}
|
|
1057
|
+
showRawJson={showRawJson}
|
|
1058
|
+
sortOrder={sortOrder}
|
|
1059
|
+
snapshot={snapshot}
|
|
1060
|
+
transcript={transcript}
|
|
1061
|
+
transcriptFilters={transcriptFilters}
|
|
1062
|
+
onJumpToSearchResult={jumpToTranscriptSearchResult}
|
|
1063
|
+
onLoadFullThread={() => setShouldLoadTranscript(true)}
|
|
1064
|
+
onSearchInputChange={updateTranscriptSearchInput}
|
|
1065
|
+
onSortOrderChange={updateSortOrder}
|
|
1066
|
+
onTranscriptFilterChange={updateTranscriptFilter}
|
|
1067
|
+
/>
|
|
584
1068
|
|
|
585
1069
|
<TabsContent value="metadata">
|
|
586
1070
|
<ThreadMetadataPanels snapshot={viewSnapshot} />
|