@agent-native/core 0.84.24 → 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 (82) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +18 -0
  3. package/corpus/core/package.json +1 -1
  4. package/corpus/core/src/agent/production-agent.ts +12 -8
  5. package/corpus/core/src/agent/run-manager.ts +58 -6
  6. package/corpus/core/src/agent/types.ts +9 -2
  7. package/corpus/core/src/client/AssistantChat.tsx +51 -3
  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 +83 -26
  14. package/corpus/core/src/client/tool-display.ts +1 -1
  15. package/corpus/core/src/sharing/actions/set-resource-visibility.ts +3 -3
  16. package/corpus/templates/clips/.agents/skills/video-sharing/SKILL.md +4 -2
  17. package/corpus/templates/clips/app/components/player/share-dialog.tsx +9 -0
  18. package/corpus/templates/clips/app/i18n/ar-SA.ts +3 -0
  19. package/corpus/templates/clips/app/i18n/de-DE.ts +3 -0
  20. package/corpus/templates/clips/app/i18n/en-US.ts +3 -0
  21. package/corpus/templates/clips/app/i18n/es-ES.ts +3 -0
  22. package/corpus/templates/clips/app/i18n/fr-FR.ts +3 -0
  23. package/corpus/templates/clips/app/i18n/hi-IN.ts +3 -0
  24. package/corpus/templates/clips/app/i18n/ja-JP.ts +3 -0
  25. package/corpus/templates/clips/app/i18n/ko-KR.ts +3 -0
  26. package/corpus/templates/clips/app/i18n/pt-BR.ts +3 -0
  27. package/corpus/templates/clips/app/i18n/zh-CN.ts +3 -0
  28. package/corpus/templates/clips/app/i18n/zh-TW.ts +3 -0
  29. package/corpus/templates/clips/changelog/2026-07-01-shared-clips-can-now-copy-a-ready-to-send-prompt-that-tells-.md +6 -0
  30. package/corpus/templates/design/actions/insert-asset.ts +14 -2
  31. package/corpus/templates/design/actions/insert-design-native-asset.ts +16 -4
  32. package/corpus/templates/design/actions/insert-figma-library-asset.ts +14 -2
  33. package/corpus/templates/design/app/components/design/DesignCanvas.tsx +14 -0
  34. package/corpus/templates/design/app/components/design/DesignExtensionsPanel.tsx +54 -10
  35. package/corpus/templates/design/app/components/design/EditPanel.tsx +23 -16
  36. package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +7 -2
  37. package/corpus/templates/design/app/components/design/inspector/InspectorAiActions.tsx +9 -5
  38. package/corpus/templates/design/app/components/layout/Header.tsx +44 -1
  39. package/corpus/templates/design/app/pages/DesignEditor.tsx +267 -105
  40. package/corpus/templates/design/app/pages/DesignSystemSetup.tsx +15 -1
  41. package/corpus/templates/design/app/pages/Index.tsx +20 -9
  42. package/corpus/templates/design/app/routes/settings.tsx +1 -1
  43. package/corpus/templates/design/changelog/2026-07-01-design-editor-polish-makes-zoom-assets-sharing-and-agent-handoffs-clearer.md +6 -0
  44. package/corpus/templates/design/changelog/2026-07-01-design-qa-fixes-make-exports-permissions-and-tools-clearer.md +6 -0
  45. package/dist/agent/production-agent.d.ts.map +1 -1
  46. package/dist/agent/production-agent.js +12 -9
  47. package/dist/agent/production-agent.js.map +1 -1
  48. package/dist/agent/run-manager.d.ts.map +1 -1
  49. package/dist/agent/run-manager.js +56 -4
  50. package/dist/agent/run-manager.js.map +1 -1
  51. package/dist/agent/types.d.ts +3 -0
  52. package/dist/agent/types.d.ts.map +1 -1
  53. package/dist/agent/types.js.map +1 -1
  54. package/dist/client/AssistantChat.d.ts.map +1 -1
  55. package/dist/client/AssistantChat.js +52 -3
  56. package/dist/client/AssistantChat.js.map +1 -1
  57. package/dist/client/FeedbackButton.js +2 -2
  58. package/dist/client/FeedbackButton.js.map +1 -1
  59. package/dist/client/components/PresenceBar.d.ts.map +1 -1
  60. package/dist/client/components/PresenceBar.js +6 -2
  61. package/dist/client/components/PresenceBar.js.map +1 -1
  62. package/dist/client/composer/ComposerPlusMenu.d.ts.map +1 -1
  63. package/dist/client/composer/ComposerPlusMenu.js +15 -7
  64. package/dist/client/composer/ComposerPlusMenu.js.map +1 -1
  65. package/dist/client/sharing/ShareButton.js +15 -2
  66. package/dist/client/sharing/ShareButton.js.map +1 -1
  67. package/dist/client/sharing/ShareDialog.js +16 -1
  68. package/dist/client/sharing/ShareDialog.js.map +1 -1
  69. package/dist/client/sse-event-processor.d.ts.map +1 -1
  70. package/dist/client/sse-event-processor.js +64 -21
  71. package/dist/client/sse-event-processor.js.map +1 -1
  72. package/dist/client/tool-display.js +1 -1
  73. package/dist/client/tool-display.js.map +1 -1
  74. package/dist/collab/awareness.d.ts +2 -2
  75. package/dist/collab/awareness.d.ts.map +1 -1
  76. package/dist/notifications/routes.d.ts +3 -3
  77. package/dist/observability/routes.d.ts +5 -5
  78. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  79. package/dist/server/transcribe-voice.d.ts +1 -1
  80. package/dist/sharing/actions/set-resource-visibility.js +3 -3
  81. package/dist/sharing/actions/set-resource-visibility.js.map +1 -1
  82. 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: 4968
31
+ - template files: 4971
@@ -1,5 +1,23 @@
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
+
15
+ ## 0.84.25
16
+
17
+ ### Patch Changes
18
+
19
+ - a11293f: Keep tail-resume reconnect content display-only, recover zero-byte action-prep stalls, and polish Design screen tool labels.
20
+
3
21
  ## 0.84.24
4
22
 
5
23
  ### Patch Changes
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-native/core",
3
- "version": "0.84.24",
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
  };
@@ -2843,24 +2845,26 @@ export async function runAgentLoop(opts: {
2843
2845
  // reasons, then collapse it when content arrives.
2844
2846
  send({ type: "thinking", text: event.text });
2845
2847
  } else if (event.type === "tool-input-start") {
2846
- if (event.id && event.name) {
2847
- toolInputNames.set(event.id, event.name);
2848
- toolInputBytes.set(event.id, 0);
2848
+ const key = event.id ?? event.name;
2849
+ if (key && event.name) {
2850
+ toolInputNames.set(key, event.name);
2851
+ toolInputBytes.set(key, 0);
2849
2852
  }
2850
- sendToolInputActivity(event.name, undefined, true);
2853
+ sendToolInputActivity(event.name, key, undefined, true);
2851
2854
  } else if (event.type === "tool-input-delta") {
2855
+ const key = event.id ?? event.name;
2852
2856
  const toolName =
2853
2857
  event.name ??
2854
2858
  (event.id ? toolInputNames.get(event.id) : undefined);
2855
2859
  let progressBytes: number | undefined;
2856
- if (event.id) {
2857
- const previous = toolInputBytes.get(event.id) ?? 0;
2860
+ if (key) {
2861
+ const previous = toolInputBytes.get(key) ?? 0;
2858
2862
  progressBytes =
2859
2863
  previous +
2860
2864
  new TextEncoder().encode(event.text ?? "").byteLength;
2861
- toolInputBytes.set(event.id, progressBytes);
2865
+ toolInputBytes.set(key, progressBytes);
2862
2866
  }
2863
- sendToolInputActivity(toolName, progressBytes);
2867
+ sendToolInputActivity(toolName, key, progressBytes);
2864
2868
  } else if (event.type === "gateway-heartbeat") {
2865
2869
  send({ type: "stream_keepalive" });
2866
2870
  } else if (event.type === "tool-call") {
@@ -16,6 +16,7 @@ import {
16
16
  bumpRunProgress,
17
17
  reapIfStale,
18
18
  reapUnclaimedBackgroundRun,
19
+ reconcileTerminalRunFromEvents,
19
20
  ensureTerminalRunEvent,
20
21
  setRunError,
21
22
  setRunTerminalReason,
@@ -125,6 +126,12 @@ export const TERMINAL_RUN_RECONNECT_WINDOW_MS = 10 * 60 * 1000;
125
126
 
126
127
  const PROVIDER_RATE_LIMITED_ERROR_CODE = "provider_rate_limited";
127
128
 
129
+ function isPreparingActionActivityEvent(event: AgentChatEvent): boolean {
130
+ if (event.type !== "activity") return false;
131
+ const label = event.label.trim().toLowerCase();
132
+ return label.startsWith("preparing ") && label.includes(" action");
133
+ }
134
+
128
135
  function getRunErrorMessage(err: unknown): string {
129
136
  if (
130
137
  typeof err === "object" &&
@@ -423,6 +430,7 @@ export function startRun(
423
430
  // chunk. The stuck-detector threshold is on the order of tens of seconds,
424
431
  // so 1s resolution is plenty.
425
432
  let lastProgressBumpAt = 0;
433
+ const preparingActivityBytes = new Map<string, number>();
426
434
  let eventPersistenceErrorCaptured = false;
427
435
  const bumpProgressIfDue = () => {
428
436
  const now = Date.now();
@@ -430,6 +438,42 @@ export function startRun(
430
438
  lastProgressBumpAt = now;
431
439
  bumpRunProgress(runId).catch(() => {});
432
440
  };
441
+ const shouldBumpProgressForEvent = (event: AgentChatEvent): boolean => {
442
+ if (event.type === "stream_keepalive") return false;
443
+ if (event.type === "activity" && isPreparingActionActivityEvent(event)) {
444
+ const toolKey =
445
+ event.id?.trim() || event.tool?.trim() || event.label.trim();
446
+ const progressBytes =
447
+ typeof event.progressBytes === "number" &&
448
+ Number.isFinite(event.progressBytes) &&
449
+ event.progressBytes >= 0
450
+ ? Math.floor(event.progressBytes)
451
+ : undefined;
452
+ if (progressBytes === undefined) return false;
453
+ if (!event.id?.trim()) return progressBytes > 0;
454
+ const previousBytes = preparingActivityBytes.get(toolKey) ?? 0;
455
+ if (progressBytes <= previousBytes) {
456
+ preparingActivityBytes.set(
457
+ toolKey,
458
+ Math.max(previousBytes, progressBytes),
459
+ );
460
+ return false;
461
+ }
462
+ preparingActivityBytes.set(toolKey, progressBytes);
463
+ return true;
464
+ }
465
+ if (event.type === "tool_start" || event.type === "tool_done") {
466
+ preparingActivityBytes.clear();
467
+ }
468
+ if (
469
+ event.type === "clear" ||
470
+ event.type === "done" ||
471
+ event.type === "error"
472
+ ) {
473
+ preparingActivityBytes.clear();
474
+ }
475
+ return true;
476
+ };
433
477
 
434
478
  // Periodic SQL abort check interval (for cross-isolate abort on Workers).
435
479
  // Also self-aborts when our row is no longer status='running' — catches the
@@ -539,8 +583,12 @@ export function startRun(
539
583
  // Bump the durable progress timestamp. Distinct from the heartbeat:
540
584
  // heartbeat = "process is up", progress = "real work is happening." The
541
585
  // gap between them is what the client-side stuck-detector reads to tell
542
- // a hung run from a healthy one.
543
- bumpProgressIfDue();
586
+ // a hung run from a healthy one. Keepalive and zero-byte action prep are
587
+ // liveness only; streamed input bytes, text, and tool lifecycle events are
588
+ // real progress.
589
+ if (shouldBumpProgressForEvent(runEvent.event)) {
590
+ bumpProgressIfDue();
591
+ }
544
592
 
545
593
  // Persist event to SQL. Events are chained through persistenceChain so
546
594
  // inserts commit in seq order — an out-of-order commit would advance the
@@ -733,12 +781,16 @@ export function startRun(
733
781
  try {
734
782
  await insertRunPromise;
735
783
  if (!terminalPersistenceError) {
736
- const statusUpdated = await updateRunStatusIfRunning(
737
- runId,
738
- finalStatus,
739
- );
784
+ let statusUpdated = false;
785
+ try {
786
+ statusUpdated = await updateRunStatusIfRunning(runId, finalStatus);
787
+ } catch {
788
+ statusUpdated = false;
789
+ }
740
790
  if (statusUpdated) {
741
791
  await setRunTerminalReason(runId, terminalReason);
792
+ } else {
793
+ await reconcileTerminalRunFromEvents(runId).catch(() => false);
742
794
  }
743
795
  }
744
796
  } catch {
@@ -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,8 @@ 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);
1460
+ const reconnectCanMaterializeRef = useRef(false);
1459
1461
  const reconnectAbortRef = useRef<AbortController | null>(null);
1460
1462
  // Nuclear stop: user clicked stop. Clears the stop button/indicator AND
1461
1463
  // lets new submissions go through immediately — prevents the "stuck
@@ -1774,6 +1776,8 @@ const AssistantChatInner = forwardRef<
1774
1776
 
1775
1777
  reconnectRunIdRef.current = runId;
1776
1778
  const afterSeq = resolveReconnectAfterSeq(threadId, runId);
1779
+ reconnectTailOnlyRef.current = afterSeq > 0;
1780
+ reconnectCanMaterializeRef.current = afterSeq === 0;
1777
1781
  const storedActivityTool = getActiveRunActivityTool(threadId, runId);
1778
1782
  setRunningActivityTool(storedActivityTool);
1779
1783
  setActiveRun({
@@ -1939,10 +1943,12 @@ const AssistantChatInner = forwardRef<
1939
1943
  await refreshThreadFromServer();
1940
1944
  setReconnectContent([]);
1941
1945
  setReconnectFrozen(false);
1946
+ reconnectTailOnlyRef.current = false;
1942
1947
  } else {
1943
1948
  settleInterruptedToolCalls(latestContent);
1944
1949
  setReconnectContent([...latestContent]);
1945
1950
  setReconnectFrozen(latestContent.length > 0);
1951
+ reconnectCanMaterializeRef.current = latestContent.length > 0;
1946
1952
  }
1947
1953
  setRunErrorInfo({
1948
1954
  message:
@@ -1956,6 +1962,10 @@ const AssistantChatInner = forwardRef<
1956
1962
  reconnectAbortRef.current = null;
1957
1963
  setIsReconnecting(false);
1958
1964
  reconnectRunIdRef.current = null;
1965
+ reconnectTailOnlyRef.current = false;
1966
+ if (afterSeq > 0) {
1967
+ reconnectCanMaterializeRef.current = false;
1968
+ }
1959
1969
  window.dispatchEvent(
1960
1970
  new CustomEvent("agentNative.chatRunning", {
1961
1971
  detail: { isRunning: false, tabId: tabId || threadId },
@@ -1964,7 +1974,7 @@ const AssistantChatInner = forwardRef<
1964
1974
  return;
1965
1975
  }
1966
1976
 
1967
- setReconnectFrozen(true);
1977
+ setReconnectFrozen(afterSeq === 0);
1968
1978
  let loaded = false;
1969
1979
  for (let attempt = 0; attempt < 10; attempt++) {
1970
1980
  await new Promise((r) => setTimeout(r, 500));
@@ -1973,16 +1983,25 @@ const AssistantChatInner = forwardRef<
1973
1983
  if (repoHasAssistantMessage(repo)) {
1974
1984
  setReconnectContent([]);
1975
1985
  setReconnectFrozen(false);
1986
+ reconnectCanMaterializeRef.current = false;
1976
1987
  loaded = true;
1977
1988
  break;
1978
1989
  }
1979
1990
  }
1980
1991
 
1981
1992
  if (reconnectRunIdRef.current === runId) {
1993
+ if (afterSeq > 0) {
1994
+ setReconnectContent([]);
1995
+ setReconnectFrozen(false);
1996
+ }
1982
1997
  clearActiveRunIfMatches(threadId, runId);
1983
1998
  reconnectAbortRef.current = null;
1984
1999
  setIsReconnecting(false);
1985
2000
  reconnectRunIdRef.current = null;
2001
+ reconnectTailOnlyRef.current = false;
2002
+ if (loaded || afterSeq > 0 || latestContent.length === 0) {
2003
+ reconnectCanMaterializeRef.current = false;
2004
+ }
1986
2005
  window.dispatchEvent(
1987
2006
  new CustomEvent("agentNative.chatRunning", {
1988
2007
  detail: { isRunning: false, tabId: tabId || threadId },
@@ -1990,7 +2009,12 @@ const AssistantChatInner = forwardRef<
1990
2009
  );
1991
2010
  }
1992
2011
  if (!loaded) {
1993
- await refreshThreadFromServer();
2012
+ const repo = await refreshThreadFromServer();
2013
+ if (afterSeq > 0 || repoHasAssistantMessage(repo)) {
2014
+ setReconnectContent([]);
2015
+ setReconnectFrozen(false);
2016
+ reconnectCanMaterializeRef.current = false;
2017
+ }
1994
2018
  }
1995
2019
  };
1996
2020
 
@@ -2004,6 +2028,7 @@ const AssistantChatInner = forwardRef<
2004
2028
  useCallback(async (): Promise<boolean> => {
2005
2029
  if (!threadId) return false;
2006
2030
  try {
2031
+ const storedActiveRun = getActiveRun();
2007
2032
  const runRes = await fetch(
2008
2033
  `${apiUrl}/runs/active?threadId=${encodeURIComponent(threadId)}`,
2009
2034
  );
@@ -2014,6 +2039,11 @@ const AssistantChatInner = forwardRef<
2014
2039
  runInfo.status !== "running" ||
2015
2040
  activeRunLooksStale(runInfo)
2016
2041
  ) {
2042
+ if (storedActiveRun?.threadId === threadId) {
2043
+ clearActiveRunIfMatches(threadId, storedActiveRun.runId);
2044
+ } else if (runInfo.runId) {
2045
+ clearActiveRunIfMatches(threadId, String(runInfo.runId));
2046
+ }
2017
2047
  await refreshThreadFromServer();
2018
2048
  return false;
2019
2049
  }
@@ -2684,6 +2714,7 @@ const AssistantChatInner = forwardRef<
2684
2714
  if (reconnectFrozen) {
2685
2715
  setReconnectFrozen(false);
2686
2716
  setReconnectContent([]);
2717
+ reconnectCanMaterializeRef.current = false;
2687
2718
  }
2688
2719
  if (forceStopped) {
2689
2720
  setForceStopped(false);
@@ -2704,6 +2735,11 @@ const AssistantChatInner = forwardRef<
2704
2735
 
2705
2736
  const materializeFrozenReconnectContent = useCallback(() => {
2706
2737
  if (!reconnectFrozen || reconnectContent.length === 0) return;
2738
+ if (!reconnectCanMaterializeRef.current) {
2739
+ setReconnectFrozen(false);
2740
+ setReconnectContent([]);
2741
+ return;
2742
+ }
2707
2743
  try {
2708
2744
  const frozenContent = cloneContentParts(reconnectContent);
2709
2745
  settleInterruptedToolCalls(frozenContent);
@@ -2744,6 +2780,7 @@ const AssistantChatInner = forwardRef<
2744
2780
  threadRuntime.import(ensureMessageMetadata(repo));
2745
2781
  setReconnectFrozen(false);
2746
2782
  setReconnectContent([]);
2783
+ reconnectCanMaterializeRef.current = false;
2747
2784
  } catch (err) {
2748
2785
  captureError(err, {
2749
2786
  tags: {
@@ -2792,7 +2829,18 @@ const AssistantChatInner = forwardRef<
2792
2829
  reconnectAbortRef.current = null;
2793
2830
  reconnectRunIdRef.current = null;
2794
2831
  setIsReconnecting(false);
2795
- setReconnectFrozen(reconnectContent.length > 0);
2832
+ const shouldFreezeReconnectContent =
2833
+ !reconnectTailOnlyRef.current &&
2834
+ reconnectCanMaterializeRef.current &&
2835
+ reconnectContent.length > 0;
2836
+ if (shouldFreezeReconnectContent) {
2837
+ setReconnectFrozen(true);
2838
+ } else {
2839
+ setReconnectFrozen(false);
2840
+ setReconnectContent([]);
2841
+ reconnectCanMaterializeRef.current = false;
2842
+ }
2843
+ reconnectTailOnlyRef.current = false;
2796
2844
  }
2797
2845
  threadRuntime.cancelRun();
2798
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,