@agent-native/core 0.78.8 → 0.79.0

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 (181) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +25 -0
  3. package/corpus/core/docs/content/actions.md +4 -2
  4. package/corpus/core/docs/content/agent-surfaces.md +9 -0
  5. package/corpus/core/docs/content/extensions.md +10 -0
  6. package/corpus/core/docs/content/generative-ui.md +217 -0
  7. package/corpus/core/docs/content/key-concepts.md +2 -2
  8. package/corpus/core/docs/content/mcp-apps.md +3 -5
  9. package/corpus/core/docs/content/native-chat-ui.md +9 -5
  10. package/corpus/core/docs/content/tracking.md +9 -3
  11. package/corpus/core/package.json +1 -1
  12. package/corpus/core/src/action-ui.ts +1 -0
  13. package/corpus/core/src/client/analytics.ts +195 -12
  14. package/corpus/core/src/client/chat/index.ts +1 -0
  15. package/corpus/core/src/client/chat/tool-call-display.tsx +5 -2
  16. package/corpus/core/src/client/chat/widgets/InlineExtensionWidget.tsx +139 -0
  17. package/corpus/core/src/client/chat/widgets/builtin-tool-renderers.tsx +27 -1
  18. package/corpus/core/src/client/conversation/AgentConversation.tsx +38 -0
  19. package/corpus/core/src/client/conversation/code-agent-transcript.ts +9 -0
  20. package/corpus/core/src/client/conversation/types.ts +4 -0
  21. package/corpus/core/src/client/extensions/EmbeddedExtension.tsx +27 -5
  22. package/corpus/core/src/client/extensions/ExtensionViewer.tsx +26 -5
  23. package/corpus/core/src/client/extensions/InlineExtensionFrame.tsx +654 -0
  24. package/corpus/core/src/client/extensions/iframe-bridge.ts +30 -0
  25. package/corpus/core/src/client/extensions/index.ts +5 -0
  26. package/corpus/core/src/client/index.ts +1 -0
  27. package/corpus/core/src/client/session-replay.ts +89 -14
  28. package/corpus/core/src/code-agents/transcript-normalizer.ts +14 -0
  29. package/corpus/core/src/extensions/actions.ts +218 -1
  30. package/corpus/core/src/extensions/html-shell.ts +70 -3
  31. package/corpus/core/src/extensions/routes.ts +12 -4
  32. package/corpus/core/src/index.browser.ts +1 -0
  33. package/corpus/core/src/index.ts +1 -0
  34. package/corpus/core/src/mcp/build-server.ts +1 -1
  35. package/corpus/core/src/mcp/embed-app.ts +11 -1
  36. package/corpus/core/src/server/agent-chat-plugin.ts +21 -9
  37. package/corpus/core/src/templates/workspace-core/.agents/skills/extensions/SKILL.md +13 -0
  38. package/corpus/core/src/templates/workspace-core/.agents/skills/extensions/references/api.md +2 -0
  39. package/corpus/core/src/templates/workspace-core/.agents/skills/generative-ui/SKILL.md +115 -0
  40. package/corpus/templates/analytics/.agents/skills/dashboard-management/SKILL.md +198 -59
  41. package/corpus/templates/analytics/AGENTS.md +12 -0
  42. package/corpus/templates/analytics/actions/dashboard-mutation-api.ts +1105 -0
  43. package/corpus/templates/analytics/actions/dashboard-panel-order.ts +169 -0
  44. package/corpus/templates/analytics/actions/get-sql-dashboard.ts +89 -6
  45. package/corpus/templates/analytics/actions/mutate-dashboard.ts +333 -0
  46. package/corpus/templates/analytics/actions/reorder-dashboard-panels.ts +214 -0
  47. package/corpus/templates/analytics/actions/send-dashboard-report-now.ts +48 -21
  48. package/corpus/templates/analytics/actions/update-dashboard.ts +193 -100
  49. package/corpus/templates/analytics/app/components/dashboard/SqlChart.tsx +11 -0
  50. package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +1 -1
  51. package/corpus/templates/analytics/app/global.css +81 -10
  52. package/corpus/templates/analytics/app/i18n-data.ts +249 -4
  53. package/corpus/templates/analytics/app/pages/adhoc/explorer-dashboard/ChartCard.tsx +3 -3
  54. package/corpus/templates/analytics/app/pages/adhoc/explorer-dashboard/index.tsx +41 -1
  55. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/EmailReportDialog.tsx +1 -1
  56. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/PanelEditorDialog.tsx +4 -4
  57. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/SqlChartCard.tsx +48 -18
  58. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/dashboard-layout.ts +32 -0
  59. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx +204 -70
  60. package/corpus/templates/analytics/app/pages/sessions/SessionDetailPage.tsx +1101 -227
  61. package/corpus/templates/analytics/app/pages/sessions/SessionsPage.tsx +160 -133
  62. package/corpus/templates/analytics/changelog/2026-06-26-agents-can-now-edit-dashboards-through-a-typed-mutation-scri.md +6 -0
  63. package/corpus/templates/analytics/changelog/2026-06-26-dashboard-bar-chart-hovers-stay-subtle-in-dark-mode.md +6 -0
  64. package/corpus/templates/analytics/changelog/2026-06-26-dashboard-chart-moves-are-now-handled-by-panel-id-making-sim.md +6 -0
  65. package/corpus/templates/analytics/changelog/2026-06-26-dashboard-charts-drag-smoothly-with-stable-cards-inactive-ch.md +6 -0
  66. package/corpus/templates/analytics/changelog/2026-06-26-dashboard-charts-show-a-refresh-spinner-while-keeping-existi.md +6 -0
  67. package/corpus/templates/analytics/changelog/2026-06-26-dashboard-edits-stay-stable-while-chart-order-and-deletions-.md +6 -0
  68. package/corpus/templates/analytics/changelog/2026-06-26-dashboard-email-reports-queue-immediate-sends.md +6 -0
  69. package/corpus/templates/analytics/changelog/2026-06-26-explorer-dashboards-now-drag-charts-with-a-stable-preview-an.md +6 -0
  70. package/corpus/templates/analytics/changelog/2026-06-26-session-replay-playback-can-now-be-toggled-by-clicking-the-r.md +6 -0
  71. package/corpus/templates/analytics/changelog/2026-06-26-session-replays-now-open-from-the-full-row-and-play-with-tim.md +6 -0
  72. package/corpus/templates/analytics/server/lib/dashboard-report-subscriptions.ts +21 -0
  73. package/corpus/templates/analytics/server/lib/dashboard-report.ts +41 -17
  74. package/corpus/templates/analytics/server/plugins/agent-chat.ts +5 -0
  75. package/dist/action-ui.d.ts +1 -0
  76. package/dist/action-ui.d.ts.map +1 -1
  77. package/dist/action-ui.js +1 -0
  78. package/dist/action-ui.js.map +1 -1
  79. package/dist/client/analytics.d.ts.map +1 -1
  80. package/dist/client/analytics.js +173 -12
  81. package/dist/client/analytics.js.map +1 -1
  82. package/dist/client/chat/index.d.ts +1 -1
  83. package/dist/client/chat/index.d.ts.map +1 -1
  84. package/dist/client/chat/index.js +1 -1
  85. package/dist/client/chat/index.js.map +1 -1
  86. package/dist/client/chat/tool-call-display.d.ts.map +1 -1
  87. package/dist/client/chat/tool-call-display.js +3 -1
  88. package/dist/client/chat/tool-call-display.js.map +1 -1
  89. package/dist/client/chat/widgets/InlineExtensionWidget.d.ts +17 -0
  90. package/dist/client/chat/widgets/InlineExtensionWidget.d.ts.map +1 -0
  91. package/dist/client/chat/widgets/InlineExtensionWidget.js +85 -0
  92. package/dist/client/chat/widgets/InlineExtensionWidget.js.map +1 -0
  93. package/dist/client/chat/widgets/builtin-tool-renderers.d.ts.map +1 -1
  94. package/dist/client/chat/widgets/builtin-tool-renderers.js +15 -2
  95. package/dist/client/chat/widgets/builtin-tool-renderers.js.map +1 -1
  96. package/dist/client/conversation/AgentConversation.d.ts.map +1 -1
  97. package/dist/client/conversation/AgentConversation.js +33 -0
  98. package/dist/client/conversation/AgentConversation.js.map +1 -1
  99. package/dist/client/conversation/code-agent-transcript.js +8 -0
  100. package/dist/client/conversation/code-agent-transcript.js.map +1 -1
  101. package/dist/client/conversation/types.d.ts +4 -0
  102. package/dist/client/conversation/types.d.ts.map +1 -1
  103. package/dist/client/conversation/types.js.map +1 -1
  104. package/dist/client/extensions/EmbeddedExtension.d.ts.map +1 -1
  105. package/dist/client/extensions/EmbeddedExtension.js +29 -1
  106. package/dist/client/extensions/EmbeddedExtension.js.map +1 -1
  107. package/dist/client/extensions/ExtensionViewer.d.ts.map +1 -1
  108. package/dist/client/extensions/ExtensionViewer.js +28 -1
  109. package/dist/client/extensions/ExtensionViewer.js.map +1 -1
  110. package/dist/client/extensions/InlineExtensionFrame.d.ts +25 -0
  111. package/dist/client/extensions/InlineExtensionFrame.d.ts.map +1 -0
  112. package/dist/client/extensions/InlineExtensionFrame.js +488 -0
  113. package/dist/client/extensions/InlineExtensionFrame.js.map +1 -0
  114. package/dist/client/extensions/iframe-bridge.d.ts +2 -0
  115. package/dist/client/extensions/iframe-bridge.d.ts.map +1 -1
  116. package/dist/client/extensions/iframe-bridge.js +23 -0
  117. package/dist/client/extensions/iframe-bridge.js.map +1 -1
  118. package/dist/client/extensions/index.d.ts +1 -0
  119. package/dist/client/extensions/index.d.ts.map +1 -1
  120. package/dist/client/extensions/index.js +1 -0
  121. package/dist/client/extensions/index.js.map +1 -1
  122. package/dist/client/index.d.ts +1 -1
  123. package/dist/client/index.d.ts.map +1 -1
  124. package/dist/client/index.js +1 -1
  125. package/dist/client/index.js.map +1 -1
  126. package/dist/client/session-replay.d.ts +11 -1
  127. package/dist/client/session-replay.d.ts.map +1 -1
  128. package/dist/client/session-replay.js +74 -14
  129. package/dist/client/session-replay.js.map +1 -1
  130. package/dist/code-agents/transcript-normalizer.d.ts +2 -0
  131. package/dist/code-agents/transcript-normalizer.d.ts.map +1 -1
  132. package/dist/code-agents/transcript-normalizer.js +8 -0
  133. package/dist/code-agents/transcript-normalizer.js.map +1 -1
  134. package/dist/collab/awareness.d.ts +2 -2
  135. package/dist/collab/awareness.d.ts.map +1 -1
  136. package/dist/collab/routes.d.ts +2 -2
  137. package/dist/extensions/actions.d.ts.map +1 -1
  138. package/dist/extensions/actions.js +200 -1
  139. package/dist/extensions/actions.js.map +1 -1
  140. package/dist/extensions/html-shell.d.ts.map +1 -1
  141. package/dist/extensions/html-shell.js +70 -3
  142. package/dist/extensions/html-shell.js.map +1 -1
  143. package/dist/extensions/routes.d.ts.map +1 -1
  144. package/dist/extensions/routes.js +11 -4
  145. package/dist/extensions/routes.js.map +1 -1
  146. package/dist/file-upload/actions/upload-image.d.ts +2 -2
  147. package/dist/index.browser.d.ts +1 -1
  148. package/dist/index.browser.d.ts.map +1 -1
  149. package/dist/index.browser.js +1 -1
  150. package/dist/index.browser.js.map +1 -1
  151. package/dist/index.d.ts +1 -1
  152. package/dist/index.d.ts.map +1 -1
  153. package/dist/index.js +1 -1
  154. package/dist/index.js.map +1 -1
  155. package/dist/mcp/build-server.js +1 -1
  156. package/dist/mcp/build-server.js.map +1 -1
  157. package/dist/mcp/embed-app.d.ts.map +1 -1
  158. package/dist/mcp/embed-app.js +11 -1
  159. package/dist/mcp/embed-app.js.map +1 -1
  160. package/dist/observability/routes.d.ts +7 -7
  161. package/dist/provider-api/corpus-jobs.d.ts +2 -2
  162. package/dist/resources/handlers.d.ts +2 -2
  163. package/dist/server/agent-chat-plugin.d.ts.map +1 -1
  164. package/dist/server/agent-chat-plugin.js +21 -9
  165. package/dist/server/agent-chat-plugin.js.map +1 -1
  166. package/dist/server/agent-engine-api-key-route.d.ts +2 -2
  167. package/dist/templates/workspace-core/.agents/skills/extensions/SKILL.md +13 -0
  168. package/dist/templates/workspace-core/.agents/skills/extensions/references/api.md +2 -0
  169. package/dist/templates/workspace-core/.agents/skills/generative-ui/SKILL.md +115 -0
  170. package/docs/content/actions.md +4 -2
  171. package/docs/content/agent-surfaces.md +9 -0
  172. package/docs/content/extensions.md +10 -0
  173. package/docs/content/generative-ui.md +217 -0
  174. package/docs/content/key-concepts.md +2 -2
  175. package/docs/content/mcp-apps.md +3 -5
  176. package/docs/content/native-chat-ui.md +9 -5
  177. package/docs/content/tracking.md +9 -3
  178. package/package.json +1 -1
  179. package/src/templates/workspace-core/.agents/skills/extensions/SKILL.md +13 -0
  180. package/src/templates/workspace-core/.agents/skills/extensions/references/api.md +2 -0
  181. package/src/templates/workspace-core/.agents/skills/generative-ui/SKILL.md +115 -0
@@ -0,0 +1,169 @@
1
+ export type PanelOrderTarget =
2
+ | { position: "top" | "bottom" }
3
+ | { index: number }
4
+ | { beforePanelId: string }
5
+ | { afterPanelId: string };
6
+
7
+ export interface PanelOrderResult {
8
+ panelCount: number;
9
+ panelOrder: string[];
10
+ firstPanelIds: string[];
11
+ movedPanelIds: string[];
12
+ missingPanelIds: string[];
13
+ skippedDuplicatePanelIds: string[];
14
+ insertIndex: number;
15
+ }
16
+
17
+ function panelsFromConfig(config: Record<string, unknown>) {
18
+ const panels = config.panels;
19
+ if (!Array.isArray(panels)) {
20
+ throw new Error("config.panels must be an array");
21
+ }
22
+ return panels as Array<Record<string, unknown>>;
23
+ }
24
+
25
+ function panelId(panel: Record<string, unknown>): string {
26
+ return typeof panel.id === "string" ? panel.id : "";
27
+ }
28
+
29
+ export function getPanelOrder(config: Record<string, unknown>): string[] {
30
+ return panelsFromConfig(config).map(panelId).filter(Boolean);
31
+ }
32
+
33
+ export function getFirstPanelIds(
34
+ config: Record<string, unknown>,
35
+ count = 10,
36
+ ): string[] {
37
+ return getPanelOrder(config).slice(0, count);
38
+ }
39
+
40
+ function uniquePanelIds(ids: string[]): {
41
+ ids: string[];
42
+ duplicates: string[];
43
+ } {
44
+ const seen = new Set<string>();
45
+ const out: string[] = [];
46
+ const duplicates: string[] = [];
47
+ for (const raw of ids) {
48
+ const id = raw.trim();
49
+ if (!id) continue;
50
+ if (seen.has(id)) {
51
+ duplicates.push(id);
52
+ continue;
53
+ }
54
+ seen.add(id);
55
+ out.push(id);
56
+ }
57
+ return { ids: out, duplicates };
58
+ }
59
+
60
+ function resolveInsertIndex(
61
+ remainingPanels: Array<Record<string, unknown>>,
62
+ target: PanelOrderTarget,
63
+ ): number {
64
+ if ("position" in target) {
65
+ return target.position === "bottom" ? remainingPanels.length : 0;
66
+ }
67
+ if ("index" in target) {
68
+ const index = target.index;
69
+ if (
70
+ !Number.isInteger(index) ||
71
+ index < 0 ||
72
+ index > remainingPanels.length
73
+ ) {
74
+ throw new Error(
75
+ `index must be an integer between 0 and ${remainingPanels.length}`,
76
+ );
77
+ }
78
+ return index;
79
+ }
80
+
81
+ const targetId =
82
+ "beforePanelId" in target ? target.beforePanelId : target.afterPanelId;
83
+ const index = remainingPanels.findIndex(
84
+ (panel) => panelId(panel) === targetId,
85
+ );
86
+ if (index < 0) {
87
+ throw new Error(`target panel "${targetId}" was not found`);
88
+ }
89
+ return "beforePanelId" in target ? index : index + 1;
90
+ }
91
+
92
+ export function movePanelsById(
93
+ config: Record<string, unknown>,
94
+ panelIds: string[],
95
+ target: PanelOrderTarget = { position: "top" },
96
+ ): PanelOrderResult {
97
+ const panels = panelsFromConfig(config);
98
+ const { ids, duplicates } = uniquePanelIds(panelIds);
99
+ if (ids.length === 0) {
100
+ throw new Error("panelIds must include at least one panel id");
101
+ }
102
+
103
+ const wanted = new Set(ids);
104
+ const found = new Set<string>();
105
+ const moving: Array<Record<string, unknown>> = [];
106
+ const remaining: Array<Record<string, unknown>> = [];
107
+
108
+ for (const panel of panels) {
109
+ const id = panelId(panel);
110
+ if (wanted.has(id)) {
111
+ found.add(id);
112
+ moving.push(panel);
113
+ } else {
114
+ remaining.push(panel);
115
+ }
116
+ }
117
+
118
+ const missing = ids.filter((id) => !found.has(id));
119
+ if (missing.length > 0) {
120
+ throw new Error(`panel id(s) not found: ${missing.join(", ")}`);
121
+ }
122
+
123
+ const orderedMoving = ids.map(
124
+ (id) => moving.find((panel) => panelId(panel) === id)!,
125
+ );
126
+ const insertIndex = resolveInsertIndex(remaining, target);
127
+ const nextPanels = [
128
+ ...remaining.slice(0, insertIndex),
129
+ ...orderedMoving,
130
+ ...remaining.slice(insertIndex),
131
+ ];
132
+
133
+ config.panels = nextPanels;
134
+ const order = getPanelOrder(config);
135
+ return {
136
+ panelCount: order.length,
137
+ panelOrder: order,
138
+ firstPanelIds: order.slice(0, 10),
139
+ movedPanelIds: ids,
140
+ missingPanelIds: [],
141
+ skippedDuplicatePanelIds: duplicates,
142
+ insertIndex,
143
+ };
144
+ }
145
+
146
+ export function applyPanelOrder(
147
+ config: Record<string, unknown>,
148
+ requestedPanelIds: string[],
149
+ ): PanelOrderResult {
150
+ return movePanelsById(config, requestedPanelIds, { position: "top" });
151
+ }
152
+
153
+ export function compactDashboardResult(
154
+ config: Record<string, unknown>,
155
+ movedPanelIds: string[] = [],
156
+ ): {
157
+ panelCount: number;
158
+ panelOrder: string[];
159
+ firstPanelIds: string[];
160
+ movedPanelIds: string[];
161
+ } {
162
+ const order = getPanelOrder(config);
163
+ return {
164
+ panelCount: order.length,
165
+ panelOrder: order,
166
+ firstPanelIds: order.slice(0, 10),
167
+ movedPanelIds,
168
+ };
169
+ }
@@ -6,16 +6,83 @@ import {
6
6
  } from "@agent-native/core/server";
7
7
  import { z } from "zod";
8
8
 
9
+ import { buildDashboardPanelGroups } from "../app/pages/adhoc/sql-dashboard/dashboard-layout";
10
+ import {
11
+ clampDashboardColumns,
12
+ type SqlPanel,
13
+ } from "../app/pages/adhoc/sql-dashboard/types";
9
14
  import { loadDashboardSeed } from "../server/lib/dashboard-seeds";
10
15
  import { getDashboard } from "../server/lib/dashboards-store";
16
+ import { getPanelOrder } from "./dashboard-panel-order";
17
+
18
+ function dashboardLayoutSummary(config: Record<string, unknown>) {
19
+ const panels = Array.isArray(config.panels)
20
+ ? (config.panels as SqlPanel[])
21
+ : [];
22
+ const columns = clampDashboardColumns(config.columns);
23
+ const groups = buildDashboardPanelGroups(panels, columns);
24
+ const panelOrder = getPanelOrder(config);
25
+
26
+ return {
27
+ panelCount: panelOrder.length,
28
+ panelOrder,
29
+ firstPanelIds: panelOrder.slice(0, 10),
30
+ groups: groups.map((group) => ({
31
+ key: group.key,
32
+ sectionId: group.section?.id ?? null,
33
+ sectionTitle: group.section?.title ?? null,
34
+ columns: group.columns,
35
+ rows: group.rows.map((row, rowIndex) => ({
36
+ rowIndex,
37
+ panelIds: row.panels.map((panel) => panel.id),
38
+ })),
39
+ })),
40
+ };
41
+ }
42
+
43
+ function panelSummaries(config: Record<string, unknown>) {
44
+ const panels = Array.isArray(config.panels)
45
+ ? (config.panels as Array<Record<string, unknown>>)
46
+ : [];
47
+ return panels.map((panel, index) => {
48
+ const panelConfig =
49
+ panel.config &&
50
+ typeof panel.config === "object" &&
51
+ !Array.isArray(panel.config)
52
+ ? (panel.config as Record<string, unknown>)
53
+ : {};
54
+ return {
55
+ index,
56
+ id: typeof panel.id === "string" ? panel.id : "",
57
+ title: typeof panel.title === "string" ? panel.title : "",
58
+ chartType: typeof panel.chartType === "string" ? panel.chartType : "",
59
+ source: typeof panel.source === "string" ? panel.source : undefined,
60
+ width: typeof panel.width === "number" ? panel.width : undefined,
61
+ columns: typeof panel.columns === "number" ? panel.columns : undefined,
62
+ tab: typeof panel.tab === "string" ? panel.tab : undefined,
63
+ description:
64
+ typeof panelConfig.description === "string"
65
+ ? panelConfig.description
66
+ : undefined,
67
+ };
68
+ });
69
+ }
11
70
 
12
71
  function seededResponse(
13
72
  id: string,
14
73
  seed: Record<string, unknown>,
74
+ includeConfig: boolean,
15
75
  ): Record<string, unknown> {
16
- return {
76
+ const base = {
17
77
  id,
18
- ...seed,
78
+ name: typeof seed.name === "string" ? seed.name : id,
79
+ description:
80
+ typeof seed.description === "string" ? seed.description : undefined,
81
+ filters: seed.filters,
82
+ variables: seed.variables,
83
+ columns: seed.columns,
84
+ panels: panelSummaries(seed),
85
+ layout: dashboardLayoutSummary(seed),
19
86
  ownerEmail: null,
20
87
  orgId: null,
21
88
  visibility: "org",
@@ -23,13 +90,20 @@ function seededResponse(
23
90
  hiddenAt: null,
24
91
  hiddenBy: null,
25
92
  };
93
+ return includeConfig ? { ...base, ...seed } : base;
26
94
  }
27
95
 
28
96
  export default defineAction({
29
97
  description:
30
- "Get a SQL analytics dashboard by ID, including its full panel config, visibility, and access metadata.",
98
+ "Get a SQL analytics dashboard by ID. By default this returns compact panel summaries and layout/order fields without giant SQL strings; use includeConfig=true only when you need the full dashboard config for a detailed SQL/config edit.",
31
99
  schema: z.object({
32
100
  id: z.string().describe("The dashboard ID"),
101
+ includeConfig: z
102
+ .boolean()
103
+ .optional()
104
+ .describe(
105
+ "If true, include the full dashboard config including panel SQL. Defaults to false to keep agent context compact.",
106
+ ),
33
107
  }),
34
108
  http: { method: "GET" },
35
109
  readOnly: true,
@@ -69,15 +143,23 @@ export default defineAction({
69
143
  const dash = await getDashboard(args.id, ctx);
70
144
  if (!dash || dash.kind !== "sql") {
71
145
  const seed = loadDashboardSeed(args.id);
72
- if (seed) return seededResponse(args.id, seed);
146
+ if (seed)
147
+ return seededResponse(args.id, seed, args.includeConfig === true);
73
148
  throw Object.assign(new Error("Dashboard not found"), {
74
149
  statusCode: 404,
75
150
  });
76
151
  }
77
152
  const config = dash.config as Record<string, unknown>;
78
- return {
153
+ const base = {
79
154
  id: args.id,
80
- ...config,
155
+ name: typeof config.name === "string" ? config.name : args.id,
156
+ description:
157
+ typeof config.description === "string" ? config.description : undefined,
158
+ filters: config.filters,
159
+ variables: config.variables,
160
+ columns: config.columns,
161
+ panels: panelSummaries(config),
162
+ layout: dashboardLayoutSummary(config),
81
163
  ownerEmail: dash.ownerEmail,
82
164
  orgId: dash.orgId,
83
165
  visibility: dash.visibility,
@@ -90,5 +172,6 @@ export default defineAction({
90
172
  createdAt: dash.createdAt,
91
173
  updatedAt: dash.updatedAt,
92
174
  };
175
+ return args.includeConfig === true ? { ...base, ...config } : base;
93
176
  },
94
177
  });
@@ -0,0 +1,333 @@
1
+ import { defineAction, embedApp } from "@agent-native/core";
2
+ import {
3
+ applyText,
4
+ hasCollabState,
5
+ seedFromText,
6
+ } from "@agent-native/core/collab";
7
+ import {
8
+ buildDeepLink,
9
+ getRequestOrgId,
10
+ getRequestUserEmail,
11
+ } from "@agent-native/core/server";
12
+ import { z } from "zod";
13
+
14
+ import { getDashboard, upsertDashboard } from "../server/lib/dashboards-store";
15
+ import {
16
+ applyDashboardMutationOperations,
17
+ DASHBOARD_MUTATION_API_TYPES,
18
+ DASHBOARD_MUTATION_EXAMPLES,
19
+ parseDashboardMutationScript,
20
+ type DashboardMutationOperation,
21
+ } from "./dashboard-mutation-api";
22
+ import { compactDashboardResult } from "./dashboard-panel-order";
23
+ import { validateDashboardConfig, validatePanelSql } from "./update-dashboard";
24
+
25
+ const mutationTargetSchema = {
26
+ position: z.enum(["top", "bottom"]).optional(),
27
+ index: z.number().int().nonnegative().optional(),
28
+ beforePanelId: z.string().optional(),
29
+ afterPanelId: z.string().optional(),
30
+ };
31
+
32
+ const mutationOperationSchema = z.discriminatedUnion("op", [
33
+ z.object({
34
+ op: z.literal("movePanels"),
35
+ panelIds: z.array(z.string()).min(1),
36
+ ...mutationTargetSchema,
37
+ }),
38
+ z.object({
39
+ op: z.literal("removePanels"),
40
+ panelIds: z.array(z.string()).min(1),
41
+ }),
42
+ z.object({
43
+ op: z.literal("updatePanel"),
44
+ panelId: z.string(),
45
+ patch: z.record(z.string(), z.unknown()),
46
+ }),
47
+ z.object({
48
+ op: z.literal("updatePanelPath"),
49
+ panelId: z.string(),
50
+ path: z.string(),
51
+ value: z.unknown(),
52
+ }),
53
+ z.object({
54
+ op: z.literal("insertPanel"),
55
+ panel: z.record(z.string(), z.unknown()),
56
+ ...mutationTargetSchema,
57
+ }),
58
+ z.object({
59
+ op: z.literal("duplicatePanel"),
60
+ panelId: z.string(),
61
+ newPanelId: z.string(),
62
+ patch: z.record(z.string(), z.unknown()).optional(),
63
+ ...mutationTargetSchema,
64
+ }),
65
+ z.object({
66
+ op: z.literal("setDashboard"),
67
+ patch: z.record(z.string(), z.unknown()),
68
+ }),
69
+ ]);
70
+
71
+ function parseJsonArrayString(
72
+ value: string,
73
+ fieldName: string,
74
+ ): DashboardMutationOperation[] {
75
+ let parsed: unknown;
76
+ try {
77
+ parsed = JSON.parse(value);
78
+ } catch (err: any) {
79
+ throw new Error(`${fieldName} must be a JSON array: ${err.message}`);
80
+ }
81
+ if (!Array.isArray(parsed)) {
82
+ throw new Error(`${fieldName} must be a JSON array`);
83
+ }
84
+ return parsed.map((op, index) => {
85
+ try {
86
+ return mutationOperationSchema.parse(op) as DashboardMutationOperation;
87
+ } catch (err: any) {
88
+ throw new Error(`${fieldName}[${index}] is invalid: ${err.message}`);
89
+ }
90
+ });
91
+ }
92
+
93
+ const operationsInputSchema = z
94
+ .union([
95
+ z.array(mutationOperationSchema),
96
+ z.string().transform((value) => parseJsonArrayString(value, "operations")),
97
+ ])
98
+ .optional();
99
+
100
+ const apiHelp =
101
+ "Constrained TypeScript-like dashboard mutation script. The server parses only calls on `dashboard`; it does not execute arbitrary JavaScript. " +
102
+ "No variables, imports, loops, functions, templates, network, filesystem, or DB access. Arguments must be JSON-compatible literals, so quote object keys. " +
103
+ "Subjects: dashboard.set, dashboard.panel, dashboard.panels, dashboard.panelsMatching, dashboard.section, dashboard.insertPanel. " +
104
+ "Selection methods: moveToTop, moveToBottom, moveBefore, moveAfter, moveToIndex, remove, set, setTitle, setSql, setWidth, setConfig, setConfigPath, duplicate. " +
105
+ `Examples: ${DASHBOARD_MUTATION_EXAMPLES.slice(0, 3).join(" ")}`;
106
+
107
+ function resolveScope() {
108
+ const orgId = getRequestOrgId() || null;
109
+ const email = getRequestUserEmail();
110
+ if (!email) throw new Error("no authenticated user");
111
+ return { orgId, email };
112
+ }
113
+
114
+ function resolveDashboardId(args: { dashboardId?: string; id?: string }) {
115
+ const dashboardId = args.dashboardId || args.id;
116
+ if (!dashboardId) {
117
+ throw new Error("provide `dashboardId` (or legacy `id`).");
118
+ }
119
+ return dashboardId;
120
+ }
121
+
122
+ function cloneConfig(config: Record<string, unknown>): Record<string, unknown> {
123
+ return JSON.parse(JSON.stringify(config)) as Record<string, unknown>;
124
+ }
125
+
126
+ async function syncToCollab(
127
+ dashboardId: string,
128
+ config: Record<string, unknown>,
129
+ ): Promise<void> {
130
+ const docId = `dash-${dashboardId}`;
131
+ const configStr = JSON.stringify(config);
132
+ try {
133
+ const exists = await hasCollabState(docId);
134
+ if (exists) {
135
+ await applyText(docId, configStr, "content", "agent");
136
+ } else {
137
+ await seedFromText(docId, configStr);
138
+ }
139
+ } catch {
140
+ // SQL remains the source of truth; live collab sync is best-effort.
141
+ }
142
+ }
143
+
144
+ function mutationCanChangePanelSql(op: DashboardMutationOperation): boolean {
145
+ if (op.op === "insertPanel" || op.op === "duplicatePanel") return true;
146
+ if (op.op === "setDashboard") {
147
+ return Object.keys(op.patch).some((key) =>
148
+ ["filters", "variables"].includes(key),
149
+ );
150
+ }
151
+ if (op.op !== "updatePanel") return false;
152
+ return Object.keys(op.patch).some((key) =>
153
+ ["sql", "source", "chartType"].includes(key),
154
+ );
155
+ }
156
+
157
+ function movedPanelIdsFrom(operations: DashboardMutationOperation[]): string[] {
158
+ const moved = new Set<string>();
159
+ for (const op of operations) {
160
+ if (op.op !== "movePanels") continue;
161
+ for (const id of op.panelIds) moved.add(id);
162
+ }
163
+ return Array.from(moved);
164
+ }
165
+
166
+ function helpResult() {
167
+ return {
168
+ mutationApiVersion: 1,
169
+ apiTypes: DASHBOARD_MUTATION_API_TYPES,
170
+ examples: DASHBOARD_MUTATION_EXAMPLES,
171
+ summary:
172
+ "Use `code` for constrained dashboard mutation scripts, or `operations` for the equivalent structured ops.",
173
+ };
174
+ }
175
+
176
+ export default defineAction({
177
+ description:
178
+ "Apply general SQL dashboard edits through a small typed mutation API in ONE atomic save. " +
179
+ "Prefer this for dashboard layout and panel edits: move panels by id, edit titles/SQL/width/config, remove panels, duplicate panels, insert panels, or patch dashboard fields. " +
180
+ "This is code-shaped but not arbitrary code execution: the server parses the allowed dashboard methods, validates the resulting config with the same invariants as update-dashboard, saves once, syncs collab, and returns compact proof. " +
181
+ "The main code argument is a string, so it avoids brittle JSON-pointer indexes and native-array serialization issues. " +
182
+ `Common example: ${DASHBOARD_MUTATION_EXAMPLES[0]}`,
183
+ schema: z.object({
184
+ dashboardId: z
185
+ .string()
186
+ .optional()
187
+ .describe("Dashboard id, e.g. 'agent-native-templates-first-party'."),
188
+ id: z
189
+ .string()
190
+ .optional()
191
+ .describe("Legacy alias for dashboardId. Prefer dashboardId."),
192
+ code: z.string().optional().describe(apiHelp),
193
+ operations: operationsInputSchema.describe(
194
+ "Structured equivalent of the typed script. Native callers should pass an array of mutation ops; shell/legacy callers may pass a JSON string. " +
195
+ "Supported ops: movePanels, removePanels, updatePanel, updatePanelPath, insertPanel, duplicatePanel, setDashboard.",
196
+ ),
197
+ dryRun: z
198
+ .boolean()
199
+ .optional()
200
+ .describe(
201
+ "If true, validate and return the resulting compact proof without saving.",
202
+ ),
203
+ returnConfig: z
204
+ .boolean()
205
+ .optional()
206
+ .describe(
207
+ "If true, include the full resulting dashboard config. Defaults to false to keep tool output compact.",
208
+ ),
209
+ returnTypes: z
210
+ .boolean()
211
+ .optional()
212
+ .describe("If true, include the allowed TypeScript API and examples."),
213
+ }),
214
+ http: { method: "POST" },
215
+ mcpApp: {
216
+ compactCatalog: true,
217
+ resource: embedApp({
218
+ title: "Dashboard preview",
219
+ description: "Open the mutated dashboard in the real Analytics UI.",
220
+ iframeTitle: "Agent-Native Analytics",
221
+ openLabel: "Open dashboard",
222
+ height: 680,
223
+ }),
224
+ },
225
+ run: async (args) => {
226
+ const wantsHelpOnly =
227
+ args.returnTypes === true &&
228
+ !args.dashboardId &&
229
+ !args.id &&
230
+ !args.code &&
231
+ !args.operations;
232
+ if (wantsHelpOnly) return helpResult();
233
+
234
+ const dashboardId = resolveDashboardId(args);
235
+ const suppliedModes = [args.code, args.operations].filter(
236
+ (value) => value !== undefined,
237
+ ).length;
238
+ if (suppliedModes === 0) {
239
+ throw new Error("provide `code` or `operations`.");
240
+ }
241
+ if (suppliedModes > 1) {
242
+ throw new Error("provide only one of `code` or `operations`.");
243
+ }
244
+
245
+ const scope = resolveScope();
246
+ const ctx = { email: scope.email, orgId: scope.orgId };
247
+ const existing = await getDashboard(dashboardId, ctx);
248
+ if (!existing) {
249
+ throw new Error(
250
+ `dashboard "${dashboardId}" not found (or you don't have access).`,
251
+ );
252
+ }
253
+ if (existing.kind !== "sql") {
254
+ throw new Error(
255
+ `mutate-dashboard only supports SQL dashboards; "${dashboardId}" is ${existing.kind}.`,
256
+ );
257
+ }
258
+
259
+ const root = cloneConfig(existing.config as Record<string, unknown>);
260
+ const operations = args.operations
261
+ ? (args.operations as DashboardMutationOperation[])
262
+ : parseDashboardMutationScript(root, args.code!);
263
+ const mutation = applyDashboardMutationOperations(root, operations);
264
+
265
+ const validation = validateDashboardConfig(root);
266
+ if (validation) throw new Error(validation);
267
+ if (operations.some(mutationCanChangePanelSql)) {
268
+ const sqlError = await validatePanelSql(root);
269
+ if (sqlError) throw new Error(sqlError);
270
+ }
271
+
272
+ if (args.dryRun !== true) {
273
+ await upsertDashboard(dashboardId, existing.kind, root, ctx);
274
+ await syncToCollab(dashboardId, root);
275
+ }
276
+
277
+ const compact = compactDashboardResult(root, movedPanelIdsFrom(operations));
278
+ const summary =
279
+ `${args.dryRun === true ? "Dry-ran" : "Applied"} ${operations.length} dashboard mutation op(s) for "${dashboardId}". ` +
280
+ `First panels: ${compact.firstPanelIds.join(", ")}.`;
281
+
282
+ return {
283
+ id: dashboardId,
284
+ dashboardId,
285
+ name: typeof root.name === "string" ? root.name : dashboardId,
286
+ mutationApiVersion: 1,
287
+ saved: args.dryRun !== true,
288
+ dryRun: args.dryRun === true,
289
+ appliedOps: operations.length,
290
+ ...compact,
291
+ commandLog: mutation.commandLog,
292
+ changedPanelIds: mutation.changedPanelIds,
293
+ insertedPanelIds: mutation.insertedPanelIds,
294
+ removedPanelIds: mutation.removedPanelIds,
295
+ dashboardFieldsChanged: mutation.dashboardFieldsChanged,
296
+ ...(args.returnConfig === true ? { config: root } : {}),
297
+ ...(args.returnTypes === true
298
+ ? {
299
+ apiTypes: DASHBOARD_MUTATION_API_TYPES,
300
+ examples: DASHBOARD_MUTATION_EXAMPLES,
301
+ }
302
+ : {}),
303
+ summary,
304
+ urlPath: `/dashboards/${dashboardId}`,
305
+ deepLink: buildDeepLink({
306
+ app: "analytics",
307
+ view: "adhoc",
308
+ params: { dashboardId },
309
+ }),
310
+ message:
311
+ `${summary} ` +
312
+ (args.returnConfig === true
313
+ ? ""
314
+ : "Full config omitted; call get-sql-dashboard with includeConfig=true only if full SQL/config is needed."),
315
+ };
316
+ },
317
+ link: ({ result }) => {
318
+ const dashboardId =
319
+ result && typeof result === "object"
320
+ ? (result as { dashboardId?: string }).dashboardId
321
+ : undefined;
322
+ if (!dashboardId) return null;
323
+ return {
324
+ url: buildDeepLink({
325
+ app: "analytics",
326
+ view: "adhoc",
327
+ params: { dashboardId },
328
+ }),
329
+ label: "Open dashboard in Analytics",
330
+ view: "adhoc",
331
+ };
332
+ },
333
+ });