@agent-native/core 0.80.0 → 0.80.1

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 (220) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +8 -0
  3. package/corpus/core/docs/content/locales/ar-SA/skills-guide.mdx +5 -0
  4. package/corpus/core/docs/content/locales/ar-SA/template-design.mdx +14 -1
  5. package/corpus/core/docs/content/locales/de-DE/skills-guide.mdx +5 -0
  6. package/corpus/core/docs/content/locales/de-DE/template-design.mdx +15 -1
  7. package/corpus/core/docs/content/locales/es-ES/skills-guide.mdx +5 -0
  8. package/corpus/core/docs/content/locales/es-ES/template-design.mdx +15 -1
  9. package/corpus/core/docs/content/locales/fr-FR/skills-guide.mdx +5 -0
  10. package/corpus/core/docs/content/locales/fr-FR/template-design.mdx +15 -1
  11. package/corpus/core/docs/content/locales/hi-IN/skills-guide.mdx +5 -0
  12. package/corpus/core/docs/content/locales/hi-IN/template-design.mdx +15 -1
  13. package/corpus/core/docs/content/locales/ja-JP/skills-guide.mdx +5 -0
  14. package/corpus/core/docs/content/locales/ja-JP/template-design.mdx +16 -1
  15. package/corpus/core/docs/content/locales/ko-KR/skills-guide.mdx +5 -0
  16. package/corpus/core/docs/content/locales/ko-KR/template-design.mdx +14 -1
  17. package/corpus/core/docs/content/locales/pt-BR/skills-guide.mdx +5 -0
  18. package/corpus/core/docs/content/locales/pt-BR/template-design.mdx +14 -1
  19. package/corpus/core/docs/content/locales/zh-CN/skills-guide.mdx +5 -0
  20. package/corpus/core/docs/content/locales/zh-CN/template-design.mdx +14 -1
  21. package/corpus/core/docs/content/locales/zh-TW/skills-guide.mdx +5 -0
  22. package/corpus/core/docs/content/locales/zh-TW/template-design.mdx +14 -1
  23. package/corpus/core/docs/content/skills-guide.mdx +5 -0
  24. package/corpus/core/docs/content/template-design.mdx +14 -1
  25. package/corpus/core/package.json +1 -1
  26. package/corpus/core/src/application-state/handlers.ts +1 -1
  27. package/corpus/core/src/brand-kit/index.ts +1 -1
  28. package/corpus/core/src/cli/skills.ts +18 -0
  29. package/corpus/core/src/client/AgentPanel.tsx +1 -1
  30. package/corpus/core/src/client/MultiTabAssistantChat.tsx +1 -1
  31. package/corpus/core/src/client/extensions/portable-extension.ts +1 -1
  32. package/corpus/core/src/code-agents/transcript-normalizer.ts +1 -1
  33. package/corpus/core/src/extensions/path.ts +1 -4
  34. package/corpus/core/src/integrations/a2a-continuation-processor.ts +1 -1
  35. package/corpus/core/src/mcp/build-server.ts +1 -1
  36. package/corpus/core/src/org/handlers.ts +5 -5
  37. package/corpus/core/src/org/plugin.ts +2 -2
  38. package/corpus/core/src/scripts/call-agent.ts +1 -1
  39. package/corpus/core/src/scripts/db/safety.ts +1 -1
  40. package/corpus/core/src/server/auth.ts +3 -0
  41. package/corpus/core/src/server/builder-design-systems.ts +308 -0
  42. package/corpus/core/src/server/core-routes-plugin.ts +29 -3
  43. package/corpus/core/src/server/index.ts +8 -0
  44. package/corpus/core/src/styles/agent-native.css +14 -7
  45. package/corpus/core/src/usage/store.ts +2 -2
  46. package/corpus/templates/analytics/actions/dashboard-mutation-api.ts +298 -15
  47. package/corpus/templates/analytics/actions/get-sql-dashboard.ts +9 -4
  48. package/corpus/templates/analytics/actions/mutate-dashboard.ts +18 -2
  49. package/corpus/templates/analytics/actions/query-agent-native-analytics.ts +12 -2
  50. package/corpus/templates/analytics/app/hooks/use-replay-storage-status.ts +66 -0
  51. package/corpus/templates/analytics/app/i18n/zh-TW.ts +37 -0
  52. package/corpus/templates/analytics/app/i18n-data.ts +404 -0
  53. package/corpus/templates/analytics/app/pages/Settings.tsx +333 -0
  54. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/PanelEditorDialog.tsx +2 -1
  55. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/dashboard-layout.ts +121 -3
  56. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx +49 -26
  57. package/corpus/templates/analytics/app/pages/sessions/SessionsPage.tsx +137 -36
  58. package/corpus/templates/analytics/changelog/2026-06-29-analytics-agents-can-now-place-new-charts-into-requested-das.md +6 -0
  59. package/corpus/templates/analytics/changelog/2026-06-29-analytics-queries-now-report-failed-source-query-errors-inst.md +6 -0
  60. package/corpus/templates/analytics/changelog/2026-06-29-charts-can-now-be-dropped-between-full-dashboard-rows-to-cre.md +6 -0
  61. package/corpus/templates/analytics/changelog/2026-06-29-dashboard-email-reports-include-the-full-dashboard-image-wit.md +6 -0
  62. package/corpus/templates/analytics/changelog/2026-06-29-main-surface-border-reaches-the-top.md +6 -0
  63. package/corpus/templates/analytics/changelog/2026-06-29-session-replay-now-records-playable-sessions-in-production-a.md +6 -0
  64. package/corpus/templates/analytics/changelog/2026-06-29-set-up-session-replay-storage-from-settings-connect-builder-.md +6 -0
  65. package/corpus/templates/analytics/scripts/cleanup-empty-session-recordings.ts +102 -0
  66. package/corpus/templates/analytics/server/lib/dashboard-report.ts +49 -26
  67. package/corpus/templates/analytics/server/lib/real-data-actions.ts +81 -20
  68. package/corpus/templates/analytics/server/lib/replay-storage.ts +79 -0
  69. package/corpus/templates/analytics/server/lib/s3-upload-provider.ts +255 -0
  70. package/corpus/templates/analytics/server/lib/session-replay.ts +29 -8
  71. package/corpus/templates/analytics/server/plugins/agent-chat.ts +11 -2
  72. package/corpus/templates/analytics/server/plugins/onboarding.ts +97 -0
  73. package/corpus/templates/clips/desktop/src/lib/recorder.ts +24 -0
  74. package/corpus/templates/clips/desktop/src/lib/transcription-capture.ts +105 -5
  75. package/corpus/templates/content/actions/set-image-alt-text.ts +1 -1
  76. package/corpus/templates/content/shared/document-export.ts +1 -1
  77. package/corpus/templates/design/.agents/skills/design-systems/SKILL.md +8 -18
  78. package/corpus/templates/design/.agents/skills/visual-edit/SKILL.md +17 -0
  79. package/corpus/templates/design/actions/add-localhost-screens.ts +32 -5
  80. package/corpus/templates/design/actions/index-design-system-with-builder.ts +259 -0
  81. package/corpus/templates/design/actions/update-file.ts +9 -2
  82. package/corpus/templates/design/app/components/design/CanvasContextMenu.tsx +33 -75
  83. package/corpus/templates/design/app/components/design/DesignCanvas.tsx +72 -9
  84. package/corpus/templates/design/app/components/design/DesignExtensionsPanel.tsx +41 -39
  85. package/corpus/templates/design/app/components/design/EditPanel.tsx +1029 -249
  86. package/corpus/templates/design/app/components/design/LayersPanel.tsx +450 -101
  87. package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +978 -154
  88. package/corpus/templates/design/app/components/design/TweaksPanel.tsx +7 -9
  89. package/corpus/templates/design/app/components/design/inspector/AlignmentMatrix.tsx +133 -66
  90. package/corpus/templates/design/app/components/design/inspector/AutoLayoutMatrix.tsx +1234 -344
  91. package/corpus/templates/design/app/components/design/inspector/ConstraintsWidget.tsx +144 -94
  92. package/corpus/templates/design/app/components/design/inspector/ExportSettingsPanel.tsx +301 -72
  93. package/corpus/templates/design/app/components/design/inspector/FigmaColorPicker.tsx +876 -286
  94. package/corpus/templates/design/app/components/design/inspector/GradientEditor.tsx +601 -0
  95. package/corpus/templates/design/app/components/design/inspector/ImageFillControls.tsx +242 -0
  96. package/corpus/templates/design/app/components/design/inspector/ScrubInput.tsx +30 -7
  97. package/corpus/templates/design/app/components/design/inspector/ShaderControls.tsx +212 -155
  98. package/corpus/templates/design/app/components/design/inspector/ShaderFillsPanel.tsx +423 -0
  99. package/corpus/templates/design/app/components/design/inspector/figma-icons.tsx +487 -107
  100. package/corpus/templates/design/app/components/design/inspector/index.ts +27 -0
  101. package/corpus/templates/design/app/global.css +29 -3
  102. package/corpus/templates/design/app/i18n/zh-TW.ts +13 -0
  103. package/corpus/templates/design/app/i18n-data.ts +239 -1
  104. package/corpus/templates/design/app/pages/DesignEditor.tsx +1023 -353
  105. package/corpus/templates/design/app/pages/DesignSystemSetup.tsx +57 -221
  106. package/corpus/templates/design/changelog/2026-06-29-canvas-selection-no-longer-flashes-broken-prototype-html-whe.md +6 -0
  107. package/corpus/templates/design/changelog/2026-06-29-command-menu-selections-are-clearer-in-dark-mode.md +6 -0
  108. package/corpus/templates/design/changelog/2026-06-29-design-mode-top-chrome-now-groups-collaborators-preview-shar.md +6 -0
  109. package/corpus/templates/design/changelog/2026-06-29-element-selection-now-stays-on-the-intended-layer-after-sele.md +6 -0
  110. package/corpus/templates/design/changelog/2026-06-29-screen-overview-full-view-controls-now-sit-above-frames-inst.md +6 -0
  111. package/corpus/templates/design/changelog/2026-06-29-screen-overview-tools-can-draw-shapes-create-screens-and-kee.md +6 -0
  112. package/corpus/templates/design/changelog/2026-06-29-the-pen-tool-now-creates-anchor-based-bezier-vectors-with-ha.md +6 -0
  113. package/corpus/templates/design/server/handlers/import-figma-system.ts +38 -85
  114. package/corpus/templates/design/server/lib/design-snapshot.ts +9 -1
  115. package/corpus/templates/design/shared/code-layer.ts +209 -9
  116. package/corpus/templates/design/shared/html-content.ts +21 -0
  117. package/corpus/templates/design/shared/pen-path.ts +207 -0
  118. package/corpus/templates/mail/app/lib/utils.ts +2 -2
  119. package/corpus/templates/slides/.agents/skills/design-systems/SKILL.md +9 -10
  120. package/corpus/templates/slides/AGENTS.md +3 -2
  121. package/corpus/templates/slides/actions/import-file.ts +26 -17
  122. package/corpus/templates/slides/actions/index-design-system-with-builder.ts +259 -0
  123. package/corpus/templates/slides/app/components/design-system/DesignSystemSetup.tsx +35 -167
  124. package/corpus/templates/slides/app/components/design-system/fig-import-response.ts +9 -13
  125. package/corpus/templates/slides/app/context/DeckContext.tsx +1 -1
  126. package/corpus/templates/slides/app/i18n/ar-SA.ts +9 -37
  127. package/corpus/templates/slides/app/i18n/de-DE.ts +10 -25
  128. package/corpus/templates/slides/app/i18n/en-US.ts +9 -26
  129. package/corpus/templates/slides/app/i18n/es-ES.ts +10 -28
  130. package/corpus/templates/slides/app/i18n/fr-FR.ts +10 -28
  131. package/corpus/templates/slides/app/i18n/hi-IN.ts +9 -26
  132. package/corpus/templates/slides/app/i18n/ja-JP.ts +10 -22
  133. package/corpus/templates/slides/app/i18n/ko-KR.ts +9 -23
  134. package/corpus/templates/slides/app/i18n/pt-BR.ts +10 -28
  135. package/corpus/templates/slides/app/i18n/zh-CN.ts +9 -22
  136. package/corpus/templates/slides/app/i18n/zh-TW.ts +9 -22
  137. package/corpus/templates/slides/server/handlers/import-figma-system.ts +44 -12
  138. package/corpus/templates/slides/server/lib/chat-attachments.ts +1 -1
  139. package/corpus/templates/videos/app/components/NewCompositionPopover.tsx +1 -1
  140. package/dist/application-state/handlers.js +1 -1
  141. package/dist/application-state/handlers.js.map +1 -1
  142. package/dist/brand-kit/index.d.ts +1 -1
  143. package/dist/brand-kit/index.js +1 -1
  144. package/dist/brand-kit/index.js.map +1 -1
  145. package/dist/cli/skills.d.ts.map +1 -1
  146. package/dist/cli/skills.js +18 -0
  147. package/dist/cli/skills.js.map +1 -1
  148. package/dist/client/AgentPanel.js +1 -1
  149. package/dist/client/AgentPanel.js.map +1 -1
  150. package/dist/client/MultiTabAssistantChat.js +1 -1
  151. package/dist/client/MultiTabAssistantChat.js.map +1 -1
  152. package/dist/client/extensions/portable-extension.d.ts +1 -1
  153. package/dist/client/extensions/portable-extension.d.ts.map +1 -1
  154. package/dist/client/extensions/portable-extension.js.map +1 -1
  155. package/dist/code-agents/transcript-normalizer.js +1 -1
  156. package/dist/code-agents/transcript-normalizer.js.map +1 -1
  157. package/dist/collab/awareness.d.ts +2 -2
  158. package/dist/collab/awareness.d.ts.map +1 -1
  159. package/dist/extensions/path.d.ts +1 -1
  160. package/dist/extensions/path.d.ts.map +1 -1
  161. package/dist/extensions/path.js.map +1 -1
  162. package/dist/integrations/a2a-continuation-processor.js +1 -1
  163. package/dist/integrations/a2a-continuation-processor.js.map +1 -1
  164. package/dist/mcp/build-server.d.ts +1 -1
  165. package/dist/mcp/build-server.d.ts.map +1 -1
  166. package/dist/mcp/build-server.js.map +1 -1
  167. package/dist/observability/routes.d.ts +3 -3
  168. package/dist/org/handlers.js +5 -5
  169. package/dist/org/handlers.js.map +1 -1
  170. package/dist/org/plugin.js +2 -2
  171. package/dist/org/plugin.js.map +1 -1
  172. package/dist/progress/routes.d.ts +1 -1
  173. package/dist/scripts/call-agent.js +1 -1
  174. package/dist/scripts/call-agent.js.map +1 -1
  175. package/dist/scripts/db/safety.js +1 -1
  176. package/dist/scripts/db/safety.js.map +1 -1
  177. package/dist/server/auth.d.ts.map +1 -1
  178. package/dist/server/auth.js +3 -0
  179. package/dist/server/auth.js.map +1 -1
  180. package/dist/server/builder-design-systems.d.ts +28 -0
  181. package/dist/server/builder-design-systems.d.ts.map +1 -0
  182. package/dist/server/builder-design-systems.js +217 -0
  183. package/dist/server/builder-design-systems.js.map +1 -0
  184. package/dist/server/core-routes-plugin.d.ts +1 -0
  185. package/dist/server/core-routes-plugin.d.ts.map +1 -1
  186. package/dist/server/core-routes-plugin.js +20 -3
  187. package/dist/server/core-routes-plugin.js.map +1 -1
  188. package/dist/server/index.d.ts +1 -0
  189. package/dist/server/index.d.ts.map +1 -1
  190. package/dist/server/index.js +1 -0
  191. package/dist/server/index.js.map +1 -1
  192. package/dist/server/transcribe-voice.d.ts +1 -1
  193. package/dist/styles/agent-native.css +14 -7
  194. package/dist/usage/store.js +2 -2
  195. package/dist/usage/store.js.map +1 -1
  196. package/docs/content/locales/ar-SA/skills-guide.mdx +5 -0
  197. package/docs/content/locales/ar-SA/template-design.mdx +14 -1
  198. package/docs/content/locales/de-DE/skills-guide.mdx +5 -0
  199. package/docs/content/locales/de-DE/template-design.mdx +15 -1
  200. package/docs/content/locales/es-ES/skills-guide.mdx +5 -0
  201. package/docs/content/locales/es-ES/template-design.mdx +15 -1
  202. package/docs/content/locales/fr-FR/skills-guide.mdx +5 -0
  203. package/docs/content/locales/fr-FR/template-design.mdx +15 -1
  204. package/docs/content/locales/hi-IN/skills-guide.mdx +5 -0
  205. package/docs/content/locales/hi-IN/template-design.mdx +15 -1
  206. package/docs/content/locales/ja-JP/skills-guide.mdx +5 -0
  207. package/docs/content/locales/ja-JP/template-design.mdx +16 -1
  208. package/docs/content/locales/ko-KR/skills-guide.mdx +5 -0
  209. package/docs/content/locales/ko-KR/template-design.mdx +14 -1
  210. package/docs/content/locales/pt-BR/skills-guide.mdx +5 -0
  211. package/docs/content/locales/pt-BR/template-design.mdx +14 -1
  212. package/docs/content/locales/zh-CN/skills-guide.mdx +5 -0
  213. package/docs/content/locales/zh-CN/template-design.mdx +14 -1
  214. package/docs/content/locales/zh-TW/skills-guide.mdx +5 -0
  215. package/docs/content/locales/zh-TW/template-design.mdx +14 -1
  216. package/docs/content/skills-guide.mdx +5 -0
  217. package/docs/content/template-design.mdx +14 -1
  218. package/package.json +1 -1
  219. package/corpus/templates/design/actions/import-figma-file.ts +0 -166
  220. package/corpus/templates/slides/server/lib/fig-design-system.ts +0 -110
@@ -1,3 +1,14 @@
1
+ import {
2
+ buildDashboardPanelGroups,
3
+ columnExpansionForDropSlot,
4
+ movePanelToDropSlot,
5
+ type DashboardDropSlot,
6
+ } from "../app/pages/adhoc/sql-dashboard/dashboard-layout";
7
+ import {
8
+ clampDashboardColumns,
9
+ MAX_DASHBOARD_COLUMNS,
10
+ type SqlPanel,
11
+ } from "../app/pages/adhoc/sql-dashboard/types";
1
12
  import { movePanelsById, type PanelOrderTarget } from "./dashboard-panel-order";
2
13
 
3
14
  export const DASHBOARD_MUTATION_API_TYPES = `type DashboardScript = {
@@ -57,6 +68,14 @@ type PanelSelection = {
57
68
  moveBefore(panelId: string): void;
58
69
  moveAfter(panelId: string): void;
59
70
  moveToIndex(index: number): void;
71
+ moveNextTo(panelId: string): void; // same visible row, after panelId; expands row columns when needed
72
+ nextTo(panelId: string): void; // alias for moveNextTo
73
+ moveToRow(rowNumber: number): void; // 1-based visible row number, end of row
74
+ moveToRowStart(rowNumber: number): void;
75
+ moveToRowEnd(rowNumber: number): void;
76
+ atRow(rowNumber: number): void; // alias for moveToRow
77
+ atRowStart(rowNumber: number): void;
78
+ atRowEnd(rowNumber: number): void;
60
79
  remove(): void;
61
80
  set(patch: PanelPatch): void;
62
81
  setTitle(title: string): void;
@@ -77,6 +96,10 @@ type InsertedPanel = {
77
96
  before(panelId: string): void;
78
97
  after(panelId: string): void;
79
98
  atIndex(index: number): void;
99
+ nextTo(panelId: string): void; // same visible row, after panelId; expands row columns when needed
100
+ atRow(rowNumber: number): void; // 1-based visible row number, end of row
101
+ atRowStart(rowNumber: number): void;
102
+ atRowEnd(rowNumber: number): void;
80
103
  };`;
81
104
 
82
105
  export const DASHBOARD_MUTATION_EXAMPLES = [
@@ -88,6 +111,8 @@ export const DASHBOARD_MUTATION_EXAMPLES = [
88
111
  'dashboard.panel("retention").setConfigPath("yAxis.format","percent");',
89
112
  'dashboard.section("retention-activity-section").append(["repeat-users","retention-over-time"]);',
90
113
  'dashboard.insertPanel({"id":"new-kpi","title":"New KPI","source":"first-party","chartType":"metric","width":1,"sql":"SELECT COUNT(*) AS value FROM analytics_events"}).atTop();',
114
+ 'dashboard.insertPanel({"id":"new-chart","title":"New Chart","source":"first-party","chartType":"line","width":1,"sql":"SELECT date, COUNT(*) AS value FROM analytics_events GROUP BY date ORDER BY date"}).nextTo("retention-over-time");',
115
+ 'dashboard.insertPanel({"id":"row-chart","title":"Row Chart","source":"first-party","chartType":"bar","width":1,"sql":"SELECT name, COUNT(*) AS value FROM analytics_events GROUP BY name"}).atRow(2);',
91
116
  ] as const;
92
117
 
93
118
  export type DashboardMutationOperation =
@@ -98,6 +123,9 @@ export type DashboardMutationOperation =
98
123
  index?: number;
99
124
  beforePanelId?: string;
100
125
  afterPanelId?: string;
126
+ nextToPanelId?: string;
127
+ rowNumber?: number;
128
+ rowPosition?: "start" | "end";
101
129
  }
102
130
  | {
103
131
  op: "removePanels";
@@ -121,6 +149,9 @@ export type DashboardMutationOperation =
121
149
  index?: number;
122
150
  beforePanelId?: string;
123
151
  afterPanelId?: string;
152
+ nextToPanelId?: string;
153
+ rowNumber?: number;
154
+ rowPosition?: "start" | "end";
124
155
  }
125
156
  | {
126
157
  op: "duplicatePanel";
@@ -131,6 +162,9 @@ export type DashboardMutationOperation =
131
162
  index?: number;
132
163
  beforePanelId?: string;
133
164
  afterPanelId?: string;
165
+ nextToPanelId?: string;
166
+ rowNumber?: number;
167
+ rowPosition?: "start" | "end";
134
168
  }
135
169
  | {
136
170
  op: "setDashboard";
@@ -156,8 +190,17 @@ type MutationTarget = {
156
190
  index?: number;
157
191
  beforePanelId?: string;
158
192
  afterPanelId?: string;
193
+ nextToPanelId?: string;
194
+ rowNumber?: number;
195
+ rowPosition?: "start" | "end";
159
196
  };
160
197
 
198
+ type VisualPlacementTarget =
199
+ | { nextToPanelId: string }
200
+ | { rowNumber: number; rowPosition?: "start" | "end" };
201
+
202
+ type DashboardPlacementTarget = PanelOrderTarget | VisualPlacementTarget;
203
+
161
204
  const DASHBOARD_SUBJECTS = [
162
205
  "set",
163
206
  "panel",
@@ -173,6 +216,14 @@ const PANEL_METHODS = [
173
216
  "moveBefore",
174
217
  "moveAfter",
175
218
  "moveToIndex",
219
+ "moveNextTo",
220
+ "nextTo",
221
+ "moveToRow",
222
+ "moveToRowStart",
223
+ "moveToRowEnd",
224
+ "atRow",
225
+ "atRowStart",
226
+ "atRowEnd",
176
227
  "remove",
177
228
  "set",
178
229
  "setTitle",
@@ -189,11 +240,19 @@ const PLACEMENT_METHODS = [
189
240
  "moveBefore",
190
241
  "moveAfter",
191
242
  "moveToIndex",
243
+ "moveNextTo",
244
+ "moveToRow",
245
+ "moveToRowStart",
246
+ "moveToRowEnd",
192
247
  "atTop",
193
248
  "atBottom",
194
249
  "before",
195
250
  "after",
196
251
  "atIndex",
252
+ "nextTo",
253
+ "atRow",
254
+ "atRowStart",
255
+ "atRowEnd",
197
256
  ];
198
257
 
199
258
  function panelsFromConfig(config: Record<string, unknown>) {
@@ -339,6 +398,14 @@ function assertNumber(value: unknown, label: string): number {
339
398
  return value;
340
399
  }
341
400
 
401
+ function assertPositiveInteger(value: unknown, label: string): number {
402
+ const number = assertNumber(value, label);
403
+ if (!Number.isInteger(number) || number < 1) {
404
+ throw new Error(`${label} must be a positive integer`);
405
+ }
406
+ return number;
407
+ }
408
+
342
409
  function assertObject(value: unknown, label: string): Record<string, unknown> {
343
410
  if (!value || typeof value !== "object" || Array.isArray(value)) {
344
411
  throw new Error(`${label} must be an object`);
@@ -359,23 +426,172 @@ function uniqueStrings(values: string[]): string[] {
359
426
 
360
427
  function targetFromOperation(
361
428
  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) {
429
+ ): DashboardPlacementTarget {
430
+ const targetNames = [
431
+ op.position !== undefined ? "position" : null,
432
+ op.index !== undefined ? "index" : null,
433
+ op.beforePanelId ? "beforePanelId" : null,
434
+ op.afterPanelId ? "afterPanelId" : null,
435
+ op.nextToPanelId ? "nextToPanelId" : null,
436
+ op.rowNumber !== undefined ? "rowNumber" : null,
437
+ ].filter((item): item is string => item !== null);
438
+ if (targetNames.length > 1) {
369
439
  throw new Error(
370
- `${op.op} accepts only one of index, beforePanelId, or afterPanelId`,
440
+ `${op.op} accepts only one placement target (${targetNames.join(", ")})`,
371
441
  );
372
442
  }
443
+ if (op.rowPosition !== undefined && op.rowNumber === undefined) {
444
+ throw new Error(`${op.op} rowPosition requires rowNumber`);
445
+ }
446
+ if (op.nextToPanelId) return { nextToPanelId: op.nextToPanelId };
447
+ if (op.rowNumber !== undefined) {
448
+ if (!Number.isInteger(op.rowNumber) || op.rowNumber < 1) {
449
+ throw new Error(`${op.op} rowNumber must be a positive integer`);
450
+ }
451
+ return {
452
+ rowNumber: op.rowNumber,
453
+ rowPosition: op.rowPosition ?? "end",
454
+ };
455
+ }
373
456
  if (op.beforePanelId) return { beforePanelId: op.beforePanelId };
374
457
  if (op.afterPanelId) return { afterPanelId: op.afterPanelId };
375
458
  if (op.index !== undefined) return { index: op.index };
376
459
  return { position: op.position ?? "bottom" };
377
460
  }
378
461
 
462
+ function isVisualPlacementTarget(
463
+ target: DashboardPlacementTarget,
464
+ ): target is VisualPlacementTarget {
465
+ return "nextToPanelId" in target || "rowNumber" in target;
466
+ }
467
+
468
+ function dashboardColumns(config: Record<string, unknown>): number {
469
+ return clampDashboardColumns(config.columns);
470
+ }
471
+
472
+ function panelsForLayout(config: Record<string, unknown>): SqlPanel[] {
473
+ return panelsFromConfig(config) as unknown as SqlPanel[];
474
+ }
475
+
476
+ function setGroupColumns(
477
+ config: Record<string, unknown>,
478
+ sectionPanelId: string | null,
479
+ columns: number,
480
+ ): void {
481
+ if (sectionPanelId === null) {
482
+ config.columns = columns;
483
+ return;
484
+ }
485
+ const section = requirePanel(panelsFromConfig(config), sectionPanelId);
486
+ section.columns = columns;
487
+ }
488
+
489
+ function resolveVisualDropSlot(
490
+ config: Record<string, unknown>,
491
+ target: VisualPlacementTarget,
492
+ ): DashboardDropSlot {
493
+ const groups = buildDashboardPanelGroups(
494
+ panelsForLayout(config),
495
+ dashboardColumns(config),
496
+ );
497
+
498
+ if ("nextToPanelId" in target) {
499
+ for (const group of groups) {
500
+ for (let rowIndex = 0; rowIndex < group.rows.length; rowIndex++) {
501
+ const row = group.rows[rowIndex];
502
+ const columnIndex = row.panels.findIndex(
503
+ (panel) => panel.id === target.nextToPanelId,
504
+ );
505
+ if (columnIndex >= 0) {
506
+ return {
507
+ type: "column",
508
+ groupKey: group.key,
509
+ rowIndex,
510
+ columnIndex: columnIndex + 1,
511
+ };
512
+ }
513
+ }
514
+ }
515
+ throw new Error(
516
+ `target ${missingPanelHelp(panelsFromConfig(config), target.nextToPanelId)}`,
517
+ );
518
+ }
519
+
520
+ let rowNumber = 1;
521
+ for (const group of groups) {
522
+ for (let rowIndex = 0; rowIndex < group.rows.length; rowIndex++) {
523
+ const row = group.rows[rowIndex];
524
+ if (rowNumber === target.rowNumber) {
525
+ return {
526
+ type: "column",
527
+ groupKey: group.key,
528
+ rowIndex,
529
+ columnIndex: target.rowPosition === "start" ? 0 : row.panels.length,
530
+ };
531
+ }
532
+ rowNumber++;
533
+ }
534
+ }
535
+
536
+ throw new Error(
537
+ `rowNumber ${target.rowNumber} was not found; dashboard has ${rowNumber - 1} visible row(s)`,
538
+ );
539
+ }
540
+
541
+ function ensureVisualDropSlotCapacity(
542
+ config: Record<string, unknown>,
543
+ panelId: string,
544
+ slot: DashboardDropSlot,
545
+ ): { columns: number; sectionPanelId: string | null } | null {
546
+ if (slot.type !== "column") return null;
547
+ const groups = buildDashboardPanelGroups(
548
+ panelsForLayout(config),
549
+ dashboardColumns(config),
550
+ );
551
+ const group = groups.find((item) => item.key === slot.groupKey);
552
+ const row = group?.rows[slot.rowIndex];
553
+ if (!group || !row) return null;
554
+ const rowContainsPanel = row.panels.some((panel) => panel.id === panelId);
555
+ const requiredColumns = rowContainsPanel
556
+ ? row.panels.length
557
+ : row.panels.length + 1;
558
+ if (requiredColumns > MAX_DASHBOARD_COLUMNS) {
559
+ throw new Error(
560
+ `row ${slot.rowIndex + 1} already has ${MAX_DASHBOARD_COLUMNS} panels; cannot place another panel in that row`,
561
+ );
562
+ }
563
+
564
+ return columnExpansionForDropSlot(groups, panelId, slot);
565
+ }
566
+
567
+ function moveSinglePanelToVisualTarget(
568
+ config: Record<string, unknown>,
569
+ panelId: string,
570
+ target: VisualPlacementTarget,
571
+ ): number {
572
+ if ("nextToPanelId" in target && target.nextToPanelId === panelId) {
573
+ throw new Error("nextTo target cannot be the moving panel itself");
574
+ }
575
+ requirePanel(panelsFromConfig(config), panelId);
576
+ const slot = resolveVisualDropSlot(config, target);
577
+ const expansion = ensureVisualDropSlotCapacity(config, panelId, slot);
578
+ const currentPanels = panelsForLayout(config);
579
+ const nextPanels = movePanelToDropSlot(
580
+ currentPanels,
581
+ panelId,
582
+ slot,
583
+ dashboardColumns(config),
584
+ );
585
+ if (nextPanels === currentPanels) {
586
+ throw new Error("visual placement did not change the dashboard layout");
587
+ }
588
+ config.panels = nextPanels;
589
+ if (expansion) {
590
+ setGroupColumns(config, expansion.sectionPanelId, expansion.columns);
591
+ }
592
+ return findPanelIndex(panelsFromConfig(config), panelId);
593
+ }
594
+
379
595
  function findPanelIndex(
380
596
  panels: Array<Record<string, unknown>>,
381
597
  id: string,
@@ -418,7 +634,7 @@ function enhancePanelError(
418
634
  function insertPanel(
419
635
  config: Record<string, unknown>,
420
636
  panel: Record<string, unknown>,
421
- target: PanelOrderTarget,
637
+ target: DashboardPlacementTarget,
422
638
  ): number {
423
639
  const panels = panelsFromConfig(config);
424
640
  const id = assertString(panel.id, "panel.id");
@@ -426,6 +642,28 @@ function insertPanel(
426
642
  throw new Error(`panel "${id}" already exists`);
427
643
  }
428
644
  const placeholderId = `__agent_native_insert_${id}`;
645
+ if (isVisualPlacementTarget(target)) {
646
+ const slot = resolveVisualDropSlot(config, target);
647
+ config.panels = [...panels, { ...panel, id: placeholderId }];
648
+ const expansion = ensureVisualDropSlotCapacity(config, placeholderId, slot);
649
+ const currentPanels = panelsForLayout(config);
650
+ const nextPanels = movePanelToDropSlot(
651
+ currentPanels,
652
+ placeholderId,
653
+ slot,
654
+ dashboardColumns(config),
655
+ );
656
+ if (nextPanels === currentPanels) {
657
+ throw new Error("visual placement did not change the dashboard layout");
658
+ }
659
+ config.panels = nextPanels;
660
+ if (expansion) {
661
+ setGroupColumns(config, expansion.sectionPanelId, expansion.columns);
662
+ }
663
+ const inserted = requirePanel(panelsFromConfig(config), placeholderId);
664
+ inserted.id = id;
665
+ return findPanelIndex(panelsFromConfig(config), id);
666
+ }
429
667
  config.panels = [...panels, { ...panel, id: placeholderId }];
430
668
  const result = movePanelsById(config, [placeholderId], target);
431
669
  const nextPanels = panelsFromConfig(config);
@@ -520,7 +758,7 @@ function duplicatePanel(
520
758
  panelIdToCopy: string,
521
759
  newPanelId: string,
522
760
  patch: Record<string, unknown>,
523
- target: PanelOrderTarget,
761
+ target: DashboardPlacementTarget,
524
762
  ): number {
525
763
  const panels = panelsFromConfig(config);
526
764
  const source = requirePanel(panels, panelIdToCopy);
@@ -552,13 +790,25 @@ export function applyDashboardMutationOperations(
552
790
  try {
553
791
  switch (op.op) {
554
792
  case "movePanels": {
555
- let result;
556
- try {
557
- result = movePanelsById(
793
+ const target = targetFromOperation(op);
794
+ if (isVisualPlacementTarget(target)) {
795
+ if (op.panelIds.length !== 1) {
796
+ throw new Error(
797
+ "row-aware visual placement supports one moving panel at a time",
798
+ );
799
+ }
800
+ const index = moveSinglePanelToVisualTarget(
558
801
  config,
559
- op.panelIds,
560
- targetFromOperation(op),
802
+ op.panelIds[0],
803
+ target,
561
804
  );
805
+ changedPanelIds.add(op.panelIds[0]);
806
+ commandLog.push(`movePanels(${op.panelIds[0]}) -> index ${index}`);
807
+ break;
808
+ }
809
+ let result;
810
+ try {
811
+ result = movePanelsById(config, op.panelIds, target);
562
812
  } catch (err) {
563
813
  enhancePanelError(config, err);
564
814
  }
@@ -887,6 +1137,31 @@ function targetFromChainCall(call: ParsedCall): MutationTarget {
887
1137
  case "moveToIndex":
888
1138
  case "atIndex":
889
1139
  return { index: assertNumber(call.args[0], `${call.name} index`) };
1140
+ case "moveNextTo":
1141
+ case "nextTo":
1142
+ return {
1143
+ nextToPanelId: assertString(call.args[0], `${call.name} panelId`),
1144
+ };
1145
+ case "moveToRow":
1146
+ case "moveToRowEnd":
1147
+ case "atRow":
1148
+ case "atRowEnd":
1149
+ return {
1150
+ rowNumber: assertPositiveInteger(
1151
+ call.args[0],
1152
+ `${call.name} rowNumber`,
1153
+ ),
1154
+ rowPosition: "end",
1155
+ };
1156
+ case "moveToRowStart":
1157
+ case "atRowStart":
1158
+ return {
1159
+ rowNumber: assertPositiveInteger(
1160
+ call.args[0],
1161
+ `${call.name} rowNumber`,
1162
+ ),
1163
+ rowPosition: "start",
1164
+ };
890
1165
  default:
891
1166
  throw new Error(
892
1167
  `unsupported placement method "${call.name}".${methodHelp(call.name, PLACEMENT_METHODS)}`,
@@ -904,6 +1179,14 @@ function operationFromPanelCommand(
904
1179
  case "moveBefore":
905
1180
  case "moveAfter":
906
1181
  case "moveToIndex":
1182
+ case "moveNextTo":
1183
+ case "nextTo":
1184
+ case "moveToRow":
1185
+ case "moveToRowStart":
1186
+ case "moveToRowEnd":
1187
+ case "atRow":
1188
+ case "atRowStart":
1189
+ case "atRowEnd":
907
1190
  return [
908
1191
  {
909
1192
  op: "movePanels",
@@ -22,6 +22,7 @@ function dashboardLayoutSummary(config: Record<string, unknown>) {
22
22
  const columns = clampDashboardColumns(config.columns);
23
23
  const groups = buildDashboardPanelGroups(panels, columns);
24
24
  const panelOrder = getPanelOrder(config);
25
+ let visibleRowNumber = 1;
25
26
 
26
27
  return {
27
28
  panelCount: panelOrder.length,
@@ -32,10 +33,14 @@ function dashboardLayoutSummary(config: Record<string, unknown>) {
32
33
  sectionId: group.section?.id ?? null,
33
34
  sectionTitle: group.section?.title ?? null,
34
35
  columns: group.columns,
35
- rows: group.rows.map((row, rowIndex) => ({
36
- rowIndex,
37
- panelIds: row.panels.map((panel) => panel.id),
38
- })),
36
+ rows: group.rows.map((row, rowIndex) => {
37
+ const rowNumber = visibleRowNumber++;
38
+ return {
39
+ rowNumber,
40
+ rowIndex,
41
+ panelIds: row.panels.map((panel) => panel.id),
42
+ };
43
+ }),
39
44
  })),
40
45
  };
41
46
  }
@@ -27,6 +27,20 @@ const mutationTargetSchema = {
27
27
  index: z.number().int().nonnegative().optional(),
28
28
  beforePanelId: z.string().optional(),
29
29
  afterPanelId: z.string().optional(),
30
+ nextToPanelId: z
31
+ .string()
32
+ .optional()
33
+ .describe("Place the panel in the same visible row, after this panel id."),
34
+ rowNumber: z
35
+ .number()
36
+ .int()
37
+ .positive()
38
+ .optional()
39
+ .describe("1-based visible row number for row-aware placement."),
40
+ rowPosition: z
41
+ .enum(["start", "end"])
42
+ .optional()
43
+ .describe("Where in rowNumber to place the panel. Defaults to end."),
30
44
  };
31
45
 
32
46
  const mutationOperationSchema = z.discriminatedUnion("op", [
@@ -101,8 +115,9 @@ const apiHelp =
101
115
  "Constrained TypeScript-like dashboard mutation script. The server parses only calls on `dashboard`; it does not execute arbitrary JavaScript. " +
102
116
  "No variables, imports, loops, functions, templates, network, filesystem, or DB access. Arguments must be JSON-compatible literals, so quote object keys. " +
103
117
  "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(" ")}`;
118
+ "Selection methods: moveToTop, moveToBottom, moveBefore, moveAfter, moveToIndex, moveNextTo, moveToRow, remove, set, setTitle, setSql, setWidth, setConfig, setConfigPath, duplicate. " +
119
+ "Inserted panels support atTop, atBottom, before, after, atIndex, nextTo, atRow, atRowStart, and atRowEnd. Use nextTo(panelId) or atRow(rowNumber) for visible row placement. " +
120
+ `Examples: ${DASHBOARD_MUTATION_EXAMPLES.slice(0, 5).join(" ")}`;
106
121
 
107
122
  function resolveScope() {
108
123
  const orgId = getRequestOrgId() || null;
@@ -177,6 +192,7 @@ export default defineAction({
177
192
  description:
178
193
  "Apply general SQL dashboard edits through a small typed mutation API in ONE atomic save. " +
179
194
  "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. " +
195
+ "For user placement requests like 'second row' or 'next to return rates', use row-aware placement such as `dashboard.insertPanel(...).nextTo(\"retention-over-time\")` or `.atRow(2)`, then verify rendered rows from `get-sql-dashboard.layout.groups`. " +
180
196
  "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
197
  "The main code argument is a string, so it avoids brittle JSON-pointer indexes and native-array serialization issues. " +
182
198
  `Common example: ${DASHBOARD_MUTATION_EXAMPLES[0]}`,
@@ -15,14 +15,24 @@ function resolveScope() {
15
15
 
16
16
  export default defineAction({
17
17
  description:
18
- "Query first-party analytics events recorded through this app's analytics collector endpoint (/track) and session replay summaries recorded through /api/analytics/replay. Use this for questions about app/site traffic, product events, template/app usage, conversions, session recordings, and other first-party data collected by this analytics app. Use source-specific actions such as BigQuery, GA4, Mixpanel, PostHog, or Amplitude when the user asks for those sources or the relevant data lives there. SQL may read analytics_events and session_recordings only; session_replay_chunks is intentionally unavailable, and reads are automatically scoped to the current user/org.",
18
+ "Query first-party analytics events recorded through this app's analytics collector endpoint (/track) and session replay summaries recorded through /api/analytics/replay. Use this for questions about app/site traffic, product events, template/app usage, conversions, session recordings, and other first-party data collected by this analytics app. Use source-specific actions such as BigQuery, GA4, Mixpanel, PostHog, or Amplitude when the user asks for those sources or the relevant data lives there. SQL may read analytics_events and session_recordings only; session_replay_chunks is intentionally unavailable, and reads are automatically scoped to the current user/org. analytics_events columns include event_name, timestamp, event_date, user_id, anonymous_id, user_key, session_id, app, template, signed_in, url, path, hostname, referrer, properties, and context. session_recordings columns include id, session_id, user_id, anonymous_id, user_key, started_at, ended_at, duration_ms, chunk_count, event_count, page_count, error_count, rage_click_count, app, template, status, first_url, last_url, path, hostname, referrer, and metadata.",
19
19
  schema: z.object({
20
20
  sql: z
21
21
  .string()
22
22
  .describe(
23
- "Read-only SQL over analytics_events and session_recordings, e.g. SELECT event_name, COUNT(*) AS events FROM analytics_events WHERE timestamp >= '2026-05-01T04:00:00Z' AND timestamp < '2026-05-02T04:00:00Z' GROUP BY event_name ORDER BY events DESC",
23
+ "Read-only SQL over analytics_events and session_recordings. Use literal values, not bind placeholders. Example: SELECT event_name, COUNT(*) AS events FROM analytics_events WHERE timestamp >= '2026-05-01T04:00:00Z' AND timestamp < '2026-05-02T04:00:00Z' GROUP BY event_name ORDER BY events DESC",
24
24
  ),
25
25
  }),
26
+ outputSchema: z.object({
27
+ rows: z.array(z.record(z.string(), z.unknown())),
28
+ schema: z.array(
29
+ z.object({
30
+ name: z.string(),
31
+ type: z.string(),
32
+ }),
33
+ ),
34
+ }),
35
+ readOnly: true,
26
36
  http: false,
27
37
  run: async (args) => {
28
38
  return queryFirstPartyAnalytics(args.sql, resolveScope());
@@ -0,0 +1,66 @@
1
+ import { agentNativePath } from "@agent-native/core/client";
2
+ import { useQuery, useQueryClient } from "@tanstack/react-query";
3
+ import { useEffect } from "react";
4
+
5
+ export interface ReplayStorageStatus {
6
+ configured: boolean;
7
+ activeProvider?: { id: string; name: string } | null;
8
+ builderConfigured?: boolean;
9
+ }
10
+
11
+ export const REPLAY_STORAGE_STATUS_KEY = [
12
+ "analytics",
13
+ "replay-storage-status",
14
+ ] as const;
15
+
16
+ export async function fetchReplayStorageStatus(): Promise<ReplayStorageStatus> {
17
+ let uploadStatus: ReplayStorageStatus | null = null;
18
+ try {
19
+ const r = await fetch(agentNativePath("/_agent-native/file-upload/status"));
20
+ uploadStatus = r.ok ? ((await r.json()) as ReplayStorageStatus) : null;
21
+ if (uploadStatus?.configured) return uploadStatus;
22
+ } catch {
23
+ // Fall through to the Builder status check.
24
+ }
25
+
26
+ try {
27
+ const r = await fetch(agentNativePath("/_agent-native/builder/status"));
28
+ const builderStatus = r.ok
29
+ ? ((await r.json()) as { configured?: boolean })
30
+ : null;
31
+ if (builderStatus?.configured) {
32
+ return {
33
+ configured: true,
34
+ activeProvider: { id: "builder", name: "Builder.io" },
35
+ builderConfigured: true,
36
+ };
37
+ }
38
+ } catch {
39
+ // Treat an unreachable status route as not configured.
40
+ }
41
+
42
+ return {
43
+ configured: false,
44
+ activeProvider: uploadStatus?.activeProvider ?? null,
45
+ builderConfigured: uploadStatus?.builderConfigured ?? false,
46
+ };
47
+ }
48
+
49
+ export function useReplayStorageStatus() {
50
+ return useQuery({
51
+ queryKey: REPLAY_STORAGE_STATUS_KEY,
52
+ queryFn: fetchReplayStorageStatus,
53
+ staleTime: 60_000,
54
+ });
55
+ }
56
+
57
+ export function usePrefetchReplayStorageStatus() {
58
+ const qc = useQueryClient();
59
+ useEffect(() => {
60
+ qc.prefetchQuery({
61
+ queryKey: REPLAY_STORAGE_STATUS_KEY,
62
+ queryFn: fetchReplayStorageStatus,
63
+ staleTime: 60_000,
64
+ });
65
+ }, [qc]);
66
+ }
@@ -116,6 +116,37 @@ const messages = {
116
116
  aboutDescription:
117
117
  "Analytics 用於連線資料來源並建置自訂儀表板。連線 Google Analytics、BigQuery、Stripe 等,然後讓代理建立儀表板。",
118
118
  aboutUsage: "在資料來源頁面管理連線。使用查詢探索器執行臨時 BigQuery SQL。",
119
+ replayStorage: "工作階段回放儲存",
120
+ replayStorageDescription:
121
+ "工作階段回放錄製需要已設定的檔案上傳供應商。連線 Builder.io 以使用免費方案的物件儲存,或使用您自己的 S3 相容儲存桶。",
122
+ connected: "已連線",
123
+ connectBuilder: "連線 Builder.io",
124
+ checkingBuilder: "正在檢查 Builder.io...",
125
+ builderConnected: "已連線 Builder.io",
126
+ builderConnectedToast: "已連線 Builder.io",
127
+ builderConnectedFor: "已連線至 {{orgName}}。",
128
+ builderConnectedGeneric: "回放分塊儲存於 Builder.io。",
129
+ builderIncludes: "Builder.io 的免費方案包含回放分塊的物件儲存。",
130
+ s3Title: "S3 相容儲存",
131
+ secondary: "次要",
132
+ active: "使用中",
133
+ s3BuilderConnectedDescription:
134
+ "選用。使用您自己的儲存桶取代 Builder.io 儲存。",
135
+ s3CurrentProvider: "目前供應商:{{providerName}}。",
136
+ s3OwnBucketDescription:
137
+ "AWS S3、Cloudflare R2、DigitalOcean Spaces、MinIO 或任何 S3 相容服務。",
138
+ configureS3: "設定 S3",
139
+ hideS3: "隱藏",
140
+ s3EndpointLabel: "端點 URL",
141
+ s3BucketLabel: "儲存桶名稱",
142
+ s3AccessKeyLabel: "存取金鑰 ID",
143
+ s3SecretAccessKeyLabel: "私密存取金鑰",
144
+ s3RegionLabel: "地區(選用)",
145
+ s3PublicBaseUrlLabel: "公開基底 URL(選用)",
146
+ saveStorage: "儲存儲存設定",
147
+ storageRequired: "請輸入必填的儲存欄位。",
148
+ storageSaved: "已儲存儲存設定。",
149
+ storageSaveFailed: "無法儲存儲存設定。",
119
150
  },
120
151
  chat: {
121
152
  emptyState: "讓我分析儀表板、比較趨勢或深入檢視資料...",
@@ -802,6 +833,12 @@ const messages = {
802
833
  path: "路徑",
803
834
  eventName: "事件",
804
835
  time: "時間",
836
+ storageSetupTitle: "連線回放儲存",
837
+ storageSetupDescription:
838
+ "工作階段回放錄製需要先設定儲存才能儲存分塊。連線 Builder.io 以使用免費方案儲存,或設定您自己的 S3 相容儲存桶。",
839
+ storageConnected: "已連線儲存",
840
+ connectBuilder: "使用 Builder.io(免費)",
841
+ configureS3: "設定 S3 儲存",
805
842
  },
806
843
  catalog: {
807
844
  description: "需要起點時,可安裝儀表板範本。",