spiracha 2.0.0 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +27 -12
- package/README.md +42 -19
- package/apps/ui/AGENTS.md +18 -10
- package/apps/ui/README.md +36 -11
- package/apps/ui/src/components/analytics-breakdowns.tsx +106 -0
- package/apps/ui/src/components/antigravity-conversations-table.tsx +51 -11
- package/apps/ui/src/components/antigravity-workspaces-table.tsx +1 -1
- package/apps/ui/src/components/app-shell.tsx +46 -3
- 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 +23 -5
- package/apps/ui/src/components/delete-confirm-dialog.tsx +4 -3
- package/apps/ui/src/components/export-dialog.tsx +79 -74
- 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/projects-table.tsx +4 -1
- 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/route-error-panel.tsx +13 -0
- package/apps/ui/src/components/selection-actions-toolbar.tsx +80 -0
- package/apps/ui/src/components/thread-goals-panel.tsx +50 -0
- package/apps/ui/src/components/thread-tools-panel.tsx +142 -0
- package/apps/ui/src/components/threads-table.tsx +13 -47
- 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 +209 -11
- package/apps/ui/src/lib/antigravity-transcript-events.ts +57 -73
- package/apps/ui/src/lib/claude-code-queries.ts +8 -0
- package/apps/ui/src/lib/claude-code-server.ts +185 -7
- package/apps/ui/src/lib/claude-code-transcript-events.ts +8 -1
- package/apps/ui/src/lib/codex-queries.ts +16 -0
- package/apps/ui/src/lib/codex-server.ts +134 -52
- package/apps/ui/src/lib/cursor-server.ts +68 -53
- package/apps/ui/src/lib/cursor-transcript-events.ts +11 -86
- package/apps/ui/src/lib/delete-batch.ts +52 -0
- package/apps/ui/src/lib/error-presentation.ts +43 -0
- package/apps/ui/src/lib/export-mutation.ts +20 -0
- package/apps/ui/src/lib/export-options.ts +15 -0
- 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 +168 -0
- package/apps/ui/src/lib/grok-transcript-events.ts +149 -0
- package/apps/ui/src/lib/kiro-server.ts +97 -7
- package/apps/ui/src/lib/mutation-error.ts +15 -0
- package/apps/ui/src/lib/opencode-server.ts +96 -7
- package/apps/ui/src/lib/path-utils.ts +1 -1
- package/apps/ui/src/lib/qoder-queries.ts +15 -0
- package/apps/ui/src/lib/qoder-server.ts +67 -11
- package/apps/ui/src/lib/qoder-transcript-events.ts +1 -1
- package/apps/ui/src/lib/route-search.ts +114 -0
- package/apps/ui/src/lib/route-state-reset.tsx +10 -0
- package/apps/ui/src/lib/settings-server.ts +33 -0
- package/apps/ui/src/lib/settings-store.tsx +82 -38
- package/apps/ui/src/lib/settings.ts +65 -0
- package/apps/ui/src/lib/source-session-export-server.ts +86 -3
- package/apps/ui/src/lib/thread-metadata.ts +22 -0
- package/apps/ui/src/lib/thread-transcript-load.ts +15 -0
- package/apps/ui/src/lib/thread-transcript-stats.ts +10 -1
- package/apps/ui/src/lib/workspace-delete-navigation.ts +20 -0
- package/apps/ui/src/routeTree.gen.ts +341 -234
- package/apps/ui/src/routes/__root.tsx +12 -15
- package/apps/ui/src/routes/analytics.tsx +10 -61
- package/apps/ui/src/routes/antigravity-conversations.$conversationId.tsx +149 -29
- package/apps/ui/src/routes/antigravity.$workspaceKey.tsx +262 -42
- package/apps/ui/src/routes/antigravity.index.tsx +2 -2
- 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 +168 -28
- package/apps/ui/src/routes/claude-code.$workspaceKey.tsx +179 -45
- package/apps/ui/src/routes/claude-code.index.tsx +2 -2
- package/apps/ui/src/routes/codex.$project.tsx +33 -31
- package/apps/ui/src/routes/codex.index.tsx +5 -10
- package/apps/ui/src/routes/cursor-threads.$composerId.tsx +31 -18
- package/apps/ui/src/routes/cursor.$workspaceKey.tsx +44 -28
- package/apps/ui/src/routes/cursor.index.tsx +5 -2
- package/apps/ui/src/routes/grok-sessions.$sessionId.tsx +401 -0
- package/apps/ui/src/routes/grok.$workspaceKey.tsx +295 -0
- package/apps/ui/src/routes/grok.index.tsx +48 -0
- package/apps/ui/src/routes/index.tsx +2 -18
- package/apps/ui/src/routes/kiro-sessions.$sessionId.tsx +91 -27
- package/apps/ui/src/routes/kiro.$workspaceKey.tsx +213 -51
- package/apps/ui/src/routes/kiro.index.tsx +2 -2
- package/apps/ui/src/routes/opencode-sessions.$sessionId.tsx +108 -29
- package/apps/ui/src/routes/opencode.$workspaceKey.tsx +231 -55
- package/apps/ui/src/routes/opencode.index.tsx +6 -2
- package/apps/ui/src/routes/qoder-sessions.$sessionId.tsx +19 -15
- package/apps/ui/src/routes/qoder.$workspaceKey.tsx +89 -48
- package/apps/ui/src/routes/qoder.index.tsx +2 -2
- package/apps/ui/src/routes/threads.$threadId.tsx +620 -105
- package/apps/ui/vite.config.ts +22 -12
- package/bin/spiracha.ts +28 -1
- package/package.json +37 -27
- package/src/client.ts +183 -5
- package/src/lib/antigravity-db.ts +424 -94
- package/src/lib/antigravity-exporter-types.ts +3 -0
- package/src/lib/antigravity-keychain.ts +7 -8
- package/src/lib/antigravity-projects.ts +201 -0
- package/src/lib/antigravity-transcript-phase.ts +47 -0
- package/src/lib/claude-code-db.ts +534 -42
- 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-analytics.ts +32 -5
- package/src/lib/codex-browser-db.ts +540 -137
- package/src/lib/codex-browser-export.ts +18 -27
- package/src/lib/codex-browser-types.ts +21 -1
- package/src/lib/codex-thread-cache.ts +106 -23
- package/src/lib/codex-thread-parser.ts +149 -55
- package/src/lib/codex-thread-recovery.ts +38 -11
- package/src/lib/codex-transcript-filter.ts +31 -0
- package/src/lib/codex-transcript-renderer.ts +87 -79
- package/src/lib/concurrency.ts +56 -3
- package/src/lib/conversation-api.ts +410 -27
- package/src/lib/conversation-data/adapter-helpers.ts +25 -3
- package/src/lib/conversation-data/antigravity-adapter.ts +70 -26
- package/src/lib/conversation-data/claude-code-adapter.ts +69 -12
- package/src/lib/conversation-data/codex-adapter.ts +108 -73
- package/src/lib/conversation-data/cursor-adapter.ts +77 -29
- package/src/lib/conversation-data/grok-adapter.ts +223 -0
- package/src/lib/conversation-data/index.ts +105 -11
- package/src/lib/conversation-data/kiro-adapter.ts +59 -12
- package/src/lib/conversation-data/message-selector.ts +2 -5
- package/src/lib/conversation-data/opencode-adapter.ts +67 -17
- package/src/lib/conversation-data/path-match.ts +1 -13
- package/src/lib/conversation-data/qoder-adapter.ts +67 -37
- package/src/lib/conversation-data/types.ts +43 -0
- package/src/lib/conversation-zip-export.ts +75 -0
- package/src/lib/cursor-db.ts +68 -28
- package/src/lib/cursor-id.ts +17 -0
- package/src/lib/cursor-recovery.ts +146 -35
- package/src/lib/cursor-transcript-phase.ts +40 -0
- package/src/lib/cursor-transcript.ts +21 -5
- package/src/lib/grok-db.ts +1068 -0
- package/src/lib/grok-exporter-types.ts +110 -0
- package/src/lib/grok-transcript-phase.ts +61 -0
- package/src/lib/grok-transcript.ts +167 -0
- package/src/lib/kiro-db.ts +187 -43
- package/src/lib/kiro-transcript.ts +0 -4
- package/src/lib/model-label.ts +11 -3
- package/src/lib/opencode-db.ts +599 -60
- package/src/lib/opencode-think-tags.ts +17 -3
- package/src/lib/opencode-transcript.ts +8 -5
- package/src/lib/portable-path.ts +9 -0
- package/src/lib/qoder-acp-client.ts +11 -2
- package/src/lib/qoder-db.ts +51 -17
- package/src/lib/qoder-transcript-phase.ts +64 -0
- package/src/lib/qoder-transcript.ts +30 -2
- package/src/lib/shared.ts +63 -15
- package/src/lib/sqlite-error.ts +31 -5
- package/src/lib/transcript-load-limiter.ts +87 -0
- package/src/lib/ui-cache.ts +185 -24
- package/src/lib/ui-export-archive.ts +58 -39
- package/src/lib/ui-export-files.ts +75 -15
- package/src/lib/ui-export-zip.ts +40 -0
- package/apps/ui/package.json +0 -65
|
@@ -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,168 @@ 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 { RouteErrorPanel } from '#/components/route-error-panel';
|
|
21
|
+
import { ThreadGoalsPanel } from '#/components/thread-goals-panel';
|
|
22
|
+
import { ThreadToolsPanel } from '#/components/thread-tools-panel';
|
|
23
|
+
import {
|
|
24
|
+
getTranscriptEventKey,
|
|
25
|
+
shouldShowEvent,
|
|
26
|
+
type TranscriptSortOrder,
|
|
27
|
+
TranscriptView,
|
|
28
|
+
} from '#/components/transcript-view';
|
|
14
29
|
import { Badge } from '#/components/ui/badge';
|
|
15
30
|
import { Button } from '#/components/ui/button';
|
|
16
31
|
import { Checkbox } from '#/components/ui/checkbox';
|
|
32
|
+
import { Input } from '#/components/ui/input';
|
|
17
33
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '#/components/ui/tabs';
|
|
18
|
-
import {
|
|
34
|
+
import {
|
|
35
|
+
threadSnapshotQueryOptions,
|
|
36
|
+
threadTranscriptPreviewQueryOptions,
|
|
37
|
+
threadTranscriptQueryOptions,
|
|
38
|
+
} from '#/lib/codex-queries';
|
|
19
39
|
import { deleteThreadFn, exportThreadFn, type getThreadSnapshotFn } from '#/lib/codex-server';
|
|
20
40
|
import { downloadTextFile, downloadUrlFile } from '#/lib/download';
|
|
21
|
-
import {
|
|
41
|
+
import {
|
|
42
|
+
formatBooleanLabel,
|
|
43
|
+
formatBytes,
|
|
44
|
+
formatDateTime,
|
|
45
|
+
formatList,
|
|
46
|
+
formatModelLabel,
|
|
47
|
+
formatTokens,
|
|
48
|
+
} from '#/lib/formatters';
|
|
49
|
+
import { getMutationErrorMessage } from '#/lib/mutation-error';
|
|
50
|
+
import { applyPathTransforms } from '#/lib/path-utils';
|
|
51
|
+
import {
|
|
52
|
+
parseThreadTranscriptSearch,
|
|
53
|
+
type ThreadTranscriptSearch,
|
|
54
|
+
withThreadTranscriptSearch,
|
|
55
|
+
} from '#/lib/route-search';
|
|
56
|
+
import { RouteStateResetBoundary } from '#/lib/route-state-reset';
|
|
22
57
|
import { useSettings } from '#/lib/settings-store';
|
|
58
|
+
import { formatSandboxPolicy } from '#/lib/thread-metadata';
|
|
59
|
+
import { shouldLoadFullThreadTranscript } from '#/lib/thread-transcript-load';
|
|
23
60
|
|
|
24
|
-
type
|
|
61
|
+
type ThreadSnapshotResponse = Awaited<ReturnType<typeof getThreadSnapshotFn>>;
|
|
62
|
+
type ThreadTranscript = ParsedCodexTranscript;
|
|
63
|
+
type ThreadSnapshot = Omit<ThreadSnapshotResponse, 'availableTools' | 'transcript'> & {
|
|
64
|
+
availableTools: DynamicToolDefinition[];
|
|
65
|
+
transcript: ThreadTranscript | null;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
type TranscriptSearchResult = {
|
|
69
|
+
event: MessageEvent;
|
|
70
|
+
eventIndex: number;
|
|
71
|
+
eventKey: string;
|
|
72
|
+
messageNumber: number;
|
|
73
|
+
phase: string | null;
|
|
74
|
+
roleLabel: string;
|
|
75
|
+
snippet: string;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
type TranscriptSearchPanelProps = {
|
|
79
|
+
activeResultIndex: number;
|
|
80
|
+
query: string;
|
|
81
|
+
results: TranscriptSearchResult[];
|
|
82
|
+
onJumpToResult: (index: number) => void;
|
|
83
|
+
onQueryChange: (value: string) => void;
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
type TranscriptSearchFilters = {
|
|
87
|
+
showCommentary: boolean;
|
|
88
|
+
showExtraEvents: boolean;
|
|
89
|
+
showToolCalls: boolean;
|
|
90
|
+
showUserMessages: boolean;
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const SEARCH_SNIPPET_RADIUS = 72;
|
|
94
|
+
|
|
95
|
+
const getTranscriptFiltersFromSearch = (search: ThreadTranscriptSearch) => ({
|
|
96
|
+
showCommentary: search.commentary === true,
|
|
97
|
+
showExtraEvents: search.extra === true,
|
|
98
|
+
showToolCalls: search.tools === true,
|
|
99
|
+
showUserMessages: search.user === true,
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
type ThreadTranscriptFilters = ReturnType<typeof getTranscriptFiltersFromSearch>;
|
|
103
|
+
|
|
104
|
+
const normalizeTranscriptSearchText = (value: string) => value.replace(/\s+/gu, ' ').trim();
|
|
105
|
+
|
|
106
|
+
const getTranscriptSearchRoleLabel = (event: MessageEvent, assistantModel: string | null) => {
|
|
107
|
+
if (event.role === 'assistant') {
|
|
108
|
+
return formatModelLabel(event.model ?? assistantModel);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return event.role === 'system' ? 'System' : 'User';
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const buildTranscriptSearchSnippet = (text: string, query: string) => {
|
|
115
|
+
const normalizedText = normalizeTranscriptSearchText(text);
|
|
116
|
+
const normalizedQuery = normalizeTranscriptSearchText(query).toLowerCase();
|
|
117
|
+
const matchIndex = normalizedText.toLowerCase().indexOf(normalizedQuery);
|
|
118
|
+
|
|
119
|
+
if (matchIndex < 0) {
|
|
120
|
+
return normalizedText.slice(0, SEARCH_SNIPPET_RADIUS * 2);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const start = Math.max(0, matchIndex - SEARCH_SNIPPET_RADIUS);
|
|
124
|
+
const end = Math.min(normalizedText.length, matchIndex + normalizedQuery.length + SEARCH_SNIPPET_RADIUS);
|
|
125
|
+
const prefix = start > 0 ? '...' : '';
|
|
126
|
+
const suffix = end < normalizedText.length ? '...' : '';
|
|
127
|
+
|
|
128
|
+
return `${prefix}${normalizedText.slice(start, end)}${suffix}`;
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
const buildTranscriptSearchResults = (
|
|
132
|
+
events: ThreadEvent[],
|
|
133
|
+
query: string,
|
|
134
|
+
assistantModel: string | null,
|
|
135
|
+
filters: TranscriptSearchFilters,
|
|
136
|
+
transform: (text: string) => string,
|
|
137
|
+
): TranscriptSearchResult[] => {
|
|
138
|
+
const normalizedQuery = normalizeTranscriptSearchText(query).toLowerCase();
|
|
139
|
+
if (!normalizedQuery) {
|
|
140
|
+
return [];
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const results: TranscriptSearchResult[] = [];
|
|
144
|
+
let messageNumber = 0;
|
|
145
|
+
|
|
146
|
+
events.forEach((event, index) => {
|
|
147
|
+
if (event.kind !== 'message') {
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (
|
|
152
|
+
!shouldShowEvent(
|
|
153
|
+
event,
|
|
154
|
+
filters.showToolCalls,
|
|
155
|
+
filters.showExtraEvents,
|
|
156
|
+
filters.showCommentary,
|
|
157
|
+
filters.showUserMessages,
|
|
158
|
+
)
|
|
159
|
+
) {
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
messageNumber += 1;
|
|
164
|
+
const searchText = normalizeTranscriptSearchText(transform(event.text));
|
|
165
|
+
if (!searchText.toLowerCase().includes(normalizedQuery)) {
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
results.push({
|
|
170
|
+
event,
|
|
171
|
+
eventIndex: index,
|
|
172
|
+
eventKey: getTranscriptEventKey(event, index),
|
|
173
|
+
messageNumber,
|
|
174
|
+
phase: event.phase,
|
|
175
|
+
roleLabel: getTranscriptSearchRoleLabel(event, assistantModel),
|
|
176
|
+
snippet: buildTranscriptSearchSnippet(searchText, query),
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
return results;
|
|
181
|
+
};
|
|
25
182
|
|
|
26
183
|
type TranscriptControlsProps = {
|
|
27
184
|
rawJsonDisabled?: boolean;
|
|
@@ -91,6 +248,7 @@ const buildRuntimeItems = (snapshot: ThreadSnapshot) => {
|
|
|
91
248
|
{ label: 'Reasoning effort', value: snapshot.thread.reasoning_effort ?? 'n/a' },
|
|
92
249
|
{ label: 'CLI version', value: snapshot.thread.cli_version },
|
|
93
250
|
{ label: 'Approval mode', value: snapshot.thread.approval_mode },
|
|
251
|
+
{ label: 'Sandbox policy', value: formatSandboxPolicy(snapshot.thread.sandbox_policy) },
|
|
94
252
|
{ label: 'Memory mode', value: snapshot.thread.memory_mode },
|
|
95
253
|
{ label: 'Has user event', value: formatBooleanLabel(Boolean(snapshot.thread.has_user_event)) },
|
|
96
254
|
];
|
|
@@ -162,7 +320,7 @@ const buildTranscriptStatsItems = (snapshot: ThreadSnapshot) => {
|
|
|
162
320
|
return [
|
|
163
321
|
{ label: 'Transcript load', value: 'Deferred for oversized rollout' },
|
|
164
322
|
{ label: 'Rollout size', value: formatBytes(snapshot.rollout.fileSizeBytes) },
|
|
165
|
-
{ label: '
|
|
323
|
+
{ label: 'Deferred load', value: 'Load the transcript manually to inspect it.' },
|
|
166
324
|
];
|
|
167
325
|
}
|
|
168
326
|
|
|
@@ -240,6 +398,98 @@ function TranscriptControls({
|
|
|
240
398
|
);
|
|
241
399
|
}
|
|
242
400
|
|
|
401
|
+
function TranscriptSearchPanel({
|
|
402
|
+
activeResultIndex,
|
|
403
|
+
query,
|
|
404
|
+
results,
|
|
405
|
+
onJumpToResult,
|
|
406
|
+
onQueryChange,
|
|
407
|
+
}: TranscriptSearchPanelProps) {
|
|
408
|
+
const hasQuery = normalizeTranscriptSearchText(query).length > 0;
|
|
409
|
+
const hasResults = results.length > 0;
|
|
410
|
+
const statusLabel = hasQuery
|
|
411
|
+
? hasResults
|
|
412
|
+
? `${activeResultIndex + 1} / ${results.length}`
|
|
413
|
+
: 'No matches'
|
|
414
|
+
: 'Search';
|
|
415
|
+
|
|
416
|
+
const handleKeyDown = (event: KeyboardEvent<HTMLInputElement>) => {
|
|
417
|
+
if (event.key !== 'Enter' || !hasResults) {
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
event.preventDefault();
|
|
422
|
+
onJumpToResult(event.shiftKey ? activeResultIndex - 1 : activeResultIndex);
|
|
423
|
+
};
|
|
424
|
+
|
|
425
|
+
return (
|
|
426
|
+
<section className="rounded-xl border border-[var(--border)] bg-[var(--panel)] px-4 py-3 shadow-[var(--panel-shadow)]">
|
|
427
|
+
<div className="flex flex-col gap-3 lg:flex-row lg:items-center">
|
|
428
|
+
<div className="relative min-w-0 flex-1">
|
|
429
|
+
<Search className="pointer-events-none absolute top-1/2 left-3 size-4 -translate-y-1/2 text-[var(--muted-foreground)]" />
|
|
430
|
+
<Input
|
|
431
|
+
aria-label="Search transcript messages"
|
|
432
|
+
className="h-10 rounded-full border-[var(--border)] bg-[var(--panel-secondary)] pr-4 pl-9"
|
|
433
|
+
placeholder="Search transcript messages"
|
|
434
|
+
value={query}
|
|
435
|
+
onChange={(event) => onQueryChange(event.target.value)}
|
|
436
|
+
onKeyDown={handleKeyDown}
|
|
437
|
+
/>
|
|
438
|
+
</div>
|
|
439
|
+
|
|
440
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
441
|
+
<span className="min-w-20 text-right text-[var(--muted-foreground)] text-sm">{statusLabel}</span>
|
|
442
|
+
<Button
|
|
443
|
+
className="rounded-full"
|
|
444
|
+
disabled={!hasResults}
|
|
445
|
+
size="sm"
|
|
446
|
+
type="button"
|
|
447
|
+
variant="outline"
|
|
448
|
+
onClick={() => onJumpToResult(activeResultIndex - 1)}
|
|
449
|
+
>
|
|
450
|
+
<ChevronUp className="size-4" />
|
|
451
|
+
Prev
|
|
452
|
+
</Button>
|
|
453
|
+
<Button
|
|
454
|
+
className="rounded-full"
|
|
455
|
+
disabled={!hasResults}
|
|
456
|
+
size="sm"
|
|
457
|
+
type="button"
|
|
458
|
+
variant="outline"
|
|
459
|
+
onClick={() => onJumpToResult(activeResultIndex + 1)}
|
|
460
|
+
>
|
|
461
|
+
<ChevronDown className="size-4" />
|
|
462
|
+
Next
|
|
463
|
+
</Button>
|
|
464
|
+
</div>
|
|
465
|
+
</div>
|
|
466
|
+
|
|
467
|
+
{hasQuery && hasResults ? (
|
|
468
|
+
<div className="mt-3 max-h-72 overflow-auto rounded-xl border border-[var(--border)] bg-[var(--panel-secondary)]">
|
|
469
|
+
{results.map((result, index) => (
|
|
470
|
+
<button
|
|
471
|
+
key={result.eventKey}
|
|
472
|
+
aria-current={index === activeResultIndex ? 'true' : undefined}
|
|
473
|
+
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)]"
|
|
474
|
+
type="button"
|
|
475
|
+
onClick={() => onJumpToResult(index)}
|
|
476
|
+
>
|
|
477
|
+
<span className="flex flex-wrap items-center gap-2">
|
|
478
|
+
<span className="font-medium text-sm">Message {result.messageNumber}</span>
|
|
479
|
+
<Badge variant="outline">{result.roleLabel}</Badge>
|
|
480
|
+
{result.phase ? <Badge variant="outline">{result.phase}</Badge> : null}
|
|
481
|
+
</span>
|
|
482
|
+
<span className="mt-1 block min-w-0 break-words text-[var(--muted-foreground)] text-sm leading-5 [overflow-wrap:anywhere]">
|
|
483
|
+
{result.snippet}
|
|
484
|
+
</span>
|
|
485
|
+
</button>
|
|
486
|
+
))}
|
|
487
|
+
</div>
|
|
488
|
+
) : null}
|
|
489
|
+
</section>
|
|
490
|
+
);
|
|
491
|
+
}
|
|
492
|
+
|
|
243
493
|
function ThreadMetadataPanels({ snapshot }: ThreadMetadataProps) {
|
|
244
494
|
return (
|
|
245
495
|
<div className="grid gap-4 xl:grid-cols-[1.1fr_0.9fr]">
|
|
@@ -251,29 +501,7 @@ function ThreadMetadataPanels({ snapshot }: ThreadMetadataProps) {
|
|
|
251
501
|
|
|
252
502
|
<div className="space-y-4">
|
|
253
503
|
<MetadataSection items={buildRelationItems(snapshot)} title="Relations and summary" />
|
|
254
|
-
|
|
255
|
-
<section className="rounded-[1.6rem] border border-[var(--border)] bg-[var(--panel)] p-5 shadow-[var(--panel-shadow)]">
|
|
256
|
-
<h3 className="font-semibold text-[var(--muted-foreground)] text-sm uppercase tracking-[0.18em]">
|
|
257
|
-
Available tools
|
|
258
|
-
</h3>
|
|
259
|
-
<div className="mt-4 space-y-3">
|
|
260
|
-
{snapshot.availableTools.map((tool) => (
|
|
261
|
-
<div
|
|
262
|
-
key={`${tool.name}-${tool.namespace ?? 'global'}`}
|
|
263
|
-
className="rounded-xl border border-[var(--border)] bg-[var(--panel-secondary)] p-3.5"
|
|
264
|
-
>
|
|
265
|
-
<div className="flex flex-wrap items-center gap-2">
|
|
266
|
-
<p className="font-medium font-mono text-sm">{tool.name}</p>
|
|
267
|
-
{tool.namespace ? <Badge variant="outline">{tool.namespace}</Badge> : null}
|
|
268
|
-
</div>
|
|
269
|
-
<p className="mt-1.5 text-[var(--muted-foreground)] text-sm">
|
|
270
|
-
{tool.description || 'No description.'}
|
|
271
|
-
</p>
|
|
272
|
-
</div>
|
|
273
|
-
))}
|
|
274
|
-
</div>
|
|
275
|
-
</section>
|
|
276
|
-
|
|
504
|
+
<ThreadGoalsPanel goals={snapshot.goals} />
|
|
277
505
|
<MetadataSection items={buildTranscriptStatsItems(snapshot)} title="Transcript stats" />
|
|
278
506
|
</div>
|
|
279
507
|
</div>
|
|
@@ -319,12 +547,12 @@ function DeferredTranscriptNotice({
|
|
|
319
547
|
<p className="mt-2 text-[var(--muted-foreground)] text-sm leading-6">
|
|
320
548
|
{missing
|
|
321
549
|
? '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.
|
|
550
|
+
: `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
551
|
</p>
|
|
324
552
|
{missing ? null : (
|
|
325
553
|
<div className="mt-4">
|
|
326
554
|
<Button disabled={pending} variant="outline" onClick={onLoad}>
|
|
327
|
-
{pending ? 'Loading
|
|
555
|
+
{pending ? 'Loading full thread...' : 'Load Full Thread'}
|
|
328
556
|
</Button>
|
|
329
557
|
</div>
|
|
330
558
|
)}
|
|
@@ -332,27 +560,43 @@ function DeferredTranscriptNotice({
|
|
|
332
560
|
);
|
|
333
561
|
}
|
|
334
562
|
|
|
335
|
-
function
|
|
336
|
-
|
|
563
|
+
function LargeThreadPreviewNotice({
|
|
564
|
+
fileSizeBytes,
|
|
565
|
+
fullTranscriptLoaded,
|
|
566
|
+
pending,
|
|
567
|
+
onLoad,
|
|
568
|
+
}: {
|
|
569
|
+
fileSizeBytes: number | null;
|
|
570
|
+
fullTranscriptLoaded: boolean;
|
|
571
|
+
pending: boolean;
|
|
572
|
+
onLoad: () => void;
|
|
573
|
+
}) {
|
|
574
|
+
const buttonLabel = fullTranscriptLoaded
|
|
575
|
+
? 'Full thread loaded'
|
|
576
|
+
: pending
|
|
577
|
+
? 'Loading full thread...'
|
|
578
|
+
: 'Load Full Thread';
|
|
579
|
+
|
|
337
580
|
return (
|
|
338
|
-
<
|
|
339
|
-
<
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
{isSqlite ? 'Codex may have an exclusive lock on the database. Reload to retry.' : error.message}
|
|
581
|
+
<section className="rounded-[1.6rem] border border-[var(--border)] bg-[var(--panel)] p-5 shadow-[var(--panel-shadow)]">
|
|
582
|
+
<h3 className="font-semibold text-base">Showing the latest matching messages</h3>
|
|
583
|
+
<p className="mt-2 text-[var(--muted-foreground)] text-sm leading-6">
|
|
584
|
+
This rollout is {formatBytes(fileSizeBytes)}, so Spiracha loaded a small window from the end of the
|
|
585
|
+
thread using the current transcript filters. Load the full thread when you need earlier messages.
|
|
344
586
|
</p>
|
|
345
|
-
<
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
>
|
|
350
|
-
|
|
351
|
-
</button>
|
|
352
|
-
</div>
|
|
587
|
+
<div className="mt-4">
|
|
588
|
+
<Button disabled={pending || fullTranscriptLoaded} variant="outline" onClick={onLoad}>
|
|
589
|
+
{buttonLabel}
|
|
590
|
+
</Button>
|
|
591
|
+
</div>
|
|
592
|
+
</section>
|
|
353
593
|
);
|
|
354
594
|
}
|
|
355
595
|
|
|
596
|
+
function ThreadErrorComponent({ error }: { error: Error }) {
|
|
597
|
+
return <RouteErrorPanel error={error} title="Failed to load thread" />;
|
|
598
|
+
}
|
|
599
|
+
|
|
356
600
|
const getThreadExportErrorMessage = (transcriptMissing: boolean, error: unknown): string | null => {
|
|
357
601
|
if (transcriptMissing) {
|
|
358
602
|
return 'The rollout JSONL file is missing from disk, so this thread cannot be exported right now.';
|
|
@@ -371,10 +615,199 @@ export const Route = createFileRoute('/threads/$threadId')({
|
|
|
371
615
|
title="Loading thread"
|
|
372
616
|
/>
|
|
373
617
|
),
|
|
618
|
+
validateSearch: parseThreadTranscriptSearch,
|
|
374
619
|
});
|
|
375
620
|
|
|
376
|
-
|
|
377
|
-
const navigate = useNavigate();
|
|
621
|
+
const useThreadTranscriptRouteSearch = () => {
|
|
622
|
+
const navigate = useNavigate({ from: Route.fullPath });
|
|
623
|
+
const search = Route.useSearch();
|
|
624
|
+
const transcriptFilters = useMemo(() => getTranscriptFiltersFromSearch(search), [search]);
|
|
625
|
+
|
|
626
|
+
const updateTranscriptSearch = (patch: Partial<ThreadTranscriptSearch>) => {
|
|
627
|
+
startTransition(() => {
|
|
628
|
+
void navigate({
|
|
629
|
+
params: true,
|
|
630
|
+
replace: true,
|
|
631
|
+
search: (previous: Record<string, unknown>) => withThreadTranscriptSearch(previous, patch),
|
|
632
|
+
});
|
|
633
|
+
});
|
|
634
|
+
};
|
|
635
|
+
|
|
636
|
+
return {
|
|
637
|
+
navigate,
|
|
638
|
+
search,
|
|
639
|
+
transcriptFilters,
|
|
640
|
+
updateTranscriptSearch,
|
|
641
|
+
};
|
|
642
|
+
};
|
|
643
|
+
|
|
644
|
+
const useTranscriptSearchResults = ({
|
|
645
|
+
assistantModel,
|
|
646
|
+
filters,
|
|
647
|
+
projectPath,
|
|
648
|
+
query,
|
|
649
|
+
settings,
|
|
650
|
+
transcript,
|
|
651
|
+
}: {
|
|
652
|
+
assistantModel: string | null;
|
|
653
|
+
filters: TranscriptSearchFilters;
|
|
654
|
+
projectPath: string;
|
|
655
|
+
query: string;
|
|
656
|
+
settings: ReturnType<typeof useSettings>['settings'];
|
|
657
|
+
transcript: ThreadTranscript | null;
|
|
658
|
+
}) =>
|
|
659
|
+
useMemo(
|
|
660
|
+
() =>
|
|
661
|
+
transcript
|
|
662
|
+
? buildTranscriptSearchResults(transcript.events, query, assistantModel, filters, (text) =>
|
|
663
|
+
applyPathTransforms(text, settings, projectPath),
|
|
664
|
+
)
|
|
665
|
+
: [],
|
|
666
|
+
[assistantModel, filters, projectPath, query, settings, transcript],
|
|
667
|
+
);
|
|
668
|
+
|
|
669
|
+
function ThreadTranscriptTab({
|
|
670
|
+
activeEventJumpSignal,
|
|
671
|
+
activeSearchResultIndex,
|
|
672
|
+
activeTranscriptEventKey,
|
|
673
|
+
fullTranscriptLoaded,
|
|
674
|
+
loadError,
|
|
675
|
+
loadingFullTranscript,
|
|
676
|
+
searchInput,
|
|
677
|
+
searchResults,
|
|
678
|
+
showRawJson,
|
|
679
|
+
sortOrder,
|
|
680
|
+
snapshot,
|
|
681
|
+
transcript,
|
|
682
|
+
transcriptFilters,
|
|
683
|
+
onJumpToSearchResult,
|
|
684
|
+
onLoadFullThread,
|
|
685
|
+
onSearchInputChange,
|
|
686
|
+
onSortOrderChange,
|
|
687
|
+
onTranscriptFilterChange,
|
|
688
|
+
}: {
|
|
689
|
+
activeEventJumpSignal: number;
|
|
690
|
+
activeSearchResultIndex: number;
|
|
691
|
+
activeTranscriptEventKey: string | null;
|
|
692
|
+
fullTranscriptLoaded: boolean;
|
|
693
|
+
loadError: unknown;
|
|
694
|
+
loadingFullTranscript: boolean;
|
|
695
|
+
searchInput: string;
|
|
696
|
+
searchResults: TranscriptSearchResult[];
|
|
697
|
+
showRawJson: boolean;
|
|
698
|
+
sortOrder: TranscriptSortOrder;
|
|
699
|
+
snapshot: ThreadSnapshot;
|
|
700
|
+
transcript: ThreadTranscript | null;
|
|
701
|
+
transcriptFilters: ThreadTranscriptFilters;
|
|
702
|
+
onJumpToSearchResult: (index: number) => void;
|
|
703
|
+
onLoadFullThread: () => void;
|
|
704
|
+
onSearchInputChange: (value: string) => void;
|
|
705
|
+
onSortOrderChange: (value: TranscriptSortOrder) => void;
|
|
706
|
+
onTranscriptFilterChange: (patch: Partial<ThreadTranscriptSearch>) => void;
|
|
707
|
+
}) {
|
|
708
|
+
return (
|
|
709
|
+
<TabsContent className="space-y-3" value="transcript">
|
|
710
|
+
<TranscriptControls
|
|
711
|
+
rawJsonDisabled={!transcript?.rawIncluded}
|
|
712
|
+
showCommentary={transcriptFilters.showCommentary}
|
|
713
|
+
showExtraEvents={transcriptFilters.showExtraEvents}
|
|
714
|
+
showRawJson={showRawJson}
|
|
715
|
+
showToolCalls={transcriptFilters.showToolCalls}
|
|
716
|
+
showUserMessages={transcriptFilters.showUserMessages}
|
|
717
|
+
onShowCommentaryChange={(commentary) => onTranscriptFilterChange({ commentary })}
|
|
718
|
+
onShowExtraEventsChange={(extra) => onTranscriptFilterChange({ extra })}
|
|
719
|
+
onShowRawJsonChange={(raw) => onTranscriptFilterChange({ raw })}
|
|
720
|
+
onShowToolCallsChange={(tools) => onTranscriptFilterChange({ tools })}
|
|
721
|
+
onShowUserMessagesChange={(user) => onTranscriptFilterChange({ user })}
|
|
722
|
+
/>
|
|
723
|
+
{transcript?.isPartial && snapshot.rollout.shouldDeferTranscriptLoad ? (
|
|
724
|
+
<LargeThreadPreviewNotice
|
|
725
|
+
fileSizeBytes={snapshot.rollout.fileSizeBytes}
|
|
726
|
+
fullTranscriptLoaded={fullTranscriptLoaded}
|
|
727
|
+
pending={loadingFullTranscript}
|
|
728
|
+
onLoad={onLoadFullThread}
|
|
729
|
+
/>
|
|
730
|
+
) : null}
|
|
731
|
+
{transcript ? (
|
|
732
|
+
<TranscriptSearchPanel
|
|
733
|
+
activeResultIndex={activeSearchResultIndex}
|
|
734
|
+
query={searchInput}
|
|
735
|
+
results={searchResults}
|
|
736
|
+
onJumpToResult={onJumpToSearchResult}
|
|
737
|
+
onQueryChange={onSearchInputChange}
|
|
738
|
+
/>
|
|
739
|
+
) : null}
|
|
740
|
+
{transcript ? (
|
|
741
|
+
<TranscriptView
|
|
742
|
+
activeEventJumpSignal={activeEventJumpSignal}
|
|
743
|
+
activeEventKey={activeTranscriptEventKey}
|
|
744
|
+
assistantModel={snapshot.thread.model}
|
|
745
|
+
events={transcript.events}
|
|
746
|
+
projectPath={snapshot.thread.cwd}
|
|
747
|
+
showCommentary={transcriptFilters.showCommentary}
|
|
748
|
+
showExtraEvents={transcriptFilters.showExtraEvents}
|
|
749
|
+
showRawJson={showRawJson && transcript.rawIncluded}
|
|
750
|
+
showToolCalls={transcriptFilters.showToolCalls}
|
|
751
|
+
showUserMessages={transcriptFilters.showUserMessages}
|
|
752
|
+
sortOrder={sortOrder}
|
|
753
|
+
onSortOrderChange={onSortOrderChange}
|
|
754
|
+
/>
|
|
755
|
+
) : (
|
|
756
|
+
<DeferredTranscriptNotice
|
|
757
|
+
fileSizeBytes={snapshot.rollout.fileSizeBytes}
|
|
758
|
+
missing={snapshot.transcriptState === 'missing'}
|
|
759
|
+
pending={loadingFullTranscript}
|
|
760
|
+
onLoad={onLoadFullThread}
|
|
761
|
+
/>
|
|
762
|
+
)}
|
|
763
|
+
{loadError ? (
|
|
764
|
+
<p className="text-[var(--destructive)] text-sm">
|
|
765
|
+
Failed to load transcript preview:{' '}
|
|
766
|
+
{loadError instanceof Error ? loadError.message : 'Unknown error'}
|
|
767
|
+
</p>
|
|
768
|
+
) : null}
|
|
769
|
+
</TabsContent>
|
|
770
|
+
);
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
function ThreadToolsTab({
|
|
774
|
+
loadingTranscript,
|
|
775
|
+
showRawJson,
|
|
776
|
+
snapshot,
|
|
777
|
+
sortOrder,
|
|
778
|
+
transcript,
|
|
779
|
+
onLoadTranscript,
|
|
780
|
+
onSortOrderChange,
|
|
781
|
+
}: {
|
|
782
|
+
loadingTranscript: boolean;
|
|
783
|
+
showRawJson: boolean;
|
|
784
|
+
snapshot: ThreadSnapshot;
|
|
785
|
+
sortOrder: TranscriptSortOrder;
|
|
786
|
+
transcript: ThreadTranscript | null;
|
|
787
|
+
onLoadTranscript: () => void;
|
|
788
|
+
onSortOrderChange: (value: TranscriptSortOrder) => void;
|
|
789
|
+
}) {
|
|
790
|
+
return (
|
|
791
|
+
<TabsContent value="tools">
|
|
792
|
+
<ThreadToolsPanel
|
|
793
|
+
assistantModel={snapshot.thread.model}
|
|
794
|
+
availableTools={snapshot.availableTools}
|
|
795
|
+
events={transcript?.events ?? null}
|
|
796
|
+
loadingTranscript={loadingTranscript}
|
|
797
|
+
projectPath={snapshot.thread.cwd}
|
|
798
|
+
showRawJson={showRawJson && Boolean(transcript?.rawIncluded)}
|
|
799
|
+
sortOrder={sortOrder}
|
|
800
|
+
transcriptIsPartial={transcript?.isPartial}
|
|
801
|
+
transcriptState={transcript ? 'available' : snapshot.transcriptState}
|
|
802
|
+
onLoadTranscript={onLoadTranscript}
|
|
803
|
+
onSortOrderChange={onSortOrderChange}
|
|
804
|
+
/>
|
|
805
|
+
</TabsContent>
|
|
806
|
+
);
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
function ThreadDetailPageContent() {
|
|
810
|
+
const { navigate, search, transcriptFilters, updateTranscriptSearch } = useThreadTranscriptRouteSearch();
|
|
378
811
|
const queryClient = useQueryClient();
|
|
379
812
|
const params = Route.useParams();
|
|
380
813
|
const snapshot = useSuspenseQuery(threadSnapshotQueryOptions(params.threadId)).data;
|
|
@@ -383,19 +816,82 @@ function ThreadDetailPage() {
|
|
|
383
816
|
const [shouldLoadTranscript, setShouldLoadTranscript] = useState(
|
|
384
817
|
!snapshot.rollout.shouldDeferTranscriptLoad && !transcriptMissing,
|
|
385
818
|
);
|
|
386
|
-
const
|
|
387
|
-
const
|
|
388
|
-
const
|
|
389
|
-
const [
|
|
390
|
-
const [
|
|
819
|
+
const showRawJson = search.raw === true;
|
|
820
|
+
const sortOrder: TranscriptSortOrder = search.sort ?? 'earliest';
|
|
821
|
+
const transcriptSearchInput = search.q ?? '';
|
|
822
|
+
const [activeSearchResultIndex, setActiveSearchResultIndex] = useState(0);
|
|
823
|
+
const [activeTranscriptEventKey, setActiveTranscriptEventKey] = useState<string | null>(null);
|
|
824
|
+
const [activeEventJumpSignal, setActiveEventJumpSignal] = useState(0);
|
|
391
825
|
const [exportOpen, setExportOpen] = useState(false);
|
|
392
826
|
const [deleteOpen, setDeleteOpen] = useState(false);
|
|
827
|
+
const transcriptPreviewQuery = useQuery({
|
|
828
|
+
...threadTranscriptPreviewQueryOptions(params.threadId, transcriptFilters),
|
|
829
|
+
enabled: snapshot.rollout.shouldDeferTranscriptLoad && !shouldLoadTranscript && !transcriptMissing,
|
|
830
|
+
});
|
|
393
831
|
const transcriptQuery = useQuery({
|
|
394
832
|
...threadTranscriptQueryOptions(params.threadId),
|
|
395
|
-
enabled:
|
|
833
|
+
enabled: shouldLoadFullThreadTranscript({
|
|
834
|
+
shouldLoadTranscript,
|
|
835
|
+
snapshotTranscript: snapshot.transcript,
|
|
836
|
+
transcriptMissing,
|
|
837
|
+
}),
|
|
838
|
+
});
|
|
839
|
+
const transcript = transcriptQuery.data ?? transcriptPreviewQuery.data ?? snapshot.transcript ?? null;
|
|
840
|
+
const viewSnapshot = {
|
|
841
|
+
...snapshot,
|
|
842
|
+
availableTools:
|
|
843
|
+
snapshot.availableTools.length > 0 ? snapshot.availableTools : (transcript?.sessionMeta.dynamicTools ?? []),
|
|
844
|
+
transcript,
|
|
845
|
+
};
|
|
846
|
+
const fullTranscriptLoaded = Boolean(transcriptQuery.data && !transcriptQuery.data.isPartial);
|
|
847
|
+
const transcriptSearchResults = useTranscriptSearchResults({
|
|
848
|
+
assistantModel: snapshot.thread.model,
|
|
849
|
+
filters: transcriptFilters,
|
|
850
|
+
projectPath: snapshot.thread.cwd,
|
|
851
|
+
query: transcriptSearchInput,
|
|
852
|
+
settings,
|
|
853
|
+
transcript,
|
|
396
854
|
});
|
|
397
|
-
|
|
398
|
-
|
|
855
|
+
|
|
856
|
+
useEffect(() => {
|
|
857
|
+
setActiveSearchResultIndex((current) =>
|
|
858
|
+
transcriptSearchResults.length === 0 ? 0 : Math.min(current, transcriptSearchResults.length - 1),
|
|
859
|
+
);
|
|
860
|
+
setActiveTranscriptEventKey((current) =>
|
|
861
|
+
current && transcriptSearchResults.some((result) => result.eventKey === current) ? current : null,
|
|
862
|
+
);
|
|
863
|
+
}, [transcriptSearchResults]);
|
|
864
|
+
|
|
865
|
+
const updateTranscriptSearchInput = (value: string) => {
|
|
866
|
+
setActiveSearchResultIndex(0);
|
|
867
|
+
setActiveTranscriptEventKey(null);
|
|
868
|
+
updateTranscriptSearch({ q: value });
|
|
869
|
+
};
|
|
870
|
+
|
|
871
|
+
const updateTranscriptFilter = (patch: Partial<ThreadTranscriptSearch>) => {
|
|
872
|
+
setActiveSearchResultIndex(0);
|
|
873
|
+
setActiveTranscriptEventKey(null);
|
|
874
|
+
updateTranscriptSearch(patch);
|
|
875
|
+
};
|
|
876
|
+
|
|
877
|
+
const updateSortOrder = (sort: TranscriptSortOrder) => {
|
|
878
|
+
updateTranscriptSearch({ sort });
|
|
879
|
+
};
|
|
880
|
+
|
|
881
|
+
const jumpToTranscriptSearchResult = (index: number) => {
|
|
882
|
+
if (transcriptSearchResults.length === 0) {
|
|
883
|
+
return;
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
const wrappedIndex =
|
|
887
|
+
((index % transcriptSearchResults.length) + transcriptSearchResults.length) %
|
|
888
|
+
transcriptSearchResults.length;
|
|
889
|
+
const result = transcriptSearchResults[wrappedIndex]!;
|
|
890
|
+
|
|
891
|
+
setActiveSearchResultIndex(wrappedIndex);
|
|
892
|
+
setActiveTranscriptEventKey(result.eventKey);
|
|
893
|
+
setActiveEventJumpSignal((current) => current + 1);
|
|
894
|
+
};
|
|
399
895
|
|
|
400
896
|
const exportThreadMutation = useMutation({
|
|
401
897
|
mutationFn: async (options: {
|
|
@@ -458,6 +954,9 @@ function ThreadDetailPage() {
|
|
|
458
954
|
},
|
|
459
955
|
}),
|
|
460
956
|
onSuccess: async () => {
|
|
957
|
+
queryClient.removeQueries({ queryKey: ['thread', snapshot.thread.id] });
|
|
958
|
+
queryClient.removeQueries({ queryKey: ['thread-transcript-preview', snapshot.thread.id] });
|
|
959
|
+
queryClient.removeQueries({ queryKey: ['thread-transcript', snapshot.thread.id] });
|
|
461
960
|
await Promise.all([
|
|
462
961
|
queryClient.invalidateQueries({ queryKey: ['analytics'] }),
|
|
463
962
|
queryClient.invalidateQueries({ queryKey: ['dashboard'] }),
|
|
@@ -502,13 +1001,10 @@ function ThreadDetailPage() {
|
|
|
502
1001
|
params: { project: snapshot.project },
|
|
503
1002
|
to: '/codex/$project',
|
|
504
1003
|
},
|
|
505
|
-
{ label: snapshot.thread.title },
|
|
1004
|
+
{ label: snapshot.thread.title, title: snapshot.thread.title, truncate: true },
|
|
506
1005
|
]}
|
|
507
1006
|
/>
|
|
508
1007
|
}
|
|
509
|
-
eyebrow={snapshot.project}
|
|
510
|
-
subtitle={snapshot.thread.preview}
|
|
511
|
-
title={snapshot.thread.title}
|
|
512
1008
|
/>
|
|
513
1009
|
|
|
514
1010
|
<div className="grid gap-3 md:grid-cols-2 xl:grid-cols-4">
|
|
@@ -529,10 +1025,13 @@ function ThreadDetailPage() {
|
|
|
529
1025
|
</div>
|
|
530
1026
|
|
|
531
1027
|
<Tabs className="space-y-4" defaultValue="transcript">
|
|
532
|
-
<TabsList className="grid w-
|
|
1028
|
+
<TabsList className="grid w-full grid-cols-4 rounded-full border border-[var(--border)] bg-[var(--panel)] p-1 sm:w-fit sm:min-w-[30rem]">
|
|
533
1029
|
<TabsTrigger className="rounded-full px-5 text-sm" value="transcript">
|
|
534
1030
|
Transcript
|
|
535
1031
|
</TabsTrigger>
|
|
1032
|
+
<TabsTrigger className="rounded-full px-5 text-sm" value="tools">
|
|
1033
|
+
Tools
|
|
1034
|
+
</TabsTrigger>
|
|
536
1035
|
<TabsTrigger className="rounded-full px-5 text-sm" value="metadata">
|
|
537
1036
|
Metadata
|
|
538
1037
|
</TabsTrigger>
|
|
@@ -541,46 +1040,42 @@ function ThreadDetailPage() {
|
|
|
541
1040
|
</TabsTrigger>
|
|
542
1041
|
</TabsList>
|
|
543
1042
|
|
|
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
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
{
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
{transcriptQuery.error instanceof Error ? transcriptQuery.error.message : 'Unknown error'}
|
|
581
|
-
</p>
|
|
582
|
-
) : null}
|
|
583
|
-
</TabsContent>
|
|
1043
|
+
<ThreadTranscriptTab
|
|
1044
|
+
activeEventJumpSignal={activeEventJumpSignal}
|
|
1045
|
+
activeSearchResultIndex={activeSearchResultIndex}
|
|
1046
|
+
activeTranscriptEventKey={activeTranscriptEventKey}
|
|
1047
|
+
fullTranscriptLoaded={fullTranscriptLoaded}
|
|
1048
|
+
loadError={
|
|
1049
|
+
transcriptQuery.isError
|
|
1050
|
+
? transcriptQuery.error
|
|
1051
|
+
: transcriptPreviewQuery.isError
|
|
1052
|
+
? transcriptPreviewQuery.error
|
|
1053
|
+
: null
|
|
1054
|
+
}
|
|
1055
|
+
loadingFullTranscript={transcriptQuery.isFetching}
|
|
1056
|
+
searchInput={transcriptSearchInput}
|
|
1057
|
+
searchResults={transcriptSearchResults}
|
|
1058
|
+
showRawJson={showRawJson}
|
|
1059
|
+
sortOrder={sortOrder}
|
|
1060
|
+
snapshot={snapshot}
|
|
1061
|
+
transcript={transcript}
|
|
1062
|
+
transcriptFilters={transcriptFilters}
|
|
1063
|
+
onJumpToSearchResult={jumpToTranscriptSearchResult}
|
|
1064
|
+
onLoadFullThread={() => setShouldLoadTranscript(true)}
|
|
1065
|
+
onSearchInputChange={updateTranscriptSearchInput}
|
|
1066
|
+
onSortOrderChange={updateSortOrder}
|
|
1067
|
+
onTranscriptFilterChange={updateTranscriptFilter}
|
|
1068
|
+
/>
|
|
1069
|
+
|
|
1070
|
+
<ThreadToolsTab
|
|
1071
|
+
loadingTranscript={transcriptQuery.isFetching}
|
|
1072
|
+
showRawJson={showRawJson}
|
|
1073
|
+
snapshot={viewSnapshot}
|
|
1074
|
+
sortOrder={sortOrder}
|
|
1075
|
+
transcript={transcript}
|
|
1076
|
+
onLoadTranscript={() => setShouldLoadTranscript(true)}
|
|
1077
|
+
onSortOrderChange={updateSortOrder}
|
|
1078
|
+
/>
|
|
584
1079
|
|
|
585
1080
|
<TabsContent value="metadata">
|
|
586
1081
|
<ThreadMetadataPanels snapshot={viewSnapshot} />
|
|
@@ -595,11 +1090,17 @@ function ThreadDetailPage() {
|
|
|
595
1090
|
confirmLabel={deleteThreadMutation.isPending ? 'Deleting...' : 'Delete thread'}
|
|
596
1091
|
defaultDeleteSessionFiles
|
|
597
1092
|
description="Delete this thread from the Codex database. Enable Delete Session files if you also want to remove the rollout JSONL from disk."
|
|
1093
|
+
errorMessage={getMutationErrorMessage(deleteThreadMutation.error, 'Thread delete failed')}
|
|
598
1094
|
open={deleteOpen}
|
|
599
1095
|
showDeleteSessionFilesOption
|
|
600
1096
|
title="Delete this thread from Codex DB?"
|
|
601
1097
|
onConfirm={({ deleteSessionFiles }) => deleteThreadMutation.mutate({ deleteSessionFiles })}
|
|
602
|
-
onOpenChange={
|
|
1098
|
+
onOpenChange={(open) => {
|
|
1099
|
+
setDeleteOpen(open);
|
|
1100
|
+
if (!open) {
|
|
1101
|
+
deleteThreadMutation.reset();
|
|
1102
|
+
}
|
|
1103
|
+
}}
|
|
603
1104
|
/>
|
|
604
1105
|
|
|
605
1106
|
<ExportDialog
|
|
@@ -612,8 +1113,22 @@ function ThreadDetailPage() {
|
|
|
612
1113
|
exportThreadMutation.mutate(options);
|
|
613
1114
|
}
|
|
614
1115
|
}}
|
|
615
|
-
onOpenChange={
|
|
1116
|
+
onOpenChange={(open) => {
|
|
1117
|
+
setExportOpen(open);
|
|
1118
|
+
if (!open) {
|
|
1119
|
+
exportThreadMutation.reset();
|
|
1120
|
+
}
|
|
1121
|
+
}}
|
|
616
1122
|
/>
|
|
617
1123
|
</div>
|
|
618
1124
|
);
|
|
619
1125
|
}
|
|
1126
|
+
|
|
1127
|
+
function ThreadDetailPage() {
|
|
1128
|
+
const { threadId } = Route.useParams();
|
|
1129
|
+
return (
|
|
1130
|
+
<RouteStateResetBoundary routeKey={threadId}>
|
|
1131
|
+
<ThreadDetailPageContent />
|
|
1132
|
+
</RouteStateResetBoundary>
|
|
1133
|
+
);
|
|
1134
|
+
}
|