@agent-native/core 0.92.5 → 0.92.6

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 (147) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +13 -0
  3. package/corpus/core/docs/content/template-assets.mdx +139 -0
  4. package/corpus/core/package.json +1 -1
  5. package/corpus/core/src/agent/production-agent.ts +96 -6
  6. package/corpus/core/src/client/AssistantChat.tsx +127 -15
  7. package/corpus/core/src/client/chat/tool-call-display.tsx +9 -3
  8. package/corpus/core/src/client/org/TeamPage.tsx +274 -177
  9. package/corpus/core/src/client/settings/DemoModeSection.tsx +6 -6
  10. package/corpus/core/src/client/settings/SettingsPanel.tsx +1 -1
  11. package/corpus/core/src/client/settings/SettingsTabsPage.tsx +49 -1
  12. package/corpus/core/src/demo/actions/toggle-demo-mode.ts +2 -2
  13. package/corpus/core/src/demo/config.ts +3 -3
  14. package/corpus/core/src/demo/redact.ts +720 -0
  15. package/corpus/core/src/extensions/url-safety.ts +13 -1
  16. package/corpus/core/src/localization/default-messages.ts +7 -0
  17. package/corpus/core/src/templates/default/app/i18n/ar-SA.ts +7 -0
  18. package/corpus/core/src/templates/default/app/i18n/de-DE.ts +7 -0
  19. package/corpus/core/src/templates/default/app/i18n/en-US.ts +7 -0
  20. package/corpus/core/src/templates/default/app/i18n/es-ES.ts +7 -0
  21. package/corpus/core/src/templates/default/app/i18n/fr-FR.ts +7 -0
  22. package/corpus/core/src/templates/default/app/i18n/hi-IN.ts +7 -0
  23. package/corpus/core/src/templates/default/app/i18n/ja-JP.ts +7 -0
  24. package/corpus/core/src/templates/default/app/i18n/ko-KR.ts +7 -0
  25. package/corpus/core/src/templates/default/app/i18n/pt-BR.ts +7 -0
  26. package/corpus/core/src/templates/default/app/i18n/zh-CN.ts +7 -0
  27. package/corpus/core/src/templates/default/app/i18n/zh-TW.ts +7 -0
  28. package/corpus/templates/analytics/AGENTS.md +6 -3
  29. package/corpus/templates/analytics/changelog/2026-07-09-uptime-monitors-use-scheduled-workers-in-production-serverless.md +6 -0
  30. package/corpus/templates/analytics/docs/uptime-monitoring.md +5 -0
  31. package/corpus/templates/analytics/server/plugins/uptime-monitor-jobs.ts +14 -2
  32. package/corpus/templates/assets/.agents/skills/image-generation/SKILL.md +35 -0
  33. package/corpus/templates/assets/.agents/skills/library-management/SKILL.md +19 -0
  34. package/corpus/templates/assets/AGENTS.md +13 -0
  35. package/corpus/templates/assets/actions/_generation-preset-settings.ts +73 -0
  36. package/corpus/templates/assets/actions/_preset-skeleton-validation.ts +63 -0
  37. package/corpus/templates/assets/actions/create-generation-preset.ts +22 -8
  38. package/corpus/templates/assets/actions/duplicate-library.ts +49 -1
  39. package/corpus/templates/assets/actions/generate-image-batch.ts +14 -1
  40. package/corpus/templates/assets/actions/generate-image.ts +195 -26
  41. package/corpus/templates/assets/actions/import-asset-from-url.ts +323 -0
  42. package/corpus/templates/assets/actions/list-generation-presets.ts +1 -1
  43. package/corpus/templates/assets/actions/rerun-generation-run.ts +37 -0
  44. package/corpus/templates/assets/actions/update-generation-preset.ts +19 -1
  45. package/corpus/templates/assets/app/i18n/ar-SA.ts +1 -0
  46. package/corpus/templates/assets/app/i18n/de-DE.ts +1 -0
  47. package/corpus/templates/assets/app/i18n/es-ES.ts +1 -0
  48. package/corpus/templates/assets/app/i18n/fr-FR.ts +1 -0
  49. package/corpus/templates/assets/app/i18n/hi-IN.ts +1 -0
  50. package/corpus/templates/assets/app/i18n/ja-JP.ts +1 -0
  51. package/corpus/templates/assets/app/i18n/ko-KR.ts +1 -0
  52. package/corpus/templates/assets/app/i18n/pt-BR.ts +1 -0
  53. package/corpus/templates/assets/app/i18n/zh-CN.ts +1 -0
  54. package/corpus/templates/assets/app/i18n/zh-TW.ts +41 -0
  55. package/corpus/templates/assets/app/i18n-data.ts +93 -0
  56. package/corpus/templates/assets/app/routes/brand-kits.$id.tsx +66 -11
  57. package/corpus/templates/assets/app/routes/brand-kits.$id_.presets.$presetId.tsx +959 -24
  58. package/corpus/templates/assets/changelog/2026-07-07-preset-skeleton-controls-fit-correctly-on-narrower-edit-page.md +6 -0
  59. package/corpus/templates/assets/changelog/2026-07-09-import-reference-images-from-url.md +6 -0
  60. package/corpus/templates/assets/changelog/2026-07-09-preset-reference-board.md +6 -0
  61. package/corpus/templates/assets/server/handlers/assets.ts +7 -27
  62. package/corpus/templates/assets/server/lib/generation.ts +83 -2
  63. package/corpus/templates/assets/server/lib/preset-chat-context.ts +17 -4
  64. package/corpus/templates/assets/server/lib/preset-references.ts +182 -0
  65. package/corpus/templates/assets/server/lib/upload-validation.ts +38 -0
  66. package/corpus/templates/assets/shared/api.ts +20 -0
  67. package/corpus/templates/calendar/.agents/skills/event-management/SKILL.md +6 -0
  68. package/corpus/templates/calendar/actions/create-event.ts +8 -2
  69. package/corpus/templates/calendar/app/components/calendar/CreateEventDialog.tsx +22 -7
  70. package/corpus/templates/calendar/changelog/2026-07-09-calendar-events-with-guests-now-automatically-get-a-google-m.md +6 -0
  71. package/corpus/templates/calendar/changelog/2026-07-09-new-events-now-mark-the-creator-rsvp-as-yes.md +6 -0
  72. package/corpus/templates/calendar/server/handlers/events.ts +9 -2
  73. package/corpus/templates/calendar/server/lib/event-video-conferencing.ts +58 -0
  74. package/corpus/templates/calendar/server/lib/google-calendar.ts +1 -0
  75. package/corpus/templates/clips/changelog/2026-07-09-meeting-reminder-popovers-can-now-be-dismissed-with-a-top-le.md +6 -0
  76. package/corpus/templates/clips/desktop/src/overlays/meeting-notification.tsx +3 -1
  77. package/corpus/templates/clips/desktop/src/styles.css +13 -9
  78. package/corpus/templates/clips/desktop/src-tauri/src/notifications.rs +5 -2
  79. package/dist/agent/production-agent.d.ts +34 -1
  80. package/dist/agent/production-agent.d.ts.map +1 -1
  81. package/dist/agent/production-agent.js +58 -5
  82. package/dist/agent/production-agent.js.map +1 -1
  83. package/dist/client/AssistantChat.d.ts +3 -1
  84. package/dist/client/AssistantChat.d.ts.map +1 -1
  85. package/dist/client/AssistantChat.js +96 -15
  86. package/dist/client/AssistantChat.js.map +1 -1
  87. package/dist/client/chat/tool-call-display.d.ts.map +1 -1
  88. package/dist/client/chat/tool-call-display.js +5 -1
  89. package/dist/client/chat/tool-call-display.js.map +1 -1
  90. package/dist/client/org/TeamPage.d.ts.map +1 -1
  91. package/dist/client/org/TeamPage.js +32 -13
  92. package/dist/client/org/TeamPage.js.map +1 -1
  93. package/dist/client/settings/DemoModeSection.d.ts +3 -3
  94. package/dist/client/settings/DemoModeSection.js +4 -4
  95. package/dist/client/settings/DemoModeSection.js.map +1 -1
  96. package/dist/client/settings/SettingsPanel.js +1 -1
  97. package/dist/client/settings/SettingsPanel.js.map +1 -1
  98. package/dist/client/settings/SettingsTabsPage.d.ts.map +1 -1
  99. package/dist/client/settings/SettingsTabsPage.js +38 -2
  100. package/dist/client/settings/SettingsTabsPage.js.map +1 -1
  101. package/dist/collab/awareness.d.ts +2 -2
  102. package/dist/collab/awareness.d.ts.map +1 -1
  103. package/dist/demo/actions/toggle-demo-mode.js +2 -2
  104. package/dist/demo/actions/toggle-demo-mode.js.map +1 -1
  105. package/dist/demo/config.js +3 -3
  106. package/dist/demo/config.js.map +1 -1
  107. package/dist/demo/redact.d.ts +1 -1
  108. package/dist/demo/redact.d.ts.map +1 -1
  109. package/dist/demo/redact.js +0 -0
  110. package/dist/demo/redact.js.map +1 -1
  111. package/dist/extensions/url-safety.d.ts +5 -0
  112. package/dist/extensions/url-safety.d.ts.map +1 -1
  113. package/dist/extensions/url-safety.js +10 -0
  114. package/dist/extensions/url-safety.js.map +1 -1
  115. package/dist/file-upload/actions/upload-image.d.ts +1 -1
  116. package/dist/localization/default-messages.d.ts +7 -0
  117. package/dist/localization/default-messages.d.ts.map +1 -1
  118. package/dist/localization/default-messages.js +7 -0
  119. package/dist/localization/default-messages.js.map +1 -1
  120. package/dist/observability/routes.d.ts +5 -5
  121. package/dist/progress/routes.d.ts +1 -1
  122. package/dist/provider-api/corpus-jobs.d.ts +2 -2
  123. package/dist/resources/handlers.d.ts +1 -1
  124. package/dist/templates/default/app/i18n/ar-SA.ts +7 -0
  125. package/dist/templates/default/app/i18n/de-DE.ts +7 -0
  126. package/dist/templates/default/app/i18n/en-US.ts +7 -0
  127. package/dist/templates/default/app/i18n/es-ES.ts +7 -0
  128. package/dist/templates/default/app/i18n/fr-FR.ts +7 -0
  129. package/dist/templates/default/app/i18n/hi-IN.ts +7 -0
  130. package/dist/templates/default/app/i18n/ja-JP.ts +7 -0
  131. package/dist/templates/default/app/i18n/ko-KR.ts +7 -0
  132. package/dist/templates/default/app/i18n/pt-BR.ts +7 -0
  133. package/dist/templates/default/app/i18n/zh-CN.ts +7 -0
  134. package/dist/templates/default/app/i18n/zh-TW.ts +7 -0
  135. package/docs/content/template-assets.mdx +139 -0
  136. package/package.json +1 -1
  137. package/src/templates/default/app/i18n/ar-SA.ts +7 -0
  138. package/src/templates/default/app/i18n/de-DE.ts +7 -0
  139. package/src/templates/default/app/i18n/en-US.ts +7 -0
  140. package/src/templates/default/app/i18n/es-ES.ts +7 -0
  141. package/src/templates/default/app/i18n/fr-FR.ts +7 -0
  142. package/src/templates/default/app/i18n/hi-IN.ts +7 -0
  143. package/src/templates/default/app/i18n/ja-JP.ts +7 -0
  144. package/src/templates/default/app/i18n/ko-KR.ts +7 -0
  145. package/src/templates/default/app/i18n/pt-BR.ts +7 -0
  146. package/src/templates/default/app/i18n/zh-CN.ts +7 -0
  147. package/src/templates/default/app/i18n/zh-TW.ts +7 -0
package/corpus/README.md CHANGED
@@ -27,5 +27,5 @@ rg -n "defineAction|useActionQuery" node_modules/@agent-native/core/corpus
27
27
 
28
28
  ## Generated Counts
29
29
 
30
- - core files: 2154
31
- - template files: 5047
30
+ - core files: 2155
31
+ - template files: 5057
@@ -1,5 +1,18 @@
1
1
  # @agent-native/core
2
2
 
3
+ ## 0.92.6
4
+
5
+ ### Patch Changes
6
+
7
+ - 5f187db: Highlight run-code source snippets in chat and keep raw tool output popovers within the available viewport.
8
+ - 5f187db: Fix duplicate in-progress chat output during reconnect handoff so streaming text and tool calls render once while the live assistant message catches up. Also stop a reconnect tool spinner from rendering a second card beside its live pending tool card when their reader-local and server-scoped ids don't match, in every reconnect/handoff window.
9
+ - 5f187db: Focus the settings search field automatically when desktop users open settings.
10
+ - 5f187db: Improve organization settings so invites stay above the fold and members appear in a separate table card.
11
+ - 6a29ba0: `ssrfSafeFetch` hardening: a new `httpsOnly` option validates the URL scheme on the initial request and on every redirect hop, so HTTPS-only callers cannot be downgraded to plain HTTP by a redirect from the untrusted origin. Followed redirect responses now also have their bodies cancelled so each hop's connection is released immediately instead of being held until GC.
12
+ - 5f187db: Remove the divider between settings tabs and their tab content.
13
+ - 5f187db: Preserve dashboard, chart, and label names in demo mode while continuing to anonymize free-text/contact names, emails, and numbers.
14
+ - c32c174: add preset-driven-flow to asset template docs
15
+
3
16
  ## 0.92.5
4
17
 
5
18
  ### Patch Changes
@@ -102,6 +102,145 @@ Live demo: [assets.agent-native.com](https://assets.agent-native.com).
102
102
  4. **Use the asset elsewhere.** Copy the export, embed the picker in another
103
103
  app, or let another agent call Assets over A2A.
104
104
 
105
+ ## The preset-driven workflow
106
+
107
+ Assets produces its best, most repeatable results when work is split into two
108
+ jobs that need very different skills:
109
+
110
+ 1. **Designing the recipe** — slow, craft-heavy, done once per use case. A
111
+ designer works with the agent to build a **generation preset**: the rules,
112
+ references, locked-in model, and a prompt template that has been QA'd until
113
+ it reliably produces on-brand output.
114
+ 2. **Running the recipe** — fast and repeatable. A marketer pastes a campaign
115
+ brief, tags the preset, and gets a finished, brand-safe asset without
116
+ touching model settings, aspect ratios, masks, or prompt wording.
117
+
118
+ A preset is the contract between these two people. The designer encodes taste
119
+ and technical judgment once; everyone else inherits it on every generation.
120
+ Think of presets as the design system for AI-generated assets: a raw prompt is
121
+ a throwaway that drifts every time it is retyped, while a preset is durable,
122
+ shared, and carries the decisions (model, aspect ratio, text and reference
123
+ rules, logo handling, fixed layout) that actually determine whether the output
124
+ is usable.
125
+
126
+ ### Designers: build one preset per use case
127
+
128
+ Create a separate preset for each repeatable format rather than one generic
129
+ "social post" preset that tries to do everything. Specific presets give the
130
+ model clearer rules and produce better output. Good examples:
131
+
132
+ - LinkedIn ad
133
+ - Webinar announcement
134
+ - Event speaker card
135
+ - Podcast guest card
136
+ - Customer story quote card
137
+ - Blog hero
138
+ - Blog body diagram
139
+
140
+ The primary way to build one is **conversationally**: describe the use case to
141
+ the agent and let it create the preset and fill in sensible defaults — model,
142
+ aspect ratio and size, a first prompt template, and text/reference/logo
143
+ policies — then refine in the same conversation ("make it darker", "tighten the
144
+ text policy", "try a square variant"). Because that reasoning shows up directly
145
+ in the defaults the agent picks, run this design conversation on the strongest
146
+ available model.
147
+
148
+ Each preset locks down:
149
+
150
+ | Setting | What it controls |
151
+ | ------------------- | -------------------------------------------------------------------------------------------------------------------- |
152
+ | Prompt template | The QA'd creative recipe, with slots for the campaign brief |
153
+ | Model | The right image engine for this job (see below) |
154
+ | Aspect ratio + size | Output format for the target channel |
155
+ | Text policy | Whether and how literal copy is rendered vs suppressed |
156
+ | Reference policy | Which brand references anchor the look |
157
+ | Reference board | Named fixed or per-run visual references, such as a usual host, guest speaker, product, backdrop, or composition cue |
158
+ | Logo | Composites the library's canonical logo pixel-perfect after generation, so the model never redraws it |
159
+ | Skeleton / mask | A fixed brand plate the subject is composited or inpainted into, for locked layouts |
160
+
161
+ ### Choosing the model per preset
162
+
163
+ Pick the model in the preset so downstream users never have to:
164
+
165
+ - **A general high-quality image model** for hero images, rich scenes, and
166
+ complex compositions, when you need broad aspect-ratio flexibility.
167
+ - **A fast image model** for high-volume social where turnaround matters more
168
+ than maximum polish.
169
+ - **gpt-image-2** for customer-facing, text-heavy, and identity-sensitive work.
170
+ It renders crisp literal copy inside the image, and it is the engine used for
171
+ **mask inpainting** into a fixed layout.
172
+ - **gpt-image-1** for transparent **cutout** subjects that are composited onto a
173
+ brand plate afterward.
174
+
175
+ When a use case has a **fixed brand frame** that must survive every generation
176
+ (a webinar template with a locked header and logo, an ad plate with a reserved
177
+ product area), build a preset skeleton: upload the background plate, choose
178
+ gpt-image-2, and optionally add a same-size mask. **Opaque areas are preserved,
179
+ transparent areas are editable**, so the logo, framing, and existing copy come
180
+ back untouched while only the art region changes. Note that gpt-image edits are
181
+ guided semantic rewrites, not pixel-only patches, so the prompt should still
182
+ describe the whole intended image and restate what must not change.
183
+
184
+ Use the **reference board** when a preset needs annotated visual inputs that
185
+ ride along with each run. Each board entry names one thing — a person, product,
186
+ style, background, or composition example — and can pin up to four images plus a
187
+ plain-language description. Fixed entries attach automatically. Variable
188
+ entries can be replaced per run with `presetReferenceFills`, and required
189
+ variable entries block generation until the agent supplies images from user
190
+ attachments or the library.
191
+
192
+ For example, a Webinar Announcement preset might use a skeleton plate and mask
193
+ for the locked event chrome, a fixed "Steve" subject entry for the usual host,
194
+ and a required variable "Guest speaker" entry for each event. Use a
195
+ `background` entry for setting context. Use a `composition` entry for layout
196
+ inspiration from a past poster; exact placement belongs in the skeleton plate,
197
+ not a reference image.
198
+
199
+ ### QA the prompt until it is reliable
200
+
201
+ The model, references, and skeleton set the stage, but the prompt template is
202
+ what turns a brief into a consistently good asset. Have the agent generate
203
+ several candidates against realistic briefs and tighten the template until it
204
+ holds. Techniques worth baking in:
205
+
206
+ - **Structure the prompt in a fixed order**: scene, subject, key details,
207
+ constraints. Name the intended use so the model picks the right polish level.
208
+ - **For literal copy, be surgical**: put exact text in quotes or ALL CAPS,
209
+ specify font, size, color, and placement, and for tricky brand names spell
210
+ them out. Ask for the text exactly once with no extra characters.
211
+ - **For skeleton or inpainting presets, restate the invariants every run**:
212
+ "change only the center region, preserve the header, logo, and framing."
213
+
214
+ Before handing a preset to the wider team, run realistic QA generations
215
+ (including briefs you did not write) and confirm it: follows the use case,
216
+ respects the mask, preserves fixed text and logos, avoids inventing extra
217
+ headlines or badges, uses references correctly, places the subject in the right
218
+ region, handles short and long briefs, and still holds up after a refinement.
219
+
220
+ ### Everyone else: paste the brief, tag the preset, generate
221
+
222
+ Once a preset exists, running it is three moves: paste the campaign brief, tag
223
+ the preset with `@`, and generate. The tagged preset injects its full aesthetic
224
+ brief into the agent, and the saved model, format, tier, logo, and skeleton all
225
+ apply automatically.
226
+
227
+ ```text
228
+ Create a LinkedIn ad for our webinar on AI landing pages.
229
+ Audience: B2B marketing teams.
230
+ Message: launch campaign pages faster while staying on brand.
231
+ CTA: Register now.
232
+
233
+ @Webinar Announcement
234
+ ```
235
+
236
+ No model picking, no aspect-ratio math, no mask files, no prompt engineering.
237
+ Rule of thumb: designers own the preset, everyone else owns the brief. If a
238
+ user finds themselves choosing models, masks, or layout rules during normal
239
+ use, the preset is not specific enough yet.
240
+
241
+ For deeper prompting guidance on the underlying models, see OpenAI's
242
+ [GPT Image prompting guide](https://developers.openai.com/cookbook/examples/multimodal/image-gen-models-prompting-guide).
243
+
105
244
  ## Useful prompts
106
245
 
107
246
  - "Generate three blog hero options using the Acme product references."
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-native/core",
3
- "version": "0.92.5",
3
+ "version": "0.92.6",
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": {
@@ -4193,7 +4193,7 @@ export async function runAgentLoop(opts: {
4193
4193
  // `_agentImages` result field (stripped from the JSON the model
4194
4194
  // reads, even in demo mode). Demo mode drops the images themselves —
4195
4195
  // text redaction can't scrub pixels, and a screenshot would leak the
4196
- // real names/emails the redaction exists to hide. The images array
4196
+ // real contact names/emails/numbers the redaction exists to hide. The images array
4197
4197
  // never touches the ledger — only the compact text notes appended
4198
4198
  // below are persisted.
4199
4199
  let imageNotes: string[] = [];
@@ -4664,7 +4664,40 @@ export function backgroundContinuationReasonForRun(
4664
4664
  }
4665
4665
 
4666
4666
  export async function runAgentLoopWithMainChatInternalContinuations(
4667
- opts: Parameters<typeof runAgentLoop>[0],
4667
+ opts: Parameters<typeof runAgentLoop>[0] & {
4668
+ /**
4669
+ * Opt-in: also catch a thrown `isResumableEngineError` (gateway drop /
4670
+ * transport interruption surviving engine retries) and retry it in-process
4671
+ * — the same "continue from where you left off" treatment already applied
4672
+ * to in-loop `auto_continue` events below — instead of letting it
4673
+ * propagate out to the caller. Default false/omitted preserves the exact
4674
+ * prior behavior: such an error is NOT caught here and propagates to
4675
+ * `startRun`'s outer catch, which marks the run "errored" and (for a
4676
+ * background worker) hands off to `chainServerDrivenContinuation` for a
4677
+ * fresh invocation.
4678
+ *
4679
+ * Only safe to enable when the CALLER is a proven durable-background
4680
+ * worker (`isInBackgroundFunctionRuntime()`): such a worker has minutes of
4681
+ * remaining budget on this same invocation, so resuming in-process avoids
4682
+ * a self-dispatch entirely — critically, it avoids the bg-function
4683
+ * self-dispatch 404 (a background function cannot invoke its own public
4684
+ * URL from inside a live invocation) that this same recoverable-error
4685
+ * path would otherwise hit via `chainServerDrivenContinuation`. A
4686
+ * foreground turn (~40s budget) must NOT set this — it keeps depending on
4687
+ * the existing cross-invocation recovery (foreground self-chain or the
4688
+ * client's `auto_continue` re-POST), unchanged.
4689
+ */
4690
+ resumeResumableErrorsInProcess?: boolean;
4691
+ /**
4692
+ * Override for the per-invocation continuation attempt cap. Defaults to
4693
+ * `MAIN_CHAT_INTERNAL_CONTINUATION_LIMIT` (6) when omitted — unchanged for
4694
+ * every existing caller. A proven background-function worker (15-min
4695
+ * budget) can pass a higher cap (e.g. `MAX_BACKGROUND_RUN_CONTINUATIONS`)
4696
+ * so a run that needs several resumable-error recoveries within one
4697
+ * 13-minute chunk isn't cut off at the foreground-sized limit.
4698
+ */
4699
+ maxContinuations?: number;
4700
+ },
4668
4701
  ): Promise<Awaited<ReturnType<typeof runAgentLoop>>> {
4669
4702
  const usage: Awaited<ReturnType<typeof runAgentLoop>> = {
4670
4703
  inputTokens: 0,
@@ -4681,11 +4714,18 @@ export async function runAgentLoopWithMainChatInternalContinuations(
4681
4714
  usage.model = next.model;
4682
4715
  };
4683
4716
 
4717
+ const resumeResumableErrorsInProcess =
4718
+ opts.resumeResumableErrorsInProcess === true;
4719
+ const maxContinuations =
4720
+ typeof opts.maxContinuations === "number" && opts.maxContinuations > 0
4721
+ ? opts.maxContinuations
4722
+ : MAIN_CHAT_INTERNAL_CONTINUATION_LIMIT;
4723
+
4684
4724
  const localTurnEvents: AgentChatEvent[] = [];
4685
4725
  let lastAttemptWasUnfinishedContinuation = false;
4686
4726
  for (
4687
4727
  let attempt = 0;
4688
- !opts.signal.aborted && attempt < MAIN_CHAT_INTERNAL_CONTINUATION_LIMIT;
4728
+ !opts.signal.aborted && attempt < maxContinuations;
4689
4729
  attempt++
4690
4730
  ) {
4691
4731
  lastAttemptWasUnfinishedContinuation = false;
@@ -4703,8 +4743,23 @@ export async function runAgentLoopWithMainChatInternalContinuations(
4703
4743
  opts.send(event);
4704
4744
  };
4705
4745
 
4706
- const nextUsage = await runAgentLoop({ ...opts, send });
4707
- addUsage(nextUsage);
4746
+ try {
4747
+ const nextUsage = await runAgentLoop({ ...opts, send });
4748
+ addUsage(nextUsage);
4749
+ } catch (err) {
4750
+ // Preserve exact prior behavior unless explicitly opted in: an aborted
4751
+ // signal (real soft-timeout / user stop) or a non-resumable error is
4752
+ // always rethrown, and when `resumeResumableErrorsInProcess` is not
4753
+ // set, EVERY thrown error is rethrown exactly as before.
4754
+ if (
4755
+ !resumeResumableErrorsInProcess ||
4756
+ opts.signal.aborted ||
4757
+ !isResumableEngineError(err)
4758
+ ) {
4759
+ throw err;
4760
+ }
4761
+ continuationReason = continuationReasonForResumableError(err);
4762
+ }
4708
4763
 
4709
4764
  if (!continuationReason || opts.signal.aborted) {
4710
4765
  return usage;
@@ -6525,10 +6580,25 @@ export function createProductionAgentHandler(
6525
6580
  // chaining via the regular function — the Netlify
6526
6581
  // `-background` function is only emitted into the deploy
6527
6582
  // output when the durable flag is on.
6583
+ //
6584
+ // `&& !runsInBackgroundFunction`: a worker PROVEN to already
6585
+ // be executing inside the real `-background` function must
6586
+ // never dispatch back to that same function's own URL — a
6587
+ // background function invoking itself by URL from inside a
6588
+ // live invocation is a documented Netlify platform
6589
+ // limitation (404), unlike the initial foreground→background
6590
+ // dispatch or a worker that landed on the regular function
6591
+ // (a genuinely different function calling the background
6592
+ // one, which works). This only changes the dispatch TARGET
6593
+ // for that one proven-in-bg-function case — by this point
6594
+ // (with the in-process resumable-error resume above) it is
6595
+ // reached only when the chunk genuinely exhausted its
6596
+ // budget, so falling back to the regular `_process-run`
6597
+ // function (40s clamp) here is the correct, safe target.
6528
6598
  chainViaDurableBackground:
6529
6599
  isAgentChatDurableBackgroundEnabled({
6530
6600
  appOptIn: options.durableBackgroundRuns === true,
6531
- }),
6601
+ }) && !runsInBackgroundFunction,
6532
6602
  });
6533
6603
  }
6534
6604
  } finally {
@@ -6946,6 +7016,26 @@ export function createProductionAgentHandler(
6946
7016
  .slice(0, 200),
6947
7017
  }
6948
7018
  : {}),
7019
+ // A worker PROVEN to be running inside the real 15-min Netlify
7020
+ // `-background` function (`runsInBackgroundFunction`) has minutes of
7021
+ // budget left on THIS invocation. Let it catch a recoverable
7022
+ // transport/gateway error (isResumableEngineError) and resume the
7023
+ // agent loop in-process instead of unwinding out to `startRun`'s
7024
+ // outer catch, which would otherwise hand off to
7025
+ // `chainServerDrivenContinuation` — and for a worker already inside
7026
+ // the background function, that hand-off is a same-function
7027
+ // self-dispatch that 404s on Netlify (a background function cannot
7028
+ // invoke its own public URL from inside a live invocation). A
7029
+ // foreground turn or a worker that landed on the regular ~60s
7030
+ // function (runsInBackgroundFunction false) must NOT set this — it
7031
+ // keeps depending on the existing cross-invocation recovery
7032
+ // (foreground self-chain / client auto_continue re-POST), unchanged.
7033
+ ...(runsInBackgroundFunction
7034
+ ? {
7035
+ resumeResumableErrorsInProcess: true,
7036
+ maxContinuations: MAX_BACKGROUND_RUN_CONTINUATIONS,
7037
+ }
7038
+ : {}),
6949
7039
  };
6950
7040
 
6951
7041
  send({ type: "activity", label: "Contacting model" });
@@ -674,9 +674,34 @@ function toolCallFingerprintFromContentPart(part: unknown): string | null {
674
674
  return `${name}\u0000${argsText}`;
675
675
  }
676
676
 
677
+ function toolCallNameFromContentPart(part: unknown): string | null {
678
+ if (!part || typeof part !== "object") return null;
679
+ const candidate = part as { type?: unknown; toolName?: unknown };
680
+ if (candidate.type !== "tool-call") return null;
681
+ return typeof candidate.toolName === "string" && candidate.toolName
682
+ ? candidate.toolName
683
+ : null;
684
+ }
685
+
686
+ function toolCallIdsRepresentSameLocalCall(
687
+ renderedId: string,
688
+ reconnectId: string,
689
+ ): boolean {
690
+ return renderedId === reconnectId || renderedId.endsWith(`:${reconnectId}`);
691
+ }
692
+
677
693
  function collectRenderedToolCallStates(messages: readonly unknown[]): {
678
694
  byId: Map<string, { rank: number }>;
679
695
  latestAssistantByFingerprint: Map<string, { rank: number }>;
696
+ latestAssistantByName: Map<
697
+ string,
698
+ {
699
+ rank: number;
700
+ ids: Set<string>;
701
+ pendingIds: Set<string>;
702
+ pendingRank: number;
703
+ }
704
+ >;
680
705
  } {
681
706
  const byId = new Map<string, { rank: number }>();
682
707
  for (const message of messages) {
@@ -695,22 +720,50 @@ function collectRenderedToolCallStates(messages: readonly unknown[]): {
695
720
  }
696
721
 
697
722
  const latestAssistantByFingerprint = new Map<string, { rank: number }>();
723
+ const latestAssistantByName = new Map<
724
+ string,
725
+ {
726
+ rank: number;
727
+ ids: Set<string>;
728
+ pendingIds: Set<string>;
729
+ pendingRank: number;
730
+ }
731
+ >();
698
732
  const latestEntry = messages.at(-1);
699
733
  const latestMessage = getRepoMessage(latestEntry as any);
700
734
  const latestContent = latestMessage?.content;
701
735
  if (latestMessage?.role === "assistant" && Array.isArray(latestContent)) {
702
736
  for (const part of latestContent) {
703
- const fingerprint = toolCallFingerprintFromContentPart(part);
704
- if (!fingerprint) continue;
705
737
  const rank = toolCallProgressRank(part);
706
- const existing = latestAssistantByFingerprint.get(fingerprint);
707
- latestAssistantByFingerprint.set(fingerprint, {
708
- rank: Math.max(existing?.rank ?? 0, rank),
709
- });
738
+ const fingerprint = toolCallFingerprintFromContentPart(part);
739
+ if (fingerprint) {
740
+ const existing = latestAssistantByFingerprint.get(fingerprint);
741
+ latestAssistantByFingerprint.set(fingerprint, {
742
+ rank: Math.max(existing?.rank ?? 0, rank),
743
+ });
744
+ }
745
+ const name = toolCallNameFromContentPart(part);
746
+ if (name) {
747
+ const existing = latestAssistantByName.get(name);
748
+ const id = toolCallIdFromContentPart(part);
749
+ const ids = new Set(existing?.ids);
750
+ const pendingIds = new Set(existing?.pendingIds);
751
+ if (id) ids.add(id);
752
+ if (id && rank < 4) pendingIds.add(id);
753
+ latestAssistantByName.set(name, {
754
+ rank: Math.max(existing?.rank ?? 0, rank),
755
+ ids,
756
+ pendingIds,
757
+ pendingRank:
758
+ rank < 4
759
+ ? Math.max(existing?.pendingRank ?? 0, rank)
760
+ : (existing?.pendingRank ?? 0),
761
+ });
762
+ }
710
763
  }
711
764
  }
712
765
 
713
- return { byId, latestAssistantByFingerprint };
766
+ return { byId, latestAssistantByFingerprint, latestAssistantByName };
714
767
  }
715
768
 
716
769
  function assistantTextFromContent(content: unknown): string {
@@ -798,29 +851,37 @@ function trimReconnectTextAlreadyRendered(
798
851
  export function dedupeReconnectContentAgainstMessages(
799
852
  content: ContentPart[],
800
853
  messages: readonly unknown[],
854
+ options?: { suppressToolRepeats?: boolean },
801
855
  ): ContentPart[] {
802
856
  if (content.length === 0) return content;
803
857
  const snapshotDeduped = dedupePendingToolCallReplaysWithinContent(content);
804
858
  let changed = snapshotDeduped !== content;
805
859
  if (messages.length === 0) return changed ? snapshotDeduped : content;
806
- const { byId, latestAssistantByFingerprint } =
860
+ const { byId, latestAssistantByFingerprint, latestAssistantByName } =
807
861
  collectRenderedToolCallStates(messages);
808
862
  const renderedAssistantText = latestRenderedAssistantText(messages);
809
863
  if (
810
864
  byId.size === 0 &&
811
865
  latestAssistantByFingerprint.size === 0 &&
866
+ latestAssistantByName.size === 0 &&
812
867
  !renderedAssistantText
813
868
  ) {
814
869
  return changed ? snapshotDeduped : content;
815
870
  }
816
871
 
817
872
  const filtered =
818
- byId.size > 0 || latestAssistantByFingerprint.size > 0
873
+ byId.size > 0 ||
874
+ latestAssistantByFingerprint.size > 0 ||
875
+ latestAssistantByName.size > 0
819
876
  ? snapshotDeduped.filter((part) => {
820
877
  const reconnectRank = toolCallProgressRank(part);
821
878
  const id = toolCallIdFromContentPart(part);
822
879
  const existing = id ? byId.get(id) : undefined;
823
880
  if (existing) {
881
+ if (options?.suppressToolRepeats) {
882
+ changed = true;
883
+ return false;
884
+ }
824
885
  // Keep reconnect copies that are strictly ahead of the rendered
825
886
  // message (e.g. completed overlay vs lagging pending thread data).
826
887
  // Same-or-behind ranks are duplicates and should stay hidden.
@@ -842,14 +903,60 @@ export function dedupeReconnectContentAgainstMessages(
842
903
  : undefined;
843
904
  const isCompletedRepeat =
844
905
  reconnectRank >= 4 && latestByFingerprint?.rank === 4;
906
+ const keepCompletedRepeat =
907
+ isCompletedRepeat && !options?.suppressToolRepeats;
908
+ if (latestByFingerprint && options?.suppressToolRepeats) {
909
+ changed = true;
910
+ return false;
911
+ }
845
912
  if (
846
913
  latestByFingerprint &&
847
- !isCompletedRepeat &&
914
+ !keepCompletedRepeat &&
848
915
  reconnectRank <= latestByFingerprint.rank
849
916
  ) {
850
917
  changed = true;
851
918
  return false;
852
919
  }
920
+ // Activity / arg-less fallback. A reconnect spinner (activity===true)
921
+ // or a pending tool whose args have not materialized yet has no
922
+ // fingerprint, and its reader-local id (`tc_N`) never matches the
923
+ // server-scoped id (`${runId}:tc_N`) rendered in the message, so it
924
+ // slips past BOTH checks above and paints a second card beside the
925
+ // live one ("one spinning, one static"). Suppress it only when the
926
+ // latest assistant message renders the same tool that is ITSELF still
927
+ // pending (rank < 4) at an equal-or-greater rank: a spinner alongside
928
+ // a live pending card is the same in-flight call seen by two readers.
929
+ // If the rendered same-name tool is already completed, a fresh spinner
930
+ // is more likely a genuinely repeated call, so it must stay visible
931
+ // (an empty-args fingerprint would over-match — see the completed
932
+ // repeat cases below).
933
+ if (!fingerprint) {
934
+ const name = toolCallNameFromContentPart(part);
935
+ const latestByName = name
936
+ ? latestAssistantByName.get(name)
937
+ : undefined;
938
+ const matchesRenderedLocalId =
939
+ id && latestByName
940
+ ? Array.from(latestByName.ids).some((renderedId) =>
941
+ toolCallIdsRepresentSameLocalCall(renderedId, id),
942
+ )
943
+ : false;
944
+ const matchesRenderedPendingLocalId =
945
+ id && latestByName
946
+ ? Array.from(latestByName.pendingIds).some((renderedId) =>
947
+ toolCallIdsRepresentSameLocalCall(renderedId, id),
948
+ )
949
+ : false;
950
+ if (
951
+ latestByName &&
952
+ matchesRenderedLocalId &&
953
+ matchesRenderedPendingLocalId &&
954
+ reconnectRank <= latestByName.pendingRank
955
+ ) {
956
+ changed = true;
957
+ return false;
958
+ }
959
+ }
853
960
  return true;
854
961
  })
855
962
  : snapshotDeduped;
@@ -3328,8 +3435,9 @@ const AssistantChatInner = forwardRef<
3328
3435
  return;
3329
3436
  }
3330
3437
  const stillNeeded =
3331
- dedupeReconnectContentAgainstMessages(reconnectContent, messages).length >
3332
- 0;
3438
+ dedupeReconnectContentAgainstMessages(reconnectContent, messages, {
3439
+ suppressToolRepeats: true,
3440
+ }).length > 0;
3333
3441
  if (!stillNeeded) {
3334
3442
  setReconnectContent([]);
3335
3443
  setAdapterHandoffPending(false);
@@ -3888,9 +3996,13 @@ const AssistantChatInner = forwardRef<
3888
3996
  ],
3889
3997
  );
3890
3998
 
3891
- const visibleReconnectContent = useMemo(
3892
- () => dedupeReconnectContentAgainstMessages(reconnectContent, messages),
3893
- [messages, reconnectContent],
3999
+ // Do not memoize this on `messages` identity. assistant-ui can update the
4000
+ // live assistant message content in place while streaming, and the reconnect
4001
+ // overlay must hide as soon as that live message has caught up.
4002
+ const visibleReconnectContent = dedupeReconnectContentAgainstMessages(
4003
+ reconnectContent,
4004
+ messages,
4005
+ { suppressToolRepeats: adapterHandoffPending },
3894
4006
  );
3895
4007
  const chatScrollResetKey = `${tabId ?? ""}:${threadId ?? ""}`;
3896
4008
 
@@ -157,6 +157,12 @@ function inferToolTextLanguage(
157
157
  ): string {
158
158
  const keyName = (key ?? "").toLowerCase();
159
159
  const tool = (toolName ?? "").toLowerCase();
160
+ if (
161
+ keyName === "code" &&
162
+ (tool.includes("run-code") || tool.includes("run_code"))
163
+ ) {
164
+ return "javascript";
165
+ }
160
166
  if (
161
167
  keyName === "sql" ||
162
168
  keyName.endsWith("sql") ||
@@ -352,7 +358,7 @@ function ToolOutputPopover({
352
358
  side="bottom"
353
359
  sideOffset={6}
354
360
  collisionPadding={12}
355
- className="flex w-[min(calc(100vw-2rem),760px)] flex-col gap-0 overflow-hidden p-0"
361
+ className="flex max-h-[min(calc(100vh-2rem),var(--radix-popover-content-available-height,75vh))] w-[min(calc(100vw-2rem),var(--radix-popover-content-available-width,760px),760px)] flex-col gap-0 overflow-hidden p-0"
356
362
  >
357
363
  <div className="flex shrink-0 items-center justify-between gap-3 border-b border-border px-4 py-3">
358
364
  <div className="truncate text-sm font-medium">{title}</div>
@@ -365,11 +371,11 @@ function ToolOutputPopover({
365
371
  {copied ? "Copied" : "Copy"}
366
372
  </button>
367
373
  </div>
368
- <div className="p-3">
374
+ <div className="min-h-0 flex-1 overflow-hidden p-3">
369
375
  <SimpleCodeViewer
370
376
  text={payload.text}
371
377
  lang={payload.lang}
372
- maxHeightClass="max-h-[75vh]"
378
+ maxHeightClass="max-h-[min(70vh,calc(var(--radix-popover-content-available-height,75vh)-4.5rem))]"
373
379
  />
374
380
  </div>
375
381
  </PopoverContent>