spiracha 1.0.0 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. package/AGENTS.md +31 -1
  2. package/README.md +61 -7
  3. package/apps/ui/AGENTS.md +70 -0
  4. package/apps/ui/README.md +72 -0
  5. package/apps/ui/dist/client/assets/_threadId-CAIeH5mq.js +1 -0
  6. package/apps/ui/dist/client/assets/analytics-CqWZmyV6.js +1 -0
  7. package/apps/ui/dist/client/assets/checkbox-DXM4lkJq.js +1 -0
  8. package/apps/ui/dist/client/assets/data-table-DnPYMPCD.js +4 -0
  9. package/apps/ui/dist/client/assets/delete-confirm-dialog-CcZaRX33.js +11 -0
  10. package/apps/ui/dist/client/assets/download-DOwxk-cG.js +1 -0
  11. package/apps/ui/dist/client/assets/es2015-Bm0kEzx2.js +41 -0
  12. package/apps/ui/dist/client/assets/formatters-C12LmYaa.js +1 -0
  13. package/apps/ui/dist/client/assets/index-DdJ7ahIt.js +22 -0
  14. package/apps/ui/dist/client/assets/input-CEsI7EpI.js +1 -0
  15. package/apps/ui/dist/client/assets/metric-card-9jwBF7rG.js +1 -0
  16. package/apps/ui/dist/client/assets/page-header-Dr_h1CVv.js +1 -0
  17. package/apps/ui/dist/client/assets/projects._project-uyNGnpjH.js +1 -0
  18. package/apps/ui/dist/client/assets/projects._project-zoM8d2nH.js +1 -0
  19. package/apps/ui/dist/client/assets/projects.index-D1CWVN-O.js +1 -0
  20. package/apps/ui/dist/client/assets/projects.index-DukMuny6.js +1 -0
  21. package/apps/ui/dist/client/assets/routes-Gr2Wwh83.js +1 -0
  22. package/apps/ui/dist/client/assets/select-CFim44gT.js +1 -0
  23. package/apps/ui/dist/client/assets/settings-DqhyDxo2.js +1 -0
  24. package/apps/ui/dist/client/assets/styles-CMrP9Jb4.css +1 -0
  25. package/apps/ui/dist/client/assets/threads._threadId-DT75NiBa.js +1 -0
  26. package/apps/ui/dist/client/assets/threads._threadId-Df5VXIuZ.js +7 -0
  27. package/apps/ui/dist/client/favicon.ico +0 -0
  28. package/apps/ui/dist/client/logo192.png +0 -0
  29. package/apps/ui/dist/client/logo512.png +0 -0
  30. package/apps/ui/dist/client/manifest.json +25 -0
  31. package/apps/ui/dist/client/robots.txt +3 -0
  32. package/apps/ui/dist/server/assets/__23tanstack-start-plugin-adapters-BzCA6dXo.js +5 -0
  33. package/apps/ui/dist/server/assets/_tanstack-start-manifest_v-C0V305Nt.js +99 -0
  34. package/apps/ui/dist/server/assets/_threadId-B6SrBR9E.js +6 -0
  35. package/apps/ui/dist/server/assets/analytics-BMxW_bZL.js +139 -0
  36. package/apps/ui/dist/server/assets/button-CmTDnzOn.js +46 -0
  37. package/apps/ui/dist/server/assets/checkbox-C0hovF41.js +19 -0
  38. package/apps/ui/dist/server/assets/codex-queries-CAF6HYiG.js +109 -0
  39. package/apps/ui/dist/server/assets/codex-server-BFZq2Y2O.js +2062 -0
  40. package/apps/ui/dist/server/assets/data-table-Cdct823O.js +189 -0
  41. package/apps/ui/dist/server/assets/delete-confirm-dialog-CWqcTXTF.js +139 -0
  42. package/apps/ui/dist/server/assets/download-C5rkk_Bo.js +289 -0
  43. package/apps/ui/dist/server/assets/formatters-FJaGZgJk.js +91 -0
  44. package/apps/ui/dist/server/assets/input-B4tEzctc.js +46 -0
  45. package/apps/ui/dist/server/assets/loading-panel-DbLdvjtR.js +27 -0
  46. package/apps/ui/dist/server/assets/metric-card-ByEeLu0r.js +23 -0
  47. package/apps/ui/dist/server/assets/model-label-B1NWGc65.js +13 -0
  48. package/apps/ui/dist/server/assets/page-header-CxdZM86z.js +25 -0
  49. package/apps/ui/dist/server/assets/path-transforms-DL2IwtYd.js +31 -0
  50. package/apps/ui/dist/server/assets/projects._project-CJ7l0ynC.js +18 -0
  51. package/apps/ui/dist/server/assets/projects._project-CLSohrBp.js +26 -0
  52. package/apps/ui/dist/server/assets/projects._project-CcJLp_A8.js +337 -0
  53. package/apps/ui/dist/server/assets/projects.index-CaplpeMy.js +26 -0
  54. package/apps/ui/dist/server/assets/projects.index-srtogpuF.js +172 -0
  55. package/apps/ui/dist/server/assets/router-C_w-haH6.js +307 -0
  56. package/apps/ui/dist/server/assets/routes-BhbxvJE7.js +34 -0
  57. package/apps/ui/dist/server/assets/routes-CPe-ppmC.js +169 -0
  58. package/apps/ui/dist/server/assets/select-GW76p-ld.js +76 -0
  59. package/apps/ui/dist/server/assets/settings-MvWDgc1u.js +100 -0
  60. package/apps/ui/dist/server/assets/settings-store-DpEJEQ7M.js +52 -0
  61. package/apps/ui/dist/server/assets/sqlite-error-LZDrnxdd.js +13 -0
  62. package/apps/ui/dist/server/assets/start-HeKLHD9b.js +4 -0
  63. package/apps/ui/dist/server/assets/threads._threadId-BSSK4nkI.js +26 -0
  64. package/apps/ui/dist/server/assets/threads._threadId-Ba7vv6-K.js +18 -0
  65. package/apps/ui/dist/server/assets/threads._threadId-euyNckhj.js +1059 -0
  66. package/apps/ui/dist/server/assets/utils-C_uf36nf.js +8 -0
  67. package/apps/ui/dist/server/server.js +5678 -0
  68. package/package.json +53 -7
  69. package/src/export-chats.ts +4 -18
  70. package/src/lib/claude-exporter.ts +1 -1
  71. package/src/lib/codex-analytics.ts +100 -0
  72. package/src/lib/codex-browser-db.ts +605 -0
  73. package/src/lib/codex-browser-export.ts +429 -0
  74. package/src/lib/codex-browser-types.ts +224 -0
  75. package/src/lib/codex-exporter-cli.ts +6 -1
  76. package/src/lib/codex-exporter-db.ts +19 -20
  77. package/src/lib/codex-exporter-transcript.ts +158 -34
  78. package/src/lib/codex-exporter-types.ts +8 -0
  79. package/src/lib/codex-thread-cache.ts +58 -0
  80. package/src/lib/codex-thread-parser.ts +604 -0
  81. package/src/lib/interactive-cli.ts +10 -25
  82. package/src/lib/model-label.ts +24 -0
  83. package/src/lib/native-open.ts +54 -0
  84. package/src/lib/path-transforms.ts +46 -0
  85. package/src/lib/shared.ts +15 -1
  86. package/src/lib/sqlite-error.ts +14 -0
  87. package/src/lib/sqlite-retry.ts +53 -0
  88. package/src/lib/ui-cache.ts +96 -0
  89. package/src/lib/ui-export-files.ts +77 -0
  90. package/src/mcp-server.ts +1 -0
  91. package/src/spiracha.ts +16 -4
  92. package/src/ui-cli.ts +310 -0
@@ -0,0 +1,1059 @@
1
+ import { t as applyPathTransforms$1 } from "./path-transforms-DL2IwtYd.js";
2
+ import { t as cn } from "./utils-C_uf36nf.js";
3
+ import { t as Button } from "./button-CmTDnzOn.js";
4
+ import { n as useSettings } from "./settings-store-DpEJEQ7M.js";
5
+ import { a as threadSnapshotQueryOptions, c as deleteThreadFn, o as threadTranscriptQueryOptions, u as exportThreadFn } from "./codex-queries-CAF6HYiG.js";
6
+ import { t as Route } from "./threads._threadId-Ba7vv6-K.js";
7
+ import { t as Checkbox$1 } from "./checkbox-C0hovF41.js";
8
+ import { t as MetricCard } from "./metric-card-ByEeLu0r.js";
9
+ import { t as PageHeader } from "./page-header-CxdZM86z.js";
10
+ import { a as formatModelLabel, i as formatList, n as formatBytes, r as formatDateTime, s as formatTokens, t as formatBooleanLabel } from "./formatters-FJaGZgJk.js";
11
+ import { t as DeleteConfirmDialog } from "./delete-confirm-dialog-CWqcTXTF.js";
12
+ import { n as downloadUrlFile, r as ExportDialog, t as downloadTextFile } from "./download-C5rkk_Bo.js";
13
+ import { useEffect, useRef, useState } from "react";
14
+ import { Link, useNavigate } from "@tanstack/react-router";
15
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
16
+ import { useMutation, useQuery, useQueryClient, useSuspenseQuery } from "@tanstack/react-query";
17
+ import { Check, Copy, Download, Trash2 } from "lucide-react";
18
+ import { cva } from "class-variance-authority";
19
+ import { Slot, Tabs } from "radix-ui";
20
+ import { useVirtualizer } from "@tanstack/react-virtual";
21
+ //#region src/components/json-panel.tsx
22
+ function JsonPanel({ title, value }) {
23
+ return /* @__PURE__ */ jsxs("section", {
24
+ className: "rounded-[1.6rem] border border-[var(--border)] bg-[var(--panel)] p-5 shadow-[var(--panel-shadow)]",
25
+ children: [/* @__PURE__ */ jsx("h3", {
26
+ className: "font-semibold text-[var(--muted-foreground)] text-sm uppercase tracking-[0.18em]",
27
+ children: title
28
+ }), /* @__PURE__ */ jsx("pre", {
29
+ className: "mt-4 overflow-x-auto rounded-2xl border border-[var(--border)] bg-[var(--code-background)] p-4 text-[var(--code-foreground)] text-xs leading-5",
30
+ children: JSON.stringify(value, null, 2)
31
+ })]
32
+ });
33
+ }
34
+ //#endregion
35
+ //#region src/components/metadata-section.tsx
36
+ function MetadataSection({ items, title }) {
37
+ return /* @__PURE__ */ jsxs("section", {
38
+ className: "rounded-[1.6rem] border border-[var(--border)] bg-[var(--panel)] p-5 shadow-[var(--panel-shadow)]",
39
+ children: [/* @__PURE__ */ jsx("h3", {
40
+ className: "font-semibold text-[var(--muted-foreground)] text-sm uppercase tracking-[0.18em]",
41
+ children: title
42
+ }), /* @__PURE__ */ jsx("dl", {
43
+ className: "mt-4 space-y-3",
44
+ children: items.map((item) => /* @__PURE__ */ jsxs("div", {
45
+ className: "grid gap-1 sm:grid-cols-[11rem_1fr] sm:items-start",
46
+ children: [/* @__PURE__ */ jsx("dt", {
47
+ className: "font-medium text-[var(--muted-foreground)] text-xs uppercase tracking-[0.14em]",
48
+ children: item.label
49
+ }), /* @__PURE__ */ jsx("dd", {
50
+ className: "min-w-0 text-sm leading-6",
51
+ children: item.value
52
+ })]
53
+ }, item.label))
54
+ })]
55
+ });
56
+ }
57
+ //#endregion
58
+ //#region src/components/ui/badge.tsx
59
+ var badgeVariants = cva("inline-flex w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-full border border-transparent px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-3", {
60
+ defaultVariants: { variant: "default" },
61
+ variants: { variant: {
62
+ default: "bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
63
+ destructive: "bg-destructive text-white focus-visible:ring-destructive/20 dark:bg-destructive/60 dark:focus-visible:ring-destructive/40 [a&]:hover:bg-destructive/90",
64
+ ghost: "[a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
65
+ link: "text-primary underline-offset-4 [a&]:hover:underline",
66
+ outline: "border-border text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
67
+ secondary: "bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90"
68
+ } }
69
+ });
70
+ function Badge({ className, variant = "default", asChild = false, ...props }) {
71
+ return /* @__PURE__ */ jsx(asChild ? Slot.Root : "span", {
72
+ "data-slot": "badge",
73
+ "data-variant": variant,
74
+ className: cn(badgeVariants({ variant }), className),
75
+ ...props
76
+ });
77
+ }
78
+ //#endregion
79
+ //#region src/lib/path-utils.ts
80
+ var applyPathTransforms = (text, settings, projectPath) => {
81
+ return applyPathTransforms$1(text, {
82
+ ...settings,
83
+ projectPath
84
+ });
85
+ };
86
+ //#endregion
87
+ //#region src/components/transcript-view.tsx
88
+ var isCommentaryMessage = (event) => event.kind === "message" && event.role === "assistant" && event.phase === "commentary";
89
+ var shouldShowEvent = (event, showToolCalls, showExtraEvents, showCommentary) => {
90
+ if (isCommentaryMessage(event) && !showCommentary) return false;
91
+ if (event.kind === "message") return !event.isHiddenByDefault || showExtraEvents;
92
+ if (event.kind === "tool_call" || event.kind === "tool_output") return showToolCalls;
93
+ return showExtraEvents;
94
+ };
95
+ var getEventTone = (event) => {
96
+ if (event.kind === "message" && event.role === "assistant") return "border-[var(--accent)]/40 bg-[var(--panel)]";
97
+ if (event.kind === "message" && event.role === "user") return "border-[var(--border)] bg-[var(--panel-secondary)]";
98
+ if (event.kind === "tool_call" || event.kind === "tool_output") return "border-[var(--border)] bg-[var(--code-background)]";
99
+ return "border-[var(--border)] bg-[var(--panel-secondary)]/80";
100
+ };
101
+ var getMessageTitle = (event, assistantModel) => {
102
+ const modelLabel = formatModelLabel(event.model ?? assistantModel);
103
+ if (event.variant === "agent_message") return event.role === "assistant" ? modelLabel : "Assistant update";
104
+ return event.role === "assistant" ? modelLabel : "User";
105
+ };
106
+ var getNonMessageTitle = (event) => {
107
+ switch (event.kind) {
108
+ case "tool_call": return `Tool call: ${event.name}`;
109
+ case "tool_output": return "Tool output";
110
+ case "task_started": return "Task started";
111
+ case "task_complete": return "Task complete";
112
+ case "token_count": return "Token update";
113
+ case "reasoning": return "Reasoning";
114
+ case "web_search": return "Web search";
115
+ }
116
+ };
117
+ var getEventTitle = (event, assistantModel) => event.kind === "message" ? getMessageTitle(event, assistantModel) : getNonMessageTitle(event);
118
+ var copyToClipboard = async (text) => {
119
+ if (typeof navigator !== "undefined" && navigator.clipboard) try {
120
+ await navigator.clipboard.writeText(text);
121
+ return true;
122
+ } catch {}
123
+ return false;
124
+ };
125
+ var getEventMarkdownBody = (event, transform) => {
126
+ switch (event.kind) {
127
+ case "message": return transform(event.text || "No text content");
128
+ case "tool_call": return [event.command ?? event.name, event.workdir].filter((value) => Boolean(value)).map(transform).join("\n\n");
129
+ case "tool_output": return transform(event.summary || event.outputText || "");
130
+ case "task_started": return `Context window: ${event.modelContextWindow ?? "n/a"}\n\nCollaboration mode: ${event.collaborationModeKind ?? "n/a"}`;
131
+ case "task_complete": return `Duration: ${event.durationMs ?? "n/a"} ms\n\nFirst token: ${event.timeToFirstTokenMs ?? "n/a"} ms`;
132
+ case "token_count": return JSON.stringify(event.rateLimits, null, 2);
133
+ case "reasoning": return event.summary.join(" ") || "Reasoning content is not directly available.";
134
+ case "web_search": return [`Phase: ${event.phase}${event.status ? ` · ${event.status}` : ""}`, event.query ? transform(event.query) : null].filter(Boolean).join("\n\n");
135
+ }
136
+ };
137
+ var getEventMarkdown = (event, assistantModel, transform) => `## ${getEventTitle(event, assistantModel)}\n\n${getEventMarkdownBody(event, transform)}`;
138
+ var renderMessageBody = (event, t) => /* @__PURE__ */ jsx("p", {
139
+ className: "min-w-0 whitespace-pre-wrap break-words text-sm leading-6 [overflow-wrap:anywhere]",
140
+ children: t(event.text || "No text content")
141
+ });
142
+ var renderToolCallBody = (event, t) => /* @__PURE__ */ jsxs("div", {
143
+ className: "space-y-2 text-sm",
144
+ children: [/* @__PURE__ */ jsx("p", {
145
+ className: "min-w-0 break-words font-medium [overflow-wrap:anywhere]",
146
+ children: t(event.command ?? event.name)
147
+ }), event.workdir ? /* @__PURE__ */ jsx("p", {
148
+ className: "font-mono text-[var(--muted-foreground)] text-xs",
149
+ children: t(event.workdir)
150
+ }) : null]
151
+ });
152
+ var renderToolOutputBody = (event, t) => /* @__PURE__ */ jsx("p", {
153
+ className: "min-w-0 whitespace-pre-wrap break-words text-sm leading-6 [overflow-wrap:anywhere]",
154
+ children: t(event.summary || event.outputText || "")
155
+ });
156
+ var renderTaskStartedBody = (event) => /* @__PURE__ */ jsxs("div", {
157
+ className: "text-[var(--muted-foreground)] text-sm",
158
+ children: [
159
+ "Context window: ",
160
+ event.modelContextWindow ?? "n/a",
161
+ " · Collaboration mode: ",
162
+ event.collaborationModeKind ?? "n/a"
163
+ ]
164
+ });
165
+ var renderTaskCompleteBody = (event) => /* @__PURE__ */ jsxs("div", {
166
+ className: "text-[var(--muted-foreground)] text-sm",
167
+ children: [
168
+ "Duration: ",
169
+ event.durationMs ?? "n/a",
170
+ " ms · First token: ",
171
+ event.timeToFirstTokenMs ?? "n/a",
172
+ " ms"
173
+ ]
174
+ });
175
+ var renderTokenCountBody = (event) => /* @__PURE__ */ jsx("pre", {
176
+ className: "overflow-x-auto text-xs leading-5",
177
+ children: JSON.stringify(event.rateLimits, null, 2)
178
+ });
179
+ var renderReasoningBody = (event) => /* @__PURE__ */ jsxs("div", {
180
+ className: "space-y-2 text-sm",
181
+ children: [/* @__PURE__ */ jsx("p", { children: event.summary.join(" ") || "Reasoning content is not directly available." }), event.hasEncryptedContent ? /* @__PURE__ */ jsx("p", {
182
+ className: "text-[var(--muted-foreground)] text-xs",
183
+ children: "Encrypted reasoning payload captured."
184
+ }) : null]
185
+ });
186
+ var renderWebSearchBody = (event) => /* @__PURE__ */ jsxs("div", {
187
+ className: "space-y-2 text-sm",
188
+ children: [/* @__PURE__ */ jsxs("p", { children: [
189
+ "Phase: ",
190
+ event.phase,
191
+ event.status ? ` · ${event.status}` : ""
192
+ ] }), event.query ? /* @__PURE__ */ jsx("p", {
193
+ className: "text-[var(--muted-foreground)]",
194
+ children: event.query
195
+ }) : null]
196
+ });
197
+ var renderEventBody = (event, t) => {
198
+ switch (event.kind) {
199
+ case "message": return renderMessageBody(event, t);
200
+ case "tool_call": return renderToolCallBody(event, t);
201
+ case "tool_output": return renderToolOutputBody(event, t);
202
+ case "task_started": return renderTaskStartedBody(event);
203
+ case "task_complete": return renderTaskCompleteBody(event);
204
+ case "token_count": return renderTokenCountBody(event);
205
+ case "reasoning": return renderReasoningBody(event);
206
+ case "web_search": return renderWebSearchBody(event);
207
+ }
208
+ };
209
+ function TranscriptEventCard({ assistantModel, copied, event, isSelected, showRawJson, transform, onCopy, onSelectionChange }) {
210
+ return /* @__PURE__ */ jsxs("article", {
211
+ className: cn("min-w-0 overflow-hidden rounded-xl border p-3.5 shadow-[var(--panel-shadow)]", isSelected && "ring-2 ring-[var(--accent)]/35", getEventTone(event)),
212
+ children: [
213
+ /* @__PURE__ */ jsxs("div", {
214
+ className: "flex flex-wrap items-start justify-between gap-2",
215
+ children: [/* @__PURE__ */ jsxs("div", {
216
+ className: "flex min-w-0 flex-1 flex-wrap items-center gap-2",
217
+ children: [
218
+ /* @__PURE__ */ jsx(Checkbox$1, {
219
+ "aria-label": `Select ${getEventTitle(event, assistantModel)}`,
220
+ checked: isSelected,
221
+ onCheckedChange: (checked) => onSelectionChange(event, checked === true)
222
+ }),
223
+ /* @__PURE__ */ jsx("h3", {
224
+ className: "min-w-0 break-words font-semibold text-sm [overflow-wrap:anywhere]",
225
+ children: getEventTitle(event, assistantModel)
226
+ }),
227
+ event.kind === "message" && event.phase ? /* @__PURE__ */ jsx(Badge, {
228
+ variant: "outline",
229
+ children: event.phase
230
+ }) : null,
231
+ event.kind !== "message" ? /* @__PURE__ */ jsx(Badge, {
232
+ variant: "outline",
233
+ children: event.kind.replaceAll("_", " ")
234
+ }) : null
235
+ ]
236
+ }), event.timestamp ? /* @__PURE__ */ jsx("p", {
237
+ className: "shrink-0 text-[var(--muted-foreground)] text-xs",
238
+ suppressHydrationWarning: true,
239
+ children: formatDateTime(event.timestamp)
240
+ }) : null]
241
+ }),
242
+ /* @__PURE__ */ jsx("div", {
243
+ className: "mt-2.5 min-w-0",
244
+ children: renderEventBody(event, transform)
245
+ }),
246
+ /* @__PURE__ */ jsx("div", {
247
+ className: "mt-3 flex justify-end",
248
+ children: /* @__PURE__ */ jsx(Button, {
249
+ "aria-label": "Copy message",
250
+ className: "text-[var(--muted-foreground)] hover:bg-[var(--panel-secondary)] hover:text-[var(--foreground)]",
251
+ size: "icon-xs",
252
+ variant: "ghost",
253
+ onClick: () => onCopy(event),
254
+ children: copied ? /* @__PURE__ */ jsx(Check, { className: "text-[var(--accent)]" }) : /* @__PURE__ */ jsx(Copy, {})
255
+ })
256
+ }),
257
+ showRawJson ? /* @__PURE__ */ jsx("pre", {
258
+ className: "mt-3 overflow-x-auto rounded-lg border border-[var(--border)] bg-[var(--code-background)] p-3 text-[var(--code-foreground)] text-xs leading-5",
259
+ children: JSON.stringify(event.raw, null, 2)
260
+ }) : null
261
+ ]
262
+ });
263
+ }
264
+ function TranscriptView({ assistantModel, events, projectPath, showCommentary, showExtraEvents, showRawJson, showToolCalls }) {
265
+ const { settings } = useSettings();
266
+ const visibleEvents = events.filter((event) => shouldShowEvent(event, showToolCalls, showExtraEvents, showCommentary));
267
+ const [copiedEventKeys, setCopiedEventKeys] = useState([]);
268
+ const [copiedSelection, setCopiedSelection] = useState(false);
269
+ const [copyErrorMessage, setCopyErrorMessage] = useState(null);
270
+ const [selectedEventKeys, setSelectedEventKeys] = useState([]);
271
+ const parentRef = useRef(null);
272
+ const timeoutIdsRef = useRef([]);
273
+ const useVirtualList = visibleEvents.length > 40;
274
+ const getEventKey = (event) => {
275
+ if (event.kind === "tool_call") return `${event.kind}-${event.sequence}-${event.callId ?? event.timestamp ?? event.name}`;
276
+ if (event.kind === "tool_output") return `${event.kind}-${event.sequence}-${event.callId ?? event.timestamp ?? "output"}`;
277
+ if (event.kind === "message") return `${event.kind}-${event.sequence}-${event.variant}-${event.timestamp ?? event.role}`;
278
+ return `${event.kind}-${event.sequence}-${event.timestamp ?? "event"}`;
279
+ };
280
+ const transform = (text) => applyPathTransforms(text, settings, projectPath);
281
+ const visibleEventKeys = visibleEvents.map(getEventKey);
282
+ const visibleEventKeySet = new Set(visibleEventKeys);
283
+ const selectedEventKeySet = new Set(selectedEventKeys);
284
+ const virtualizer = useVirtualizer({
285
+ count: useVirtualList ? visibleEvents.length : 0,
286
+ estimateSize: () => 160,
287
+ getItemKey: (index) => getEventKey(visibleEvents[index]) ?? `event-${index}`,
288
+ getScrollElement: () => parentRef.current,
289
+ measureElement: (element) => element.getBoundingClientRect().height,
290
+ overscan: 8
291
+ });
292
+ useEffect(() => {
293
+ return () => {
294
+ for (const timeoutId of timeoutIdsRef.current) window.clearTimeout(timeoutId);
295
+ timeoutIdsRef.current = [];
296
+ };
297
+ }, []);
298
+ useEffect(() => {
299
+ setSelectedEventKeys((current) => {
300
+ const next = current.filter((key) => visibleEventKeySet.has(key));
301
+ return next.length === current.length ? current : next;
302
+ });
303
+ }, [visibleEventKeySet]);
304
+ const scheduleTimeout = (callback, delayMs) => {
305
+ const timeoutId = window.setTimeout(() => {
306
+ timeoutIdsRef.current = timeoutIdsRef.current.filter((entry) => entry !== timeoutId);
307
+ callback();
308
+ }, delayMs);
309
+ timeoutIdsRef.current.push(timeoutId);
310
+ };
311
+ const showCopyFailure = () => {
312
+ setCopyErrorMessage("Copy failed");
313
+ scheduleTimeout(() => {
314
+ setCopyErrorMessage((current) => current === "Copy failed" ? null : current);
315
+ }, 1500);
316
+ };
317
+ const visibleSelectedKeys = selectedEventKeys.filter((key) => visibleEventKeySet.has(key));
318
+ const allVisibleSelected = visibleEvents.length > 0 && visibleSelectedKeys.length === visibleEvents.length;
319
+ const handleSelectionChange = (event, checked) => {
320
+ const key = getEventKey(event);
321
+ setSelectedEventKeys((current) => checked ? current.includes(key) ? current : [...current, key] : current.filter((entry) => entry !== key));
322
+ };
323
+ const handleToggleAllVisible = (checked) => {
324
+ setSelectedEventKeys((current) => checked ? [...new Set([...current.filter((key) => !visibleEventKeySet.has(key)), ...visibleEventKeys])] : current.filter((key) => !visibleEventKeySet.has(key)));
325
+ };
326
+ const handleCopyEvent = async (event) => {
327
+ if (!await copyToClipboard(getEventMarkdown(event, assistantModel, transform))) {
328
+ showCopyFailure();
329
+ return;
330
+ }
331
+ const key = getEventKey(event);
332
+ setCopiedEventKeys((current) => [...new Set([...current, key])]);
333
+ scheduleTimeout(() => {
334
+ setCopiedEventKeys((current) => current.filter((entry) => entry !== key));
335
+ }, 1500);
336
+ };
337
+ const handleCopySelected = async () => {
338
+ const selectedEvents = visibleEvents.filter((event) => visibleSelectedKeys.includes(getEventKey(event)));
339
+ if (selectedEvents.length === 0) return;
340
+ if (!await copyToClipboard(selectedEvents.map((event) => getEventMarkdown(event, assistantModel, transform)).join("\n\n"))) {
341
+ showCopyFailure();
342
+ return;
343
+ }
344
+ setCopiedSelection(true);
345
+ scheduleTimeout(() => {
346
+ setCopiedSelection(false);
347
+ }, 1500);
348
+ };
349
+ if (useVirtualList) return /* @__PURE__ */ jsxs("div", {
350
+ ref: parentRef,
351
+ className: "h-[70vh] overflow-auto rounded-xl border border-[var(--border)] bg-[var(--panel)] p-3",
352
+ children: [/* @__PURE__ */ jsxs("div", {
353
+ className: "sticky top-0 z-10 mb-3 flex items-center justify-between rounded-xl border border-[var(--border)] bg-[var(--panel)]/95 px-3 py-2 backdrop-blur",
354
+ children: [/* @__PURE__ */ jsxs("div", {
355
+ className: "flex items-center gap-2",
356
+ children: [
357
+ /* @__PURE__ */ jsx(Checkbox$1, {
358
+ "aria-label": "Select visible messages",
359
+ checked: allVisibleSelected,
360
+ onCheckedChange: (checked) => handleToggleAllVisible(checked === true)
361
+ }),
362
+ /* @__PURE__ */ jsxs("span", {
363
+ className: "text-[var(--muted-foreground)] text-sm",
364
+ children: [visibleSelectedKeys.length, " selected"]
365
+ }),
366
+ copyErrorMessage ? /* @__PURE__ */ jsx("span", {
367
+ className: "text-[var(--destructive)] text-sm",
368
+ children: copyErrorMessage
369
+ }) : null
370
+ ]
371
+ }), /* @__PURE__ */ jsxs(Button, {
372
+ "aria-label": "Copy selected messages",
373
+ className: "hover:bg-[var(--panel-secondary)] hover:text-[var(--foreground)]",
374
+ disabled: visibleSelectedKeys.length === 0,
375
+ size: "sm",
376
+ variant: "outline",
377
+ onClick: () => void handleCopySelected(),
378
+ children: [copiedSelection ? /* @__PURE__ */ jsx(Check, { className: "text-[var(--accent)]" }) : /* @__PURE__ */ jsx(Copy, {}), copiedSelection ? "Copied" : "Copy"]
379
+ })]
380
+ }), /* @__PURE__ */ jsx("div", {
381
+ className: "relative w-full",
382
+ style: { height: `${virtualizer.getTotalSize()}px` },
383
+ children: virtualizer.getVirtualItems().map((item) => /* @__PURE__ */ jsx("div", {
384
+ ref: virtualizer.measureElement,
385
+ "data-index": item.index,
386
+ className: "absolute top-0 left-0 w-full pb-3.5",
387
+ style: { transform: `translateY(${item.start}px)` },
388
+ children: /* @__PURE__ */ jsx(TranscriptEventCard, {
389
+ assistantModel,
390
+ copied: copiedEventKeys.includes(getEventKey(visibleEvents[item.index])),
391
+ event: visibleEvents[item.index],
392
+ isSelected: selectedEventKeySet.has(getEventKey(visibleEvents[item.index])),
393
+ showRawJson,
394
+ transform,
395
+ onCopy: (event) => void handleCopyEvent(event),
396
+ onSelectionChange: handleSelectionChange
397
+ })
398
+ }, item.key))
399
+ })]
400
+ });
401
+ return /* @__PURE__ */ jsxs("div", {
402
+ className: "space-y-3.5",
403
+ children: [/* @__PURE__ */ jsxs("div", {
404
+ className: "sticky top-0 z-10 flex items-center justify-between rounded-xl border border-[var(--border)] bg-[var(--panel)]/95 px-3 py-2 backdrop-blur",
405
+ children: [/* @__PURE__ */ jsxs("div", {
406
+ className: "flex items-center gap-2",
407
+ children: [
408
+ /* @__PURE__ */ jsx(Checkbox$1, {
409
+ "aria-label": "Select visible messages",
410
+ checked: allVisibleSelected,
411
+ onCheckedChange: (checked) => handleToggleAllVisible(checked === true)
412
+ }),
413
+ /* @__PURE__ */ jsxs("span", {
414
+ className: "text-[var(--muted-foreground)] text-sm",
415
+ children: [visibleSelectedKeys.length, " selected"]
416
+ }),
417
+ copyErrorMessage ? /* @__PURE__ */ jsx("span", {
418
+ className: "text-[var(--destructive)] text-sm",
419
+ children: copyErrorMessage
420
+ }) : null
421
+ ]
422
+ }), /* @__PURE__ */ jsxs(Button, {
423
+ "aria-label": "Copy selected messages",
424
+ className: "hover:bg-[var(--panel-secondary)] hover:text-[var(--foreground)]",
425
+ disabled: visibleSelectedKeys.length === 0,
426
+ size: "sm",
427
+ variant: "outline",
428
+ onClick: () => void handleCopySelected(),
429
+ children: [copiedSelection ? /* @__PURE__ */ jsx(Check, { className: "text-[var(--accent)]" }) : /* @__PURE__ */ jsx(Copy, {}), copiedSelection ? "Copied" : "Copy"]
430
+ })]
431
+ }), visibleEvents.map((event) => /* @__PURE__ */ jsx(TranscriptEventCard, {
432
+ assistantModel,
433
+ copied: copiedEventKeys.includes(getEventKey(event)),
434
+ event,
435
+ isSelected: selectedEventKeySet.has(getEventKey(event)),
436
+ showRawJson,
437
+ transform,
438
+ onCopy: (selectedEvent) => void handleCopyEvent(selectedEvent),
439
+ onSelectionChange: handleSelectionChange
440
+ }, getEventKey(event)))]
441
+ });
442
+ }
443
+ //#endregion
444
+ //#region src/components/ui/tabs.tsx
445
+ function Tabs$1({ className, orientation = "horizontal", ...props }) {
446
+ return /* @__PURE__ */ jsx(Tabs.Root, {
447
+ "data-slot": "tabs",
448
+ "data-orientation": orientation,
449
+ orientation,
450
+ className: cn("group/tabs flex gap-2 data-[orientation=horizontal]:flex-col", className),
451
+ ...props
452
+ });
453
+ }
454
+ var tabsListVariants = cva("group/tabs-list inline-flex w-fit items-center justify-center rounded-lg p-[3px] text-muted-foreground group-data-[orientation=horizontal]/tabs:min-h-11 group-data-[orientation=vertical]/tabs:h-fit group-data-[orientation=vertical]/tabs:flex-col data-[variant=line]:rounded-none", {
455
+ defaultVariants: { variant: "default" },
456
+ variants: { variant: {
457
+ default: "bg-muted",
458
+ line: "gap-1 bg-transparent"
459
+ } }
460
+ });
461
+ function TabsList({ className, variant = "default", ...props }) {
462
+ return /* @__PURE__ */ jsx(Tabs.List, {
463
+ "data-slot": "tabs-list",
464
+ "data-variant": variant,
465
+ className: cn(tabsListVariants({ variant }), className),
466
+ ...props
467
+ });
468
+ }
469
+ function TabsTrigger({ className, ...props }) {
470
+ return /* @__PURE__ */ jsx(Tabs.Trigger, {
471
+ "data-slot": "tabs-trigger",
472
+ className: cn("relative inline-flex h-auto min-h-9 min-w-0 flex-1 items-center justify-center gap-1.5 whitespace-nowrap rounded-md border border-transparent px-3 py-2 font-medium text-foreground/60 text-sm transition-all hover:text-foreground focus-visible:border-ring focus-visible:outline-1 focus-visible:outline-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 group-data-[orientation=vertical]/tabs:w-full group-data-[orientation=vertical]/tabs:justify-start group-data-[variant=default]/tabs-list:data-[state=active]:shadow-sm group-data-[variant=line]/tabs-list:data-[state=active]:shadow-none dark:text-muted-foreground dark:hover:text-foreground [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0", "group-data-[variant=line]/tabs-list:bg-transparent group-data-[variant=line]/tabs-list:data-[state=active]:bg-transparent dark:group-data-[variant=line]/tabs-list:data-[state=active]:border-transparent dark:group-data-[variant=line]/tabs-list:data-[state=active]:bg-transparent", "data-[state=active]:bg-background data-[state=active]:text-foreground dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 dark:data-[state=active]:text-foreground", "after:absolute after:bg-foreground after:opacity-0 after:transition-opacity group-data-[orientation=horizontal]/tabs:after:inset-x-0 group-data-[orientation=vertical]/tabs:after:inset-y-0 group-data-[orientation=vertical]/tabs:after:-right-1 group-data-[orientation=horizontal]/tabs:after:bottom-[-3px] group-data-[orientation=horizontal]/tabs:after:h-0.5 group-data-[orientation=vertical]/tabs:after:w-0.5 group-data-[variant=line]/tabs-list:data-[state=active]:after:opacity-100", className),
473
+ ...props
474
+ });
475
+ }
476
+ function TabsContent({ className, ...props }) {
477
+ return /* @__PURE__ */ jsx(Tabs.Content, {
478
+ "data-slot": "tabs-content",
479
+ className: cn("flex-1 outline-none", className),
480
+ ...props
481
+ });
482
+ }
483
+ //#endregion
484
+ //#region src/routes/threads.$threadId.tsx?tsr-split=component
485
+ var buildThreadItems = (snapshot) => {
486
+ return [
487
+ {
488
+ label: "Thread ID",
489
+ value: /* @__PURE__ */ jsx("span", {
490
+ "data-mono": "true",
491
+ children: snapshot.thread.id
492
+ })
493
+ },
494
+ {
495
+ label: "Project",
496
+ value: snapshot.project
497
+ },
498
+ {
499
+ label: "CWD",
500
+ value: /* @__PURE__ */ jsx("span", {
501
+ "data-mono": "true",
502
+ children: snapshot.thread.cwd
503
+ })
504
+ },
505
+ {
506
+ label: "Created",
507
+ value: /* @__PURE__ */ jsx("span", {
508
+ suppressHydrationWarning: true,
509
+ children: formatDateTime(snapshot.thread.created_at_ms ?? snapshot.thread.created_at * 1e3)
510
+ })
511
+ },
512
+ {
513
+ label: "Updated",
514
+ value: /* @__PURE__ */ jsx("span", {
515
+ suppressHydrationWarning: true,
516
+ children: formatDateTime(snapshot.thread.updated_at_ms ?? snapshot.thread.updated_at * 1e3)
517
+ })
518
+ },
519
+ {
520
+ label: "Session started",
521
+ value: /* @__PURE__ */ jsx("span", {
522
+ suppressHydrationWarning: true,
523
+ children: formatDateTime(snapshot.transcript?.sessionMeta.timestamp ?? null)
524
+ })
525
+ },
526
+ {
527
+ label: "Rollout size",
528
+ value: formatBytes(snapshot.rollout.fileSizeBytes)
529
+ },
530
+ {
531
+ label: "Archived",
532
+ value: formatBooleanLabel(Boolean(snapshot.thread.archived))
533
+ },
534
+ {
535
+ label: "Archived at",
536
+ value: /* @__PURE__ */ jsx("span", {
537
+ suppressHydrationWarning: true,
538
+ children: formatDateTime(snapshot.thread.archived_at ? snapshot.thread.archived_at * 1e3 : null)
539
+ })
540
+ }
541
+ ];
542
+ };
543
+ var buildRuntimeItems = (snapshot) => {
544
+ return [
545
+ {
546
+ label: "Source",
547
+ value: snapshot.thread.source
548
+ },
549
+ {
550
+ label: "Originator",
551
+ value: snapshot.transcript?.sessionMeta.originator ?? "n/a"
552
+ },
553
+ {
554
+ label: "Model provider",
555
+ value: snapshot.thread.model_provider
556
+ },
557
+ {
558
+ label: "Reasoning effort",
559
+ value: snapshot.thread.reasoning_effort ?? "n/a"
560
+ },
561
+ {
562
+ label: "CLI version",
563
+ value: snapshot.thread.cli_version
564
+ },
565
+ {
566
+ label: "Approval mode",
567
+ value: snapshot.thread.approval_mode
568
+ },
569
+ {
570
+ label: "Memory mode",
571
+ value: snapshot.thread.memory_mode
572
+ },
573
+ {
574
+ label: "Has user event",
575
+ value: formatBooleanLabel(Boolean(snapshot.thread.has_user_event))
576
+ }
577
+ ];
578
+ };
579
+ var buildGitItems = (snapshot) => {
580
+ return [
581
+ {
582
+ label: "Git branch",
583
+ value: snapshot.thread.git_branch ?? "n/a"
584
+ },
585
+ {
586
+ label: "Git SHA",
587
+ value: snapshot.thread.git_sha ?? "n/a"
588
+ },
589
+ {
590
+ label: "Git remote",
591
+ value: snapshot.thread.git_origin_url ?? "n/a"
592
+ },
593
+ {
594
+ label: "Agent nickname",
595
+ value: snapshot.thread.agent_nickname ?? "n/a"
596
+ },
597
+ {
598
+ label: "Agent role",
599
+ value: snapshot.thread.agent_role ?? "n/a"
600
+ },
601
+ {
602
+ label: "Agent path",
603
+ value: snapshot.thread.agent_path ?? "n/a"
604
+ }
605
+ ];
606
+ };
607
+ var buildRelationItems = (snapshot) => {
608
+ const parentThreadValue = snapshot.relations.parentThreadId ? /* @__PURE__ */ jsx(Link, {
609
+ className: "text-[var(--accent)]",
610
+ params: { threadId: snapshot.relations.parentThreadId },
611
+ to: "/threads/$threadId",
612
+ children: snapshot.relations.parentThreadId
613
+ }) : "n/a";
614
+ const childThreadValue = snapshot.relations.childEdges.length > 0 ? /* @__PURE__ */ jsx("div", {
615
+ className: "flex flex-wrap gap-2",
616
+ children: snapshot.relations.childEdges.map((edge) => /* @__PURE__ */ jsx(Link, {
617
+ className: "rounded-full border border-[var(--border)] px-3 py-1 text-[var(--accent)] text-xs",
618
+ params: { threadId: edge.child_thread_id },
619
+ to: "/threads/$threadId",
620
+ children: edge.child_thread_id
621
+ }, edge.child_thread_id))
622
+ }) : "n/a";
623
+ return [
624
+ {
625
+ label: "Parent thread",
626
+ value: parentThreadValue
627
+ },
628
+ {
629
+ label: "Child threads",
630
+ value: childThreadValue
631
+ },
632
+ {
633
+ label: "First user message",
634
+ value: snapshot.thread.first_user_message || "n/a"
635
+ },
636
+ {
637
+ label: "Preview",
638
+ value: snapshot.thread.preview || "n/a"
639
+ }
640
+ ];
641
+ };
642
+ var buildTranscriptStatsItems = (snapshot) => {
643
+ if (!snapshot.transcript) {
644
+ if (snapshot.transcriptState === "missing") return [
645
+ {
646
+ label: "Transcript load",
647
+ value: "Transcript file missing from disk"
648
+ },
649
+ {
650
+ label: "Rollout path",
651
+ value: snapshot.thread.rollout_path
652
+ },
653
+ {
654
+ label: "Preview mode",
655
+ value: "Export and transcript browsing are unavailable until the file exists again."
656
+ }
657
+ ];
658
+ return [
659
+ {
660
+ label: "Transcript load",
661
+ value: "Deferred for oversized rollout"
662
+ },
663
+ {
664
+ label: "Rollout size",
665
+ value: formatBytes(snapshot.rollout.fileSizeBytes)
666
+ },
667
+ {
668
+ label: "Preview mode",
669
+ value: "Load the transcript manually to inspect it."
670
+ }
671
+ ];
672
+ }
673
+ return [
674
+ {
675
+ label: "Event kinds",
676
+ value: formatList([...new Set(snapshot.transcript.events.map((event) => event.kind))])
677
+ },
678
+ {
679
+ label: "Stats scope",
680
+ value: snapshot.transcript.statsArePartial ? "Preview only" : "Full transcript"
681
+ },
682
+ {
683
+ label: "Tool calls",
684
+ value: String(snapshot.transcript.stats.toolCallCount)
685
+ },
686
+ {
687
+ label: "Exec calls",
688
+ value: String(snapshot.transcript.stats.execCommandCount)
689
+ },
690
+ {
691
+ label: "Web search events",
692
+ value: String(snapshot.transcript.stats.webSearchEventCount)
693
+ },
694
+ {
695
+ label: "Assistant messages",
696
+ value: String(snapshot.transcript.stats.assistantMessageCount)
697
+ },
698
+ {
699
+ label: "Commentary updates",
700
+ value: String(snapshot.transcript.stats.commentaryCount)
701
+ }
702
+ ];
703
+ };
704
+ function TranscriptControls({ rawJsonDisabled = false, showCommentary, showExtraEvents, showRawJson, showToolCalls, onShowCommentaryChange, onShowExtraEventsChange, onShowRawJsonChange, onShowToolCallsChange }) {
705
+ return /* @__PURE__ */ jsxs("div", {
706
+ className: "flex flex-wrap gap-4 rounded-xl border border-[var(--border)] bg-[var(--panel)] px-4 py-3 shadow-[var(--panel-shadow)]",
707
+ children: [
708
+ /* @__PURE__ */ jsxs("div", {
709
+ className: "flex items-center gap-2 text-sm",
710
+ children: [/* @__PURE__ */ jsx(Checkbox$1, {
711
+ checked: showToolCalls,
712
+ onCheckedChange: (checked) => onShowToolCallsChange(checked === true)
713
+ }), /* @__PURE__ */ jsx("span", { children: "Show tool calls" })]
714
+ }),
715
+ /* @__PURE__ */ jsxs("div", {
716
+ className: "flex items-center gap-2 text-sm",
717
+ children: [/* @__PURE__ */ jsx(Checkbox$1, {
718
+ checked: showCommentary,
719
+ onCheckedChange: (checked) => onShowCommentaryChange(checked === true)
720
+ }), /* @__PURE__ */ jsx("span", { children: "Show commentary" })]
721
+ }),
722
+ /* @__PURE__ */ jsxs("div", {
723
+ className: "flex items-center gap-2 text-sm",
724
+ children: [/* @__PURE__ */ jsx(Checkbox$1, {
725
+ checked: showExtraEvents,
726
+ onCheckedChange: (checked) => onShowExtraEventsChange(checked === true)
727
+ }), /* @__PURE__ */ jsx("span", { children: "Show extra events" })]
728
+ }),
729
+ /* @__PURE__ */ jsxs("div", {
730
+ className: "flex items-center gap-2 text-sm",
731
+ children: [/* @__PURE__ */ jsx(Checkbox$1, {
732
+ checked: showRawJson,
733
+ disabled: rawJsonDisabled,
734
+ onCheckedChange: (checked) => onShowRawJsonChange(checked === true)
735
+ }), /* @__PURE__ */ jsx("span", { children: "Raw JSON" })]
736
+ })
737
+ ]
738
+ });
739
+ }
740
+ function ThreadMetadataPanels({ snapshot }) {
741
+ return /* @__PURE__ */ jsxs("div", {
742
+ className: "grid gap-4 xl:grid-cols-[1.1fr_0.9fr]",
743
+ children: [/* @__PURE__ */ jsxs("div", {
744
+ className: "space-y-4",
745
+ children: [
746
+ /* @__PURE__ */ jsx(MetadataSection, {
747
+ items: buildThreadItems(snapshot),
748
+ title: "Thread"
749
+ }),
750
+ /* @__PURE__ */ jsx(MetadataSection, {
751
+ items: buildRuntimeItems(snapshot),
752
+ title: "Runtime"
753
+ }),
754
+ /* @__PURE__ */ jsx(MetadataSection, {
755
+ items: buildGitItems(snapshot),
756
+ title: "Git and agent"
757
+ })
758
+ ]
759
+ }), /* @__PURE__ */ jsxs("div", {
760
+ className: "space-y-4",
761
+ children: [
762
+ /* @__PURE__ */ jsx(MetadataSection, {
763
+ items: buildRelationItems(snapshot),
764
+ title: "Relations and summary"
765
+ }),
766
+ /* @__PURE__ */ jsxs("section", {
767
+ className: "rounded-[1.6rem] border border-[var(--border)] bg-[var(--panel)] p-5 shadow-[var(--panel-shadow)]",
768
+ children: [/* @__PURE__ */ jsx("h3", {
769
+ className: "font-semibold text-[var(--muted-foreground)] text-sm uppercase tracking-[0.18em]",
770
+ children: "Available tools"
771
+ }), /* @__PURE__ */ jsx("div", {
772
+ className: "mt-4 space-y-3",
773
+ children: snapshot.availableTools.map((tool) => /* @__PURE__ */ jsxs("div", {
774
+ className: "rounded-xl border border-[var(--border)] bg-[var(--panel-secondary)] p-3.5",
775
+ children: [/* @__PURE__ */ jsxs("div", {
776
+ className: "flex flex-wrap items-center gap-2",
777
+ children: [/* @__PURE__ */ jsx("p", {
778
+ className: "font-medium font-mono text-sm",
779
+ children: tool.name
780
+ }), tool.namespace ? /* @__PURE__ */ jsx(Badge, {
781
+ variant: "outline",
782
+ children: tool.namespace
783
+ }) : null]
784
+ }), /* @__PURE__ */ jsx("p", {
785
+ className: "mt-1.5 text-[var(--muted-foreground)] text-sm",
786
+ children: tool.description || "No description."
787
+ })]
788
+ }, `${tool.name}-${tool.namespace ?? "global"}`))
789
+ })]
790
+ }),
791
+ /* @__PURE__ */ jsx(MetadataSection, {
792
+ items: buildTranscriptStatsItems(snapshot),
793
+ title: "Transcript stats"
794
+ })
795
+ ]
796
+ })]
797
+ });
798
+ }
799
+ function ThreadRawPanels({ snapshot }) {
800
+ if (!snapshot.transcript) return /* @__PURE__ */ jsx("div", {
801
+ className: "rounded-xl border border-[var(--border)] bg-[var(--panel)] px-5 py-4 text-sm",
802
+ children: snapshot.transcriptState === "missing" ? "The rollout JSONL file is missing from disk, so raw transcript payloads are unavailable." : "Raw transcript payloads are deferred for oversized rollouts. Use Export if you only need the full thread contents, or load the transcript manually from the Transcript tab."
803
+ });
804
+ return /* @__PURE__ */ jsxs("div", {
805
+ className: "space-y-4",
806
+ children: [
807
+ /* @__PURE__ */ jsx(JsonPanel, {
808
+ title: "Session meta",
809
+ value: snapshot.transcript.sessionMeta
810
+ }),
811
+ /* @__PURE__ */ jsx(JsonPanel, {
812
+ title: "Turn contexts",
813
+ value: snapshot.transcript.turnContexts
814
+ }),
815
+ /* @__PURE__ */ jsx(JsonPanel, {
816
+ title: "Sandbox policy",
817
+ value: snapshot.thread.sandbox_policy
818
+ })
819
+ ]
820
+ });
821
+ }
822
+ function DeferredTranscriptNotice({ fileSizeBytes, missing, pending, onLoad }) {
823
+ return /* @__PURE__ */ jsxs("section", {
824
+ className: "rounded-[1.6rem] border border-[var(--border)] bg-[var(--panel)] p-5 shadow-[var(--panel-shadow)]",
825
+ children: [
826
+ /* @__PURE__ */ jsx("h3", {
827
+ className: "font-semibold text-base",
828
+ children: missing ? "Transcript file missing" : "This is a very big thread"
829
+ }),
830
+ /* @__PURE__ */ jsx("p", {
831
+ className: "mt-2 text-[var(--muted-foreground)] text-sm leading-6",
832
+ children: missing ? "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." : `Spiracha skipped loading the transcript automatically because the rollout file is ${formatBytes(fileSizeBytes)}. Export still works immediately. If you need to inspect the thread here, load a bounded preview manually.`
833
+ }),
834
+ missing ? null : /* @__PURE__ */ jsx("div", {
835
+ className: "mt-4",
836
+ children: /* @__PURE__ */ jsx(Button, {
837
+ disabled: pending,
838
+ variant: "outline",
839
+ onClick: onLoad,
840
+ children: pending ? "Loading preview..." : "Load transcript preview"
841
+ })
842
+ })
843
+ ]
844
+ });
845
+ }
846
+ function ThreadDetailPage() {
847
+ const navigate = useNavigate();
848
+ const queryClient = useQueryClient();
849
+ const params = Route.useParams();
850
+ const snapshot = useSuspenseQuery(threadSnapshotQueryOptions(params.threadId)).data;
851
+ const { settings } = useSettings();
852
+ const [shouldLoadTranscript, setShouldLoadTranscript] = useState(!snapshot.rollout.shouldDeferTranscriptLoad);
853
+ const [showToolCalls, setShowToolCalls] = useState(false);
854
+ const [showCommentary, setShowCommentary] = useState(false);
855
+ const [showExtraEvents, setShowExtraEvents] = useState(false);
856
+ const [showRawJson, setShowRawJson] = useState(false);
857
+ const [exportOpen, setExportOpen] = useState(false);
858
+ const [deleteOpen, setDeleteOpen] = useState(false);
859
+ const transcriptQuery = useQuery({
860
+ ...threadTranscriptQueryOptions(params.threadId),
861
+ enabled: shouldLoadTranscript && snapshot.transcript === null
862
+ });
863
+ const transcript = snapshot.transcript ?? transcriptQuery.data ?? null;
864
+ const viewSnapshot = {
865
+ ...snapshot,
866
+ transcript
867
+ };
868
+ const exportThreadMutation = useMutation({
869
+ mutationFn: async (options) => {
870
+ console.info("[spiracha:export-ui] request", {
871
+ outputFormat: options.outputFormat,
872
+ project: snapshot.project,
873
+ selectedThreadCount: 1,
874
+ selectedThreadIds: [snapshot.thread.id]
875
+ });
876
+ const download = await exportThreadFn({ data: {
877
+ ...options,
878
+ ...settings,
879
+ threadId: snapshot.thread.id
880
+ } });
881
+ console.info("[spiracha:export-ui] response", {
882
+ downloadUrl: download.mode === "download_url" ? download.downloadUrl : null,
883
+ fileName: download.fileName,
884
+ mode: download.mode,
885
+ project: snapshot.project,
886
+ selectedThreadCount: 1
887
+ });
888
+ if (download.mode === "download") {
889
+ downloadTextFile(download.fileName, download.content, download.mimeType);
890
+ return;
891
+ }
892
+ await downloadUrlFile(download.fileName, download.downloadUrl);
893
+ },
894
+ onError: (error) => {
895
+ console.error("[spiracha:export-ui] failed", {
896
+ error: error instanceof Error ? error.message : String(error),
897
+ project: snapshot.project,
898
+ selectedThreadCount: 1,
899
+ selectedThreadIds: [snapshot.thread.id]
900
+ });
901
+ },
902
+ onSuccess: async () => {
903
+ setExportOpen(false);
904
+ }
905
+ });
906
+ const deleteThreadMutation = useMutation({
907
+ mutationFn: (input) => deleteThreadFn({ data: {
908
+ deleteSessionFiles: input.deleteSessionFiles,
909
+ threadId: snapshot.thread.id
910
+ } }),
911
+ onSuccess: async () => {
912
+ await Promise.all([
913
+ queryClient.invalidateQueries({ queryKey: ["analytics"] }),
914
+ queryClient.invalidateQueries({ queryKey: ["dashboard"] }),
915
+ queryClient.invalidateQueries({ queryKey: ["project-threads", snapshot.project] }),
916
+ queryClient.invalidateQueries({ queryKey: ["projects"] })
917
+ ]);
918
+ navigate({
919
+ params: { project: snapshot.project },
920
+ to: "/projects/$project"
921
+ });
922
+ }
923
+ });
924
+ return /* @__PURE__ */ jsxs("div", {
925
+ className: "space-y-5",
926
+ children: [
927
+ /* @__PURE__ */ jsx(PageHeader, {
928
+ actions: /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs(Button, {
929
+ className: "rounded-full",
930
+ variant: "outline",
931
+ onClick: () => setExportOpen(true),
932
+ children: [/* @__PURE__ */ jsx(Download, { className: "mr-2 size-4" }), "Export"]
933
+ }), /* @__PURE__ */ jsxs(Button, {
934
+ className: "rounded-full border-[var(--destructive)]/20 text-[var(--destructive)]",
935
+ variant: "outline",
936
+ onClick: () => setDeleteOpen(true),
937
+ children: [/* @__PURE__ */ jsx(Trash2, { className: "mr-2 size-4" }), "Delete"]
938
+ })] }),
939
+ eyebrow: snapshot.project,
940
+ subtitle: snapshot.thread.preview,
941
+ title: snapshot.thread.title
942
+ }),
943
+ /* @__PURE__ */ jsxs("div", {
944
+ className: "grid gap-3 md:grid-cols-2 xl:grid-cols-4",
945
+ children: [
946
+ /* @__PURE__ */ jsx(MetricCard, {
947
+ label: "Model",
948
+ value: snapshot.thread.model ?? "unknown"
949
+ }),
950
+ /* @__PURE__ */ jsx(MetricCard, {
951
+ label: "Tokens",
952
+ value: formatTokens(snapshot.thread.tokens_used)
953
+ }),
954
+ /* @__PURE__ */ jsx(MetricCard, {
955
+ label: "Updated",
956
+ value: /* @__PURE__ */ jsx("span", {
957
+ suppressHydrationWarning: true,
958
+ children: formatDateTime(snapshot.thread.updated_at_ms ?? snapshot.thread.updated_at * 1e3)
959
+ })
960
+ }),
961
+ /* @__PURE__ */ jsx(MetricCard, {
962
+ label: "Thread source",
963
+ value: snapshot.thread.thread_source ?? transcript?.sessionMeta.threadSource ?? "n/a"
964
+ })
965
+ ]
966
+ }),
967
+ /* @__PURE__ */ jsxs(Tabs$1, {
968
+ className: "space-y-4",
969
+ defaultValue: "transcript",
970
+ children: [
971
+ /* @__PURE__ */ jsxs(TabsList, {
972
+ className: "grid w-fit min-w-[24rem] grid-cols-3 rounded-full border border-[var(--border)] bg-[var(--panel)] p-1",
973
+ children: [
974
+ /* @__PURE__ */ jsx(TabsTrigger, {
975
+ className: "rounded-full px-5 text-sm",
976
+ value: "transcript",
977
+ children: "Transcript"
978
+ }),
979
+ /* @__PURE__ */ jsx(TabsTrigger, {
980
+ className: "rounded-full px-5 text-sm",
981
+ value: "metadata",
982
+ children: "Metadata"
983
+ }),
984
+ /* @__PURE__ */ jsx(TabsTrigger, {
985
+ className: "rounded-full px-5 text-sm",
986
+ value: "raw",
987
+ children: "Raw"
988
+ })
989
+ ]
990
+ }),
991
+ /* @__PURE__ */ jsxs(TabsContent, {
992
+ className: "space-y-3",
993
+ value: "transcript",
994
+ children: [
995
+ /* @__PURE__ */ jsx(TranscriptControls, {
996
+ rawJsonDisabled: !transcript?.rawIncluded,
997
+ showCommentary,
998
+ showExtraEvents,
999
+ showRawJson,
1000
+ showToolCalls,
1001
+ onShowCommentaryChange: setShowCommentary,
1002
+ onShowExtraEventsChange: setShowExtraEvents,
1003
+ onShowRawJsonChange: setShowRawJson,
1004
+ onShowToolCallsChange: setShowToolCalls
1005
+ }),
1006
+ transcript ? /* @__PURE__ */ jsx(TranscriptView, {
1007
+ assistantModel: snapshot.thread.model,
1008
+ events: transcript.events,
1009
+ projectPath: snapshot.thread.cwd,
1010
+ showCommentary,
1011
+ showExtraEvents,
1012
+ showRawJson: showRawJson && transcript.rawIncluded,
1013
+ showToolCalls
1014
+ }) : /* @__PURE__ */ jsx(DeferredTranscriptNotice, {
1015
+ fileSizeBytes: snapshot.rollout.fileSizeBytes,
1016
+ missing: snapshot.transcriptState === "missing",
1017
+ pending: transcriptQuery.isFetching,
1018
+ onLoad: () => setShouldLoadTranscript(true)
1019
+ }),
1020
+ transcriptQuery.isError ? /* @__PURE__ */ jsxs("p", {
1021
+ className: "text-[var(--destructive)] text-sm",
1022
+ children: [
1023
+ "Failed to load transcript preview:",
1024
+ " ",
1025
+ transcriptQuery.error instanceof Error ? transcriptQuery.error.message : "Unknown error"
1026
+ ]
1027
+ }) : null
1028
+ ]
1029
+ }),
1030
+ /* @__PURE__ */ jsx(TabsContent, {
1031
+ value: "metadata",
1032
+ children: /* @__PURE__ */ jsx(ThreadMetadataPanels, { snapshot: viewSnapshot })
1033
+ }),
1034
+ /* @__PURE__ */ jsx(TabsContent, {
1035
+ value: "raw",
1036
+ children: /* @__PURE__ */ jsx(ThreadRawPanels, { snapshot: viewSnapshot })
1037
+ })
1038
+ ]
1039
+ }),
1040
+ /* @__PURE__ */ jsx(DeleteConfirmDialog, {
1041
+ confirmLabel: deleteThreadMutation.isPending ? "Deleting..." : "Delete thread",
1042
+ description: "Delete this thread from the Codex database. Enable Delete Session files if you also want to remove the rollout JSONL from disk.",
1043
+ open: deleteOpen,
1044
+ showDeleteSessionFilesOption: true,
1045
+ title: "Delete this thread from Codex DB?",
1046
+ onConfirm: ({ deleteSessionFiles }) => deleteThreadMutation.mutate({ deleteSessionFiles }),
1047
+ onOpenChange: setDeleteOpen
1048
+ }),
1049
+ /* @__PURE__ */ jsx(ExportDialog, {
1050
+ open: exportOpen,
1051
+ pending: exportThreadMutation.isPending,
1052
+ onExport: (options) => exportThreadMutation.mutate(options),
1053
+ onOpenChange: setExportOpen
1054
+ })
1055
+ ]
1056
+ });
1057
+ }
1058
+ //#endregion
1059
+ export { ThreadDetailPage as component };