@agent-native/core 0.187.0 → 0.187.1-nightly-20260925003003

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 (158) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +4 -3
  3. package/corpus/templates/analytics/changelog/2026-09-24-analytics-sidebar-footer-aligns-consistently.md +6 -0
  4. package/corpus/templates/analytics/changelog/2026-09-24-dashboard-emails-match-stacked-area-totals.md +6 -0
  5. package/corpus/templates/analytics/server/lib/report-chart-svg.ts +47 -20
  6. package/corpus/templates/calendar/app/components/calendar/AttendeeAutocomplete.tsx +1 -1
  7. package/corpus/templates/calendar/changelog/2026-09-24-guest-inputs-wrap-beneath-existing-guests-so-the-active-inpu.md +6 -0
  8. package/corpus/templates/clips/app/components/editor/stitch-manager.tsx +8 -3
  9. package/corpus/templates/clips/app/lib/recording-link.ts +21 -0
  10. package/corpus/templates/clips/app/routes/record.tsx +27 -8
  11. package/corpus/templates/clips/changelog/2026-09-24-the-camera-bubble-stays-visible-while-you-reposition-it-befo.md +6 -0
  12. package/corpus/templates/clips/desktop/src-tauri/src/clips/mod.rs +7 -0
  13. package/corpus/templates/clips/desktop/src-tauri/src/lib.rs +63 -55
  14. package/corpus/templates/design/.generated/bridge/editor-chrome.generated.ts +251 -74
  15. package/corpus/templates/design/app/components/design/DesignCanvas.tsx +66 -56
  16. package/corpus/templates/design/app/components/design/EditPanel.tsx +19 -0
  17. package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +59 -9
  18. package/corpus/templates/design/app/components/design/edit-panel/interaction-state-helpers.ts +4 -0
  19. package/corpus/templates/design/app/components/design/edit-panel/layout-properties.tsx +119 -0
  20. package/corpus/templates/design/app/components/design/edit-panel/stroke-properties.tsx +48 -51
  21. package/corpus/templates/design/app/components/design/edit-panel/style-change-types.ts +1 -0
  22. package/corpus/templates/design/app/components/design/inspector/AutoLayoutMatrix.tsx +298 -187
  23. package/corpus/templates/design/app/components/design/inspector/GradientEditor.tsx +2 -2
  24. package/corpus/templates/design/app/components/design/inspector/index.ts +4 -0
  25. package/corpus/templates/design/app/components/design/multi-screen/types.ts +4 -2
  26. package/corpus/templates/design/app/i18n/en-US.ts +6 -0
  27. package/corpus/templates/design/app/pages/Present.tsx +101 -87
  28. package/corpus/templates/design/app/pages/design-editor/clone-and-pen-edit.ts +75 -14
  29. package/corpus/templates/design/app/pages/design-editor/commands/README.md +1 -0
  30. package/corpus/templates/design/app/pages/design-editor/commands/commit-relative-style-delta-to-selected-layers.ts +69 -56
  31. package/corpus/templates/design/app/pages/design-editor/commands/mode-change.ts +16 -0
  32. package/corpus/templates/design/app/pages/design-editor/commands/scale-selection.ts +17 -2
  33. package/corpus/templates/design/app/pages/design-editor/commands/styles-change.ts +23 -30
  34. package/corpus/templates/design/app/pages/design-editor/commands/swap-fill-stroke.ts +105 -0
  35. package/corpus/templates/design/app/pages/design-editor/pending-edits.ts +21 -0
  36. package/corpus/templates/design/app/pages/present-review-state.ts +7 -1
  37. package/corpus/templates/design/changelog/2026-09-24-a-vector-s-stroke-row-shows-position-beside-weight-instead-o.md +6 -0
  38. package/corpus/templates/design/changelog/2026-09-24-css-margins-can-be-adjusted-on-the-canvas-and-in-the-layout-.md +6 -0
  39. package/corpus/templates/design/changelog/2026-09-24-design-reviews-show-the-real-canvas-beside-its-conversation.md +6 -0
  40. package/corpus/templates/design/changelog/2026-09-24-double-clicking-text-or-a-vector-on-the-board-starts-editing.md +6 -0
  41. package/corpus/templates/design/changelog/2026-09-24-interact-opens-a-focused-responsive-preview-without-reloadin.md +6 -0
  42. package/corpus/templates/design/changelog/2026-09-24-keep-gradient-values-visible-and-limit-canvas-paints-to-supp.md +6 -0
  43. package/corpus/templates/design/changelog/2026-09-24-open-pen-paths-no-longer-paint-a-filled-triangle-after-shift.md +6 -0
  44. package/corpus/templates/design/changelog/2026-09-24-re-clicking-the-active-responsive-preview-returns-to-the-can.md +6 -0
  45. package/corpus/templates/design/changelog/2026-09-24-resized-vectors-can-be-point-edited-and-the-k-scale-tool-sho.md +6 -0
  46. package/corpus/templates/design/changelog/2026-09-24-while-editing-text-click-to-place-the-caret-and-double-click.md +6 -0
  47. package/corpus/templates/design/e2e/helpers.ts +19 -2
  48. package/corpus/templates/mail/app/components/email/ComposeModal.tsx +1 -1
  49. package/corpus/templates/mail/changelog/2026-09-24-gmail-push-ack.md +6 -0
  50. package/corpus/templates/mail/changelog/2026-09-24-the-composer-keeps-its-taller-layout-in-a-narrower-window.md +6 -0
  51. package/corpus/templates/mail/server/db/schema.ts +26 -1
  52. package/corpus/templates/mail/server/lib/inbox-store.ts +68 -23
  53. package/corpus/templates/mail/server/lib/inbox-sync.ts +43 -17
  54. package/corpus/templates/mail/server/plugins/db.ts +19 -0
  55. package/corpus/templates/mail/server/routes/api/gmail/push.post.ts +4 -15
  56. package/corpus/templates/slides/.agents/skills/create-deck/SKILL.md +45 -30
  57. package/corpus/templates/slides/.agents/skills/slide-editing/SKILL.md +18 -2
  58. package/corpus/templates/slides/actions/export-html.ts +28 -17
  59. package/corpus/templates/slides/app/hooks/use-deck-design-system.ts +27 -15
  60. package/corpus/templates/slides/app/pages/DeckEditor.tsx +1 -1
  61. package/corpus/templates/slides/changelog/2026-09-23-decks-with-no-design-system-now-follow-a-theme-chosen-for-th.md +6 -0
  62. package/corpus/templates/slides/shared/slide-background.ts +3 -1
  63. package/dist/agent/engine/builder-engine.js +11 -0
  64. package/dist/agent/engine/types.d.ts +1 -0
  65. package/dist/agent/production-agent.d.ts +2 -0
  66. package/dist/agent/production-agent.js +32 -4
  67. package/dist/agent/run-loop-with-resume.js +6 -0
  68. package/dist/cli/index.js +4 -4
  69. package/dist/cli/process.d.ts +10 -1
  70. package/dist/cli/process.js +66 -0
  71. package/dist/cli/workspace-dev.js +2 -0
  72. package/dist/client/agent-chat.js +13 -14
  73. package/dist/client/error-format.js +4 -4
  74. package/dist/client/observability/ObservabilityDashboard.js +95 -65
  75. package/dist/client/observability/OutputPreview.d.ts +2 -1
  76. package/dist/client/observability/OutputPreview.js +64 -7
  77. package/dist/client/observability/useObservability.d.ts +2 -1
  78. package/dist/client/observability/useObservability.js +5 -1
  79. package/dist/client/settings/UsageSection.js +105 -17
  80. package/dist/collab/routes.d.ts +1 -1
  81. package/dist/collab/struct-routes.d.ts +1 -1
  82. package/dist/db/client.d.ts +44 -0
  83. package/dist/db/client.js +62 -3
  84. package/dist/db/create-get-db.js +23 -1
  85. package/dist/db/embedded-runtime.d.ts +21 -0
  86. package/dist/db/embedded-runtime.js +27 -0
  87. package/dist/db/migrations.js +2 -0
  88. package/dist/db/server-runtime.d.ts +22 -0
  89. package/dist/db/server-runtime.js +26 -0
  90. package/dist/deploy/build.d.ts +29 -0
  91. package/dist/deploy/build.js +204 -121
  92. package/dist/feature-flags/index.d.ts +1 -1
  93. package/dist/feature-flags/index.js +1 -1
  94. package/dist/feature-flags/registry.d.ts +1 -0
  95. package/dist/feature-flags/registry.js +5 -0
  96. package/dist/file-upload/actions/upload-image.d.ts +1 -1
  97. package/dist/index.d.ts +1 -1
  98. package/dist/index.js +1 -1
  99. package/dist/integrations/webhook-handler.js +4 -1
  100. package/dist/jobs/background-automation-runner.js +6 -1
  101. package/dist/localization/core-messages/ar-SA.js +9 -1
  102. package/dist/localization/core-messages/de-DE.js +9 -1
  103. package/dist/localization/core-messages/en-US.d.ts +9 -1
  104. package/dist/localization/core-messages/en-US.js +9 -1
  105. package/dist/localization/core-messages/es-ES.js +9 -1
  106. package/dist/localization/core-messages/fr-FR.js +9 -1
  107. package/dist/localization/core-messages/hi-IN.js +9 -1
  108. package/dist/localization/core-messages/ja-JP.js +9 -1
  109. package/dist/localization/core-messages/ko-KR.js +9 -1
  110. package/dist/localization/core-messages/pt-BR.js +9 -1
  111. package/dist/localization/core-messages/supplemental/ar-SA.js +1 -0
  112. package/dist/localization/core-messages/supplemental/de-DE.js +1 -0
  113. package/dist/localization/core-messages/supplemental/en-US.js +1 -0
  114. package/dist/localization/core-messages/supplemental/es-ES.js +1 -0
  115. package/dist/localization/core-messages/supplemental/fr-FR.js +1 -0
  116. package/dist/localization/core-messages/supplemental/hi-IN.js +1 -0
  117. package/dist/localization/core-messages/supplemental/ja-JP.js +1 -0
  118. package/dist/localization/core-messages/supplemental/ko-KR.js +1 -0
  119. package/dist/localization/core-messages/supplemental/pt-BR.js +1 -0
  120. package/dist/localization/core-messages/supplemental/zh-CN.js +1 -0
  121. package/dist/localization/core-messages/supplemental/zh-TW.js +1 -0
  122. package/dist/localization/core-messages/zh-CN.js +9 -1
  123. package/dist/localization/core-messages/zh-TW.js +9 -1
  124. package/dist/localization/core-messages.d.ts +9 -1
  125. package/dist/localization/mcp-settings-messages.d.ts +1 -0
  126. package/dist/mcp/connect-route.js +45 -8
  127. package/dist/mcp/connect-store.d.ts +7 -5
  128. package/dist/mcp/connect-store.js +30 -39
  129. package/dist/notifications/routes.d.ts +3 -3
  130. package/dist/observability/actions/get-observability-review-detail.d.ts +7 -0
  131. package/dist/observability/actions/get-observability-review-detail.js +25 -0
  132. package/dist/observability/reviews.d.ts +9 -1
  133. package/dist/observability/reviews.js +20 -0
  134. package/dist/observability/routes.d.ts +3 -3
  135. package/dist/observability/types.d.ts +8 -0
  136. package/dist/org/handlers.d.ts +1 -1
  137. package/dist/provider-api/actions/custom-provider-registration.d.ts +7 -7
  138. package/dist/provider-api/actions/provider-api.d.ts +9 -9
  139. package/dist/scripts/db/query.js +6 -1
  140. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  141. package/dist/server/agent-engine-ollama-models-route.d.ts +2 -2
  142. package/dist/server/agent-teams.js +4 -1
  143. package/dist/server/auth.js +60 -20
  144. package/dist/server/better-auth-instance.js +7 -1
  145. package/dist/server/core-routes-plugin.d.ts +1 -1
  146. package/dist/server/core-routes-plugin.js +2 -0
  147. package/dist/server/embedded.js +6 -0
  148. package/dist/server/framework-request-handler.js +6 -0
  149. package/dist/server/realtime-token.d.ts +1 -1
  150. package/dist/usage/actions/get-usage-metrics.js +4 -1
  151. package/dist/usage/metrics-store.d.ts +19 -0
  152. package/dist/usage/metrics-store.js +106 -14
  153. package/dist/usage/store.d.ts +7 -2
  154. package/dist/usage/store.js +25 -4
  155. package/dist/vite/action-types-plugin.js +4 -0
  156. package/dist/vite/client.d.ts +6 -1
  157. package/dist/vite/client.js +40 -1
  158. package/package.json +4 -3
package/corpus/README.md CHANGED
@@ -31,4 +31,4 @@ rg -n "defineAction|useActionQuery" node_modules/@agent-native/core/corpus
31
31
 
32
32
  ## Generated Counts
33
33
 
34
- - template files: 9886
34
+ - template files: 9904
@@ -2665,8 +2665,8 @@ export function Sidebar({ mobile }: { mobile?: boolean } = {}) {
2665
2665
  </div>
2666
2666
  </nav>
2667
2667
 
2668
- <div className="mt-3 shrink-0 min-w-0 space-y-0.5 border-t border-border/70 px-2 pt-3">
2669
- <nav className="flex min-w-0 flex-col space-y-0.5">
2668
+ <div className="mt-3 shrink-0 min-w-0 space-y-1 border-t border-border/70 pt-3">
2669
+ <nav className="flex min-w-0 flex-col space-y-0.5 px-2">
2670
2670
  {bottomItems.map((item) => {
2671
2671
  const Icon = item.icon;
2672
2672
  const isActive = location.pathname === item.href;
@@ -2694,8 +2694,9 @@ export function Sidebar({ mobile }: { mobile?: boolean } = {}) {
2694
2694
  collapsed={false}
2695
2695
  collapsible={false}
2696
2696
  feedback={footerFeedback}
2697
+ className="space-y-1 px-2"
2697
2698
  orgSwitcher={
2698
- <OrgSwitcher className="min-w-0 flex-1 !bg-transparent !text-primary hover:!bg-accent/60 hover:!text-primary" />
2699
+ <OrgSwitcher className="min-w-0 flex-1 !px-2 !bg-transparent !text-primary hover:!bg-accent/60 hover:!text-primary" />
2699
2700
  }
2700
2701
  footerExtras={
2701
2702
  <>
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: improved
3
+ date: 2026-09-24
4
+ ---
5
+
6
+ Analytics sidebar navigation and footer controls align consistently, with full-width dividers.
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-09-24
4
+ ---
5
+
6
+ Dashboard emails now match the live dashboard's stacked area chart totals.
@@ -367,26 +367,29 @@ function buildAxisScale(
367
367
  }
368
368
 
369
369
  /**
370
- * Segment edges for a stacked bar group. Positives and negatives stack away
371
- * from zero separately, and the y-domain has to cover every segment edge — the
372
- * running total alone puts a mixed-sign stack's tallest bar off-canvas.
370
+ * Segment edges for diverging bars or signed cumulative areas. The y-domain
371
+ * has to cover every edge so mixed-sign segments stay inside the plot.
373
372
  */
374
373
  function stackSegments(
375
374
  series: ResolvedSeries[],
376
375
  seriesIndexes: number[],
377
376
  labelCount: number,
377
+ mode: "diverging" | "cumulative",
378
378
  ): Map<string, { base: number; top: number }> {
379
379
  const segments = new Map<string, { base: number; top: number }>();
380
380
  for (let labelIndex = 0; labelIndex < labelCount; labelIndex += 1) {
381
381
  let up = 0;
382
382
  let down = 0;
383
+ let total = 0;
383
384
  for (const seriesIndex of seriesIndexes) {
384
385
  const value = series[seriesIndex].data[labelIndex];
385
386
  if (value === null) continue;
386
- const base = value >= 0 ? up : down;
387
- if (value >= 0) up += value;
388
- else down += value;
389
- segments.set(`${labelIndex}:${seriesIndex}`, { base, top: base + value });
387
+ const base = mode === "cumulative" ? total : value >= 0 ? up : down;
388
+ const top = base + value;
389
+ if (mode === "cumulative") total = top;
390
+ else if (value >= 0) up = top;
391
+ else down = top;
392
+ segments.set(`${labelIndex}:${seriesIndex}`, { base, top });
390
393
  }
391
394
  }
392
395
  return segments;
@@ -464,17 +467,29 @@ function renderCartesianChartSvg({
464
467
  const plotHeight = Math.max(1, chartBottom - chartTop);
465
468
 
466
469
  const stackedBars = stacked && type === "bar";
467
- // Stacks group per axis, matching how the dashboard renderer stacks them, so
468
- // a dual-axis chart never sums two different units into one bar.
469
- const stackedSegments = stackedBars
470
- ? new Map([
471
- ...stackSegments(series, leftAxisIndexes, labels.length),
472
- ...stackSegments(series, rightAxisIndexes, labels.length),
473
- ])
474
- : new Map<string, { base: number; top: number }>();
470
+ const stackedAreas = stacked && type === "area";
471
+ // Stacks group per axis, matching the dashboard renderer, so a dual-axis
472
+ // chart never sums different units onto one scale.
473
+ const stackedSegments =
474
+ stackedBars || stackedAreas
475
+ ? new Map([
476
+ ...stackSegments(
477
+ series,
478
+ leftAxisIndexes,
479
+ labels.length,
480
+ stackedAreas ? "cumulative" : "diverging",
481
+ ),
482
+ ...stackSegments(
483
+ series,
484
+ rightAxisIndexes,
485
+ labels.length,
486
+ stackedAreas ? "cumulative" : "diverging",
487
+ ),
488
+ ])
489
+ : new Map<string, { base: number; top: number }>();
475
490
 
476
491
  const axisValues = (seriesIndexes: number[]): number[] =>
477
- stackedBars
492
+ stackedBars || stackedAreas
478
493
  ? seriesIndexes.flatMap((seriesIndex) =>
479
494
  labels.flatMap((_, labelIndex) => {
480
495
  const segment = stackedSegments.get(`${labelIndex}:${seriesIndex}`);
@@ -583,15 +598,19 @@ function renderCartesianChartSvg({
583
598
  const zeroText = formatCoord(zeroY);
584
599
  const segments: Array<{
585
600
  start: number;
586
- points: Array<[string, string]>;
601
+ points: Array<[string, string, string]>;
587
602
  }> = [];
588
603
  labels.forEach((_, index) => {
589
604
  const value = entry.data[index];
590
605
  if (value === null) return;
606
+ const stack = stackedAreas
607
+ ? stackedSegments.get(`${index}:${seriesIndex}`)
608
+ : undefined;
591
609
  const x = chartLeft + slot * index + slot / 2;
592
- const point: [string, string] = [
610
+ const point: [string, string, string] = [
593
611
  x.toFixed(1),
594
- yFor(value).toFixed(1),
612
+ yFor(stack?.top ?? value).toFixed(1),
613
+ yFor(stack?.base ?? 0).toFixed(1),
595
614
  ];
596
615
  const open = segments[segments.length - 1];
597
616
  if (open && open.start + open.points.length === index) {
@@ -613,7 +632,15 @@ function renderCartesianChartSvg({
613
632
  .join(" ");
614
633
  const area =
615
634
  type === "area"
616
- ? `<path d="${path} L ${chartLeft + slot * (start + points.length - 0.5)},${zeroText} L ${chartLeft + slot * (start + 0.5)},${zeroText} Z" fill="${entry.color}" fill-opacity="0.18"/>`
635
+ ? `<path d="${path} ${
636
+ stackedAreas
637
+ ? points
638
+ .slice()
639
+ .reverse()
640
+ .map(([x, , baseY]) => `L ${x},${baseY}`)
641
+ .join(" ")
642
+ : `L ${chartLeft + slot * (start + points.length - 0.5)},${zeroText} L ${chartLeft + slot * (start + 0.5)},${zeroText}`
643
+ } Z" fill="${entry.color}" fill-opacity="0.18"/>`
617
644
  : "";
618
645
  return `${area}<path d="${path}" fill="none" stroke="${entry.color}" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>`;
619
646
  })
@@ -345,7 +345,7 @@ export const AttendeeAutocomplete = forwardRef<
345
345
  className={cn(
346
346
  variant === "box"
347
347
  ? "rounded-md border border-input bg-transparent px-2 py-1.5 text-sm focus-within:ring-1 focus-within:ring-ring"
348
- : "flex min-w-0 flex-1 items-center gap-2",
348
+ : "flex min-w-0 flex-1 flex-wrap items-center gap-2",
349
349
  className,
350
350
  )}
351
351
  onClick={() => inputRef.current?.focus()}
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-09-24
4
+ ---
5
+
6
+ Guest inputs wrap beneath existing guests so the active input stays visible
@@ -1,6 +1,7 @@
1
1
  import {
2
2
  useActionMutation,
3
3
  useActionQuery,
4
+ useSession,
4
5
  } from "@agent-native/core/client/hooks";
5
6
  import { useT } from "@agent-native/core/client/i18n";
6
7
  import {
@@ -22,7 +23,7 @@ import {
22
23
  } from "@/components/ui/dialog";
23
24
  import { ScrollArea } from "@/components/ui/scroll-area";
24
25
  import { exportConcat } from "@/lib/ffmpeg-export";
25
- import { copyRecordingShareLink } from "@/lib/recording-link";
26
+ import { copyFreshRecordingShareLink } from "@/lib/recording-link";
26
27
  import { formatMs } from "@/lib/timestamp-mapping";
27
28
  import { uploadFileClient } from "@/lib/upload-file-client";
28
29
  import { cn } from "@/lib/utils";
@@ -51,6 +52,7 @@ export function StitchManager({
51
52
  seedRecordingId,
52
53
  }: StitchManagerProps) {
53
54
  const t = useT();
55
+ const { session } = useSession();
54
56
  const [queue, setQueue] = useState<RecordingLite[]>([]);
55
57
  const [busy, setBusy] = useState(false);
56
58
  const [progress, setProgress] = useState(0);
@@ -152,7 +154,10 @@ export function StitchManager({
152
154
  });
153
155
  const newRecordingId = (result as { id?: string } | null)?.id;
154
156
  if (newRecordingId) {
155
- const copied = await copyRecordingShareLink(newRecordingId);
157
+ const copied = await copyFreshRecordingShareLink(
158
+ newRecordingId,
159
+ session,
160
+ );
156
161
  if (copied) {
157
162
  toast.success(t("stitchManager.created"), {
158
163
  description: t("recordRoute.linkCopied"),
@@ -162,7 +167,7 @@ export function StitchManager({
162
167
  action: {
163
168
  label: t("recordRoute.copyLinkAction"),
164
169
  onClick: () => {
165
- void copyRecordingShareLink(newRecordingId);
170
+ void copyFreshRecordingShareLink(newRecordingId, session);
166
171
  },
167
172
  },
168
173
  });
@@ -30,3 +30,24 @@ export async function copyRecordingShareLink(
30
30
  ): Promise<boolean> {
31
31
  return writeClipboardText(recordingShareUrl(recordingId, ownerId));
32
32
  }
33
+
34
+ /**
35
+ * Share URL for a recording the signed-in visitor just finished. Unlike the
36
+ * reshare dialog, which must check the viewer's role, the recorder always owns
37
+ * the row they just created, so no ownership check is needed. Omitting the
38
+ * session drops `via`, and with it `referrer_user` on the signup.
39
+ */
40
+ export function freshRecordingShareUrl(
41
+ recordingId: string,
42
+ session: { userId?: string | null } | null | undefined,
43
+ ): string {
44
+ return recordingShareUrl(recordingId, session?.userId ?? undefined);
45
+ }
46
+
47
+ /** Copy variant of {@link freshRecordingShareUrl}. */
48
+ export async function copyFreshRecordingShareLink(
49
+ recordingId: string,
50
+ session: { userId?: string | null } | null | undefined,
51
+ ): Promise<boolean> {
52
+ return writeClipboardText(freshRecordingShareUrl(recordingId, session));
53
+ }
@@ -6,7 +6,11 @@ import {
6
6
  agentNativePath,
7
7
  appBasePath,
8
8
  } from "@agent-native/core/client/api-path";
9
- import { callAction, getBrowserTabId } from "@agent-native/core/client/hooks";
9
+ import {
10
+ callAction,
11
+ getBrowserTabId,
12
+ useSession,
13
+ } from "@agent-native/core/client/hooks";
10
14
  import { useT } from "@agent-native/core/client/i18n";
11
15
  import { useLiveTranscription } from "@agent-native/core/client/transcription/use-live-transcription";
12
16
  import type { BrowserDiagnosticsData } from "@shared/browser-diagnostics";
@@ -80,7 +84,7 @@ import {
80
84
  loadRecorderPreferences,
81
85
  saveRecorderPreferences,
82
86
  } from "@/lib/recorder-preferences";
83
- import { copyRecordingShareLink } from "@/lib/recording-link";
87
+ import { copyFreshRecordingShareLink } from "@/lib/recording-link";
84
88
  import {
85
89
  buildCaptureTitle,
86
90
  defaultRecordingTitle,
@@ -910,6 +914,9 @@ export default function RecordRoute() {
910
914
  const t = useT();
911
915
  const navigate = useNavigate();
912
916
  const location = useLocation();
917
+ // Named distinctly from the local `session` upload-attempt counters used
918
+ // inside uploadFile/startFlow below — this is the signed-in visitor.
919
+ const { session: authSession } = useSession();
913
920
  const {
914
921
  dismiss: dismissUploadToast,
915
922
  error: failUploadToast,
@@ -933,12 +940,12 @@ export default function RecordRoute() {
933
940
  action: {
934
941
  label: t("recordRoute.copyLinkAction"),
935
942
  onClick: () => {
936
- void copyRecordingShareLink(recordingId);
943
+ void copyFreshRecordingShareLink(recordingId, authSession);
937
944
  },
938
945
  },
939
946
  });
940
947
  },
941
- [completeUploadToast, t],
948
+ [authSession, completeUploadToast, t],
942
949
  );
943
950
  const [uiState, setUiState] = useState<UiState>("idle");
944
951
  const [savingKind, setSavingKind] = useState<"recording" | "upload" | null>(
@@ -1936,7 +1943,7 @@ export default function RecordRoute() {
1936
1943
  } else if (createdId && !reportContext) {
1937
1944
  showSavedToast(
1938
1945
  t("recordRoute.videoUploaded"),
1939
- await copyRecordingShareLink(createdId),
1946
+ await copyFreshRecordingShareLink(createdId, authSession),
1940
1947
  createdId,
1941
1948
  );
1942
1949
  } else {
@@ -2017,6 +2024,7 @@ export default function RecordRoute() {
2017
2024
  }
2018
2025
  },
2019
2026
  [
2027
+ authSession,
2020
2028
  completeUploadToast,
2021
2029
  failUploadToast,
2022
2030
  infoUploadToast,
@@ -2197,7 +2205,8 @@ export default function RecordRoute() {
2197
2205
  } else {
2198
2206
  showSavedToast(
2199
2207
  t("recordRoute.recordingSaved"),
2200
- await (pendingCopy ?? copyRecordingShareLink(recordingId)),
2208
+ await (pendingCopy ??
2209
+ copyFreshRecordingShareLink(recordingId, authSession)),
2201
2210
  recordingId,
2202
2211
  );
2203
2212
  }
@@ -2227,7 +2236,14 @@ export default function RecordRoute() {
2227
2236
  void navigate(`/r/${recordingId}`);
2228
2237
  }, 50);
2229
2238
  },
2230
- [completeUploadToast, infoUploadToast, navigate, showSavedToast, t],
2239
+ [
2240
+ authSession,
2241
+ completeUploadToast,
2242
+ infoUploadToast,
2243
+ navigate,
2244
+ showSavedToast,
2245
+ t,
2246
+ ],
2231
2247
  );
2232
2248
 
2233
2249
  const doStop = useCallback(async () => {
@@ -2317,7 +2333,9 @@ export default function RecordRoute() {
2317
2333
  const pendingCopy =
2318
2334
  stopResult.waitingForStorage || bugReportContextRef.current
2319
2335
  ? undefined
2320
- : copyRecordingShareLink(pending.id).catch(() => false);
2336
+ : copyFreshRecordingShareLink(pending.id, authSession).catch(
2337
+ () => false,
2338
+ );
2321
2339
  await diagnosticsSave;
2322
2340
  await finishSavedRecording(pending.id, stopResult, pendingCopy);
2323
2341
  } catch (err) {
@@ -2362,6 +2380,7 @@ export default function RecordRoute() {
2362
2380
  });
2363
2381
  }
2364
2382
  }, [
2383
+ authSession,
2365
2384
  failUploadToast,
2366
2385
  finishSavedRecording,
2367
2386
  liveTranscription,
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-09-24
4
+ ---
5
+
6
+ The camera bubble stays visible while you reposition it before recording, then the popover resumes its inactive auto-hide after the drag ends.
@@ -251,6 +251,10 @@ fn clamp_existing_bubble_window(app: &AppHandle, window: &WebviewWindow) {
251
251
  /// yield to it.
252
252
  static BUBBLE_DRAGGING: AtomicBool = AtomicBool::new(false);
253
253
 
254
+ pub fn is_bubble_dragging() -> bool {
255
+ BUBBLE_DRAGGING.load(Ordering::SeqCst)
256
+ }
257
+
254
258
  /// Anchor captured at the start of a hand-drag: the global cursor position and
255
259
  /// the bubble window's top-left, both in physical px with a desktop top-left
256
260
  /// origin (the same space `cursor_position()` / `outer_position()` report in).
@@ -3067,6 +3071,8 @@ pub async fn bubble_drag_start(app: AppHandle) -> Result<(), String> {
3067
3071
  win_y: pos.y,
3068
3072
  });
3069
3073
  BUBBLE_DRAGGING.store(true, Ordering::SeqCst);
3074
+ // Invalidate a blur dismissal delivered before the drag command reached Rust.
3075
+ mark_popover_shown(&app);
3070
3076
  Ok(())
3071
3077
  }
3072
3078
 
@@ -3118,6 +3124,7 @@ pub async fn bubble_drag_end(app: AppHandle) -> Result<(), String> {
3118
3124
  if let Some(window) = app.get_webview_window(BUBBLE_LABEL) {
3119
3125
  clamp_existing_bubble_window(&app, &window);
3120
3126
  }
3127
+ crate::schedule_popover_dismissal(&app);
3121
3128
  Ok(())
3122
3129
  }
3123
3130
 
@@ -67,9 +67,62 @@ use util::{
67
67
  pub(crate) const TRAY_PNG: &[u8] = include_bytes!("../icons/tray.png");
68
68
 
69
69
  const POPOVER_BLUR_GUARD: Duration = Duration::from_millis(1500);
70
+ // Focused(false) can arrive before the bubble's pointer-down reaches Rust.
71
+ const POPOVER_BLUR_SETTLE: Duration = Duration::from_millis(100);
70
72
 
71
- fn popover_blur_delay(elapsed: Duration) -> Option<Duration> {
72
- (elapsed < POPOVER_BLUR_GUARD).then(|| POPOVER_BLUR_GUARD - elapsed)
73
+ fn popover_blur_delay(elapsed: Duration, bubble_dragging: bool) -> Option<Duration> {
74
+ if bubble_dragging {
75
+ None
76
+ } else if elapsed < POPOVER_BLUR_GUARD {
77
+ Some(POPOVER_BLUR_GUARD - elapsed)
78
+ } else {
79
+ Some(POPOVER_BLUR_SETTLE)
80
+ }
81
+ }
82
+
83
+ pub(crate) fn schedule_popover_dismissal(app_handle: &tauri::AppHandle) {
84
+ if is_recording_active(app_handle) && clips::popover_is_parked(app_handle) {
85
+ dlog!("[clips-tray] popover blur ignored — popover parked");
86
+ return;
87
+ }
88
+ if !config::auto_hide_popover_enabled(app_handle) {
89
+ dlog!("[clips-tray] popover blur ignored — auto-hide disabled");
90
+ return;
91
+ }
92
+ let Some(handle) = app_handle.get_webview_window("popover") else {
93
+ return;
94
+ };
95
+ let shown_at = app_handle
96
+ .try_state::<PopoverShownAt>()
97
+ .and_then(|s| s.0.lock().ok().and_then(|g| *g));
98
+ let elapsed = shown_at.map(|t| t.elapsed()).unwrap_or(Duration::MAX);
99
+ let Some(delay) = popover_blur_delay(elapsed, clips::is_bubble_dragging()) else {
100
+ dlog!("[clips-tray] popover blur ignored — camera bubble drag active");
101
+ return;
102
+ };
103
+ dlog!(
104
+ "[clips-tray] popover blur, elapsed_ms={}",
105
+ elapsed.as_millis()
106
+ );
107
+ let delayed_handle = handle.clone();
108
+ let delayed_app_handle = (*app_handle).clone();
109
+ tauri::async_runtime::spawn(async move {
110
+ tokio::time::sleep(delay).await;
111
+ let still_current = delayed_app_handle
112
+ .try_state::<PopoverShownAt>()
113
+ .and_then(|state| state.0.lock().ok().map(|guard| *guard == shown_at))
114
+ .unwrap_or(false);
115
+ if !still_current
116
+ || clips::is_bubble_dragging()
117
+ || (is_recording_active(&delayed_app_handle)
118
+ && clips::popover_is_parked(&delayed_app_handle))
119
+ || !config::auto_hide_popover_enabled(&delayed_app_handle)
120
+ || delayed_handle.is_focused().unwrap_or(true)
121
+ {
122
+ return;
123
+ }
124
+ clips::hide_popover(&delayed_app_handle);
125
+ });
73
126
  }
74
127
 
75
128
  fn present_popover(app: &tauri::AppHandle) {
@@ -496,7 +549,6 @@ pub fn run() {
496
549
  // itself macOS briefly steals focus from the popover, which would
497
550
  // fire Focused(false) and hide the window we literally just showed.
498
551
  if let Some(window) = app.get_webview_window("popover") {
499
- let handle = window.clone();
500
552
  let app_handle = app.handle().clone();
501
553
  // NOTE: Intentionally NOT calling window.open_devtools()
502
554
  // here. An auto-opened devtools window steals focus from
@@ -506,55 +558,7 @@ pub fn run() {
506
558
  // Element if they need devtools.
507
559
  window.on_window_event(move |event| {
508
560
  if let tauri::WindowEvent::Focused(false) = event {
509
- // A parked popover is still alive for WebKit, but it
510
- // is intentionally not a user-facing menu. Ignore
511
- // its blur while native capture owns the handoff;
512
- // a full-size visible popover must still dismiss even
513
- // if an earlier start flow left the recording flag
514
- // latched.
515
- if is_recording_active(&app_handle) && clips::popover_is_parked(&app_handle) {
516
- dlog!("[clips-tray] popover blur ignored — popover parked");
517
- return;
518
- }
519
- if !config::auto_hide_popover_enabled(&app_handle) {
520
- dlog!("[clips-tray] popover blur ignored — auto-hide disabled");
521
- return;
522
- }
523
- let shown_at = app_handle
524
- .try_state::<PopoverShownAt>()
525
- .and_then(|s| s.0.lock().ok().and_then(|g| *g));
526
- let elapsed = shown_at.map(|t| t.elapsed()).unwrap_or(Duration::MAX);
527
- dlog!(
528
- "[clips-tray] popover blur, elapsed_ms={}",
529
- elapsed.as_millis()
530
- );
531
- if let Some(delay) = popover_blur_delay(elapsed) {
532
- let Some(shown_at) = shown_at else {
533
- return;
534
- };
535
- let delayed_handle = handle.clone();
536
- let delayed_app_handle = app_handle.clone();
537
- tauri::async_runtime::spawn(async move {
538
- tokio::time::sleep(delay).await;
539
- let still_current = delayed_app_handle
540
- .try_state::<PopoverShownAt>()
541
- .and_then(|state| {
542
- state.0.lock().ok().map(|guard| *guard == Some(shown_at))
543
- })
544
- .unwrap_or(false);
545
- if !still_current
546
- || (is_recording_active(&delayed_app_handle)
547
- && clips::popover_is_parked(&delayed_app_handle))
548
- || !config::auto_hide_popover_enabled(&delayed_app_handle)
549
- || delayed_handle.is_focused().unwrap_or(true)
550
- {
551
- return;
552
- }
553
- clips::hide_popover(&delayed_app_handle);
554
- });
555
- } else {
556
- clips::hide_popover(&app_handle);
557
- }
561
+ schedule_popover_dismissal(&app_handle);
558
562
  }
559
563
  });
560
564
  }
@@ -661,15 +665,19 @@ pub fn run() {
661
665
 
662
666
  #[cfg(test)]
663
667
  mod tests {
664
- use super::{popover_blur_delay, POPOVER_BLUR_GUARD};
668
+ use super::{popover_blur_delay, POPOVER_BLUR_GUARD, POPOVER_BLUR_SETTLE};
665
669
  use std::time::Duration;
666
670
 
667
671
  #[test]
668
672
  fn retries_blur_until_guard_expires() {
669
673
  assert_eq!(
670
- popover_blur_delay(Duration::from_millis(400)),
674
+ popover_blur_delay(Duration::from_millis(400), false),
671
675
  Some(Duration::from_millis(1100))
672
676
  );
673
- assert_eq!(popover_blur_delay(POPOVER_BLUR_GUARD), None);
677
+ assert_eq!(
678
+ popover_blur_delay(POPOVER_BLUR_GUARD, false),
679
+ Some(POPOVER_BLUR_SETTLE)
680
+ );
681
+ assert_eq!(popover_blur_delay(Duration::MAX, true), None);
674
682
  }
675
683
  }