@agent-native/core 0.70.0 → 0.70.1

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 (167) hide show
  1. package/README.md +15 -15
  2. package/corpus/README.md +2 -2
  3. package/corpus/core/CHANGELOG.md +61 -0
  4. package/corpus/core/docs/design/durable-agent-runs.md +217 -0
  5. package/corpus/core/package.json +1 -1
  6. package/corpus/core/src/action.ts +89 -0
  7. package/corpus/core/src/agent/model-config.ts +0 -2
  8. package/corpus/core/src/agent/run-loop-with-resume.ts +48 -0
  9. package/corpus/core/src/cli/skills.ts +2 -2
  10. package/corpus/core/src/client/AgentPanel.tsx +5 -3
  11. package/corpus/core/src/client/AssistantChat.tsx +8 -0
  12. package/corpus/core/src/client/client-surface.ts +41 -0
  13. package/corpus/core/src/client/composer/TiptapComposer.tsx +123 -12
  14. package/corpus/core/src/client/feedback-context.ts +4 -0
  15. package/corpus/core/src/client/index.ts +1 -0
  16. package/corpus/core/src/client/sse-event-processor.ts +7 -1
  17. package/corpus/core/src/mcp/build-server.ts +57 -1
  18. package/corpus/core/src/mcp/embed-app.ts +86 -2
  19. package/corpus/core/src/mcp/oauth-route.ts +109 -13
  20. package/corpus/core/src/server/core-routes-plugin.ts +78 -0
  21. package/corpus/core/src/server/onboarding-html.ts +35 -0
  22. package/corpus/core/src/templates/workspace-core/.agents/skills/reliable-mutations/SKILL.md +72 -0
  23. package/corpus/templates/analytics/.agents/skills/dashboard-management/SKILL.md +22 -0
  24. package/corpus/templates/analytics/AGENTS.md +8 -1
  25. package/corpus/templates/analytics/actions/install-dashboard-template.ts +97 -2
  26. package/corpus/templates/analytics/actions/update-dashboard.ts +36 -7
  27. package/corpus/templates/analytics/app/components/dashboard/SqlChart.tsx +30 -3
  28. package/corpus/templates/analytics/app/components/layout/Header.tsx +0 -1
  29. package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +2 -4
  30. package/corpus/templates/analytics/app/hooks/use-navigation-state.ts +0 -3
  31. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/SqlChartCard.tsx +32 -2
  32. package/corpus/templates/analytics/changelog/2026-06-23-long-dashboard-and-analysis-names-in-the-sidebar-now-truncat.md +6 -0
  33. package/corpus/templates/analytics/changelog/2026-06-23-open-any-dashboard-chart-full-screen-in-a-modal-from-the-pan.md +6 -0
  34. package/corpus/templates/assets/actions/generate-image.ts +24 -0
  35. package/corpus/templates/assets/app/components/layout/Sidebar.tsx +2 -2
  36. package/corpus/templates/assets/app/global.css +17 -1
  37. package/corpus/templates/assets/app/routes/_index.tsx +94 -14
  38. package/corpus/templates/assets/changelog/2026-06-23-chat-dates-in-the-sidebar-no-longer-wrap-onto-two-lines.md +6 -0
  39. package/corpus/templates/assets/changelog/2026-06-23-clicking-image-video-or-refine-on-the-start-screen-now-drops.md +6 -0
  40. package/corpus/templates/assets/changelog/2026-06-23-pick-your-image-generation-model-right-from-the-chat-model-m.md +6 -0
  41. package/corpus/templates/clips/app/components/library/library-grid.tsx +1 -20
  42. package/corpus/templates/clips/app/components/recorder/countdown-overlay.tsx +72 -48
  43. package/corpus/templates/clips/app/lib/countdown-audio-cue.ts +35 -17
  44. package/corpus/templates/clips/changelog/2026-06-23-removed-select-button-from-library-hover-a-clip-to-select-it.md +6 -0
  45. package/corpus/templates/clips/changelog/2026-06-23-shared-video-links-now-play-for-anyone-without-signing-in.md +6 -0
  46. package/corpus/templates/clips/changelog/2026-06-23-skip-or-cancel-the-recording-countdown-with-buttons-beside-t.md +6 -0
  47. package/corpus/templates/clips/changelog/2026-06-23-the-desktop-camera-bubble-now-looks-sharp-the-instant-it-ope.md +6 -0
  48. package/corpus/templates/clips/changelog/2026-06-23-the-desktop-recording-widget-now-grows-downward-only-when-yo.md +6 -0
  49. package/corpus/templates/clips/changelog/2026-06-23-the-recording-start-sound-is-now-a-softer-more-modern-chime.md +6 -0
  50. package/corpus/templates/clips/chrome-extension/public/manifest.json +17 -8
  51. package/corpus/templates/clips/chrome-extension/src/background.ts +769 -180
  52. package/corpus/templates/clips/chrome-extension/src/content-script.ts +177 -0
  53. package/corpus/templates/clips/chrome-extension/src/offscreen.ts +466 -329
  54. package/corpus/templates/clips/chrome-extension/src/overlay.css +377 -0
  55. package/corpus/templates/clips/chrome-extension/src/overlay.html +12 -0
  56. package/corpus/templates/clips/chrome-extension/src/overlay.ts +330 -0
  57. package/corpus/templates/clips/chrome-extension/src/permission.html +227 -0
  58. package/corpus/templates/clips/chrome-extension/src/permission.ts +105 -0
  59. package/corpus/templates/clips/chrome-extension/src/popup.html +3 -0
  60. package/corpus/templates/clips/chrome-extension/src/popup.ts +50 -2
  61. package/corpus/templates/clips/chrome-extension/src/styles.css +50 -0
  62. package/corpus/templates/clips/chrome-extension/vite.config.ts +3 -0
  63. package/corpus/templates/clips/desktop/src/lib/audio-cue.ts +41 -24
  64. package/corpus/templates/clips/desktop/src/lib/bubble-webrtc.ts +119 -2
  65. package/corpus/templates/clips/desktop/src/overlays/countdown.tsx +21 -2
  66. package/corpus/templates/clips/desktop/src/styles.css +70 -2
  67. package/corpus/templates/clips/desktop/src-tauri/src/clips/mod.rs +73 -0
  68. package/corpus/templates/clips/server/routes/api/video/[recordingId].get.ts +34 -5
  69. package/corpus/templates/content/AGENTS.md +20 -3
  70. package/corpus/templates/content/actions/_database-utils.ts +15 -0
  71. package/corpus/templates/content/actions/_property-utils.ts +371 -4
  72. package/corpus/templates/content/actions/configure-document-property.ts +35 -1
  73. package/corpus/templates/content/actions/create-content-database.ts +5 -1
  74. package/corpus/templates/content/actions/delete-document-property.ts +52 -2
  75. package/corpus/templates/content/actions/duplicate-document-property.ts +34 -17
  76. package/corpus/templates/content/actions/reorder-document-property.ts +79 -0
  77. package/corpus/templates/content/actions/set-document-property.ts +39 -0
  78. package/corpus/templates/content/app/components/editor/DocumentBlockFields.tsx +806 -0
  79. package/corpus/templates/content/app/components/editor/DocumentDatabase.tsx +226 -68
  80. package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +59 -30
  81. package/corpus/templates/content/app/components/editor/DocumentProperties.tsx +25 -1
  82. package/corpus/templates/content/app/components/editor/blockFieldSaveController.ts +180 -0
  83. package/corpus/templates/content/app/components/editor/blockFieldSaveRegistry.ts +179 -0
  84. package/corpus/templates/content/app/components/editor/previewDocumentSaveController.ts +244 -0
  85. package/corpus/templates/content/app/components/editor/previewDocumentSaveRegistry.ts +132 -0
  86. package/corpus/templates/content/app/global.css +9 -0
  87. package/corpus/templates/content/app/hooks/use-document-properties.ts +21 -0
  88. package/corpus/templates/content/server/db/schema.ts +30 -0
  89. package/corpus/templates/content/server/plugins/db.ts +87 -0
  90. package/corpus/templates/content/shared/api.ts +7 -0
  91. package/corpus/templates/content/shared/properties.ts +109 -0
  92. package/corpus/templates/forms/.agents/skills/form-responses/SKILL.md +10 -0
  93. package/corpus/templates/forms/actions/export-responses.ts +6 -0
  94. package/corpus/templates/forms/actions/list-responses.ts +2 -0
  95. package/corpus/templates/forms/actions/response-insights.ts +2 -0
  96. package/corpus/templates/forms/app/pages/ResponsesPage.tsx +138 -1
  97. package/corpus/templates/forms/changelog/2026-06-23-response-tables-now-show-the-page-each-submission-came-from-.md +6 -0
  98. package/corpus/templates/forms/changelog/2026-06-23-response-tables-now-show-whether-feedback-came-from-the-web-.md +6 -0
  99. package/corpus/templates/forms/server/db/schema.ts +7 -0
  100. package/corpus/templates/forms/server/handlers/submissions.ts +19 -2
  101. package/corpus/templates/forms/server/lib/integrations.ts +76 -2
  102. package/corpus/templates/forms/server/plugins/db.ts +20 -0
  103. package/corpus/templates/forms/shared/types.ts +12 -0
  104. package/corpus/templates/plan/app/components/plan/wireframe/html-artboard.css +3 -1
  105. package/corpus/templates/plan/changelog/2026-06-23-large-diagrams-in-a-plan-now-scroll-within-their-block-inste.md +6 -0
  106. package/dist/action.js +87 -0
  107. package/dist/action.js.map +1 -1
  108. package/dist/agent/engine/builder-engine.d.ts +1 -1
  109. package/dist/agent/engine/builder-engine.d.ts.map +1 -1
  110. package/dist/agent/model-config.d.ts +2 -2
  111. package/dist/agent/model-config.d.ts.map +1 -1
  112. package/dist/agent/model-config.js +0 -2
  113. package/dist/agent/model-config.js.map +1 -1
  114. package/dist/agent/run-loop-with-resume.d.ts +13 -0
  115. package/dist/agent/run-loop-with-resume.d.ts.map +1 -1
  116. package/dist/agent/run-loop-with-resume.js +44 -0
  117. package/dist/agent/run-loop-with-resume.js.map +1 -1
  118. package/dist/cli/skills.js +2 -2
  119. package/dist/cli/skills.js.map +1 -1
  120. package/dist/client/AgentPanel.d.ts.map +1 -1
  121. package/dist/client/AgentPanel.js +5 -3
  122. package/dist/client/AgentPanel.js.map +1 -1
  123. package/dist/client/AssistantChat.d.ts +6 -1
  124. package/dist/client/AssistantChat.d.ts.map +1 -1
  125. package/dist/client/AssistantChat.js +2 -2
  126. package/dist/client/AssistantChat.js.map +1 -1
  127. package/dist/client/client-surface.d.ts +17 -0
  128. package/dist/client/client-surface.d.ts.map +1 -0
  129. package/dist/client/client-surface.js +24 -0
  130. package/dist/client/client-surface.js.map +1 -0
  131. package/dist/client/composer/TiptapComposer.d.ts +26 -1
  132. package/dist/client/composer/TiptapComposer.d.ts.map +1 -1
  133. package/dist/client/composer/TiptapComposer.js +27 -5
  134. package/dist/client/composer/TiptapComposer.js.map +1 -1
  135. package/dist/client/feedback-context.d.ts +3 -0
  136. package/dist/client/feedback-context.d.ts.map +1 -1
  137. package/dist/client/feedback-context.js +2 -0
  138. package/dist/client/feedback-context.js.map +1 -1
  139. package/dist/client/index.d.ts +1 -0
  140. package/dist/client/index.d.ts.map +1 -1
  141. package/dist/client/index.js +1 -0
  142. package/dist/client/index.js.map +1 -1
  143. package/dist/client/sse-event-processor.d.ts.map +1 -1
  144. package/dist/client/sse-event-processor.js +7 -1
  145. package/dist/client/sse-event-processor.js.map +1 -1
  146. package/dist/mcp/build-server.d.ts +21 -0
  147. package/dist/mcp/build-server.d.ts.map +1 -1
  148. package/dist/mcp/build-server.js +43 -1
  149. package/dist/mcp/build-server.js.map +1 -1
  150. package/dist/mcp/embed-app.d.ts.map +1 -1
  151. package/dist/mcp/embed-app.js +86 -2
  152. package/dist/mcp/embed-app.js.map +1 -1
  153. package/dist/mcp/oauth-route.d.ts.map +1 -1
  154. package/dist/mcp/oauth-route.js +95 -12
  155. package/dist/mcp/oauth-route.js.map +1 -1
  156. package/dist/server/core-routes-plugin.d.ts.map +1 -1
  157. package/dist/server/core-routes-plugin.js +63 -0
  158. package/dist/server/core-routes-plugin.js.map +1 -1
  159. package/dist/server/onboarding-html.d.ts.map +1 -1
  160. package/dist/server/onboarding-html.js +35 -0
  161. package/dist/server/onboarding-html.js.map +1 -1
  162. package/dist/templates/workspace-core/.agents/skills/reliable-mutations/SKILL.md +72 -0
  163. package/docs/design/durable-agent-runs.md +217 -0
  164. package/package.json +1 -1
  165. package/src/templates/workspace-core/.agents/skills/reliable-mutations/SKILL.md +72 -0
  166. package/corpus/templates/analytics/app/pages/About.tsx +0 -108
  167. package/corpus/templates/analytics/app/routes/about.tsx +0 -9
@@ -423,15 +423,23 @@ function redirectWithOAuthError(params: {
423
423
  return redirect(url.toString());
424
424
  }
425
425
 
426
- function redirectWithCode(params: {
426
+ function buildCodeRedirectUrl(params: {
427
427
  redirectUri: string;
428
428
  code: string;
429
429
  state?: string;
430
- }): Response {
430
+ }): string {
431
431
  const url = new URL(params.redirectUri);
432
432
  url.searchParams.set("code", params.code);
433
433
  if (params.state) url.searchParams.set("state", params.state);
434
- return redirect(url.toString());
434
+ return url.toString();
435
+ }
436
+
437
+ function redirectWithCode(params: {
438
+ redirectUri: string;
439
+ code: string;
440
+ state?: string;
441
+ }): Response {
442
+ return redirect(buildCodeRedirectUrl(params));
435
443
  }
436
444
 
437
445
  function codeChallengeForVerifier(verifier: string): string {
@@ -530,6 +538,19 @@ function isValidCodeVerifier(value: unknown): value is string {
530
538
  );
531
539
  }
532
540
 
541
+ // Shared styling for the browser-facing OAuth pages (consent + post-authorize
542
+ // confirmation) so they read as one coherent dark surface.
543
+ const OAUTH_PAGE_BASE_STYLE = `
544
+ :root { color-scheme: dark; font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; background: #09090b; color: #f4f4f5; }
545
+ body { min-height: 100vh; display: grid; place-items: center; margin: 0; padding: 24px; }
546
+ main { width: min(520px, 100%); border: 1px solid #27272a; border-radius: 8px; background: #111113; padding: 24px; box-shadow: 0 24px 80px rgba(0,0,0,.35); }
547
+ h1 { font-size: 22px; line-height: 1.2; margin: 0 0 10px; }
548
+ p { color: #a1a1aa; line-height: 1.5; margin: 0 0 18px; }
549
+ .actions { display: flex; gap: 10px; justify-content: flex-end; }
550
+ button, .btn { border: 0; border-radius: 6px; padding: 10px 14px; font: inherit; font-weight: 650; cursor: pointer; text-decoration: none; display: inline-block; }
551
+ .primary { background: #f4f4f5; color: #09090b; }
552
+ .secondary { background: #27272a; color: #f4f4f5; }`;
553
+
533
554
  function renderConsentPage(params: {
534
555
  appName: string;
535
556
  email: string;
@@ -552,18 +573,9 @@ function renderConsentPage(params: {
552
573
  <meta charset="utf-8">
553
574
  <meta name="viewport" content="width=device-width, initial-scale=1">
554
575
  <title>Authorize ${escapeHtml(params.appName)}</title>
555
- <style>
556
- :root { color-scheme: dark; font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; background: #09090b; color: #f4f4f5; }
557
- body { min-height: 100vh; display: grid; place-items: center; margin: 0; padding: 24px; }
558
- main { width: min(520px, 100%); border: 1px solid #27272a; border-radius: 8px; background: #111113; padding: 24px; box-shadow: 0 24px 80px rgba(0,0,0,.35); }
559
- h1 { font-size: 22px; line-height: 1.2; margin: 0 0 10px; }
560
- p { color: #a1a1aa; line-height: 1.5; margin: 0 0 18px; }
576
+ <style>${OAUTH_PAGE_BASE_STYLE}
561
577
  ul { margin: 0 0 22px; padding-left: 22px; color: #d4d4d8; }
562
578
  code { color: #67e8f9; }
563
- .actions { display: flex; gap: 10px; justify-content: flex-end; }
564
- button { border: 0; border-radius: 6px; padding: 10px 14px; font-weight: 650; cursor: pointer; }
565
- .primary { background: #f4f4f5; color: #09090b; }
566
- .secondary { background: #27272a; color: #f4f4f5; }
567
579
  </style>
568
580
  </head>
569
581
  <body>
@@ -583,6 +595,61 @@ function renderConsentPage(params: {
583
595
  </html>`;
584
596
  }
585
597
 
598
+ // Shown after the user approves a native/desktop client (cursor://, vscode://, …)
599
+ // whose redirect is a private-use scheme. A bare 302 to a custom scheme hands the
600
+ // code to the OS app but leaves the browser tab dangling on a blank/error page, so
601
+ // we render a friendly confirmation that also re-fires the deep link (so the client
602
+ // still receives the code) and tells the user they can return to their agent.
603
+ function renderAuthorizedPage(params: {
604
+ appName: string;
605
+ clientName: string | null;
606
+ redirectUrl: string;
607
+ }): string {
608
+ const friendlyClient = params.clientName?.trim() || null;
609
+ const returnTarget = friendlyClient ?? "your agent";
610
+ const openLabel = friendlyClient
611
+ ? `Open ${escapeHtml(friendlyClient)}`
612
+ : "Return to your agent";
613
+ return `<!doctype html>
614
+ <html lang="en">
615
+ <head>
616
+ <meta charset="utf-8">
617
+ <meta name="viewport" content="width=device-width, initial-scale=1">
618
+ <title>Authorized</title>
619
+ <style>${OAUTH_PAGE_BASE_STYLE}
620
+ main.success { text-align: center; }
621
+ .check { width: 56px; height: 56px; margin: 4px auto 16px; display: grid; place-items: center; border-radius: 999px; background: rgba(34,197,94,.12); }
622
+ .check svg { width: 30px; height: 30px; }
623
+ .success .actions { justify-content: center; margin-top: 4px; }
624
+ .hint { color: #71717a; font-size: 13px; margin: 18px 0 0; }
625
+ </style>
626
+ </head>
627
+ <body>
628
+ <main class="success">
629
+ <div class="check">
630
+ <svg viewBox="0 0 24 24" fill="none" stroke="#22c55e" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
631
+ <path d="M20 6 9 17l-5-5"></path>
632
+ </svg>
633
+ </div>
634
+ <h1>You're all set</h1>
635
+ <p>${escapeHtml(params.appName)} has authorized ${escapeHtml(returnTarget)}. You can return to ${escapeHtml(returnTarget)} to continue.</p>
636
+ <div class="actions">
637
+ <a class="btn primary" id="return-link" href="${escapeHtml(params.redirectUrl)}">${openLabel}</a>
638
+ </div>
639
+ <p class="hint">You can close this tab.</p>
640
+ </main>
641
+ <script>
642
+ (function () {
643
+ var link = document.getElementById("return-link");
644
+ if (link && link.href) {
645
+ try { window.location.href = link.href; } catch (e) {}
646
+ }
647
+ })();
648
+ </script>
649
+ </body>
650
+ </html>`;
651
+ }
652
+
586
653
  async function resolveOrgDomain(
587
654
  orgId: string | undefined,
588
655
  ): Promise<string | undefined> {
@@ -744,6 +811,35 @@ async function handleAuthorize(
744
811
  scope,
745
812
  resource,
746
813
  });
814
+
815
+ // Native/desktop clients register a private-use scheme (cursor://, vscode://, …).
816
+ // A 302 to that scheme opens the app but leaves the browser tab dangling, so we
817
+ // render a friendly confirmation page that re-fires the deep link instead. For
818
+ // https/loopback callbacks the client (or its local server) renders its own page,
819
+ // so keep the standard redirect there.
820
+ let isDeepLinkRedirect = false;
821
+ try {
822
+ const protocol = new URL(redirectUri).protocol;
823
+ isDeepLinkRedirect =
824
+ protocol !== "http:" &&
825
+ protocol !== "https:" &&
826
+ isPrivateUseRedirectScheme(protocol);
827
+ } catch {
828
+ isDeepLinkRedirect = false;
829
+ }
830
+ if (isDeepLinkRedirect) {
831
+ return html(
832
+ renderAuthorizedPage({
833
+ appName: options.appName || options.appId || "Agent Native",
834
+ clientName: client.clientName ?? null,
835
+ redirectUrl: buildCodeRedirectUrl({
836
+ redirectUri,
837
+ state,
838
+ code: code.code,
839
+ }),
840
+ }),
841
+ );
842
+ }
747
843
  return redirectWithCode({ redirectUri, state, code: code.code });
748
844
  }
749
845
 
@@ -32,6 +32,7 @@ import {
32
32
  MCP_EMBED_CORS_ALLOW_HEADERS,
33
33
  shouldAllowMcpEmbedCredentials,
34
34
  } from "../shared/mcp-embed-headers.js";
35
+ import { captureError } from "./capture-error.js";
35
36
  import { handleMcpConnect } from "../mcp/connect-route.js";
36
37
  import {
37
38
  handleMcpOAuth,
@@ -3161,6 +3162,83 @@ export function createCoreRoutesPlugin(
3161
3162
  `${P}/embed/start`,
3162
3163
  createEmbedStartRouteHandler({ getExistingSession: getSession }),
3163
3164
  );
3165
+
3166
+ // POST /_agent-native/mcp/embed-error — telemetry sink for MCP App
3167
+ // embed shells. The shell runs in a sandboxed, opaque-origin iframe
3168
+ // (Codex, Cursor, ChatGPT, Claude) with no session cookie or CSRF
3169
+ // token, so this endpoint is intentionally unauthenticated and
3170
+ // CORS-open to the SAME sandbox origins as /embed/start. It forwards a
3171
+ // small, bounded diagnostic payload to Sentry via captureError so we
3172
+ // can see *why* an inline embed failed (handshake timeout, transplant
3173
+ // fetch status/CORS, auth, CSP) per host. Best-effort: always 204,
3174
+ // never throws, body capped, no client-trusted identity.
3175
+ getH3App(nitroApp).use(
3176
+ `${P}/mcp/embed-error`,
3177
+ defineEventHandler(async (event: H3Event) => {
3178
+ const origin = getHeader(event, "origin");
3179
+ if (origin && isMcpEmbedCorsOrigin(origin)) {
3180
+ setResponseHeader(event, "Access-Control-Allow-Origin", origin);
3181
+ setResponseHeader(event, "Vary", "Origin");
3182
+ setResponseHeader(
3183
+ event,
3184
+ "Access-Control-Allow-Methods",
3185
+ "POST,OPTIONS",
3186
+ );
3187
+ setResponseHeader(
3188
+ event,
3189
+ "Access-Control-Allow-Headers",
3190
+ MCP_EMBED_CORS_ALLOW_HEADERS,
3191
+ );
3192
+ }
3193
+ const method = getMethod(event);
3194
+ if (method === "OPTIONS") {
3195
+ setResponseStatus(event, 204);
3196
+ return "";
3197
+ }
3198
+ if (method !== "POST") {
3199
+ setResponseStatus(event, 405);
3200
+ return { error: "Method not allowed" };
3201
+ }
3202
+ const body = await readBody(event).catch(() => undefined);
3203
+ const rec =
3204
+ body && typeof body === "object" && !Array.isArray(body)
3205
+ ? (body as Record<string, unknown>)
3206
+ : {};
3207
+ const str = (value: unknown, max: number): string | undefined =>
3208
+ typeof value === "string" && value
3209
+ ? value.slice(0, max)
3210
+ : undefined;
3211
+ const message = str(rec.message, 500) ?? "MCP embed failed";
3212
+ try {
3213
+ captureError(new Error(message), {
3214
+ route: `${P}/mcp/embed-error`,
3215
+ method: "POST",
3216
+ userAgent:
3217
+ str(rec.userAgent, 300) ?? getHeader(event, "user-agent"),
3218
+ tags: {
3219
+ source: "mcp-embed-shell",
3220
+ embed_stage: str(rec.stage, 60),
3221
+ embed_render_mode: str(rec.renderMode, 40),
3222
+ embed_host: str(rec.host, 160),
3223
+ embed_bridge: str(rec.bridge, 40),
3224
+ },
3225
+ extra: {
3226
+ embedUrl: str(rec.url, 600),
3227
+ httpStatus:
3228
+ typeof rec.status === "number"
3229
+ ? rec.status
3230
+ : str(rec.status, 40),
3231
+ detail: str(rec.detail, 1200),
3232
+ origin,
3233
+ },
3234
+ });
3235
+ } catch {
3236
+ // Observability must never throw back into the request path.
3237
+ }
3238
+ setResponseStatus(event, 204);
3239
+ return "";
3240
+ }),
3241
+ );
3164
3242
  }
3165
3243
 
3166
3244
  if (!options.disableAppState) {
@@ -1191,6 +1191,7 @@ ${
1191
1191
  }
1192
1192
  }
1193
1193
  function __anFinishOAuthExchange(ret, flowId, sessionToken) {
1194
+ __anGoogleSignInInFlight = false;
1194
1195
  if (__anIsBuilderPreview()) {
1195
1196
  if (sessionToken) {
1196
1197
  __anSetOAuthDebug('OAuth exchange redeemed; applying session bridge to embedded app', flowId);
@@ -1361,6 +1362,8 @@ ${identitySsoScript}
1361
1362
  }
1362
1363
  var __anOAuthPollTimer = null;
1363
1364
  var __anOAuthPollCount = 0;
1365
+ var __anGoogleSignInInFlight = false;
1366
+ var __anGoogleRecoverBound = false;
1364
1367
  function __anNewOAuthFlowId() {
1365
1368
  try {
1366
1369
  if (window.crypto && typeof window.crypto.randomUUID === 'function') {
@@ -1401,6 +1404,34 @@ ${identitySsoScript}
1401
1404
  err.textContent = message;
1402
1405
  err.classList.add('show');
1403
1406
  btn.disabled = false;
1407
+ __anGoogleSignInInFlight = false;
1408
+ }
1409
+ function __anRecoverGoogleSignInAfterReturn() {
1410
+ // The user left for the Google sign-in window and came back. If the flow
1411
+ // never completed (e.g. they closed the window to switch profiles), the
1412
+ // button is stuck disabled with no error path firing for up to 5 minutes.
1413
+ // Re-enable it so they can retry. Wait briefly first so a genuinely
1414
+ // in-flight exchange can still finish and navigate without a flicker.
1415
+ if (!__anGoogleSignInInFlight) return;
1416
+ setTimeout(function() {
1417
+ if (!__anGoogleSignInInFlight) return;
1418
+ var btn = document.getElementById('google-btn');
1419
+ if (!btn || !btn.disabled) return;
1420
+ if (__anOAuthPollTimer) {
1421
+ clearInterval(__anOAuthPollTimer);
1422
+ __anOAuthPollTimer = null;
1423
+ }
1424
+ btn.disabled = false;
1425
+ __anGoogleSignInInFlight = false;
1426
+ }, 1200);
1427
+ }
1428
+ function __anBindGoogleRecover() {
1429
+ if (__anGoogleRecoverBound) return;
1430
+ __anGoogleRecoverBound = true;
1431
+ window.addEventListener('focus', __anRecoverGoogleSignInAfterReturn);
1432
+ document.addEventListener('visibilitychange', function() {
1433
+ if (document.visibilityState === 'visible') __anRecoverGoogleSignInAfterReturn();
1434
+ });
1404
1435
  }
1405
1436
  function __anHandlePopupOAuthFailure(ret, btn, err, flowId, redirectReason, builderFrameMessage) {
1406
1437
  if (__anIsBuilderPreview() && __anIsInFrame()) {
@@ -2013,6 +2044,8 @@ ${
2013
2044
  var err = document.getElementById('google-err');
2014
2045
  var ret = __anGetReturnPath();
2015
2046
  btn.disabled = true;
2047
+ __anGoogleSignInInFlight = true;
2048
+ __anBindGoogleRecover();
2016
2049
  err.classList.remove('show');
2017
2050
  if (__anResolveAuthFlow() === 'popup') {
2018
2051
  __anStartPopupOAuth(ret, btn, err);
@@ -2037,11 +2070,13 @@ ${
2037
2070
  err.textContent = data.message || 'Google OAuth is not configured.';
2038
2071
  err.classList.add('show');
2039
2072
  btn.disabled = false;
2073
+ __anGoogleSignInInFlight = false;
2040
2074
  }
2041
2075
  } catch (e) {
2042
2076
  err.textContent = 'Failed to connect. Please try again.';
2043
2077
  err.classList.add('show');
2044
2078
  btn.disabled = false;
2079
+ __anGoogleSignInInFlight = false;
2045
2080
  }
2046
2081
  }`
2047
2082
  : ""
@@ -0,0 +1,72 @@
1
+ ---
2
+ name: reliable-mutations
3
+ description: >-
4
+ How the agent must perform writes so they actually persist under the hosted
5
+ ~40s run budget. Use whenever you create, update, delete, or batch-write app
6
+ data — especially "do this for many items" loops, or any task where the user
7
+ expects N things to end up saved.
8
+ ---
9
+
10
+ # Reliable Mutations
11
+
12
+ ## Rule
13
+
14
+ Make a change in **one atomic call** when the action supports it, then **verify
15
+ the persisted end state and report concrete proof** (counts/ids). Never drive a
16
+ multi-step change by looping many small writes, and never report success from a
17
+ tool ✓ alone.
18
+
19
+ ## Why
20
+
21
+ Hosted agent runs have a ~40s soft budget (it exists to hand off cleanly under
22
+ the upstream gateway/function walls — it is correct and is not raisable; see the
23
+ durable-agent-runs design doc). When you loop many sequential writes inside one
24
+ turn, the budget can cut you off mid-loop. The run resumes in a new chunk, but
25
+ the resume often re-does earlier steps instead of advancing, so the loop churns
26
+ and the *saved* result ends up partial — or empty — even though each individual
27
+ tool call appeared to succeed. The user gets told "done" while the data says
28
+ otherwise. One atomic call commits or fails as a unit; verification turns a
29
+ hopeful ✓ into a fact.
30
+
31
+ ## How
32
+
33
+ 1. **Prefer a single atomic call.** If an action accepts the whole set (add
34
+ many, set all, bulk update), pass the full batch in one call so it commits
35
+ atomically. Check the action surface for a batch/plural form before reaching
36
+ for a loop.
37
+ 2. **Do not loop many small writes under the run budget.** A sequence of N
38
+ per-item writes in one turn will race the ~40s cutoff and can leave partial
39
+ or no state. If no batch action exists, that is a gap in the action layer —
40
+ add or extend an action that accepts the batch (see the `actions` skill)
41
+ rather than papering over it with a loop.
42
+ 3. **Verify the end state after writing.** Re-read the data (a list/read action,
43
+ a count query) and confirm the result matches intent — the right number of
44
+ rows, the expected ids/fields. Do this before you tell the user it worked.
45
+ 4. **Report proof-of-done, not vibes.** State concrete evidence: "saved 12 of 12
46
+ panels (ids …)" or "updated 5 rows". Do not infer success from the presence
47
+ of a tool ✓ on an individual call.
48
+ 5. **On a time-budget cutoff, fail loud.** If the turn is cut before the change
49
+ is fully committed and verified, say so explicitly and report what *did*
50
+ persist (M of N) and what remains. Never round a partial or unverified write
51
+ up to "done".
52
+
53
+ ## Don't
54
+
55
+ - Don't loop `for each item: write(item)` for a large set in a single hosted
56
+ turn.
57
+ - Don't claim completion because every tool call returned ✓ — a ✓ on an aborted
58
+ chunk does not mean the row was committed.
59
+ - Don't silently shrink the scope ("I added a few of them") and present it as the
60
+ finished task.
61
+ - Don't try to "fix" this by asking for a longer run timeout — the budget is
62
+ correct; restructure the write instead.
63
+
64
+ ## Related
65
+
66
+ - `actions` — define or extend a batch/atomic action when only per-item writes
67
+ exist.
68
+ - `storing-data` — where app data lives and how reads/writes are scoped.
69
+ - `performance` — avoid query waterfalls when verifying end state.
70
+ - Design doc: `packages/core/docs/design/durable-agent-runs.md` — the real
71
+ ceiling fix (checkpointed and durable runs); this skill is the agent-facing
72
+ mitigation that reduces how often the ceiling is hit.
@@ -174,6 +174,28 @@ pnpm action update-dashboard --dashboardId weekly-metrics --config '<full json>'
174
174
 
175
175
  After a mutation, navigate to the dashboard if the user is elsewhere. The app syncs through the framework's polling/query invalidation path.
176
176
 
177
+ ## Reliable Bulk Edits
178
+
179
+ This is the dashboard-specific application of the framework-wide `reliable-mutations` skill — read that for the general rule (one atomic write, verify end state, report proof-of-done).
180
+
181
+ Hosted agent runs have a **~40s budget**. Many sequential `update-dashboard` calls (one per panel, plus schema-discovery calls) will blow that budget and leave the dashboard in a partial state — earlier inserts looked like they succeeded (✓), but nothing actually persisted. Avoid this:
182
+
183
+ - **Batch ALL changes into ONE `update-dashboard` call.** A single `update-dashboard` is atomic: it applies every op to an in-memory config, validates all panel SQL, then upserts once. Never loop the action.
184
+ - To add N panels, pass N ops in one call: `ops: [{op:"insert", path:"/panels/-", value:<panel>}, … ]` (`/panels/-` appends to the end).
185
+ - The `ops` format needs no discovery: each op is `{ op, path, from?, value? }`, `op ∈ set | replace | remove | insert | move | move-before`, and `path` is a JSON Pointer (e.g. `/panels/3`, `/panels/3/title`, `/name`).
186
+ - **To add a shipped template's panels, prefer `install-dashboard-template` with `mergePanels: true`** and the existing `dashboardId`. It appends only the template panels whose id is not already present (preserving existing panels and order) in one atomic save — you don't author each panel yourself.
187
+ - **Always verify the returned proof-of-done and report it.** `update-dashboard` returns `panelCount`, `appliedOps`, and a `summary` string; `install-dashboard-template --mergePanels` returns `addedPanelIds`, `skippedExistingIds`, and `panelCount`. Tell the user the resulting panel count instead of assuming success.
188
+
189
+ ```bash
190
+ # Add several panels in ONE atomic call (never one call per panel)
191
+ pnpm action update-dashboard --dashboardId weekly-metrics \
192
+ --ops '[{"op":"insert","path":"/panels/-","value":{"id":"p1","title":"A","source":"first-party","chartType":"metric","width":1,"sql":"SELECT COUNT(*) AS value FROM analytics_events"}},
193
+ {"op":"insert","path":"/panels/-","value":{"id":"p2","title":"B","source":"first-party","chartType":"metric","width":1,"sql":"SELECT COUNT(DISTINCT user_id) AS value FROM analytics_events"}}]'
194
+
195
+ # Append a template's panels to an existing dashboard in one call
196
+ pnpm action install-dashboard-template --templateId skills-cli-funnel --dashboardId weekly-metrics --mergePanels true
197
+ ```
198
+
177
199
  ## Archiving vs deleting
178
200
 
179
201
  Dashboards have a soft-delete state. The default user-facing destructive action is **Archive** (recoverable). Hard delete still exists, but lives behind a "Delete permanently" confirm in both the page header and the sidebar dropdown — and in the agent surface, behind the older `delete-dashboard` action. Archived rows stay in the `dashboards` table with `archived_at` set, are hidden from the default sidebar list, and remain accessible by id (so deep links in chat history keep working) until explicitly purged.
@@ -87,7 +87,14 @@ details live in `.agents/skills/`.
87
87
  `id`, category, data sources, panel count, and installed dashboard IDs.
88
88
  - `install-dashboard-template` installs a catalog template into normal
89
89
  SQL-backed dashboards. Required: `templateId`. Optional: `dashboardId`,
90
- `name`, `overwrite`, and `forceNew`.
90
+ `name`, `overwrite`, `forceNew`, and `mergePanels`.
91
+ - To add a template's panels to an existing dashboard, call
92
+ `install-dashboard-template` with `mergePanels: true` and the existing
93
+ `dashboardId`. It appends only the template panels whose id is not already
94
+ present (preserving existing panels and order) in one atomic save and returns
95
+ `{ addedPanelIds, skippedExistingIds, panelCount }`. Prefer this over looping
96
+ `update-dashboard` to add many panels — sequential calls time out on the ~40s
97
+ hosted run budget.
91
98
  - Node Exporter ships as `node-exporter-macos` for Darwin/Homebrew
92
99
  `node_exporter` scrapes and `node-exporter-full` for the Linux-focused
93
100
  Grafana 1860 revision 45 full dashboard converted into native Analytics
@@ -43,7 +43,8 @@ function uniqueConstraintMessage(err: unknown): boolean {
43
43
 
44
44
  export default defineAction({
45
45
  description:
46
- "Install a dashboard template from the Analytics catalog into the user's SQL-backed dashboards. Use list-dashboard-templates first when choosing a template.",
46
+ "Install a dashboard template from the Analytics catalog into the user's SQL-backed dashboards. Use list-dashboard-templates first when choosing a template. " +
47
+ "To ADD a template's panels to an EXISTING dashboard in ONE call (the preferred way to bulk-add panels), pass `mergePanels: true` with the `dashboardId` of the existing dashboard: it appends every template panel whose id is not already present, preserves all existing panels and their order, and saves once. This avoids looping update-dashboard, which times out on the ~40s hosted run budget.",
47
48
  schema: z.object({
48
49
  templateId: z
49
50
  .string()
@@ -52,7 +53,7 @@ export default defineAction({
52
53
  .string()
53
54
  .optional()
54
55
  .describe(
55
- "Optional dashboard id to write. Omit to reuse an existing installed copy or create a unique id.",
56
+ "Optional dashboard id to write. Omit to reuse an existing installed copy or create a unique id. Required when mergePanels is true.",
56
57
  ),
57
58
  name: z
58
59
  .string()
@@ -70,6 +71,12 @@ export default defineAction({
70
71
  .describe(
71
72
  "If true, create another copy even when this template is installed.",
72
73
  ),
74
+ mergePanels: z
75
+ .boolean()
76
+ .optional()
77
+ .describe(
78
+ "If true AND a dashboard already exists at dashboardId, APPEND this template's panels (only the ones whose id is not already present) to the existing dashboard in one atomic save, preserving all existing panels and their order. Returns { addedPanelIds, skippedExistingIds, panelCount }. Non-destructive; does not change overwrite/forceNew behavior.",
79
+ ),
73
80
  }),
74
81
  mcpApp: {
75
82
  compactCatalog: true,
@@ -90,6 +97,94 @@ export default defineAction({
90
97
  if (!entry)
91
98
  throw new Error(`Unknown dashboard template: ${args.templateId}`);
92
99
 
100
+ // Append/merge mode: add this template's panels to an existing dashboard
101
+ // in ONE atomic save instead of looping update-dashboard (which times out
102
+ // on the ~40s hosted run budget). Non-destructive: existing panels and
103
+ // their order are preserved; only template panels with a new id are added.
104
+ if (args.mergePanels) {
105
+ const targetId = args.dashboardId?.trim();
106
+ if (!targetId) {
107
+ throw new Error(
108
+ "mergePanels=true requires dashboardId (the existing dashboard to append the template's panels to).",
109
+ );
110
+ }
111
+ const target = await getDashboard(targetId, ctx);
112
+ if (!target) {
113
+ throw new Error(
114
+ `Dashboard "${targetId}" not found (or you don't have access). mergePanels appends to an existing dashboard — install the template normally first, or omit mergePanels to create a new copy.`,
115
+ );
116
+ }
117
+
118
+ const targetConfig = target.config as Record<string, unknown>;
119
+ const existingPanels = Array.isArray(targetConfig.panels)
120
+ ? (targetConfig.panels as Array<Record<string, unknown>>)
121
+ : [];
122
+ const existingIds = new Set(
123
+ existingPanels
124
+ .map((panel) => (typeof panel?.id === "string" ? panel.id : null))
125
+ .filter((id): id is string => !!id),
126
+ );
127
+
128
+ const seedConfig = cloneDashboardConfig(entry);
129
+ const seedPanels = Array.isArray(seedConfig.panels)
130
+ ? (seedConfig.panels as unknown as Array<Record<string, unknown>>)
131
+ : [];
132
+
133
+ const addedPanelIds: string[] = [];
134
+ const skippedExistingIds: string[] = [];
135
+ const appended: Array<Record<string, unknown>> = [];
136
+ for (const panel of seedPanels) {
137
+ const id = typeof panel?.id === "string" ? panel.id : null;
138
+ if (id && existingIds.has(id)) {
139
+ skippedExistingIds.push(id);
140
+ continue;
141
+ }
142
+ appended.push(panel);
143
+ if (id) {
144
+ addedPanelIds.push(id);
145
+ existingIds.add(id);
146
+ }
147
+ }
148
+
149
+ const mergedConfig: Record<string, unknown> = {
150
+ ...targetConfig,
151
+ panels: [...existingPanels, ...appended],
152
+ };
153
+ const panelCount = (mergedConfig.panels as unknown[]).length;
154
+
155
+ if (appended.length > 0) {
156
+ const saved = await upsertDashboard(
157
+ targetId,
158
+ target.kind,
159
+ mergedConfig,
160
+ ctx,
161
+ );
162
+ await syncToCollab(targetId, mergedConfig);
163
+ targetConfig.name = saved.title;
164
+ }
165
+
166
+ return {
167
+ templateId: entry.id,
168
+ templateName: entry.name,
169
+ dashboardId: targetId,
170
+ name: target.title,
171
+ merged: true,
172
+ addedPanelIds,
173
+ skippedExistingIds,
174
+ panelCount,
175
+ urlPath: `/dashboards/${targetId}`,
176
+ deepLink: buildDeepLink({
177
+ app: "analytics",
178
+ view: "adhoc",
179
+ params: { dashboardId: targetId },
180
+ }),
181
+ message:
182
+ appended.length > 0
183
+ ? `Added ${addedPanelIds.length} panel(s) from "${entry.name}" to "${target.title}"; ${skippedExistingIds.length} already present. Dashboard now has ${panelCount} panel(s).`
184
+ : `No new panels to add from "${entry.name}" — all ${skippedExistingIds.length} template panel id(s) already present. Dashboard has ${panelCount} panel(s).`,
185
+ };
186
+ }
187
+
93
188
  const installed = (await listDashboardCatalog(ctx)).find(
94
189
  (template) => template.id === entry.id,
95
190
  );