@agent-native/core 0.84.2 → 0.84.3

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 (41) 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/agent/production-agent.ts +9 -2
  5. package/corpus/core/src/agent/run-loop-with-resume.ts +38 -8
  6. package/corpus/core/src/agent/thread-data-builder.ts +6 -0
  7. package/corpus/core/src/client/agent-chat-adapter.ts +208 -9
  8. package/corpus/core/src/client/sharing/ShareButton.tsx +70 -21
  9. package/corpus/core/src/client/sse-event-processor.ts +15 -0
  10. package/corpus/templates/analytics/AGENTS.md +1 -1
  11. package/corpus/templates/design/app/pages/DesignEditor.tsx +129 -100
  12. package/corpus/templates/design/changelog/2026-06-30-agent-chat-now-reports-saved-design-generations-clearly-when.md +6 -0
  13. package/corpus/templates/design/changelog/2026-06-30-apply-styles-now-appears-only-for-localhost-visual-edit-scre.md +6 -0
  14. package/corpus/templates/design/changelog/2026-06-30-share-options-now-make-export-and-agent-handoff-easier-to-no.md +6 -0
  15. package/corpus/templates/design/changelog/2026-07-01-share-general-access-menu-stays-open-when-choosing-organization.md +6 -0
  16. package/dist/agent/production-agent.d.ts.map +1 -1
  17. package/dist/agent/production-agent.js +9 -2
  18. package/dist/agent/production-agent.js.map +1 -1
  19. package/dist/agent/run-loop-with-resume.d.ts +2 -2
  20. package/dist/agent/run-loop-with-resume.d.ts.map +1 -1
  21. package/dist/agent/run-loop-with-resume.js +31 -8
  22. package/dist/agent/run-loop-with-resume.js.map +1 -1
  23. package/dist/agent/thread-data-builder.d.ts +2 -0
  24. package/dist/agent/thread-data-builder.d.ts.map +1 -1
  25. package/dist/agent/thread-data-builder.js +5 -0
  26. package/dist/agent/thread-data-builder.js.map +1 -1
  27. package/dist/client/agent-chat-adapter.d.ts.map +1 -1
  28. package/dist/client/agent-chat-adapter.js +179 -8
  29. package/dist/client/agent-chat-adapter.js.map +1 -1
  30. package/dist/client/sharing/ShareButton.d.ts +6 -0
  31. package/dist/client/sharing/ShareButton.d.ts.map +1 -1
  32. package/dist/client/sharing/ShareButton.js +25 -11
  33. package/dist/client/sharing/ShareButton.js.map +1 -1
  34. package/dist/client/sse-event-processor.d.ts +4 -0
  35. package/dist/client/sse-event-processor.d.ts.map +1 -1
  36. package/dist/client/sse-event-processor.js +13 -0
  37. package/dist/client/sse-event-processor.js.map +1 -1
  38. package/dist/notifications/routes.d.ts +1 -1
  39. package/dist/observability/routes.d.ts +5 -5
  40. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  41. 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: 4902
31
+ - template files: 4906
@@ -1,5 +1,13 @@
1
1
  # @agent-native/core
2
2
 
3
+ ## 0.84.3
4
+
5
+ ### Patch Changes
6
+
7
+ - af049a8: Improve agent-chat timeout recovery so completed tool actions end with a clear saved-result note instead of a generic connection failure, and bound repeated no-progress tool stalls.
8
+ - af049a8: Allow share panels to hide copyable link fields, omit the bottom Done action, and render compact host-provided footer actions.
9
+ - af049a8: Stack nested share popover menus above their parent panel so users can change visibility.
10
+
3
11
  ## 0.84.2
4
12
 
5
13
  ### Patch Changes
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-native/core",
3
- "version": "0.84.2",
3
+ "version": "0.84.3",
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": {
@@ -3330,7 +3330,10 @@ export async function runAgentLoop(opts: {
3330
3330
  }
3331
3331
 
3332
3332
  const DEFAULT_TOOL_RESULT_CHARS = 50_000;
3333
- const DEFAULT_TOOL_TIMEOUT_MS = 60_000;
3333
+ // Default action tools should not undercut durable/background runs. The
3334
+ // run-manager still aborts foreground hosted runs around 40s, while
3335
+ // background runs get nearly the full 15-minute function budget.
3336
+ const DEFAULT_TOOL_TIMEOUT_MS = 12 * 60_000;
3334
3337
  const toolTimeoutMs =
3335
3338
  actionEntry.timeoutMs ??
3336
3339
  opts.toolLimits?.timeoutMs ??
@@ -3798,7 +3801,11 @@ export async function runAgentLoop(opts: {
3798
3801
  input: toolCall.input as Record<string, unknown>,
3799
3802
  result,
3800
3803
  ...(isError ? { isError: true } : {}),
3801
- ...(isError ? { completedSideEffect: false } : {}),
3804
+ ...(isError
3805
+ ? { completedSideEffect: false }
3806
+ : actionEntry.readOnly !== true
3807
+ ? { completedSideEffect: true }
3808
+ : {}),
3802
3809
  ...(mcpApp ? { mcpApp } : {}),
3803
3810
  ...(actionEntry.chatUI ? { chatUI: actionEntry.chatUI } : {}),
3804
3811
  });
@@ -79,6 +79,24 @@ async function appendToolCallJournalNote(
79
79
  }
80
80
  }
81
81
 
82
+ async function hasCompletedSideEffectToolCallInCurrentTurn(
83
+ threadId: string | undefined,
84
+ ): Promise<boolean> {
85
+ if (!threadId) return false;
86
+ try {
87
+ const events = await getCurrentTurnEventsForThread(threadId);
88
+ if (events.length === 0) return false;
89
+ return events.some(
90
+ (event) =>
91
+ event.type === "tool_done" &&
92
+ event.completedSideEffect === true &&
93
+ event.isError !== true,
94
+ );
95
+ } catch {
96
+ return false;
97
+ }
98
+ }
99
+
82
100
  /**
83
101
  * Cap on continuation iterations inside a single
84
102
  * `runAgentLoopDirectWithSoftTimeout` invocation. The host's hard function
@@ -102,12 +120,12 @@ export const RUN_BUDGET_EXHAUSTED_ERROR_CODE = "run_budget_exhausted";
102
120
  * exhausts its in-invocation continuation budget without finishing. Generic and
103
121
  * framework-level (not app-specific). Mirrors the `reliable-mutations` skill's
104
122
  * "fail loud, retry as a single bulk action" guidance so the user understands
105
- * the turn stopped before finishing and was not partially saved by the run
106
- * itself. */
123
+ * the turn stopped before finishing without implying that earlier completed
124
+ * tool calls did not persist. */
107
125
  export const RUN_BUDGET_EXHAUSTED_MESSAGE =
108
- "I ran out of time before finishing this step (hosted runs have a ~40s budget). " +
109
- "I stopped rather than leave things half-done — nothing was partially saved by me here. " +
110
- "Please retry, ideally as a single bulk action.";
126
+ "I ran out of time before finishing this step. " +
127
+ "I stopped rather than keep retrying silently. " +
128
+ "Check any completed tool cards above before retrying, ideally as one smaller follow-up.";
111
129
 
112
130
  /**
113
131
  * Internal entry point used by the agent-chat plugin's run handler. Wraps
@@ -189,7 +207,11 @@ export async function runAgentLoopDirectWithSoftTimeout(
189
207
  // Clear partial text the client received before the abort so the
190
208
  // resumed model doesn't re-emit it and produce duplicated output.
191
209
  lastAttemptWasUnfinishedContinuation = true;
192
- opts.send({ type: "clear" });
210
+ if (
211
+ !(await hasCompletedSideEffectToolCallInCurrentTurn(opts.threadId))
212
+ ) {
213
+ opts.send({ type: "clear" });
214
+ }
193
215
  appendAgentLoopContinuation(opts.messages, "run_timeout");
194
216
  await appendToolCallJournalNote(opts.messages, opts.threadId);
195
217
  continue;
@@ -209,7 +231,11 @@ export async function runAgentLoopDirectWithSoftTimeout(
209
231
  // the in-memory messages array, so the next attempt re-emits it).
210
232
  if (!upstreamSignal.aborted && isResumableEngineError(err)) {
211
233
  lastAttemptWasUnfinishedContinuation = true;
212
- opts.send({ type: "clear" });
234
+ if (
235
+ !(await hasCompletedSideEffectToolCallInCurrentTurn(opts.threadId))
236
+ ) {
237
+ opts.send({ type: "clear" });
238
+ }
213
239
  appendAgentLoopContinuation(
214
240
  opts.messages,
215
241
  continuationReasonForResumableError(err),
@@ -234,7 +260,11 @@ export async function runAgentLoopDirectWithSoftTimeout(
234
260
  if (!upstreamSignal.aborted && lastAttemptWasUnfinishedContinuation) {
235
261
  // Discard any partial text already streamed for the unfinished attempt so
236
262
  // the terminal message stands alone instead of trailing a half sentence.
237
- opts.send({ type: "clear" });
263
+ // Preserve completed tool cards: they are the user's only durable proof
264
+ // that a side effect landed before the final assistant note timed out.
265
+ if (!(await hasCompletedSideEffectToolCallInCurrentTurn(opts.threadId))) {
266
+ opts.send({ type: "clear" });
267
+ }
238
268
  opts.send({
239
269
  type: "error",
240
270
  error: RUN_BUDGET_EXHAUSTED_MESSAGE,
@@ -18,6 +18,8 @@ interface ContentPart {
18
18
  argsText?: string;
19
19
  args?: Record<string, string>;
20
20
  result?: string;
21
+ isError?: boolean;
22
+ completedSideEffect?: boolean;
21
23
  mcpApp?: AgentMcpAppPayload;
22
24
  chatUI?: ActionChatUIConfig;
23
25
  }
@@ -159,6 +161,10 @@ export function buildAssistantMessage(
159
161
  part.result === undefined
160
162
  ) {
161
163
  part.result = event.result ?? "";
164
+ if (event.isError !== undefined) part.isError = event.isError;
165
+ if (event.completedSideEffect !== undefined) {
166
+ part.completedSideEffect = event.completedSideEffect;
167
+ }
162
168
  if (event.mcpApp) part.mcpApp = event.mcpApp;
163
169
  if (event.chatUI) part.chatUI = event.chatUI;
164
170
  break;
@@ -697,6 +697,41 @@ function hasContinuationProgress(content: ContentPart[]): boolean {
697
697
  );
698
698
  }
699
699
 
700
+ function lastCompletedSideEffectTool(
701
+ content: ContentPart[],
702
+ ): Extract<ContentPart, { type: "tool-call" }> | undefined {
703
+ for (let i = content.length - 1; i >= 0; i--) {
704
+ const part = content[i];
705
+ if (
706
+ part.type === "tool-call" &&
707
+ part.activity !== true &&
708
+ part.result !== undefined &&
709
+ part.isError !== true &&
710
+ part.completedSideEffect === true
711
+ ) {
712
+ return part;
713
+ }
714
+ }
715
+ return undefined;
716
+ }
717
+
718
+ function humanizeActionName(toolName: string): string {
719
+ return toolName
720
+ .replace(/^agent:/, "")
721
+ .replace(/[-_]+/g, " ")
722
+ .trim();
723
+ }
724
+
725
+ function completedToolTimeoutMessage(toolName: string): string {
726
+ if (toolName === "generate-design" || toolName === "update-design") {
727
+ return "The design was saved, but the assistant timed out before sending its final note. You can open the generated design from the completed tool card above.";
728
+ }
729
+ if (toolName === "present-design-variants") {
730
+ return "The design variants were saved, but the assistant timed out before sending its final note. You can review the completed variants from the tool card above.";
731
+ }
732
+ return `The ${humanizeActionName(toolName)} action completed, but the assistant timed out before sending its final response. The saved result is in the completed tool card above.`;
733
+ }
734
+
700
735
  /**
701
736
  * Signature of the *unique* sentence-like segments in a continuation's newly
702
737
  * streamed text, used to detect a degenerate repetition loop. A stuck model
@@ -785,6 +820,8 @@ function toolContinuationKey(
785
820
  stableJson(part.args),
786
821
  part.result === undefined ? "pending" : "done",
787
822
  part.result ?? "",
823
+ part.isError === true ? "error" : "",
824
+ part.completedSideEffect === true ? "side-effect" : "",
788
825
  part.activity === true ? "activity" : "tool",
789
826
  part.mcpApp ? "mcp-app" : "",
790
827
  ].join("\u0000");
@@ -851,6 +888,8 @@ function incrementalActionGuidance(tool: string): string | undefined {
851
888
  case "generate-design":
852
889
  case "update-design":
853
890
  return "persist a minimal first version (fewer files) with `generate-design`, then refine individual files with `edit-design` search/replace instead of resending everything";
891
+ case "present-design-variants":
892
+ return "save compact but complete variant screens with `present-design-variants` first, keeping each HTML direction focused enough to finish, then refine the chosen direction with `generate-design` or `edit-design`";
854
893
  case "create-visual-plan":
855
894
  case "create-ui-plan":
856
895
  case "create-plan-design":
@@ -1349,6 +1388,7 @@ export function createAgentChatAdapter(
1349
1388
  // its own budget rather than inheriting the prior payload's.
1350
1389
  let lastInFlightToolSignature: string | undefined;
1351
1390
  let repeatedInFlightToolCount = 0;
1391
+ let recoveryGaveUpOnInFlightTool = false;
1352
1392
  const MAX_REPEATED_INFLIGHT_TOOL_STALLS = 3;
1353
1393
  const continuationHistoryFragments: string[] = [];
1354
1394
  const structuredContinuationFragments: AgentChatStructuredMessage[] = [];
@@ -1390,6 +1430,9 @@ export function createAgentChatAdapter(
1390
1430
  };
1391
1431
 
1392
1432
  const exhaustedRecoveryMessage = (reason?: string): string => {
1433
+ if (recoveryGaveUpOnInFlightTool) {
1434
+ return "The agent got stuck waiting for the same tool to finish, so I stopped the automatic retries. The tool did not report a completed result.";
1435
+ }
1393
1436
  if (recoveryGaveUpOnRepetition) {
1394
1437
  return "The agent got stuck repeating the same response without finishing, so I stopped the automatic retries. This often happens when it tries to re-type a large pasted file into one action — starting a new chat, or asking for a smaller first step, usually gets it unstuck.";
1395
1438
  }
@@ -1674,6 +1717,83 @@ export function createAgentChatAdapter(
1674
1717
  return false;
1675
1718
  };
1676
1719
 
1720
+ const reconnectBackgroundContinuationForRunTimeout =
1721
+ async function* (): AsyncGenerator<
1722
+ ChatModelRunResult,
1723
+ boolean,
1724
+ unknown
1725
+ > {
1726
+ if (!threadId || !runId) return false;
1727
+ const interruptedRunId = runId;
1728
+ const interruptedLastSeq = lastSeq;
1729
+ let lastActiveRunError: unknown = null;
1730
+ for (let attempt = 0; attempt < 3; attempt++) {
1731
+ if (attempt > 0) {
1732
+ await delay(500, abortSignal);
1733
+ }
1734
+ if (abortSignal.aborted) return true;
1735
+ try {
1736
+ const activeRes = await fetch(
1737
+ `${apiUrl}/runs/active?threadId=${encodeURIComponent(threadId)}`,
1738
+ { signal: abortSignal },
1739
+ );
1740
+ if (!activeRes.ok) {
1741
+ if (activeRes.status === 404) return false;
1742
+ lastActiveRunError = new Error(
1743
+ `Active run lookup failed: ${activeRes.status}`,
1744
+ );
1745
+ continue;
1746
+ }
1747
+ const active = await activeRes.json();
1748
+ if (!active?.active || !active.runId) return false;
1749
+ const activeRunId = String(active.runId);
1750
+ const dispatchMode =
1751
+ typeof active.dispatchMode === "string"
1752
+ ? active.dispatchMode
1753
+ : "";
1754
+ if (activeRunId === interruptedRunId) {
1755
+ if (dispatchMode.startsWith("background")) continue;
1756
+ return false;
1757
+ }
1758
+ const activeTurnId =
1759
+ typeof active.turnId === "string" ? active.turnId : "";
1760
+ const activeStatus =
1761
+ typeof active.status === "string" ? active.status : "";
1762
+ if (!dispatchMode.startsWith("background")) return false;
1763
+ if (activeTurnId && activeTurnId !== turnId) return false;
1764
+ if (activeStatus !== "running" && activeStatus !== "starting") {
1765
+ return false;
1766
+ }
1767
+ runId = activeRunId;
1768
+ if (!attemptedRunIds.includes(activeRunId)) {
1769
+ attemptedRunIds.push(activeRunId);
1770
+ }
1771
+ lastSeq = -1;
1772
+ setActiveRun({ threadId, runId: activeRunId, lastSeq: -1 });
1773
+ const reconnected = yield* reconnectCurrentRun();
1774
+ if (reconnected) return true;
1775
+ } catch (activeErr: unknown) {
1776
+ if (
1777
+ activeErr instanceof Error &&
1778
+ activeErr.name === "AbortError"
1779
+ ) {
1780
+ clearActiveRun();
1781
+ return true;
1782
+ }
1783
+ lastActiveRunError = activeErr;
1784
+ }
1785
+ }
1786
+ if (lastActiveRunError) {
1787
+ captureChatClientError(
1788
+ lastActiveRunError,
1789
+ "reconnect-background-continuation-failed",
1790
+ );
1791
+ }
1792
+ runId = interruptedRunId;
1793
+ lastSeq = interruptedLastSeq;
1794
+ return false;
1795
+ };
1796
+
1677
1797
  const visibleContentForContinuation = (): ContentPart[] => {
1678
1798
  return contentAfterContinuationPrefix(
1679
1799
  content,
@@ -1683,7 +1803,11 @@ export function createAgentChatAdapter(
1683
1803
 
1684
1804
  const prepareAutoContinuation = (
1685
1805
  signal: AgentAutoContinueSignal,
1686
- ): { ok: boolean; resetVisibleContent: boolean } => {
1806
+ ): {
1807
+ ok: boolean;
1808
+ resetVisibleContent: boolean;
1809
+ completedToolName?: string;
1810
+ } => {
1687
1811
  lastAutoContinueReason = signal.reason;
1688
1812
  if (signal.errorInfo) {
1689
1813
  lastRecoverableRunError = signal.errorInfo;
@@ -1703,6 +1827,7 @@ export function createAgentChatAdapter(
1703
1827
  // before tool_start; treating it as progress caused silent retry
1704
1828
  // loops when the LLM timed out while assembling a large tool input.
1705
1829
  const hasInFlightTool = hasInFlightToolCall(visibleContent);
1830
+ const completedSideEffectTool = lastCompletedSideEffectTool(content);
1706
1831
  // Either real output or an actively-running tool counts as progress
1707
1832
  // for the stalled/empty caps.
1708
1833
  const madeProgress = madeContentProgress || hasInFlightTool;
@@ -1717,11 +1842,11 @@ export function createAgentChatAdapter(
1717
1842
  // MAX_REPEATED_INFLIGHT_TOOL_STALLS consecutive stream_ended events,
1718
1843
  // bail with a clear message.
1719
1844
  //
1720
- // Only count stream_ended (connection drop / reconnect failed), NOT
1721
- // run_timeout (server legitimately still executing a slow tool). A
1722
- // run_timeout with an in-flight tool means the server is actively
1723
- // working and reconnection may still recover the result; a repeated
1724
- // stream_ended means the connection keeps breaking under that payload.
1845
+ // Count broken streams (connection drop / reconnect failed) and
1846
+ // no_progress stalls (the client aborts a stream that stayed open but
1847
+ // stopped producing events). Do NOT count run_timeout: with an
1848
+ // in-flight tool that means the server is still actively executing a
1849
+ // slow action and reconnection may recover the result.
1725
1850
  const currentInFlightToolPart = visibleContent.find(
1726
1851
  (p): p is Extract<ContentPart, { type: "tool-call" }> =>
1727
1852
  p.type === "tool-call" &&
@@ -1732,8 +1857,9 @@ export function createAgentChatAdapter(
1732
1857
  const currentInFlightToolSignature = currentInFlightToolPart
1733
1858
  ? inFlightToolInputSignature(currentInFlightToolPart)
1734
1859
  : undefined;
1735
- const isConnectionDrop = signal.reason === "stream_ended";
1736
- if (currentInFlightToolName && isConnectionDrop) {
1860
+ const isBrokenInFlightTool =
1861
+ signal.reason === "stream_ended" || signal.reason === "no_progress";
1862
+ if (currentInFlightToolName && isBrokenInFlightTool) {
1737
1863
  if (
1738
1864
  currentInFlightToolName === lastInFlightToolName &&
1739
1865
  currentInFlightToolSignature === lastInFlightToolSignature
@@ -1772,13 +1898,31 @@ export function createAgentChatAdapter(
1772
1898
  emptyTransientContinuationAttempts = 0;
1773
1899
  } else {
1774
1900
  totalTransientContinuationAttempts += 1;
1901
+ // If a mutating action already completed, do not turn a missing
1902
+ // closing sentence into a scary connection failure. Give the model
1903
+ // one continuation opportunity (the completed tool itself counts
1904
+ // as progress on the first timeout); if the follow-up produces no
1905
+ // new content, stop locally with a clear "saved, final note timed
1906
+ // out" message.
1907
+ if (
1908
+ completedSideEffectTool &&
1909
+ !hasInFlightToolCall(content) &&
1910
+ !madeContentProgress &&
1911
+ !hasInFlightTool
1912
+ ) {
1913
+ return {
1914
+ ok: false,
1915
+ resetVisibleContent: false,
1916
+ completedToolName: completedSideEffectTool.toolName,
1917
+ };
1918
+ }
1775
1919
  // Bail when the same write tool is stuck in-flight across too many
1776
1920
  // consecutive continuations. Checked before the text-repeat guard
1777
1921
  // because hasInFlightTool=true would mask the repeat as progress.
1778
1922
  if (
1779
1923
  repeatedInFlightToolCount >= MAX_REPEATED_INFLIGHT_TOOL_STALLS
1780
1924
  ) {
1781
- recoveryGaveUpOnRepetition = true;
1925
+ recoveryGaveUpOnInFlightTool = true;
1782
1926
  return { ok: false, resetVisibleContent: false };
1783
1927
  }
1784
1928
  // Bail fast on a non-advancing repetition loop, well before the
@@ -2175,6 +2319,11 @@ export function createAgentChatAdapter(
2175
2319
  if (err.reason === "no_progress") {
2176
2320
  await abortCurrentRun();
2177
2321
  }
2322
+ if (err.reason === "run_timeout" && !err.errorInfo) {
2323
+ const reconnected =
2324
+ yield* reconnectBackgroundContinuationForRunTimeout();
2325
+ if (reconnected) return;
2326
+ }
2178
2327
  if (err.reason === "stream_ended") {
2179
2328
  const reconnected = yield* reconnectCurrentRun();
2180
2329
  if (reconnected) return;
@@ -2183,6 +2332,31 @@ export function createAgentChatAdapter(
2183
2332
  }
2184
2333
  const continuation = prepareAutoContinuation(err);
2185
2334
  if (!continuation.ok) {
2335
+ if (continuation.completedToolName) {
2336
+ const message = completedToolTimeoutMessage(
2337
+ continuation.completedToolName,
2338
+ );
2339
+ content.push({ type: "text", text: message });
2340
+ yield {
2341
+ content: [...content],
2342
+ status: {
2343
+ type: "complete" as const,
2344
+ reason: "stop" as const,
2345
+ },
2346
+ metadata: {
2347
+ custom: {
2348
+ ...(runId ? { runId } : {}),
2349
+ runWarning: {
2350
+ message,
2351
+ errorCode: "final_response_timeout_after_tool",
2352
+ recoverable: true,
2353
+ },
2354
+ },
2355
+ },
2356
+ };
2357
+ clearActiveRun();
2358
+ return;
2359
+ }
2186
2360
  const preservedError =
2187
2361
  err.errorInfo ?? lastRecoverableRunError ?? null;
2188
2362
  const message =
@@ -2355,6 +2529,31 @@ export function createAgentChatAdapter(
2355
2529
  new AgentAutoContinueSignal({ reason: "stream_ended" }),
2356
2530
  );
2357
2531
  if (!continuation.ok) {
2532
+ if (continuation.completedToolName) {
2533
+ const message = completedToolTimeoutMessage(
2534
+ continuation.completedToolName,
2535
+ );
2536
+ content.push({ type: "text", text: message });
2537
+ yield {
2538
+ content: [...content],
2539
+ status: {
2540
+ type: "complete" as const,
2541
+ reason: "stop" as const,
2542
+ },
2543
+ metadata: {
2544
+ custom: {
2545
+ ...(runId ? { runId } : {}),
2546
+ runWarning: {
2547
+ message,
2548
+ errorCode: "final_response_timeout_after_tool",
2549
+ recoverable: true,
2550
+ },
2551
+ },
2552
+ },
2553
+ };
2554
+ clearActiveRun();
2555
+ return;
2556
+ }
2358
2557
  const message = exhaustedRecoveryMessage("stream_ended");
2359
2558
  captureChatClientError(err, "recovery-exhausted");
2360
2559
  const runError = {
@@ -15,6 +15,7 @@ import {
15
15
  import { useQueryClient } from "@tanstack/react-query";
16
16
  import {
17
17
  useCallback,
18
+ type ComponentPropsWithoutRef,
18
19
  useEffect,
19
20
  useId,
20
21
  useMemo,
@@ -69,6 +70,10 @@ export interface ShareButtonProps {
69
70
  /** Where to render share links in the popover. Defaults to the bottom,
70
71
  * matching the historical Google-Docs-style share dialog. */
71
72
  shareUrlPlacement?: "top" | "bottom";
73
+ /** Whether to render copyable share URL fields. Defaults to true. */
74
+ showShareLinks?: boolean;
75
+ /** Whether to render the bottom Done button. Defaults to true. */
76
+ showDoneButton?: boolean;
72
77
  /** Optional placeholder shown in the share-URL slot when `shareUrl` is
73
78
  * undefined. Use this to explain *why* there's no link yet (e.g. "Publish
74
79
  * this form to get a public response link") instead of leaving the slot
@@ -99,6 +104,8 @@ export interface ShareButtonProps {
99
104
  generalAccessLabel?: ReactNode;
100
105
  /** Optional note rendered between general access and the copyable link. */
101
106
  accessNote?: ReactNode;
107
+ /** Optional host-rendered footer for compact app-specific share actions. */
108
+ shareFooterContent?: ReactNode;
102
109
  /** Optional Notion-style organization access control. When present, the
103
110
  * share panel exposes a "Hide in search" switch under Advanced for org
104
111
  * visibility. */
@@ -171,6 +178,8 @@ const BUTTON_GHOST_ICON = cn(
171
178
  );
172
179
  const SHARE_POPOVER_SURFACE =
173
180
  "border border-border bg-popover text-popover-foreground";
181
+ const SHARE_NESTED_OVERLAY_ATTR = "data-agent-native-share-overlay";
182
+ const SHARE_NESTED_OVERLAY_Z = "z-[100020]";
174
183
  const MEMBER_SUGGESTION_LIMIT = 25;
175
184
  const MEMBER_SEARCH_DEBOUNCE_MS = 140;
176
185
 
@@ -219,6 +228,28 @@ const ROLE_OPTIONS: Array<{ value: Role; label: string; description: string }> =
219
228
  },
220
229
  ];
221
230
 
231
+ type SharePopoverInteractOutsideEvent = Parameters<
232
+ NonNullable<
233
+ ComponentPropsWithoutRef<typeof PopoverContent>["onInteractOutside"]
234
+ >
235
+ >[0];
236
+
237
+ function isShareNestedOverlayTarget(target: EventTarget | null): boolean {
238
+ return (
239
+ target instanceof Element &&
240
+ target.closest(`[${SHARE_NESTED_OVERLAY_ATTR}]`) !== null
241
+ );
242
+ }
243
+
244
+ function handleSharePopoverInteractOutside(
245
+ event: SharePopoverInteractOutsideEvent,
246
+ ) {
247
+ const originalTarget = event.detail.originalEvent.target;
248
+ if (isShareNestedOverlayTarget(originalTarget)) {
249
+ event.preventDefault();
250
+ }
251
+ }
252
+
222
253
  /**
223
254
  * Framework share control. Renders a shadcn-outline-styled trigger that
224
255
  * opens a Google-Docs-style popover anchored beneath it. Uses Tailwind
@@ -344,12 +375,14 @@ export function ShareButton(props: ShareButtonProps) {
344
375
  <PopoverContent
345
376
  align="end"
346
377
  sideOffset={6}
378
+ data-agent-native-share-overlay=""
347
379
  className={cn(
348
380
  "z-[2000] w-[min(460px,92vw)] rounded-lg p-4 shadow-lg",
349
381
  SHARE_POPOVER_SURFACE,
350
382
  props.popoverClassName,
351
383
  )}
352
384
  onOpenAutoFocus={(e) => e.preventDefault()}
385
+ onInteractOutside={handleSharePopoverInteractOutside}
353
386
  >
354
387
  <SharePanel
355
388
  {...props}
@@ -622,9 +655,11 @@ function SharePanel(
622
655
  </>
623
656
  );
624
657
  const showShareLinks =
625
- Boolean(props.shareUrl) ||
626
- Boolean(props.shareUrlPlaceholder) ||
627
- Boolean(props.secondaryShareUrl);
658
+ (props.showShareLinks ?? true) &&
659
+ (Boolean(props.shareUrl) ||
660
+ Boolean(props.shareUrlPlaceholder) ||
661
+ Boolean(props.secondaryShareUrl));
662
+ const showDoneButton = props.showDoneButton ?? true;
628
663
  const shareUrlPlacement = props.shareUrlPlacement ?? "bottom";
629
664
  const extraTabs = props.shareTabs?.tabs ?? [];
630
665
  const hasTabs = extraTabs.length > 0;
@@ -821,11 +856,13 @@ function SharePanel(
821
856
  <div className="mb-4 h-7 rounded-md bg-muted animate-pulse" />
822
857
  <div className="mb-2 text-sm font-semibold">{generalAccessLabel}</div>
823
858
  <div className="mb-4 h-9 rounded-md bg-muted animate-pulse" />
824
- <div className="mt-2 flex justify-end">
825
- <button type="button" onClick={onClose} className={BUTTON_PRIMARY_SM}>
826
- Done
827
- </button>
828
- </div>
859
+ {showDoneButton ? (
860
+ <div className="mt-2 flex justify-end">
861
+ <button type="button" onClick={onClose} className={BUTTON_PRIMARY_SM}>
862
+ Done
863
+ </button>
864
+ </div>
865
+ ) : null}
829
866
  </div>
830
867
  ) : (
831
868
  <div>
@@ -990,15 +1027,19 @@ function SharePanel(
990
1027
 
991
1028
  {showShareLinks && shareUrlPlacement === "bottom" ? shareLinks : null}
992
1029
 
993
- <div className="mt-2 flex justify-end">
994
- <button
995
- type="button"
996
- onClick={handleDone}
997
- className={BUTTON_PRIMARY_SM}
998
- >
999
- Done
1000
- </button>
1001
- </div>
1030
+ {props.shareFooterContent}
1031
+
1032
+ {showDoneButton ? (
1033
+ <div className="mt-2 flex justify-end">
1034
+ <button
1035
+ type="button"
1036
+ onClick={handleDone}
1037
+ className={BUTTON_PRIMARY_SM}
1038
+ >
1039
+ Done
1040
+ </button>
1041
+ </div>
1042
+ ) : null}
1002
1043
  </div>
1003
1044
  );
1004
1045
 
@@ -1076,8 +1117,13 @@ function AdvancedAccessPopover({
1076
1117
  <PopoverContent
1077
1118
  align="start"
1078
1119
  sideOffset={6}
1120
+ data-agent-native-share-overlay=""
1079
1121
  onOpenAutoFocus={(event) => event.preventDefault()}
1080
- className={cn("z-[2300] w-72 p-3 shadow-lg", SHARE_POPOVER_SURFACE)}
1122
+ className={cn(
1123
+ SHARE_NESTED_OVERLAY_Z,
1124
+ "w-72 p-3 shadow-lg",
1125
+ SHARE_POPOVER_SURFACE,
1126
+ )}
1081
1127
  >
1082
1128
  <div className="space-y-3">
1083
1129
  <div>
@@ -1291,9 +1337,11 @@ function MemberAutocomplete({
1291
1337
  <PopoverContent
1292
1338
  align="start"
1293
1339
  sideOffset={4}
1340
+ data-agent-native-share-overlay=""
1294
1341
  onOpenAutoFocus={(event) => event.preventDefault()}
1295
1342
  className={cn(
1296
- "z-[2200] w-[var(--radix-popper-anchor-width)] min-w-[18rem] rounded-md p-1 shadow-lg",
1343
+ SHARE_NESTED_OVERLAY_Z,
1344
+ "w-[var(--radix-popper-anchor-width)] min-w-[18rem] rounded-md p-1 shadow-lg",
1297
1345
  SHARE_POPOVER_SURFACE,
1298
1346
  )}
1299
1347
  >
@@ -1441,8 +1489,7 @@ function CopyLinkField({
1441
1489
  // Radix Select wrappers styled like shadcn Select (no native <select> anywhere)
1442
1490
  // ---------------------------------------------------------------------------
1443
1491
 
1444
- const selectContentClass =
1445
- "z-[2100] min-w-[12rem] overflow-hidden rounded-md border border-border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0";
1492
+ const selectContentClass = `${SHARE_NESTED_OVERLAY_Z} min-w-[12rem] overflow-hidden rounded-md border border-border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0`;
1446
1493
  const selectItemClass =
1447
1494
  "relative flex w-full cursor-pointer select-none items-start gap-2 rounded-sm py-2 ps-8 pe-3 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50";
1448
1495
 
@@ -1517,6 +1564,7 @@ function RoleSelect(props: {
1517
1564
  </Select.Trigger>
1518
1565
  <Select.Portal>
1519
1566
  <Select.Content
1567
+ data-agent-native-share-overlay=""
1520
1568
  className={selectContentClass}
1521
1569
  position="popper"
1522
1570
  sideOffset={4}
@@ -1563,6 +1611,7 @@ function VisibilitySelect(props: {
1563
1611
  </Select.Trigger>
1564
1612
  <Select.Portal>
1565
1613
  <Select.Content
1614
+ data-agent-native-share-overlay=""
1566
1615
  className={selectContentClass}
1567
1616
  position="popper"
1568
1617
  sideOffset={4}