@agent-native/core 0.80.6 → 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.
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: 4587
31
+ - template files: 4588
@@ -1,5 +1,11 @@
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
+
3
9
  ## 0.80.6
4
10
 
5
11
  ### Patch Changes
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-native/core",
3
- "version": "0.80.6",
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": {
@@ -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
@@ -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: "لوحة تحكم بلا عنوان",
@@ -121,7 +121,9 @@ export function SqlChartCard({
121
121
  const [expanded, setExpanded] = useState(false);
122
122
  const [exportCsv, setExportCsv] = useState<(() => void) | null>(null);
123
123
  const [shouldLoadData, setShouldLoadData] = useState(
124
- eagerLoad || panel.chartType === "section",
124
+ eagerLoad ||
125
+ panel.chartType === "section" ||
126
+ panel.chartType === "extension",
125
127
  );
126
128
  const cardRef = useRef<HTMLDivElement | null>(null);
127
129
  const chartQueryKey = useMemo(
@@ -159,7 +161,9 @@ export function SqlChartCard({
159
161
  setShouldLoadData(true);
160
162
  return;
161
163
  }
162
- if (panel.chartType === "section") {
164
+ // Sections are layout-only and extensions render their own iframe — neither
165
+ // waits on the intersection observer that gates SQL panels.
166
+ if (panel.chartType === "section" || panel.chartType === "extension") {
163
167
  setShouldLoadData(true);
164
168
  return;
165
169
  }
@@ -286,6 +290,88 @@ export function SqlChartCard({
286
290
  );
287
291
  }
288
292
 
293
+ // Extension panels render their sandboxed iframe full-bleed with no card chrome
294
+ // or title — the extension owns its own UI. Editable dashboards still get a
295
+ // hover overlay for delete/drag. Editing routes through the agent (the manual
296
+ // panel editor has no extension picker), so no inline edit action here.
297
+ if (panel.chartType === "extension") {
298
+ return (
299
+ <div
300
+ ref={setCardNodeRef}
301
+ style={isDragSource ? { zIndex: 50 } : undefined}
302
+ data-dragging={isDragSource ? "true" : undefined}
303
+ className="dashboard-extension-card group relative h-full"
304
+ >
305
+ <SqlChart panel={panel} resolvedSql={resolvedSql} loadData />
306
+ {editable ? (
307
+ <div className="absolute right-1 top-1 flex items-center gap-1 opacity-0 group-hover:opacity-100">
308
+ <DropdownMenu>
309
+ <Tooltip>
310
+ <TooltipTrigger asChild>
311
+ <DropdownMenuTrigger asChild>
312
+ <button
313
+ className="p-1 rounded bg-background/80 text-muted-foreground hover:text-foreground"
314
+ aria-label={t("sqlDashboard.panelOptions")}
315
+ >
316
+ <IconDotsVertical className="h-3.5 w-3.5" />
317
+ </button>
318
+ </DropdownMenuTrigger>
319
+ </TooltipTrigger>
320
+ <TooltipContent>
321
+ {t("sqlDashboard.panelOptions")}
322
+ </TooltipContent>
323
+ </Tooltip>
324
+ <DropdownMenuContent align="end" className="w-40">
325
+ <DropdownMenuItem
326
+ onSelect={(e) => {
327
+ e.preventDefault();
328
+ setConfirmOpen(true);
329
+ }}
330
+ >
331
+ <IconTrash className="h-4 w-4 mr-2" />
332
+ {t("sidebar.delete")}
333
+ </DropdownMenuItem>
334
+ </DropdownMenuContent>
335
+ </DropdownMenu>
336
+ <PanelDragHandle
337
+ panelId={panel.id}
338
+ label={t("sqlDashboard.dragToReorder")}
339
+ className="p-1 rounded bg-background/80 cursor-grab active:cursor-grabbing text-muted-foreground/50 hover:text-muted-foreground"
340
+ iconClassName="h-3.5 w-3.5"
341
+ />
342
+ </div>
343
+ ) : null}
344
+ {editable ? (
345
+ <AlertDialog open={confirmOpen} onOpenChange={setConfirmOpen}>
346
+ <AlertDialogContent>
347
+ <AlertDialogHeader>
348
+ <AlertDialogTitle>
349
+ {t("sqlDashboard.deletePanelTitle")}
350
+ </AlertDialogTitle>
351
+ <AlertDialogDescription>
352
+ {t("sqlDashboard.deletePanelDescription", {
353
+ title: panel.title,
354
+ })}
355
+ </AlertDialogDescription>
356
+ </AlertDialogHeader>
357
+ <AlertDialogFooter>
358
+ <AlertDialogCancel>{t("sidebar.cancel")}</AlertDialogCancel>
359
+ <AlertDialogAction
360
+ onClick={() => {
361
+ setConfirmOpen(false);
362
+ onRemove();
363
+ }}
364
+ >
365
+ {t("sidebar.delete")}
366
+ </AlertDialogAction>
367
+ </AlertDialogFooter>
368
+ </AlertDialogContent>
369
+ </AlertDialog>
370
+ ) : null}
371
+ </div>
372
+ );
373
+ }
374
+
289
375
  // Every non-section panel exposes at least the Full screen view action, so the
290
376
  // options menu always renders — including on read-only / shared dashboards.
291
377
  const showPanelMenu = true;
@@ -15,7 +15,8 @@ export type ChartType =
15
15
  | "pie"
16
16
  | "section"
17
17
  | "heatmap"
18
- | "callout";
18
+ | "callout"
19
+ | "extension";
19
20
 
20
21
  export type FilterType =
21
22
  | "date"
@@ -79,6 +80,11 @@ export interface SqlPanelConfig {
79
80
  sortable?: boolean;
80
81
  columns?: TableColumnConfig[];
81
82
  limit?: number;
83
+ /**
84
+ * Extension panels only (`chartType: "extension"`): id of the extension to
85
+ * render inline as a sandboxed iframe instead of running the SQL pipeline.
86
+ */
87
+ extensionId?: string;
82
88
  }
83
89
 
84
90
  export interface SqlPanel {
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: added
3
+ date: 2026-06-29
4
+ ---
5
+
6
+ Dashboards can now include extension panels that embed a sandboxed extension inline instead of a SQL chart
@@ -10,11 +10,20 @@ export interface EmbeddedExtensionProps {
10
10
  className?: string;
11
11
  /** Initial iframe height before content reports a real height. */
12
12
  initialHeight?: number;
13
+ /** Fires once when the embedded iframe first signals content readiness — its
14
+ * first height report, or iframe load as a fallback. Hosts that gate on
15
+ * content paint (e.g. dashboard report screenshots) use this. */
16
+ onReady?: () => void;
17
+ /** Fires when the extension can't be loaded for this viewer (e.g. 403/404 —
18
+ * the extension isn't shared with them or no longer exists). Hosts can use
19
+ * this to render an explanatory fallback instead of a blank panel. By default
20
+ * the component renders nothing on failure (slot-style silent skip). */
21
+ onUnavailable?: (status?: number) => void;
13
22
  }
14
23
  /**
15
24
  * Renders a extension inline as a small auto-sized iframe — for use inside an
16
25
  * `<ExtensionSlot>`. Different from `<ExtensionViewer>` (which is full-page with a
17
26
  * toolbar): no header, sized to content, receives a `slotContext`.
18
27
  */
19
- export declare function EmbeddedExtension({ extensionId, slotId, context, className, initialHeight, }: EmbeddedExtensionProps): import("react").JSX.Element | null;
28
+ export declare function EmbeddedExtension({ extensionId, slotId, context, className, initialHeight, onReady, onUnavailable, }: EmbeddedExtensionProps): import("react").JSX.Element | null;
20
29
  //# sourceMappingURL=EmbeddedExtension.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"EmbeddedExtension.d.ts","sourceRoot":"","sources":["../../../src/client/extensions/EmbeddedExtension.tsx"],"names":[],"mappings":"AAgEA,MAAM,WAAW,sBAAsB;IACrC,WAAW,EAAE,MAAM,CAAC;IACpB;2CACuC;IACvC,MAAM,EAAE,MAAM,CAAC;IACf;iDAC6C;IAC7C,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IACzC,0DAA0D;IAC1D,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,kEAAkE;IAClE,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,EAChC,WAAW,EACX,MAAM,EACN,OAAO,EACP,SAAS,EACT,aAAkB,GACnB,EAAE,sBAAsB,sCA6PxB"}
1
+ {"version":3,"file":"EmbeddedExtension.d.ts","sourceRoot":"","sources":["../../../src/client/extensions/EmbeddedExtension.tsx"],"names":[],"mappings":"AAiEA,MAAM,WAAW,sBAAsB;IACrC,WAAW,EAAE,MAAM,CAAC;IACpB;2CACuC;IACvC,MAAM,EAAE,MAAM,CAAC;IACf;iDAC6C;IAC7C,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IACzC,0DAA0D;IAC1D,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,kEAAkE;IAClE,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB;;qEAEiE;IACjE,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB;;;4EAGwE;IACxE,aAAa,CAAC,EAAE,CAAC,MAAM,CAAC,EAAE,MAAM,KAAK,IAAI,CAAC;CAC3C;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,EAChC,WAAW,EACX,MAAM,EACN,OAAO,EACP,SAAS,EACT,aAAkB,EAClB,OAAO,EACP,aAAa,GACd,EAAE,sBAAsB,sCA8RxB"}
@@ -10,7 +10,7 @@ import { Popover, PopoverContent, PopoverTrigger, } from "../components/ui/popov
10
10
  import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, } from "../components/ui/tooltip.js";
11
11
  import { useT } from "../i18n.js";
12
12
  import { deleteOrHideExtension, invalidateExtensionRemoval, } from "./delete-extension.js";
13
- import { extensionLoadError, shouldRetryExtensionLoad, } from "./extension-load-error.js";
13
+ import { extensionLoadError, extensionLoadErrorStatus, shouldRetryExtensionLoad, } from "./extension-load-error.js";
14
14
  import { isAllowedExtensionPath, sanitizeExtensionRequestOptions, checkBridgePolicy, } from "./iframe-bridge.js";
15
15
  function serializeChatValue(value) {
16
16
  if (value === undefined || value === null)
@@ -29,8 +29,19 @@ function serializeChatValue(value) {
29
29
  * `<ExtensionSlot>`. Different from `<ExtensionViewer>` (which is full-page with a
30
30
  * toolbar): no header, sized to content, receives a `slotContext`.
31
31
  */
32
- export function EmbeddedExtension({ extensionId, slotId, context, className, initialHeight = 80, }) {
32
+ export function EmbeddedExtension({ extensionId, slotId, context, className, initialHeight = 80, onReady, onUnavailable, }) {
33
33
  const iframeRef = useRef(null);
34
+ // Latch the readiness signal so onReady fires at most once per iframe
35
+ // instance. Reset when the iframe is recreated (extensionId/updatedAt change).
36
+ const onReadyRef = useRef(onReady);
37
+ onReadyRef.current = onReady;
38
+ const readyFiredRef = useRef(false);
39
+ const fireReady = () => {
40
+ if (readyFiredRef.current)
41
+ return;
42
+ readyFiredRef.current = true;
43
+ onReadyRef.current?.();
44
+ };
34
45
  const [height, setHeight] = useState(initialHeight);
35
46
  const [isDark, setIsDark] = useState(false);
36
47
  // (audit H4) Mirror ExtensionViewer's role-aware gating; deny-by-default until
@@ -56,7 +67,7 @@ export function EmbeddedExtension({ extensionId, slotId, context, className, ini
56
67
  });
57
68
  return () => observer.disconnect();
58
69
  }, []);
59
- const { data: extension, isFetching, isLoading, } = useQuery({
70
+ const { data: extension, isFetching, isLoading, isError, error, } = useQuery({
60
71
  queryKey: ["extension", extensionId],
61
72
  queryFn: async () => {
62
73
  const res = await fetch(agentNativePath(`/_agent-native/extensions/${extensionId}`));
@@ -74,6 +85,20 @@ export function EmbeddedExtension({ extensionId, slotId, context, className, ini
74
85
  retry: shouldRetryExtensionLoad,
75
86
  retryDelay: (attempt) => Math.min(1000 * 2 ** attempt, 4000),
76
87
  });
88
+ // Notify the host once when the extension can't be loaded for this viewer so
89
+ // it can show a fallback instead of a blank panel.
90
+ const onUnavailableRef = useRef(onUnavailable);
91
+ onUnavailableRef.current = onUnavailable;
92
+ const unavailableFiredRef = useRef(false);
93
+ useEffect(() => {
94
+ unavailableFiredRef.current = false;
95
+ }, [extensionId]);
96
+ useEffect(() => {
97
+ if (isError && !isFetching && !unavailableFiredRef.current) {
98
+ unavailableFiredRef.current = true;
99
+ onUnavailableRef.current?.(extensionLoadErrorStatus(error));
100
+ }
101
+ }, [isError, isFetching, error]);
77
102
  // Initial dark state is baked into the URL on first load only; subsequent
78
103
  // theme toggles update the iframe's <html class="dark"> via postMessage so
79
104
  // the user's interaction state inside the extension survives the toggle.
@@ -88,6 +113,7 @@ export function EmbeddedExtension({ extensionId, slotId, context, className, ini
88
113
  useEffect(() => {
89
114
  bridgeContextRef.current = { role: "viewer", isAuthor: false };
90
115
  bindingLatchedRef.current = false;
116
+ readyFiredRef.current = false;
91
117
  }, [extensionId, extension?.updatedAt]);
92
118
  useEffect(() => {
93
119
  const win = iframeRef.current?.contentWindow;
@@ -140,6 +166,8 @@ export function EmbeddedExtension({ extensionId, slotId, context, className, ini
140
166
  const h = Number(message.height);
141
167
  if (Number.isFinite(h) && h > 0) {
142
168
  setHeight(Math.ceil(h));
169
+ // First laid-out height means the content has painted.
170
+ fireReady();
143
171
  }
144
172
  return;
145
173
  }
@@ -231,6 +259,9 @@ export function EmbeddedExtension({ extensionId, slotId, context, className, ini
231
259
  }
232
260
  return (_jsxs("div", { className: `relative group/embedded-extension ${className ?? ""}`, children: [_jsx("iframe", { ref: iframeRef, src: iframeSrc, title: extension.name, sandbox: "allow-scripts allow-forms", style: { width: "100%", border: 0, height, display: "block" }, onLoad: () => {
233
261
  iframeRef.current?.contentWindow?.postMessage({ type: "agent-native-slot-context", context: context ?? {} }, "*");
262
+ // Fallback readiness signal in case the extension never reports a
263
+ // height (e.g. fixed-height content that skips auto-resize).
264
+ fireReady();
234
265
  } }, `${extensionId}-${extension.updatedAt ?? ""}`), _jsx(EmbeddedToolMenu, { extensionId: extensionId, slotId: slotId, toolName: extension.name, canDelete: extension.canDelete })] }));
235
266
  }
236
267
  function EmbeddedToolMenu({ extensionId, slotId, toolName, canDelete, }) {
@@ -1 +1 @@
1
- {"version":3,"file":"EmbeddedExtension.js","sourceRoot":"","sources":["../../../src/client/extensions/EmbeddedExtension.tsx"],"names":[],"mappings":"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","sourcesContent":["import {\n IconDots,\n IconExternalLink,\n IconLayoutSidebarRightCollapse,\n IconTrash,\n} from \"@tabler/icons-react\";\nimport { useQuery, useQueryClient } from \"@tanstack/react-query\";\nimport { useEffect, useMemo, useRef, useState } from \"react\";\nimport { useNavigate } from \"react-router\";\n\nimport { extensionPath } from \"../../extensions/path.js\";\nimport { sendToAgentChat } from \"../agent-chat.js\";\nimport { agentNativePath } from \"../api-path.js\";\nimport {\n Popover,\n PopoverContent,\n PopoverTrigger,\n} from \"../components/ui/popover.js\";\nimport {\n Tooltip,\n TooltipContent,\n TooltipProvider,\n TooltipTrigger,\n} from \"../components/ui/tooltip.js\";\nimport { useT } from \"../i18n.js\";\nimport {\n deleteOrHideExtension,\n invalidateExtensionRemoval,\n} from \"./delete-extension.js\";\nimport {\n extensionLoadError,\n shouldRetryExtensionLoad,\n} from \"./extension-load-error.js\";\nimport {\n isAllowedExtensionPath,\n sanitizeExtensionRequestOptions,\n checkBridgePolicy,\n type BridgePolicyContext,\n type ExtensionBridgeRole,\n} from \"./iframe-bridge.js\";\n\ninterface Extension {\n id: string;\n name: string;\n description?: string;\n content?: string;\n updatedAt?: string;\n canDelete?: boolean;\n source?: {\n mode?: \"database\" | \"local-files\";\n permissions?: BridgePolicyContext[\"permissions\"];\n };\n}\n\nfunction serializeChatValue(value: unknown): string | undefined {\n if (value === undefined || value === null) return undefined;\n if (typeof value === \"string\") return value;\n try {\n return JSON.stringify(value);\n } catch {\n return String(value);\n }\n}\n\nexport interface EmbeddedExtensionProps {\n extensionId: string;\n /** Slot identifier passed via the iframe URL so the extension runtime knows it's\n * embedded and enables auto-resize. */\n slotId: string;\n /** Object pushed into the extension as `window.slotContext`. Re-posted whenever\n * the host re-renders with a new context. */\n context?: Record<string, unknown> | null;\n /** Optional className applied to the iframe container. */\n className?: string;\n /** Initial iframe height before content reports a real height. */\n initialHeight?: number;\n}\n\n/**\n * Renders a extension inline as a small auto-sized iframe — for use inside an\n * `<ExtensionSlot>`. Different from `<ExtensionViewer>` (which is full-page with a\n * toolbar): no header, sized to content, receives a `slotContext`.\n */\nexport function EmbeddedExtension({\n extensionId,\n slotId,\n context,\n className,\n initialHeight = 80,\n}: EmbeddedExtensionProps) {\n const iframeRef = useRef<HTMLIFrameElement | null>(null);\n const [height, setHeight] = useState<number>(initialHeight);\n const [isDark, setIsDark] = useState(false);\n // (audit H4) Mirror ExtensionViewer's role-aware gating; deny-by-default until\n // the iframe's render binding announcement arrives.\n const bridgeContextRef = useRef<BridgePolicyContext>({\n role: \"viewer\",\n isAuthor: false,\n });\n // (audit H4) Latch the render binding once per iframe instance. The shell\n // posts the server-resolved binding BEFORE user content runs; any later\n // agent-native-extension-binding message is attacker-controllable (it\n // originates inside the same sandboxed realm as user code) and must be\n // ignored so a viewer cannot self-escalate to owner.\n const bindingLatchedRef = useRef(false);\n\n useEffect(() => {\n setIsDark(document.documentElement.classList.contains(\"dark\"));\n const observer = new MutationObserver(() => {\n setIsDark(document.documentElement.classList.contains(\"dark\"));\n });\n observer.observe(document.documentElement, {\n attributes: true,\n attributeFilter: [\"class\"],\n });\n return () => observer.disconnect();\n }, []);\n\n const {\n data: extension,\n isFetching,\n isLoading,\n } = useQuery<Extension>({\n queryKey: [\"extension\", extensionId],\n queryFn: async () => {\n const res = await fetch(\n agentNativePath(`/_agent-native/extensions/${extensionId}`),\n );\n if (res.status === 404) {\n throw extensionLoadError(404, \"Extension not found\");\n }\n if (res.status === 403) {\n throw extensionLoadError(403, \"Extension access denied\");\n }\n if (!res.ok) {\n throw extensionLoadError(res.status, \"Failed to fetch extension\");\n }\n return res.json();\n },\n retry: shouldRetryExtensionLoad,\n retryDelay: (attempt) => Math.min(1000 * 2 ** attempt, 4000),\n });\n\n // Initial dark state is baked into the URL on first load only; subsequent\n // theme toggles update the iframe's <html class=\"dark\"> via postMessage so\n // the user's interaction state inside the extension survives the toggle.\n const initialDarkRef = useRef(isDark);\n const iframeSrc = useMemo(() => {\n const v = encodeURIComponent(extension?.updatedAt ?? \"\");\n return agentNativePath(\n `/_agent-native/extensions/${extensionId}/render?slot=${encodeURIComponent(slotId)}&dark=${initialDarkRef.current}&v=${v}`,\n );\n }, [extensionId, slotId, extension?.updatedAt]);\n\n // Reset role + binding latch to deny-by-default whenever the iframe is\n // recreated (its key changes). The new render's first binding announcement\n // re-establishes the role.\n useEffect(() => {\n bridgeContextRef.current = { role: \"viewer\", isAuthor: false };\n bindingLatchedRef.current = false;\n }, [extensionId, extension?.updatedAt]);\n\n useEffect(() => {\n const win = iframeRef.current?.contentWindow;\n if (!win) return;\n win.postMessage({ type: \"agent-native-theme-update\", isDark }, \"*\");\n }, [isDark]);\n\n // Forward slot context whenever it changes. The iframe's own load handler\n // posts the initial value once it's ready; this effect handles updates.\n const contextJson = JSON.stringify(context ?? {});\n useEffect(() => {\n const win = iframeRef.current?.contentWindow;\n if (!win) return;\n win.postMessage(\n { type: \"agent-native-slot-context\", context: context ?? {} },\n \"*\",\n );\n }, [contextJson]);\n\n // Bridge extension requests + height reports.\n useEffect(() => {\n const handleMessage = async (event: MessageEvent) => {\n if (event.source !== iframeRef.current?.contentWindow) return;\n const message = event.data;\n if (!message || typeof message !== \"object\") return;\n\n if (message.type === \"agent-native-extension-binding\") {\n // Only the FIRST announcement (sent by the shell before user content\n // runs) is trusted. Ignore re-announcements — a malicious extension\n // body could otherwise postMessage a forged owner binding to escalate.\n if (bindingLatchedRef.current) return;\n bindingLatchedRef.current = true;\n const binding = (message as any).binding ?? {};\n const role: ExtensionBridgeRole =\n binding.role === \"owner\" ||\n binding.role === \"admin\" ||\n binding.role === \"editor\" ||\n binding.role === \"viewer\"\n ? binding.role\n : \"viewer\";\n bridgeContextRef.current = {\n role,\n isAuthor: !!binding.isAuthor,\n source: binding.source === \"local-files\" ? \"local-files\" : \"database\",\n permissions:\n binding && typeof binding.permissions === \"object\"\n ? binding.permissions\n : undefined,\n };\n return;\n }\n\n if (message.type === \"agent-native-extension-resize\") {\n const h = Number(message.height);\n if (Number.isFinite(h) && h > 0) {\n setHeight(Math.ceil(h));\n }\n return;\n }\n\n if (message.type === \"agent-native-send-to-chat\") {\n const text = serializeChatValue((message as any).message);\n if (!text?.trim()) return;\n sendToAgentChat({\n message: text,\n context: serializeChatValue((message as any).context),\n submit: (message as any).submit !== false,\n openSidebar: (message as any).openSidebar !== false,\n });\n return;\n }\n\n if (message.type !== \"agent-native-extension-request\") return;\n\n const requestId = String(message.requestId ?? \"\");\n const path = String(message.path ?? \"\");\n const respond = (payload: Record<string, unknown>) => {\n iframeRef.current?.contentWindow?.postMessage(\n { type: \"agent-native-extension-response\", requestId, ...payload },\n \"*\",\n );\n };\n\n if (!requestId || !isAllowedExtensionPath(path, extensionId)) {\n respond({ error: \"Extension request path is not allowed\" });\n return;\n }\n\n try {\n const options = sanitizeExtensionRequestOptions(message.options);\n // (audit H4) Role-aware gating: viewer-shared extensions can read but not\n // write. The bridge policy is decided here in the parent before the\n // request leaves; the server enforces a second layer.\n const policy = checkBridgePolicy(path, options.method ?? \"GET\", {\n ...bridgeContextRef.current,\n extensionId,\n });\n if (!policy.ok) {\n respond({\n response: {\n ok: false,\n status: 403,\n statusText: \"Forbidden\",\n body: { error: policy.error },\n },\n });\n return;\n }\n // (audit H5) Same extension-bridge tagging as <ExtensionViewer>. action-routes\n // uses these headers to enforce per-action `toolCallable` opt-in.\n const finalHeaders = new Headers(options.headers ?? undefined);\n finalHeaders.set(\"X-Agent-Native-Extension-Bridge\", \"1\");\n finalHeaders.set(\"X-Agent-Native-Extension-Id\", extensionId);\n finalHeaders.set(\"X-Agent-Native-Tool-Bridge\", \"1\");\n finalHeaders.set(\"X-Agent-Native-Tool-Id\", extensionId);\n const res = await fetch(agentNativePath(path), {\n ...options,\n headers: finalHeaders,\n credentials: \"same-origin\",\n });\n const text = await res.text();\n let body: unknown = text;\n if (text) {\n try {\n body = JSON.parse(text);\n } catch {\n body = text;\n }\n }\n respond({\n response: {\n ok: res.ok,\n status: res.status,\n statusText: res.statusText,\n body,\n },\n });\n } catch (err: any) {\n respond({ error: err?.message ?? \"Extension host request failed\" });\n }\n };\n\n window.addEventListener(\"message\", handleMessage);\n return () => window.removeEventListener(\"message\", handleMessage);\n }, [extensionId]);\n\n if (!extension) {\n if (!isLoading && !isFetching) return null;\n return (\n <div\n className={className}\n style={{ height: initialHeight }}\n aria-busy=\"true\"\n />\n );\n }\n\n return (\n <div className={`relative group/embedded-extension ${className ?? \"\"}`}>\n <iframe\n ref={iframeRef}\n key={`${extensionId}-${extension.updatedAt ?? \"\"}`}\n src={iframeSrc}\n title={extension.name}\n sandbox=\"allow-scripts allow-forms\"\n style={{ width: \"100%\", border: 0, height, display: \"block\" }}\n onLoad={() => {\n iframeRef.current?.contentWindow?.postMessage(\n { type: \"agent-native-slot-context\", context: context ?? {} },\n \"*\",\n );\n }}\n />\n <EmbeddedToolMenu\n extensionId={extensionId}\n slotId={slotId}\n toolName={extension.name}\n canDelete={extension.canDelete}\n />\n </div>\n );\n}\n\nfunction EmbeddedToolMenu({\n extensionId,\n slotId,\n toolName,\n canDelete,\n}: {\n extensionId: string;\n slotId: string;\n toolName: string;\n canDelete?: boolean;\n}) {\n const t = useT();\n const [open, setOpen] = useState(false);\n const [confirmingDelete, setConfirmingDelete] = useState(false);\n const queryClient = useQueryClient();\n const navigate = useNavigate();\n\n const closeMenu = () => {\n setOpen(false);\n setConfirmingDelete(false);\n };\n\n const removeFromSlot = async () => {\n closeMenu();\n queryClient.setQueryData<any[]>([\"slot-installs\", slotId], (old) =>\n (old ?? []).filter((i) => i.extensionId !== extensionId),\n );\n try {\n await fetch(\n agentNativePath(\n `/_agent-native/slots/${encodeURIComponent(slotId)}/install/${encodeURIComponent(extensionId)}`,\n ),\n { method: \"DELETE\" },\n );\n } finally {\n queryClient.invalidateQueries({ queryKey: [\"slot-installs\", slotId] });\n }\n };\n\n const deleteExtension = async () => {\n closeMenu();\n try {\n await deleteOrHideExtension({ id: extensionId, canDelete });\n invalidateExtensionRemoval(queryClient, extensionId);\n } catch {\n queryClient.invalidateQueries({ queryKey: [\"extension\", extensionId] });\n }\n };\n\n return (\n <Popover\n open={open}\n onOpenChange={(o) => {\n setOpen(o);\n if (!o) setConfirmingDelete(false);\n }}\n >\n <TooltipProvider delayDuration={200}>\n <Tooltip>\n <TooltipTrigger asChild>\n <PopoverTrigger asChild>\n <button\n type=\"button\"\n className=\"absolute top-1 right-1 flex h-6 w-6 items-center justify-center rounded-md bg-background/60 text-muted-foreground/60 opacity-0 hover:bg-accent hover:text-foreground hover:opacity-100 group-hover/embedded-extension:opacity-100 cursor-pointer transition-opacity\"\n aria-label={t(\"extensions.optionsFor\", { name: toolName })}\n >\n <IconDots className=\"h-3.5 w-3.5\" />\n </button>\n </PopoverTrigger>\n </TooltipTrigger>\n <TooltipContent>\n {t(\"extensions.optionsFor\", { name: toolName })}\n </TooltipContent>\n </Tooltip>\n </TooltipProvider>\n <PopoverContent align=\"end\" sideOffset={4} className=\"w-56 p-1\">\n {!confirmingDelete ? (\n <div className=\"flex flex-col\">\n <button\n type=\"button\"\n onClick={() => {\n closeMenu();\n navigate(extensionPath(extensionId, toolName));\n }}\n className=\"flex items-center gap-2 rounded-sm px-2 py-1.5 text-[12px] hover:bg-accent cursor-pointer text-left\"\n >\n <IconExternalLink className=\"h-3.5 w-3.5\" />\n <span>{t(\"extensions.openFullView\")}</span>\n </button>\n <button\n type=\"button\"\n onClick={removeFromSlot}\n className=\"flex items-center gap-2 rounded-sm px-2 py-1.5 text-[12px] hover:bg-accent cursor-pointer text-left\"\n >\n <IconLayoutSidebarRightCollapse className=\"h-3.5 w-3.5\" />\n <span>{t(\"extensions.removeFromWidgetArea\")}</span>\n </button>\n {canDelete !== false && (\n <>\n <div className=\"my-1 h-px bg-border/40\" />\n <button\n type=\"button\"\n onClick={() => setConfirmingDelete(true)}\n className=\"flex items-center gap-2 rounded-sm px-2 py-1.5 text-[12px] text-destructive hover:bg-destructive/10 cursor-pointer text-left\"\n >\n <IconTrash className=\"h-3.5 w-3.5\" />\n <span>{t(\"extensions.deleteExtensionEllipsis\")}</span>\n </button>\n </>\n )}\n </div>\n ) : (\n <div className=\"flex flex-col gap-2 p-2\">\n <p className=\"text-[12px]\">\n {t(\"extensions.deleteQuestion\", { name: toolName })}{\" \"}\n {t(\"extensions.deleteEverywhereConfirmation\")}\n </p>\n <div className=\"flex justify-end gap-1\">\n <button\n type=\"button\"\n onClick={() => setConfirmingDelete(false)}\n className=\"rounded-md px-2 py-1 text-[12px] hover:bg-accent cursor-pointer\"\n >\n {t(\"extensions.cancel\")}\n </button>\n <button\n type=\"button\"\n onClick={deleteExtension}\n className=\"rounded-md bg-destructive px-2 py-1 text-[12px] text-destructive-foreground hover:bg-destructive/90 cursor-pointer\"\n >\n {t(\"extensions.delete\")}\n </button>\n </div>\n </div>\n )}\n </PopoverContent>\n </Popover>\n );\n}\n"]}
1
+ {"version":3,"file":"EmbeddedExtension.js","sourceRoot":"","sources":["../../../src/client/extensions/EmbeddedExtension.tsx"],"names":[],"mappings":"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","sourcesContent":["import {\n IconDots,\n IconExternalLink,\n IconLayoutSidebarRightCollapse,\n IconTrash,\n} from \"@tabler/icons-react\";\nimport { useQuery, useQueryClient } from \"@tanstack/react-query\";\nimport { useEffect, useMemo, useRef, useState } from \"react\";\nimport { useNavigate } from \"react-router\";\n\nimport { extensionPath } from \"../../extensions/path.js\";\nimport { sendToAgentChat } from \"../agent-chat.js\";\nimport { agentNativePath } from \"../api-path.js\";\nimport {\n Popover,\n PopoverContent,\n PopoverTrigger,\n} from \"../components/ui/popover.js\";\nimport {\n Tooltip,\n TooltipContent,\n TooltipProvider,\n TooltipTrigger,\n} from \"../components/ui/tooltip.js\";\nimport { useT } from \"../i18n.js\";\nimport {\n deleteOrHideExtension,\n invalidateExtensionRemoval,\n} from \"./delete-extension.js\";\nimport {\n extensionLoadError,\n extensionLoadErrorStatus,\n shouldRetryExtensionLoad,\n} from \"./extension-load-error.js\";\nimport {\n isAllowedExtensionPath,\n sanitizeExtensionRequestOptions,\n checkBridgePolicy,\n type BridgePolicyContext,\n type ExtensionBridgeRole,\n} from \"./iframe-bridge.js\";\n\ninterface Extension {\n id: string;\n name: string;\n description?: string;\n content?: string;\n updatedAt?: string;\n canDelete?: boolean;\n source?: {\n mode?: \"database\" | \"local-files\";\n permissions?: BridgePolicyContext[\"permissions\"];\n };\n}\n\nfunction serializeChatValue(value: unknown): string | undefined {\n if (value === undefined || value === null) return undefined;\n if (typeof value === \"string\") return value;\n try {\n return JSON.stringify(value);\n } catch {\n return String(value);\n }\n}\n\nexport interface EmbeddedExtensionProps {\n extensionId: string;\n /** Slot identifier passed via the iframe URL so the extension runtime knows it's\n * embedded and enables auto-resize. */\n slotId: string;\n /** Object pushed into the extension as `window.slotContext`. Re-posted whenever\n * the host re-renders with a new context. */\n context?: Record<string, unknown> | null;\n /** Optional className applied to the iframe container. */\n className?: string;\n /** Initial iframe height before content reports a real height. */\n initialHeight?: number;\n /** Fires once when the embedded iframe first signals content readiness — its\n * first height report, or iframe load as a fallback. Hosts that gate on\n * content paint (e.g. dashboard report screenshots) use this. */\n onReady?: () => void;\n /** Fires when the extension can't be loaded for this viewer (e.g. 403/404 —\n * the extension isn't shared with them or no longer exists). Hosts can use\n * this to render an explanatory fallback instead of a blank panel. By default\n * the component renders nothing on failure (slot-style silent skip). */\n onUnavailable?: (status?: number) => void;\n}\n\n/**\n * Renders a extension inline as a small auto-sized iframe — for use inside an\n * `<ExtensionSlot>`. Different from `<ExtensionViewer>` (which is full-page with a\n * toolbar): no header, sized to content, receives a `slotContext`.\n */\nexport function EmbeddedExtension({\n extensionId,\n slotId,\n context,\n className,\n initialHeight = 80,\n onReady,\n onUnavailable,\n}: EmbeddedExtensionProps) {\n const iframeRef = useRef<HTMLIFrameElement | null>(null);\n // Latch the readiness signal so onReady fires at most once per iframe\n // instance. Reset when the iframe is recreated (extensionId/updatedAt change).\n const onReadyRef = useRef(onReady);\n onReadyRef.current = onReady;\n const readyFiredRef = useRef(false);\n const fireReady = () => {\n if (readyFiredRef.current) return;\n readyFiredRef.current = true;\n onReadyRef.current?.();\n };\n const [height, setHeight] = useState<number>(initialHeight);\n const [isDark, setIsDark] = useState(false);\n // (audit H4) Mirror ExtensionViewer's role-aware gating; deny-by-default until\n // the iframe's render binding announcement arrives.\n const bridgeContextRef = useRef<BridgePolicyContext>({\n role: \"viewer\",\n isAuthor: false,\n });\n // (audit H4) Latch the render binding once per iframe instance. The shell\n // posts the server-resolved binding BEFORE user content runs; any later\n // agent-native-extension-binding message is attacker-controllable (it\n // originates inside the same sandboxed realm as user code) and must be\n // ignored so a viewer cannot self-escalate to owner.\n const bindingLatchedRef = useRef(false);\n\n useEffect(() => {\n setIsDark(document.documentElement.classList.contains(\"dark\"));\n const observer = new MutationObserver(() => {\n setIsDark(document.documentElement.classList.contains(\"dark\"));\n });\n observer.observe(document.documentElement, {\n attributes: true,\n attributeFilter: [\"class\"],\n });\n return () => observer.disconnect();\n }, []);\n\n const {\n data: extension,\n isFetching,\n isLoading,\n isError,\n error,\n } = useQuery<Extension>({\n queryKey: [\"extension\", extensionId],\n queryFn: async () => {\n const res = await fetch(\n agentNativePath(`/_agent-native/extensions/${extensionId}`),\n );\n if (res.status === 404) {\n throw extensionLoadError(404, \"Extension not found\");\n }\n if (res.status === 403) {\n throw extensionLoadError(403, \"Extension access denied\");\n }\n if (!res.ok) {\n throw extensionLoadError(res.status, \"Failed to fetch extension\");\n }\n return res.json();\n },\n retry: shouldRetryExtensionLoad,\n retryDelay: (attempt) => Math.min(1000 * 2 ** attempt, 4000),\n });\n\n // Notify the host once when the extension can't be loaded for this viewer so\n // it can show a fallback instead of a blank panel.\n const onUnavailableRef = useRef(onUnavailable);\n onUnavailableRef.current = onUnavailable;\n const unavailableFiredRef = useRef(false);\n useEffect(() => {\n unavailableFiredRef.current = false;\n }, [extensionId]);\n useEffect(() => {\n if (isError && !isFetching && !unavailableFiredRef.current) {\n unavailableFiredRef.current = true;\n onUnavailableRef.current?.(extensionLoadErrorStatus(error));\n }\n }, [isError, isFetching, error]);\n\n // Initial dark state is baked into the URL on first load only; subsequent\n // theme toggles update the iframe's <html class=\"dark\"> via postMessage so\n // the user's interaction state inside the extension survives the toggle.\n const initialDarkRef = useRef(isDark);\n const iframeSrc = useMemo(() => {\n const v = encodeURIComponent(extension?.updatedAt ?? \"\");\n return agentNativePath(\n `/_agent-native/extensions/${extensionId}/render?slot=${encodeURIComponent(slotId)}&dark=${initialDarkRef.current}&v=${v}`,\n );\n }, [extensionId, slotId, extension?.updatedAt]);\n\n // Reset role + binding latch to deny-by-default whenever the iframe is\n // recreated (its key changes). The new render's first binding announcement\n // re-establishes the role.\n useEffect(() => {\n bridgeContextRef.current = { role: \"viewer\", isAuthor: false };\n bindingLatchedRef.current = false;\n readyFiredRef.current = false;\n }, [extensionId, extension?.updatedAt]);\n\n useEffect(() => {\n const win = iframeRef.current?.contentWindow;\n if (!win) return;\n win.postMessage({ type: \"agent-native-theme-update\", isDark }, \"*\");\n }, [isDark]);\n\n // Forward slot context whenever it changes. The iframe's own load handler\n // posts the initial value once it's ready; this effect handles updates.\n const contextJson = JSON.stringify(context ?? {});\n useEffect(() => {\n const win = iframeRef.current?.contentWindow;\n if (!win) return;\n win.postMessage(\n { type: \"agent-native-slot-context\", context: context ?? {} },\n \"*\",\n );\n }, [contextJson]);\n\n // Bridge extension requests + height reports.\n useEffect(() => {\n const handleMessage = async (event: MessageEvent) => {\n if (event.source !== iframeRef.current?.contentWindow) return;\n const message = event.data;\n if (!message || typeof message !== \"object\") return;\n\n if (message.type === \"agent-native-extension-binding\") {\n // Only the FIRST announcement (sent by the shell before user content\n // runs) is trusted. Ignore re-announcements — a malicious extension\n // body could otherwise postMessage a forged owner binding to escalate.\n if (bindingLatchedRef.current) return;\n bindingLatchedRef.current = true;\n const binding = (message as any).binding ?? {};\n const role: ExtensionBridgeRole =\n binding.role === \"owner\" ||\n binding.role === \"admin\" ||\n binding.role === \"editor\" ||\n binding.role === \"viewer\"\n ? binding.role\n : \"viewer\";\n bridgeContextRef.current = {\n role,\n isAuthor: !!binding.isAuthor,\n source: binding.source === \"local-files\" ? \"local-files\" : \"database\",\n permissions:\n binding && typeof binding.permissions === \"object\"\n ? binding.permissions\n : undefined,\n };\n return;\n }\n\n if (message.type === \"agent-native-extension-resize\") {\n const h = Number(message.height);\n if (Number.isFinite(h) && h > 0) {\n setHeight(Math.ceil(h));\n // First laid-out height means the content has painted.\n fireReady();\n }\n return;\n }\n\n if (message.type === \"agent-native-send-to-chat\") {\n const text = serializeChatValue((message as any).message);\n if (!text?.trim()) return;\n sendToAgentChat({\n message: text,\n context: serializeChatValue((message as any).context),\n submit: (message as any).submit !== false,\n openSidebar: (message as any).openSidebar !== false,\n });\n return;\n }\n\n if (message.type !== \"agent-native-extension-request\") return;\n\n const requestId = String(message.requestId ?? \"\");\n const path = String(message.path ?? \"\");\n const respond = (payload: Record<string, unknown>) => {\n iframeRef.current?.contentWindow?.postMessage(\n { type: \"agent-native-extension-response\", requestId, ...payload },\n \"*\",\n );\n };\n\n if (!requestId || !isAllowedExtensionPath(path, extensionId)) {\n respond({ error: \"Extension request path is not allowed\" });\n return;\n }\n\n try {\n const options = sanitizeExtensionRequestOptions(message.options);\n // (audit H4) Role-aware gating: viewer-shared extensions can read but not\n // write. The bridge policy is decided here in the parent before the\n // request leaves; the server enforces a second layer.\n const policy = checkBridgePolicy(path, options.method ?? \"GET\", {\n ...bridgeContextRef.current,\n extensionId,\n });\n if (!policy.ok) {\n respond({\n response: {\n ok: false,\n status: 403,\n statusText: \"Forbidden\",\n body: { error: policy.error },\n },\n });\n return;\n }\n // (audit H5) Same extension-bridge tagging as <ExtensionViewer>. action-routes\n // uses these headers to enforce per-action `toolCallable` opt-in.\n const finalHeaders = new Headers(options.headers ?? undefined);\n finalHeaders.set(\"X-Agent-Native-Extension-Bridge\", \"1\");\n finalHeaders.set(\"X-Agent-Native-Extension-Id\", extensionId);\n finalHeaders.set(\"X-Agent-Native-Tool-Bridge\", \"1\");\n finalHeaders.set(\"X-Agent-Native-Tool-Id\", extensionId);\n const res = await fetch(agentNativePath(path), {\n ...options,\n headers: finalHeaders,\n credentials: \"same-origin\",\n });\n const text = await res.text();\n let body: unknown = text;\n if (text) {\n try {\n body = JSON.parse(text);\n } catch {\n body = text;\n }\n }\n respond({\n response: {\n ok: res.ok,\n status: res.status,\n statusText: res.statusText,\n body,\n },\n });\n } catch (err: any) {\n respond({ error: err?.message ?? \"Extension host request failed\" });\n }\n };\n\n window.addEventListener(\"message\", handleMessage);\n return () => window.removeEventListener(\"message\", handleMessage);\n }, [extensionId]);\n\n if (!extension) {\n if (!isLoading && !isFetching) return null;\n return (\n <div\n className={className}\n style={{ height: initialHeight }}\n aria-busy=\"true\"\n />\n );\n }\n\n return (\n <div className={`relative group/embedded-extension ${className ?? \"\"}`}>\n <iframe\n ref={iframeRef}\n key={`${extensionId}-${extension.updatedAt ?? \"\"}`}\n src={iframeSrc}\n title={extension.name}\n sandbox=\"allow-scripts allow-forms\"\n style={{ width: \"100%\", border: 0, height, display: \"block\" }}\n onLoad={() => {\n iframeRef.current?.contentWindow?.postMessage(\n { type: \"agent-native-slot-context\", context: context ?? {} },\n \"*\",\n );\n // Fallback readiness signal in case the extension never reports a\n // height (e.g. fixed-height content that skips auto-resize).\n fireReady();\n }}\n />\n <EmbeddedToolMenu\n extensionId={extensionId}\n slotId={slotId}\n toolName={extension.name}\n canDelete={extension.canDelete}\n />\n </div>\n );\n}\n\nfunction EmbeddedToolMenu({\n extensionId,\n slotId,\n toolName,\n canDelete,\n}: {\n extensionId: string;\n slotId: string;\n toolName: string;\n canDelete?: boolean;\n}) {\n const t = useT();\n const [open, setOpen] = useState(false);\n const [confirmingDelete, setConfirmingDelete] = useState(false);\n const queryClient = useQueryClient();\n const navigate = useNavigate();\n\n const closeMenu = () => {\n setOpen(false);\n setConfirmingDelete(false);\n };\n\n const removeFromSlot = async () => {\n closeMenu();\n queryClient.setQueryData<any[]>([\"slot-installs\", slotId], (old) =>\n (old ?? []).filter((i) => i.extensionId !== extensionId),\n );\n try {\n await fetch(\n agentNativePath(\n `/_agent-native/slots/${encodeURIComponent(slotId)}/install/${encodeURIComponent(extensionId)}`,\n ),\n { method: \"DELETE\" },\n );\n } finally {\n queryClient.invalidateQueries({ queryKey: [\"slot-installs\", slotId] });\n }\n };\n\n const deleteExtension = async () => {\n closeMenu();\n try {\n await deleteOrHideExtension({ id: extensionId, canDelete });\n invalidateExtensionRemoval(queryClient, extensionId);\n } catch {\n queryClient.invalidateQueries({ queryKey: [\"extension\", extensionId] });\n }\n };\n\n return (\n <Popover\n open={open}\n onOpenChange={(o) => {\n setOpen(o);\n if (!o) setConfirmingDelete(false);\n }}\n >\n <TooltipProvider delayDuration={200}>\n <Tooltip>\n <TooltipTrigger asChild>\n <PopoverTrigger asChild>\n <button\n type=\"button\"\n className=\"absolute top-1 right-1 flex h-6 w-6 items-center justify-center rounded-md bg-background/60 text-muted-foreground/60 opacity-0 hover:bg-accent hover:text-foreground hover:opacity-100 group-hover/embedded-extension:opacity-100 cursor-pointer transition-opacity\"\n aria-label={t(\"extensions.optionsFor\", { name: toolName })}\n >\n <IconDots className=\"h-3.5 w-3.5\" />\n </button>\n </PopoverTrigger>\n </TooltipTrigger>\n <TooltipContent>\n {t(\"extensions.optionsFor\", { name: toolName })}\n </TooltipContent>\n </Tooltip>\n </TooltipProvider>\n <PopoverContent align=\"end\" sideOffset={4} className=\"w-56 p-1\">\n {!confirmingDelete ? (\n <div className=\"flex flex-col\">\n <button\n type=\"button\"\n onClick={() => {\n closeMenu();\n navigate(extensionPath(extensionId, toolName));\n }}\n className=\"flex items-center gap-2 rounded-sm px-2 py-1.5 text-[12px] hover:bg-accent cursor-pointer text-left\"\n >\n <IconExternalLink className=\"h-3.5 w-3.5\" />\n <span>{t(\"extensions.openFullView\")}</span>\n </button>\n <button\n type=\"button\"\n onClick={removeFromSlot}\n className=\"flex items-center gap-2 rounded-sm px-2 py-1.5 text-[12px] hover:bg-accent cursor-pointer text-left\"\n >\n <IconLayoutSidebarRightCollapse className=\"h-3.5 w-3.5\" />\n <span>{t(\"extensions.removeFromWidgetArea\")}</span>\n </button>\n {canDelete !== false && (\n <>\n <div className=\"my-1 h-px bg-border/40\" />\n <button\n type=\"button\"\n onClick={() => setConfirmingDelete(true)}\n className=\"flex items-center gap-2 rounded-sm px-2 py-1.5 text-[12px] text-destructive hover:bg-destructive/10 cursor-pointer text-left\"\n >\n <IconTrash className=\"h-3.5 w-3.5\" />\n <span>{t(\"extensions.deleteExtensionEllipsis\")}</span>\n </button>\n </>\n )}\n </div>\n ) : (\n <div className=\"flex flex-col gap-2 p-2\">\n <p className=\"text-[12px]\">\n {t(\"extensions.deleteQuestion\", { name: toolName })}{\" \"}\n {t(\"extensions.deleteEverywhereConfirmation\")}\n </p>\n <div className=\"flex justify-end gap-1\">\n <button\n type=\"button\"\n onClick={() => setConfirmingDelete(false)}\n className=\"rounded-md px-2 py-1 text-[12px] hover:bg-accent cursor-pointer\"\n >\n {t(\"extensions.cancel\")}\n </button>\n <button\n type=\"button\"\n onClick={deleteExtension}\n className=\"rounded-md bg-destructive px-2 py-1 text-[12px] text-destructive-foreground hover:bg-destructive/90 cursor-pointer\"\n >\n {t(\"extensions.delete\")}\n </button>\n </div>\n </div>\n )}\n </PopoverContent>\n </Popover>\n );\n}\n"]}
@@ -26,8 +26,8 @@ export declare const getCollabState: import("h3").EventHandlerWithFetch<import("
26
26
  * Body: { update: string (base64), requestSource?: string }
27
27
  */
28
28
  export declare const postCollabUpdate: import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
29
- ok?: undefined;
30
29
  error: string;
30
+ ok?: undefined;
31
31
  } | {
32
32
  error?: undefined;
33
33
  ok: boolean;
@@ -41,9 +41,9 @@ export declare const postCollabUpdate: import("h3").EventHandlerWithFetch<import
41
41
  * Body: { text: string, fieldName?: string, requestSource?: string }
42
42
  */
43
43
  export declare const postCollabText: import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
44
- text?: undefined;
45
44
  ok?: undefined;
46
45
  error: string;
46
+ text?: undefined;
47
47
  } | {
48
48
  error?: undefined;
49
49
  ok: boolean;
@@ -41,16 +41,16 @@ export declare function createObservabilityHandler(): import("h3").EventHandlerW
41
41
  thumbsUpRate: number;
42
42
  avgEvalScore: number;
43
43
  } | {
44
- error?: undefined;
45
44
  summary: import("./types.js").TraceSummary;
46
45
  spans: import("./types.js").TraceSpan[];
47
46
  id?: undefined;
47
+ error?: undefined;
48
48
  ok?: undefined;
49
49
  } | {
50
- error?: undefined;
51
50
  summary?: undefined;
52
51
  spans?: undefined;
53
52
  id: string;
53
+ error?: undefined;
54
54
  ok?: undefined;
55
55
  } | {
56
56
  summary?: undefined;
@@ -59,10 +59,10 @@ export declare function createObservabilityHandler(): import("h3").EventHandlerW
59
59
  error: any;
60
60
  ok?: undefined;
61
61
  } | {
62
- error?: undefined;
63
62
  summary?: undefined;
64
63
  spans?: undefined;
65
64
  id?: undefined;
65
+ error?: undefined;
66
66
  ok: boolean;
67
67
  }>>;
68
68
  //# sourceMappingURL=routes.d.ts.map
@@ -52,8 +52,8 @@ export declare function handleDeleteResource(event: any): Promise<{
52
52
  error: string;
53
53
  ok?: undefined;
54
54
  } | {
55
- error?: undefined;
56
55
  ok: boolean;
56
+ error?: undefined;
57
57
  }>;
58
58
  /** POST /_agent-native/resources/upload — upload a file as a resource */
59
59
  export declare function handleUploadResource(event: any): Promise<import("./store.js").Resource | {
@@ -73,9 +73,9 @@ export declare function handleUploadResource(event: any): Promise<import("./stor
73
73
  runId: string | null;
74
74
  expiresAt: number | null;
75
75
  metadata: string | null;
76
- error?: undefined;
77
76
  url: string;
78
77
  provider: string;
78
+ error?: undefined;
79
79
  }>;
80
80
  export {};
81
81
  //# sourceMappingURL=handlers.d.ts.map
@@ -27,11 +27,11 @@ export declare function resolveAgentEngineApiKeyWriteTarget(event: H3Event, scop
27
27
  export declare function createAgentEngineApiKeyHandler(): import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
28
28
  error: any;
29
29
  } | {
30
- error?: undefined;
31
30
  ok: boolean;
32
31
  key: string;
33
32
  baseUrlKey?: string;
34
33
  scope: AgentEngineApiKeyScope;
34
+ error?: undefined;
35
35
  }>>;
36
36
  export {};
37
37
  //# sourceMappingURL=agent-engine-api-key-route.d.ts.map
@@ -20,7 +20,7 @@ export declare function createTranscribeVoiceHandler(): import("h3").EventHandle
20
20
  error: string;
21
21
  text?: undefined;
22
22
  } | {
23
- error?: undefined;
24
23
  text: string;
24
+ error?: undefined;
25
25
  }>>;
26
26
  //# sourceMappingURL=transcribe-voice.d.ts.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-native/core",
3
- "version": "0.80.6",
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": {