@agent-native/core 0.80.5 → 0.80.7

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 (76) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +13 -0
  3. package/corpus/core/package.json +1 -1
  4. package/corpus/core/src/cli/create.ts +1 -0
  5. package/corpus/core/src/client/extensions/EmbeddedExtension.tsx +45 -0
  6. package/corpus/core/src/sharing/registry.ts +17 -0
  7. package/corpus/core/src/templates/default/pnpm-workspace.yaml +1 -0
  8. package/corpus/core/src/templates/workspace-root/pnpm-workspace.yaml +1 -0
  9. package/corpus/templates/analytics/.agents/skills/dashboard-management/SKILL.md +42 -4
  10. package/corpus/templates/analytics/actions/dashboard-mutation-api.ts +2 -1
  11. package/corpus/templates/analytics/actions/update-dashboard.ts +23 -9
  12. package/corpus/templates/analytics/app/components/dashboard/SqlChart.tsx +72 -1
  13. package/corpus/templates/analytics/app/i18n/zh-TW.ts +2 -0
  14. package/corpus/templates/analytics/app/i18n-data.ts +34 -0
  15. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/SqlChartCard.tsx +88 -2
  16. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/types.ts +7 -1
  17. package/corpus/templates/analytics/changelog/2026-06-29-dashboards-can-now-include-extension-panels-that-embed-a-san.md +6 -0
  18. package/corpus/templates/content/AGENTS.md +45 -44
  19. package/corpus/templates/content/README.md +16 -0
  20. package/corpus/templates/content/actions/_builder-cms-read-client.ts +125 -0
  21. package/corpus/templates/content/actions/_builder-cms-source-adapter.ts +28 -6
  22. package/corpus/templates/content/actions/_builder-cms-write-adapter.ts +238 -69
  23. package/corpus/templates/content/actions/_builder-cms-write-settings.ts +125 -43
  24. package/corpus/templates/content/actions/_database-source-utils.ts +746 -97
  25. package/corpus/templates/content/actions/_database-utils.ts +17 -14
  26. package/corpus/templates/content/actions/_federation-join.ts +14 -2
  27. package/corpus/templates/content/actions/_join-suggestion.ts +89 -14
  28. package/corpus/templates/content/actions/_local-file-documents.ts +2 -1
  29. package/corpus/templates/content/actions/attach-content-database-source.ts +124 -5
  30. package/corpus/templates/content/actions/bind-content-database-source-field.ts +256 -0
  31. package/corpus/templates/content/actions/change-content-database-source-role.ts +420 -0
  32. package/corpus/templates/content/actions/create-document.ts +8 -0
  33. package/corpus/templates/content/actions/execute-builder-source-batch.ts +282 -0
  34. package/corpus/templates/content/actions/execute-builder-source-execution.ts +220 -8
  35. package/corpus/templates/content/actions/get-content-database.ts +10 -2
  36. package/corpus/templates/content/actions/prepare-builder-source-execution.ts +19 -2
  37. package/corpus/templates/content/actions/prepare-builder-source-review.ts +47 -15
  38. package/corpus/templates/content/actions/refresh-content-database-source.ts +11 -4
  39. package/corpus/templates/content/actions/review-content-database-source-change-set.ts +6 -2
  40. package/corpus/templates/content/actions/set-content-database-source-write-mode.ts +32 -12
  41. package/corpus/templates/content/actions/stage-builder-revision.ts +14 -10
  42. package/corpus/templates/content/actions/validate-builder-source-execution.ts +25 -3
  43. package/corpus/templates/content/app/components/editor/DocumentDatabase.tsx +1520 -553
  44. package/corpus/templates/content/app/components/editor/DocumentProperties.tsx +150 -1
  45. package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +647 -66
  46. package/corpus/templates/content/app/components/editor/database-sources/BuilderSourceReviewDialog.tsx +543 -204
  47. package/corpus/templates/content/app/hooks/use-content-database.ts +62 -0
  48. package/corpus/templates/content/app/i18n/zh-TW.ts +64 -0
  49. package/corpus/templates/content/app/i18n-data.ts +784 -0
  50. package/corpus/templates/content/changelog/2026-06-29-builder-review-checks-now-preserve-publish-and-unpublish-cho.md +6 -0
  51. package/corpus/templates/content/changelog/2026-06-29-database-sources-can-now-be-added-as-more-rows-or-matched.md +6 -0
  52. package/corpus/templates/content/changelog/2026-06-29-fixed-loading-the-final-rows-in-larger-databases-without-bre.md +6 -0
  53. package/corpus/templates/content/shared/api.ts +91 -1
  54. package/corpus/templates/content/vite.config.ts +14 -7
  55. package/dist/cli/create.d.ts.map +1 -1
  56. package/dist/cli/create.js +1 -0
  57. package/dist/cli/create.js.map +1 -1
  58. package/dist/client/extensions/EmbeddedExtension.d.ts +10 -1
  59. package/dist/client/extensions/EmbeddedExtension.d.ts.map +1 -1
  60. package/dist/client/extensions/EmbeddedExtension.js +34 -3
  61. package/dist/client/extensions/EmbeddedExtension.js.map +1 -1
  62. package/dist/collab/routes.d.ts +2 -2
  63. package/dist/file-upload/actions/upload-image.d.ts +2 -2
  64. package/dist/notifications/routes.d.ts +1 -1
  65. package/dist/observability/routes.d.ts +7 -7
  66. package/dist/resources/handlers.d.ts +2 -2
  67. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  68. package/dist/server/transcribe-voice.d.ts +1 -1
  69. package/dist/sharing/registry.d.ts.map +1 -1
  70. package/dist/sharing/registry.js +11 -0
  71. package/dist/sharing/registry.js.map +1 -1
  72. package/dist/templates/default/pnpm-workspace.yaml +1 -0
  73. package/dist/templates/workspace-root/pnpm-workspace.yaml +1 -0
  74. package/package.json +1 -1
  75. package/src/templates/default/pnpm-workspace.yaml +1 -0
  76. package/src/templates/workspace-root/pnpm-workspace.yaml +1 -0
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: 2041
31
- - template files: 4581
31
+ - template files: 4588
@@ -1,5 +1,18 @@
1
1
  # @agent-native/core
2
2
 
3
+ ## 0.80.7
4
+
5
+ ### Patch Changes
6
+
7
+ - 72ef787: Add `onReady` and `onUnavailable` callbacks to `EmbeddedExtension`. `onReady` fires once when the embedded iframe first signals content readiness (its first height report, or iframe load as a fallback) — hosts that gate on content paint, such as dashboard report screenshots, can use it to avoid capturing a blank extension. `onUnavailable` fires when the extension can't be loaded for the current viewer (e.g. 403/404 because it isn't shared with them or no longer exists), so hosts can render an explanatory fallback instead of a silently blank panel.
8
+
9
+ ## 0.80.6
10
+
11
+ ### Patch Changes
12
+
13
+ - f52eeb1: Pin the scaffolded workspace Nitro tracer dependency so fresh installs keep building reliably.
14
+ - f52eeb1: Ignore shareable resource registrations that originate from test modules during app runtime.
15
+
3
16
  ## 0.80.5
4
17
 
5
18
  ### Patch Changes
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-native/core",
3
- "version": "0.80.5",
3
+ "version": "0.80.7",
4
4
  "description": "Framework for agent-native application development — where AI agents and UI share SQL state, actions, and context",
5
5
  "homepage": "https://github.com/BuilderIO/agent-native#readme",
6
6
  "bugs": {
@@ -1054,6 +1054,7 @@ function postProcessStandalone(
1054
1054
  sections.overrides = {
1055
1055
  '"@assistant-ui/store"': '">=0.2.9 <0.2.14"',
1056
1056
  '"@assistant-ui/tap"': '"^0.5.14"',
1057
+ nf3: '"0.3.17"',
1057
1058
  };
1058
1059
  }
1059
1060
  let updated = mergeWorkspaceYamlSections(existing, sections);
@@ -29,6 +29,7 @@ import {
29
29
  } from "./delete-extension.js";
30
30
  import {
31
31
  extensionLoadError,
32
+ extensionLoadErrorStatus,
32
33
  shouldRetryExtensionLoad,
33
34
  } from "./extension-load-error.js";
34
35
  import {
@@ -74,6 +75,15 @@ export interface EmbeddedExtensionProps {
74
75
  className?: string;
75
76
  /** Initial iframe height before content reports a real height. */
76
77
  initialHeight?: number;
78
+ /** Fires once when the embedded iframe first signals content readiness — its
79
+ * first height report, or iframe load as a fallback. Hosts that gate on
80
+ * content paint (e.g. dashboard report screenshots) use this. */
81
+ onReady?: () => void;
82
+ /** Fires when the extension can't be loaded for this viewer (e.g. 403/404 —
83
+ * the extension isn't shared with them or no longer exists). Hosts can use
84
+ * this to render an explanatory fallback instead of a blank panel. By default
85
+ * the component renders nothing on failure (slot-style silent skip). */
86
+ onUnavailable?: (status?: number) => void;
77
87
  }
78
88
 
79
89
  /**
@@ -87,8 +97,20 @@ export function EmbeddedExtension({
87
97
  context,
88
98
  className,
89
99
  initialHeight = 80,
100
+ onReady,
101
+ onUnavailable,
90
102
  }: EmbeddedExtensionProps) {
91
103
  const iframeRef = useRef<HTMLIFrameElement | null>(null);
104
+ // Latch the readiness signal so onReady fires at most once per iframe
105
+ // instance. Reset when the iframe is recreated (extensionId/updatedAt change).
106
+ const onReadyRef = useRef(onReady);
107
+ onReadyRef.current = onReady;
108
+ const readyFiredRef = useRef(false);
109
+ const fireReady = () => {
110
+ if (readyFiredRef.current) return;
111
+ readyFiredRef.current = true;
112
+ onReadyRef.current?.();
113
+ };
92
114
  const [height, setHeight] = useState<number>(initialHeight);
93
115
  const [isDark, setIsDark] = useState(false);
94
116
  // (audit H4) Mirror ExtensionViewer's role-aware gating; deny-by-default until
@@ -120,6 +142,8 @@ export function EmbeddedExtension({
120
142
  data: extension,
121
143
  isFetching,
122
144
  isLoading,
145
+ isError,
146
+ error,
123
147
  } = useQuery<Extension>({
124
148
  queryKey: ["extension", extensionId],
125
149
  queryFn: async () => {
@@ -141,6 +165,21 @@ export function EmbeddedExtension({
141
165
  retryDelay: (attempt) => Math.min(1000 * 2 ** attempt, 4000),
142
166
  });
143
167
 
168
+ // Notify the host once when the extension can't be loaded for this viewer so
169
+ // it can show a fallback instead of a blank panel.
170
+ const onUnavailableRef = useRef(onUnavailable);
171
+ onUnavailableRef.current = onUnavailable;
172
+ const unavailableFiredRef = useRef(false);
173
+ useEffect(() => {
174
+ unavailableFiredRef.current = false;
175
+ }, [extensionId]);
176
+ useEffect(() => {
177
+ if (isError && !isFetching && !unavailableFiredRef.current) {
178
+ unavailableFiredRef.current = true;
179
+ onUnavailableRef.current?.(extensionLoadErrorStatus(error));
180
+ }
181
+ }, [isError, isFetching, error]);
182
+
144
183
  // Initial dark state is baked into the URL on first load only; subsequent
145
184
  // theme toggles update the iframe's <html class="dark"> via postMessage so
146
185
  // the user's interaction state inside the extension survives the toggle.
@@ -158,6 +197,7 @@ export function EmbeddedExtension({
158
197
  useEffect(() => {
159
198
  bridgeContextRef.current = { role: "viewer", isAuthor: false };
160
199
  bindingLatchedRef.current = false;
200
+ readyFiredRef.current = false;
161
201
  }, [extensionId, extension?.updatedAt]);
162
202
 
163
203
  useEffect(() => {
@@ -215,6 +255,8 @@ export function EmbeddedExtension({
215
255
  const h = Number(message.height);
216
256
  if (Number.isFinite(h) && h > 0) {
217
257
  setHeight(Math.ceil(h));
258
+ // First laid-out height means the content has painted.
259
+ fireReady();
218
260
  }
219
261
  return;
220
262
  }
@@ -330,6 +372,9 @@ export function EmbeddedExtension({
330
372
  { type: "agent-native-slot-context", context: context ?? {} },
331
373
  "*",
332
374
  );
375
+ // Fallback readiness signal in case the extension never reports a
376
+ // height (e.g. fixed-height content that skips auto-resize).
377
+ fireReady();
333
378
  }}
334
379
  />
335
380
  <EmbeddedToolMenu
@@ -101,6 +101,22 @@ const REGISTRY_KEY = "__agentNativeShareableResources__";
101
101
  type RegistryStore = Map<string, ShareableResourceRegistration>;
102
102
  const globalRegistry: { [K in typeof REGISTRY_KEY]?: RegistryStore } =
103
103
  globalThis as any;
104
+
105
+ function isTestRuntime(): boolean {
106
+ return (
107
+ process.env.NODE_ENV === "test" ||
108
+ process.env.VITEST === "true" ||
109
+ process.env.VITEST === "1"
110
+ );
111
+ }
112
+
113
+ function registrationCameFromTestFile(): boolean {
114
+ const stack = new Error().stack ?? "";
115
+ return /[./\\](?:[^/\\]+[.-])(?:test|spec)\.[cm]?[jt]sx?(?::\d+)?(?::\d+)?/.test(
116
+ stack,
117
+ );
118
+ }
119
+
104
120
  function getRegistry(): RegistryStore {
105
121
  let r = globalRegistry[REGISTRY_KEY];
106
122
  if (!r) {
@@ -113,6 +129,7 @@ function getRegistry(): RegistryStore {
113
129
  export function registerShareableResource(
114
130
  entry: ShareableResourceRegistration,
115
131
  ): void {
132
+ if (!isTestRuntime() && registrationCameFromTestFile()) return;
116
133
  getRegistry().set(entry.type, entry);
117
134
  }
118
135
 
@@ -1,6 +1,7 @@
1
1
  overrides:
2
2
  "@assistant-ui/store": ">=0.2.9 <0.2.14"
3
3
  "@assistant-ui/tap": "^0.5.14"
4
+ nf3: "0.3.17"
4
5
 
5
6
  allowBuilds:
6
7
  better-sqlite3: true
@@ -6,6 +6,7 @@ overrides:
6
6
  "@assistant-ui/store": ">=0.2.9 <0.2.14"
7
7
  "@assistant-ui/tap": "^0.5.14"
8
8
  better-auth: "1.6.0"
9
+ nf3: "0.3.17"
9
10
 
10
11
  onlyBuiltDependencies:
11
12
  - "@swc/core"
@@ -56,7 +56,7 @@ When the user asks for a dashboard:
56
56
  2. If a metric definition, date range, or grain is ambiguous and the choice would change the panel's numbers, use the `ask-question` clarifying tool once before building. Skip it when the dictionary or the user already settled it.
57
57
  3. If a metric is not documented, do not guess column names. Ask for the table/columns or introspect the provider schema, then propose a dictionary entry with `save-data-dictionary-entry`.
58
58
  4. Build a complete `SqlDashboardConfig` with `name` and `panels`. Optionally set top-level `columns` (1–6, default 2) to control how many grid columns the panels before any section use.
59
- 5. Every panel needs `id`, `title`, `source`, `chartType`, `width`, and `sql`. `width` is the number of grid columns the panel spans (1..6, clamped to the active section's column count). Section panels skip `source` and `sql` and may set their own `columns` (1–6) to override the dashboard default for the panels following the section.
59
+ 5. Every panel needs `id`, `title`, `source`, `chartType`, `width`, and `sql`. `width` is the number of grid columns the panel spans (1..6, clamped to the active section's column count). Section panels skip `source` and `sql` and may set their own `columns` (1–6) to override the dashboard default for the panels following the section. Extension panels (`chartType: "extension"`) also skip `source` and `sql`; instead they require `config.extensionId` (see "Embedding An Extension As A Panel").
60
60
  6. Persist with `update-dashboard`, not raw SQL or settings writes.
61
61
  7. Navigate to it with `pnpm action navigate --view=adhoc --dashboardId=<id>`.
62
62
 
@@ -85,6 +85,37 @@ production mode, call `create-extension` automatically and then tell the user
85
85
  that the request needed a bespoke surface, so you built it as an extension
86
86
  rather than forcing it into a native dashboard config.
87
87
 
88
+ ## Embedding An Extension As A Panel
89
+
90
+ Use `chartType: "extension"` to embed an existing extension as a dashboard
91
+ panel. This is different from the section above: there you replace the whole
92
+ dashboard with an extension; here you drop a single extension widget into one
93
+ panel slot alongside normal SQL charts. The panel renders the extension's
94
+ sandboxed iframe instead of running a query, so it skips `source` and `sql` and
95
+ instead requires `config.extensionId` (the id of an extension that already
96
+ exists — create it first with `create-extension`). Validation rejects an
97
+ extension panel without a non-empty `config.extensionId`.
98
+
99
+ ```jsonc
100
+ {
101
+ "id": "pipeline-widget",
102
+ "title": "Pipeline Widget",
103
+ "chartType": "extension",
104
+ "width": 3,
105
+ "config": { "extensionId": "<existing-extension-id>" },
106
+ }
107
+ ```
108
+
109
+ Notes:
110
+
111
+ - The panel renders full-bleed (no card chrome/title) and does not receive the
112
+ dashboard's filters/variables/date range — it's a standalone widget for now.
113
+ - Access is scoped per viewer: embedding does NOT grant access to the extension
114
+ (same model as ExtensionSlots). If you share a dashboard more broadly than the
115
+ embedded extension, viewers without access to that extension see an
116
+ "extension unavailable" message instead of the content. Share the extension to
117
+ the same audience as the dashboard so all viewers can see it.
118
+
88
119
  ## Config Shape
89
120
 
90
121
  ```jsonc
@@ -207,7 +238,7 @@ type PanelPatch = {
207
238
  title?: string;
208
239
  sql?: string;
209
240
  source?: "bigquery" | "ga4" | "amplitude" | "first-party" | "demo" | "prometheus";
210
- chartType?: "line" | "area" | "bar" | "metric" | "table" | "pie" | "section" | "heatmap" | "callout";
241
+ chartType?: "line" | "area" | "bar" | "metric" | "table" | "pie" | "section" | "heatmap" | "callout" | "extension";
211
242
  width?: number;
212
243
  columns?: number;
213
244
  tab?: string;
@@ -219,8 +250,9 @@ type PanelInput = PanelPatch & {
219
250
  id: string;
220
251
  title: string;
221
252
  chartType: NonNullable<PanelPatch["chartType"]>;
222
- source?: PanelPatch["source"]; // required for non-section panels
223
- sql?: string; // required for non-section panels
253
+ source?: PanelPatch["source"]; // required for non-section / non-extension panels
254
+ sql?: string; // required for non-section / non-extension panels
255
+ // For chartType "extension": config.extensionId is required (the extension to embed).
224
256
  };
225
257
 
226
258
  type PanelFilter = {
@@ -450,6 +482,12 @@ pnpm action set-resource-visibility --resourceType dashboard --resourceId weekly
450
482
 
451
483
  Writes require editor access; deletes require admin access. Owners always satisfy access checks.
452
484
 
485
+ If a dashboard embeds an extension panel (`chartType: "extension"`), sharing the
486
+ dashboard does not share the extension. Share the referenced extension to the
487
+ same audience (`share-resource --resourceType extension ...`) so all dashboard
488
+ viewers can see the embedded content; otherwise they get an "extension
489
+ unavailable" placeholder.
490
+
453
491
  ## Important Rules
454
492
 
455
493
  - Never fabricate data or create a dashboard from guessed schema. A panel's SQL must hit a real source; do not present figures you did not actually query.
@@ -36,7 +36,7 @@ type PanelPatch = {
36
36
  title?: string;
37
37
  sql?: string;
38
38
  source?: "bigquery" | "ga4" | "amplitude" | "first-party" | "demo" | "prometheus";
39
- chartType?: "line" | "area" | "bar" | "metric" | "table" | "pie" | "section" | "heatmap" | "callout";
39
+ chartType?: "line" | "area" | "bar" | "metric" | "table" | "pie" | "section" | "heatmap" | "callout" | "extension";
40
40
  width?: number;
41
41
  columns?: number;
42
42
  tab?: string;
@@ -115,6 +115,7 @@ export const DASHBOARD_MUTATION_EXAMPLES = [
115
115
  'dashboard.insertPanel({"id":"new-kpi","title":"New KPI","source":"first-party","chartType":"metric","width":1,"sql":"SELECT COUNT(*) AS value FROM analytics_events"}).atTop();',
116
116
  'dashboard.insertPanel({"id":"new-chart","title":"New Chart","source":"first-party","chartType":"line","width":1,"sql":"SELECT date, COUNT(*) AS value FROM analytics_events GROUP BY date ORDER BY date"}).nextTo("retention-over-time");',
117
117
  'dashboard.insertPanel({"id":"row-chart","title":"Row Chart","source":"first-party","chartType":"bar","width":1,"sql":"SELECT name, COUNT(*) AS value FROM analytics_events GROUP BY name"}).atRow(2);',
118
+ 'dashboard.insertPanel({"id":"pipeline-widget","title":"Pipeline Widget","chartType":"extension","width":3,"config":{"extensionId":"<existing-extension-id>"}}).atBottom();',
118
119
  ] as const;
119
120
 
120
121
  export type DashboardMutationOperation =
@@ -334,12 +334,15 @@ export function validateDashboardConfig(
334
334
  if (!p || typeof p !== "object") {
335
335
  return `panel[${i}] must be an object`;
336
336
  }
337
- // Section panels are pure layout dividers, so source and sql are optional.
338
- // Width stays required for backward-compatible dashboard payloads.
337
+ // Section panels are pure layout dividers and extension panels render their
338
+ // own iframe, so both make source and sql optional. Width stays required for
339
+ // backward-compatible dashboard payloads.
339
340
  const isSection = p.chartType === "section";
340
- const required = isSection
341
- ? (["id", "title", "chartType", "width"] as const)
342
- : (["id", "title", "sql", "source", "chartType", "width"] as const);
341
+ const isExtension = p.chartType === "extension";
342
+ const required =
343
+ isSection || isExtension
344
+ ? (["id", "title", "chartType", "width"] as const)
345
+ : (["id", "title", "sql", "source", "chartType", "width"] as const);
343
346
  for (const field of required) {
344
347
  const v = p[field];
345
348
  if (field === "width") {
@@ -352,9 +355,19 @@ export function validateDashboardConfig(
352
355
  return `panel[${i}].${field} is required (non-empty string)`;
353
356
  }
354
357
  }
355
- if (!isSection && !validSources.has(p.source as string)) {
358
+ if (!isSection && !isExtension && !validSources.has(p.source as string)) {
356
359
  return `panel[${i}].source must be 'bigquery', 'ga4', 'amplitude', 'first-party', 'demo', or 'prometheus' (got '${p.source}'). source selects the backend — put the PromQL/SQL/table name in sql, not here.`;
357
360
  }
361
+ if (isExtension) {
362
+ const cfg = p.config as Record<string, unknown> | undefined;
363
+ const extensionId =
364
+ cfg && typeof cfg.extensionId === "string"
365
+ ? cfg.extensionId.trim()
366
+ : "";
367
+ if (!extensionId) {
368
+ return `panel[${i}].config.extensionId is required for extension panels (the id of the extension to render inline)`;
369
+ }
370
+ }
358
371
  if (
359
372
  isSection &&
360
373
  p.columns !== undefined &&
@@ -382,9 +395,10 @@ export async function validatePanelSql(
382
395
  const vars = buildDryRunVars(config);
383
396
  for (let i = 0; i < panels.length; i++) {
384
397
  const p = panels[i] as Record<string, unknown>;
385
- // Sections are layout-only no SQL to dry-run. heatmap, callout, and other
386
- // query panels still validate normally below.
387
- if (p.chartType === "section") continue;
398
+ // Sections are layout-only and extensions render their own iframe neither
399
+ // has SQL to dry-run. heatmap, callout, and other query panels still
400
+ // validate normally below.
401
+ if (p.chartType === "section" || p.chartType === "extension") continue;
388
402
  if (p.source === "amplitude") {
389
403
  const raw = typeof p.sql === "string" ? p.sql : "";
390
404
  if (raw.trim()) {
@@ -1,4 +1,5 @@
1
1
  import { useT } from "@agent-native/core/client";
2
+ import { EmbeddedExtension } from "@agent-native/core/client/extensions";
2
3
  import {
3
4
  IconArrowsSort,
4
5
  IconSortAscending,
@@ -868,7 +869,10 @@ export function SqlChart({
868
869
  const t = useT();
869
870
  // Hooks must be called unconditionally before any early return.
870
871
  const isSection = panel.chartType === "section";
871
- const shouldQuery = !isSection && loadData;
872
+ const isExtension = panel.chartType === "extension";
873
+ // Sections are pure layout and extensions render their own iframe — neither
874
+ // runs the SQL pipeline.
875
+ const shouldQuery = !isSection && !isExtension && loadData;
872
876
  const sql = serializePanelSql(resolvedSql ?? panel.sql);
873
877
  const {
874
878
  data: result,
@@ -918,6 +922,24 @@ export function SqlChart({
918
922
  </div>
919
923
  );
920
924
  }
925
+
926
+ // Extension panels render a sandboxed extension iframe instead of querying a
927
+ // data source. The extension id lives in config.extensionId.
928
+ if (isExtension) {
929
+ const extensionId = panel.config?.extensionId;
930
+ if (!extensionId) {
931
+ return (
932
+ <div className="flex flex-1 items-center justify-center px-4 py-8 min-h-[120px]">
933
+ <p className="text-sm text-muted-foreground text-center">
934
+ {t("sqlDashboard.extensionMissingId")}
935
+ </p>
936
+ </div>
937
+ );
938
+ }
939
+ return (
940
+ <DashboardExtensionPanel extensionId={extensionId} panelId={panel.id} />
941
+ );
942
+ }
921
943
  const colors = panel.config?.colors || DEFAULT_COLORS;
922
944
  const yFormatter = panel.config?.yFormatter;
923
945
 
@@ -1041,6 +1063,55 @@ export function SqlChart({
1041
1063
  );
1042
1064
  }
1043
1065
 
1066
+ function DashboardExtensionPanel({
1067
+ extensionId,
1068
+ panelId,
1069
+ }: {
1070
+ extensionId: string;
1071
+ panelId: string;
1072
+ }) {
1073
+ const t = useT();
1074
+ // Hold the report-readiness marker until the extension iframe paints so
1075
+ // dashboard report screenshots don't capture a blank extension panel.
1076
+ const [ready, setReady] = useState(false);
1077
+ const [unavailable, setUnavailable] = useState(false);
1078
+
1079
+ // Embedding never grants access to the extension itself (same model as
1080
+ // ExtensionSlots). A viewer with dashboard-only access who can't see the
1081
+ // referenced extension gets a clear message instead of a blank panel.
1082
+ if (unavailable) {
1083
+ return (
1084
+ <div className="flex flex-1 items-center justify-center px-4 py-8 min-h-[120px]">
1085
+ <p className="text-sm text-muted-foreground text-center">
1086
+ {t("sqlDashboard.extensionUnavailable")}
1087
+ </p>
1088
+ </div>
1089
+ );
1090
+ }
1091
+
1092
+ return (
1093
+ <div
1094
+ className="w-full"
1095
+ data-dashboard-report-loading={ready ? undefined : "true"}
1096
+ >
1097
+ <EmbeddedExtension
1098
+ extensionId={extensionId}
1099
+ slotId={`dashboard-panel-${panelId}`}
1100
+ className="w-full"
1101
+ // Intentional for v1: extension panels are standalone widgets and do not
1102
+ // receive the dashboard's filters/variables/date range as `context`.
1103
+ initialHeight={180}
1104
+ onReady={() => setReady(true)}
1105
+ onUnavailable={() => {
1106
+ // Clear the report-loading gate so report capture doesn't hang.
1107
+ setReady(true);
1108
+ setUnavailable(true);
1109
+ }}
1110
+ />
1111
+ </div>
1112
+ );
1113
+ }
1114
+
1044
1115
  function MetricRenderer({
1045
1116
  rows,
1046
1117
  panel,
@@ -629,6 +629,8 @@ const messages = {
629
629
  off: "Off",
630
630
  since: "Since",
631
631
  sectionOptions: "部分選項",
632
+ extensionMissingId: "此擴充面板未選擇任何擴充功能。",
633
+ extensionUnavailable: "此擴充功能未與您共用,或已不存在。",
632
634
  panelOptions: "面板選項",
633
635
  fullScreen: "全螢幕",
634
636
  refresh: "重新整理",
@@ -392,6 +392,9 @@ const enUS = {
392
392
  off: "Off",
393
393
  since: "Since",
394
394
  sectionOptions: "Section options",
395
+ extensionMissingId: "This extension panel has no extension selected.",
396
+ extensionUnavailable:
397
+ "This extension isn't shared with you, or it no longer exists.",
395
398
  panelOptions: "Panel options",
396
399
  fullScreen: "Full screen",
397
400
  refresh: "Refresh",
@@ -5249,6 +5252,8 @@ const translatedAnalyticsDebtTranslations = {
5249
5252
  saveView: "保存视图",
5250
5253
  savedViews: "已保存的视图",
5251
5254
  sectionOptions: "部分选项",
5255
+ extensionMissingId: "此扩展面板未选择任何扩展。",
5256
+ extensionUnavailable: "此扩展未与您共享,或已不存在。",
5252
5257
  sharedWithOrg: "与组织共享",
5253
5258
  unhideFailed: "无法取消隐藏仪表板",
5254
5259
  untitledDashboard: "无标题仪表板",
@@ -5424,6 +5429,10 @@ const translatedAnalyticsDebtTranslations = {
5424
5429
  saveView: "Guardar vista",
5425
5430
  savedViews: "Vistas guardadas",
5426
5431
  sectionOptions: "Opciones de sección",
5432
+ extensionMissingId:
5433
+ "Este panel de extensión no tiene ninguna extensión seleccionada.",
5434
+ extensionUnavailable:
5435
+ "Esta extensión no está compartida contigo o ya no existe.",
5427
5436
  sharedWithOrg: "Compartido con la organización",
5428
5437
  unhideFailed: "No se pudo mostrar el panel",
5429
5438
  untitledDashboard: "Panel de control sin título",
@@ -5601,6 +5610,10 @@ const translatedAnalyticsDebtTranslations = {
5601
5610
  saveView: "Enregistrer la vue",
5602
5611
  savedViews: "Vues enregistrées",
5603
5612
  sectionOptions: "Options de sections",
5613
+ extensionMissingId:
5614
+ "Ce panneau d'extension n'a aucune extension sélectionnée.",
5615
+ extensionUnavailable:
5616
+ "Cette extension n'est pas partagée avec vous ou n'existe plus.",
5604
5617
  sharedWithOrg: "Partagé avec l'organisation",
5605
5618
  unhideFailed: "Impossible d'afficher le tableau de bord",
5606
5619
  untitledDashboard: "Tableau de bord sans titre",
@@ -5779,6 +5792,10 @@ const translatedAnalyticsDebtTranslations = {
5779
5792
  saveView: "Ansicht speichern",
5780
5793
  savedViews: "Gespeicherte Ansichten",
5781
5794
  sectionOptions: "Abschnittsoptionen",
5795
+ extensionMissingId:
5796
+ "Für dieses Erweiterungs-Panel ist keine Erweiterung ausgewählt.",
5797
+ extensionUnavailable:
5798
+ "Diese Erweiterung ist nicht für Sie freigegeben oder existiert nicht mehr.",
5782
5799
  sharedWithOrg: "Mit Org geteilt",
5783
5800
  unhideFailed: "Das Dashboard konnte nicht eingeblendet werden",
5784
5801
  untitledDashboard: "Unbenanntes Dashboard",
@@ -5952,6 +5969,10 @@ const translatedAnalyticsDebtTranslations = {
5952
5969
  saveView: "ビューを保存",
5953
5970
  savedViews: "保存されたビュー",
5954
5971
  sectionOptions: "セクションのオプション",
5972
+ extensionMissingId:
5973
+ "この拡張機能パネルには拡張機能が選択されていません。",
5974
+ extensionUnavailable:
5975
+ "この拡張機能はあなたと共有されていないか、存在しません。",
5955
5976
  sharedWithOrg: "組織と共有",
5956
5977
  unhideFailed: "ダッシュボードを再表示できませんでした",
5957
5978
  untitledDashboard: "無題のダッシュボード",
@@ -6124,6 +6145,10 @@ const translatedAnalyticsDebtTranslations = {
6124
6145
  saveView: "보기 저장",
6125
6146
  savedViews: "저장된 보기",
6126
6147
  sectionOptions: "섹션 옵션",
6148
+ extensionMissingId:
6149
+ "이 확장 프로그램 패널에 선택된 확장 프로그램이 없습니다.",
6150
+ extensionUnavailable:
6151
+ "이 확장 프로그램이 공유되지 않았거나 더 이상 존재하지 않습니다.",
6127
6152
  sharedWithOrg: "조직과 공유됨",
6128
6153
  unhideFailed: "대시보드를 숨기기 해제할 수 없습니다.",
6129
6154
  untitledDashboard: "제목 없는 대시보드",
@@ -6300,6 +6325,10 @@ const translatedAnalyticsDebtTranslations = {
6300
6325
  saveView: "Salvar visualização",
6301
6326
  savedViews: "Visualizações salvas",
6302
6327
  sectionOptions: "Opções de seção",
6328
+ extensionMissingId:
6329
+ "Este painel de extensão não tem nenhuma extensão selecionada.",
6330
+ extensionUnavailable:
6331
+ "Esta extensão não foi compartilhada com você ou não existe mais.",
6303
6332
  sharedWithOrg: "Compartilhado com a organização",
6304
6333
  unhideFailed: "Não foi possível exibir o painel",
6305
6334
  untitledDashboard: "Painel sem título",
@@ -6472,6 +6501,9 @@ const translatedAnalyticsDebtTranslations = {
6472
6501
  saveView: "दृश्य सहेजें",
6473
6502
  savedViews: "सहेजे गए दृश्य",
6474
6503
  sectionOptions: "अनुभाग विकल्प",
6504
+ extensionMissingId: "इस एक्सटेंशन पैनल में कोई एक्सटेंशन चयनित नहीं है।",
6505
+ extensionUnavailable:
6506
+ "यह एक्सटेंशन आपके साथ साझा नहीं किया गया है, या अब मौजूद नहीं है।",
6475
6507
  sharedWithOrg: "संगठन के साथ साझा किया गया",
6476
6508
  unhideFailed: "डैशबोर्ड को उजागर नहीं किया जा सका",
6477
6509
  untitledDashboard: "शीर्षक रहित डैशबोर्ड",
@@ -6644,6 +6676,8 @@ const translatedAnalyticsDebtTranslations = {
6644
6676
  saveView: "حفظ العرض",
6645
6677
  savedViews: "المشاهدات المحفوظة",
6646
6678
  sectionOptions: "خيارات القسم",
6679
+ extensionMissingId: "لم يتم تحديد أي إضافة في لوحة الإضافة هذه.",
6680
+ extensionUnavailable: "هذه الإضافة غير مشاركة معك، أو لم تعد موجودة.",
6647
6681
  sharedWithOrg: "تمت المشاركة مع منظمة",
6648
6682
  unhideFailed: "تعذر إظهار لوحة البيانات",
6649
6683
  untitledDashboard: "لوحة تحكم بلا عنوان",