@agent-native/core 0.84.57 → 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 (69) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +8 -0
  3. package/corpus/core/package.json +1 -1
  4. package/corpus/core/src/action.ts +11 -0
  5. package/corpus/core/src/agent/production-agent.ts +24 -46
  6. package/corpus/core/src/client/AssistantChat.tsx +69 -12
  7. package/corpus/core/src/client/MultiTabAssistantChat.tsx +1 -1
  8. package/corpus/core/src/client/conversation/use-near-bottom-autoscroll.ts +45 -2
  9. package/corpus/core/src/coding-tools/run-code.ts +1 -0
  10. package/corpus/core/src/integrations/webhook-handler.ts +10 -9
  11. package/corpus/core/src/server/action-discovery.ts +3 -0
  12. package/corpus/core/src/server/credential-provider.ts +11 -6
  13. package/corpus/templates/analytics/server/lib/agent-chat-plan-mode.ts +29 -0
  14. package/corpus/templates/analytics/server/plugins/agent-chat.ts +9 -1
  15. package/corpus/templates/design/actions/list-design-extensions.ts +2 -2
  16. package/corpus/templates/design/app/components/design/CodeWorkbenchHost.tsx +11 -89
  17. package/corpus/templates/design/app/components/design/DesignExtensionsPanel.tsx +314 -181
  18. package/corpus/templates/design/app/components/design/DesignImportPanel.tsx +92 -134
  19. package/corpus/templates/design/app/components/design/EditPanel.tsx +93 -19
  20. package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +18 -26
  21. package/corpus/templates/design/app/components/design/TokensPanel.tsx +220 -192
  22. package/corpus/templates/design/app/components/design/code-workbench-theme.ts +150 -0
  23. package/corpus/templates/design/app/i18n/zh-TW.ts +5 -7
  24. package/corpus/templates/design/app/i18n-data.ts +61 -77
  25. package/corpus/templates/design/app/lib/design-import.ts +4 -1
  26. package/corpus/templates/design/app/pages/DesignEditor.tsx +97 -48
  27. package/corpus/templates/design/changelog/2026-07-02-design-inspector-and-canvas-controls-stay-consistent-when-se.md +6 -0
  28. package/corpus/templates/design/changelog/2026-07-02-fixed-a-crash-when-opening-the-code-editor.md +6 -0
  29. package/corpus/templates/design/changelog/2026-07-02-fixed-motion-drawer-open-transition.md +6 -0
  30. package/corpus/templates/design/changelog/2026-07-02-improved-design-assets-panel.md +6 -0
  31. package/dist/action.d.ts +8 -0
  32. package/dist/action.d.ts.map +1 -1
  33. package/dist/action.js +3 -0
  34. package/dist/action.js.map +1 -1
  35. package/dist/agent/production-agent.d.ts +10 -11
  36. package/dist/agent/production-agent.d.ts.map +1 -1
  37. package/dist/agent/production-agent.js +21 -44
  38. package/dist/agent/production-agent.js.map +1 -1
  39. package/dist/client/AssistantChat.d.ts +1 -0
  40. package/dist/client/AssistantChat.d.ts.map +1 -1
  41. package/dist/client/AssistantChat.js +61 -11
  42. package/dist/client/AssistantChat.js.map +1 -1
  43. package/dist/client/MultiTabAssistantChat.js +1 -1
  44. package/dist/client/MultiTabAssistantChat.js.map +1 -1
  45. package/dist/client/conversation/use-near-bottom-autoscroll.d.ts.map +1 -1
  46. package/dist/client/conversation/use-near-bottom-autoscroll.js +43 -2
  47. package/dist/client/conversation/use-near-bottom-autoscroll.js.map +1 -1
  48. package/dist/coding-tools/run-code.d.ts.map +1 -1
  49. package/dist/coding-tools/run-code.js +1 -0
  50. package/dist/coding-tools/run-code.js.map +1 -1
  51. package/dist/collab/awareness.d.ts +2 -2
  52. package/dist/collab/awareness.d.ts.map +1 -1
  53. package/dist/collab/routes.d.ts +1 -1
  54. package/dist/file-upload/actions/upload-image.d.ts +2 -2
  55. package/dist/integrations/webhook-handler.d.ts +1 -0
  56. package/dist/integrations/webhook-handler.d.ts.map +1 -1
  57. package/dist/integrations/webhook-handler.js +9 -9
  58. package/dist/integrations/webhook-handler.js.map +1 -1
  59. package/dist/notifications/routes.d.ts +3 -3
  60. package/dist/observability/routes.d.ts +5 -5
  61. package/dist/server/action-discovery.d.ts.map +1 -1
  62. package/dist/server/action-discovery.js +3 -0
  63. package/dist/server/action-discovery.js.map +1 -1
  64. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  65. package/dist/server/credential-provider.d.ts +4 -0
  66. package/dist/server/credential-provider.d.ts.map +1 -1
  67. package/dist/server/credential-provider.js +13 -7
  68. package/dist/server/credential-provider.js.map +1 -1
  69. package/package.json +1 -1
package/corpus/README.md CHANGED
@@ -28,4 +28,4 @@ rg -n "defineAction|useActionQuery" node_modules/@agent-native/core/corpus
28
28
  ## Generated Counts
29
29
 
30
30
  - core files: 2044
31
- - template files: 5012
31
+ - template files: 5018
@@ -1,5 +1,13 @@
1
1
  # @agent-native/core
2
2
 
3
+ ## 0.84.58
4
+
5
+ ### Patch Changes
6
+
7
+ - cdc9033: Recover durable background chat runs when action input preparation stops making byte progress, and avoid duplicate tool cards when reconnects replay completed tool events.
8
+ - cdc9033: Remove the shadow from agent chat context scope pills.
9
+ - cdc9033: Prevent agent chat sidebars from jittering around nested scroll areas and suppress replayed reconnect tool cards that are already rendered in the saved thread.
10
+
3
11
  ## 0.84.57
4
12
 
5
13
  ### Patch Changes
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-native/core",
3
- "version": "0.84.57",
3
+ "version": "0.84.58",
4
4
  "description": "Framework for agent-native application development — where AI agents and UI share SQL state, actions, and context",
5
5
  "homepage": "https://github.com/BuilderIO/agent-native#readme",
6
6
  "bugs": {
@@ -345,6 +345,10 @@ interface DefineActionWithSchema<
345
345
  * Only set this manually when you need to override the inference — e.g. a
346
346
  * POST action that only reads data but can't use GET for a protocol reason. */
347
347
  readOnly?: boolean;
348
+ /** Set false for read-only tools that should stay available in Act mode but
349
+ * must not run during Plan mode because they perform substantive work
350
+ * rather than lightweight inspection. Defaults to allowed when read-only. */
351
+ allowInPlanMode?: boolean;
348
352
  /** If true, the agent may execute this action concurrently with other
349
353
  * read-only or parallel-safe tool calls emitted in the same model turn.
350
354
  * Only set this for mutating actions that are internally concurrency-safe
@@ -460,6 +464,9 @@ interface DefineActionWithParams<
460
464
  /** If true, the framework will NOT emit a screen-refresh change event after a
461
465
  * successful call. Auto-inferred as `true` when `http.method === "GET"`. */
462
466
  readOnly?: boolean;
467
+ /** Set false for read-only tools that should stay available in Act mode but
468
+ * must not run during Plan mode. See the schema overload above. */
469
+ allowInPlanMode?: boolean;
463
470
  /** If true, the agent may execute this action concurrently with other
464
471
  * read-only or parallel-safe tool calls emitted in the same model turn. */
465
472
  parallelSafe?: boolean;
@@ -525,6 +532,7 @@ export interface ActionDefinition<TInput, TReturn> {
525
532
  readonly maxBodyBytes?: number;
526
533
  readonly agentTool?: boolean;
527
534
  readonly readOnly?: boolean;
535
+ readonly allowInPlanMode?: boolean;
528
536
  readonly parallelSafe?: boolean;
529
537
  readonly toolCallable?: boolean;
530
538
  readonly publicAgent?: PublicAgentActionConfig;
@@ -752,6 +760,9 @@ export function defineAction(options: any) {
752
760
  : {}),
753
761
  ...(typeof agentTool === "boolean" ? { agentTool } : {}),
754
762
  ...(typeof readOnly === "boolean" ? { readOnly } : {}),
763
+ ...(typeof options.allowInPlanMode === "boolean"
764
+ ? { allowInPlanMode: options.allowInPlanMode }
765
+ : {}),
755
766
  ...(typeof parallelSafe === "boolean" ? { parallelSafe } : {}),
756
767
  ...(typeof toolCallable === "boolean" ? { toolCallable } : {}),
757
768
  ...(publicAgent ? { publicAgent } : {}),
@@ -26,7 +26,7 @@ import {
26
26
  parseFrontmatter,
27
27
  } from "../resources/metadata.js";
28
28
  import {
29
- isDeployCredentialFallbackAllowed,
29
+ canUseDeployCredentialFallbackForRequest,
30
30
  readDeployCredentialEnv,
31
31
  } from "../server/credential-provider.js";
32
32
  import { readBody } from "../server/h3-helpers.js";
@@ -457,32 +457,16 @@ export function engineToProvider(engineName: string): string {
457
457
  return engineName.startsWith("ai-sdk:") ? engineName.slice(7) : engineName;
458
458
  }
459
459
 
460
- /**
461
- * Returns true when this process should block generic deploy-level provider
462
- * credentials for signed-in chat requests.
463
- *
464
- * Self-hosted single-tenant deployments keep the env-var fallback so the
465
- * original BYO-server UX continues to work without a per-user key.
466
- */
467
- function shouldBlockDeployCredentialFallback(): boolean {
468
- return !isDeployCredentialFallbackAllowed();
469
- }
470
-
471
460
  /**
472
461
  * Resolve the active engine's provider and look up the user's API key for it.
473
462
  *
474
- * In shared hosted deploys we deliberately refuse the deploy-level fallback
475
- * for authenticated users. Without that gate any
476
- * signed-in user who hasn't configured their own provider key would silently
477
- * inherit the deployment's key (uncapped billing on the owner's account,
478
- * prompt logging tied to the deployment owner) — exactly the prior-incident
479
- * pattern we hit on 2026-04-29.
480
- *
481
- * Single-tenant (local-dev, self-hosted SQLite) keeps the env fallback.
463
+ * If the owner has no scoped key, fall back to provider keys supplied by the
464
+ * hosting environment only when the current request can safely use deploy-level
465
+ * credentials. This is a read from process-level config, not a request-scoped
466
+ * write to `process.env`.
482
467
  *
483
- * Callers in `agent-chat-plugin.ts`, `triggers/dispatcher.ts`,
484
- * `jobs/scheduler.ts`, and `integrations/plugin.ts` historically layer
485
- * another deployment-key fallback after this must keep the same gate.
468
+ * Callers that layer another deployment-key fallback after this should keep the
469
+ * same precedence: scoped key first, host-provided env key second.
486
470
  */
487
471
  export async function getOwnerActiveApiKey(
488
472
  ownerEmail: string | null | undefined,
@@ -495,14 +479,7 @@ export async function getOwnerActiveApiKey(
495
479
  const provider = engineToProvider(activeEngine);
496
480
  const userKey = await getOwnerApiKey(provider, ownerEmail);
497
481
  if (userKey) return userKey;
498
- if (shouldBlockDeployCredentialFallback()) {
499
- // Shared hosted default: refuse the env fallback. A null user
500
- // (unauthenticated / background context with no owner) gets undefined
501
- // here too — there's no user to bill, and the call site must surface a
502
- // "configure a key" error to the requester rather than silently using
503
- // the deploy key.
504
- return undefined;
505
- }
482
+ if (!canUseDeployCredentialFallbackForRequest()) return undefined;
506
483
  const envVar = PROVIDER_TO_ENV[provider];
507
484
  return envVar ? readDeployCredentialEnv(envVar) : undefined;
508
485
  } catch {
@@ -552,6 +529,10 @@ export interface ActionEntry {
552
529
  /** If true, completion does NOT trigger a screen-refresh change event.
553
530
  * Set automatically by `defineAction` when `http.method === "GET"`. */
554
531
  readOnly?: boolean;
532
+ /** False keeps a read-only tool available in Act mode but hides/blocks it in
533
+ * Plan mode. Use for tools that perform substantive work even without
534
+ * mutating state. */
535
+ allowInPlanMode?: boolean;
555
536
  /** If true, this action can run concurrently with other same-turn
556
537
  * read-only/parallel-safe tool calls. Only use for actions that handle
557
538
  * their own write ordering and idempotency. */
@@ -705,6 +686,7 @@ export function isPlanModeToolCallAllowed(
705
686
  input: unknown,
706
687
  entry: ActionEntry,
707
688
  ): boolean {
689
+ if (entry.allowInPlanMode === false) return false;
708
690
  if (PLAN_MODE_BLOCKED_READONLY_TOOLS.has(name)) return false;
709
691
 
710
692
  if (name === "web-request") {
@@ -802,6 +784,7 @@ export function createPlanModeActionRegistry(
802
784
 
803
785
  for (const [name, entry] of Object.entries(actions)) {
804
786
  if (name === TOOL_SEARCH_ACTION_NAME) continue;
787
+ if (entry.allowInPlanMode === false) continue;
805
788
  if (PLAN_MODE_BLOCKED_READONLY_TOOLS.has(name)) continue;
806
789
 
807
790
  const allowedActions = PLAN_MODE_ALLOWED_ACTIONS[name];
@@ -4760,15 +4743,14 @@ export function createProductionAgentHandler(
4760
4743
  // for that provider instead of the global active engine's provider.
4761
4744
  // DIAGNOSTIC-ONLY: bracket per-owner API-key resolution (settings/app_secrets reads).
4762
4745
  workerStep("apikey_start");
4746
+ const allowDeployCredentialFallback =
4747
+ canUseDeployCredentialFallbackForRequest();
4763
4748
  let userApiKey: string | undefined;
4764
4749
  if (requestEngine) {
4765
4750
  const provider = engineToProvider(requestEngine);
4766
4751
  userApiKey = await getOwnerApiKey(provider, ownerEmail);
4767
- if (!userApiKey && !shouldBlockDeployCredentialFallback()) {
4768
- // Single-tenant only: env fallback for the requested provider. Shared
4769
- // hosted deploys never silently substitute the deploy-level key for
4770
- // an authenticated user (see getOwnerActiveApiKey for the full
4771
- // rationale).
4752
+ if (!userApiKey && allowDeployCredentialFallback) {
4753
+ // Read-only env fallback for the requested provider.
4772
4754
  const envVar = PROVIDER_TO_ENV[provider];
4773
4755
  userApiKey = envVar ? readDeployCredentialEnv(envVar) : undefined;
4774
4756
  }
@@ -4779,16 +4761,12 @@ export function createProductionAgentHandler(
4779
4761
  workerStep("apikey_done");
4780
4762
 
4781
4763
  // `options.apiKey` is the value the template constructed the plugin with
4782
- // (e.g. wired from a deployment env var). On a shared hosted deploy this
4783
- // is the same cross-tenant hazard as any deploy-level provider key:
4784
- // accepting it as the final fallback would silently bill every key-less
4785
- // user to the deployment's account. Honour it only when the generic
4786
- // deploy fallback policy allows it.
4787
- const effectiveApiKey = shouldBlockDeployCredentialFallback()
4788
- ? userApiKey
4789
- : (userApiKey ??
4790
- options.apiKey ??
4791
- readDeployCredentialEnv("ANTHROPIC_API_KEY"));
4764
+ // (often wired from a deployment env var). Honor it as host-provided
4765
+ // read-only configuration after scoped keys when deploy fallback is safe.
4766
+ const hostApiKey = allowDeployCredentialFallback
4767
+ ? (options.apiKey ?? readDeployCredentialEnv("ANTHROPIC_API_KEY"))
4768
+ : undefined;
4769
+ const effectiveApiKey = userApiKey ?? hostApiKey;
4792
4770
 
4793
4771
  // Resolve engine — per-request engine override takes priority
4794
4772
  // DIAGNOSTIC-ONLY: bracket engine resolution (Builder credential / app-default
@@ -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()
@@ -0,0 +1,29 @@
1
+ import type { ActionEntry } from "@agent-native/core/server";
2
+
3
+ export const PLAN_MODE_ACT_ONLY_TOOLS = new Set([
4
+ "query-agent-native-analytics",
5
+ "bigquery",
6
+ "provider-api-request",
7
+ "provider-corpus-job",
8
+ "query-staged-dataset",
9
+ "account-deep-dive",
10
+ "hubspot-deals",
11
+ "hubspot-records",
12
+ "gong-calls",
13
+ "jira-search",
14
+ "slack-messages",
15
+ "sentry",
16
+ ]);
17
+
18
+ export function applyAnalyticsPlanModePolicy(
19
+ actions: Record<string, ActionEntry>,
20
+ ): Record<string, ActionEntry> {
21
+ return Object.fromEntries(
22
+ Object.entries(actions).map(([name, entry]) => [
23
+ name,
24
+ PLAN_MODE_ACT_ONLY_TOOLS.has(name)
25
+ ? { ...entry, allowInPlanMode: false }
26
+ : entry,
27
+ ]),
28
+ );
29
+ }
@@ -7,6 +7,7 @@ import {
7
7
  } from "@agent-native/core/server";
8
8
 
9
9
  import actionsRegistry from "../../.generated/actions-registry.js";
10
+ import { applyAnalyticsPlanModePolicy } from "../lib/agent-chat-plan-mode";
10
11
  import { renderDataDictionary } from "../lib/data-dictionary-context";
11
12
  import {
12
13
  failedDataQueryAttemptMessage,
@@ -65,6 +66,11 @@ const INITIAL_TOOL_NAMES = [
65
66
  "navigate",
66
67
  ];
67
68
 
69
+ export {
70
+ applyAnalyticsPlanModePolicy,
71
+ PLAN_MODE_ACT_ONLY_TOOLS,
72
+ } from "../lib/agent-chat-plan-mode";
73
+
68
74
  function latestUserText(
69
75
  messages: AgentLoopFinalResponseGuardContext["messages"],
70
76
  ): string {
@@ -160,7 +166,9 @@ function realDataFinalGuard(context: AgentLoopFinalResponseGuardContext) {
160
166
 
161
167
  export default createAgentChatPlugin({
162
168
  appId: "analytics",
163
- actions: loadActionsFromStaticRegistry(actionsRegistry),
169
+ actions: applyAnalyticsPlanModePolicy(
170
+ loadActionsFromStaticRegistry(actionsRegistry),
171
+ ),
164
172
  initialToolNames: INITIAL_TOOL_NAMES,
165
173
  finalResponseGuard: realDataFinalGuard,
166
174
  // Enable sandboxed JavaScript execution for analytics data processing.
@@ -68,7 +68,7 @@ const FIRST_PARTY_EXTENSIONS: FirstPartyDesignExtension[] = [
68
68
  id: "design.asset-library",
69
69
  name: "Asset Library",
70
70
  description:
71
- "Browse and insert Design-native primitives, generated or uploaded media, and rendered Figma library components into the active design screen. " +
71
+ "Browse and insert Design primitives, generated or uploaded media, and rendered Figma library components into the active design screen. " +
72
72
  "Selection-aware: inserts near the selected element when one is active.",
73
73
  icon: "Photo",
74
74
  availability: "available",
@@ -77,7 +77,7 @@ const FIRST_PARTY_EXTENSIONS: FirstPartyDesignExtension[] = [
77
77
  capabilities: [
78
78
  {
79
79
  id: "native",
80
- label: "Browse Design-native assets",
80
+ label: "Browse Design assets",
81
81
  status: "available",
82
82
  reason:
83
83
  "Uses list-design-native-assets and insert-design-native-asset for editable HTML primitives/components.",