@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,1105 @@
1
+ import { movePanelsById, type PanelOrderTarget } from "./dashboard-panel-order";
2
+
3
+ export const DASHBOARD_MUTATION_API_TYPES = `type DashboardScript = {
4
+ dashboard: {
5
+ set(patch: DashboardPatch): void;
6
+ panel(id: string): PanelSelection;
7
+ section(id: string): SectionSelection;
8
+ panels(ids: string[]): PanelSelection;
9
+ panelsMatching(filter: PanelFilter): PanelSelection;
10
+ insertPanel(panel: PanelInput): InsertedPanel;
11
+ };
12
+ };
13
+
14
+ type DashboardPatch = {
15
+ name?: string;
16
+ description?: string;
17
+ columns?: number;
18
+ filters?: unknown[];
19
+ variables?: Record<string, string>;
20
+ };
21
+
22
+ type PanelPatch = {
23
+ title?: string;
24
+ sql?: string;
25
+ source?: "bigquery" | "ga4" | "amplitude" | "first-party" | "demo" | "prometheus";
26
+ chartType?: "line" | "area" | "bar" | "metric" | "table" | "pie" | "section" | "heatmap" | "callout";
27
+ width?: number;
28
+ columns?: number;
29
+ tab?: string;
30
+ config?: Record<string, unknown>;
31
+ description?: string; // shorthand for config.description
32
+ };
33
+
34
+ type PanelInput = PanelPatch & {
35
+ id: string;
36
+ title: string;
37
+ chartType: NonNullable<PanelPatch["chartType"]>;
38
+ source?: PanelPatch["source"]; // required for non-section panels
39
+ sql?: string; // required for non-section panels
40
+ };
41
+
42
+ type PanelFilter = {
43
+ id?: string;
44
+ ids?: string[];
45
+ idIncludes?: string;
46
+ title?: string;
47
+ titleIncludes?: string;
48
+ chartType?: PanelPatch["chartType"];
49
+ source?: PanelPatch["source"];
50
+ tab?: string;
51
+ isSection?: boolean;
52
+ };
53
+
54
+ type PanelSelection = {
55
+ moveToTop(): void;
56
+ moveToBottom(): void;
57
+ moveBefore(panelId: string): void;
58
+ moveAfter(panelId: string): void;
59
+ moveToIndex(index: number): void;
60
+ remove(): void;
61
+ set(patch: PanelPatch): void;
62
+ setTitle(title: string): void;
63
+ setSql(sql: string): void;
64
+ setWidth(width: number): void;
65
+ setConfig(patch: Record<string, unknown>): void;
66
+ setConfigPath(path: string, value: unknown): void; // path under config, e.g. "yAxis.format" or "config.yAxis.format"
67
+ duplicate(newPanelId: string, patch?: PanelPatch): void;
68
+ };
69
+
70
+ type SectionSelection = PanelSelection & {
71
+ append(panelIds: string[]): void;
72
+ };
73
+
74
+ type InsertedPanel = {
75
+ atTop(): void;
76
+ atBottom(): void;
77
+ before(panelId: string): void;
78
+ after(panelId: string): void;
79
+ atIndex(index: number): void;
80
+ };`;
81
+
82
+ export const DASHBOARD_MUTATION_EXAMPLES = [
83
+ 'dashboard.panels(["dau-over-time","wau-over-time"]).moveToTop();',
84
+ 'dashboard.panel("top-referrers").setTitle("Top Referrers by Domain");',
85
+ 'dashboard.panel("retention").set({"width":2,"config":{"description":"Updated definition."}});',
86
+ 'dashboard.panelsMatching({"titleIncludes":"Signed-In"}).moveToTop();',
87
+ 'dashboard.panelsMatching({"source":"first-party"}).setWidth(2);',
88
+ 'dashboard.panel("retention").setConfigPath("yAxis.format","percent");',
89
+ 'dashboard.section("retention-activity-section").append(["repeat-users","retention-over-time"]);',
90
+ 'dashboard.insertPanel({"id":"new-kpi","title":"New KPI","source":"first-party","chartType":"metric","width":1,"sql":"SELECT COUNT(*) AS value FROM analytics_events"}).atTop();',
91
+ ] as const;
92
+
93
+ export type DashboardMutationOperation =
94
+ | {
95
+ op: "movePanels";
96
+ panelIds: string[];
97
+ position?: "top" | "bottom";
98
+ index?: number;
99
+ beforePanelId?: string;
100
+ afterPanelId?: string;
101
+ }
102
+ | {
103
+ op: "removePanels";
104
+ panelIds: string[];
105
+ }
106
+ | {
107
+ op: "updatePanel";
108
+ panelId: string;
109
+ patch: Record<string, unknown>;
110
+ }
111
+ | {
112
+ op: "updatePanelPath";
113
+ panelId: string;
114
+ path: string;
115
+ value: unknown;
116
+ }
117
+ | {
118
+ op: "insertPanel";
119
+ panel: Record<string, unknown>;
120
+ position?: "top" | "bottom";
121
+ index?: number;
122
+ beforePanelId?: string;
123
+ afterPanelId?: string;
124
+ }
125
+ | {
126
+ op: "duplicatePanel";
127
+ panelId: string;
128
+ newPanelId: string;
129
+ patch?: Record<string, unknown>;
130
+ position?: "top" | "bottom";
131
+ index?: number;
132
+ beforePanelId?: string;
133
+ afterPanelId?: string;
134
+ }
135
+ | {
136
+ op: "setDashboard";
137
+ patch: Record<string, unknown>;
138
+ };
139
+
140
+ export interface DashboardMutationResult {
141
+ operations: DashboardMutationOperation[];
142
+ commandLog: string[];
143
+ changedPanelIds: string[];
144
+ removedPanelIds: string[];
145
+ insertedPanelIds: string[];
146
+ dashboardFieldsChanged: string[];
147
+ }
148
+
149
+ type ParsedCall = {
150
+ name: string;
151
+ args: unknown[];
152
+ };
153
+
154
+ type MutationTarget = {
155
+ position?: "top" | "bottom";
156
+ index?: number;
157
+ beforePanelId?: string;
158
+ afterPanelId?: string;
159
+ };
160
+
161
+ const DASHBOARD_SUBJECTS = [
162
+ "set",
163
+ "panel",
164
+ "section",
165
+ "panels",
166
+ "panelsMatching",
167
+ "insertPanel",
168
+ ];
169
+
170
+ const PANEL_METHODS = [
171
+ "moveToTop",
172
+ "moveToBottom",
173
+ "moveBefore",
174
+ "moveAfter",
175
+ "moveToIndex",
176
+ "remove",
177
+ "set",
178
+ "setTitle",
179
+ "setSql",
180
+ "setWidth",
181
+ "setConfig",
182
+ "setConfigPath",
183
+ "duplicate",
184
+ ];
185
+
186
+ const PLACEMENT_METHODS = [
187
+ "moveToTop",
188
+ "moveToBottom",
189
+ "moveBefore",
190
+ "moveAfter",
191
+ "moveToIndex",
192
+ "atTop",
193
+ "atBottom",
194
+ "before",
195
+ "after",
196
+ "atIndex",
197
+ ];
198
+
199
+ function panelsFromConfig(config: Record<string, unknown>) {
200
+ const panels = config.panels;
201
+ if (!Array.isArray(panels)) {
202
+ throw new Error("config.panels must be an array");
203
+ }
204
+ return panels as Array<Record<string, unknown>>;
205
+ }
206
+
207
+ function panelId(panel: Record<string, unknown>): string {
208
+ return typeof panel.id === "string" ? panel.id : "";
209
+ }
210
+
211
+ function panelTitle(panel: Record<string, unknown>): string {
212
+ return typeof panel.title === "string" ? panel.title : "";
213
+ }
214
+
215
+ function panelIdsFromPanels(panels: Array<Record<string, unknown>>): string[] {
216
+ return panels.map(panelId).filter(Boolean);
217
+ }
218
+
219
+ function compactList(values: string[], max = 20): string {
220
+ const items = values.filter(Boolean).slice(0, max);
221
+ const suffix = values.length > max ? `, ... (${values.length} total)` : "";
222
+ return `${items.join(", ")}${suffix}`;
223
+ }
224
+
225
+ function editDistance(a: string, b: string): number {
226
+ const left = a.toLowerCase();
227
+ const right = b.toLowerCase();
228
+ const dp = Array.from({ length: left.length + 1 }, (_, i) =>
229
+ Array.from({ length: right.length + 1 }, (_2, j) => (i === 0 ? j : 0)),
230
+ );
231
+ for (let i = 1; i <= left.length; i++) dp[i][0] = i;
232
+ for (let i = 1; i <= left.length; i++) {
233
+ for (let j = 1; j <= right.length; j++) {
234
+ const cost = left[i - 1] === right[j - 1] ? 0 : 1;
235
+ dp[i][j] = Math.min(
236
+ dp[i - 1][j] + 1,
237
+ dp[i][j - 1] + 1,
238
+ dp[i - 1][j - 1] + cost,
239
+ );
240
+ }
241
+ }
242
+ return dp[left.length][right.length];
243
+ }
244
+
245
+ function nearest(value: string, candidates: string[], limit = 3): string[] {
246
+ const needle = value.toLowerCase();
247
+ return candidates
248
+ .map((candidate) => ({
249
+ candidate,
250
+ score:
251
+ candidate.toLowerCase().includes(needle) ||
252
+ needle.includes(candidate.toLowerCase())
253
+ ? 0
254
+ : editDistance(value, candidate),
255
+ }))
256
+ .sort((a, b) => a.score - b.score)
257
+ .slice(0, limit)
258
+ .map((item) => item.candidate);
259
+ }
260
+
261
+ function normalizeWords(value: string): string[] {
262
+ return value
263
+ .toLowerCase()
264
+ .split(/[^a-z0-9]+/)
265
+ .map((word) => word.trim())
266
+ .filter(Boolean);
267
+ }
268
+
269
+ function nearestPanelIds(
270
+ value: string,
271
+ panels: Array<Record<string, unknown>>,
272
+ limit = 3,
273
+ ): string[] {
274
+ const words = normalizeWords(value);
275
+ return panels
276
+ .map((panel) => {
277
+ const id = panelId(panel);
278
+ const searchableWords = new Set(
279
+ normalizeWords(`${id} ${panelTitle(panel)}`),
280
+ );
281
+ const matchedWords = words.filter((word) => searchableWords.has(word));
282
+ const wordScore =
283
+ words.length > 0 ? words.length - matchedWords.length : 100;
284
+ const distanceScore = Math.min(
285
+ editDistance(value, id),
286
+ editDistance(value, panelTitle(panel) || id),
287
+ );
288
+ return {
289
+ id,
290
+ score: wordScore === 0 ? 0 : wordScore * 20 + distanceScore,
291
+ };
292
+ })
293
+ .filter((item) => item.id)
294
+ .sort((a, b) => a.score - b.score)
295
+ .slice(0, limit)
296
+ .map((item) => item.id);
297
+ }
298
+
299
+ function methodHelp(method: string, methods: string[]): string {
300
+ const suggestions = nearest(method, methods);
301
+ return ` Did you mean ${suggestions.map((item) => `"${item}"`).join(", ")}? Valid methods: ${methods.join(", ")}.`;
302
+ }
303
+
304
+ function missingPanelHelp(
305
+ panels: Array<Record<string, unknown>>,
306
+ id: string,
307
+ ): string {
308
+ const ids = panelIdsFromPanels(panels);
309
+ const suggestions = nearestPanelIds(id, panels);
310
+ return (
311
+ `panel "${id}" was not found.` +
312
+ (suggestions.length
313
+ ? ` Did you mean ${suggestions.map((item) => `"${item}"`).join(", ")}?`
314
+ : "") +
315
+ ` Available panel ids: ${compactList(ids)}.`
316
+ );
317
+ }
318
+
319
+ function panelCandidateHelp(config: Record<string, unknown>): string {
320
+ const panels = panelsFromConfig(config);
321
+ const candidates = panels
322
+ .slice(0, 20)
323
+ .map((panel) => `${panelId(panel)} (${panelTitle(panel) || "untitled"})`);
324
+ const suffix = panels.length > 20 ? `, ... (${panels.length} total)` : "";
325
+ return `${candidates.join(", ")}${suffix}`;
326
+ }
327
+
328
+ function assertString(value: unknown, label: string): string {
329
+ if (typeof value !== "string" || value.trim().length === 0) {
330
+ throw new Error(`${label} must be a non-empty string`);
331
+ }
332
+ return value;
333
+ }
334
+
335
+ function assertNumber(value: unknown, label: string): number {
336
+ if (typeof value !== "number" || !Number.isFinite(value)) {
337
+ throw new Error(`${label} must be a finite number`);
338
+ }
339
+ return value;
340
+ }
341
+
342
+ function assertObject(value: unknown, label: string): Record<string, unknown> {
343
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
344
+ throw new Error(`${label} must be an object`);
345
+ }
346
+ return value as Record<string, unknown>;
347
+ }
348
+
349
+ function assertStringArray(value: unknown, label: string): string[] {
350
+ if (!Array.isArray(value)) {
351
+ throw new Error(`${label} must be an array of strings`);
352
+ }
353
+ return value.map((item, index) => assertString(item, `${label}[${index}]`));
354
+ }
355
+
356
+ function uniqueStrings(values: string[]): string[] {
357
+ return Array.from(new Set(values));
358
+ }
359
+
360
+ function targetFromOperation(
361
+ op: MutationTarget & { op: string },
362
+ ): PanelOrderTarget {
363
+ const targets = [
364
+ op.index !== undefined,
365
+ !!op.beforePanelId,
366
+ !!op.afterPanelId,
367
+ ].filter(Boolean).length;
368
+ if (targets > 1) {
369
+ throw new Error(
370
+ `${op.op} accepts only one of index, beforePanelId, or afterPanelId`,
371
+ );
372
+ }
373
+ if (op.beforePanelId) return { beforePanelId: op.beforePanelId };
374
+ if (op.afterPanelId) return { afterPanelId: op.afterPanelId };
375
+ if (op.index !== undefined) return { index: op.index };
376
+ return { position: op.position ?? "bottom" };
377
+ }
378
+
379
+ function findPanelIndex(
380
+ panels: Array<Record<string, unknown>>,
381
+ id: string,
382
+ ): number {
383
+ return panels.findIndex((panel) => panelId(panel) === id);
384
+ }
385
+
386
+ function requirePanel(
387
+ panels: Array<Record<string, unknown>>,
388
+ id: string,
389
+ ): Record<string, unknown> {
390
+ const index = findPanelIndex(panels, id);
391
+ if (index < 0) throw new Error(missingPanelHelp(panels, id));
392
+ return panels[index];
393
+ }
394
+
395
+ function requirePanelIds(config: Record<string, unknown>, ids: string[]): void {
396
+ const panels = panelsFromConfig(config);
397
+ for (const id of ids) requirePanel(panels, id);
398
+ }
399
+
400
+ function enhancePanelError(
401
+ config: Record<string, unknown>,
402
+ err: unknown,
403
+ ): never {
404
+ const message = err instanceof Error ? err.message : String(err);
405
+ const panels = panelsFromConfig(config);
406
+ const missing = message.match(/panel id\(s\) not found: (.+)$/);
407
+ if (missing) {
408
+ const ids = missing[1].split(",").map((id) => id.trim());
409
+ throw new Error(ids.map((id) => missingPanelHelp(panels, id)).join(" "));
410
+ }
411
+ const target = message.match(/target panel "([^"]+)" was not found/);
412
+ if (target) {
413
+ throw new Error(`target ${missingPanelHelp(panels, target[1])}`);
414
+ }
415
+ throw err;
416
+ }
417
+
418
+ function insertPanel(
419
+ config: Record<string, unknown>,
420
+ panel: Record<string, unknown>,
421
+ target: PanelOrderTarget,
422
+ ): number {
423
+ const panels = panelsFromConfig(config);
424
+ const id = assertString(panel.id, "panel.id");
425
+ if (findPanelIndex(panels, id) >= 0) {
426
+ throw new Error(`panel "${id}" already exists`);
427
+ }
428
+ const placeholderId = `__agent_native_insert_${id}`;
429
+ config.panels = [...panels, { ...panel, id: placeholderId }];
430
+ const result = movePanelsById(config, [placeholderId], target);
431
+ const nextPanels = panelsFromConfig(config);
432
+ const inserted = requirePanel(nextPanels, placeholderId);
433
+ inserted.id = id;
434
+ return result.insertIndex;
435
+ }
436
+
437
+ function patchPanel(
438
+ panel: Record<string, unknown>,
439
+ patch: Record<string, unknown>,
440
+ ): string[] {
441
+ const changed: string[] = [];
442
+ for (const [key, value] of Object.entries(patch)) {
443
+ if (key === "id") {
444
+ throw new Error(
445
+ "panel.id cannot be changed with set(...). Duplicate the panel with a new id or remove and insert a new panel instead.",
446
+ );
447
+ }
448
+ if (key === "description") {
449
+ const config =
450
+ panel.config &&
451
+ typeof panel.config === "object" &&
452
+ !Array.isArray(panel.config)
453
+ ? { ...(panel.config as Record<string, unknown>) }
454
+ : {};
455
+ config.description = value;
456
+ panel.config = config;
457
+ changed.push("config.description");
458
+ continue;
459
+ }
460
+ if (key === "config") {
461
+ const config =
462
+ panel.config &&
463
+ typeof panel.config === "object" &&
464
+ !Array.isArray(panel.config)
465
+ ? { ...(panel.config as Record<string, unknown>) }
466
+ : {};
467
+ Object.assign(config, assertObject(value, "patch.config"));
468
+ panel.config = config;
469
+ changed.push("config");
470
+ continue;
471
+ }
472
+ panel[key] = value;
473
+ changed.push(key);
474
+ }
475
+ return changed;
476
+ }
477
+
478
+ function setConfigPath(
479
+ panel: Record<string, unknown>,
480
+ rawPath: string,
481
+ value: unknown,
482
+ ): string {
483
+ const path = assertString(rawPath, "config path");
484
+ const segments = path
485
+ .split(".")
486
+ .map((segment) => segment.trim())
487
+ .filter(Boolean);
488
+ if (segments[0] === "config") segments.shift();
489
+ if (segments.length === 0) {
490
+ throw new Error(
491
+ 'setConfigPath path must point under panel.config, e.g. "yAxis.format".',
492
+ );
493
+ }
494
+ for (const segment of segments) {
495
+ if (["__proto__", "prototype", "constructor"].includes(segment)) {
496
+ throw new Error(`setConfigPath segment "${segment}" is not allowed`);
497
+ }
498
+ }
499
+ const config =
500
+ panel.config &&
501
+ typeof panel.config === "object" &&
502
+ !Array.isArray(panel.config)
503
+ ? { ...(panel.config as Record<string, unknown>) }
504
+ : {};
505
+ let cursor: Record<string, unknown> = config;
506
+ for (const segment of segments.slice(0, -1)) {
507
+ const existing = cursor[segment];
508
+ if (!existing || typeof existing !== "object" || Array.isArray(existing)) {
509
+ cursor[segment] = {};
510
+ }
511
+ cursor = cursor[segment] as Record<string, unknown>;
512
+ }
513
+ cursor[segments[segments.length - 1]] = value;
514
+ panel.config = config;
515
+ return `config.${segments.join(".")}`;
516
+ }
517
+
518
+ function duplicatePanel(
519
+ config: Record<string, unknown>,
520
+ panelIdToCopy: string,
521
+ newPanelId: string,
522
+ patch: Record<string, unknown>,
523
+ target: PanelOrderTarget,
524
+ ): number {
525
+ const panels = panelsFromConfig(config);
526
+ const source = requirePanel(panels, panelIdToCopy);
527
+ const duplicate = JSON.parse(JSON.stringify(source)) as Record<
528
+ string,
529
+ unknown
530
+ >;
531
+ duplicate.id = newPanelId;
532
+ patchPanel(duplicate, patch);
533
+ return insertPanel(config, duplicate, target);
534
+ }
535
+
536
+ export function applyDashboardMutationOperations(
537
+ config: Record<string, unknown>,
538
+ operations: DashboardMutationOperation[],
539
+ ): DashboardMutationResult {
540
+ if (operations.length === 0) {
541
+ throw new Error("at least one dashboard mutation operation is required");
542
+ }
543
+
544
+ const commandLog: string[] = [];
545
+ const changedPanelIds = new Set<string>();
546
+ const removedPanelIds = new Set<string>();
547
+ const insertedPanelIds = new Set<string>();
548
+ const dashboardFieldsChanged = new Set<string>();
549
+
550
+ for (let opIndex = 0; opIndex < operations.length; opIndex++) {
551
+ const op = operations[opIndex];
552
+ try {
553
+ switch (op.op) {
554
+ case "movePanels": {
555
+ let result;
556
+ try {
557
+ result = movePanelsById(
558
+ config,
559
+ op.panelIds,
560
+ targetFromOperation(op),
561
+ );
562
+ } catch (err) {
563
+ enhancePanelError(config, err);
564
+ }
565
+ for (const id of result.movedPanelIds) changedPanelIds.add(id);
566
+ commandLog.push(
567
+ `movePanels(${result.movedPanelIds.join(", ")}) -> index ${result.insertIndex}`,
568
+ );
569
+ break;
570
+ }
571
+ case "removePanels": {
572
+ const ids = uniqueStrings(op.panelIds);
573
+ const panels = panelsFromConfig(config);
574
+ for (const id of ids) requirePanel(panels, id);
575
+ config.panels = panels.filter(
576
+ (panel) => !ids.includes(panelId(panel)),
577
+ );
578
+ for (const id of ids) {
579
+ changedPanelIds.add(id);
580
+ removedPanelIds.add(id);
581
+ }
582
+ commandLog.push(`removePanels(${ids.join(", ")})`);
583
+ break;
584
+ }
585
+ case "updatePanel": {
586
+ const panel = requirePanel(panelsFromConfig(config), op.panelId);
587
+ const changedFields = patchPanel(panel, op.patch);
588
+ changedPanelIds.add(op.panelId);
589
+ commandLog.push(
590
+ `updatePanel(${op.panelId}: ${changedFields.join(", ") || "no fields"})`,
591
+ );
592
+ break;
593
+ }
594
+ case "updatePanelPath": {
595
+ const panel = requirePanel(panelsFromConfig(config), op.panelId);
596
+ const changedField = setConfigPath(panel, op.path, op.value);
597
+ changedPanelIds.add(op.panelId);
598
+ commandLog.push(`updatePanelPath(${op.panelId}: ${changedField})`);
599
+ break;
600
+ }
601
+ case "insertPanel": {
602
+ const index = insertPanel(config, op.panel, targetFromOperation(op));
603
+ const id = assertString(op.panel.id, "panel.id");
604
+ changedPanelIds.add(id);
605
+ insertedPanelIds.add(id);
606
+ commandLog.push(`insertPanel(${id}) -> index ${index}`);
607
+ break;
608
+ }
609
+ case "duplicatePanel": {
610
+ const index = duplicatePanel(
611
+ config,
612
+ op.panelId,
613
+ op.newPanelId,
614
+ op.patch ?? {},
615
+ targetFromOperation(op),
616
+ );
617
+ changedPanelIds.add(op.newPanelId);
618
+ insertedPanelIds.add(op.newPanelId);
619
+ commandLog.push(
620
+ `duplicatePanel(${op.panelId} -> ${op.newPanelId}) -> index ${index}`,
621
+ );
622
+ break;
623
+ }
624
+ case "setDashboard": {
625
+ for (const [key, value] of Object.entries(op.patch)) {
626
+ config[key] = value;
627
+ dashboardFieldsChanged.add(key);
628
+ }
629
+ commandLog.push(
630
+ `setDashboard(${Object.keys(op.patch).join(", ") || "no fields"})`,
631
+ );
632
+ break;
633
+ }
634
+ default:
635
+ throw new Error(
636
+ `unsupported dashboard mutation op ${(op as any).op}`,
637
+ );
638
+ }
639
+ } catch (err: any) {
640
+ throw new Error(`operation ${opIndex + 1} (${op.op}): ${err.message}`);
641
+ }
642
+ }
643
+
644
+ return {
645
+ operations,
646
+ commandLog,
647
+ changedPanelIds: Array.from(changedPanelIds),
648
+ removedPanelIds: Array.from(removedPanelIds),
649
+ insertedPanelIds: Array.from(insertedPanelIds),
650
+ dashboardFieldsChanged: Array.from(dashboardFieldsChanged),
651
+ };
652
+ }
653
+
654
+ function stripLineComments(code: string): string {
655
+ const lines = code.split(/\r?\n/);
656
+ return lines
657
+ .map((line) => {
658
+ let quote: string | null = null;
659
+ for (let i = 0; i < line.length; i++) {
660
+ const ch = line[i];
661
+ const next = line[i + 1];
662
+ if (quote) {
663
+ if (ch === "\\") {
664
+ i++;
665
+ } else if (ch === quote) {
666
+ quote = null;
667
+ }
668
+ continue;
669
+ }
670
+ if (ch === '"' || ch === "'") {
671
+ quote = ch;
672
+ continue;
673
+ }
674
+ if (ch === "/" && next === "/") return line.slice(0, i);
675
+ }
676
+ return line;
677
+ })
678
+ .join("\n");
679
+ }
680
+
681
+ function hasTemplateLiteralSyntax(code: string): boolean {
682
+ let quote: string | null = null;
683
+ for (let i = 0; i < code.length; i++) {
684
+ const ch = code[i];
685
+ if (quote) {
686
+ if (ch === "\\") {
687
+ i++;
688
+ } else if (ch === quote) {
689
+ quote = null;
690
+ }
691
+ continue;
692
+ }
693
+ if (ch === '"' || ch === "'") {
694
+ quote = ch;
695
+ continue;
696
+ }
697
+ if (ch === "`") return true;
698
+ }
699
+ return false;
700
+ }
701
+
702
+ function splitTopLevel(input: string, delimiter: string): string[] {
703
+ const parts: string[] = [];
704
+ let start = 0;
705
+ let quote: string | null = null;
706
+ let depth = 0;
707
+
708
+ for (let i = 0; i < input.length; i++) {
709
+ const ch = input[i];
710
+ if (quote) {
711
+ if (ch === "\\") {
712
+ i++;
713
+ } else if (ch === quote) {
714
+ quote = null;
715
+ }
716
+ continue;
717
+ }
718
+ if (ch === '"' || ch === "'") {
719
+ quote = ch;
720
+ continue;
721
+ }
722
+ if (ch === "(" || ch === "[" || ch === "{") {
723
+ depth++;
724
+ continue;
725
+ }
726
+ if (ch === ")" || ch === "]" || ch === "}") {
727
+ depth--;
728
+ if (depth < 0) throw new Error("unbalanced mutation script syntax");
729
+ continue;
730
+ }
731
+ if (depth === 0 && ch === delimiter) {
732
+ parts.push(input.slice(start, i).trim());
733
+ start = i + 1;
734
+ }
735
+ }
736
+ if (quote) throw new Error("unterminated string in mutation script");
737
+ if (depth !== 0) throw new Error("unbalanced mutation script syntax");
738
+ const tail = input.slice(start).trim();
739
+ if (tail) parts.push(tail);
740
+ return parts.filter(Boolean);
741
+ }
742
+
743
+ function parseArg(raw: string): unknown {
744
+ const value = raw.trim();
745
+ if (!value) return undefined;
746
+ if (value.startsWith("'") && value.endsWith("'")) {
747
+ return value.slice(1, -1).replace(/\\'/g, "'");
748
+ }
749
+ try {
750
+ return JSON.parse(value);
751
+ } catch (err: any) {
752
+ throw new Error(
753
+ `arguments must be JSON-compatible literals or quoted strings (got ${value}): ${err.message}`,
754
+ );
755
+ }
756
+ }
757
+
758
+ function parseArgs(raw: string): unknown[] {
759
+ const trimmed = raw.trim();
760
+ if (!trimmed) return [];
761
+ return splitTopLevel(trimmed, ",").map(parseArg);
762
+ }
763
+
764
+ function findMatchingParen(input: string, openIndex: number): number {
765
+ let quote: string | null = null;
766
+ let depth = 0;
767
+ for (let i = openIndex; i < input.length; i++) {
768
+ const ch = input[i];
769
+ if (quote) {
770
+ if (ch === "\\") {
771
+ i++;
772
+ } else if (ch === quote) {
773
+ quote = null;
774
+ }
775
+ continue;
776
+ }
777
+ if (ch === '"' || ch === "'") {
778
+ quote = ch;
779
+ continue;
780
+ }
781
+ if (ch === "(") depth++;
782
+ else if (ch === ")") {
783
+ depth--;
784
+ if (depth === 0) return i;
785
+ }
786
+ }
787
+ throw new Error("unbalanced method call in mutation script");
788
+ }
789
+
790
+ function parseCallChain(statement: string): ParsedCall[] {
791
+ let cursor = statement.trim();
792
+ if (!cursor.startsWith("dashboard.")) {
793
+ throw new Error(
794
+ `mutation statements must start with dashboard.: ${statement}`,
795
+ );
796
+ }
797
+ cursor = cursor.slice("dashboard.".length);
798
+ const calls: ParsedCall[] = [];
799
+
800
+ while (cursor.length > 0) {
801
+ const method = cursor.match(/^([A-Za-z_$][A-Za-z0-9_$]*)\s*\(/);
802
+ if (!method) {
803
+ throw new Error(`expected a dashboard API method call near: ${cursor}`);
804
+ }
805
+ const name = method[1];
806
+ const openIndex = method[0].lastIndexOf("(");
807
+ const closeIndex = findMatchingParen(cursor, openIndex);
808
+ const rawArgs = cursor.slice(openIndex + 1, closeIndex);
809
+ calls.push({ name, args: parseArgs(rawArgs) });
810
+ cursor = cursor.slice(closeIndex + 1).trim();
811
+ if (cursor.startsWith(".")) {
812
+ cursor = cursor.slice(1).trim();
813
+ continue;
814
+ }
815
+ if (cursor.length > 0) {
816
+ throw new Error(`unexpected content after method call: ${cursor}`);
817
+ }
818
+ }
819
+
820
+ return calls;
821
+ }
822
+
823
+ function panelIdsMatching(
824
+ config: Record<string, unknown>,
825
+ filter: Record<string, unknown>,
826
+ ): string[] {
827
+ const panels = panelsFromConfig(config);
828
+ return panels
829
+ .filter((panel) => {
830
+ const id = panelId(panel);
831
+ const title = typeof panel.title === "string" ? panel.title : "";
832
+ if (filter.id !== undefined && id !== filter.id) return false;
833
+ if (Array.isArray(filter.ids) && !filter.ids.includes(id)) return false;
834
+ if (
835
+ typeof filter.idIncludes === "string" &&
836
+ !id.toLowerCase().includes(filter.idIncludes.toLowerCase())
837
+ ) {
838
+ return false;
839
+ }
840
+ if (filter.title !== undefined && title !== filter.title) return false;
841
+ if (
842
+ typeof filter.titleIncludes === "string" &&
843
+ !title.toLowerCase().includes(filter.titleIncludes.toLowerCase())
844
+ ) {
845
+ return false;
846
+ }
847
+ if (
848
+ filter.chartType !== undefined &&
849
+ panel.chartType !== filter.chartType
850
+ ) {
851
+ return false;
852
+ }
853
+ if (filter.source !== undefined && panel.source !== filter.source) {
854
+ return false;
855
+ }
856
+ if (filter.tab !== undefined && panel.tab !== filter.tab) return false;
857
+ if (
858
+ filter.isSection !== undefined &&
859
+ (panel.chartType === "section") !== filter.isSection
860
+ ) {
861
+ return false;
862
+ }
863
+ return true;
864
+ })
865
+ .map(panelId)
866
+ .filter(Boolean);
867
+ }
868
+
869
+ function targetFromChainCall(call: ParsedCall): MutationTarget {
870
+ switch (call.name) {
871
+ case "moveToTop":
872
+ case "atTop":
873
+ return { position: "top" };
874
+ case "moveToBottom":
875
+ case "atBottom":
876
+ return { position: "bottom" };
877
+ case "moveBefore":
878
+ case "before":
879
+ return {
880
+ beforePanelId: assertString(call.args[0], `${call.name} panelId`),
881
+ };
882
+ case "moveAfter":
883
+ case "after":
884
+ return {
885
+ afterPanelId: assertString(call.args[0], `${call.name} panelId`),
886
+ };
887
+ case "moveToIndex":
888
+ case "atIndex":
889
+ return { index: assertNumber(call.args[0], `${call.name} index`) };
890
+ default:
891
+ throw new Error(
892
+ `unsupported placement method "${call.name}".${methodHelp(call.name, PLACEMENT_METHODS)}`,
893
+ );
894
+ }
895
+ }
896
+
897
+ function operationFromPanelCommand(
898
+ panelIds: string[],
899
+ command: ParsedCall,
900
+ ): DashboardMutationOperation[] {
901
+ switch (command.name) {
902
+ case "moveToTop":
903
+ case "moveToBottom":
904
+ case "moveBefore":
905
+ case "moveAfter":
906
+ case "moveToIndex":
907
+ return [
908
+ {
909
+ op: "movePanels",
910
+ panelIds,
911
+ ...targetFromChainCall(command),
912
+ } as DashboardMutationOperation,
913
+ ];
914
+ case "remove":
915
+ return [{ op: "removePanels", panelIds }];
916
+ case "set": {
917
+ const patch = assertObject(command.args[0], "set patch");
918
+ return panelIds.map((panelId) => ({
919
+ op: "updatePanel",
920
+ panelId,
921
+ patch,
922
+ }));
923
+ }
924
+ case "setTitle": {
925
+ const title = assertString(command.args[0], "title");
926
+ return panelIds.map((panelId) => ({
927
+ op: "updatePanel",
928
+ panelId,
929
+ patch: { title },
930
+ }));
931
+ }
932
+ case "setSql": {
933
+ const sql = assertString(command.args[0], "sql");
934
+ return panelIds.map((panelId) => ({
935
+ op: "updatePanel",
936
+ panelId,
937
+ patch: { sql },
938
+ }));
939
+ }
940
+ case "setWidth": {
941
+ const width = assertNumber(command.args[0], "width");
942
+ return panelIds.map((panelId) => ({
943
+ op: "updatePanel",
944
+ panelId,
945
+ patch: { width },
946
+ }));
947
+ }
948
+ case "setConfig": {
949
+ const configPatch = assertObject(command.args[0], "config patch");
950
+ return panelIds.map((panelId) => ({
951
+ op: "updatePanel",
952
+ panelId,
953
+ patch: { config: configPatch },
954
+ }));
955
+ }
956
+ case "setConfigPath": {
957
+ if (command.args.length < 2) {
958
+ throw new Error("setConfigPath requires path and value arguments");
959
+ }
960
+ const path = assertString(command.args[0], "config path");
961
+ return panelIds.map((panelId) => ({
962
+ op: "updatePanelPath",
963
+ panelId,
964
+ path,
965
+ value: command.args[1],
966
+ }));
967
+ }
968
+ case "duplicate":
969
+ if (panelIds.length !== 1) {
970
+ throw new Error("duplicate requires exactly one selected panel");
971
+ }
972
+ return [
973
+ {
974
+ op: "duplicatePanel",
975
+ panelId: panelIds[0],
976
+ newPanelId: assertString(command.args[0], "newPanelId"),
977
+ patch:
978
+ command.args[1] === undefined
979
+ ? undefined
980
+ : assertObject(command.args[1], "duplicate patch"),
981
+ },
982
+ ];
983
+ default:
984
+ throw new Error(
985
+ `unsupported panel method "${command.name}".${methodHelp(command.name, PANEL_METHODS)}`,
986
+ );
987
+ }
988
+ }
989
+
990
+ function operationsFromStatement(
991
+ config: Record<string, unknown>,
992
+ statement: string,
993
+ ): DashboardMutationOperation[] {
994
+ const calls = parseCallChain(statement);
995
+ if (calls.length === 0) return [];
996
+ const [subject, ...commands] = calls;
997
+
998
+ if (subject.name === "set") {
999
+ if (commands.length > 0) {
1000
+ throw new Error("dashboard.set(...) cannot be chained");
1001
+ }
1002
+ return [
1003
+ {
1004
+ op: "setDashboard",
1005
+ patch: assertObject(subject.args[0], "dashboard patch"),
1006
+ },
1007
+ ];
1008
+ }
1009
+
1010
+ if (commands.length === 0) {
1011
+ throw new Error(`mutation statement has no command: ${statement}`);
1012
+ }
1013
+
1014
+ if (subject.name === "insertPanel") {
1015
+ if (commands.length > 1) {
1016
+ throw new Error(
1017
+ "dashboard.insertPanel(...) accepts one placement method",
1018
+ );
1019
+ }
1020
+ const [placement] = commands;
1021
+ return [
1022
+ {
1023
+ op: "insertPanel",
1024
+ panel: assertObject(subject.args[0], "panel"),
1025
+ ...(placement
1026
+ ? targetFromChainCall(placement)
1027
+ : { position: "bottom" }),
1028
+ } as DashboardMutationOperation,
1029
+ ];
1030
+ }
1031
+
1032
+ if (subject.name === "section") {
1033
+ const sectionId = assertString(subject.args[0], "section id");
1034
+ requirePanelIds(config, [sectionId]);
1035
+ const command = commands[0];
1036
+ if (command.name === "append") {
1037
+ if (commands.length > 1) {
1038
+ throw new Error("dashboard.section(...).append(...) cannot be chained");
1039
+ }
1040
+ const appendIds = assertStringArray(command.args[0], "append panelIds");
1041
+ requirePanelIds(config, appendIds);
1042
+ return [
1043
+ {
1044
+ op: "movePanels",
1045
+ panelIds: appendIds,
1046
+ afterPanelId: sectionId,
1047
+ },
1048
+ ];
1049
+ }
1050
+ return operationFromPanelCommand([sectionId], command);
1051
+ }
1052
+
1053
+ let panelIds: string[];
1054
+ if (subject.name === "panel") {
1055
+ panelIds = [assertString(subject.args[0], "panel id")];
1056
+ } else if (subject.name === "panels") {
1057
+ panelIds = assertStringArray(subject.args[0], "panel ids");
1058
+ } else if (subject.name === "panelsMatching") {
1059
+ panelIds = panelIdsMatching(
1060
+ config,
1061
+ assertObject(subject.args[0], "panel filter"),
1062
+ );
1063
+ if (panelIds.length === 0) {
1064
+ throw new Error(
1065
+ `panelsMatching filter ${JSON.stringify(subject.args[0])} did not match any panels. Candidate panels: ${panelCandidateHelp(config)}.`,
1066
+ );
1067
+ }
1068
+ } else {
1069
+ throw new Error(
1070
+ `unsupported dashboard subject "${subject.name}".${methodHelp(subject.name, DASHBOARD_SUBJECTS)}`,
1071
+ );
1072
+ }
1073
+
1074
+ requirePanelIds(config, panelIds);
1075
+
1076
+ return commands.flatMap((command) =>
1077
+ operationFromPanelCommand(panelIds, command),
1078
+ );
1079
+ }
1080
+
1081
+ export function parseDashboardMutationScript(
1082
+ config: Record<string, unknown>,
1083
+ code: string,
1084
+ ): DashboardMutationOperation[] {
1085
+ if (code.length > 12_000) {
1086
+ throw new Error("mutation script is too large; keep it under 12000 chars");
1087
+ }
1088
+ if (hasTemplateLiteralSyntax(code)) {
1089
+ throw new Error("mutation script does not support template literals");
1090
+ }
1091
+ const stripped = stripLineComments(code).trim();
1092
+ if (!stripped) {
1093
+ throw new Error("mutation script is empty");
1094
+ }
1095
+ const statements = splitTopLevel(stripped, ";");
1096
+ return statements.flatMap((statement, index) => {
1097
+ try {
1098
+ return operationsFromStatement(config, statement);
1099
+ } catch (err: any) {
1100
+ throw new Error(
1101
+ `statement ${index + 1} (${JSON.stringify(statement)}): ${err.message}`,
1102
+ );
1103
+ }
1104
+ });
1105
+ }