@agent-native/core 0.84.56 → 0.84.58

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 (151) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +14 -0
  3. package/corpus/core/docs/content/locales/ar-SA/template-analytics.mdx +2 -0
  4. package/corpus/core/docs/content/locales/ar-SA/template-clips.mdx +2 -0
  5. package/corpus/core/docs/content/locales/de-DE/template-analytics.mdx +2 -0
  6. package/corpus/core/docs/content/locales/de-DE/template-clips.mdx +2 -0
  7. package/corpus/core/docs/content/locales/es-ES/template-analytics.mdx +2 -0
  8. package/corpus/core/docs/content/locales/es-ES/template-clips.mdx +2 -0
  9. package/corpus/core/docs/content/locales/fr-FR/template-analytics.mdx +2 -0
  10. package/corpus/core/docs/content/locales/fr-FR/template-clips.mdx +2 -0
  11. package/corpus/core/docs/content/locales/hi-IN/template-analytics.mdx +2 -0
  12. package/corpus/core/docs/content/locales/hi-IN/template-clips.mdx +2 -0
  13. package/corpus/core/docs/content/locales/ja-JP/template-analytics.mdx +2 -0
  14. package/corpus/core/docs/content/locales/ja-JP/template-clips.mdx +2 -0
  15. package/corpus/core/docs/content/locales/ko-KR/template-analytics.mdx +2 -0
  16. package/corpus/core/docs/content/locales/ko-KR/template-clips.mdx +2 -0
  17. package/corpus/core/docs/content/locales/pt-BR/template-analytics.mdx +2 -0
  18. package/corpus/core/docs/content/locales/pt-BR/template-clips.mdx +2 -0
  19. package/corpus/core/docs/content/locales/zh-CN/template-analytics.mdx +2 -0
  20. package/corpus/core/docs/content/locales/zh-CN/template-clips.mdx +2 -0
  21. package/corpus/core/docs/content/locales/zh-TW/template-analytics.mdx +2 -0
  22. package/corpus/core/docs/content/locales/zh-TW/template-clips.mdx +2 -0
  23. package/corpus/core/docs/content/template-analytics.mdx +2 -0
  24. package/corpus/core/docs/content/template-clips.mdx +22 -10
  25. package/corpus/core/docs/content/tracking.mdx +2 -0
  26. package/corpus/core/package.json +1 -1
  27. package/corpus/core/src/action.ts +11 -0
  28. package/corpus/core/src/agent/production-agent.ts +24 -46
  29. package/corpus/core/src/client/AssistantChat.tsx +69 -12
  30. package/corpus/core/src/client/MultiTabAssistantChat.tsx +1 -1
  31. package/corpus/core/src/client/conversation/use-near-bottom-autoscroll.ts +45 -2
  32. package/corpus/core/src/coding-tools/run-code.ts +1 -0
  33. package/corpus/core/src/integrations/webhook-handler.ts +10 -9
  34. package/corpus/core/src/server/action-discovery.ts +3 -0
  35. package/corpus/core/src/server/credential-provider.ts +11 -6
  36. package/corpus/templates/analytics/.agents/skills/session-replay/SKILL.md +8 -0
  37. package/corpus/templates/analytics/AGENTS.md +5 -0
  38. package/corpus/templates/analytics/actions/create-session-replay-agent-link.ts +31 -0
  39. package/corpus/templates/analytics/app/i18n/zh-TW.ts +2 -0
  40. package/corpus/templates/analytics/app/i18n-data.ts +20 -0
  41. package/corpus/templates/analytics/app/pages/sessions/SessionDetailPage.tsx +65 -3
  42. package/corpus/templates/analytics/changelog/2026-07-02-session-replays-can-be-copied-as-temporary-private-links-for-agents.md +6 -0
  43. package/corpus/templates/analytics/server/handlers/session-replay.ts +78 -0
  44. package/corpus/templates/analytics/server/lib/agent-chat-plan-mode.ts +29 -0
  45. package/corpus/templates/analytics/server/lib/session-replay-agent-context.ts +313 -0
  46. package/corpus/templates/analytics/server/lib/session-replay.ts +198 -3
  47. package/corpus/templates/analytics/server/plugins/agent-chat.ts +9 -1
  48. package/corpus/templates/analytics/server/routes/[...page].get.ts +104 -1
  49. package/corpus/templates/analytics/server/routes/api/session-replay/agent-context.json.get.ts +48 -0
  50. package/corpus/templates/analytics/server/routes/api/session-replay/agent-events.json.get.ts +63 -0
  51. package/corpus/templates/analytics/shared/session-replay-agent-access.ts +10 -0
  52. package/corpus/templates/clips/.agents/skills/video-sharing/SKILL.md +14 -8
  53. package/corpus/templates/clips/AGENTS.md +3 -2
  54. package/corpus/templates/clips/actions/create-recording-agent-link.ts +90 -0
  55. package/corpus/templates/clips/app/components/player/share-dialog.tsx +43 -46
  56. package/corpus/templates/clips/app/components/sharing/share-ui.tsx +5 -3
  57. package/corpus/templates/clips/app/i18n/ar-SA.ts +1 -1
  58. package/corpus/templates/clips/app/i18n/de-DE.ts +1 -1
  59. package/corpus/templates/clips/app/i18n/en-US.ts +1 -1
  60. package/corpus/templates/clips/app/i18n/es-ES.ts +1 -1
  61. package/corpus/templates/clips/app/i18n/fr-FR.ts +1 -1
  62. package/corpus/templates/clips/app/i18n/hi-IN.ts +1 -1
  63. package/corpus/templates/clips/app/i18n/ja-JP.ts +1 -1
  64. package/corpus/templates/clips/app/i18n/ko-KR.ts +1 -1
  65. package/corpus/templates/clips/app/i18n/pt-BR.ts +1 -1
  66. package/corpus/templates/clips/app/i18n/zh-CN.ts +1 -1
  67. package/corpus/templates/clips/app/i18n/zh-TW.ts +1 -1
  68. package/corpus/templates/clips/app/routes/share.$shareId.tsx +100 -46
  69. package/corpus/templates/clips/changelog/2026-07-02-private-recordings-can-be-shared-with-agents-through-temporary-links-without-making-them-public.md +6 -0
  70. package/corpus/templates/clips/server/lib/public-agent-context.ts +26 -13
  71. package/corpus/templates/clips/server/routes/api/agent-context.json.get.ts +2 -1
  72. package/corpus/templates/clips/server/routes/api/agent-frame.jpg.get.ts +2 -1
  73. package/corpus/templates/clips/server/routes/api/agent-transcript.json.get.ts +2 -1
  74. package/corpus/templates/clips/server/routes/api/public-recording.get.ts +7 -2
  75. package/corpus/templates/clips/shared/agent-context.ts +5 -0
  76. package/corpus/templates/design/actions/list-design-extensions.ts +2 -2
  77. package/corpus/templates/design/app/components/design/CodeWorkbenchHost.tsx +11 -89
  78. package/corpus/templates/design/app/components/design/DesignExtensionsPanel.tsx +314 -181
  79. package/corpus/templates/design/app/components/design/DesignImportPanel.tsx +92 -134
  80. package/corpus/templates/design/app/components/design/EditPanel.tsx +93 -19
  81. package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +18 -26
  82. package/corpus/templates/design/app/components/design/TokensPanel.tsx +220 -192
  83. package/corpus/templates/design/app/components/design/code-workbench-theme.ts +150 -0
  84. package/corpus/templates/design/app/i18n/zh-TW.ts +5 -7
  85. package/corpus/templates/design/app/i18n-data.ts +61 -77
  86. package/corpus/templates/design/app/lib/design-import.ts +4 -1
  87. package/corpus/templates/design/app/pages/DesignEditor.tsx +97 -48
  88. package/corpus/templates/design/changelog/2026-07-02-design-inspector-and-canvas-controls-stay-consistent-when-se.md +6 -0
  89. package/corpus/templates/design/changelog/2026-07-02-fixed-a-crash-when-opening-the-code-editor.md +6 -0
  90. package/corpus/templates/design/changelog/2026-07-02-fixed-motion-drawer-open-transition.md +6 -0
  91. package/corpus/templates/design/changelog/2026-07-02-improved-design-assets-panel.md +6 -0
  92. package/dist/action.d.ts +8 -0
  93. package/dist/action.d.ts.map +1 -1
  94. package/dist/action.js +3 -0
  95. package/dist/action.js.map +1 -1
  96. package/dist/agent/production-agent.d.ts +10 -11
  97. package/dist/agent/production-agent.d.ts.map +1 -1
  98. package/dist/agent/production-agent.js +21 -44
  99. package/dist/agent/production-agent.js.map +1 -1
  100. package/dist/client/AssistantChat.d.ts +1 -0
  101. package/dist/client/AssistantChat.d.ts.map +1 -1
  102. package/dist/client/AssistantChat.js +61 -11
  103. package/dist/client/AssistantChat.js.map +1 -1
  104. package/dist/client/MultiTabAssistantChat.js +1 -1
  105. package/dist/client/MultiTabAssistantChat.js.map +1 -1
  106. package/dist/client/conversation/use-near-bottom-autoscroll.d.ts.map +1 -1
  107. package/dist/client/conversation/use-near-bottom-autoscroll.js +43 -2
  108. package/dist/client/conversation/use-near-bottom-autoscroll.js.map +1 -1
  109. package/dist/coding-tools/run-code.d.ts.map +1 -1
  110. package/dist/coding-tools/run-code.js +1 -0
  111. package/dist/coding-tools/run-code.js.map +1 -1
  112. package/dist/collab/routes.d.ts +2 -2
  113. package/dist/integrations/webhook-handler.d.ts +1 -0
  114. package/dist/integrations/webhook-handler.d.ts.map +1 -1
  115. package/dist/integrations/webhook-handler.js +9 -9
  116. package/dist/integrations/webhook-handler.js.map +1 -1
  117. package/dist/notifications/routes.d.ts +3 -3
  118. package/dist/observability/routes.d.ts +3 -3
  119. package/dist/resources/handlers.d.ts +1 -1
  120. package/dist/server/action-discovery.d.ts.map +1 -1
  121. package/dist/server/action-discovery.js +3 -0
  122. package/dist/server/action-discovery.js.map +1 -1
  123. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  124. package/dist/server/credential-provider.d.ts +4 -0
  125. package/dist/server/credential-provider.d.ts.map +1 -1
  126. package/dist/server/credential-provider.js +13 -7
  127. package/dist/server/credential-provider.js.map +1 -1
  128. package/docs/content/locales/ar-SA/template-analytics.mdx +2 -0
  129. package/docs/content/locales/ar-SA/template-clips.mdx +2 -0
  130. package/docs/content/locales/de-DE/template-analytics.mdx +2 -0
  131. package/docs/content/locales/de-DE/template-clips.mdx +2 -0
  132. package/docs/content/locales/es-ES/template-analytics.mdx +2 -0
  133. package/docs/content/locales/es-ES/template-clips.mdx +2 -0
  134. package/docs/content/locales/fr-FR/template-analytics.mdx +2 -0
  135. package/docs/content/locales/fr-FR/template-clips.mdx +2 -0
  136. package/docs/content/locales/hi-IN/template-analytics.mdx +2 -0
  137. package/docs/content/locales/hi-IN/template-clips.mdx +2 -0
  138. package/docs/content/locales/ja-JP/template-analytics.mdx +2 -0
  139. package/docs/content/locales/ja-JP/template-clips.mdx +2 -0
  140. package/docs/content/locales/ko-KR/template-analytics.mdx +2 -0
  141. package/docs/content/locales/ko-KR/template-clips.mdx +2 -0
  142. package/docs/content/locales/pt-BR/template-analytics.mdx +2 -0
  143. package/docs/content/locales/pt-BR/template-clips.mdx +2 -0
  144. package/docs/content/locales/zh-CN/template-analytics.mdx +2 -0
  145. package/docs/content/locales/zh-CN/template-clips.mdx +2 -0
  146. package/docs/content/locales/zh-TW/template-analytics.mdx +2 -0
  147. package/docs/content/locales/zh-TW/template-clips.mdx +2 -0
  148. package/docs/content/template-analytics.mdx +2 -0
  149. package/docs/content/template-clips.mdx +22 -10
  150. package/docs/content/tracking.mdx +2 -0
  151. package/package.json +1 -1
@@ -588,6 +588,61 @@ export function reconnectActivityFallbackContent(
588
588
  ];
589
589
  }
590
590
 
591
+ function toolCallIdFromContentPart(part: unknown): string | null {
592
+ if (!part || typeof part !== "object") return null;
593
+ const candidate = part as { type?: unknown; toolCallId?: unknown };
594
+ if (candidate.type !== "tool-call") return null;
595
+ return typeof candidate.toolCallId === "string" && candidate.toolCallId
596
+ ? candidate.toolCallId
597
+ : null;
598
+ }
599
+
600
+ function toolCallPartHasResult(part: unknown): boolean {
601
+ if (!part || typeof part !== "object") return false;
602
+ const candidate = part as { type?: unknown; result?: unknown };
603
+ return candidate.type === "tool-call" && "result" in candidate;
604
+ }
605
+
606
+ function collectRenderedToolCallStates(
607
+ messages: readonly unknown[],
608
+ ): Map<string, { hasResult: boolean }> {
609
+ const states = new Map<string, { hasResult: boolean }>();
610
+ for (const message of messages) {
611
+ const msg = (message as { message?: unknown })?.message ?? message;
612
+ const content = (msg as { content?: unknown })?.content;
613
+ if (!Array.isArray(content)) continue;
614
+ for (const part of content) {
615
+ const id = toolCallIdFromContentPart(part);
616
+ if (!id) continue;
617
+ const existing = states.get(id);
618
+ states.set(id, {
619
+ hasResult: Boolean(existing?.hasResult || toolCallPartHasResult(part)),
620
+ });
621
+ }
622
+ }
623
+ return states;
624
+ }
625
+
626
+ export function dedupeReconnectContentAgainstMessages(
627
+ content: ContentPart[],
628
+ messages: readonly unknown[],
629
+ ): ContentPart[] {
630
+ if (content.length === 0 || messages.length === 0) return content;
631
+ const renderedToolCallStates = collectRenderedToolCallStates(messages);
632
+ if (renderedToolCallStates.size === 0) return content;
633
+
634
+ let changed = false;
635
+ const filtered = content.filter((part) => {
636
+ const id = toolCallIdFromContentPart(part);
637
+ const existing = id ? renderedToolCallStates.get(id) : undefined;
638
+ if (!id || !existing) return true;
639
+ if (toolCallPartHasResult(part) && !existing.hasResult) return true;
640
+ changed = true;
641
+ return false;
642
+ });
643
+ return changed ? filtered : content;
644
+ }
645
+
591
646
  const RECOVERY_USER_MESSAGE_PREFIXES = [
592
647
  "Continue from where you left off",
593
648
  "Continue from where you stopped",
@@ -1540,15 +1595,10 @@ const AssistantChatInner = forwardRef<
1540
1595
  });
1541
1596
  const reconnectActivityContent = useMemo(
1542
1597
  () =>
1543
- (isReconnecting || reconnectFrozen) && reconnectContent.length === 0
1598
+ isReconnecting || reconnectFrozen
1544
1599
  ? reconnectActivityFallbackContent(runningActivityTool)
1545
1600
  : [],
1546
- [
1547
- isReconnecting,
1548
- reconnectFrozen,
1549
- reconnectContent.length,
1550
- runningActivityTool,
1551
- ],
1601
+ [isReconnecting, reconnectFrozen, runningActivityTool],
1552
1602
  );
1553
1603
  const lastBroadcastRunningRef = useRef(isRunning);
1554
1604
  const tiptapRef = useRef<TiptapComposerHandle>(null);
@@ -3287,14 +3337,19 @@ const AssistantChatInner = forwardRef<
3287
3337
  ],
3288
3338
  );
3289
3339
 
3340
+ const visibleReconnectContent = useMemo(
3341
+ () => dedupeReconnectContentAgainstMessages(reconnectContent, messages),
3342
+ [messages, reconnectContent],
3343
+ );
3344
+
3290
3345
  const autoscrollFollowKey = useMemo(
3291
3346
  () =>
3292
3347
  [
3293
3348
  messages.map(messageFollowKey).join(";"),
3294
3349
  `q:${queuedMessages.map(queuedMessageFollowKey).join("|")}`,
3295
- `r:${reconnectContentFollowKey(reconnectContent)}`,
3350
+ `r:${reconnectContentFollowKey(visibleReconnectContent)}`,
3296
3351
  ].join(";;"),
3297
- [messages, queuedMessages, reconnectContent],
3352
+ [messages, queuedMessages, visibleReconnectContent],
3298
3353
  );
3299
3354
 
3300
3355
  const {
@@ -3802,11 +3857,13 @@ const AssistantChatInner = forwardRef<
3802
3857
  />
3803
3858
  )}
3804
3859
  {(isReconnecting || reconnectFrozen) &&
3805
- reconnectContent.length > 0 && (
3806
- <ReconnectStreamMessage content={reconnectContent} />
3860
+ visibleReconnectContent.length > 0 && (
3861
+ <ReconnectStreamMessage
3862
+ content={visibleReconnectContent}
3863
+ />
3807
3864
  )}
3808
3865
  {(isReconnecting || reconnectFrozen) &&
3809
- reconnectContent.length === 0 &&
3866
+ visibleReconnectContent.length === 0 &&
3810
3867
  reconnectActivityContent.length > 0 && (
3811
3868
  <ReconnectStreamMessage
3812
3869
  content={reconnectActivityContent}
@@ -283,7 +283,7 @@ function ScopeBadge({
283
283
  <PopoverTrigger asChild>
284
284
  <button
285
285
  type="button"
286
- className="inline-flex h-7 min-w-0 max-w-full cursor-pointer items-center gap-1.5 rounded-t-lg border border-b-0 border-input bg-background px-3 text-muted-foreground shadow-[0_-8px_24px_hsl(var(--background)/0.72)] transition-colors hover:bg-accent hover:text-foreground sm:max-w-72"
286
+ className="inline-flex h-7 min-w-0 max-w-full cursor-pointer items-center gap-1.5 rounded-t-lg border border-b-0 border-input bg-background px-3 text-muted-foreground transition-colors hover:bg-accent hover:text-foreground sm:max-w-72"
287
287
  aria-label={heading}
288
288
  >
289
289
  <IconLink size={11} className="shrink-0 opacity-70" />
@@ -7,6 +7,41 @@ export interface UseNearBottomAutoscrollOptions {
7
7
  enabled?: boolean;
8
8
  }
9
9
 
10
+ function eventElementTarget(event: Event): HTMLElement | null {
11
+ const target = event.target;
12
+ if (!(target instanceof Node)) return null;
13
+ if (target instanceof HTMLElement) return target;
14
+ return target.parentElement;
15
+ }
16
+
17
+ function canScrollElementVertically(
18
+ element: HTMLElement,
19
+ deltaY: number,
20
+ ): boolean {
21
+ if (element.scrollHeight <= element.clientHeight) return false;
22
+ const style = window.getComputedStyle(element);
23
+ const overflowY = style.overflowY || style.overflow;
24
+ if (!/(auto|scroll|overlay)/.test(overflowY)) return false;
25
+ if (deltaY < 0) return element.scrollTop > 0;
26
+ if (deltaY > 0) {
27
+ return element.scrollTop + element.clientHeight < element.scrollHeight - 1;
28
+ }
29
+ return false;
30
+ }
31
+
32
+ function nestedScrollableConsumesVerticalIntent(
33
+ event: Event,
34
+ root: HTMLElement,
35
+ deltaY: number,
36
+ ): boolean {
37
+ let element = eventElementTarget(event);
38
+ while (element && element !== root) {
39
+ if (canScrollElementVertically(element, deltaY)) return true;
40
+ element = element.parentElement;
41
+ }
42
+ return false;
43
+ }
44
+
10
45
  export function useNearBottomAutoscroll<TElement extends HTMLElement>({
11
46
  followKey,
12
47
  streaming = false,
@@ -83,6 +118,9 @@ export function useNearBottomAutoscroll<TElement extends HTMLElement>({
83
118
  let lastScrollHeight = el.scrollHeight;
84
119
 
85
120
  const onWheel = (event: WheelEvent) => {
121
+ if (nestedScrollableConsumesVerticalIntent(event, el, event.deltaY)) {
122
+ return;
123
+ }
86
124
  if (event.deltaY < 0) detachFromBottom();
87
125
  };
88
126
 
@@ -95,7 +133,10 @@ export function useNearBottomAutoscroll<TElement extends HTMLElement>({
95
133
  if (nextTouchY == null) return;
96
134
  const lastTouchY = lastTouchYRef.current;
97
135
  if (lastTouchY != null && nextTouchY > lastTouchY) {
98
- detachFromBottom();
136
+ const deltaY = lastTouchY - nextTouchY;
137
+ if (!nestedScrollableConsumesVerticalIntent(event, el, deltaY)) {
138
+ detachFromBottom();
139
+ }
99
140
  }
100
141
  lastTouchYRef.current = nextTouchY;
101
142
  };
@@ -104,7 +145,8 @@ export function useNearBottomAutoscroll<TElement extends HTMLElement>({
104
145
  lastTouchYRef.current = null;
105
146
  };
106
147
 
107
- const onScroll = () => {
148
+ const onScroll = (event: Event) => {
149
+ if (event.target !== el) return;
108
150
  const previousScrollTop = lastScrollTopRef.current;
109
151
  const nextScrollTop = el.scrollTop;
110
152
  const nextScrollHeight = el.scrollHeight;
@@ -133,6 +175,7 @@ export function useNearBottomAutoscroll<TElement extends HTMLElement>({
133
175
  event.key === "Home" ||
134
176
  (event.key === " " && event.shiftKey)
135
177
  ) {
178
+ if (nestedScrollableConsumesVerticalIntent(event, el, -1)) return;
136
179
  detachFromBottom();
137
180
  }
138
181
  };
@@ -75,6 +75,7 @@ export function createRunCodeEntry(
75
75
 
76
76
  return {
77
77
  readOnly: true,
78
+ allowInPlanMode: false,
78
79
  // Allow a generous per-call timeout so large data-processing jobs don't hit
79
80
  // the agent-loop's default 60 s cap.
80
81
  timeoutMs: MAX_TIMEOUT_MS,
@@ -36,7 +36,10 @@ import { isLocalDatabase } from "../db/client.js";
36
36
  import { resolveOrgIdForEmail } from "../org/context.js";
37
37
  import { withConfiguredAppBasePath } from "../server/app-base-path.js";
38
38
  import { FRAMEWORK_ROUTE_PREFIX } from "../server/core-routes-plugin.js";
39
- import { readDeployCredentialEnv } from "../server/credential-provider.js";
39
+ import {
40
+ canUseDeployCredentialFallbackForRequest,
41
+ readDeployCredentialEnv,
42
+ } from "../server/credential-provider.js";
40
43
  import { runWithRequestContext } from "../server/request-context.js";
41
44
  import { A2A_CONTINUATION_QUEUED_MARKER } from "./a2a-continuation-marker.js";
42
45
  import { signInternalToken } from "./internal-token.js";
@@ -128,11 +131,6 @@ function explicitEngineName(
128
131
  return undefined;
129
132
  }
130
133
 
131
- function isMultiTenantDeploy(): boolean {
132
- if (process.env.NODE_ENV !== "production") return false;
133
- return !isLocalDatabase();
134
- }
135
-
136
134
  function collectToolResultSummaries(
137
135
  completedRun: ActiveRun,
138
136
  ): A2AToolResultSummary[] {
@@ -142,23 +140,26 @@ function collectToolResultSummaries(
142
140
  .map((event) => ({ tool: event.tool, result: event.result }));
143
141
  }
144
142
 
145
- async function resolveIntegrationApiKey(
143
+ export async function resolveIntegrationApiKey(
146
144
  engineOption: WebhookHandlerOptions["engine"],
147
145
  ownerEmail: string,
148
146
  fallbackApiKey: string,
149
147
  ): Promise<string | undefined> {
148
+ const canUseDeployFallback = canUseDeployCredentialFallbackForRequest();
150
149
  const engineName = explicitEngineName(engineOption);
151
150
  if (engineName) {
152
151
  const provider = engineToProvider(engineName);
153
152
  const userApiKey = await getOwnerApiKey(provider, ownerEmail);
154
- if (userApiKey || isMultiTenantDeploy()) return userApiKey;
153
+ if (userApiKey) return userApiKey;
154
+ if (!canUseDeployFallback) return undefined;
155
155
  const envVar = PROVIDER_TO_ENV[provider];
156
156
  const providerEnvKey = envVar ? readDeployCredentialEnv(envVar) : undefined;
157
157
  return providerEnvKey || fallbackApiKey.trim() || undefined;
158
158
  }
159
159
 
160
160
  const userApiKey = await getOwnerActiveApiKey(ownerEmail);
161
- if (userApiKey || isMultiTenantDeploy()) return userApiKey;
161
+ if (userApiKey) return userApiKey;
162
+ if (!canUseDeployFallback) return undefined;
162
163
  return fallbackApiKey.trim() || undefined;
163
164
  }
164
165
 
@@ -186,6 +186,9 @@ function preserveActionFlags(entry: Record<string, any>): Partial<ActionEntry> {
186
186
  out.requiresAuth = entry.requiresAuth;
187
187
  }
188
188
  if (typeof entry.readOnly === "boolean") out.readOnly = entry.readOnly;
189
+ if (typeof entry.allowInPlanMode === "boolean") {
190
+ out.allowInPlanMode = entry.allowInPlanMode;
191
+ }
189
192
  if (typeof entry.parallelSafe === "boolean") {
190
193
  out.parallelSafe = entry.parallelSafe;
191
194
  }
@@ -78,6 +78,10 @@ export function readDeployCredentialEnv(key: string): string | undefined {
78
78
  * single-tenant contexts. In hosted production with a shared database, every
79
79
  * signed-in user needs their own user/org/workspace credential so one deploy
80
80
  * key does not silently power another tenant's chat.
81
+ *
82
+ * @deprecated Use `canUseDeployCredentialFallbackForRequest()` for generic
83
+ * provider secrets. This stricter helper remains for legacy call sites with
84
+ * identity-bearing deploy credentials.
81
85
  */
82
86
  export function isDeployCredentialFallbackAllowed(): boolean {
83
87
  if (!isProductionLikeRuntime()) return true;
@@ -86,9 +90,10 @@ export function isDeployCredentialFallbackAllowed(): boolean {
86
90
 
87
91
  export function canUseDeployCredentialFallbackForRequest(): boolean {
88
92
  const email = getRequestUserEmail();
89
- if (email && isHostedWorkspaceRuntime()) return false;
90
93
  if (!email) return true;
91
- return isDeployCredentialFallbackAllowed();
94
+ if (isHostedWorkspaceRuntime()) return false;
95
+ if (!isProductionLikeRuntime()) return true;
96
+ return isLocalDatabase();
92
97
  }
93
98
 
94
99
  const BUILDER_CREDENTIAL_KEYS = [
@@ -962,10 +967,10 @@ export async function resolveSecret(key: string): Promise<string | null> {
962
967
  }
963
968
  // Secrets table not ready — treat as missing.
964
969
  }
965
- // Authenticated multi-tenant context: never fall back to process.env.
966
- // The deploy-level value would silently impersonate the actual key
967
- // owner across every tenant. Local/single-tenant deployments keep the
968
- // original env fallback for BYO-server workflows.
970
+ // Read deployment-provided env values as fallbacks; framework code must not
971
+ // write to `process.env`, but keys supplied by the host remain valid config.
972
+ // Builder credentials keep a narrower path below because those keys carry a
973
+ // Builder identity rather than just enabling a provider call.
969
974
  const envFallback = (
970
975
  isBuilderCredentialKey(key)
971
976
  ? canUseBuilderDeployCredentialFallbackForRequest()
@@ -28,6 +28,14 @@ agent answers about browser recordings in the Analytics template.
28
28
  `session-recording` access before reading private blob refs.
29
29
  - SQL inline chunks are a local/dev fallback only; production should use
30
30
  private or encrypted blob storage.
31
+ - When sharing a replay with an external agent, use
32
+ `create-session-replay-agent-link`. It mints a two-hour `agent_access` URL
33
+ scoped to the recording, embeds a small SSR discovery payload on
34
+ `/sessions/:recordingId`, and advertises
35
+ `/api/session-replay/agent-context.json` plus bounded
36
+ `/api/session-replay/agent-events.json` reads.
37
+ - Do not make session recordings public just so an agent can inspect them.
38
+ Tokenized agent links are the intended handoff path.
31
39
 
32
40
  ## Capture Defaults
33
41
 
@@ -148,6 +148,11 @@ details live in `.agents/skills/`.
148
148
  `get-session-replay-summary` and fetches rrweb payloads only through scoped
149
149
  chunk routes. Do not expose storage/provider URLs, raw chunk table access, or
150
150
  unscoped replay blobs in UI, actions, dashboards, docs, or prompts.
151
+ - The session detail page can mint a temporary `Copy for agent` link through
152
+ `create-session-replay-agent-link`. That URL carries an `agent_access` token
153
+ scoped to one recording for two hours; SSR embeds an agent discovery payload
154
+ and the JSON APIs expose only summary/timeline metadata plus bounded event
155
+ reads.
151
156
  - Dashboard rows that include `recording_id` should link to
152
157
  `/sessions/:recordingId`; rows that only include `session_id` can link to a
153
158
  filtered `/sessions` search.
@@ -0,0 +1,31 @@
1
+ import { defineAction } from "@agent-native/core";
2
+ import {
3
+ getRequestContext,
4
+ getRequestOrgId,
5
+ getRequestUserEmail,
6
+ } from "@agent-native/core/server";
7
+ import { z } from "zod";
8
+
9
+ import { createSessionReplayAgentLink } from "../server/lib/session-replay-agent-context.js";
10
+
11
+ function resolveScope() {
12
+ const userEmail = getRequestUserEmail();
13
+ if (!userEmail) throw new Error("no authenticated user");
14
+ return { userEmail, orgId: getRequestOrgId() || null };
15
+ }
16
+
17
+ export default defineAction({
18
+ description:
19
+ "Create a temporary private agent-readable link for one Analytics session replay recording. The URL is scoped to that recording and expires after two hours.",
20
+ schema: z.object({
21
+ recordingId: z.string().describe("The session_recordings id"),
22
+ }),
23
+ readOnly: true,
24
+ run: async (args) => {
25
+ return createSessionReplayAgentLink({
26
+ recordingId: args.recordingId,
27
+ scope: resolveScope(),
28
+ origin: getRequestContext()?.requestOrigin,
29
+ });
30
+ },
31
+ });
@@ -803,6 +803,8 @@ const messages = {
803
803
  emptyStepVisit: "存取應用",
804
804
  installSnippetTitle: "新增工作階段回放",
805
805
  backToSessions: "返回工作階段",
806
+ copyForAgent: "複製給 Agent",
807
+ copiedForAgent: "已複製",
806
808
  askAgent: "詢問代理此工作階段",
807
809
  askSessionTitle: "詢問此工作階段",
808
810
  askSessionDescription: "將此回放作為脈絡,向 Analytics 代理傳送聚焦提示。",
@@ -841,6 +841,8 @@ const enUS = {
841
841
  emptyStepVisit: "Visit app",
842
842
  installSnippetTitle: "Add session replay",
843
843
  backToSessions: "Back to sessions",
844
+ copyForAgent: "Copy for agent",
845
+ copiedForAgent: "Copied",
844
846
  askAgent: "Ask agent about this session",
845
847
  askSessionTitle: "Ask about this session",
846
848
  askSessionDescription:
@@ -6839,6 +6841,8 @@ const translatedAnalyticsDebtCorrections = {
6839
6841
  anyActivity: "أي نشاط",
6840
6842
  eventCountCompact: "{{count}} حدث",
6841
6843
  pageCountCompact: "{{count}} صفحة",
6844
+ copyForAgent: "نسخ للوكيل",
6845
+ copiedForAgent: "تم النسخ",
6842
6846
  askSessionTitle: "اسأل عن هذه الجلسة",
6843
6847
  askSessionDescription:
6844
6848
  "أرسل مطالبة مركزة إلى وكيل Analytics مع إرفاق هذا التسجيل كسياق.",
@@ -6869,6 +6873,8 @@ const translatedAnalyticsDebtCorrections = {
6869
6873
  anyActivity: "Beliebige Aktivität",
6870
6874
  eventCountCompact: "{{count}} Ereignisse",
6871
6875
  pageCountCompact: "{{count}} Seiten",
6876
+ copyForAgent: "Für Agent kopieren",
6877
+ copiedForAgent: "Kopiert",
6872
6878
  askSessionTitle: "Zu dieser Sitzung fragen",
6873
6879
  askSessionDescription:
6874
6880
  "Sende dem Analytics-Agenten eine gezielte Eingabe mit dieser Wiedergabe als Kontext.",
@@ -6894,6 +6900,8 @@ const translatedAnalyticsDebtCorrections = {
6894
6900
  anyActivity: "Cualquier actividad",
6895
6901
  eventCountCompact: "{{count}} eventos",
6896
6902
  pageCountCompact: "{{count}} páginas",
6903
+ copyForAgent: "Copiar para agente",
6904
+ copiedForAgent: "Copiado",
6897
6905
  askSessionTitle: "Preguntar sobre esta sesión",
6898
6906
  askSessionDescription:
6899
6907
  "Envía una pregunta enfocada al agente de Analytics con esta reproducción como contexto.",
@@ -6922,6 +6930,8 @@ const translatedAnalyticsDebtCorrections = {
6922
6930
  anyActivity: "Toute activité",
6923
6931
  eventCountCompact: "{{count}} événements",
6924
6932
  pageCountCompact: "{{count}} pages",
6933
+ copyForAgent: "Copier pour l'agent",
6934
+ copiedForAgent: "Copié",
6925
6935
  askSessionTitle: "Poser une question sur cette session",
6926
6936
  askSessionDescription:
6927
6937
  "Envoyez une question ciblée à l'agent Analytics avec cette relecture en contexte.",
@@ -6948,6 +6958,8 @@ const translatedAnalyticsDebtCorrections = {
6948
6958
  anyActivity: "कोई भी गतिविधि",
6949
6959
  eventCountCompact: "{{count}} इवेंट",
6950
6960
  pageCountCompact: "{{count}} पेज",
6961
+ copyForAgent: "एजेंट के लिए कॉपी करें",
6962
+ copiedForAgent: "कॉपी हो गया",
6951
6963
  askSessionTitle: "इस सत्र के बारे में पूछें",
6952
6964
  askSessionDescription:
6953
6965
  "इस रीप्ले को संदर्भ के रूप में जोड़कर Analytics एजेंट को केंद्रित प्रॉम्प्ट भेजें।",
@@ -6973,6 +6985,8 @@ const translatedAnalyticsDebtCorrections = {
6973
6985
  anyActivity: "任意のアクティビティ",
6974
6986
  eventCountCompact: "{{count}} 件のイベント",
6975
6987
  pageCountCompact: "{{count}} ページ",
6988
+ copyForAgent: "エージェント用にコピー",
6989
+ copiedForAgent: "コピーしました",
6976
6990
  askSessionTitle: "このセッションについて質問",
6977
6991
  askSessionDescription:
6978
6992
  "このリプレイをコンテキストとして添えて、Analytics エージェントに絞り込んだプロンプトを送信します。",
@@ -6998,6 +7012,8 @@ const translatedAnalyticsDebtCorrections = {
6998
7012
  anyActivity: "모든 활동",
6999
7013
  eventCountCompact: "{{count}}개 이벤트",
7000
7014
  pageCountCompact: "{{count}}개 페이지",
7015
+ copyForAgent: "에이전트용으로 복사",
7016
+ copiedForAgent: "복사됨",
7001
7017
  askSessionTitle: "이 세션에 대해 질문",
7002
7018
  askSessionDescription:
7003
7019
  "이 리플레이를 컨텍스트로 첨부해 Analytics 에이전트에 집중된 프롬프트를 보냅니다.",
@@ -7023,6 +7039,8 @@ const translatedAnalyticsDebtCorrections = {
7023
7039
  anyActivity: "Qualquer atividade",
7024
7040
  eventCountCompact: "{{count}} eventos",
7025
7041
  pageCountCompact: "{{count}} páginas",
7042
+ copyForAgent: "Copiar para agente",
7043
+ copiedForAgent: "Copiado",
7026
7044
  askSessionTitle: "Perguntar sobre esta sessão",
7027
7045
  askSessionDescription:
7028
7046
  "Envie um prompt focado ao agente de Analytics com este replay como contexto.",
@@ -7048,6 +7066,8 @@ const translatedAnalyticsDebtCorrections = {
7048
7066
  anyActivity: "任意活动",
7049
7067
  eventCountCompact: "{{count}} 个事件",
7050
7068
  pageCountCompact: "{{count}} 个页面",
7069
+ copyForAgent: "复制给代理",
7070
+ copiedForAgent: "已复制",
7051
7071
  askSessionTitle: "询问此会话",
7052
7072
  askSessionDescription:
7053
7073
  "将此回放作为上下文,向 Analytics 代理发送聚焦提示。",
@@ -1,11 +1,16 @@
1
1
  import {
2
2
  appApiPath,
3
3
  PromptComposer,
4
+ useActionMutation,
5
+ useActionQuery,
4
6
  useSendToAgentChat,
5
7
  useT,
6
8
  } from "@agent-native/core/client";
9
+ import { SESSION_REPLAY_AGENT_ACCESS_PARAM } from "@shared/session-replay-agent-access";
7
10
  import {
8
11
  IconArrowLeft,
12
+ IconCheck,
13
+ IconCopy,
9
14
  IconExclamationCircle,
10
15
  IconKeyboard,
11
16
  IconMessageCircle,
@@ -201,7 +206,12 @@ export default function SessionDetailPage() {
201
206
  </div>
202
207
  ) : null}
203
208
  </div>
204
- {recording ? <AskSessionPopover recording={recording} /> : null}
209
+ {recording ? (
210
+ <div className="flex shrink-0 items-center gap-2">
211
+ <CopySessionForAgentButton recordingId={recording.id} />
212
+ <AskSessionPopover recording={recording} />
213
+ </div>
214
+ ) : null}
205
215
  </div>
206
216
 
207
217
  {error ? (
@@ -224,6 +234,41 @@ export default function SessionDetailPage() {
224
234
  );
225
235
  }
226
236
 
237
+ function CopySessionForAgentButton({ recordingId }: { recordingId: string }) {
238
+ const t = useT();
239
+ const [copied, setCopied] = useState(false);
240
+ const createLink = useActionMutation(
241
+ "create-session-replay-agent-link" as any,
242
+ );
243
+
244
+ async function handleCopy() {
245
+ if (createLink.isPending) return;
246
+ const result = (await createLink.mutateAsync({
247
+ recordingId,
248
+ })) as { url?: string };
249
+ if (!result?.url) return;
250
+ await navigator.clipboard.writeText(result.url).catch(() => {});
251
+ setCopied(true);
252
+ setTimeout(() => setCopied(false), 1400);
253
+ }
254
+
255
+ return (
256
+ <Button
257
+ type="button"
258
+ variant="outline"
259
+ onClick={() => void handleCopy()}
260
+ disabled={createLink.isPending}
261
+ >
262
+ {copied ? (
263
+ <IconCheck className="h-4 w-4" />
264
+ ) : (
265
+ <IconCopy className="h-4 w-4" />
266
+ )}
267
+ {copied ? t("sessions.copiedForAgent") : t("sessions.copyForAgent")}
268
+ </Button>
269
+ );
270
+ }
271
+
227
272
  function AskSessionPopover({
228
273
  recording,
229
274
  }: {
@@ -976,8 +1021,9 @@ function DetailSkeleton() {
976
1021
  }
977
1022
 
978
1023
  function useSessionReplayPlayback(recordingId: string) {
1024
+ const agentAccessToken = currentSessionReplayAgentAccessToken();
979
1025
  return useQuery({
980
- queryKey: ["session-replay-playback", recordingId],
1026
+ queryKey: ["session-replay-playback", recordingId, agentAccessToken],
981
1027
  enabled: Boolean(recordingId),
982
1028
  staleTime: 30_000,
983
1029
  gcTime: 60_000,
@@ -1080,9 +1126,25 @@ function replayUnavailableChunk(
1080
1126
  };
1081
1127
  }
1082
1128
 
1129
+ function currentSessionReplayAgentAccessToken(): string {
1130
+ const browserSearch =
1131
+ typeof window === "undefined" ? "" : window.location.search;
1132
+ return (
1133
+ new URLSearchParams(browserSearch).get(SESSION_REPLAY_AGENT_ACCESS_PARAM) ??
1134
+ ""
1135
+ );
1136
+ }
1137
+
1083
1138
  async function fetchReplayApi(path: string): Promise<Response> {
1084
1139
  const token = await getIdToken();
1085
- return fetch(appApiPath(path), {
1140
+ const browserOrigin =
1141
+ typeof window === "undefined" ? "http://localhost" : window.location.origin;
1142
+ const url = new URL(appApiPath(path), browserOrigin);
1143
+ const agentAccessToken = currentSessionReplayAgentAccessToken();
1144
+ if (agentAccessToken) {
1145
+ url.searchParams.set(SESSION_REPLAY_AGENT_ACCESS_PARAM, agentAccessToken);
1146
+ }
1147
+ return fetch(`${url.pathname}${url.search}${url.hash}`, {
1086
1148
  headers: {
1087
1149
  Accept: "application/json",
1088
1150
  ...(token ? { Authorization: `Bearer ${token}` } : {}),
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: added
3
+ date: 2026-07-02
4
+ ---
5
+
6
+ Session replays can be copied as temporary private links for agents.