@agent-native/core 0.84.25 → 0.84.27

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 (67) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +12 -0
  3. package/corpus/core/package.json +1 -1
  4. package/corpus/core/src/agent/production-agent.ts +4 -2
  5. package/corpus/core/src/agent/run-manager.ts +4 -3
  6. package/corpus/core/src/agent/types.ts +9 -2
  7. package/corpus/core/src/client/AssistantChat.tsx +15 -1
  8. package/corpus/core/src/client/FeedbackButton.tsx +2 -2
  9. package/corpus/core/src/client/components/PresenceBar.tsx +7 -2
  10. package/corpus/core/src/client/composer/ComposerPlusMenu.tsx +23 -9
  11. package/corpus/core/src/client/sharing/ShareButton.tsx +22 -3
  12. package/corpus/core/src/client/sharing/ShareDialog.tsx +13 -1
  13. package/corpus/core/src/client/sse-event-processor.ts +49 -20
  14. package/corpus/core/src/sharing/actions/set-resource-visibility.ts +3 -3
  15. package/corpus/templates/design/actions/insert-asset.ts +14 -2
  16. package/corpus/templates/design/actions/insert-design-native-asset.ts +16 -4
  17. package/corpus/templates/design/actions/insert-figma-library-asset.ts +14 -2
  18. package/corpus/templates/design/app/components/design/DesignCanvas.tsx +14 -0
  19. package/corpus/templates/design/app/components/design/DesignExtensionsPanel.tsx +54 -10
  20. package/corpus/templates/design/app/components/design/EditPanel.tsx +23 -16
  21. package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +7 -2
  22. package/corpus/templates/design/app/components/design/inspector/InspectorAiActions.tsx +9 -5
  23. package/corpus/templates/design/app/components/layout/Header.tsx +44 -1
  24. package/corpus/templates/design/app/pages/DesignEditor.tsx +267 -105
  25. package/corpus/templates/design/app/pages/DesignSystemSetup.tsx +15 -1
  26. package/corpus/templates/design/app/pages/Index.tsx +20 -9
  27. package/corpus/templates/design/app/routes/settings.tsx +1 -1
  28. package/corpus/templates/design/changelog/2026-07-01-design-editor-polish-makes-zoom-assets-sharing-and-agent-handoffs-clearer.md +6 -0
  29. package/corpus/templates/design/changelog/2026-07-01-design-qa-fixes-make-exports-permissions-and-tools-clearer.md +6 -0
  30. package/dist/agent/production-agent.d.ts.map +1 -1
  31. package/dist/agent/production-agent.js +4 -3
  32. package/dist/agent/production-agent.js.map +1 -1
  33. package/dist/agent/run-manager.d.ts.map +1 -1
  34. package/dist/agent/run-manager.js +4 -4
  35. package/dist/agent/run-manager.js.map +1 -1
  36. package/dist/agent/types.d.ts +3 -0
  37. package/dist/agent/types.d.ts.map +1 -1
  38. package/dist/agent/types.js.map +1 -1
  39. package/dist/client/AssistantChat.d.ts.map +1 -1
  40. package/dist/client/AssistantChat.js +16 -1
  41. package/dist/client/AssistantChat.js.map +1 -1
  42. package/dist/client/FeedbackButton.js +2 -2
  43. package/dist/client/FeedbackButton.js.map +1 -1
  44. package/dist/client/components/PresenceBar.d.ts.map +1 -1
  45. package/dist/client/components/PresenceBar.js +6 -2
  46. package/dist/client/components/PresenceBar.js.map +1 -1
  47. package/dist/client/composer/ComposerPlusMenu.d.ts.map +1 -1
  48. package/dist/client/composer/ComposerPlusMenu.js +15 -7
  49. package/dist/client/composer/ComposerPlusMenu.js.map +1 -1
  50. package/dist/client/sharing/ShareButton.js +15 -2
  51. package/dist/client/sharing/ShareButton.js.map +1 -1
  52. package/dist/client/sharing/ShareDialog.js +16 -1
  53. package/dist/client/sharing/ShareDialog.js.map +1 -1
  54. package/dist/client/sse-event-processor.d.ts.map +1 -1
  55. package/dist/client/sse-event-processor.js +42 -16
  56. package/dist/client/sse-event-processor.js.map +1 -1
  57. package/dist/collab/awareness.d.ts +2 -2
  58. package/dist/collab/awareness.d.ts.map +1 -1
  59. package/dist/collab/routes.d.ts +1 -1
  60. package/dist/file-upload/actions/upload-image.d.ts +2 -2
  61. package/dist/notifications/routes.d.ts +3 -3
  62. package/dist/observability/routes.d.ts +6 -6
  63. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  64. package/dist/server/transcribe-voice.d.ts +1 -1
  65. package/dist/sharing/actions/set-resource-visibility.js +3 -3
  66. package/dist/sharing/actions/set-resource-visibility.js.map +1 -1
  67. 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: 2043
31
- - template files: 4969
31
+ - template files: 4971
@@ -1,5 +1,17 @@
1
1
  # @agent-native/core
2
2
 
3
+ ## 0.84.27
4
+
5
+ ### Patch Changes
6
+
7
+ - 6fb954b: Require admin access for shareable resource visibility changes.
8
+
9
+ ## 0.84.26
10
+
11
+ ### Patch Changes
12
+
13
+ - f041345: Track action-preparation progress by streamed tool-input id so parallel same-name tool calls do not hide real progress.
14
+
3
15
  ## 0.84.25
4
16
 
5
17
  ### Patch Changes
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-native/core",
3
- "version": "0.84.25",
3
+ "version": "0.84.27",
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": {
@@ -2795,6 +2795,7 @@ export async function runAgentLoop(opts: {
2795
2795
  let lastToolInputActivityAt = 0;
2796
2796
  const sendToolInputActivity = (
2797
2797
  toolName: string | undefined,
2798
+ toolInputId?: string,
2798
2799
  progressBytes?: number,
2799
2800
  force = false,
2800
2801
  ) => {
@@ -2810,6 +2811,7 @@ export async function runAgentLoop(opts: {
2810
2811
  type: "activity",
2811
2812
  label: toolInputActivityLabel(toolName),
2812
2813
  ...(toolName ? { tool: toolName } : {}),
2814
+ ...(toolInputId ? { id: toolInputId } : {}),
2813
2815
  ...(typeof progressBytes === "number" ? { progressBytes } : {}),
2814
2816
  });
2815
2817
  };
@@ -2848,7 +2850,7 @@ export async function runAgentLoop(opts: {
2848
2850
  toolInputNames.set(key, event.name);
2849
2851
  toolInputBytes.set(key, 0);
2850
2852
  }
2851
- sendToolInputActivity(event.name, undefined, true);
2853
+ sendToolInputActivity(event.name, key, undefined, true);
2852
2854
  } else if (event.type === "tool-input-delta") {
2853
2855
  const key = event.id ?? event.name;
2854
2856
  const toolName =
@@ -2862,7 +2864,7 @@ export async function runAgentLoop(opts: {
2862
2864
  new TextEncoder().encode(event.text ?? "").byteLength;
2863
2865
  toolInputBytes.set(key, progressBytes);
2864
2866
  }
2865
- sendToolInputActivity(toolName, progressBytes);
2867
+ sendToolInputActivity(toolName, key, progressBytes);
2866
2868
  } else if (event.type === "gateway-heartbeat") {
2867
2869
  send({ type: "stream_keepalive" });
2868
2870
  } else if (event.type === "tool-call") {
@@ -441,7 +441,8 @@ export function startRun(
441
441
  const shouldBumpProgressForEvent = (event: AgentChatEvent): boolean => {
442
442
  if (event.type === "stream_keepalive") return false;
443
443
  if (event.type === "activity" && isPreparingActionActivityEvent(event)) {
444
- const toolKey = event.tool?.trim() || event.label.trim();
444
+ const toolKey =
445
+ event.id?.trim() || event.tool?.trim() || event.label.trim();
445
446
  const progressBytes =
446
447
  typeof event.progressBytes === "number" &&
447
448
  Number.isFinite(event.progressBytes) &&
@@ -449,6 +450,7 @@ export function startRun(
449
450
  ? Math.floor(event.progressBytes)
450
451
  : undefined;
451
452
  if (progressBytes === undefined) return false;
453
+ if (!event.id?.trim()) return progressBytes > 0;
452
454
  const previousBytes = preparingActivityBytes.get(toolKey) ?? 0;
453
455
  if (progressBytes <= previousBytes) {
454
456
  preparingActivityBytes.set(
@@ -461,8 +463,7 @@ export function startRun(
461
463
  return true;
462
464
  }
463
465
  if (event.type === "tool_start" || event.type === "tool_done") {
464
- const tool = event.tool?.trim();
465
- if (tool) preparingActivityBytes.delete(tool);
466
+ preparingActivityBytes.clear();
466
467
  }
467
468
  if (
468
469
  event.type === "clear" ||
@@ -212,12 +212,19 @@ export type AgentToolInput = Record<string, unknown>;
212
212
  export type AgentChatEvent =
213
213
  | { type: "text"; text: string }
214
214
  | { type: "thinking"; text: string }
215
- | { type: "activity"; label: string; tool?: string; progressBytes?: number }
215
+ | {
216
+ type: "activity";
217
+ label: string;
218
+ tool?: string;
219
+ id?: string;
220
+ progressBytes?: number;
221
+ }
216
222
  | { type: "stream_keepalive" }
217
- | { type: "tool_start"; tool: string; input: AgentToolInput }
223
+ | { type: "tool_start"; tool: string; id?: string; input: AgentToolInput }
218
224
  | {
219
225
  type: "tool_done";
220
226
  tool: string;
227
+ id?: string;
221
228
  input?: AgentToolInput;
222
229
  result: string;
223
230
  isError?: boolean;
@@ -1456,6 +1456,7 @@ const AssistantChatInner = forwardRef<
1456
1456
  // When stop is clicked during reconnect, keep content visible (don't wipe it)
1457
1457
  const [reconnectFrozen, setReconnectFrozen] = useState(false);
1458
1458
  const reconnectRunIdRef = useRef<string | null>(null);
1459
+ const reconnectTailOnlyRef = useRef(false);
1459
1460
  const reconnectCanMaterializeRef = useRef(false);
1460
1461
  const reconnectAbortRef = useRef<AbortController | null>(null);
1461
1462
  // Nuclear stop: user clicked stop. Clears the stop button/indicator AND
@@ -1775,6 +1776,7 @@ const AssistantChatInner = forwardRef<
1775
1776
 
1776
1777
  reconnectRunIdRef.current = runId;
1777
1778
  const afterSeq = resolveReconnectAfterSeq(threadId, runId);
1779
+ reconnectTailOnlyRef.current = afterSeq > 0;
1778
1780
  reconnectCanMaterializeRef.current = afterSeq === 0;
1779
1781
  const storedActivityTool = getActiveRunActivityTool(threadId, runId);
1780
1782
  setRunningActivityTool(storedActivityTool);
@@ -1941,6 +1943,7 @@ const AssistantChatInner = forwardRef<
1941
1943
  await refreshThreadFromServer();
1942
1944
  setReconnectContent([]);
1943
1945
  setReconnectFrozen(false);
1946
+ reconnectTailOnlyRef.current = false;
1944
1947
  } else {
1945
1948
  settleInterruptedToolCalls(latestContent);
1946
1949
  setReconnectContent([...latestContent]);
@@ -1959,6 +1962,7 @@ const AssistantChatInner = forwardRef<
1959
1962
  reconnectAbortRef.current = null;
1960
1963
  setIsReconnecting(false);
1961
1964
  reconnectRunIdRef.current = null;
1965
+ reconnectTailOnlyRef.current = false;
1962
1966
  if (afterSeq > 0) {
1963
1967
  reconnectCanMaterializeRef.current = false;
1964
1968
  }
@@ -1994,6 +1998,7 @@ const AssistantChatInner = forwardRef<
1994
1998
  reconnectAbortRef.current = null;
1995
1999
  setIsReconnecting(false);
1996
2000
  reconnectRunIdRef.current = null;
2001
+ reconnectTailOnlyRef.current = false;
1997
2002
  if (loaded || afterSeq > 0 || latestContent.length === 0) {
1998
2003
  reconnectCanMaterializeRef.current = false;
1999
2004
  }
@@ -2023,6 +2028,7 @@ const AssistantChatInner = forwardRef<
2023
2028
  useCallback(async (): Promise<boolean> => {
2024
2029
  if (!threadId) return false;
2025
2030
  try {
2031
+ const storedActiveRun = getActiveRun();
2026
2032
  const runRes = await fetch(
2027
2033
  `${apiUrl}/runs/active?threadId=${encodeURIComponent(threadId)}`,
2028
2034
  );
@@ -2033,6 +2039,11 @@ const AssistantChatInner = forwardRef<
2033
2039
  runInfo.status !== "running" ||
2034
2040
  activeRunLooksStale(runInfo)
2035
2041
  ) {
2042
+ if (storedActiveRun?.threadId === threadId) {
2043
+ clearActiveRunIfMatches(threadId, storedActiveRun.runId);
2044
+ } else if (runInfo.runId) {
2045
+ clearActiveRunIfMatches(threadId, String(runInfo.runId));
2046
+ }
2036
2047
  await refreshThreadFromServer();
2037
2048
  return false;
2038
2049
  }
@@ -2819,7 +2830,9 @@ const AssistantChatInner = forwardRef<
2819
2830
  reconnectRunIdRef.current = null;
2820
2831
  setIsReconnecting(false);
2821
2832
  const shouldFreezeReconnectContent =
2822
- reconnectCanMaterializeRef.current && reconnectContent.length > 0;
2833
+ !reconnectTailOnlyRef.current &&
2834
+ reconnectCanMaterializeRef.current &&
2835
+ reconnectContent.length > 0;
2823
2836
  if (shouldFreezeReconnectContent) {
2824
2837
  setReconnectFrozen(true);
2825
2838
  } else {
@@ -2827,6 +2840,7 @@ const AssistantChatInner = forwardRef<
2827
2840
  setReconnectContent([]);
2828
2841
  reconnectCanMaterializeRef.current = false;
2829
2842
  }
2843
+ reconnectTailOnlyRef.current = false;
2830
2844
  }
2831
2845
  threadRuntime.cancelRun();
2832
2846
  if (typeof window !== "undefined") {
@@ -429,7 +429,7 @@ export function FeedbackButton({
429
429
  <TooltipPrimitive.Portal>
430
430
  <TooltipPrimitive.Content
431
431
  sideOffset={6}
432
- className="z-[300] overflow-hidden rounded-md border border-border bg-popover px-2 py-1 text-[11px] text-foreground shadow-md animate-in fade-in-0 zoom-in-95"
432
+ className="z-[100040] overflow-hidden rounded-md border border-border bg-popover px-2 py-1 text-[11px] text-foreground shadow-md animate-in fade-in-0 zoom-in-95"
433
433
  >
434
434
  {resolvedLabel}
435
435
  </TooltipPrimitive.Content>
@@ -482,7 +482,7 @@ export function FeedbackButton({
482
482
  align={align}
483
483
  sideOffset={8}
484
484
  collisionPadding={16}
485
- className="z-[300] overflow-hidden rounded-lg border border-border bg-popover shadow-xl outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2"
485
+ className="z-[100040] overflow-hidden rounded-lg border border-border bg-popover shadow-xl outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2"
486
486
  style={surfaceStyle}
487
487
  >
488
488
  {submitted ? (
@@ -113,7 +113,8 @@ function UserAvatar({
113
113
  : `0 0 0 2px #fff`,
114
114
  }}
115
115
  aria-label={`${name} (${user.email})${isFollowing ? " — following" : ""}`}
116
- tabIndex={0}
116
+ tabIndex={onClick ? 0 : undefined}
117
+ role={onClick ? "button" : undefined}
117
118
  onClick={onClick}
118
119
  onKeyDown={(e) => {
119
120
  if (e.key === "Enter" || e.key === " ") onClick?.();
@@ -123,7 +124,11 @@ function UserAvatar({
123
124
  </div>
124
125
  </TooltipTrigger>
125
126
  <TooltipContent side="bottom">
126
- {isFollowing ? `Following ${name} — click to stop` : user.email}
127
+ {isFollowing
128
+ ? `Following ${name} — click to stop`
129
+ : onClick
130
+ ? `${name} — click to follow`
131
+ : user.email}
127
132
  </TooltipContent>
128
133
  </Tooltip>
129
134
  );
@@ -12,7 +12,13 @@ import {
12
12
  IconArrowLeft,
13
13
  IconX,
14
14
  } from "@tabler/icons-react";
15
- import React, { useState, useRef, useEffect, useMemo } from "react";
15
+ import React, {
16
+ useState,
17
+ useRef,
18
+ useEffect,
19
+ useMemo,
20
+ useCallback,
21
+ } from "react";
16
22
  import { createPortal } from "react-dom";
17
23
 
18
24
  import { setAgentChatContextItem } from "../agent-chat.js";
@@ -740,6 +746,16 @@ function AssetsPickerModal({
740
746
  const sourceUrl = useMemo(() => assetPickerUrl(), []);
741
747
  const iframeUrl = useMemo(() => withEmbeddedParams(sourceUrl), [sourceUrl]);
742
748
  const targetOrigin = useMemo(() => assetPickerOrigin(iframeUrl), [iframeUrl]);
749
+ const configurePicker = useCallback(() => {
750
+ if (!targetOrigin) return;
751
+ iframeRef.current?.contentWindow?.postMessage(
752
+ embedEnvelope("message", {
753
+ name: "configure",
754
+ payload: { mediaType: "image", count: 3 },
755
+ }),
756
+ targetOrigin,
757
+ );
758
+ }, [targetOrigin]);
743
759
 
744
760
  useEffect(() => {
745
761
  if (open) setPickerReady(false);
@@ -755,13 +771,7 @@ function AssetsPickerModal({
755
771
 
756
772
  if (event.data.type === "ready") {
757
773
  setPickerReady(true);
758
- iframeRef.current?.contentWindow?.postMessage(
759
- embedEnvelope("message", {
760
- name: "configure",
761
- payload: { mediaType: "image", count: 3 },
762
- }),
763
- targetOrigin,
764
- );
774
+ configurePicker();
765
775
  return;
766
776
  }
767
777
 
@@ -801,7 +811,7 @@ function AssetsPickerModal({
801
811
  window.removeEventListener("message", handleMessage);
802
812
  window.removeEventListener("keydown", handleKey);
803
813
  };
804
- }, [onOpenChange, open, targetOrigin]);
814
+ }, [configurePicker, onOpenChange, open, targetOrigin]);
805
815
 
806
816
  if (!open || typeof document === "undefined") return null;
807
817
 
@@ -846,6 +856,10 @@ function AssetsPickerModal({
846
856
  sandbox="allow-scripts allow-same-origin allow-forms allow-popups allow-downloads"
847
857
  allow="clipboard-read; clipboard-write; microphone; fullscreen"
848
858
  referrerPolicy="strict-origin-when-cross-origin"
859
+ onLoad={() => {
860
+ configurePicker();
861
+ setPickerReady(true);
862
+ }}
849
863
  />
850
864
  </div>
851
865
  ) : (
@@ -693,6 +693,10 @@ function SharePanel(
693
693
 
694
694
  const handleVisibility = (next: Visibility) => {
695
695
  if (next === visibility) return;
696
+ if (!canManage) {
697
+ setShareError("Only owners and admins can change access.");
698
+ return;
699
+ }
696
700
  setShareError(null);
697
701
  void onVisibilityChange(next).catch((err) => {
698
702
  setShareError(extractShareErrorMessage(err));
@@ -1025,6 +1029,15 @@ function SharePanel(
1025
1029
  </div>
1026
1030
  </div>
1027
1031
 
1032
+ {shareError && !canManage ? (
1033
+ <div
1034
+ role="alert"
1035
+ className="mb-4 rounded-md border border-destructive/40 bg-destructive/10 px-3 py-2 text-xs text-destructive"
1036
+ >
1037
+ {shareError}
1038
+ </div>
1039
+ ) : null}
1040
+
1028
1041
  {props.accessNote ? (
1029
1042
  <div className="mb-4 rounded-md border border-border bg-muted/35 p-3 text-xs text-muted-foreground">
1030
1043
  {props.accessNote}
@@ -1589,14 +1602,20 @@ function VisibilitySelect(props: {
1589
1602
  onChange: (v: Visibility) => void;
1590
1603
  disabled?: boolean;
1591
1604
  visibilityCopy?: ShareButtonProps["visibilityCopy"];
1605
+ /** When false, the "Private" option is omitted unless currently selected. */
1606
+ allowPrivate?: boolean;
1592
1607
  /** When false, the "Public" option is omitted. Default: true. */
1593
1608
  allowPublic?: boolean;
1594
1609
  }) {
1610
+ const allowPrivate = props.allowPrivate !== false;
1595
1611
  const allowPublic = props.allowPublic !== false;
1596
1612
  const current = visibilityMeta(props.value, props.visibilityCopy);
1597
- const options = (Object.keys(VIS_META) as Visibility[]).filter(
1598
- (k) => allowPublic || k !== "public",
1599
- );
1613
+ const options = (Object.keys(VIS_META) as Visibility[]).filter((k) => {
1614
+ if (k === props.value) return true;
1615
+ if (k === "private" && !allowPrivate) return false;
1616
+ if (k === "public" && !allowPublic) return false;
1617
+ return true;
1618
+ });
1600
1619
  return (
1601
1620
  <Select.Root
1602
1621
  value={props.value}
@@ -376,6 +376,7 @@ function LinkTab(props: {
376
376
 
377
377
  const handleVisibility = (next: Visibility) => {
378
378
  if (next === visibility) return;
379
+ if (!canManage) return;
379
380
  setVisibility.mutate(
380
381
  { resourceType, resourceId, visibility: next } as any,
381
382
  { onSuccess: () => sharesQuery.refetch() },
@@ -490,6 +491,7 @@ function InviteTab(props: {
490
491
 
491
492
  const handleVisibility = (next: Visibility) => {
492
493
  if (next === visibility) return;
494
+ if (!canManage) return;
493
495
  setVisibility.mutate(
494
496
  { resourceType, resourceId, visibility: next } as any,
495
497
  { onSuccess: () => sharesQuery.refetch() },
@@ -762,10 +764,20 @@ function VisibilitySelect(props: {
762
764
  value: Visibility;
763
765
  onChange: (v: Visibility) => void;
764
766
  disabled?: boolean;
767
+ allowPrivate?: boolean;
768
+ allowPublic?: boolean;
765
769
  }) {
766
770
  const t = useT();
767
771
  const visibilityMeta = useVisibilityMeta();
768
772
  const current = visibilityMeta[props.value];
773
+ const allowPrivate = props.allowPrivate !== false;
774
+ const allowPublic = props.allowPublic !== false;
775
+ const options = (Object.keys(VIS_ICONS) as Visibility[]).filter((k) => {
776
+ if (k === props.value) return true;
777
+ if (k === "private" && !allowPrivate) return false;
778
+ if (k === "public" && !allowPublic) return false;
779
+ return true;
780
+ });
769
781
  return (
770
782
  <Select.Root
771
783
  value={props.value}
@@ -792,7 +804,7 @@ function VisibilitySelect(props: {
792
804
  >
793
805
  <Select.Viewport>
794
806
  <SelectItems
795
- items={(Object.keys(VIS_ICONS) as Visibility[]).map((k) => ({
807
+ items={options.map((k) => ({
796
808
  value: k,
797
809
  label: visibilityMeta[k].label,
798
810
  description: visibilityMeta[k].description,
@@ -150,8 +150,8 @@ export const SSE_ACTION_PREPARATION_STALL_TIMEOUT_MS = 90_000;
150
150
 
151
151
  type ActivityTrailEntry = AgentActivityTrailEntry;
152
152
 
153
- type PreparingActionState = {
154
- tool?: string;
153
+ type PreparingActionEntry = {
154
+ tool: string;
155
155
  startedAt?: number;
156
156
  lastProgressBytes?: number;
157
157
  /**
@@ -166,6 +166,10 @@ type PreparingActionState = {
166
166
  lastProgressAt?: number;
167
167
  };
168
168
 
169
+ type PreparingActionState = {
170
+ entries?: Map<string, PreparingActionEntry>;
171
+ };
172
+
169
173
  function formatProgressBytes(bytes: number): string {
170
174
  if (bytes < 1024) return `${bytes} B`;
171
175
  if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
@@ -270,21 +274,35 @@ function updatePreparingActionState(
270
274
  if (ev.type === "activity" && isPreparingActionActivity(ev)) {
271
275
  const tool = ev.tool?.trim() || undefined;
272
276
  if (!tool) return false;
273
- if (state.tool !== tool || state.startedAt === undefined) {
274
- state.tool = tool;
275
- state.startedAt = now;
276
- state.lastProgressAt = undefined;
277
- state.lastProgressBytes = undefined;
277
+ const key = ev.id?.trim() || tool;
278
+ const entries = state.entries ?? new Map<string, PreparingActionEntry>();
279
+ state.entries = entries;
280
+ let entry = entries.get(key);
281
+ if (!entry) {
282
+ entry = {
283
+ tool,
284
+ startedAt: now,
285
+ lastProgressAt: undefined,
286
+ lastProgressBytes: undefined,
287
+ };
288
+ entries.set(key, entry);
278
289
  }
279
290
  const progressBytes = activityProgressBytes(ev);
280
- const previousBytes = state.lastProgressBytes ?? 0;
291
+ const previousBytes = entry.lastProgressBytes ?? 0;
281
292
  if (progressBytes !== undefined) {
282
- state.lastProgressBytes = Math.max(previousBytes, progressBytes);
293
+ entry.lastProgressBytes = Math.max(previousBytes, progressBytes);
294
+ }
295
+ if (!ev.id?.trim()) {
296
+ if (progressBytes !== undefined && progressBytes > 0) {
297
+ entry.lastProgressAt = now;
298
+ return true;
299
+ }
300
+ return false;
283
301
  }
284
302
  if (progressBytes !== undefined && progressBytes > previousBytes) {
285
303
  // A byte increase is proof the model is still streaming this action's
286
304
  // argument. Repeated zero-byte prep activity is only a heartbeat.
287
- state.lastProgressAt = now;
305
+ entry.lastProgressAt = now;
288
306
  return true;
289
307
  }
290
308
  return false;
@@ -299,10 +317,17 @@ function updatePreparingActionState(
299
317
  ev.type === "error" ||
300
318
  ev.type === "missing_api_key"
301
319
  ) {
302
- state.tool = undefined;
303
- state.startedAt = undefined;
304
- state.lastProgressAt = undefined;
305
- state.lastProgressBytes = undefined;
320
+ if (ev.type === "tool_start" || ev.type === "tool_done") {
321
+ const tool = ev.tool?.trim();
322
+ const id = ev.id?.trim();
323
+ for (const [key, entry] of state.entries ?? []) {
324
+ if ((id && key === id) || (!id && entry.tool === tool)) {
325
+ state.entries?.delete(key);
326
+ }
327
+ }
328
+ } else {
329
+ state.entries?.clear();
330
+ }
306
331
  }
307
332
  return undefined;
308
333
  }
@@ -313,12 +338,16 @@ function hasStalledPreparingAction(state: PreparingActionState, now: number) {
313
338
  // streaming for a long time. `lastProgressAt` advances on every delta
314
339
  // heartbeat, so an actively-streaming large output keeps resetting this and
315
340
  // survives; a genuinely stuck prep (keepalive-only, no deltas) trips it.
316
- return (
317
- state.tool !== undefined &&
318
- state.startedAt !== undefined &&
319
- now - (state.lastProgressAt ?? state.startedAt) >=
320
- SSE_ACTION_PREPARATION_STALL_TIMEOUT_MS
321
- );
341
+ for (const entry of state.entries?.values() ?? []) {
342
+ if (
343
+ entry.startedAt !== undefined &&
344
+ now - (entry.lastProgressAt ?? entry.startedAt) >=
345
+ SSE_ACTION_PREPARATION_STALL_TIMEOUT_MS
346
+ ) {
347
+ return true;
348
+ }
349
+ }
350
+ return false;
322
351
  }
323
352
 
324
353
  async function readChunkWithProgressTimeout(
@@ -16,9 +16,9 @@ import {
16
16
 
17
17
  export default defineAction({
18
18
  description:
19
- "Change the coarse visibility of a shareable resource: 'private' keeps it owner-only, 'org' shares it with all members of the owner's organization, 'public' makes it accessible to anyone with the link. Owner or admin role required.",
20
- // (audit H5) Visibility changes are admin-tier and can flip a private
21
- // resource org-wide or public. Refuse from the tools iframe bridge.
19
+ "Change the coarse visibility of a shareable resource: 'private' keeps it owner-only, 'org' shares it with all members of the owner's organization, 'public' makes it accessible to anyone with the link. Visibility changes require owner or admin role.",
20
+ // (audit H5) Visibility changes can flip a private resource org-wide or
21
+ // public. Refuse from the tools iframe bridge.
22
22
  toolCallable: false,
23
23
  mcpApp: {
24
24
  compactCatalog: true,
@@ -57,6 +57,14 @@ function escapeHtml(value: string): string {
57
57
  .replace(/"/g, "&quot;");
58
58
  }
59
59
 
60
+ function createInsertedNodeId(prefix: string): string {
61
+ const random =
62
+ typeof crypto !== "undefined" && typeof crypto.randomUUID === "function"
63
+ ? crypto.randomUUID().replace(/-/g, "").slice(0, 12)
64
+ : Math.random().toString(36).slice(2, 14);
65
+ return `inserted-${prefix}-${random}`;
66
+ }
67
+
60
68
  function insertBeforeClosingTag(
61
69
  html: string,
62
70
  closingTag: "main" | "body",
@@ -70,6 +78,7 @@ function insertBeforeClosingTag(
70
78
  function appendAssetMarkup(
71
79
  html: string,
72
80
  args: z.infer<typeof schemaInput>,
81
+ nodeId: string,
73
82
  ): string {
74
83
  const label = args.title?.trim() || args.altText?.trim() || "Generated asset";
75
84
  const escapedUrl = escapeHtml(args.assetUrl);
@@ -82,7 +91,7 @@ function appendAssetMarkup(
82
91
  ? `<video src="${escapedUrl}" controls class="w-full rounded-xl object-cover"></video>`
83
92
  : `<img src="${escapedUrl}" alt="${escapeHtml(args.altText?.trim() || label)}" class="w-full rounded-xl object-cover" />`;
84
93
  const snippet = `
85
- <section class="mx-auto my-8 max-w-5xl px-4" data-agent-native-asset${assetIdAttr}>
94
+ <section class="mx-auto my-8 max-w-5xl px-4" data-agent-native-asset data-agent-native-node-id="${escapeHtml(nodeId)}" data-agent-native-layer-name="${escapedLabel}"${assetIdAttr}>
86
95
  <figure class="overflow-hidden rounded-2xl border border-slate-200 bg-white shadow-sm">
87
96
  ${media}
88
97
  <figcaption class="px-4 py-3 text-sm text-slate-600">${escapedLabel}</figcaption>
@@ -191,7 +200,8 @@ export default defineAction({
191
200
  // Collab read is best-effort; fall back to stored content.
192
201
  }
193
202
 
194
- const content = appendAssetMarkup(base, args);
203
+ const insertedNodeId = createInsertedNodeId("asset");
204
+ const content = appendAssetMarkup(base, args, insertedNodeId);
195
205
  const now = new Date().toISOString();
196
206
  await db
197
207
  .update(schema.designFiles)
@@ -213,6 +223,8 @@ export default defineAction({
213
223
  fileId: file.id,
214
224
  filename: file.filename,
215
225
  inserted: true,
226
+ insertedNodeId,
227
+ insertedSelector: `[data-agent-native-node-id="${insertedNodeId}"]`,
216
228
  assetId: args.assetId ?? null,
217
229
  assetUrl: args.assetUrl,
218
230
  };
@@ -50,9 +50,17 @@ function insertBeforeClosingTag(
50
50
  return html.replace(pattern, `${snippet}\n</${closingTag}>`);
51
51
  }
52
52
 
53
- function nativeSnippet(kind: DesignNativeAssetKind): string {
53
+ function createInsertedNodeId(prefix: string): string {
54
+ const random =
55
+ typeof crypto !== "undefined" && typeof crypto.randomUUID === "function"
56
+ ? crypto.randomUUID().replace(/-/g, "").slice(0, 12)
57
+ : Math.random().toString(36).slice(2, 14);
58
+ return `inserted-${prefix}-${random}`;
59
+ }
60
+
61
+ function nativeSnippet(kind: DesignNativeAssetKind, nodeId: string): string {
54
62
  const attrs = (componentName: string) =>
55
- `data-agent-native-native-asset data-agent-native-component="${componentName}" data-agent-native-layer-name="${componentName}"`;
63
+ `data-agent-native-native-asset data-agent-native-node-id="${nodeId}" data-agent-native-component="${componentName}" data-agent-native-layer-name="${componentName}"`;
56
64
  switch (kind) {
57
65
  case "section-frame":
58
66
  return `
@@ -137,8 +145,9 @@ function nativeSnippet(kind: DesignNativeAssetKind): string {
137
145
  function appendNativeAssetMarkup(
138
146
  html: string,
139
147
  kind: DesignNativeAssetKind,
148
+ nodeId: string,
140
149
  ): string {
141
- const snippet = nativeSnippet(kind);
150
+ const snippet = nativeSnippet(kind, nodeId);
142
151
  return (
143
152
  insertBeforeClosingTag(html, "main", snippet) ??
144
153
  insertBeforeClosingTag(html, "body", snippet) ??
@@ -239,7 +248,8 @@ export default defineAction({
239
248
  // Collab read is best-effort; fall back to stored content.
240
249
  }
241
250
 
242
- const content = appendNativeAssetMarkup(base, args.kind);
251
+ const insertedNodeId = createInsertedNodeId("native");
252
+ const content = appendNativeAssetMarkup(base, args.kind, insertedNodeId);
243
253
  const now = new Date().toISOString();
244
254
  await db
245
255
  .update(schema.designFiles)
@@ -261,6 +271,8 @@ export default defineAction({
261
271
  fileId: file.id,
262
272
  filename: file.filename,
263
273
  inserted: true,
274
+ insertedNodeId,
275
+ insertedSelector: `[data-agent-native-node-id="${insertedNodeId}"]`,
264
276
  source: "design-native",
265
277
  kind: args.kind,
266
278
  };