@agent-native/core 0.84.25 → 0.84.28

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 +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/MultiTabAssistantChat.tsx +17 -2
  10. package/corpus/core/src/client/agent-chat.ts +118 -0
  11. package/corpus/core/src/client/components/PresenceBar.tsx +7 -2
  12. package/corpus/core/src/client/composer/ComposerPlusMenu.tsx +23 -9
  13. package/corpus/core/src/client/index.ts +4 -0
  14. package/corpus/core/src/client/sharing/ShareButton.tsx +22 -3
  15. package/corpus/core/src/client/sharing/ShareDialog.tsx +13 -1
  16. package/corpus/core/src/client/sse-event-processor.ts +49 -20
  17. package/corpus/core/src/sharing/actions/set-resource-visibility.ts +3 -3
  18. package/corpus/templates/design/actions/insert-asset.ts +14 -2
  19. package/corpus/templates/design/actions/insert-design-native-asset.ts +16 -4
  20. package/corpus/templates/design/actions/insert-figma-library-asset.ts +14 -2
  21. package/corpus/templates/design/app/components/design/DesignCanvas.tsx +14 -0
  22. package/corpus/templates/design/app/components/design/DesignExtensionsPanel.tsx +54 -10
  23. package/corpus/templates/design/app/components/design/EditPanel.tsx +23 -16
  24. package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +7 -2
  25. package/corpus/templates/design/app/components/design/inspector/InspectorAiActions.tsx +9 -5
  26. package/corpus/templates/design/app/components/layout/Header.tsx +38 -2
  27. package/corpus/templates/design/app/pages/DesignEditor.tsx +281 -105
  28. package/corpus/templates/design/app/pages/DesignSystemSetup.tsx +15 -1
  29. package/corpus/templates/design/app/pages/Index.tsx +20 -9
  30. package/corpus/templates/design/app/routes/settings.tsx +1 -1
  31. package/corpus/templates/design/changelog/2026-07-01-design-editor-polish-makes-zoom-assets-sharing-and-agent-handoffs-clearer.md +6 -0
  32. package/corpus/templates/design/changelog/2026-07-01-design-qa-fixes-make-exports-permissions-and-tools-clearer.md +6 -0
  33. package/corpus/templates/design/changelog/2026-07-01-zoom-menus-in-the-design-editor-now-open-independently-from-.md +6 -0
  34. package/dist/agent/production-agent.d.ts.map +1 -1
  35. package/dist/agent/production-agent.js +4 -3
  36. package/dist/agent/production-agent.js.map +1 -1
  37. package/dist/agent/run-manager.d.ts.map +1 -1
  38. package/dist/agent/run-manager.js +4 -4
  39. package/dist/agent/run-manager.js.map +1 -1
  40. package/dist/agent/types.d.ts +3 -0
  41. package/dist/agent/types.d.ts.map +1 -1
  42. package/dist/agent/types.js.map +1 -1
  43. package/dist/client/AssistantChat.d.ts.map +1 -1
  44. package/dist/client/AssistantChat.js +16 -1
  45. package/dist/client/AssistantChat.js.map +1 -1
  46. package/dist/client/FeedbackButton.js +2 -2
  47. package/dist/client/FeedbackButton.js.map +1 -1
  48. package/dist/client/MultiTabAssistantChat.d.ts.map +1 -1
  49. package/dist/client/MultiTabAssistantChat.js +14 -2
  50. package/dist/client/MultiTabAssistantChat.js.map +1 -1
  51. package/dist/client/agent-chat.d.ts +23 -0
  52. package/dist/client/agent-chat.d.ts.map +1 -1
  53. package/dist/client/agent-chat.js +79 -0
  54. package/dist/client/agent-chat.js.map +1 -1
  55. package/dist/client/components/PresenceBar.d.ts.map +1 -1
  56. package/dist/client/components/PresenceBar.js +6 -2
  57. package/dist/client/components/PresenceBar.js.map +1 -1
  58. package/dist/client/composer/ComposerPlusMenu.d.ts.map +1 -1
  59. package/dist/client/composer/ComposerPlusMenu.js +15 -7
  60. package/dist/client/composer/ComposerPlusMenu.js.map +1 -1
  61. package/dist/client/index.d.ts +1 -1
  62. package/dist/client/index.d.ts.map +1 -1
  63. package/dist/client/index.js +1 -1
  64. package/dist/client/index.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 +42 -16
  71. package/dist/client/sse-event-processor.js.map +1 -1
  72. package/dist/collab/awareness.d.ts +2 -2
  73. package/dist/collab/awareness.d.ts.map +1 -1
  74. package/dist/collab/routes.d.ts +1 -1
  75. package/dist/notifications/routes.d.ts +3 -3
  76. package/dist/observability/routes.d.ts +3 -3
  77. package/dist/progress/routes.d.ts +1 -1
  78. package/dist/resources/handlers.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: 4969
31
+ - template files: 4972
@@ -1,5 +1,23 @@
1
1
  # @agent-native/core
2
2
 
3
+ ## 0.84.28
4
+
5
+ ### Patch Changes
6
+
7
+ - ffd5b99: Make agent chat thread handoffs robust when opening the panel from external UI controls.
8
+
9
+ ## 0.84.27
10
+
11
+ ### Patch Changes
12
+
13
+ - 6fb954b: Require admin access for shareable resource visibility changes.
14
+
15
+ ## 0.84.26
16
+
17
+ ### Patch Changes
18
+
19
+ - f041345: Track action-preparation progress by streamed tool-input id so parallel same-name tool calls do not hide real progress.
20
+
3
21
  ## 0.84.25
4
22
 
5
23
  ### Patch Changes
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-native/core",
3
- "version": "0.84.25",
3
+ "version": "0.84.28",
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 ? (
@@ -26,7 +26,9 @@ import {
26
26
  AGENT_CHAT_REMOVE_CONTEXT_MESSAGE_TYPE,
27
27
  AGENT_CHAT_SET_CONTEXT_MESSAGE_TYPE,
28
28
  appendAgentChatContextToMessage,
29
+ claimAgentChatOpenRequest,
29
30
  claimAgentChatSubmit,
31
+ drainBufferedAgentChatOpenRequests,
30
32
  drainBufferedAgentChatSubmits,
31
33
  normalizeAgentChatContextItem,
32
34
  parseSubmitChatMessage,
@@ -2087,11 +2089,12 @@ export function MultiTabAssistantChat({
2087
2089
  useEffect(() => {
2088
2090
  const handleOpenThread = (event: Event) => {
2089
2091
  const detail = (event as CustomEvent).detail as
2090
- | { threadId?: unknown; newThread?: unknown }
2092
+ | { threadId?: unknown; newThread?: unknown; openRequestId?: unknown }
2091
2093
  | undefined;
2092
2094
  const threadId =
2093
2095
  typeof detail?.threadId === "string" ? detail.threadId : "";
2094
- if (!threadId) return;
2096
+ if (!detail || !threadId) return;
2097
+ if (!claimAgentChatOpenRequest(detail.openRequestId)) return;
2095
2098
 
2096
2099
  if (detail?.newThread === true) {
2097
2100
  newThreadIds.current.add(threadId);
@@ -2143,6 +2146,7 @@ export function MultiTabAssistantChat({
2143
2146
  const detail = (e as CustomEvent).detail;
2144
2147
  const threadId = detail?.threadId;
2145
2148
  if (!threadId) return;
2149
+ if (!claimAgentChatOpenRequest(detail.openRequestId)) return;
2146
2150
  dismissedSubAgentTabsRef.current.delete(threadId);
2147
2151
  // Prefer an explicit parent (RunsTray/background hydration knows it);
2148
2152
  // inline task cards fall back to the active orchestrator thread.
@@ -2196,6 +2200,17 @@ export function MultiTabAssistantChat({
2196
2200
  return () => window.removeEventListener("agent-task-open", handleOpenTask);
2197
2201
  }, [openTabIds, switchThread, refreshThreads, parentMap]);
2198
2202
 
2203
+ // Replay thread/task opens requested before this lazy panel's listeners
2204
+ // attached. Live events claim their id; replay drains only unclaimed requests.
2205
+ useEffect(() => {
2206
+ const buffered = drainBufferedAgentChatOpenRequests();
2207
+ for (const request of buffered) {
2208
+ window.dispatchEvent(
2209
+ new CustomEvent(request.eventType, { detail: request.detail }),
2210
+ );
2211
+ }
2212
+ }, []);
2213
+
2199
2214
  // Watch for agent-issued chat-command in application-state
2200
2215
  const lastChatCommandRef = useRef(0);
2201
2216
  useEffect(() => {
@@ -130,6 +130,26 @@ export interface AgentChatContextState {
130
130
  updatedAt: number;
131
131
  }
132
132
 
133
+ export interface AgentChatOpenThreadRequest {
134
+ threadId: string;
135
+ newThread?: boolean;
136
+ openRequestId?: string;
137
+ }
138
+
139
+ export interface AgentChatOpenTaskRequest {
140
+ threadId: string;
141
+ parentThreadId?: string;
142
+ description?: string;
143
+ name?: string;
144
+ openRequestId?: string;
145
+ }
146
+
147
+ export type BufferedAgentChatOpenRequest = {
148
+ id: string;
149
+ eventType: "agent-chat:open-thread" | "agent-task-open";
150
+ detail: AgentChatOpenThreadRequest | AgentChatOpenTaskRequest;
151
+ };
152
+
133
153
  export interface AgentComposerReference {
134
154
  label: string;
135
155
  icon?: string;
@@ -243,6 +263,14 @@ const SELF_SUBMIT_BUFFER_TTL_MS = 8000;
243
263
  const bufferedSelfSubmits: BufferedSelfSubmit[] = [];
244
264
  const claimedSubmitIds = new Set<string>();
245
265
 
266
+ interface BufferedOpenRequest extends BufferedAgentChatOpenRequest {
267
+ at: number;
268
+ }
269
+
270
+ const OPEN_REQUEST_BUFFER_TTL_MS = 8000;
271
+ const bufferedOpenRequests: BufferedOpenRequest[] = [];
272
+ const claimedOpenRequestIds = new Set<string>();
273
+
246
274
  function pruneSelfSubmitBuffer(now: number): void {
247
275
  for (let i = bufferedSelfSubmits.length - 1; i >= 0; i -= 1) {
248
276
  if (now - bufferedSelfSubmits[i].at > SELF_SUBMIT_BUFFER_TTL_MS) {
@@ -261,6 +289,32 @@ function bufferSelfSubmit(data: Record<string, unknown>): void {
261
289
  bufferedSelfSubmits.push({ id, data, at: now });
262
290
  }
263
291
 
292
+ function pruneOpenRequestBuffer(now: number): void {
293
+ for (let i = bufferedOpenRequests.length - 1; i >= 0; i -= 1) {
294
+ if (now - bufferedOpenRequests[i].at > OPEN_REQUEST_BUFFER_TTL_MS) {
295
+ const [removed] = bufferedOpenRequests.splice(i, 1);
296
+ if (removed) claimedOpenRequestIds.delete(removed.id);
297
+ }
298
+ }
299
+ }
300
+
301
+ function bufferOpenRequest(
302
+ eventType: BufferedAgentChatOpenRequest["eventType"],
303
+ detail: AgentChatOpenThreadRequest | AgentChatOpenTaskRequest,
304
+ ): BufferedOpenRequest {
305
+ const now = Date.now();
306
+ pruneOpenRequestBuffer(now);
307
+ const id = `open-${now}-${Math.random().toString(36).slice(2, 8)}`;
308
+ const entry: BufferedOpenRequest = {
309
+ id,
310
+ eventType,
311
+ detail: { ...detail, openRequestId: id },
312
+ at: now,
313
+ };
314
+ bufferedOpenRequests.push(entry);
315
+ return entry;
316
+ }
317
+
264
318
  /** Unclaimed self-submit payloads, for the panel to replay once it mounts. */
265
319
  export function drainBufferedAgentChatSubmits(): Array<
266
320
  Record<string, unknown>
@@ -279,10 +333,28 @@ export function claimAgentChatSubmit(id: string | undefined): boolean {
279
333
  return true;
280
334
  }
281
335
 
336
+ /** Unclaimed open-thread/task requests, for the panel to replay once it mounts. */
337
+ export function drainBufferedAgentChatOpenRequests(): BufferedAgentChatOpenRequest[] {
338
+ pruneOpenRequestBuffer(Date.now());
339
+ return bufferedOpenRequests
340
+ .filter((entry) => !claimedOpenRequestIds.has(entry.id))
341
+ .map(({ id, eventType, detail }) => ({ id, eventType, detail }));
342
+ }
343
+
344
+ /** Claim an open-thread/task request; false if already handled. Idless events pass. */
345
+ export function claimAgentChatOpenRequest(id: unknown): boolean {
346
+ if (typeof id !== "string" || !id) return true;
347
+ if (claimedOpenRequestIds.has(id)) return false;
348
+ claimedOpenRequestIds.add(id);
349
+ return true;
350
+ }
351
+
282
352
  /** Test-only: reset the self-submit buffer and claim set. */
283
353
  export function _resetAgentChatSubmitBufferForTests(): void {
284
354
  bufferedSelfSubmits.length = 0;
285
355
  claimedSubmitIds.clear();
356
+ bufferedOpenRequests.length = 0;
357
+ claimedOpenRequestIds.clear();
286
358
  }
287
359
 
288
360
  export function normalizeAgentChatContextItem(
@@ -638,6 +710,52 @@ function postAgentChatReferenceMessage(
638
710
  }
639
711
  }
640
712
 
713
+ function openAgentPanelForChat(): void {
714
+ if (typeof window === "undefined") return;
715
+ window.dispatchEvent(
716
+ new CustomEvent("agent-panel:set-mode", {
717
+ detail: { mode: "chat" },
718
+ }),
719
+ );
720
+ window.dispatchEvent(new CustomEvent("agent-panel:open"));
721
+ }
722
+
723
+ function dispatchBufferedOpenRequest(entry: BufferedOpenRequest): void {
724
+ if (typeof window === "undefined") return;
725
+ const dispatch = () => {
726
+ window.dispatchEvent(
727
+ new CustomEvent(entry.eventType, { detail: entry.detail }),
728
+ );
729
+ };
730
+ setTimeout(dispatch, 0);
731
+ }
732
+
733
+ export function requestAgentChatThreadOpen(
734
+ detail: AgentChatOpenThreadRequest,
735
+ ): void {
736
+ if (typeof window === "undefined" || !detail.threadId.trim()) return;
737
+ openAgentPanelForChat();
738
+ dispatchBufferedOpenRequest(
739
+ bufferOpenRequest("agent-chat:open-thread", {
740
+ ...detail,
741
+ threadId: detail.threadId.trim(),
742
+ }),
743
+ );
744
+ }
745
+
746
+ export function requestAgentTaskOpen(detail: AgentChatOpenTaskRequest): void {
747
+ if (typeof window === "undefined" || !detail.threadId.trim()) return;
748
+ openAgentPanelForChat();
749
+ const parentThreadId = detail.parentThreadId?.trim();
750
+ dispatchBufferedOpenRequest(
751
+ bufferOpenRequest("agent-task-open", {
752
+ ...detail,
753
+ threadId: detail.threadId.trim(),
754
+ ...(parentThreadId ? { parentThreadId } : {}),
755
+ }),
756
+ );
757
+ }
758
+
641
759
  function isMcpAppChatBridgeEnabled(): boolean {
642
760
  if (typeof window === "undefined" || window.parent === window) return false;
643
761
  if (readEmbedMcpChatBridgeFlagFromUrl()) markEmbedMcpChatBridgeActive();
@@ -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
  ) : (
@@ -16,12 +16,16 @@ export {
16
16
  normalizeAgentComposerReference,
17
17
  refreshAgentChatContext,
18
18
  removeAgentChatContextItem,
19
+ requestAgentChatThreadOpen,
20
+ requestAgentTaskOpen,
19
21
  sendToAgentChat,
20
22
  parseSubmitChatMessage,
21
23
  setAgentChatContextItem,
22
24
  setContextToAgentChat,
23
25
  generateTabId,
24
26
  type ParsedSubmitChat,
27
+ type AgentChatOpenTaskRequest,
28
+ type AgentChatOpenThreadRequest,
25
29
  type AgentChatContextItem,
26
30
  type AgentChatContextMessage,
27
31
  type AgentChatContextMutationOptions,
@@ -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,