@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.
- package/README.md +15 -15
- package/corpus/README.md +2 -2
- package/corpus/core/CHANGELOG.md +61 -0
- package/corpus/core/docs/design/durable-agent-runs.md +217 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/action.ts +89 -0
- package/corpus/core/src/agent/model-config.ts +0 -2
- package/corpus/core/src/agent/run-loop-with-resume.ts +48 -0
- package/corpus/core/src/cli/skills.ts +2 -2
- package/corpus/core/src/client/AgentPanel.tsx +5 -3
- package/corpus/core/src/client/AssistantChat.tsx +8 -0
- package/corpus/core/src/client/client-surface.ts +41 -0
- package/corpus/core/src/client/composer/TiptapComposer.tsx +123 -12
- package/corpus/core/src/client/feedback-context.ts +4 -0
- package/corpus/core/src/client/index.ts +1 -0
- package/corpus/core/src/client/sse-event-processor.ts +7 -1
- package/corpus/core/src/mcp/build-server.ts +57 -1
- package/corpus/core/src/mcp/embed-app.ts +86 -2
- package/corpus/core/src/mcp/oauth-route.ts +109 -13
- package/corpus/core/src/server/core-routes-plugin.ts +78 -0
- package/corpus/core/src/server/onboarding-html.ts +35 -0
- package/corpus/core/src/templates/workspace-core/.agents/skills/reliable-mutations/SKILL.md +72 -0
- package/corpus/templates/analytics/.agents/skills/dashboard-management/SKILL.md +22 -0
- package/corpus/templates/analytics/AGENTS.md +8 -1
- package/corpus/templates/analytics/actions/install-dashboard-template.ts +97 -2
- package/corpus/templates/analytics/actions/update-dashboard.ts +36 -7
- package/corpus/templates/analytics/app/components/dashboard/SqlChart.tsx +30 -3
- package/corpus/templates/analytics/app/components/layout/Header.tsx +0 -1
- package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +2 -4
- package/corpus/templates/analytics/app/hooks/use-navigation-state.ts +0 -3
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/SqlChartCard.tsx +32 -2
- package/corpus/templates/analytics/changelog/2026-06-23-long-dashboard-and-analysis-names-in-the-sidebar-now-truncat.md +6 -0
- package/corpus/templates/analytics/changelog/2026-06-23-open-any-dashboard-chart-full-screen-in-a-modal-from-the-pan.md +6 -0
- package/corpus/templates/assets/actions/generate-image.ts +24 -0
- package/corpus/templates/assets/app/components/layout/Sidebar.tsx +2 -2
- package/corpus/templates/assets/app/global.css +17 -1
- package/corpus/templates/assets/app/routes/_index.tsx +94 -14
- package/corpus/templates/assets/changelog/2026-06-23-chat-dates-in-the-sidebar-no-longer-wrap-onto-two-lines.md +6 -0
- package/corpus/templates/assets/changelog/2026-06-23-clicking-image-video-or-refine-on-the-start-screen-now-drops.md +6 -0
- package/corpus/templates/assets/changelog/2026-06-23-pick-your-image-generation-model-right-from-the-chat-model-m.md +6 -0
- package/corpus/templates/clips/app/components/library/library-grid.tsx +1 -20
- package/corpus/templates/clips/app/components/recorder/countdown-overlay.tsx +72 -48
- package/corpus/templates/clips/app/lib/countdown-audio-cue.ts +35 -17
- package/corpus/templates/clips/changelog/2026-06-23-removed-select-button-from-library-hover-a-clip-to-select-it.md +6 -0
- package/corpus/templates/clips/changelog/2026-06-23-shared-video-links-now-play-for-anyone-without-signing-in.md +6 -0
- package/corpus/templates/clips/changelog/2026-06-23-skip-or-cancel-the-recording-countdown-with-buttons-beside-t.md +6 -0
- package/corpus/templates/clips/changelog/2026-06-23-the-desktop-camera-bubble-now-looks-sharp-the-instant-it-ope.md +6 -0
- package/corpus/templates/clips/changelog/2026-06-23-the-desktop-recording-widget-now-grows-downward-only-when-yo.md +6 -0
- package/corpus/templates/clips/changelog/2026-06-23-the-recording-start-sound-is-now-a-softer-more-modern-chime.md +6 -0
- package/corpus/templates/clips/chrome-extension/public/manifest.json +17 -8
- package/corpus/templates/clips/chrome-extension/src/background.ts +769 -180
- package/corpus/templates/clips/chrome-extension/src/content-script.ts +177 -0
- package/corpus/templates/clips/chrome-extension/src/offscreen.ts +466 -329
- package/corpus/templates/clips/chrome-extension/src/overlay.css +377 -0
- package/corpus/templates/clips/chrome-extension/src/overlay.html +12 -0
- package/corpus/templates/clips/chrome-extension/src/overlay.ts +330 -0
- package/corpus/templates/clips/chrome-extension/src/permission.html +227 -0
- package/corpus/templates/clips/chrome-extension/src/permission.ts +105 -0
- package/corpus/templates/clips/chrome-extension/src/popup.html +3 -0
- package/corpus/templates/clips/chrome-extension/src/popup.ts +50 -2
- package/corpus/templates/clips/chrome-extension/src/styles.css +50 -0
- package/corpus/templates/clips/chrome-extension/vite.config.ts +3 -0
- package/corpus/templates/clips/desktop/src/lib/audio-cue.ts +41 -24
- package/corpus/templates/clips/desktop/src/lib/bubble-webrtc.ts +119 -2
- package/corpus/templates/clips/desktop/src/overlays/countdown.tsx +21 -2
- package/corpus/templates/clips/desktop/src/styles.css +70 -2
- package/corpus/templates/clips/desktop/src-tauri/src/clips/mod.rs +73 -0
- package/corpus/templates/clips/server/routes/api/video/[recordingId].get.ts +34 -5
- package/corpus/templates/content/AGENTS.md +20 -3
- package/corpus/templates/content/actions/_database-utils.ts +15 -0
- package/corpus/templates/content/actions/_property-utils.ts +371 -4
- package/corpus/templates/content/actions/configure-document-property.ts +35 -1
- package/corpus/templates/content/actions/create-content-database.ts +5 -1
- package/corpus/templates/content/actions/delete-document-property.ts +52 -2
- package/corpus/templates/content/actions/duplicate-document-property.ts +34 -17
- package/corpus/templates/content/actions/reorder-document-property.ts +79 -0
- package/corpus/templates/content/actions/set-document-property.ts +39 -0
- package/corpus/templates/content/app/components/editor/DocumentBlockFields.tsx +806 -0
- package/corpus/templates/content/app/components/editor/DocumentDatabase.tsx +226 -68
- package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +59 -30
- package/corpus/templates/content/app/components/editor/DocumentProperties.tsx +25 -1
- package/corpus/templates/content/app/components/editor/blockFieldSaveController.ts +180 -0
- package/corpus/templates/content/app/components/editor/blockFieldSaveRegistry.ts +179 -0
- package/corpus/templates/content/app/components/editor/previewDocumentSaveController.ts +244 -0
- package/corpus/templates/content/app/components/editor/previewDocumentSaveRegistry.ts +132 -0
- package/corpus/templates/content/app/global.css +9 -0
- package/corpus/templates/content/app/hooks/use-document-properties.ts +21 -0
- package/corpus/templates/content/server/db/schema.ts +30 -0
- package/corpus/templates/content/server/plugins/db.ts +87 -0
- package/corpus/templates/content/shared/api.ts +7 -0
- package/corpus/templates/content/shared/properties.ts +109 -0
- package/corpus/templates/forms/.agents/skills/form-responses/SKILL.md +10 -0
- package/corpus/templates/forms/actions/export-responses.ts +6 -0
- package/corpus/templates/forms/actions/list-responses.ts +2 -0
- package/corpus/templates/forms/actions/response-insights.ts +2 -0
- package/corpus/templates/forms/app/pages/ResponsesPage.tsx +138 -1
- package/corpus/templates/forms/changelog/2026-06-23-response-tables-now-show-the-page-each-submission-came-from-.md +6 -0
- package/corpus/templates/forms/changelog/2026-06-23-response-tables-now-show-whether-feedback-came-from-the-web-.md +6 -0
- package/corpus/templates/forms/server/db/schema.ts +7 -0
- package/corpus/templates/forms/server/handlers/submissions.ts +19 -2
- package/corpus/templates/forms/server/lib/integrations.ts +76 -2
- package/corpus/templates/forms/server/plugins/db.ts +20 -0
- package/corpus/templates/forms/shared/types.ts +12 -0
- package/corpus/templates/plan/app/components/plan/wireframe/html-artboard.css +3 -1
- package/corpus/templates/plan/changelog/2026-06-23-large-diagrams-in-a-plan-now-scroll-within-their-block-inste.md +6 -0
- package/dist/action.js +87 -0
- package/dist/action.js.map +1 -1
- package/dist/agent/engine/builder-engine.d.ts +1 -1
- package/dist/agent/engine/builder-engine.d.ts.map +1 -1
- package/dist/agent/model-config.d.ts +2 -2
- package/dist/agent/model-config.d.ts.map +1 -1
- package/dist/agent/model-config.js +0 -2
- package/dist/agent/model-config.js.map +1 -1
- package/dist/agent/run-loop-with-resume.d.ts +13 -0
- package/dist/agent/run-loop-with-resume.d.ts.map +1 -1
- package/dist/agent/run-loop-with-resume.js +44 -0
- package/dist/agent/run-loop-with-resume.js.map +1 -1
- package/dist/cli/skills.js +2 -2
- package/dist/cli/skills.js.map +1 -1
- package/dist/client/AgentPanel.d.ts.map +1 -1
- package/dist/client/AgentPanel.js +5 -3
- package/dist/client/AgentPanel.js.map +1 -1
- package/dist/client/AssistantChat.d.ts +6 -1
- package/dist/client/AssistantChat.d.ts.map +1 -1
- package/dist/client/AssistantChat.js +2 -2
- package/dist/client/AssistantChat.js.map +1 -1
- package/dist/client/client-surface.d.ts +17 -0
- package/dist/client/client-surface.d.ts.map +1 -0
- package/dist/client/client-surface.js +24 -0
- package/dist/client/client-surface.js.map +1 -0
- package/dist/client/composer/TiptapComposer.d.ts +26 -1
- package/dist/client/composer/TiptapComposer.d.ts.map +1 -1
- package/dist/client/composer/TiptapComposer.js +27 -5
- package/dist/client/composer/TiptapComposer.js.map +1 -1
- package/dist/client/feedback-context.d.ts +3 -0
- package/dist/client/feedback-context.d.ts.map +1 -1
- package/dist/client/feedback-context.js +2 -0
- package/dist/client/feedback-context.js.map +1 -1
- package/dist/client/index.d.ts +1 -0
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +1 -0
- package/dist/client/index.js.map +1 -1
- package/dist/client/sse-event-processor.d.ts.map +1 -1
- package/dist/client/sse-event-processor.js +7 -1
- package/dist/client/sse-event-processor.js.map +1 -1
- package/dist/mcp/build-server.d.ts +21 -0
- package/dist/mcp/build-server.d.ts.map +1 -1
- package/dist/mcp/build-server.js +43 -1
- package/dist/mcp/build-server.js.map +1 -1
- package/dist/mcp/embed-app.d.ts.map +1 -1
- package/dist/mcp/embed-app.js +86 -2
- package/dist/mcp/embed-app.js.map +1 -1
- package/dist/mcp/oauth-route.d.ts.map +1 -1
- package/dist/mcp/oauth-route.js +95 -12
- package/dist/mcp/oauth-route.js.map +1 -1
- package/dist/server/core-routes-plugin.d.ts.map +1 -1
- package/dist/server/core-routes-plugin.js +63 -0
- package/dist/server/core-routes-plugin.js.map +1 -1
- package/dist/server/onboarding-html.d.ts.map +1 -1
- package/dist/server/onboarding-html.js +35 -0
- package/dist/server/onboarding-html.js.map +1 -1
- package/dist/templates/workspace-core/.agents/skills/reliable-mutations/SKILL.md +72 -0
- package/docs/design/durable-agent-runs.md +217 -0
- package/package.json +1 -1
- package/src/templates/workspace-core/.agents/skills/reliable-mutations/SKILL.md +72 -0
- package/corpus/templates/analytics/app/pages/About.tsx +0 -108
- 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
|
|
426
|
+
function buildCodeRedirectUrl(params: {
|
|
427
427
|
redirectUri: string;
|
|
428
428
|
code: string;
|
|
429
429
|
state?: string;
|
|
430
|
-
}):
|
|
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
|
|
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 `
|
|
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
|
);
|