@agent-native/core 0.107.0 → 0.107.2

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 (156) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +16 -0
  3. package/corpus/core/docs/content/integrations.mdx +27 -27
  4. package/corpus/core/package.json +1 -1
  5. package/corpus/core/src/a2a/artifact-response.ts +32 -10
  6. package/corpus/core/src/agent/production-agent.ts +19 -0
  7. package/corpus/core/src/agent/run-store.ts +98 -2
  8. package/corpus/core/src/client/AssistantChat.tsx +13 -0
  9. package/corpus/core/src/client/CommandMenu.tsx +5 -0
  10. package/corpus/core/src/client/active-run-state.ts +34 -0
  11. package/corpus/core/src/client/agent-chat-adapter.ts +4 -0
  12. package/corpus/core/src/client/resources/McpConnectionSuggestion.tsx +41 -11
  13. package/corpus/core/src/client/resources/McpIntegrationDialog.tsx +27 -9
  14. package/corpus/core/src/client/resources/mcp-integration-catalog.ts +45 -0
  15. package/corpus/core/src/client/sharing/ShareButton.tsx +30 -9
  16. package/corpus/core/src/client/use-agent-chat-home-handoff.ts +2 -2
  17. package/corpus/core/src/integrations/a2a-continuation-processor.ts +107 -3
  18. package/corpus/core/src/integrations/pending-tasks-store.ts +98 -1
  19. package/corpus/core/src/integrations/plugin.ts +129 -4
  20. package/corpus/core/src/integrations/webhook-handler.ts +339 -36
  21. package/corpus/core/src/localization/default-messages.ts +4 -1
  22. package/corpus/core/src/secrets/register-framework-secrets.ts +28 -0
  23. package/corpus/core/src/server/agent-chat-plugin.ts +122 -9
  24. package/corpus/core/src/vite/client.ts +24 -0
  25. package/corpus/templates/analytics/.agents/skills/dashboard-management/SKILL.md +29 -21
  26. package/corpus/templates/analytics/AGENTS.md +8 -0
  27. package/corpus/templates/analytics/actions/dashboard-mutation-api.ts +1 -1
  28. package/corpus/templates/analytics/actions/update-dashboard.ts +6 -2
  29. package/corpus/templates/analytics/app/components/dashboard/SqlChart.tsx +35 -7
  30. package/corpus/templates/analytics/app/components/layout/Layout.tsx +1 -3
  31. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/SqlChartCard.tsx +11 -1
  32. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/extension-slot.ts +11 -0
  33. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx +27 -0
  34. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/types.ts +6 -0
  35. package/corpus/templates/analytics/changelog/2026-07-17-dashboard-extension-boxes.md +6 -0
  36. package/corpus/templates/analytics/server/lib/agent-readable-resource-context.ts +8 -0
  37. package/corpus/templates/analytics/server/plugins/agent-chat.ts +1 -0
  38. package/corpus/templates/assets/changelog/2026-07-17-the-agent-chat-sidebar-stays-closed-until-you-open-it-or-sta.md +6 -0
  39. package/corpus/templates/brain/app/hooks/use-navigation-state.ts +6 -1
  40. package/corpus/templates/brain/changelog/2026-07-17-the-agent-chat-sidebar-stays-closed-until-you-open-it-or-sta.md +6 -0
  41. package/corpus/templates/calendar/app/components/layout/AppLayout.tsx +1 -1
  42. package/corpus/templates/calendar/changelog/2026-07-17-the-agent-chat-sidebar-stays-closed-until-you-open-it-or-sta.md +6 -0
  43. package/corpus/templates/chat/app/hooks/use-navigation-state.ts +4 -1
  44. package/corpus/templates/chat/changelog/2026-07-17-the-agent-chat-sidebar-stays-closed-until-you-open-it-or-sta.md +6 -0
  45. package/corpus/templates/clips/app/components/library/library-layout.tsx +1 -1
  46. package/corpus/templates/clips/changelog/2026-07-17-the-agent-chat-sidebar-stays-closed-until-you-open-it-or-sta.md +6 -0
  47. package/corpus/templates/content/actions/_content-space-access.ts +45 -2
  48. package/corpus/templates/content/actions/create-content-database.ts +41 -7
  49. package/corpus/templates/content/app/components/layout/Layout.tsx +1 -1
  50. package/corpus/templates/content/app/root.tsx +1 -10
  51. package/corpus/templates/content/changelog/2026-07-17-creating-a-database-now-completes-reliably-in-hosted-workspa.md +6 -0
  52. package/corpus/templates/content/changelog/2026-07-17-the-agent-chat-sidebar-stays-closed-until-you-open-it-or-sta.md +6 -0
  53. package/corpus/templates/dispatch/app/hooks/use-navigation-state.ts +4 -1
  54. package/corpus/templates/dispatch/changelog/2026-07-17-the-agent-chat-sidebar-stays-closed-until-you-open-it-or-sta.md +6 -0
  55. package/corpus/templates/forms/app/components/layout/Layout.tsx +2 -4
  56. package/corpus/templates/forms/app/components/ui/popover.tsx +25 -1
  57. package/corpus/templates/forms/app/hooks/use-navigation-state.ts +6 -1
  58. package/corpus/templates/forms/app/pages/FormBuilderPage.tsx +13 -10
  59. package/corpus/templates/forms/app/root.tsx +6 -1
  60. package/corpus/templates/forms/changelog/2026-07-17-form-builder-popovers-stay-within-the-viewport.md +6 -0
  61. package/corpus/templates/forms/changelog/2026-07-17-form-editor-loading-matches-current-sidebar-free-layout.md +6 -0
  62. package/corpus/templates/forms/changelog/2026-07-17-the-agent-chat-sidebar-stays-closed-until-you-open-it-or-sta.md +6 -0
  63. package/corpus/templates/mail/app/components/layout/AppLayout.tsx +1 -1
  64. package/corpus/templates/mail/changelog/2026-07-17-the-agent-chat-sidebar-stays-closed-until-you-open-it-or-sta.md +6 -0
  65. package/corpus/templates/plan/app/components/layout/Sidebar.tsx +14 -3
  66. package/corpus/templates/plan/app/hooks/use-navigation-state.ts +8 -1
  67. package/corpus/templates/plan/app/root.tsx +0 -2
  68. package/corpus/templates/plan/changelog/2026-07-17-the-agent-chat-sidebar-stays-closed-until-you-open-it-or-sta.md +6 -0
  69. package/corpus/templates/slides/app/components/layout/Layout.tsx +1 -1
  70. package/corpus/templates/slides/app/context/DeckContext.tsx +23 -23
  71. package/corpus/templates/slides/changelog/2026-07-17-duplicated-slides-now-stay-in-the-deck.md +6 -0
  72. package/corpus/templates/slides/changelog/2026-07-17-the-agent-chat-sidebar-stays-closed-until-you-open-it-or-sta.md +6 -0
  73. package/corpus/templates/tasks/app/components/layout/Layout.tsx +2 -13
  74. package/corpus/templates/tasks/changelog/2026-07-17-the-agent-chat-sidebar-stays-closed-until-you-open-it.md +6 -0
  75. package/dist/a2a/artifact-response.d.ts +5 -1
  76. package/dist/a2a/artifact-response.d.ts.map +1 -1
  77. package/dist/a2a/artifact-response.js +18 -12
  78. package/dist/a2a/artifact-response.js.map +1 -1
  79. package/dist/agent/production-agent.d.ts.map +1 -1
  80. package/dist/agent/production-agent.js +13 -1
  81. package/dist/agent/production-agent.js.map +1 -1
  82. package/dist/agent/run-store.d.ts +17 -0
  83. package/dist/agent/run-store.d.ts.map +1 -1
  84. package/dist/agent/run-store.js +84 -2
  85. package/dist/agent/run-store.js.map +1 -1
  86. package/dist/client/AssistantChat.d.ts.map +1 -1
  87. package/dist/client/AssistantChat.js +10 -1
  88. package/dist/client/AssistantChat.js.map +1 -1
  89. package/dist/client/CommandMenu.d.ts.map +1 -1
  90. package/dist/client/CommandMenu.js +4 -0
  91. package/dist/client/CommandMenu.js.map +1 -1
  92. package/dist/client/active-run-state.d.ts +7 -0
  93. package/dist/client/active-run-state.d.ts.map +1 -1
  94. package/dist/client/active-run-state.js +28 -0
  95. package/dist/client/active-run-state.js.map +1 -1
  96. package/dist/client/agent-chat-adapter.d.ts.map +1 -1
  97. package/dist/client/agent-chat-adapter.js +4 -1
  98. package/dist/client/agent-chat-adapter.js.map +1 -1
  99. package/dist/client/resources/McpConnectionSuggestion.d.ts.map +1 -1
  100. package/dist/client/resources/McpConnectionSuggestion.js +27 -8
  101. package/dist/client/resources/McpConnectionSuggestion.js.map +1 -1
  102. package/dist/client/resources/McpIntegrationDialog.d.ts.map +1 -1
  103. package/dist/client/resources/McpIntegrationDialog.js +4 -2
  104. package/dist/client/resources/McpIntegrationDialog.js.map +1 -1
  105. package/dist/client/resources/mcp-integration-catalog.d.ts +6 -0
  106. package/dist/client/resources/mcp-integration-catalog.d.ts.map +1 -1
  107. package/dist/client/resources/mcp-integration-catalog.js +33 -0
  108. package/dist/client/resources/mcp-integration-catalog.js.map +1 -1
  109. package/dist/client/sharing/ShareButton.d.ts.map +1 -1
  110. package/dist/client/sharing/ShareButton.js +15 -12
  111. package/dist/client/sharing/ShareButton.js.map +1 -1
  112. package/dist/client/use-agent-chat-home-handoff.d.ts +1 -1
  113. package/dist/client/use-agent-chat-home-handoff.d.ts.map +1 -1
  114. package/dist/client/use-agent-chat-home-handoff.js +1 -1
  115. package/dist/client/use-agent-chat-home-handoff.js.map +1 -1
  116. package/dist/collab/awareness.d.ts +2 -2
  117. package/dist/collab/awareness.d.ts.map +1 -1
  118. package/dist/collab/routes.d.ts +1 -1
  119. package/dist/integrations/a2a-continuation-processor.js +62 -5
  120. package/dist/integrations/a2a-continuation-processor.js.map +1 -1
  121. package/dist/integrations/pending-tasks-store.d.ts +3 -0
  122. package/dist/integrations/pending-tasks-store.d.ts.map +1 -1
  123. package/dist/integrations/pending-tasks-store.js +75 -1
  124. package/dist/integrations/pending-tasks-store.js.map +1 -1
  125. package/dist/integrations/plugin.d.ts.map +1 -1
  126. package/dist/integrations/plugin.js +81 -5
  127. package/dist/integrations/plugin.js.map +1 -1
  128. package/dist/integrations/webhook-handler.d.ts +23 -2
  129. package/dist/integrations/webhook-handler.d.ts.map +1 -1
  130. package/dist/integrations/webhook-handler.js +257 -35
  131. package/dist/integrations/webhook-handler.js.map +1 -1
  132. package/dist/localization/default-messages.d.ts +2 -0
  133. package/dist/localization/default-messages.d.ts.map +1 -1
  134. package/dist/localization/default-messages.js +3 -1
  135. package/dist/localization/default-messages.js.map +1 -1
  136. package/dist/notifications/routes.d.ts +2 -2
  137. package/dist/observability/routes.d.ts +5 -5
  138. package/dist/resources/handlers.d.ts +1 -1
  139. package/dist/secrets/register-framework-secrets.d.ts.map +1 -1
  140. package/dist/secrets/register-framework-secrets.js +25 -0
  141. package/dist/secrets/register-framework-secrets.js.map +1 -1
  142. package/dist/secrets/routes.d.ts +9 -9
  143. package/dist/server/agent-chat-plugin.d.ts +6 -0
  144. package/dist/server/agent-chat-plugin.d.ts.map +1 -1
  145. package/dist/server/agent-chat-plugin.js +94 -7
  146. package/dist/server/agent-chat-plugin.js.map +1 -1
  147. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  148. package/dist/templates/chat/app/hooks/use-navigation-state.ts +4 -1
  149. package/dist/templates/chat/changelog/2026-07-17-the-agent-chat-sidebar-stays-closed-until-you-open-it-or-sta.md +6 -0
  150. package/dist/vite/client.d.ts.map +1 -1
  151. package/dist/vite/client.js +20 -0
  152. package/dist/vite/client.js.map +1 -1
  153. package/docs/content/integrations.mdx +27 -27
  154. package/package.json +1 -1
  155. package/src/templates/chat/app/hooks/use-navigation-state.ts +4 -1
  156. package/src/templates/chat/changelog/2026-07-17-the-agent-chat-sidebar-stays-closed-until-you-open-it-or-sta.md +6 -0
@@ -14,6 +14,7 @@ import {
14
14
  } from "h3";
15
15
 
16
16
  import {
17
+ appendA2AArtifactLinks,
17
18
  buildA2ARecoverableArtifactMessage,
18
19
  type A2AToolResultSummary,
19
20
  } from "../a2a/artifact-response.js";
@@ -27,6 +28,7 @@ import {
27
28
  createA2AApproval,
28
29
  updateTaskStatusMessage,
29
30
  } from "../a2a/task-store.js";
31
+ import type { Message as A2AMessage } from "../a2a/types.js";
30
32
  import type { ActionHttpConfig } from "../action.js";
31
33
  import {
32
34
  canUpdateAgentAppModelDefaultSettings,
@@ -69,7 +71,7 @@ import {
69
71
  callerHasRunAccess,
70
72
  callerHasThreadAccess,
71
73
  } from "../agent/run-ownership.js";
72
- import { readBackgroundRunClaim } from "../agent/run-store.js";
74
+ import { markTurnAborted, readBackgroundRunClaim } from "../agent/run-store.js";
73
75
  import {
74
76
  buildCurrentTimeUserContext,
75
77
  buildRuntimeContextPrompt,
@@ -275,6 +277,74 @@ export { loadRunCodeToolEntries };
275
277
  export { shouldDisableRecurringJobsRuntime };
276
278
  export { finalizeClaimedAgentChatProcessRunFailure };
277
279
 
280
+ export function createSerializedA2ATaskStatusWriter(
281
+ taskId: string,
282
+ writeStatus: (
283
+ taskId: string,
284
+ message: A2AMessage,
285
+ ) => Promise<void> = updateTaskStatusMessage,
286
+ onError: (error: unknown) => void = (error) => {
287
+ console.error(
288
+ `[A2A] Failed to persist recoverable artifact message for task ${taskId}:`,
289
+ error,
290
+ );
291
+ },
292
+ ): {
293
+ enqueue: (message: A2AMessage) => void;
294
+ flush: () => Promise<void>;
295
+ } {
296
+ const maxAttempts = 3;
297
+ let latestWrite: Promise<void> = Promise.resolve();
298
+
299
+ const persistWithRetry = async (message: A2AMessage): Promise<void> => {
300
+ let lastError: unknown;
301
+ for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
302
+ try {
303
+ await writeStatus(taskId, message);
304
+ return;
305
+ } catch (error) {
306
+ lastError = error;
307
+ }
308
+ }
309
+ try {
310
+ onError(lastError);
311
+ } catch {
312
+ // The durable-write error below remains the authoritative failure.
313
+ }
314
+ throw lastError;
315
+ };
316
+
317
+ return {
318
+ enqueue(message) {
319
+ // A newer checkpoint supersedes an earlier failed checkpoint, so keep
320
+ // the queue moving. flush() still rejects when the latest write itself
321
+ // cannot be made durable after bounded retries.
322
+ latestWrite = latestWrite
323
+ .catch(() => undefined)
324
+ .then(() => {
325
+ return persistWithRetry(message);
326
+ });
327
+ },
328
+ flush() {
329
+ return latestWrite;
330
+ },
331
+ };
332
+ }
333
+
334
+ export async function resolveA2ARecoverableArtifactSecret(
335
+ orgId: string | null | undefined = getRequestOrgId(),
336
+ ): Promise<string | undefined> {
337
+ const globalSecret = process.env.A2A_SECRET?.trim();
338
+ if (globalSecret) return globalSecret;
339
+ if (!orgId) return undefined;
340
+ try {
341
+ const { getOrgA2ASecret } = await import("../org/context.js");
342
+ return (await getOrgA2ASecret(orgId))?.trim() || undefined;
343
+ } catch {
344
+ return undefined;
345
+ }
346
+ }
347
+
278
348
  export function buildLeanRunPolicyPrompt(
279
349
  codeEditingSurfaceRestriction: string,
280
350
  prodCodeExecPromptNote: string,
@@ -1468,6 +1538,10 @@ export function createAgentChatPlugin(
1468
1538
  const a2aEvents: AgentChatEvent[] = [];
1469
1539
  const a2aToolResults: A2AToolResultSummary[] = [];
1470
1540
  let lastRecoverableArtifactText = "";
1541
+ const recoverableArtifactSecret =
1542
+ await resolveA2ARecoverableArtifactSecret();
1543
+ const recoverableArtifactStatusWriter =
1544
+ createSerializedA2ATaskStatusWriter(context.taskId);
1471
1545
  const controller = new AbortController();
1472
1546
 
1473
1547
  console.log(
@@ -1504,16 +1578,28 @@ export function createAgentChatPlugin(
1504
1578
  isError: event.isError,
1505
1579
  completedSideEffect: event.completedSideEffect,
1506
1580
  });
1507
- const recoverableArtifactText =
1581
+ const artifactBaseUrl = resolveArtifactBaseUrl(context.event);
1582
+ const recoverableArtifactMessage =
1508
1583
  buildA2ARecoverableArtifactMessage(a2aToolResults, {
1509
- baseUrl: resolveArtifactBaseUrl(context.event),
1584
+ baseUrl: artifactBaseUrl,
1510
1585
  });
1586
+ const recoverableArtifactText = recoverableArtifactMessage
1587
+ ? appendA2AArtifactLinks(
1588
+ recoverableArtifactMessage,
1589
+ a2aToolResults,
1590
+ {
1591
+ baseUrl: artifactBaseUrl,
1592
+ includePersistedArtifactMarker: true,
1593
+ persistedArtifactSecret: recoverableArtifactSecret,
1594
+ },
1595
+ )
1596
+ : null;
1511
1597
  if (
1512
1598
  recoverableArtifactText &&
1513
1599
  recoverableArtifactText !== lastRecoverableArtifactText
1514
1600
  ) {
1515
1601
  lastRecoverableArtifactText = recoverableArtifactText;
1516
- updateTaskStatusMessage(context.taskId, {
1602
+ recoverableArtifactStatusWriter.enqueue({
1517
1603
  role: "agent",
1518
1604
  metadata: { agentNativeRecoverableArtifacts: true },
1519
1605
  parts: [
@@ -1522,11 +1608,6 @@ export function createAgentChatPlugin(
1522
1608
  text: recoverableArtifactText,
1523
1609
  },
1524
1610
  ],
1525
- }).catch((err) => {
1526
- console.error(
1527
- `[A2A] Failed to persist recoverable artifact message for task ${context.taskId}:`,
1528
- err,
1529
- );
1530
1611
  });
1531
1612
  }
1532
1613
  } else if (event.type === "error") {
@@ -1548,6 +1629,10 @@ export function createAgentChatPlugin(
1548
1629
  },
1549
1630
  );
1550
1631
 
1632
+ // The continuation can observe terminal output immediately, so make
1633
+ // its latest mutation checkpoint durable first.
1634
+ await recoverableArtifactStatusWriter.flush();
1635
+
1551
1636
  const approval = [...a2aEvents]
1552
1637
  .reverse()
1553
1638
  .find(
@@ -4137,6 +4222,31 @@ Non-code requests are still fine on this surface: read data, navigate the UI, su
4137
4222
  callerHasRunAccess(owner, runId, "viewer", { orgId });
4138
4223
  const canEditRun = (runId: string) =>
4139
4224
  callerHasRunAccess(owner, runId, "editor", { orgId });
4225
+ const canEditThread = (threadId: string) =>
4226
+ callerHasThreadAccess(owner, threadId, "editor", { orgId });
4227
+
4228
+ // Route: POST /runs/turn/:turnId/abort
4229
+ // Covers the short window where the client has sent a turn but the
4230
+ // server has not yet created its background run id.
4231
+ const turnAbortMatch =
4232
+ url.match(/\/runs\/turn\/([^/?]+)\/abort/) ||
4233
+ url.match(/^\/turn\/([^/?]+)\/abort/);
4234
+ if (turnAbortMatch && method === "POST") {
4235
+ const turnId = decodeURIComponent(turnAbortMatch[1]);
4236
+ const body = await readBody(event).catch(() => null);
4237
+ const threadId =
4238
+ typeof body?.threadId === "string" ? body.threadId : "";
4239
+ if (
4240
+ !/^[a-zA-Z0-9_-]{1,160}$/.test(turnId) ||
4241
+ !threadId ||
4242
+ !(await canEditThread(threadId))
4243
+ ) {
4244
+ setResponseStatus(event, 404);
4245
+ return { error: "Run not found" };
4246
+ }
4247
+ await markTurnAborted(threadId, turnId);
4248
+ return { ok: true };
4249
+ }
4140
4250
 
4141
4251
  // Route: GET /runs/list?goalId=agent-team|agent-harness
4142
4252
  // Returns background agents in the Code hub-compatible run shape.
@@ -5180,6 +5290,9 @@ Non-code requests are still fine on this surface: read data, navigate the UI, su
5180
5290
  }
5181
5291
  workerBody = {
5182
5292
  ...parsedPayload,
5293
+ ...(prepared.body.internalContinuation === true
5294
+ ? { internalContinuation: true }
5295
+ : {}),
5183
5296
  [AGENT_CHAT_BACKGROUND_RUN_FIELD]: preparedMarker,
5184
5297
  };
5185
5298
  }
@@ -2579,6 +2579,27 @@ function createAgentNativePlugins(
2579
2579
  ].filter(Boolean);
2580
2580
  }
2581
2581
 
2582
+ function resolveAgentNativeTemplate(cwd: string): string {
2583
+ const configured = [
2584
+ process.env.AGENT_NATIVE_TEMPLATE,
2585
+ process.env.VITE_AGENT_NATIVE_TEMPLATE,
2586
+ process.env.VITE_APP_TEMPLATE,
2587
+ ].find((value) => value?.trim());
2588
+ if (configured) return configured.trim().toLowerCase();
2589
+
2590
+ const normalizedCwd = cwd.replaceAll("\\", "/");
2591
+ const marker = "/templates/";
2592
+ const markerIndex = normalizedCwd.lastIndexOf(marker);
2593
+ if (markerIndex === -1) return "";
2594
+ return (
2595
+ normalizedCwd
2596
+ .slice(markerIndex + marker.length)
2597
+ .split("/")[0]
2598
+ ?.trim()
2599
+ .toLowerCase() ?? ""
2600
+ );
2601
+ }
2602
+
2582
2603
  function createAgentNativeConfig(
2583
2604
  options: ClientConfigOptions | AgentNativeVitePluginOptions = {},
2584
2605
  command?: AgentNativeViteCommand,
@@ -2683,6 +2704,9 @@ function createAgentNativeConfig(
2683
2704
  __AGENT_NATIVE_MCP_INTEGRATIONS_CONFIG__: JSON.stringify(
2684
2705
  normalizeMcpIntegrationsConfig(options.mcpIntegrations),
2685
2706
  ),
2707
+ __AGENT_NATIVE_TEMPLATE__: JSON.stringify(
2708
+ resolveAgentNativeTemplate(cwd),
2709
+ ),
2686
2710
  },
2687
2711
  server: {
2688
2712
  ...(userConfig.server ?? {}),
@@ -83,7 +83,7 @@ When the user asks for a dashboard:
83
83
  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.
84
84
  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`.
85
85
  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.
86
- 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").
86
+ 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`; use `config.extensionSlotId` for new panels, with legacy `config.extensionId` supported for existing direct embeds (see "Embedding An Extension As A Panel").
87
87
  6. Persist with `update-dashboard`, not raw SQL or settings writes.
88
88
  7. Navigate to it with `pnpm action navigate --view=adhoc --dashboardId=<id>`.
89
89
 
@@ -114,14 +114,18 @@ rather than forcing it into a native dashboard config.
114
114
 
115
115
  ## Embedding An Extension As A Panel
116
116
 
117
- Use `chartType: "extension"` to embed an existing extension as a dashboard
118
- panel. This is different from the section above: there you replace the whole
119
- dashboard with an extension; here you drop a single extension widget into one
120
- panel slot alongside normal SQL charts. The panel renders the extension's
121
- sandboxed iframe instead of running a query, so it skips `source` and `sql` and
122
- instead requires `config.extensionId` (the id of an extension that already
123
- exists — create it first with `create-extension`). Validation rejects an
124
- extension panel without a non-empty `config.extensionId`.
117
+ Use `chartType: "extension"` to add an extension box alongside normal SQL
118
+ charts. The panel skips `source` and `sql`. New boxes use a stable slot id in
119
+ `config.extensionSlotId`:
120
+
121
+ ```text
122
+ analytics.dashboard.<dashboard-id>.panel.<panel-id>
123
+ ```
124
+
125
+ Create or choose the extension, call `add-extension-slot-target` with the
126
+ extension id and slot id, then call `install-extension` with the same values.
127
+ The dashboard panel is shared, while the installed extension is per-user.
128
+ Empty slots show the normal install affordance instead of a broken iframe.
125
129
 
126
130
  ```jsonc
127
131
  {
@@ -129,21 +133,22 @@ extension panel without a non-empty `config.extensionId`.
129
133
  "title": "Pipeline Widget",
130
134
  "chartType": "extension",
131
135
  "width": 3,
132
- "config": { "extensionId": "<existing-extension-id>" },
136
+ "config": {
137
+ "extensionSlotId": "analytics.dashboard.weekly-metrics.panel.pipeline-widget",
138
+ },
133
139
  }
134
140
  ```
135
141
 
136
142
  Notes:
137
143
 
138
- - The panel renders full-bleed (no card chrome/title) and does not receive the
139
- dashboard's filters/variables/date range it's a standalone widget for now.
140
- - Access is scoped per viewer: embedding does NOT grant access to the extension
141
- (same model as ExtensionSlots). If you share a dashboard more broadly than the
142
- embedded extension, viewers without access to that extension see an
143
- "extension unavailable" message instead of the content. Share the extension to
144
- the same audience as the dashboard so all viewers can see it.
144
+ - Slot-backed extensions receive the dashboard id, name, description, current
145
+ filters, and panel id/title/slot id as live context.
146
+ - Installs and extension access are per viewer. Sharing the dashboard does not
147
+ automatically install or grant access to its extension for other viewers.
148
+ - `config.extensionId` remains supported for legacy direct embeds. Use it only
149
+ when preserving an existing dashboard; new boxes should use a slot.
145
150
 
146
- ## Cloning An Extension-Backed Dashboard (e.g. per-customer copies)
151
+ ## Cloning A Legacy Direct-Extension Dashboard (e.g. per-customer copies)
147
152
 
148
153
  When the user asks for a copy of an existing extension-backed dashboard for a
149
154
  different customer/org (for example "make an Intuit version of the Roku usage
@@ -154,8 +159,11 @@ from that written file — never by pulling the body into chat context first or
154
159
  re-typing it as a `content` argument.
155
160
 
156
161
  1. `get-sql-dashboard` with `includeConfig: true` on the source dashboard and
157
- confirm the target panel is `chartType: "extension"`; grab its
158
- `config.extensionId`.
162
+ confirm the target panel is a legacy `chartType: "extension"` panel with
163
+ `config.extensionId`; grab that extension id. For a slot-backed panel, clone
164
+ the dashboard panel with a new stable `extensionSlotId`, then target and
165
+ install the desired extension into that slot instead of using this body-copy
166
+ playbook.
159
167
  2. `get-extension` for that id with `forceContent: true` **exactly once**. Reuse
160
168
  that body for the rest of the turn — a second same-run read intentionally
161
169
  omits `content` and returns `contentOmitted` instead. That is not the content
@@ -397,7 +405,7 @@ type PanelInput = PanelPatch & {
397
405
  chartType: NonNullable<PanelPatch["chartType"]>;
398
406
  source?: PanelPatch["source"]; // required for non-section / non-extension panels
399
407
  sql?: string; // required for non-section / non-extension panels
400
- // For chartType "extension": config.extensionId is required (the extension to embed).
408
+ // For chartType "extension": use config.extensionSlotId; extensionId is legacy direct embed compatibility.
401
409
  };
402
410
 
403
411
  type PanelFilter = {
@@ -163,6 +163,14 @@ membership id when its native update status reports `update-available`.
163
163
  serialization traps. The script is constrained: only documented dashboard
164
164
  method calls with JSON-compatible arguments are parsed; variables, imports,
165
165
  loops, functions, network, filesystem, and DB access are not available.
166
+ - Dashboard extension boxes use `chartType: "extension"` with
167
+ `config.extensionSlotId`. The stable per-box slot is
168
+ `analytics.dashboard.<dashboard-id>.panel.<panel-id>`. To add an extension,
169
+ create/choose it, call `add-extension-slot-target` with that slot id, then
170
+ `install-extension` with the same slot id. Installs are per-user; the
171
+ dashboard panel remains shared. Slot-backed extensions receive dashboard id,
172
+ name, description, current filters, and the panel id/title/slot id as context.
173
+ `config.extensionId` is legacy direct embedding for existing dashboards.
166
174
  - Dashboard saves keep bounded history in SQL. Use
167
175
  `list-dashboard-revisions` to inspect undo points and
168
176
  `restore-dashboard-revision` to restore one instead of hand-editing history
@@ -132,7 +132,7 @@ export const DASHBOARD_MUTATION_EXAMPLES = [
132
132
  `dashboard.insertPanel({"id":"new-kpi","title":"New KPI","source":"first-party","chartType":"metric","width":1,"config":{"timeScope":"dashboard"},"sql":"SELECT COUNT(*) AS value FROM analytics_events WHERE event_date >= '{{dateStart}}' AND event_date < '{{dateEnd}}'"}).atTop();`,
133
133
  `dashboard.insertPanel({"id":"new-chart","title":"New Chart","source":"first-party","chartType":"line","width":1,"config":{"timeScope":"dashboard"},"sql":"SELECT event_date AS date, COUNT(*) AS value FROM analytics_events WHERE event_date >= '{{dateStart}}' AND event_date < '{{dateEnd}}' GROUP BY event_date ORDER BY event_date"}).nextTo("retention-over-time");`,
134
134
  `dashboard.insertPanel({"id":"row-chart","title":"Row Chart","source":"first-party","chartType":"bar","width":1,"config":{"timeScope":"dashboard"},"sql":"SELECT event_name AS name, COUNT(*) AS value FROM analytics_events WHERE event_date >= '{{dateStart}}' AND event_date < '{{dateEnd}}' GROUP BY event_name"}).atRow(2);`,
135
- 'dashboard.insertPanel({"id":"pipeline-widget","title":"Pipeline Widget","chartType":"extension","width":3,"config":{"extensionId":"<existing-extension-id>"}}).atBottom();',
135
+ 'dashboard.insertPanel({"id":"pipeline-widget","title":"Pipeline Widget","chartType":"extension","width":3,"config":{"extensionSlotId":"analytics.dashboard.<dashboard-id>.panel.pipeline-widget"}}).atBottom();',
136
136
  ] as const;
137
137
 
138
138
  export type DashboardMutationOperation =
@@ -369,8 +369,12 @@ export function validateDashboardConfig(
369
369
  cfg && typeof cfg.extensionId === "string"
370
370
  ? cfg.extensionId.trim()
371
371
  : "";
372
- if (!extensionId) {
373
- return `panel[${i}].config.extensionId is required for extension panels (the id of the extension to render inline)`;
372
+ const extensionSlotId =
373
+ cfg && typeof cfg.extensionSlotId === "string"
374
+ ? cfg.extensionSlotId.trim()
375
+ : "";
376
+ if (!extensionId && !extensionSlotId) {
377
+ return `panel[${i}].config.extensionId or config.extensionSlotId is required for extension panels`;
374
378
  }
375
379
  }
376
380
  if (
@@ -1,5 +1,8 @@
1
1
  import { useDemoModeStatus, useT } from "@agent-native/core/client";
2
- import { EmbeddedExtension } from "@agent-native/core/client/extensions";
2
+ import {
3
+ EmbeddedExtension,
4
+ ExtensionSlot,
5
+ } from "@agent-native/core/client/extensions";
3
6
  import {
4
7
  IconArrowsSort,
5
8
  IconSortAscending,
@@ -1185,6 +1188,8 @@ interface SqlChartProps {
1185
1188
  className?: string;
1186
1189
  loadData?: boolean;
1187
1190
  onExportCsvChange?: (handler: (() => void) | null) => void;
1191
+ /** Dashboard/panel state sent to slot-backed extension boxes. */
1192
+ extensionContext?: Record<string, unknown> | null;
1188
1193
  }
1189
1194
 
1190
1195
  export function SqlChart({
@@ -1192,6 +1197,7 @@ export function SqlChart({
1192
1197
  resolvedSql,
1193
1198
  loadData = true,
1194
1199
  onExportCsvChange,
1200
+ extensionContext,
1195
1201
  }: SqlChartProps) {
1196
1202
  const t = useT();
1197
1203
  const { enabled: demoModeEnabled } = useDemoModeStatus();
@@ -1266,11 +1272,12 @@ export function SqlChart({
1266
1272
  );
1267
1273
  }
1268
1274
 
1269
- // Extension panels render a sandboxed extension iframe instead of querying a
1270
- // data source. The extension id lives in config.extensionId.
1275
+ // Extension panels render either a named extension-point slot or a legacy
1276
+ // direct extension iframe instead of querying a data source.
1271
1277
  if (isExtension) {
1272
1278
  const extensionId = panel.config?.extensionId;
1273
- if (!extensionId) {
1279
+ const slotId = panel.config?.extensionSlotId;
1280
+ if (!extensionId && !slotId) {
1274
1281
  return (
1275
1282
  <div className="flex flex-1 items-center justify-center px-4 py-8 min-h-[120px]">
1276
1283
  <p className="text-sm text-muted-foreground text-center">
@@ -1280,7 +1287,12 @@ export function SqlChart({
1280
1287
  );
1281
1288
  }
1282
1289
  return (
1283
- <DashboardExtensionPanel extensionId={extensionId} panelId={panel.id} />
1290
+ <DashboardExtensionPanel
1291
+ extensionId={extensionId}
1292
+ panelId={panel.id}
1293
+ slotId={slotId}
1294
+ context={extensionContext}
1295
+ />
1284
1296
  );
1285
1297
  }
1286
1298
  const colors = panel.config?.colors || DEFAULT_COLORS;
@@ -1409,9 +1421,13 @@ export function SqlChart({
1409
1421
  function DashboardExtensionPanel({
1410
1422
  extensionId,
1411
1423
  panelId,
1424
+ slotId,
1425
+ context,
1412
1426
  }: {
1413
- extensionId: string;
1427
+ extensionId?: string;
1414
1428
  panelId: string;
1429
+ slotId?: string;
1430
+ context?: Record<string, unknown> | null;
1415
1431
  }) {
1416
1432
  const t = useT();
1417
1433
  // Hold the report-readiness marker until the extension iframe paints so
@@ -1419,6 +1435,18 @@ function DashboardExtensionPanel({
1419
1435
  const [ready, setReady] = useState(false);
1420
1436
  const [unavailable, setUnavailable] = useState(false);
1421
1437
 
1438
+ if (slotId) {
1439
+ return (
1440
+ <ExtensionSlot
1441
+ id={slotId}
1442
+ context={context}
1443
+ showEmptyAffordance
1444
+ className="min-h-[120px] w-full"
1445
+ toolClassName="w-full"
1446
+ />
1447
+ );
1448
+ }
1449
+
1422
1450
  // Embedding never grants access to the extension itself (same model as
1423
1451
  // ExtensionSlots). A viewer with dashboard-only access who can't see the
1424
1452
  // referenced extension gets a clear message instead of a blank panel.
@@ -1438,7 +1466,7 @@ function DashboardExtensionPanel({
1438
1466
  data-dashboard-report-loading={ready ? undefined : "true"}
1439
1467
  >
1440
1468
  <EmbeddedExtension
1441
- extensionId={extensionId}
1469
+ extensionId={extensionId!}
1442
1470
  slotId={`dashboard-panel-${panelId}`}
1443
1471
  className="w-full"
1444
1472
  // Intentional for v1: extension panels are standalone widgets and do not
@@ -163,8 +163,6 @@ export function Layout({ children }: LayoutProps) {
163
163
  activePath: location.pathname,
164
164
  enabled: !isAskRoute && !reportScreenshot,
165
165
  });
166
- const sidebarScope = chatHomeHandoffActive ? null : analyticsScope;
167
-
168
166
  useEffect(() => {
169
167
  function handleChatRunning(event: Event) {
170
168
  const detail = (event as CustomEvent).detail;
@@ -294,7 +292,7 @@ export function Layout({ children }: LayoutProps) {
294
292
  t("chat.suggestionAnomalies"),
295
293
  t("chat.suggestionMrr"),
296
294
  ]}
297
- scope={sidebarScope}
295
+ scope={analyticsScope}
298
296
  composerSlot={<CreativeContextComposerChip />}
299
297
  >
300
298
  {contentFrame}
@@ -67,6 +67,7 @@ interface SqlChartCardProps {
67
67
  isDragSource?: boolean;
68
68
  selectedForChat?: boolean;
69
69
  onSelectForChat?: (options?: SelectDashboardPanelOptions) => void;
70
+ extensionContext?: Record<string, unknown> | null;
70
71
  }
71
72
 
72
73
  const PanelDragHandle = memo(function PanelDragHandle({
@@ -122,6 +123,7 @@ export function SqlChartCard({
122
123
  isDragSource = false,
123
124
  selectedForChat = false,
124
125
  onSelectForChat,
126
+ extensionContext,
125
127
  }: SqlChartCardProps) {
126
128
  const t = useT();
127
129
  const queryClient = useQueryClient();
@@ -346,6 +348,7 @@ export function SqlChartCard({
346
348
  panel={panel}
347
349
  resolvedSql={resolvedSql}
348
350
  loadData
351
+ extensionContext={extensionContext}
349
352
  />
350
353
  )}
351
354
  <div className="absolute right-1 top-1 flex items-center gap-1 opacity-0 group-hover:opacity-100">
@@ -435,6 +438,7 @@ export function SqlChartCard({
435
438
  panel={panel}
436
439
  resolvedSql={resolvedSql}
437
440
  loadData
441
+ extensionContext={extensionContext}
438
442
  />
439
443
  </ChartFillHeight>
440
444
  </div>
@@ -616,6 +620,7 @@ export function SqlChartCard({
616
620
  resolvedSql={resolvedSql}
617
621
  loadData={shouldLoadData}
618
622
  onExportCsvChange={handleExportCsvChange}
623
+ extensionContext={extensionContext}
619
624
  />
620
625
  </CardContent>
621
626
  </Card>
@@ -627,7 +632,12 @@ export function SqlChartCard({
627
632
  </DialogHeader>
628
633
  <div className="flex min-h-0 flex-1 flex-col overflow-auto">
629
634
  <ChartFillHeight>
630
- <SqlChart panel={panel} resolvedSql={resolvedSql} loadData />
635
+ <SqlChart
636
+ panel={panel}
637
+ resolvedSql={resolvedSql}
638
+ loadData
639
+ extensionContext={extensionContext}
640
+ />
631
641
  </ChartFillHeight>
632
642
  </div>
633
643
  </DialogContent>
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Stable extension-point name for a dashboard box. The dashboard and panel ids
3
+ * are encoded independently so arbitrary saved ids cannot collapse into one
4
+ * slot or add path separators to the slots API.
5
+ */
6
+ export function dashboardExtensionSlotId(
7
+ dashboardId: string,
8
+ panelId: string,
9
+ ): string {
10
+ return `analytics.dashboard.${encodeURIComponent(dashboardId)}.panel.${encodeURIComponent(panelId)}`;
11
+ }
@@ -142,6 +142,7 @@ import {
142
142
  resolveFilterVars,
143
143
  } from "./DashboardFilterBar";
144
144
  import { EmailReportDialog } from "./EmailReportDialog";
145
+ import { dashboardExtensionSlotId } from "./extension-slot";
145
146
  import { interpolate } from "./interpolate";
146
147
  import { serializePanelSql } from "./panel-sql";
147
148
  import { AddPanelPopover, PanelEditorDialog } from "./PanelEditorDialog";
@@ -271,6 +272,7 @@ const PanelCell = memo(function PanelCell({
271
272
  onRemovePanel,
272
273
  onEditPanel,
273
274
  onSavePanel,
275
+ dashboardExtensionContext,
274
276
  }: {
275
277
  panel: SqlPanel;
276
278
  vars: Record<string, string>;
@@ -286,6 +288,7 @@ const PanelCell = memo(function PanelCell({
286
288
  onRemovePanel: (panelId: string) => void;
287
289
  onEditPanel: (panel: SqlPanel) => void;
288
290
  onSavePanel: (panel: SqlPanel) => Promise<void>;
291
+ dashboardExtensionContext: Record<string, unknown>;
289
292
  }) {
290
293
  const resolved = useMemo(
291
294
  () =>
@@ -366,6 +369,23 @@ const PanelCell = memo(function PanelCell({
366
369
  isDragSource={isDragSource}
367
370
  selectedForChat={selectedForChat}
368
371
  onSelectForChat={handleSelectForChat}
372
+ extensionContext={
373
+ panel.chartType === "extension"
374
+ ? {
375
+ ...dashboardExtensionContext,
376
+ panel: {
377
+ id: panel.id,
378
+ title: panel.title,
379
+ slotId:
380
+ panel.config?.extensionSlotId ??
381
+ dashboardExtensionSlotId(
382
+ String(dashboardExtensionContext.dashboardId),
383
+ panel.id,
384
+ ),
385
+ },
386
+ }
387
+ : undefined
388
+ }
369
389
  />
370
390
  </div>
371
391
  );
@@ -1966,6 +1986,13 @@ export default function SqlDashboardPage() {
1966
1986
  onRemovePanel={removePanel}
1967
1987
  onEditPanel={openEditPanel}
1968
1988
  onSavePanel={handleSavePanel}
1989
+ dashboardExtensionContext={{
1990
+ dashboardId,
1991
+ dashboardName: dashboard.name,
1992
+ dashboardDescription:
1993
+ dashboard.description ?? null,
1994
+ filters: vars,
1995
+ }}
1969
1996
  />
1970
1997
  <DashboardDropLine
1971
1998
  slot={{
@@ -99,8 +99,14 @@ export interface SqlPanelConfig {
99
99
  /**
100
100
  * Extension panels only (`chartType: "extension"`): id of the extension to
101
101
  * render inline as a sandboxed iframe instead of running the SQL pipeline.
102
+ * Kept for dashboards saved before slot-backed dashboard boxes.
102
103
  */
103
104
  extensionId?: string;
105
+ /**
106
+ * Extension panels only: a named extension-point slot. The dashboard renders
107
+ * its installed extensions as this panel's box and supplies dashboard context.
108
+ */
109
+ extensionSlotId?: string;
104
110
  }
105
111
 
106
112
  export interface SqlPanel {
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: added
3
+ date: 2026-07-17
4
+ ---
5
+
6
+ Dashboards can now host installed extensions as contextual, slot-backed boxes.
@@ -64,6 +64,14 @@ function panelSummaries(config: Record<string, unknown>) {
64
64
  typeof panelConfig.description === "string"
65
65
  ? panelConfig.description
66
66
  : undefined,
67
+ extensionId:
68
+ typeof panelConfig.extensionId === "string"
69
+ ? panelConfig.extensionId
70
+ : undefined,
71
+ extensionSlotId:
72
+ typeof panelConfig.extensionSlotId === "string"
73
+ ? panelConfig.extensionSlotId
74
+ : undefined,
67
75
  };
68
76
  });
69
77
  }