@agent-native/core 0.84.66 → 0.85.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (155) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +20 -0
  3. package/corpus/core/docs/content/locales/ar-SA/tracking.mdx +55 -0
  4. package/corpus/core/docs/content/locales/de-DE/tracking.mdx +55 -0
  5. package/corpus/core/docs/content/locales/es-ES/tracking.mdx +55 -0
  6. package/corpus/core/docs/content/locales/fr-FR/tracking.mdx +55 -0
  7. package/corpus/core/docs/content/locales/hi-IN/tracking.mdx +55 -0
  8. package/corpus/core/docs/content/locales/ja-JP/tracking.mdx +55 -0
  9. package/corpus/core/docs/content/locales/ko-KR/tracking.mdx +55 -0
  10. package/corpus/core/docs/content/locales/pt-BR/tracking.mdx +55 -0
  11. package/corpus/core/docs/content/locales/zh-CN/tracking.mdx +55 -0
  12. package/corpus/core/docs/content/locales/zh-TW/tracking.mdx +55 -0
  13. package/corpus/core/docs/content/tracking.mdx +2 -0
  14. package/corpus/core/package.json +1 -1
  15. package/corpus/core/src/agent/production-agent.ts +199 -38
  16. package/corpus/core/src/agent/run-manager.ts +79 -0
  17. package/corpus/core/src/agent/run-store.ts +117 -5
  18. package/corpus/core/src/agent/types.ts +13 -0
  19. package/corpus/core/src/client/RunStuckBanner.tsx +14 -0
  20. package/corpus/core/src/client/agent-chat-adapter.ts +475 -0
  21. package/corpus/core/src/client/analytics.ts +2 -0
  22. package/corpus/core/src/client/error-format.ts +4 -0
  23. package/corpus/core/src/client/extensions/ExtensionsSidebarSection.tsx +1 -1
  24. package/corpus/core/src/client/session-replay.ts +774 -2
  25. package/corpus/core/src/client/sse-event-processor.ts +65 -9
  26. package/corpus/core/src/client/use-run-stuck-detection.ts +31 -7
  27. package/corpus/core/src/db/runtime-diagnostics.ts +331 -0
  28. package/corpus/core/src/server/agent-chat-plugin.ts +115 -2
  29. package/corpus/core/src/server/analytics.ts +1 -34
  30. package/corpus/core/src/server/core-routes-plugin.ts +91 -4
  31. package/corpus/core/src/server/http-response-telemetry.ts +189 -0
  32. package/corpus/core/src/server/self-dispatch.ts +29 -0
  33. package/corpus/core/src/server/ssr-handler.ts +18 -377
  34. package/corpus/templates/analytics/.agents/skills/session-replay/SKILL.md +53 -1
  35. package/corpus/templates/analytics/AGENTS.md +13 -0
  36. package/corpus/templates/analytics/actions/view-screen.ts +25 -0
  37. package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +20 -0
  38. package/corpus/templates/analytics/app/global.css +55 -0
  39. package/corpus/templates/analytics/app/i18n/zh-TW.ts +100 -1
  40. package/corpus/templates/analytics/app/i18n-data.ts +1226 -0
  41. package/corpus/templates/analytics/app/pages/Settings.tsx +3 -0
  42. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/EmailReportDialog.tsx +182 -144
  43. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx +11 -18
  44. package/corpus/templates/analytics/app/pages/analyses/AnalysisDetail.tsx +11 -18
  45. package/corpus/templates/analytics/app/pages/sessions/SessionDetailPage.tsx +49 -1
  46. package/corpus/templates/analytics/app/pages/sessions/SessionDevToolsPanel.tsx +557 -0
  47. package/corpus/templates/analytics/app/pages/sessions/session-replay-devtools.ts +274 -0
  48. package/corpus/templates/analytics/app/pages/settings/AlertRulesSettingsCard.tsx +974 -0
  49. package/corpus/templates/analytics/changelog/2026-07-02-alert-rules-can-now-be-viewed-and-managed-from-settings.md +6 -0
  50. package/corpus/templates/analytics/changelog/2026-07-02-recent-ask-chats-show-a-loading-placeholder-while-your-chat-.md +6 -0
  51. package/corpus/templates/analytics/changelog/2026-07-02-session-replays-now-capture-console-logs-and-network-request.md +6 -0
  52. package/corpus/templates/analytics/changelog/2026-07-02-sharing-status-labels-use-neutral-icons-instead-of-colored-d.md +6 -0
  53. package/corpus/templates/analytics/changelog/2026-07-02-the-session-replay-viewer-has-a-dev-tools-panel-with-console.md +6 -0
  54. package/corpus/templates/analytics/server/db/schema.ts +3 -0
  55. package/corpus/templates/analytics/server/jobs/analytics-alerts.ts +4 -0
  56. package/corpus/templates/analytics/server/lib/analytics-alerts.ts +142 -1
  57. package/corpus/templates/analytics/server/lib/session-replay-agent-context.ts +369 -8
  58. package/corpus/templates/analytics/server/lib/session-replay.ts +76 -2
  59. package/corpus/templates/analytics/server/routes/api/session-replay/agent-diagnostics.json.get.ts +101 -0
  60. package/corpus/templates/analytics/shared/session-replay-diagnostics.ts +78 -0
  61. package/corpus/templates/clips/server/routes/api/video/[recordingId].get.ts +0 -2
  62. package/corpus/templates/design/app/pages/DesignEditor.tsx +24 -12
  63. package/corpus/templates/design/changelog/2026-07-02-the-temporary-code-tab-is-hidden-from-the-design-editor-side.md +6 -0
  64. package/corpus/templates/forms/server/lib/public-form-ssr.ts +0 -1
  65. package/corpus/templates/plan/actions/create-visual-recap.ts +1 -1
  66. package/corpus/templates/plan/actions/update-local-plan-folder.ts +3 -1
  67. package/corpus/templates/plan/actions/update-visual-plan.ts +1 -1
  68. package/corpus/templates/plan/changelog/2026-07-02-trying-to-edit-a-recap-or-plan-you-can-only-view-now-explain.md +6 -0
  69. package/corpus/templates/plan/server/plans.ts +26 -9
  70. package/dist/agent/production-agent.d.ts.map +1 -1
  71. package/dist/agent/production-agent.js +155 -34
  72. package/dist/agent/production-agent.js.map +1 -1
  73. package/dist/agent/run-manager.d.ts +29 -0
  74. package/dist/agent/run-manager.d.ts.map +1 -1
  75. package/dist/agent/run-manager.js +73 -0
  76. package/dist/agent/run-manager.js.map +1 -1
  77. package/dist/agent/run-store.d.ts +41 -0
  78. package/dist/agent/run-store.d.ts.map +1 -1
  79. package/dist/agent/run-store.js +100 -4
  80. package/dist/agent/run-store.js.map +1 -1
  81. package/dist/agent/types.d.ts +13 -0
  82. package/dist/agent/types.d.ts.map +1 -1
  83. package/dist/agent/types.js.map +1 -1
  84. package/dist/client/RunStuckBanner.d.ts.map +1 -1
  85. package/dist/client/RunStuckBanner.js +13 -0
  86. package/dist/client/RunStuckBanner.js.map +1 -1
  87. package/dist/client/agent-chat-adapter.d.ts.map +1 -1
  88. package/dist/client/agent-chat-adapter.js +410 -0
  89. package/dist/client/agent-chat-adapter.js.map +1 -1
  90. package/dist/client/analytics.d.ts +1 -1
  91. package/dist/client/analytics.d.ts.map +1 -1
  92. package/dist/client/analytics.js.map +1 -1
  93. package/dist/client/error-format.d.ts.map +1 -1
  94. package/dist/client/error-format.js +2 -0
  95. package/dist/client/error-format.js.map +1 -1
  96. package/dist/client/extensions/ExtensionsSidebarSection.js +1 -1
  97. package/dist/client/extensions/ExtensionsSidebarSection.js.map +1 -1
  98. package/dist/client/session-replay.d.ts +36 -0
  99. package/dist/client/session-replay.d.ts.map +1 -1
  100. package/dist/client/session-replay.js +579 -1
  101. package/dist/client/session-replay.js.map +1 -1
  102. package/dist/client/sse-event-processor.d.ts +20 -1
  103. package/dist/client/sse-event-processor.d.ts.map +1 -1
  104. package/dist/client/sse-event-processor.js +42 -11
  105. package/dist/client/sse-event-processor.js.map +1 -1
  106. package/dist/client/use-run-stuck-detection.d.ts +14 -4
  107. package/dist/client/use-run-stuck-detection.d.ts.map +1 -1
  108. package/dist/client/use-run-stuck-detection.js +18 -4
  109. package/dist/client/use-run-stuck-detection.js.map +1 -1
  110. package/dist/collab/awareness.d.ts +2 -2
  111. package/dist/collab/awareness.d.ts.map +1 -1
  112. package/dist/collab/routes.d.ts +1 -1
  113. package/dist/db/runtime-diagnostics.d.ts +52 -0
  114. package/dist/db/runtime-diagnostics.d.ts.map +1 -0
  115. package/dist/db/runtime-diagnostics.js +250 -0
  116. package/dist/db/runtime-diagnostics.js.map +1 -0
  117. package/dist/observability/routes.d.ts +5 -5
  118. package/dist/provider-api/corpus-jobs.d.ts +2 -2
  119. package/dist/resources/handlers.d.ts +2 -2
  120. package/dist/secrets/routes.d.ts +3 -3
  121. package/dist/server/agent-chat-plugin.d.ts.map +1 -1
  122. package/dist/server/agent-chat-plugin.js +101 -2
  123. package/dist/server/agent-chat-plugin.js.map +1 -1
  124. package/dist/server/analytics.d.ts +1 -19
  125. package/dist/server/analytics.d.ts.map +1 -1
  126. package/dist/server/analytics.js +1 -31
  127. package/dist/server/analytics.js.map +1 -1
  128. package/dist/server/core-routes-plugin.d.ts +17 -0
  129. package/dist/server/core-routes-plugin.d.ts.map +1 -1
  130. package/dist/server/core-routes-plugin.js +65 -5
  131. package/dist/server/core-routes-plugin.js.map +1 -1
  132. package/dist/server/http-response-telemetry.d.ts +4 -0
  133. package/dist/server/http-response-telemetry.d.ts.map +1 -0
  134. package/dist/server/http-response-telemetry.js +174 -0
  135. package/dist/server/http-response-telemetry.js.map +1 -0
  136. package/dist/server/self-dispatch.d.ts +17 -0
  137. package/dist/server/self-dispatch.d.ts.map +1 -1
  138. package/dist/server/self-dispatch.js +11 -0
  139. package/dist/server/self-dispatch.js.map +1 -1
  140. package/dist/server/ssr-handler.d.ts.map +1 -1
  141. package/dist/server/ssr-handler.js +18 -271
  142. package/dist/server/ssr-handler.js.map +1 -1
  143. package/dist/server/transcribe-voice.d.ts +1 -1
  144. package/docs/content/locales/ar-SA/tracking.mdx +55 -0
  145. package/docs/content/locales/de-DE/tracking.mdx +55 -0
  146. package/docs/content/locales/es-ES/tracking.mdx +55 -0
  147. package/docs/content/locales/fr-FR/tracking.mdx +55 -0
  148. package/docs/content/locales/hi-IN/tracking.mdx +55 -0
  149. package/docs/content/locales/ja-JP/tracking.mdx +55 -0
  150. package/docs/content/locales/ko-KR/tracking.mdx +55 -0
  151. package/docs/content/locales/pt-BR/tracking.mdx +55 -0
  152. package/docs/content/locales/zh-CN/tracking.mdx +55 -0
  153. package/docs/content/locales/zh-TW/tracking.mdx +55 -0
  154. package/docs/content/tracking.mdx +2 -0
  155. package/package.json +1 -1
@@ -0,0 +1,557 @@
1
+ import { useT } from "@agent-native/core/client";
2
+ import {
3
+ IconChevronRight,
4
+ IconCloudDataConnection,
5
+ IconPlayerPlay,
6
+ IconSearch,
7
+ IconTerminal2,
8
+ } from "@tabler/icons-react";
9
+ import { type ReactNode, useEffect, useMemo, useRef, useState } from "react";
10
+
11
+ import {
12
+ Collapsible,
13
+ CollapsibleContent,
14
+ CollapsibleTrigger,
15
+ } from "@/components/ui/collapsible";
16
+ import { Input } from "@/components/ui/input";
17
+ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
18
+ import { cn } from "@/lib/utils";
19
+
20
+ import {
21
+ type ConsoleLevelFilter,
22
+ consoleLevelBucket,
23
+ filterConsoleEntries,
24
+ filterNetworkEntries,
25
+ formatOffsetClock,
26
+ latestEntryIndexAt,
27
+ middleTruncate,
28
+ type NetworkKindFilter,
29
+ networkDisplayUrl,
30
+ type ReplayConsoleEntry,
31
+ type ReplayDevToolsDiagnostics,
32
+ type ReplayNetworkEntry,
33
+ } from "./session-replay-devtools";
34
+
35
+ /** Pause row auto-follow for a while after the user scrolls the list. */
36
+ const MANUAL_SCROLL_FOLLOW_PAUSE_MS = 4000;
37
+
38
+ export function SessionDevToolsPanel({
39
+ diagnostics,
40
+ currentTime,
41
+ onSeek,
42
+ }: {
43
+ diagnostics: ReplayDevToolsDiagnostics;
44
+ currentTime: number;
45
+ onSeek: (ms: number) => void;
46
+ }) {
47
+ const t = useT();
48
+ const [tab, setTab] = useState<"console" | "network">("console");
49
+ const [consoleLevel, setConsoleLevel] = useState<ConsoleLevelFilter>("all");
50
+ const [consoleQuery, setConsoleQuery] = useState("");
51
+ const [networkKind, setNetworkKind] = useState<NetworkKindFilter>("all");
52
+ const [networkQuery, setNetworkQuery] = useState("");
53
+
54
+ const filteredConsole = useMemo(
55
+ () => filterConsoleEntries(diagnostics.console, consoleLevel, consoleQuery),
56
+ [diagnostics.console, consoleLevel, consoleQuery],
57
+ );
58
+ const filteredNetwork = useMemo(
59
+ () => filterNetworkEntries(diagnostics.network, networkKind, networkQuery),
60
+ [diagnostics.network, networkKind, networkQuery],
61
+ );
62
+
63
+ const activeConsoleId =
64
+ tab === "console"
65
+ ? (filteredConsole[latestEntryIndexAt(filteredConsole, currentTime)]
66
+ ?.id ?? null)
67
+ : null;
68
+ const activeNetworkId =
69
+ tab === "network"
70
+ ? (filteredNetwork[latestEntryIndexAt(filteredNetwork, currentTime)]
71
+ ?.id ?? null)
72
+ : null;
73
+
74
+ const consoleLevelCounts = useMemo(() => {
75
+ const counts = { log: 0, info: 0, warn: 0, error: 0 };
76
+ for (const entry of diagnostics.console) {
77
+ counts[consoleLevelBucket(entry.level)] += 1;
78
+ }
79
+ return counts;
80
+ }, [diagnostics.console]);
81
+
82
+ const networkKindCounts = useMemo(() => {
83
+ const counts = { fetch: 0, xhr: 0, failed: 0 };
84
+ for (const entry of diagnostics.network) {
85
+ counts[entry.api] += 1;
86
+ if (entry.failed) counts.failed += 1;
87
+ }
88
+ return counts;
89
+ }, [diagnostics.network]);
90
+
91
+ return (
92
+ <div className="analytics-session-devtools shrink-0 border-t">
93
+ <Tabs
94
+ value={tab}
95
+ onValueChange={(value) => setTab(value as "console" | "network")}
96
+ >
97
+ <div className="flex items-center gap-2 px-3 pt-2">
98
+ <TabsList className="h-8 p-0.5">
99
+ <TabsTrigger value="console" className="h-7 gap-1.5 px-2.5 text-xs">
100
+ <IconTerminal2 className="h-3.5 w-3.5" />
101
+ {t("sessions.devtoolsConsoleTab", {
102
+ count: String(diagnostics.console.length),
103
+ })}
104
+ {diagnostics.consoleErrorCount > 0 ? (
105
+ <span className="h-1.5 w-1.5 rounded-full bg-red-500" />
106
+ ) : null}
107
+ </TabsTrigger>
108
+ <TabsTrigger value="network" className="h-7 gap-1.5 px-2.5 text-xs">
109
+ <IconCloudDataConnection className="h-3.5 w-3.5" />
110
+ {t("sessions.devtoolsNetworkTab", {
111
+ count: String(diagnostics.network.length),
112
+ })}
113
+ {diagnostics.networkFailedCount > 0 ? (
114
+ <span className="h-1.5 w-1.5 rounded-full bg-red-500" />
115
+ ) : null}
116
+ </TabsTrigger>
117
+ </TabsList>
118
+ </div>
119
+
120
+ <TabsContent value="console" className="mt-0">
121
+ <div className="flex flex-wrap items-center gap-1.5 px-3 py-2">
122
+ <FilterChip
123
+ label={t("sessions.devtoolsFilterAll", {
124
+ count: String(diagnostics.console.length),
125
+ })}
126
+ active={consoleLevel === "all"}
127
+ onClick={() => setConsoleLevel("all")}
128
+ />
129
+ <FilterChip
130
+ label={t("sessions.devtoolsFilterLog", {
131
+ count: String(consoleLevelCounts.log),
132
+ })}
133
+ active={consoleLevel === "log"}
134
+ onClick={() => setConsoleLevel("log")}
135
+ />
136
+ <FilterChip
137
+ label={t("sessions.devtoolsFilterInfo", {
138
+ count: String(consoleLevelCounts.info),
139
+ })}
140
+ active={consoleLevel === "info"}
141
+ onClick={() => setConsoleLevel("info")}
142
+ />
143
+ <FilterChip
144
+ label={t("sessions.devtoolsFilterWarning", {
145
+ count: String(consoleLevelCounts.warn),
146
+ })}
147
+ active={consoleLevel === "warn"}
148
+ onClick={() => setConsoleLevel("warn")}
149
+ tone="warn"
150
+ />
151
+ <FilterChip
152
+ label={t("sessions.devtoolsFilterError", {
153
+ count: String(consoleLevelCounts.error),
154
+ })}
155
+ active={consoleLevel === "error"}
156
+ onClick={() => setConsoleLevel("error")}
157
+ tone="error"
158
+ />
159
+ <DevToolsSearchInput
160
+ value={consoleQuery}
161
+ onChange={setConsoleQuery}
162
+ placeholder={t("sessions.devtoolsConsoleSearch")}
163
+ />
164
+ </div>
165
+ <DevToolsScrollArea activeEntryId={activeConsoleId}>
166
+ {filteredConsole.length ? (
167
+ filteredConsole.map((entry) => (
168
+ <ConsoleRow
169
+ key={entry.id}
170
+ entry={entry}
171
+ active={entry.id === activeConsoleId}
172
+ onSeek={onSeek}
173
+ />
174
+ ))
175
+ ) : (
176
+ <DevToolsEmptyState
177
+ message={
178
+ diagnostics.console.length
179
+ ? t("sessions.devtoolsNoConsoleMatches")
180
+ : t("sessions.devtoolsNoConsole")
181
+ }
182
+ />
183
+ )}
184
+ </DevToolsScrollArea>
185
+ </TabsContent>
186
+
187
+ <TabsContent value="network" className="mt-0">
188
+ <div className="flex flex-wrap items-center gap-1.5 px-3 py-2">
189
+ <FilterChip
190
+ label={t("sessions.devtoolsFilterAll", {
191
+ count: String(diagnostics.network.length),
192
+ })}
193
+ active={networkKind === "all"}
194
+ onClick={() => setNetworkKind("all")}
195
+ />
196
+ <FilterChip
197
+ label={t("sessions.devtoolsFilterFetch", {
198
+ count: String(networkKindCounts.fetch),
199
+ })}
200
+ active={networkKind === "fetch"}
201
+ onClick={() => setNetworkKind("fetch")}
202
+ />
203
+ <FilterChip
204
+ label={t("sessions.devtoolsFilterXhr", {
205
+ count: String(networkKindCounts.xhr),
206
+ })}
207
+ active={networkKind === "xhr"}
208
+ onClick={() => setNetworkKind("xhr")}
209
+ />
210
+ <FilterChip
211
+ label={t("sessions.devtoolsFilterFailed", {
212
+ count: String(networkKindCounts.failed),
213
+ })}
214
+ active={networkKind === "failed"}
215
+ onClick={() => setNetworkKind("failed")}
216
+ tone="error"
217
+ />
218
+ <DevToolsSearchInput
219
+ value={networkQuery}
220
+ onChange={setNetworkQuery}
221
+ placeholder={t("sessions.devtoolsNetworkSearch")}
222
+ />
223
+ </div>
224
+ <DevToolsScrollArea activeEntryId={activeNetworkId}>
225
+ {filteredNetwork.length ? (
226
+ filteredNetwork.map((entry) => (
227
+ <NetworkRow
228
+ key={entry.id}
229
+ entry={entry}
230
+ active={entry.id === activeNetworkId}
231
+ onSeek={onSeek}
232
+ />
233
+ ))
234
+ ) : (
235
+ <DevToolsEmptyState
236
+ message={
237
+ diagnostics.network.length
238
+ ? t("sessions.devtoolsNoNetworkMatches")
239
+ : t("sessions.devtoolsNoNetwork")
240
+ }
241
+ />
242
+ )}
243
+ </DevToolsScrollArea>
244
+ </TabsContent>
245
+ </Tabs>
246
+ </div>
247
+ );
248
+ }
249
+
250
+ function DevToolsScrollArea({
251
+ activeEntryId,
252
+ children,
253
+ }: {
254
+ activeEntryId: string | null;
255
+ children: ReactNode;
256
+ }) {
257
+ const containerRef = useRef<HTMLDivElement>(null);
258
+ const lastManualScrollAtRef = useRef(0);
259
+
260
+ useEffect(() => {
261
+ if (!activeEntryId) return;
262
+ if (
263
+ Date.now() - lastManualScrollAtRef.current <
264
+ MANUAL_SCROLL_FOLLOW_PAUSE_MS
265
+ ) {
266
+ return;
267
+ }
268
+ const row = containerRef.current?.querySelector(
269
+ `[data-entry-id="${activeEntryId}"]`,
270
+ );
271
+ row?.scrollIntoView({ block: "nearest" });
272
+ }, [activeEntryId]);
273
+
274
+ const markManualScroll = () => {
275
+ lastManualScrollAtRef.current = Date.now();
276
+ };
277
+
278
+ return (
279
+ <div
280
+ ref={containerRef}
281
+ className="max-h-56 overflow-y-auto border-t"
282
+ onWheel={markManualScroll}
283
+ onPointerDown={markManualScroll}
284
+ onTouchMove={markManualScroll}
285
+ >
286
+ {children}
287
+ </div>
288
+ );
289
+ }
290
+
291
+ function FilterChip({
292
+ label,
293
+ active,
294
+ onClick,
295
+ tone = "default",
296
+ }: {
297
+ label: string;
298
+ active: boolean;
299
+ onClick: () => void;
300
+ tone?: "default" | "warn" | "error";
301
+ }) {
302
+ return (
303
+ <button
304
+ type="button"
305
+ className={cn(
306
+ "inline-flex h-6 items-center rounded-full border px-2 text-[11px] font-medium text-muted-foreground transition-colors hover:bg-muted hover:text-foreground",
307
+ active &&
308
+ tone === "default" &&
309
+ "border-primary/40 bg-primary/10 text-primary",
310
+ active &&
311
+ tone === "warn" &&
312
+ "border-amber-500/40 bg-amber-500/10 text-amber-600 dark:text-amber-400",
313
+ active &&
314
+ tone === "error" &&
315
+ "border-red-500/40 bg-red-500/10 text-red-600 dark:text-red-400",
316
+ )}
317
+ aria-pressed={active}
318
+ onClick={onClick}
319
+ >
320
+ {label}
321
+ </button>
322
+ );
323
+ }
324
+
325
+ function DevToolsSearchInput({
326
+ value,
327
+ onChange,
328
+ placeholder,
329
+ }: {
330
+ value: string;
331
+ onChange: (value: string) => void;
332
+ placeholder: string;
333
+ }) {
334
+ return (
335
+ <div className="relative ms-auto w-full max-w-56">
336
+ <IconSearch className="pointer-events-none absolute start-2 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-muted-foreground" />
337
+ <Input
338
+ type="search"
339
+ className="h-7 ps-7 text-xs"
340
+ value={value}
341
+ placeholder={placeholder}
342
+ onChange={(event) => onChange(event.target.value)}
343
+ />
344
+ </div>
345
+ );
346
+ }
347
+
348
+ function DevToolsEmptyState({ message }: { message: string }) {
349
+ return (
350
+ <div className="px-4 py-8 text-center text-sm text-muted-foreground">
351
+ {message}
352
+ </div>
353
+ );
354
+ }
355
+
356
+ function JumpToButton({
357
+ offsetMs,
358
+ onSeek,
359
+ }: {
360
+ offsetMs: number;
361
+ onSeek: (ms: number) => void;
362
+ }) {
363
+ const t = useT();
364
+ return (
365
+ <button
366
+ type="button"
367
+ className="inline-flex shrink-0 items-center gap-1 rounded border px-1.5 py-0.5 text-[11px] font-medium text-muted-foreground opacity-0 transition-opacity hover:bg-muted hover:text-foreground focus-visible:opacity-100 group-hover:opacity-100"
368
+ onClick={(event) => {
369
+ event.stopPropagation();
370
+ onSeek(offsetMs);
371
+ }}
372
+ >
373
+ <IconPlayerPlay className="h-3 w-3" />
374
+ {t("sessions.devtoolsJumpTo")}
375
+ </button>
376
+ );
377
+ }
378
+
379
+ function ConsoleRow({
380
+ entry,
381
+ active,
382
+ onSeek,
383
+ }: {
384
+ entry: ReplayConsoleEntry;
385
+ active: boolean;
386
+ onSeek: (ms: number) => void;
387
+ }) {
388
+ const t = useT();
389
+ const [open, setOpen] = useState(false);
390
+ const hasDetail = Boolean(entry.stack || entry.args.length > 1 || entry.url);
391
+ const bucket = consoleLevelBucket(entry.level);
392
+
393
+ return (
394
+ <Collapsible open={open} onOpenChange={setOpen}>
395
+ <div
396
+ data-entry-id={entry.id}
397
+ className={cn(
398
+ "group flex items-start gap-2 border-b px-3 py-1.5 transition-colors last:border-b-0 hover:bg-muted/50",
399
+ active && "bg-muted",
400
+ )}
401
+ >
402
+ <span className="mt-0.5 w-10 shrink-0 font-mono text-[11px] text-muted-foreground">
403
+ {formatOffsetClock(entry.offsetMs)}
404
+ </span>
405
+ <span
406
+ className={cn(
407
+ "mt-1.5 h-2 w-2 shrink-0 rounded-full",
408
+ bucket === "error" && "bg-red-500",
409
+ bucket === "warn" && "bg-amber-500",
410
+ bucket === "info" && "bg-sky-500",
411
+ bucket === "log" && "bg-muted-foreground/50",
412
+ )}
413
+ aria-hidden
414
+ />
415
+ <span className="min-w-0 flex-1">
416
+ <span
417
+ className={cn(
418
+ "block truncate font-mono text-xs",
419
+ bucket === "error" && "text-red-600 dark:text-red-400",
420
+ bucket === "warn" && "text-amber-600 dark:text-amber-400",
421
+ bucket !== "error" && bucket !== "warn" && "text-foreground/80",
422
+ )}
423
+ title={entry.message}
424
+ >
425
+ {entry.message}
426
+ </span>
427
+ </span>
428
+ {entry.repeat > 1 ? (
429
+ <span className="mt-0.5 inline-flex shrink-0 items-center rounded-full bg-muted px-1.5 font-mono text-[10px] text-muted-foreground">
430
+ {t("sessions.devtoolsRepeatCount", {
431
+ count: String(entry.repeat),
432
+ })}
433
+ </span>
434
+ ) : null}
435
+ {entry.source !== "console" ? (
436
+ <span className="mt-0.5 inline-flex shrink-0 items-center rounded border px-1 font-mono text-[10px] text-muted-foreground">
437
+ {entry.source}
438
+ </span>
439
+ ) : null}
440
+ <JumpToButton offsetMs={entry.offsetMs} onSeek={onSeek} />
441
+ {hasDetail ? (
442
+ <CollapsibleTrigger asChild>
443
+ <button
444
+ type="button"
445
+ className="mt-0.5 shrink-0 rounded p-0.5 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
446
+ aria-label={t("sessions.devtoolsToggleDetails")}
447
+ >
448
+ <IconChevronRight
449
+ className={cn(
450
+ "h-3.5 w-3.5 transition-transform rtl:-scale-x-100",
451
+ open && "rotate-90 rtl:scale-x-100",
452
+ )}
453
+ />
454
+ </button>
455
+ </CollapsibleTrigger>
456
+ ) : null}
457
+ </div>
458
+ {hasDetail ? (
459
+ <CollapsibleContent>
460
+ <div className="space-y-2 border-b bg-muted/30 px-3 py-2 ps-[3.75rem]">
461
+ {entry.url ? (
462
+ <p
463
+ className="truncate font-mono text-[11px] text-muted-foreground"
464
+ title={entry.url}
465
+ >
466
+ {entry.url}
467
+ </p>
468
+ ) : null}
469
+ {entry.args.length > 1 ? (
470
+ <div>
471
+ <p className="mb-1 text-[10px] font-semibold uppercase tracking-wide text-muted-foreground">
472
+ {t("sessions.devtoolsArgs")}
473
+ </p>
474
+ <pre className="overflow-x-auto whitespace-pre-wrap break-all font-mono text-[11px] text-foreground/80">
475
+ {entry.args.join("\n")}
476
+ </pre>
477
+ </div>
478
+ ) : null}
479
+ {entry.stack ? (
480
+ <div>
481
+ <p className="mb-1 text-[10px] font-semibold uppercase tracking-wide text-muted-foreground">
482
+ {t("sessions.devtoolsStack")}
483
+ </p>
484
+ <pre className="max-h-40 overflow-auto whitespace-pre-wrap break-all font-mono text-[11px] text-muted-foreground">
485
+ {entry.stack}
486
+ </pre>
487
+ </div>
488
+ ) : null}
489
+ </div>
490
+ </CollapsibleContent>
491
+ ) : null}
492
+ </Collapsible>
493
+ );
494
+ }
495
+
496
+ function NetworkRow({
497
+ entry,
498
+ active,
499
+ onSeek,
500
+ }: {
501
+ entry: ReplayNetworkEntry;
502
+ active: boolean;
503
+ onSeek: (ms: number) => void;
504
+ }) {
505
+ const t = useT();
506
+ const displayUrl = middleTruncate(networkDisplayUrl(entry.url), 72);
507
+
508
+ return (
509
+ <div
510
+ data-entry-id={entry.id}
511
+ className={cn(
512
+ "group flex items-center gap-2 border-b px-3 py-1.5 transition-colors last:border-b-0 hover:bg-muted/50",
513
+ active && "bg-muted",
514
+ )}
515
+ >
516
+ <span className="w-10 shrink-0 font-mono text-[11px] text-muted-foreground">
517
+ {formatOffsetClock(entry.offsetMs)}
518
+ </span>
519
+ <span
520
+ className={cn(
521
+ "w-12 shrink-0 font-mono text-[11px] font-semibold",
522
+ entry.failed
523
+ ? "text-red-600 dark:text-red-400"
524
+ : "text-muted-foreground",
525
+ )}
526
+ >
527
+ {entry.status > 0 ? entry.status : t("sessions.devtoolsFailedStatus")}
528
+ </span>
529
+ <span className="w-12 shrink-0 font-mono text-[11px] text-muted-foreground">
530
+ {entry.method}
531
+ </span>
532
+ <span className="w-10 shrink-0 font-mono text-[10px] uppercase text-muted-foreground/70">
533
+ {entry.api === "xhr" ? "XHR" : "fetch"}
534
+ </span>
535
+ <span
536
+ className="min-w-0 flex-1 truncate font-mono text-xs text-foreground/80"
537
+ title={entry.url}
538
+ >
539
+ {displayUrl}
540
+ </span>
541
+ {entry.error ? (
542
+ <span
543
+ className="max-w-32 shrink-0 truncate font-mono text-[11px] text-red-600 dark:text-red-400"
544
+ title={entry.error}
545
+ >
546
+ {entry.error}
547
+ </span>
548
+ ) : null}
549
+ <span className="w-14 shrink-0 text-end font-mono text-[11px] text-muted-foreground">
550
+ {t("sessions.devtoolsDurationMs", {
551
+ ms: String(entry.durationMs),
552
+ })}
553
+ </span>
554
+ <JumpToButton offsetMs={entry.offsetMs} onSeek={onSeek} />
555
+ </div>
556
+ );
557
+ }