@agent-native/core 0.84.67 → 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 (145) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +14 -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/core-routes-plugin.ts +91 -4
  30. package/corpus/core/src/server/http-response-telemetry.ts +189 -0
  31. package/corpus/core/src/server/self-dispatch.ts +29 -0
  32. package/corpus/templates/analytics/.agents/skills/session-replay/SKILL.md +53 -1
  33. package/corpus/templates/analytics/AGENTS.md +13 -0
  34. package/corpus/templates/analytics/actions/view-screen.ts +25 -0
  35. package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +20 -0
  36. package/corpus/templates/analytics/app/global.css +55 -0
  37. package/corpus/templates/analytics/app/i18n/zh-TW.ts +100 -1
  38. package/corpus/templates/analytics/app/i18n-data.ts +1226 -0
  39. package/corpus/templates/analytics/app/pages/Settings.tsx +3 -0
  40. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/EmailReportDialog.tsx +182 -144
  41. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx +11 -18
  42. package/corpus/templates/analytics/app/pages/analyses/AnalysisDetail.tsx +11 -18
  43. package/corpus/templates/analytics/app/pages/sessions/SessionDetailPage.tsx +49 -1
  44. package/corpus/templates/analytics/app/pages/sessions/SessionDevToolsPanel.tsx +557 -0
  45. package/corpus/templates/analytics/app/pages/sessions/session-replay-devtools.ts +274 -0
  46. package/corpus/templates/analytics/app/pages/settings/AlertRulesSettingsCard.tsx +974 -0
  47. package/corpus/templates/analytics/changelog/2026-07-02-alert-rules-can-now-be-viewed-and-managed-from-settings.md +6 -0
  48. package/corpus/templates/analytics/changelog/2026-07-02-recent-ask-chats-show-a-loading-placeholder-while-your-chat-.md +6 -0
  49. package/corpus/templates/analytics/changelog/2026-07-02-session-replays-now-capture-console-logs-and-network-request.md +6 -0
  50. package/corpus/templates/analytics/changelog/2026-07-02-sharing-status-labels-use-neutral-icons-instead-of-colored-d.md +6 -0
  51. package/corpus/templates/analytics/changelog/2026-07-02-the-session-replay-viewer-has-a-dev-tools-panel-with-console.md +6 -0
  52. package/corpus/templates/analytics/server/db/schema.ts +3 -0
  53. package/corpus/templates/analytics/server/jobs/analytics-alerts.ts +4 -0
  54. package/corpus/templates/analytics/server/lib/analytics-alerts.ts +142 -1
  55. package/corpus/templates/analytics/server/lib/session-replay-agent-context.ts +369 -8
  56. package/corpus/templates/analytics/server/lib/session-replay.ts +76 -2
  57. package/corpus/templates/analytics/server/routes/api/session-replay/agent-diagnostics.json.get.ts +101 -0
  58. package/corpus/templates/analytics/shared/session-replay-diagnostics.ts +78 -0
  59. package/corpus/templates/design/app/pages/DesignEditor.tsx +24 -12
  60. package/corpus/templates/design/changelog/2026-07-02-the-temporary-code-tab-is-hidden-from-the-design-editor-side.md +6 -0
  61. package/corpus/templates/plan/actions/create-visual-recap.ts +1 -1
  62. package/corpus/templates/plan/actions/update-local-plan-folder.ts +3 -1
  63. package/corpus/templates/plan/actions/update-visual-plan.ts +1 -1
  64. package/corpus/templates/plan/changelog/2026-07-02-trying-to-edit-a-recap-or-plan-you-can-only-view-now-explain.md +6 -0
  65. package/corpus/templates/plan/server/plans.ts +26 -9
  66. package/dist/agent/production-agent.d.ts.map +1 -1
  67. package/dist/agent/production-agent.js +155 -34
  68. package/dist/agent/production-agent.js.map +1 -1
  69. package/dist/agent/run-manager.d.ts +29 -0
  70. package/dist/agent/run-manager.d.ts.map +1 -1
  71. package/dist/agent/run-manager.js +73 -0
  72. package/dist/agent/run-manager.js.map +1 -1
  73. package/dist/agent/run-store.d.ts +41 -0
  74. package/dist/agent/run-store.d.ts.map +1 -1
  75. package/dist/agent/run-store.js +100 -4
  76. package/dist/agent/run-store.js.map +1 -1
  77. package/dist/agent/types.d.ts +13 -0
  78. package/dist/agent/types.d.ts.map +1 -1
  79. package/dist/agent/types.js.map +1 -1
  80. package/dist/client/RunStuckBanner.d.ts.map +1 -1
  81. package/dist/client/RunStuckBanner.js +13 -0
  82. package/dist/client/RunStuckBanner.js.map +1 -1
  83. package/dist/client/agent-chat-adapter.d.ts.map +1 -1
  84. package/dist/client/agent-chat-adapter.js +410 -0
  85. package/dist/client/agent-chat-adapter.js.map +1 -1
  86. package/dist/client/analytics.d.ts +1 -1
  87. package/dist/client/analytics.d.ts.map +1 -1
  88. package/dist/client/analytics.js.map +1 -1
  89. package/dist/client/error-format.d.ts.map +1 -1
  90. package/dist/client/error-format.js +2 -0
  91. package/dist/client/error-format.js.map +1 -1
  92. package/dist/client/extensions/ExtensionsSidebarSection.js +1 -1
  93. package/dist/client/extensions/ExtensionsSidebarSection.js.map +1 -1
  94. package/dist/client/session-replay.d.ts +36 -0
  95. package/dist/client/session-replay.d.ts.map +1 -1
  96. package/dist/client/session-replay.js +579 -1
  97. package/dist/client/session-replay.js.map +1 -1
  98. package/dist/client/sse-event-processor.d.ts +20 -1
  99. package/dist/client/sse-event-processor.d.ts.map +1 -1
  100. package/dist/client/sse-event-processor.js +42 -11
  101. package/dist/client/sse-event-processor.js.map +1 -1
  102. package/dist/client/use-run-stuck-detection.d.ts +14 -4
  103. package/dist/client/use-run-stuck-detection.d.ts.map +1 -1
  104. package/dist/client/use-run-stuck-detection.js +18 -4
  105. package/dist/client/use-run-stuck-detection.js.map +1 -1
  106. package/dist/collab/awareness.d.ts +2 -2
  107. package/dist/collab/awareness.d.ts.map +1 -1
  108. package/dist/collab/routes.d.ts +2 -2
  109. package/dist/db/runtime-diagnostics.d.ts +52 -0
  110. package/dist/db/runtime-diagnostics.d.ts.map +1 -0
  111. package/dist/db/runtime-diagnostics.js +250 -0
  112. package/dist/db/runtime-diagnostics.js.map +1 -0
  113. package/dist/notifications/routes.d.ts +1 -1
  114. package/dist/observability/routes.d.ts +3 -3
  115. package/dist/progress/routes.d.ts +1 -1
  116. package/dist/provider-api/corpus-jobs.d.ts +2 -2
  117. package/dist/resources/handlers.d.ts +1 -1
  118. package/dist/secrets/routes.d.ts +3 -3
  119. package/dist/server/agent-chat-plugin.d.ts.map +1 -1
  120. package/dist/server/agent-chat-plugin.js +101 -2
  121. package/dist/server/agent-chat-plugin.js.map +1 -1
  122. package/dist/server/core-routes-plugin.d.ts +17 -0
  123. package/dist/server/core-routes-plugin.d.ts.map +1 -1
  124. package/dist/server/core-routes-plugin.js +65 -5
  125. package/dist/server/core-routes-plugin.js.map +1 -1
  126. package/dist/server/http-response-telemetry.d.ts +4 -0
  127. package/dist/server/http-response-telemetry.d.ts.map +1 -0
  128. package/dist/server/http-response-telemetry.js +174 -0
  129. package/dist/server/http-response-telemetry.js.map +1 -0
  130. package/dist/server/self-dispatch.d.ts +17 -0
  131. package/dist/server/self-dispatch.d.ts.map +1 -1
  132. package/dist/server/self-dispatch.js +11 -0
  133. package/dist/server/self-dispatch.js.map +1 -1
  134. package/docs/content/locales/ar-SA/tracking.mdx +55 -0
  135. package/docs/content/locales/de-DE/tracking.mdx +55 -0
  136. package/docs/content/locales/es-ES/tracking.mdx +55 -0
  137. package/docs/content/locales/fr-FR/tracking.mdx +55 -0
  138. package/docs/content/locales/hi-IN/tracking.mdx +55 -0
  139. package/docs/content/locales/ja-JP/tracking.mdx +55 -0
  140. package/docs/content/locales/ko-KR/tracking.mdx +55 -0
  141. package/docs/content/locales/pt-BR/tracking.mdx +55 -0
  142. package/docs/content/locales/zh-CN/tracking.mdx +55 -0
  143. package/docs/content/locales/zh-TW/tracking.mdx +55 -0
  144. package/docs/content/tracking.mdx +2 -0
  145. package/package.json +1 -1
@@ -43,6 +43,7 @@ import { useReplayStorageStatus } from "@/hooks/use-replay-storage-status";
43
43
  import { cn } from "@/lib/utils";
44
44
 
45
45
  import changelog from "../../CHANGELOG.md?raw";
46
+ import { AlertRulesSettingsCard } from "./settings/AlertRulesSettingsCard";
46
47
 
47
48
  const S3_STORAGE_FIELDS = [
48
49
  {
@@ -418,6 +419,8 @@ export default function Settings() {
418
419
  </CardContent>
419
420
  </Card>
420
421
 
422
+ <AlertRulesSettingsCard />
423
+
421
424
  <Card className="bg-card border-border/50">
422
425
  <CardHeader>
423
426
  <CardTitle className="text-base">
@@ -5,9 +5,8 @@ import {
5
5
  useT,
6
6
  } from "@agent-native/core/client";
7
7
  import {
8
+ IconChevronLeft,
8
9
  IconFilter,
9
- IconMail,
10
- IconPencil,
11
10
  IconPlus,
12
11
  IconSend,
13
12
  IconTrash,
@@ -50,6 +49,8 @@ type DashboardReportSubscription = {
50
49
  lastError: string | null;
51
50
  };
52
51
 
52
+ type DialogView = "list" | "form";
53
+
53
54
  interface EmailReportDialogProps {
54
55
  open: boolean;
55
56
  onOpenChange: (open: boolean) => void;
@@ -98,6 +99,7 @@ export function EmailReportDialog({
98
99
  const defaultEmail = session?.email ?? "";
99
100
  const defaultName = `${dashboardName} daily email`;
100
101
 
102
+ const [view, setView] = useState<DialogView>("form");
101
103
  const [selectedId, setSelectedId] = useState<string | undefined>();
102
104
  const [name, setName] = useState(defaultName);
103
105
  const [recipientsText, setRecipientsText] = useState(defaultEmail);
@@ -155,6 +157,7 @@ export function EmailReportDialog({
155
157
  setTimeOfDay(sub.timeOfDay);
156
158
  setTimezone(sub.timezone);
157
159
  setEnabled(sub.enabled);
160
+ setView("form");
158
161
  };
159
162
 
160
163
  const resetForm = () => {
@@ -167,6 +170,16 @@ export function EmailReportDialog({
167
170
  setEnabled(true);
168
171
  };
169
172
 
173
+ const startNewSubscription = () => {
174
+ resetForm();
175
+ setView("form");
176
+ };
177
+
178
+ const returnToList = () => {
179
+ resetForm();
180
+ setView("list");
181
+ };
182
+
170
183
  useEffect(() => {
171
184
  if (!open) {
172
185
  initialized.current = false;
@@ -174,9 +187,11 @@ export function EmailReportDialog({
174
187
  }
175
188
  if (initialized.current || isLoading) return;
176
189
  if (subscriptions[0]) {
177
- loadSubscription(subscriptions[0]);
190
+ resetForm();
191
+ setView("list");
178
192
  } else {
179
193
  resetForm();
194
+ setView("form");
180
195
  }
181
196
  initialized.current = true;
182
197
  }, [open, isLoading, subscriptions, defaultEmail, defaultName]);
@@ -213,8 +228,12 @@ export function EmailReportDialog({
213
228
  const handleDelete = async () => {
214
229
  if (!selectedId) return;
215
230
  try {
231
+ const hasRemainingSubscriptions = subscriptions.some(
232
+ (sub) => sub.id !== selectedId,
233
+ );
216
234
  await deleteSubscription.mutateAsync({ id: selectedId });
217
235
  resetForm();
236
+ setView(hasRemainingSubscriptions ? "list" : "form");
218
237
  await refetch();
219
238
  toast.success(t("sqlDashboard.reportSubscriptionDeleted"));
220
239
  } catch (err: any) {
@@ -241,6 +260,9 @@ export function EmailReportDialog({
241
260
  }
242
261
  };
243
262
 
263
+ const isListView = subscriptions.length > 0 && view === "list";
264
+ const isFormView = !isListView;
265
+
244
266
  return (
245
267
  <Dialog open={open} onOpenChange={onOpenChange}>
246
268
  <DialogContent className="sm:max-w-[560px] max-h-[90vh] overflow-y-auto">
@@ -253,30 +275,26 @@ export function EmailReportDialog({
253
275
  </DialogDescription>
254
276
  </DialogHeader>
255
277
 
256
- <div className="space-y-4">
257
- <div className="space-y-2">
258
- <div className="flex items-center justify-between gap-3">
259
- <Label>{t("sqlDashboard.existingReportSubscriptions")}</Label>
260
- <Button variant="outline" size="sm" onClick={resetForm}>
261
- <IconPlus className="mr-1.5 h-3.5 w-3.5" />
262
- {t("sqlDashboard.newSubscription")}
263
- </Button>
264
- </div>
265
- {subscriptions.length === 0 ? (
266
- <div className="rounded-md border border-dashed px-3 py-4 text-sm text-muted-foreground">
267
- {t("sqlDashboard.noReportSubscriptions")}
278
+ <div className="space-y-5">
279
+ {isListView ? (
280
+ <div className="space-y-3">
281
+ <div className="flex items-center justify-between gap-3">
282
+ <Label>{t("sqlDashboard.existingReportSubscriptions")}</Label>
283
+ <Button
284
+ variant="outline"
285
+ size="sm"
286
+ onClick={startNewSubscription}
287
+ >
288
+ <IconPlus className="mr-1.5 h-3.5 w-3.5" />
289
+ {t("sqlDashboard.newSubscription")}
290
+ </Button>
268
291
  </div>
269
- ) : (
270
292
  <div className="space-y-2">
271
293
  {subscriptions.map((sub) => (
272
294
  <button
273
295
  key={sub.id}
274
296
  type="button"
275
- className={`flex w-full items-center justify-between gap-3 rounded-md border px-3 py-2 text-left transition-colors ${
276
- sub.id === selectedId
277
- ? "border-primary bg-primary/5"
278
- : "hover:bg-muted/60"
279
- }`}
297
+ className="flex w-full items-center justify-between gap-3 rounded-md border px-3 py-2 text-left transition-colors hover:bg-muted/60 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
280
298
  onClick={() => loadSubscription(sub)}
281
299
  >
282
300
  <span className="min-w-0">
@@ -293,144 +311,164 @@ export function EmailReportDialog({
293
311
  </button>
294
312
  ))}
295
313
  </div>
296
- )}
297
- </div>
298
-
299
- <div className="grid gap-4 sm:grid-cols-2">
300
- <div className="space-y-2 sm:col-span-2">
301
- <Label htmlFor="dashboard-report-name">
302
- {t("sqlDashboard.reportName")}
303
- </Label>
304
- <Input
305
- id="dashboard-report-name"
306
- value={name}
307
- onChange={(event) => setName(event.target.value)}
308
- />
309
314
  </div>
315
+ ) : null}
310
316
 
311
- <div className="space-y-2 sm:col-span-2">
312
- <Label htmlFor="dashboard-report-recipients">
313
- {t("sqlDashboard.reportRecipients")}
314
- </Label>
315
- <Textarea
316
- id="dashboard-report-recipients"
317
- value={recipientsText}
318
- onChange={(event) => setRecipientsText(event.target.value)}
319
- placeholder={t("sqlDashboard.reportRecipientsPlaceholder")}
320
- className="min-h-[76px]"
321
- />
322
- </div>
317
+ {isFormView ? (
318
+ <>
319
+ <div className="flex items-center justify-between gap-3">
320
+ {subscriptions.length > 0 ? (
321
+ <Button
322
+ variant="ghost"
323
+ size="sm"
324
+ className="-ml-2 px-2"
325
+ onClick={returnToList}
326
+ >
327
+ <IconChevronLeft className="mr-1.5 h-3.5 w-3.5" />
328
+ {t("sqlDashboard.existingReportSubscriptions")}
329
+ </Button>
330
+ ) : null}
331
+ <p className="text-sm font-medium">
332
+ {selectedId
333
+ ? t("sqlDashboard.updateSubscription")
334
+ : t("sqlDashboard.newSubscription")}
335
+ </p>
336
+ </div>
323
337
 
324
- <div className="space-y-2">
325
- <Label htmlFor="dashboard-report-time">
326
- {t("sqlDashboard.reportSendTime")}
327
- </Label>
328
- <Input
329
- id="dashboard-report-time"
330
- type="time"
331
- value={timeOfDay}
332
- onChange={(event) => setTimeOfDay(event.target.value)}
333
- />
334
- </div>
338
+ <div className="grid gap-4 sm:grid-cols-2">
339
+ <div className="space-y-2 sm:col-span-2">
340
+ <Label htmlFor="dashboard-report-name">
341
+ {t("sqlDashboard.reportName")}
342
+ </Label>
343
+ <Input
344
+ id="dashboard-report-name"
345
+ value={name}
346
+ onChange={(event) => setName(event.target.value)}
347
+ />
348
+ </div>
335
349
 
336
- <div className="space-y-2">
337
- <Label htmlFor="dashboard-report-timezone">
338
- {t("sqlDashboard.reportTimezone")}
339
- </Label>
340
- <Input
341
- id="dashboard-report-timezone"
342
- value={timezone}
343
- onChange={(event) => setTimezone(event.target.value)}
344
- />
345
- </div>
350
+ <div className="space-y-2 sm:col-span-2">
351
+ <Label htmlFor="dashboard-report-recipients">
352
+ {t("sqlDashboard.reportRecipients")}
353
+ </Label>
354
+ <Textarea
355
+ id="dashboard-report-recipients"
356
+ value={recipientsText}
357
+ onChange={(event) => setRecipientsText(event.target.value)}
358
+ placeholder={t("sqlDashboard.reportRecipientsPlaceholder")}
359
+ className="min-h-[76px]"
360
+ />
361
+ </div>
346
362
 
347
- <div className="flex items-center justify-between gap-3 rounded-md border px-3 py-2 sm:col-span-2">
348
- <div className="min-w-0">
349
- <Label>{t("sqlDashboard.reportEnabled")}</Label>
350
- <p className="text-xs text-muted-foreground">
351
- {t("sqlDashboard.reportFilterSnapshot", {
352
- count: reportFilterCount,
353
- })}
354
- </p>
355
- </div>
356
- <div className="flex shrink-0 items-center gap-2">
357
- <Button
358
- type="button"
359
- variant="outline"
360
- size="sm"
361
- onClick={() => setReportFilters(currentFilterSnapshot)}
362
- disabled={reportFiltersMatchCurrent}
363
- >
364
- <IconFilter className="mr-1.5 h-3.5 w-3.5" />
365
- {t("sqlDashboard.reportUseCurrentFilters")}
366
- </Button>
367
- <Switch checked={enabled} onCheckedChange={setEnabled} />
368
- </div>
369
- </div>
363
+ <div className="space-y-2">
364
+ <Label htmlFor="dashboard-report-time">
365
+ {t("sqlDashboard.reportSendTime")}
366
+ </Label>
367
+ <Input
368
+ id="dashboard-report-time"
369
+ type="time"
370
+ value={timeOfDay}
371
+ onChange={(event) => setTimeOfDay(event.target.value)}
372
+ />
373
+ </div>
370
374
 
371
- {selectedSubscription ? (
372
- <div className="grid gap-2 text-xs text-muted-foreground sm:col-span-2 sm:grid-cols-2">
373
- <div>
374
- {t("sqlDashboard.reportLastRun")}:{" "}
375
- {formatDate(
376
- selectedSubscription.lastRunAt,
377
- t("sqlDashboard.never"),
378
- )}
375
+ <div className="space-y-2">
376
+ <Label htmlFor="dashboard-report-timezone">
377
+ {t("sqlDashboard.reportTimezone")}
378
+ </Label>
379
+ <Input
380
+ id="dashboard-report-timezone"
381
+ value={timezone}
382
+ onChange={(event) => setTimezone(event.target.value)}
383
+ />
379
384
  </div>
380
- <div>
381
- {t("sqlDashboard.reportNextRun")}:{" "}
382
- {formatDate(
383
- selectedSubscription.nextRunAt,
384
- t("sqlDashboard.never"),
385
- )}
385
+
386
+ <div className="flex items-center justify-between gap-3 rounded-md border px-3 py-2 sm:col-span-2">
387
+ <div className="min-w-0">
388
+ <Label>{t("sqlDashboard.reportEnabled")}</Label>
389
+ <p className="text-xs text-muted-foreground">
390
+ {t("sqlDashboard.reportFilterSnapshot", {
391
+ count: reportFilterCount,
392
+ })}
393
+ </p>
394
+ </div>
395
+ <div className="flex shrink-0 items-center gap-2">
396
+ <Button
397
+ type="button"
398
+ variant="outline"
399
+ size="sm"
400
+ onClick={() => setReportFilters(currentFilterSnapshot)}
401
+ disabled={reportFiltersMatchCurrent}
402
+ >
403
+ <IconFilter className="mr-1.5 h-3.5 w-3.5" />
404
+ {t("sqlDashboard.reportUseCurrentFilters")}
405
+ </Button>
406
+ <Switch checked={enabled} onCheckedChange={setEnabled} />
407
+ </div>
386
408
  </div>
409
+
410
+ {selectedSubscription ? (
411
+ <div className="grid gap-2 text-xs text-muted-foreground sm:col-span-2 sm:grid-cols-2">
412
+ <div>
413
+ {t("sqlDashboard.reportLastRun")}:{" "}
414
+ {formatDate(
415
+ selectedSubscription.lastRunAt,
416
+ t("sqlDashboard.never"),
417
+ )}
418
+ </div>
419
+ <div>
420
+ {t("sqlDashboard.reportNextRun")}:{" "}
421
+ {formatDate(
422
+ selectedSubscription.nextRunAt,
423
+ t("sqlDashboard.never"),
424
+ )}
425
+ </div>
426
+ </div>
427
+ ) : null}
387
428
  </div>
388
- ) : null}
389
- </div>
429
+ </>
430
+ ) : null}
390
431
  </div>
391
432
 
392
- <DialogFooter className="gap-2 sm:gap-0">
393
- {selectedId ? (
394
- <Button
395
- variant="outline"
396
- size="sm"
397
- onClick={handleDelete}
398
- disabled={deleteSubscription.isPending}
399
- >
400
- <IconTrash className="mr-1.5 h-3.5 w-3.5" />
401
- {t("sidebar.delete")}
402
- </Button>
403
- ) : null}
404
- {selectedId ? (
433
+ {isFormView ? (
434
+ <DialogFooter className="gap-2 sm:gap-0">
435
+ {selectedId ? (
436
+ <Button
437
+ variant="outline"
438
+ size="sm"
439
+ onClick={handleDelete}
440
+ disabled={deleteSubscription.isPending}
441
+ >
442
+ <IconTrash className="mr-1.5 h-3.5 w-3.5" />
443
+ {t("sidebar.delete")}
444
+ </Button>
445
+ ) : null}
446
+ {selectedId ? (
447
+ <Button
448
+ variant="outline"
449
+ size="sm"
450
+ onClick={handleSendNow}
451
+ disabled={sendNow.isPending}
452
+ >
453
+ <IconSend className="mr-1.5 h-3.5 w-3.5" />
454
+ {sendNow.isPending
455
+ ? t("sqlDashboard.reportSending")
456
+ : t("sqlDashboard.reportSendNow")}
457
+ </Button>
458
+ ) : null}
405
459
  <Button
406
- variant="outline"
407
460
  size="sm"
408
- onClick={handleSendNow}
409
- disabled={sendNow.isPending}
461
+ onClick={handleSave}
462
+ disabled={saveSubscription.isPending}
410
463
  >
411
- <IconSend className="mr-1.5 h-3.5 w-3.5" />
412
- {sendNow.isPending
413
- ? t("sqlDashboard.reportSending")
414
- : t("sqlDashboard.reportSendNow")}
464
+ {saveSubscription.isPending
465
+ ? t("sqlDashboard.saving")
466
+ : selectedId
467
+ ? t("sqlDashboard.updateSubscription")
468
+ : t("sqlDashboard.saveSubscription")}
415
469
  </Button>
416
- ) : null}
417
- <Button
418
- size="sm"
419
- onClick={handleSave}
420
- disabled={saveSubscription.isPending}
421
- >
422
- {selectedId ? (
423
- <IconPencil className="mr-1.5 h-3.5 w-3.5" />
424
- ) : (
425
- <IconMail className="mr-1.5 h-3.5 w-3.5" />
426
- )}
427
- {saveSubscription.isPending
428
- ? t("sqlDashboard.saving")
429
- : selectedId
430
- ? t("sqlDashboard.updateSubscription")
431
- : t("sqlDashboard.saveSubscription")}
432
- </Button>
433
- </DialogFooter>
470
+ </DialogFooter>
471
+ ) : null}
434
472
  </DialogContent>
435
473
  </Dialog>
436
474
  );
@@ -27,17 +27,20 @@ import {
27
27
  } from "@dnd-kit/core";
28
28
  import {
29
29
  IconArchive,
30
+ IconBuilding,
30
31
  IconClock,
31
32
  IconDotsVertical,
32
33
  IconEye,
33
34
  IconEyeOff,
34
35
  IconGripVertical,
35
36
  IconInfoCircle,
37
+ IconLock,
36
38
  IconMail,
37
39
  IconPencil,
38
40
  IconPlus,
39
41
  IconTrash,
40
42
  IconUser,
43
+ IconWorld,
41
44
  IconX,
42
45
  } from "@tabler/icons-react";
43
46
  import { useQuery, useQueryClient } from "@tanstack/react-query";
@@ -1422,24 +1425,14 @@ export default function SqlDashboardPage() {
1422
1425
  </span>
1423
1426
  )}
1424
1427
  {dashboardVisibility ? (
1425
- <span
1426
- className={`flex items-center gap-1.5 font-medium ${
1427
- dashboardVisibility === "public"
1428
- ? "text-green-600"
1429
- : dashboardVisibility === "org"
1430
- ? "text-blue-600"
1431
- : "text-yellow-600"
1432
- }`}
1433
- >
1434
- <span
1435
- className={`h-1.5 w-1.5 rounded-full ${
1436
- dashboardVisibility === "public"
1437
- ? "bg-green-500"
1438
- : dashboardVisibility === "org"
1439
- ? "bg-blue-500"
1440
- : "bg-yellow-500"
1441
- }`}
1442
- />
1428
+ <span className="flex items-center gap-1.5">
1429
+ {dashboardVisibility === "public" ? (
1430
+ <IconWorld className="h-3 w-3" />
1431
+ ) : dashboardVisibility === "org" ? (
1432
+ <IconBuilding className="h-3 w-3" />
1433
+ ) : (
1434
+ <IconLock className="h-3 w-3" />
1435
+ )}
1443
1436
  {dashboardVisibility === "public"
1444
1437
  ? t("sqlDashboard.public")
1445
1438
  : dashboardVisibility === "org"
@@ -11,7 +11,10 @@ import {
11
11
  IconTrash,
12
12
  IconClock,
13
13
  IconArrowLeft,
14
+ IconBuilding,
14
15
  IconDatabase,
16
+ IconLock,
17
+ IconWorld,
15
18
  } from "@tabler/icons-react";
16
19
  import { useQuery, useQueryClient } from "@tanstack/react-query";
17
20
  import { useEffect } from "react";
@@ -288,24 +291,14 @@ export default function AnalysisDetail() {
288
291
  {analysis.author && (
289
292
  <span>{t("analyses.byAuthor", { author: analysis.author })}</span>
290
293
  )}
291
- <span
292
- className={`flex items-center gap-1.5 font-medium ${
293
- analysis.visibility === "public"
294
- ? "text-green-600"
295
- : analysis.visibility === "org"
296
- ? "text-blue-600"
297
- : "text-yellow-600"
298
- }`}
299
- >
300
- <span
301
- className={`h-1.5 w-1.5 rounded-full ${
302
- analysis.visibility === "public"
303
- ? "bg-green-500"
304
- : analysis.visibility === "org"
305
- ? "bg-blue-500"
306
- : "bg-yellow-500"
307
- }`}
308
- />
294
+ <span className="flex items-center gap-1.5">
295
+ {analysis.visibility === "public" ? (
296
+ <IconWorld className="h-3 w-3" />
297
+ ) : analysis.visibility === "org" ? (
298
+ <IconBuilding className="h-3 w-3" />
299
+ ) : (
300
+ <IconLock className="h-3 w-3" />
301
+ )}
309
302
  {analysis.visibility === "public"
310
303
  ? t("analyses.public")
311
304
  : analysis.visibility === "org"
@@ -21,6 +21,7 @@ import {
21
21
  IconPlayerSkipForward,
22
22
  IconPlayerTrackNext,
23
23
  IconRoute,
24
+ IconTerminal2,
24
25
  IconTimelineEvent,
25
26
  } from "@tabler/icons-react";
26
27
  import { useQuery } from "@tanstack/react-query";
@@ -51,6 +52,9 @@ import {
51
52
  import { getIdToken } from "@/lib/auth";
52
53
  import { cn } from "@/lib/utils";
53
54
 
55
+ import { extractReplayDiagnostics } from "./session-replay-devtools";
56
+ import { SessionDevToolsPanel } from "./SessionDevToolsPanel";
57
+
54
58
  type SessionRecordingSummary = {
55
59
  id: string;
56
60
  clientRecordingId: string;
@@ -393,6 +397,7 @@ function ReplayPlayer({
393
397
  const [totalTime, setTotalTime] = useState(0);
394
398
  const [speed, setSpeed] = useState(DEFAULT_SPEED);
395
399
  const [skipInactive, setSkipInactive] = useState(true);
400
+ const [devToolsOpen, setDevToolsOpen] = useState(false);
396
401
  const [streamedDims, setStreamedDims] = useState<{
397
402
  width: number;
398
403
  height: number;
@@ -414,6 +419,9 @@ function ReplayPlayer({
414
419
  () => currentUrlAt(events, currentTime),
415
420
  [events, currentTime],
416
421
  );
422
+ const diagnostics = useMemo(() => extractReplayDiagnostics(events), [events]);
423
+ const devToolsIssueCount =
424
+ diagnostics.consoleErrorCount + diagnostics.networkFailedCount;
417
425
 
418
426
  useEffect(() => {
419
427
  const el = stageAreaRef.current;
@@ -494,7 +502,14 @@ function ReplayPlayer({
494
502
  showWarning: false,
495
503
  showDebug: false,
496
504
  triggerFocus: false,
497
- mouseTail: false,
505
+ // Subtle FullStory-style trail behind the visitor cursor. The canvas
506
+ // is resized alongside the iframe in applyReplayFrameDimensions.
507
+ mouseTail: {
508
+ strokeStyle: "rgba(59, 130, 246, 0.35)",
509
+ lineWidth: 2,
510
+ duration: 600,
511
+ lineCap: "round",
512
+ },
498
513
  insertStyleRules: [
499
514
  "[data-radix-popper-content-wrapper], .Toastify, [class*='toast'], [class*='Toast'] { display: none !important; }",
500
515
  ],
@@ -793,6 +808,31 @@ function ReplayPlayer({
793
808
  : t("sessions.skipInactiveOff")}
794
809
  </TooltipContent>
795
810
  </Tooltip>
811
+
812
+ <button
813
+ type="button"
814
+ className={cn(
815
+ "inline-flex h-8 items-center gap-1.5 rounded-md border px-2 text-xs font-medium transition-colors hover:bg-muted",
816
+ devToolsOpen && "border-primary/40 bg-primary/10 text-primary",
817
+ )}
818
+ onClick={() => setDevToolsOpen((value) => !value)}
819
+ aria-pressed={devToolsOpen}
820
+ aria-expanded={devToolsOpen}
821
+ >
822
+ <IconTerminal2 className="h-4 w-4" />
823
+ {t("sessions.devtools")}
824
+ {devToolsIssueCount > 0 ? (
825
+ <span
826
+ className="inline-flex h-4 min-w-4 items-center justify-center rounded-full bg-red-500 px-1 font-mono text-[10px] font-semibold leading-none text-white"
827
+ aria-label={t("sessions.devtoolsIssueCount", {
828
+ count: String(devToolsIssueCount),
829
+ })}
830
+ >
831
+ {devToolsIssueCount > 99 ? "99+" : devToolsIssueCount}
832
+ </span>
833
+ ) : null}
834
+ </button>
835
+
796
836
  <span className="ms-auto hidden text-xs text-muted-foreground lg:inline">
797
837
  {t("sessions.replayEventCount", {
798
838
  events: String(response.eventCount),
@@ -801,6 +841,14 @@ function ReplayPlayer({
801
841
  </span>
802
842
  </div>
803
843
 
844
+ {devToolsOpen ? (
845
+ <SessionDevToolsPanel
846
+ diagnostics={diagnostics}
847
+ currentTime={currentTime}
848
+ onSeek={(ms) => seek(ms, true)}
849
+ />
850
+ ) : null}
851
+
804
852
  {response.unavailableChunks > 0 ? (
805
853
  <div className="border-t px-4 py-2 text-xs text-muted-foreground">
806
854
  {t("sessions.unavailableChunks", {