@agent-native/core 0.73.0 → 0.74.0

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 (49) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +39 -0
  3. package/corpus/core/package.json +1 -1
  4. package/corpus/core/src/agent/durable-background.ts +15 -5
  5. package/corpus/core/src/agent/production-agent.ts +55 -8
  6. package/corpus/core/src/client/ConnectBuilderCard.tsx +7 -7
  7. package/corpus/core/src/client/NewWorkspaceAppFlow.tsx +1 -1
  8. package/corpus/core/src/client/blocks/library/AnnotatedCodeBlock.tsx +33 -18
  9. package/corpus/core/src/client/blocks/library/annotation-rail.tsx +3 -4
  10. package/corpus/core/src/client/components/CodeRequiredDialog.tsx +5 -8
  11. package/corpus/core/src/server/agent-chat-plugin.ts +7 -7
  12. package/corpus/core/src/server/self-dispatch.ts +8 -1
  13. package/corpus/templates/analytics/actions/compose-dashboard.ts +34 -0
  14. package/corpus/templates/analytics/actions/install-dashboard-template.ts +51 -3
  15. package/corpus/templates/analytics/app/components/dashboard/SqlChart.tsx +31 -8
  16. package/corpus/templates/analytics/changelog/2026-06-24-dashboard-filters-now-apply-consistently-to-first-party-traf.md +6 -0
  17. package/corpus/templates/analytics/seeds/dashboards/agent-native-templates-first-party.json +97 -28
  18. package/corpus/templates/analytics/server/lib/first-party-metric-catalog.ts +57 -10
  19. package/corpus/templates/clips/app/components/library/library-layout.tsx +7 -0
  20. package/corpus/templates/clips/app/routes/_app.settings._index.tsx +786 -323
  21. package/corpus/templates/clips/changelog/2026-06-24-settings-now-puts-what-s-new-in-a-compact-sidebar-prioritize.md +6 -0
  22. package/corpus/templates/plan/changelog/2026-06-24-annotated-code-callouts-now-use-numbered-gutter-markers-that.md +6 -0
  23. package/dist/agent/durable-background.d.ts.map +1 -1
  24. package/dist/agent/durable-background.js +15 -5
  25. package/dist/agent/durable-background.js.map +1 -1
  26. package/dist/agent/production-agent.d.ts.map +1 -1
  27. package/dist/agent/production-agent.js +52 -8
  28. package/dist/agent/production-agent.js.map +1 -1
  29. package/dist/client/ConnectBuilderCard.d.ts.map +1 -1
  30. package/dist/client/ConnectBuilderCard.js +6 -6
  31. package/dist/client/ConnectBuilderCard.js.map +1 -1
  32. package/dist/client/NewWorkspaceAppFlow.js +1 -1
  33. package/dist/client/NewWorkspaceAppFlow.js.map +1 -1
  34. package/dist/client/blocks/library/AnnotatedCodeBlock.d.ts.map +1 -1
  35. package/dist/client/blocks/library/AnnotatedCodeBlock.js +14 -10
  36. package/dist/client/blocks/library/AnnotatedCodeBlock.js.map +1 -1
  37. package/dist/client/blocks/library/annotation-rail.d.ts +3 -4
  38. package/dist/client/blocks/library/annotation-rail.d.ts.map +1 -1
  39. package/dist/client/blocks/library/annotation-rail.js +3 -4
  40. package/dist/client/blocks/library/annotation-rail.js.map +1 -1
  41. package/dist/client/components/CodeRequiredDialog.d.ts.map +1 -1
  42. package/dist/client/components/CodeRequiredDialog.js +2 -1
  43. package/dist/client/components/CodeRequiredDialog.js.map +1 -1
  44. package/dist/server/agent-chat-plugin.js +7 -7
  45. package/dist/server/agent-chat-plugin.js.map +1 -1
  46. package/dist/server/self-dispatch.d.ts.map +1 -1
  47. package/dist/server/self-dispatch.js +5 -1
  48. package/dist/server/self-dispatch.js.map +1 -1
  49. 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: 1160
31
- - template files: 4022
31
+ - template files: 4025
@@ -1,5 +1,44 @@
1
1
  # @agent-native/core
2
2
 
3
+ ## 0.74.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 4a0d3c4: Durable background agent runs are robust again, and **on by default** for hosted apps.
8
+ - **Graceful inline fallback (the safety fix).** When the foreground turn can't
9
+ hand off to a background worker — the HMAC self-dispatch self-POST fails fast,
10
+ e.g. a connection error or a non-2xx returned within the settle window — the
11
+ agent-chat handler no longer breaks the chat with
12
+ `Failed to dispatch background run`. It now degrades to a normal synchronous
13
+ (inline) run, reusing the already-inserted run row. The run is claimed
14
+ atomically (`claimBackgroundRun`, a conditional `dispatch_mode: background →
15
+ background-processing` UPDATE) before running inline, so the SQL atomic claim
16
+ is the single owner — a delayed background delivery that arrives afterward
17
+ loses the claim and no-ops, and the run can never double-execute. A dispatch
18
+ that _did_ land (so a worker already owns the run) still streams the worker's
19
+ events instead of running a second copy.
20
+ - **Default-on, safely.** `AGENT_CHAT_DURABLE_BACKGROUND` is now opt-out for
21
+ hosted apps: unset/empty/unknown counts as enabled; opt a specific app back
22
+ out with an explicit falsy value (`false`/`0`/`no`/`off`). The gate still
23
+ composes with the existing guards, so a run only goes durable when the runtime
24
+ is hosted/serverless **and** `A2A_SECRET` is configured — local dev and
25
+ unconfigured apps stay on the synchronous inline path unchanged. Default-on is
26
+ safe precisely because a failed dispatch degrades to a working inline run. The
27
+ Netlify 15-min `-background` function emit (`isDurableBackgroundDeployEnabled`)
28
+ remains opt-in until its path is separately verified; with it off, the
29
+ default-on baseline runs the worker through the standard function and
30
+ server-chains continuations.
31
+ - **More diagnosable dispatch errors.** Self-dispatch failures now log the
32
+ resolved base URL so a failure tied to which host the self-POST targets
33
+ (custom domain vs deploy URL) is visible in logs. The URL resolution order is
34
+ unchanged (it matches the working A2A/agent-teams self-dispatch paths).
35
+
36
+ ### Patch Changes
37
+
38
+ - 4a0d3c4: Show numbered annotation markers in the annotated-code gutter, matching the diff
39
+ annotation affordance while preserving the hover popover behavior.
40
+ - 4a0d3c4: Clarify Builder code-change handoff fallbacks when cloud agents are unavailable.
41
+
3
42
  ## 0.73.0
4
43
 
5
44
  ### Minor Changes
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-native/core",
3
- "version": "0.73.0",
3
+ "version": "0.74.0",
4
4
  "type": "module",
5
5
  "engines": {
6
6
  "node": ">=22"
@@ -19,12 +19,20 @@
19
19
  * agent-chat handler must behave byte-for-byte like the current synchronous
20
20
  * path. The gate is true only when ALL of these hold:
21
21
  * 1. `AGENT_CHAT_DURABLE_BACKGROUND` env is not explicitly disabled. It is
22
- * DEFAULT-ON: unset/empty counts as enabled; set it to a falsy value
23
- * (`false`/`0`/`no`/`off`) to opt a specific app back out.
22
+ * DEFAULT-ON: unset/empty/unknown counts as enabled; set it to a falsy
23
+ * value (`false`/`0`/`no`/`off`) to opt a specific app back out.
24
24
  * 2. The runtime is hosted/serverless (local dev keeps the inline path so SSE
25
25
  * stays a single live stream and no second function is needed).
26
26
  * 3. `A2A_SECRET` is configured (the HMAC handoff is required to authenticate
27
27
  * the background dispatch; without it the dispatch can't be trusted).
28
+ *
29
+ * Default-on is safe because a *dispatch failure degrades to an inline run*: if
30
+ * the self-dispatch self-POST can't be delivered (fast connection error or
31
+ * fast non-2xx), the foreground handler runs the turn synchronously instead of
32
+ * erroring (see `production-agent.ts` — the inline fallback claims the run row
33
+ * atomically so a delayed delivery can never double-execute). So an app where
34
+ * durable dispatch happens to fail still gets a working chat, just without the
35
+ * 15-min budget.
28
36
  */
29
37
  import {
30
38
  hasConfiguredA2ASecret,
@@ -94,10 +102,12 @@ function isFlagEnabled(): boolean {
94
102
  // in sync with AGENT_CHAT_DURABLE_BACKGROUND_ENV.
95
103
  //
96
104
  // DEFAULT-ON: durable background runs are the desired behavior for every
97
- // hosted app (verified in prod long multi-step runs complete past the 40s
98
- // soft-timeout with no thrash). So an unset flag means ON; an app opts OUT
99
- // only with an explicit falsy value. This still composes with the hosted +
105
+ // hosted app. So an unset/empty/unknown flag means ON; an app opts OUT only
106
+ // with an explicit falsy value. This still composes with the hosted +
100
107
  // A2A_SECRET gates below, so non-hosted / unconfigured apps stay synchronous.
108
+ // Safety net: a failed dispatch degrades to a synchronous inline run (see
109
+ // production-agent.ts), so default-on cannot break chat even if the
110
+ // self-dispatch can't be delivered on a given app.
101
111
  const raw = process.env.AGENT_CHAT_DURABLE_BACKGROUND;
102
112
  if (raw == null) return true;
103
113
  const normalized = raw.trim().toLowerCase();
@@ -4363,6 +4363,7 @@ export function createProductionAgentHandler(
4363
4363
  // change. With the flag OFF this whole branch is skipped and the inline
4364
4364
  // `startRun` path below runs exactly as before (byte-for-byte).
4365
4365
  if (dispatchToBackground) {
4366
+ let backgroundRowInserted = false;
4366
4367
  try {
4367
4368
  // Insert the run row up front so /runs/active sees it immediately and
4368
4369
  // the slot stays held while the background function cold-starts. Mark
@@ -4370,6 +4371,7 @@ export function createProductionAgentHandler(
4370
4371
  await insertRun(runId, effectiveThreadId, effectiveTurnId, {
4371
4372
  dispatchMode: "background",
4372
4373
  });
4374
+ backgroundRowInserted = true;
4373
4375
  } catch (err) {
4374
4376
  // A duplicate-PK collision means the row already exists (ret­ried POST);
4375
4377
  // any other failure means we can't safely hand off — fall back to the
@@ -4418,14 +4420,59 @@ export function createProductionAgentHandler(
4418
4420
  setResponseStatus(event, 500);
4419
4421
  return { error: "Failed to subscribe to background run" };
4420
4422
  }
4421
- // Dispatch failed before any worker could claim the run. Fail loud: flip
4422
- // the row terminal so the held slot is released (and any reconnect sees a
4423
- // terminal status instead of spinning), then 500 so the client can retry.
4424
- // We do NOT silently fall through to inline here that risks a later,
4425
- // delayed background delivery double-executing the same runId.
4426
- await updateRunStatusIfRunning(runId, "errored").catch(() => {});
4427
- setResponseStatus(event, 500);
4428
- return { error: "Failed to dispatch background run" };
4423
+
4424
+ // ─── Dispatch failed degrade to a normal synchronous (inline) run ────
4425
+ // `fireInternalDispatch` throws ONLY when the self-POST failed *fast*
4426
+ // (rejected the connection, or returned a non-2xx within the ~250ms settle
4427
+ // race). The `_process-run` route verifies the HMAC token and validates
4428
+ // the body BEFORE it ever reaches the SQL atomic claim, so a fast throw
4429
+ // means NO background worker claimed this run — there is nothing to
4430
+ // double-execute. Rather than break the chat with "Failed to dispatch
4431
+ // background run", we run the turn inline (the same synchronous path the
4432
+ // flag-off branch below takes), reusing the already-inserted run row.
4433
+ //
4434
+ // Safety against a (very improbable) delayed background delivery: claim the
4435
+ // run atomically here via `claimBackgroundRun`, which flips the row's
4436
+ // dispatch_mode `background → background-processing` in one conditional
4437
+ // UPDATE. If a delayed dispatch DID land and a worker already won the
4438
+ // claim, our claim returns false and we must NOT run inline (that would
4439
+ // double-execute) — fall back to subscribing to the worker's run instead.
4440
+ // The SQL atomic claim is the single source of truth for ownership, so at
4441
+ // most one of {inline fallback, background worker} ever executes this run.
4442
+ if (backgroundRowInserted) {
4443
+ let claimedInline = false;
4444
+ try {
4445
+ claimedInline = await claimBackgroundRun(runId);
4446
+ } catch (err) {
4447
+ console.error(
4448
+ "[agent-chat] inline-fallback claim failed:",
4449
+ err instanceof Error ? err.message : err,
4450
+ );
4451
+ }
4452
+ if (!claimedInline) {
4453
+ // A background worker already owns this run (a delayed delivery landed
4454
+ // after our fast throw). Stream its events instead of running a second
4455
+ // copy. If we somehow can't subscribe, surface an error rather than
4456
+ // risk a double-run.
4457
+ const stream = subscribeToRun(runId, 0);
4458
+ if (stream) {
4459
+ setResponseHeader(event, "Content-Type", "text/event-stream");
4460
+ setResponseHeader(event, "Cache-Control", "no-cache");
4461
+ setResponseHeader(event, "Connection", "keep-alive");
4462
+ setResponseHeader(event, "X-Run-Id", runId);
4463
+ return stream;
4464
+ }
4465
+ await updateRunStatusIfRunning(runId, "errored").catch(() => {});
4466
+ setResponseStatus(event, 500);
4467
+ return { error: "Failed to dispatch background run" };
4468
+ }
4469
+ // We own the run. `startRun` (below) calls `insertRun` again, but its
4470
+ // duplicate-PK collision is swallowed (`insertRun(...).catch(() => {})`),
4471
+ // so the existing `background-processing` row is reused — no double row.
4472
+ }
4473
+ // Fall through to the inline `startRun` path below (the same one the
4474
+ // flag-off branch uses). If the row was never inserted, `startRun` inserts
4475
+ // it fresh; if it was, the claim above made us the sole owner.
4429
4476
  }
4430
4477
 
4431
4478
  const trackedProgressOwner =
@@ -7,6 +7,9 @@ import { cn } from "./utils.js";
7
7
  import { agentNativePath } from "./api-path.js";
8
8
 
9
9
  const DESKTOP_DOWNLOAD_URL = "https://www.agent-native.com/download";
10
+ const CODE_CHANGE_FALLBACK_DETAIL =
11
+ "Edit locally or use Builder.io to edit this code in the cloud and continue customizing the app any way you like.";
12
+ const CODE_CHANGE_FALLBACK_TEXT = `This requires a code change. ${CODE_CHANGE_FALLBACK_DETAIL}`;
10
13
 
11
14
  function isLocalBrowserOutsideDesktop() {
12
15
  if (typeof window === "undefined" || typeof navigator === "undefined") {
@@ -200,7 +203,7 @@ export function ConnectBuilderCard({
200
203
  // so the render tree below stays flat.
201
204
  const connectedCapabilityText = builderEnabled
202
205
  ? "AI credits and cloud code changes are ready to use."
203
- : "AI credits are ready to use. Builder Cloud Agents for code changes are not available for this workspace yet.";
206
+ : `AI credits are ready to use. ${CODE_CHANGE_FALLBACK_TEXT}`;
204
207
  let title: string;
205
208
  let subtitle: React.ReactNode;
206
209
  if (runResult) {
@@ -215,20 +218,17 @@ export function ConnectBuilderCard({
215
218
  </>
216
219
  );
217
220
  } else if (showWaitlist) {
218
- title = waitlistJoined
219
- ? "You're on the waitlist"
220
- : "Builder Cloud Agents coming soon";
221
+ title = "This requires a code change";
221
222
  subtitle = waitlistJoined ? (
222
223
  <>
223
- We'll let you know when Builder Cloud Agents are available for this
224
- workspace.{" "}
224
+ You're on the waitlist. {CODE_CHANGE_FALLBACK_DETAIL}{" "}
225
225
  {localBrowser
226
226
  ? "Since this project is already running locally, open it in the desktop app for local coding tools or keep editing from your clone."
227
227
  : "You can still clone the project locally and use the desktop app for code changes."}
228
228
  </>
229
229
  ) : (
230
230
  <>
231
- You don't have access to Builder Cloud Agents for this workspace yet.{" "}
231
+ {CODE_CHANGE_FALLBACK_DETAIL}{" "}
232
232
  {localBrowser
233
233
  ? "Since this project is already running locally, open it in the desktop app for local coding tools or keep editing from your clone."
234
234
  : "You can still clone the project locally and use the desktop app for code changes."}
@@ -294,7 +294,7 @@ export function NewWorkspaceAppFlow({
294
294
  } else {
295
295
  setStatusMessage(
296
296
  result?.message ||
297
- "Builder app creation is coming soon here. Open this workspace in Builder to create an app from this prompt.",
297
+ "This requires a code change. Edit locally or use Builder.io to edit this code in the cloud and continue customizing the app any way you like.",
298
298
  );
299
299
  }
300
300
  }
@@ -16,6 +16,7 @@ import {
16
16
  AnnotationHiddenStack,
17
17
  AnnotationHoverCard,
18
18
  AnnotationInlineOverlayStack,
19
+ AnnotationGutterMarker,
19
20
  anchorFromElements,
20
21
  buildLineMarkerMap,
21
22
  hasRailAnnotations,
@@ -42,9 +43,9 @@ import { DevInput, DevLabel, DevTextarea } from "./dev-doc-ui.js";
42
43
  * pairs, so it reads correctly in BOTH light and dark mode. Code lines render as
43
44
  * `<span>`s (never one `<pre>` per line) so they don't pick up document
44
45
  * code/pre chrome. Lives in core so any app can register the dev-doc block.
45
- * Each annotated range also gets a small sticky glowing indicator at the top
46
- * right of its first line, making the hover affordance visible without a note
47
- * column.
46
+ * Each annotated range also gets a numbered marker in the left gutter on the
47
+ * first line, matching the diff block's annotation affordance without adding a
48
+ * persistent note column.
48
49
  *
49
50
  * Editing is panel-driven (config-style, like the diff/HTML blocks): a monospace
50
51
  * code Textarea, filename/language Inputs, and add/remove-able annotation rows.
@@ -283,6 +284,7 @@ function AnnotatedCodeRead({
283
284
  );
284
285
  const rangeStartMarkers =
285
286
  markers?.filter((item) => item.range?.start === lineNo) ?? [];
287
+ const showMarkerColumn = hasAnnotations;
286
288
 
287
289
  const buildAnchorForItem = (
288
290
  item: ResolvedAnnotation<AnnotatedCodeAnnotation>,
@@ -373,14 +375,13 @@ function AnnotatedCodeRead({
373
375
  <span className="w-11 shrink-0 select-none px-3 text-right text-[11px] tabular-nums text-plan-muted/60">
374
376
  {lineNo}
375
377
  </span>
376
- <span className="flex-1 whitespace-pre pr-4 text-plan-code-text">
377
- {highlightedLines[lineNo - 1]}
378
- </span>
379
- {rangeStartMarkers.length > 0 && (
378
+ {showMarkerColumn && (
380
379
  <span
381
380
  aria-hidden
382
- className="sticky right-0.5 z-10 ml-2 flex h-[22px] shrink-0 items-center gap-1 self-start pr-0.5"
383
- data-annotated-code-marker-stack
381
+ className="flex w-6 shrink-0 select-none items-center justify-center gap-1 py-0"
382
+ data-annotated-code-marker-stack={
383
+ rangeStartMarkers.length > 0 ? "" : undefined
384
+ }
384
385
  >
385
386
  {rangeStartMarkers.map((item) => (
386
387
  <span
@@ -394,24 +395,35 @@ function AnnotatedCodeRead({
394
395
  ) ?? event.currentTarget;
395
396
  openAnnotation(item, row);
396
397
  }}
397
- className="group inline-flex size-3 cursor-pointer items-center justify-center rounded-full outline-none transition-transform hover:scale-110"
398
+ onClick={(event) => {
399
+ event.stopPropagation();
400
+ const row =
401
+ event.currentTarget.closest<HTMLElement>(
402
+ "[data-code-line]",
403
+ ) ?? event.currentTarget;
404
+ openAnnotation(item, row);
405
+ }}
406
+ className="inline-flex cursor-pointer outline-none transition-transform hover:scale-110"
398
407
  >
399
- <span
400
- className={cn(
401
- "block rounded-full ring-1 transition-all duration-150",
402
- activeIndex === item.index
403
- ? "size-[6px] bg-yellow-300 ring-yellow-200 shadow-[0_0_0_3px_rgba(250,204,21,0.30),0_0_16px_rgba(250,204,21,0.92)] dark:bg-yellow-200 dark:ring-yellow-100/70 dark:shadow-[0_0_0_3px_rgba(253,224,71,0.26),0_0_16px_rgba(253,224,71,0.76)]"
404
- : "size-1 bg-yellow-300/95 ring-yellow-200/65 shadow-[0_0_0_2px_rgba(250,204,21,0.18),0_0_11px_rgba(250,204,21,0.56)] group-hover:size-[6px] group-hover:bg-yellow-300 group-hover:ring-yellow-200 group-hover:shadow-[0_0_0_3px_rgba(250,204,21,0.30),0_0_16px_rgba(250,204,21,0.90)] dark:bg-yellow-200/78 dark:ring-yellow-100/38 dark:shadow-[0_0_0_2px_rgba(253,224,71,0.16),0_0_11px_rgba(253,224,71,0.46)] dark:group-hover:bg-yellow-200 dark:group-hover:ring-yellow-100/68 dark:group-hover:shadow-[0_0_0_3px_rgba(253,224,71,0.26),0_0_16px_rgba(253,224,71,0.74)]",
405
- )}
408
+ <AnnotationGutterMarker
409
+ marker={item.marker}
410
+ active={
411
+ activeIndex === item.index ||
412
+ persistentAnnotationIndexes.has(item.index)
413
+ }
406
414
  />
407
415
  </span>
408
416
  ))}
409
417
  </span>
410
418
  )}
419
+ <span className="flex-1 whitespace-pre pr-4 text-plan-code-text">
420
+ {highlightedLines[lineNo - 1]}
421
+ </span>
411
422
  {overlayItems.length > 0 && (
412
423
  <AnnotationInlineOverlayStack
413
424
  items={overlayItems}
414
425
  ctx={ctx}
426
+ showMarker
415
427
  containerRef={codeRef}
416
428
  mode={showAnnotationOverlays ? "capture" : "margin"}
417
429
  side={showAnnotationOverlays ? "right" : annotationMarginSide}
@@ -503,7 +515,9 @@ function AnnotatedCodeRead({
503
515
  live in a visually-hidden stack (a11y + tests) and surface ONE at a
504
516
  time as an on-hover popover anchored to the right of the code. */}
505
517
  {codeSurface}
506
- {hasAnnotations && <AnnotationHiddenStack items={resolved} ctx={ctx} />}
518
+ {hasAnnotations && (
519
+ <AnnotationHiddenStack items={resolved} ctx={ctx} showMarker />
520
+ )}
507
521
  {hasAnnotations &&
508
522
  !showAnnotationOverlays &&
509
523
  !activeItemIsPersistentlyVisible &&
@@ -513,6 +527,7 @@ function AnnotatedCodeRead({
513
527
  item={activeItem}
514
528
  anchor={hover.anchor}
515
529
  ctx={ctx}
530
+ showMarker
516
531
  preferredSide={annotationHoverSide}
517
532
  hoverFallbackSide={annotationHoverFallbackSide}
518
533
  onMouseEnter={hover.cancelClose}
@@ -26,11 +26,10 @@ import type { BlockRenderContext } from "../types.js";
26
26
  * into stable, marker-numbered, range-resolved records and a line→markers map.
27
27
  * - `rangeLabel` — the human "Line 8" / "Lines 3–6" label.
28
28
  * - `AnnotationGutterMarker` — the numbered amber pip placed on an annotated row
29
- * (used by the diff grid; the annotated-code surface uses its own rail bar).
29
+ * by both the diff grid and the annotated-code gutter.
30
30
  * - `AnnotationNoteRail` — the responsive list of note cards with two-way hover.
31
- * `showMarker` opts the diff block into a leading numbered pip on each card so
32
- * a note can be matched to its `①`/`②` row marker; annotated-code omits it to
33
- * keep its original card chrome.
31
+ * `showMarker` opts blocks into a leading numbered pip on each card so a note
32
+ * can be matched to its `①`/`②` row marker.
34
33
  *
35
34
  * `AnnotatedCodeBlock` annotates a single code surface; `DiffBlock` annotates a
36
35
  * before/after grid (each annotation also carries a `side`). The shared types
@@ -12,6 +12,8 @@ import { trackEvent } from "../analytics.js";
12
12
  import { withBuilderConnectTrackingParams } from "../settings/useBuilderStatus.js";
13
13
 
14
14
  const DESKTOP_DOWNLOAD_URL = "https://www.agent-native.com/download";
15
+ const CODE_CHANGE_FALLBACK_DETAIL =
16
+ "Edit locally or use Builder.io to edit this code in the cloud and continue customizing the app any way you like.";
15
17
 
16
18
  export interface CodeRequiredDialogProps {
17
19
  open: boolean;
@@ -219,15 +221,10 @@ export function CodeRequiredDialog({
219
221
  <IconExternalLink size={24} />
220
222
  </div>
221
223
  <div style={s.optionText}>
222
- <span style={s.optionTitle}>
223
- Builder Cloud Agents coming soon
224
- </span>
225
- <span style={s.optionDesc}>
226
- You don't have access yet. Use the desktop app or your local
227
- clone for this code change.
228
- </span>
224
+ <span style={s.optionTitle}>This requires a code change</span>
225
+ <span style={s.optionDesc}>{CODE_CHANGE_FALLBACK_DETAIL}</span>
229
226
  </div>
230
- <span style={s.badge}>Coming soon</span>
227
+ <span style={s.badge}>Code change</span>
231
228
  </div>
232
229
  ) : (
233
230
  <a
@@ -2039,7 +2039,7 @@ function createBuilderBrowserTool(deps: {
2039
2039
  const entries: Record<string, ActionEntry> = {
2040
2040
  "connect-builder": {
2041
2041
  tool: {
2042
- description: `Render a Builder.io card inline in the chat. Call this as the first step (no code exploration or planning needed) when the user asks to modify the APP'S OWN SOURCE CODE: add a feature, change the UI chrome, edit a React component, add a route, add an integration, fix a bug in the app itself, or anything else that requires source-file edits while in hosted/production mode. ${extensionRequestGuidance}Do NOT call this for content the app is meant to produce — creating a video, generating a design, drafting an email, building a slide deck, making a dashboard, etc. — those run through the app's own domain actions, not Builder. Do NOT mention 'click Send to Builder' in your response unless this card is already in the conversation. If Builder is connected and Builder Cloud Agents are available, the card shows a 'Send to Builder' button that hands the work off to Builder's cloud agent and returns a branch URL. If \`builderEnabled\` is false, the card shows a waitlist/local-dev fallback instead; never tell the user to enable Builder Cloud Agents in Builder org settings or beta settings, and do not claim the Builder card has everything, is pre-loaded for handoff, or can run the cloud agent. When you call this for a code-change request, pass the user's request verbatim as the \`prompt\` arg so the card can forward it to Builder unchanged when cloud agents are available.`,
2042
+ description: `Render a Builder.io card inline in the chat. Call this as the first step (no code exploration or planning needed) when the user asks to modify the APP'S OWN SOURCE CODE: add a feature, change the UI chrome, edit a React component, add a route, add an integration, fix a bug in the app itself, or anything else that requires source-file edits while in hosted/production mode. ${extensionRequestGuidance}Do NOT call this for content the app is meant to produce — creating a video, generating a design, drafting an email, building a slide deck, making a dashboard, etc. — those run through the app's own domain actions, not Builder. Do NOT mention 'click Send to Builder' in your response unless this card is already in the conversation. The tool result includes \`builderEnabled\`; treat \`true\` as "Builder Cloud Agents can take the code-change handoff" and \`false\` as "this still needs a code change, but no Builder Cloud Agent can run here." If Builder is connected and Builder Cloud Agents are available, the card shows a 'Send to Builder' button that hands the work off to Builder's cloud agent and returns a branch URL. If \`builderEnabled\` is false, the card still renders but shows the code-change fallback: "This requires a code change. Edit locally or use Builder.io to edit this code in the cloud and continue customizing the app any way you like." Never tell the user to enable Builder Cloud Agents in Builder org settings or beta settings, and do not claim the Builder card has everything, is pre-loaded for handoff, or can run the cloud agent when \`builderEnabled\` is false. When you call this for a code-change request, pass the user's request verbatim as the \`prompt\` arg so the card can forward it to Builder unchanged when cloud agents are available.`,
2043
2043
  parameters: {
2044
2044
  type: "object",
2045
2045
  properties: {
@@ -2920,7 +2920,7 @@ Do NOT add this offer for one-shot work: lookups (find Alice, what's the schema,
2920
2920
 
2921
2921
  builder: `### Connecting Builder.io
2922
2922
 
2923
- When the user asks to connect Builder.io or you hit a "Builder not configured" error, call the \`connect-builder\` tool. It renders a one-click Connect card inline — do NOT write out multi-step setup instructions yourself. If Builder Cloud Agents are not available for this workspace, never send the user to Builder org settings or beta settings; use the card's waitlist/local-dev fallback.`,
2923
+ When the user asks to connect Builder.io or you hit a "Builder not configured" error, call the \`connect-builder\` tool. It renders a Connect/code-change card inline — do NOT write out multi-step setup instructions yourself. Inspect the returned \`builderEnabled\` flag: \`true\` means Builder Cloud Agents can take the code-change handoff, while \`false\` means this requires a code change and the user should edit locally or use Builder.io to edit this code in the cloud and continue customizing the app any way they like. If Builder Cloud Agents are not available for this workspace, never send the user to Builder org settings or beta settings.`,
2924
2924
 
2925
2925
  browser: `### Browser Automation
2926
2926
 
@@ -3108,14 +3108,14 @@ ${extensionConnectBuilderGuard}
3108
3108
  In Act mode, when the user asks you to change the UI, modify code, add a feature, fix a bug in the app itself, change styles, add a hook, create a component, add a route, add an integration, or anything else that requires editing source files — you MUST take exactly these steps, in order:
3109
3109
 
3110
3110
  1. Briefly acknowledge the user's specific request in their own terms — one short clause naming what they asked for (e.g. "Got it — wider subject lines in the email list."). Do NOT restate the request verbatim, do NOT add a generic preamble, and do NOT promise outcomes. Skip this step entirely if the user already knows you're handing off (e.g. they said "send this to Builder").
3111
- 2. Call the \`connect-builder\` tool, passing the user's full request verbatim as the \`prompt\` argument. This renders an inline card. If Builder is connected and \`builderEnabled\` is true, the card hands the prompt off to Builder's cloud agent on one click and returns a branch URL. If Builder is not connected, it shows the Connect Builder flow. If \`builderEnabled\` is false, it shows a waitlist/local-dev fallback instead of a cloud handoff.
3112
- 3. After the card renders, inspect the tool result and write one sentence that frames the next click around what the user just asked — not as a Builder pitch. Examples: "Click Send to Builder and it'll wire that up for you." / "Connect Builder once and it'll handle this and future changes." / "Builder Cloud Agents aren't available here yet join the waitlist in the card, or use the Agent Native Desktop app for local code changes: https://www.agent-native.com/download." Do NOT say the card is pre-loaded, has everything, or lead with "Builder Cloud Agents are…" as if it were the headline.
3111
+ 2. Call the \`connect-builder\` tool, passing the user's full request verbatim as the \`prompt\` argument. This renders an inline card. The returned \`builderEnabled\` flag is authoritative: \`true\` means Builder Cloud Agents can take the code-change handoff; \`false\` means this still requires a code change, but no Builder Cloud Agent can run from this workspace. If Builder is connected and \`builderEnabled\` is true, the card hands the prompt off to Builder's cloud agent on one click and returns a branch URL. If Builder is not connected and \`builderEnabled\` is true, it shows the Connect Builder flow. If \`builderEnabled\` is false, it still shows the card, but as a code-change fallback instead of a runnable cloud handoff.
3112
+ 3. After the card renders, inspect the tool result and write one sentence that frames the next click around what the user just asked — not as a Builder pitch. If \`builderEnabled\` is true and the card can send, say something like "Click Send to Builder and it'll wire that up for you." If the user needs to connect first, say something like "Connect Builder once and it'll handle this and future changes." If \`builderEnabled\` is false, say: "This requires a code change. Edit locally or use Builder.io to edit this code in the cloud and continue customizing the app any way you like." Do NOT say "click Send to Builder", mention a branch URL, say the card is pre-loaded, or imply a Builder Cloud Agent can run when \`builderEnabled\` is false.
3113
3113
 
3114
- When \`builderEnabled\` is false, Builder Cloud Agents are not self-serve from Builder org settings. Do NOT tell the user to go to Builder org settings, beta features, "Cloud Agents", or "AI Agents" to enable them. The only allowed guidance is the card's waitlist/local-dev fallback: join the waitlist when the card offers it, use Agent Native Desktop, or edit from a local clone.
3114
+ When \`builderEnabled\` is false, Builder Cloud Agents are not self-serve from Builder org settings. Do NOT tell the user to go to Builder org settings, beta features, "Cloud Agents", or "AI Agents" to enable them. The only allowed guidance is the card's code-change fallback: edit locally, use Agent Native Desktop, or use Builder.io to edit this code in the cloud and continue customizing the app any way you like.
3115
3115
 
3116
3116
  **Act-mode handoff — do exactly this:** The moment a code change is requested, call \`connect-builder\` with the user's verbatim request, then write one sentence framing the next click. That is the whole job: Builder plans and edits in its own sandbox, so the planning, file-mapping, and code all happen there, not here. Keep your turn to the call plus that one sentence — skip code exploration, implementation plans, \`resources\` writes of specs, and sub-agents (they have no code-editing tools either). You don't need filesystem access to recommend Builder, so don't reach for it or list tools you lack.
3117
3117
 
3118
- Builder Cloud Agents are the recommended path when available because they are one-click and run in the cloud. Local dev and the desktop app are the fallback when cloud agent access is waitlisted or unavailable.
3118
+ Builder Cloud Agents are the recommended path when \`builderEnabled\` is true because they are one-click and run in the cloud. Local dev, Agent Native Desktop, and Builder.io cloud editing are the fallback when \`builderEnabled\` is false.
3119
3119
  ${FRAMEWORK_CORE}`;
3120
3120
 
3121
3121
  const DEV_FRAMEWORK_PROMPT = `## Agent-Native Framework — Development Mode
@@ -3157,7 +3157,7 @@ ${extensionInstructionsCompact}
3157
3157
 
3158
3158
  ### Code Changes — Call \`connect-builder\`
3159
3159
 
3160
- In Act mode, when a request genuinely needs a source-code edit (per the rules above), do this in order: (1) briefly acknowledge the user's specific ask in their own terms — one short clause, no preamble — then (2) call \`connect-builder\` with the user's request as the \`prompt\`, then (3) write one sentence framing the next click around what they asked for, not a Builder pitch. Do NOT write code, list files, make plans, or spawn sub-agents. Mention \`npx agent-native create <app-name>\`, Agent Native Desktop, or the waitlist card as the fallback when Builder Cloud Agents aren't available. Never tell users to enable Builder Cloud Agents from Builder org settings, beta features, "Cloud Agents", or "AI Agents"; they are not self-serve there.
3160
+ In Act mode, when a request genuinely needs a source-code edit (per the rules above), do this in order: (1) briefly acknowledge the user's specific ask in their own terms — one short clause, no preamble — then (2) call \`connect-builder\` with the user's request as the \`prompt\`, then (3) inspect \`builderEnabled\` in the result and write one sentence framing the next click around what they asked for, not a Builder pitch. When \`builderEnabled\` is true, Builder Cloud Agents can take the handoff; when \`builderEnabled\` is false, say this requires a code change and they can edit locally or use Builder.io to edit this code in the cloud and continue customizing the app any way they like. Do NOT write code, list files, make plans, or spawn sub-agents. Mention \`npx agent-native create <app-name>\`, Agent Native Desktop, or the code-change fallback when Builder Cloud Agents aren't available. Never tell users to enable Builder Cloud Agents from Builder org settings, beta features, "Cloud Agents", or "AI Agents"; they are not self-serve there.
3161
3161
  ${FRAMEWORK_CORE_COMPACT}`;
3162
3162
 
3163
3163
  const DEV_FRAMEWORK_PROMPT_COMPACT = `## Agent-Native Framework — Development Mode
@@ -150,7 +150,14 @@ export async function fireInternalDispatch(
150
150
  }
151
151
  });
152
152
  dispatchPromise.catch((err) => {
153
- console.error(`[self-dispatch] dispatch to ${options.path} failed:`, err);
153
+ // Include the resolved base URL: a self-dispatch failure is almost always
154
+ // about *which* host we POST to (custom domain behind an edge/auth wall vs
155
+ // the deploy URL), and that is invisible from the error alone. Keeps prod
156
+ // logs diagnostic without changing the URL resolution order.
157
+ console.error(
158
+ `[self-dispatch] dispatch to ${options.path} (base ${baseUrl}) failed:`,
159
+ err,
160
+ );
154
161
  });
155
162
 
156
163
  const settleMs = options.settleMs ?? DEFAULT_DISPATCH_SETTLE_MS;
@@ -7,10 +7,12 @@ import {
7
7
  import { z } from "zod";
8
8
  import { getDashboard, upsertDashboard } from "../server/lib/dashboards-store";
9
9
  import {
10
+ buildFirstPartyDashboardFilters,
10
11
  buildPanel,
11
12
  listMetricKeys,
12
13
  type ComposedPanel,
13
14
  type MetricWindow,
15
+ usesFirstPartyDashboardFilters,
14
16
  } from "../server/lib/first-party-metric-catalog";
15
17
  import { validateFirstPartyAnalyticsSql } from "../server/lib/first-party-analytics.js";
16
18
  import {
@@ -97,6 +99,32 @@ const metricSchema = z.union([
97
99
 
98
100
  const METRIC_KEYS = listMetricKeys();
99
101
 
102
+ function filterId(filter: unknown): string | null {
103
+ if (!filter || typeof filter !== "object" || Array.isArray(filter)) {
104
+ return null;
105
+ }
106
+ const id = (filter as { id?: unknown }).id;
107
+ return typeof id === "string" && id.trim() ? id : null;
108
+ }
109
+
110
+ function withFirstPartyDashboardFilters(
111
+ config: Record<string, unknown>,
112
+ ): Record<string, unknown> {
113
+ const filters = Array.isArray(config.filters) ? [...config.filters] : [];
114
+ const existingIds = new Set(
115
+ filters
116
+ .map((filter) => filterId(filter))
117
+ .filter((id): id is string => id !== null),
118
+ );
119
+ for (const filter of buildFirstPartyDashboardFilters()) {
120
+ if (!existingIds.has(filter.id)) {
121
+ filters.push(filter);
122
+ existingIds.add(filter.id);
123
+ }
124
+ }
125
+ return { ...config, filters };
126
+ }
127
+
100
128
  export default defineAction({
101
129
  description:
102
130
  "Build a large first-party analytics dashboard in ONE fast call: name the metrics you want and the SERVER generates the validated SQL + chart config for every panel. " +
@@ -249,6 +277,12 @@ export default defineAction({
249
277
  };
250
278
  }
251
279
 
280
+ if (
281
+ composedPanels.some((panel) => usesFirstPartyDashboardFilters(panel.sql))
282
+ ) {
283
+ finalConfig = withFirstPartyDashboardFilters(finalConfig);
284
+ }
285
+
252
286
  const panelCount = Array.isArray(finalConfig.panels)
253
287
  ? (finalConfig.panels as unknown[]).length
254
288
  : 0;
@@ -41,6 +41,49 @@ function uniqueConstraintMessage(err: unknown): boolean {
41
41
  return /unique|constraint|primary key/i.test(message);
42
42
  }
43
43
 
44
+ function filterId(filter: unknown): string | null {
45
+ if (!filter || typeof filter !== "object" || Array.isArray(filter)) {
46
+ return null;
47
+ }
48
+ const id = (filter as { id?: unknown }).id;
49
+ return typeof id === "string" && id.trim() ? id : null;
50
+ }
51
+
52
+ function mergeMissingFilters(
53
+ targetConfig: Record<string, unknown>,
54
+ seedConfig: Record<string, unknown>,
55
+ ): { config: Record<string, unknown>; addedFilterIds: string[] } {
56
+ const seedFilters = Array.isArray(seedConfig.filters)
57
+ ? (seedConfig.filters as unknown[])
58
+ : [];
59
+ if (seedFilters.length === 0) {
60
+ return { config: targetConfig, addedFilterIds: [] };
61
+ }
62
+
63
+ const targetFilters = Array.isArray(targetConfig.filters)
64
+ ? [...targetConfig.filters]
65
+ : [];
66
+ const existingIds = new Set(
67
+ targetFilters
68
+ .map((filter) => filterId(filter))
69
+ .filter((id): id is string => id !== null),
70
+ );
71
+ const addedFilterIds: string[] = [];
72
+ for (const filter of seedFilters) {
73
+ const id = filterId(filter);
74
+ if (id && existingIds.has(id)) continue;
75
+ targetFilters.push(filter);
76
+ if (id) {
77
+ existingIds.add(id);
78
+ addedFilterIds.push(id);
79
+ }
80
+ }
81
+
82
+ return addedFilterIds.length > 0
83
+ ? { config: { ...targetConfig, filters: targetFilters }, addedFilterIds }
84
+ : { config: targetConfig, addedFilterIds };
85
+ }
86
+
44
87
  export default defineAction({
45
88
  description:
46
89
  "Install a dashboard template from the Analytics catalog into the user's SQL-backed dashboards. Use list-dashboard-templates first when choosing a template. " +
@@ -125,7 +168,10 @@ export default defineAction({
125
168
  .filter((id): id is string => !!id),
126
169
  );
127
170
 
128
- const seedConfig = cloneDashboardConfig(entry);
171
+ const seedConfig = cloneDashboardConfig(entry) as unknown as Record<
172
+ string,
173
+ unknown
174
+ >;
129
175
  const seedPanels = Array.isArray(seedConfig.panels)
130
176
  ? (seedConfig.panels as unknown as Array<Record<string, unknown>>)
131
177
  : [];
@@ -146,13 +192,15 @@ export default defineAction({
146
192
  }
147
193
  }
148
194
 
149
- const mergedConfig: Record<string, unknown> = {
195
+ let mergedConfig: Record<string, unknown> = {
150
196
  ...targetConfig,
151
197
  panels: [...existingPanels, ...appended],
152
198
  };
199
+ const filterMerge = mergeMissingFilters(mergedConfig, seedConfig);
200
+ mergedConfig = filterMerge.config;
153
201
  const panelCount = (mergedConfig.panels as unknown[]).length;
154
202
 
155
- if (appended.length > 0) {
203
+ if (appended.length > 0 || filterMerge.addedFilterIds.length > 0) {
156
204
  const saved = await upsertDashboard(
157
205
  targetId,
158
206
  target.kind,