@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,14 +1,29 @@
1
1
  import {
2
+ agentNativePath,
2
3
  ChangelogSettingsCard,
3
4
  LanguagePicker,
4
5
  SettingsTabsPage,
5
6
  openAgentSettings,
7
+ useBuilderConnectFlow,
8
+ useBuilderStatus,
6
9
  useT,
7
10
  } from "@agent-native/core/client";
8
11
  import { TeamPage } from "@agent-native/core/client/org";
12
+ import {
13
+ IconCheck,
14
+ IconChevronDown,
15
+ IconCloud,
16
+ IconExternalLink,
17
+ IconKey,
18
+ IconLoader2,
19
+ IconServer,
20
+ } from "@tabler/icons-react";
21
+ import { useState } from "react";
9
22
  import { Link } from "react-router";
23
+ import { toast } from "sonner";
10
24
 
11
25
  import { useAuth } from "@/components/auth/AuthProvider";
26
+ import { Badge } from "@/components/ui/badge";
12
27
  import { Button } from "@/components/ui/button";
13
28
  import {
14
29
  Card,
@@ -17,14 +32,160 @@ import {
17
32
  CardHeader,
18
33
  CardTitle,
19
34
  } from "@/components/ui/card";
35
+ import {
36
+ Collapsible,
37
+ CollapsibleContent,
38
+ CollapsibleTrigger,
39
+ } from "@/components/ui/collapsible";
40
+ import { Input } from "@/components/ui/input";
20
41
  import { Label } from "@/components/ui/label";
42
+ import { useReplayStorageStatus } from "@/hooks/use-replay-storage-status";
43
+ import { cn } from "@/lib/utils";
21
44
 
22
45
  import changelog from "../../CHANGELOG.md?raw";
23
46
 
47
+ const S3_STORAGE_FIELDS = [
48
+ {
49
+ key: "S3_ENDPOINT",
50
+ labelKey: "settings.s3EndpointLabel",
51
+ placeholder: "https://s3.us-east-1.amazonaws.com",
52
+ required: true,
53
+ },
54
+ {
55
+ key: "S3_BUCKET",
56
+ labelKey: "settings.s3BucketLabel",
57
+ placeholder: "my-replays-bucket",
58
+ required: true,
59
+ },
60
+ {
61
+ key: "S3_ACCESS_KEY_ID",
62
+ labelKey: "settings.s3AccessKeyLabel",
63
+ placeholder: "AKIA...",
64
+ required: true,
65
+ },
66
+ {
67
+ key: "S3_SECRET_ACCESS_KEY",
68
+ labelKey: "settings.s3SecretAccessKeyLabel",
69
+ placeholder: "••••••••",
70
+ required: true,
71
+ secret: true,
72
+ },
73
+ {
74
+ key: "S3_REGION",
75
+ labelKey: "settings.s3RegionLabel",
76
+ placeholder: "us-east-1",
77
+ },
78
+ {
79
+ key: "S3_PUBLIC_BASE_URL",
80
+ labelKey: "settings.s3PublicBaseUrlLabel",
81
+ placeholder: "https://cdn.example.com",
82
+ },
83
+ ] as const;
84
+
85
+ async function saveS3StorageSettings(
86
+ values: Record<string, string>,
87
+ ): Promise<void> {
88
+ const vars = S3_STORAGE_FIELDS.map((field) => ({
89
+ key: field.key,
90
+ value: (values[field.key] ?? "").trim(),
91
+ })).filter((entry) => entry.value.length > 0);
92
+
93
+ if (vars.length === 0) {
94
+ throw new Error("Enter at least one storage value.");
95
+ }
96
+
97
+ for (const { key, value } of vars) {
98
+ const res = await fetch(agentNativePath("/_agent-native/secrets/adhoc"), {
99
+ method: "POST",
100
+ headers: { "Content-Type": "application/json" },
101
+ body: JSON.stringify({
102
+ name: key,
103
+ value,
104
+ scope: "workspace",
105
+ description: "Analytics S3-compatible replay storage", // i18n-ignore -- secret metadata description, not visible UI
106
+ }),
107
+ });
108
+
109
+ if (!res.ok) {
110
+ const body = (await res.json().catch(() => null)) as {
111
+ error?: string;
112
+ } | null;
113
+ throw new Error(body?.error ?? `Save failed (${res.status})`);
114
+ }
115
+ }
116
+ }
117
+
24
118
  export default function Settings() {
25
119
  const { auth } = useAuth();
26
120
  const t = useT();
27
121
 
122
+ const storageStatus = useReplayStorageStatus();
123
+ const builderStatus = useBuilderStatus();
124
+ const builderConnect = useBuilderConnectFlow({
125
+ popupUrl:
126
+ builderStatus.status?.cliAuthUrl ?? builderStatus.status?.connectUrl,
127
+ trackingSource: "analytics_settings",
128
+ trackingFlow: "replay_storage",
129
+ onConnected: async () => {
130
+ await Promise.all([storageStatus.refetch(), builderStatus.refetch()]);
131
+ toast.success(t("settings.builderConnectedToast"));
132
+ },
133
+ });
134
+
135
+ const [savingStorage, setSavingStorage] = useState(false);
136
+ const [s3Values, setS3Values] = useState<Record<string, string>>({});
137
+ const [s3Expanded, setS3Expanded] = useState(false);
138
+
139
+ const storageConfigured = !!storageStatus.data?.configured;
140
+ const activeProviderName = storageStatus.data?.activeProvider?.name ?? null;
141
+ const activeProviderId = storageStatus.data?.activeProvider?.id ?? null;
142
+ const builderConnected = Boolean(
143
+ builderConnect.configured ||
144
+ builderStatus.status?.configured ||
145
+ storageStatus.data?.builderConfigured ||
146
+ activeProviderId === "builder",
147
+ );
148
+ const builderOrgName =
149
+ builderConnect.orgName ?? builderStatus.status?.orgName ?? null;
150
+ const builderStatusLoading =
151
+ storageStatus.isLoading ||
152
+ builderStatus.loading ||
153
+ !builderConnect.hasFetchedStatus;
154
+ const s3Configured = activeProviderId === "s3";
155
+ const s3Collapsed = builderConnected && !s3Expanded;
156
+
157
+ async function handleSaveS3Storage() {
158
+ const missing = s3Configured
159
+ ? []
160
+ : S3_STORAGE_FIELDS.filter(
161
+ (field) =>
162
+ "required" in field &&
163
+ field.required &&
164
+ !(s3Values[field.key] ?? "").trim(),
165
+ );
166
+ if (missing.length > 0) {
167
+ toast.error(t("settings.storageRequired"));
168
+ return;
169
+ }
170
+
171
+ setSavingStorage(true);
172
+ try {
173
+ await saveS3StorageSettings(s3Values);
174
+ setS3Values((current) => ({
175
+ ...current,
176
+ S3_SECRET_ACCESS_KEY: "",
177
+ }));
178
+ await storageStatus.refetch();
179
+ toast.success(t("settings.storageSaved"));
180
+ } catch (err) {
181
+ toast.error(
182
+ err instanceof Error ? err.message : t("settings.storageSaveFailed"),
183
+ );
184
+ } finally {
185
+ setSavingStorage(false);
186
+ }
187
+ }
188
+
28
189
  return (
29
190
  <SettingsTabsPage
30
191
  teamLabel={t("navigation.team")}
@@ -67,6 +228,178 @@ export default function Settings() {
67
228
  </CardContent>
68
229
  </Card>
69
230
 
231
+ <Card
232
+ id="replay-storage"
233
+ className="bg-card border-border/50 scroll-mt-16"
234
+ >
235
+ <CardHeader>
236
+ <CardTitle className="text-base flex items-center gap-2">
237
+ <IconCloud className="size-4 text-primary" />
238
+ {t("settings.replayStorage")}
239
+ </CardTitle>
240
+ <CardDescription>
241
+ {t("settings.replayStorageDescription")}
242
+ </CardDescription>
243
+ </CardHeader>
244
+ <CardContent className="space-y-4">
245
+ <div
246
+ className={cn(
247
+ "flex flex-col gap-3 rounded-md border px-3 py-3 sm:flex-row sm:items-center sm:justify-between",
248
+ builderConnected
249
+ ? "border-primary/35 bg-primary/5"
250
+ : "border-border bg-accent/30",
251
+ )}
252
+ >
253
+ <div className="min-w-0">
254
+ <div className="flex items-center gap-2 text-sm font-medium">
255
+ {builderConnected ? (
256
+ <IconCheck className="h-4 w-4 text-primary" />
257
+ ) : (
258
+ <IconKey className="h-4 w-4 text-muted-foreground" />
259
+ )}
260
+ {builderStatusLoading
261
+ ? t("settings.checkingBuilder")
262
+ : builderConnected
263
+ ? t("settings.builderConnected")
264
+ : t("settings.connectBuilder")}
265
+ </div>
266
+ <p className="mt-0.5 text-xs text-muted-foreground">
267
+ {builderConnected
268
+ ? builderOrgName
269
+ ? t("settings.builderConnectedFor", {
270
+ orgName: builderOrgName,
271
+ })
272
+ : t("settings.builderConnectedGeneric")
273
+ : t("settings.builderIncludes")}
274
+ </p>
275
+ </div>
276
+ {builderConnected ? (
277
+ <Badge variant="secondary" className="shrink-0">
278
+ {t("settings.connected")}
279
+ </Badge>
280
+ ) : (
281
+ <Button
282
+ type="button"
283
+ variant="outline"
284
+ size="sm"
285
+ className="shrink-0"
286
+ onClick={() =>
287
+ builderConnect.start({
288
+ trackingSource: "analytics_settings_replay_storage",
289
+ trackingFlow: "replay_storage",
290
+ })
291
+ }
292
+ disabled={builderConnect.connecting || builderStatusLoading}
293
+ >
294
+ {builderConnect.connecting ? (
295
+ <IconLoader2 className="h-4 w-4 animate-spin" />
296
+ ) : (
297
+ <IconExternalLink className="h-4 w-4" />
298
+ )}
299
+ {t("settings.connectBuilder")}
300
+ </Button>
301
+ )}
302
+ </div>
303
+
304
+ <Collapsible
305
+ open={builderConnected ? !s3Collapsed : true}
306
+ onOpenChange={(open) => setS3Expanded(open)}
307
+ >
308
+ <div className="rounded-md border border-border">
309
+ <div className="flex flex-col gap-3 px-3 py-3 sm:flex-row sm:items-center sm:justify-between">
310
+ <div className="min-w-0">
311
+ <div className="flex flex-wrap items-center gap-2 text-sm font-medium">
312
+ <IconServer className="h-4 w-4 text-muted-foreground" />
313
+ {t("settings.s3Title")}
314
+ <Badge variant="outline" className="text-[10px]">
315
+ {t("settings.secondary")}
316
+ </Badge>
317
+ {s3Configured ? (
318
+ <Badge variant="secondary" className="text-[10px]">
319
+ {t("settings.active")}
320
+ </Badge>
321
+ ) : null}
322
+ </div>
323
+ <p className="mt-0.5 text-xs text-muted-foreground">
324
+ {builderConnected
325
+ ? t("settings.s3BuilderConnectedDescription")
326
+ : storageConfigured && activeProviderName
327
+ ? t("settings.s3CurrentProvider", {
328
+ providerName: activeProviderName,
329
+ })
330
+ : t("settings.s3OwnBucketDescription")}
331
+ </p>
332
+ </div>
333
+ {builderConnected ? (
334
+ <CollapsibleTrigger asChild>
335
+ <Button
336
+ type="button"
337
+ variant="ghost"
338
+ size="sm"
339
+ className="shrink-0"
340
+ >
341
+ {s3Collapsed
342
+ ? t("settings.configureS3")
343
+ : t("settings.hideS3")}
344
+ <IconChevronDown
345
+ className={cn(
346
+ "h-4 w-4 transition-transform",
347
+ !s3Collapsed && "rotate-180",
348
+ )}
349
+ />
350
+ </Button>
351
+ </CollapsibleTrigger>
352
+ ) : null}
353
+ </div>
354
+
355
+ <CollapsibleContent>
356
+ <div className="space-y-4 border-t border-border px-3 py-4">
357
+ <div className="grid gap-4 sm:grid-cols-2">
358
+ {S3_STORAGE_FIELDS.map((field) => (
359
+ <div key={field.key} className="space-y-1.5">
360
+ <Label htmlFor={field.key}>
361
+ {t(field.labelKey)}
362
+ </Label>
363
+ <Input
364
+ id={field.key}
365
+ type={
366
+ "secret" in field && field.secret
367
+ ? "password"
368
+ : "text"
369
+ }
370
+ value={s3Values[field.key] ?? ""}
371
+ onChange={(event) =>
372
+ setS3Values((current) => ({
373
+ ...current,
374
+ [field.key]: event.target.value,
375
+ }))
376
+ }
377
+ placeholder={field.placeholder}
378
+ autoComplete="off"
379
+ disabled={savingStorage}
380
+ />
381
+ </div>
382
+ ))}
383
+ </div>
384
+
385
+ <div className="flex justify-end">
386
+ <Button
387
+ onClick={handleSaveS3Storage}
388
+ disabled={savingStorage || storageStatus.isLoading}
389
+ >
390
+ {savingStorage && (
391
+ <IconLoader2 className="h-4 w-4 animate-spin" />
392
+ )}
393
+ {t("settings.saveStorage")}
394
+ </Button>
395
+ </div>
396
+ </div>
397
+ </CollapsibleContent>
398
+ </div>
399
+ </Collapsible>
400
+ </CardContent>
401
+ </Card>
402
+
70
403
  <Card className="bg-card border-border/50">
71
404
  <CardHeader>
72
405
  <CardTitle className="text-base">
@@ -229,7 +229,7 @@ function PanelEditorContent({
229
229
  `The \`demo\` source is reserved for the built-in Node Exporter demo and does not satisfy REAL_DATA_REQUIRED unless the user explicitly asks to work on that demo dashboard. ` +
230
230
  `If no source can answer, report the exact unavailable/error result instead of saving a panel with guessed schema or metrics. ` +
231
231
  `Use the \`mutate-dashboard\` action with code like \`dashboard.insertPanel({"id":"new-panel","title":"New Panel","source":"first-party","chartType":"metric","width":1,"sql":"SELECT COUNT(*) AS value FROM analytics_events"}).atBottom();\` ` +
232
- `to append, or \`.before("panel-id")\`, \`.after("panel-id")\`, or \`.atIndex(n)\` to place the panel. ` +
232
+ `to append, or \`.nextTo("panel-id")\`, \`.atRow(2)\`, \`.atRowStart(2)\`, \`.before("panel-id")\`, \`.after("panel-id")\`, or \`.atIndex(n)\` to place the panel. Prefer \`.nextTo("panel-id")\` or \`.atRow(rowNumber)\` for visible row placement requests; they keep the chart in the intended rendered row and expand/rebalance that row when needed. ` +
233
233
  `Panel shape: { id (unique slug), title, sql, source ('bigquery'|'ga4'|'amplitude'|'first-party'|'demo'|'prometheus'), chartType ('line'|'area'|'bar'|'metric'|'table'|'pie'|'section'), width (legacy integer 1..6; set to 1 unless editing existing data), tab? (use 'Group / Tab' for grouped tabs), columns? (section panels only - 1..6 max panels per row for panels following this section), config? }. ` +
234
234
  `Visible layout auto-fits by row: one panel in a row spans the row, two split it, three split it into thirds, up to the section column limit. ` +
235
235
  `For amplitude panels, sql is a JSON descriptor: {"event":"event name","groupBy":"property","days":30}. ` +
@@ -238,6 +238,7 @@ function PanelEditorContent({
238
238
  `For prometheus panels, sql is a JSON descriptor: {"promql":"rate(http_requests_total[5m])","mode":"range","range":"1h","step":"30s"}. mode defaults to "range"; range defaults to "1h"; step is auto if omitted. Returned rows have shape {timestamp, series, value} — set config.xKey="timestamp", config.yKey="value", and a single series in config.yKeys for clean charting. ` +
239
239
  `Config is optional: { xKey, yKey, yKeys, yFormatter ('number'|'currency'|'percent'), description, columns, pivot, limit, color, colors, stacked, legend, valueLabels }. ` +
240
240
  `Chart legends render automatically; set config.legend=false only when the user explicitly asks to hide the legend. ` +
241
+ `Use \`get-sql-dashboard.layout.groups[].rows[].rowNumber/panelIds\` to identify and verify visible rows. ` +
241
242
  `Consult the data dictionary first via \`list-data-dictionary --search <topic>\`, then use AGENTS.md, .agents/skills, and connected data-source instructions before writing SQL. ` +
242
243
  `Every BigQuery panel is dry-run validated on save — if columns/tables are wrong the save returns a 400 with the BQ error and you must fix the SQL and retry. ` +
243
244
  `After the mutation saves, verify the returned panelCount, appliedOps, and insertedPanelIds; the UI refreshes automatically.`,
@@ -1,4 +1,9 @@
1
- import { clampDashboardColumns, clampPanelWidth, type SqlPanel } from "./types";
1
+ import {
2
+ clampDashboardColumns,
3
+ clampPanelWidth,
4
+ MAX_DASHBOARD_COLUMNS,
5
+ type SqlPanel,
6
+ } from "./types";
2
7
 
3
8
  export type DashboardPanelRow = {
4
9
  key: string;
@@ -38,6 +43,19 @@ export type DashboardClientRect = {
38
43
  bottom: number;
39
44
  };
40
45
 
46
+ export type DashboardDropSlotCandidate = {
47
+ id: string;
48
+ slot: DashboardDropSlot;
49
+ rect: DashboardClientRect;
50
+ };
51
+
52
+ export type DashboardColumnExpansion = {
53
+ columns: number;
54
+ sectionPanelId: string | null;
55
+ };
56
+
57
+ const COLUMN_SLOT_EDGE_PROXIMITY_PX = 48;
58
+
41
59
  function rowKey(panels: SqlPanel[], index: number): string {
42
60
  return panels.map((panel) => panel.id).join(":") || `empty-${index}`;
43
61
  }
@@ -62,6 +80,77 @@ export function distanceFromPointerToRect(
62
80
  return Math.hypot(dx, dy);
63
81
  }
64
82
 
83
+ function pointerIsInsideRect(
84
+ pointer: DashboardPointerCoordinates,
85
+ rect: DashboardClientRect,
86
+ ): boolean {
87
+ return (
88
+ pointer.x >= rect.left &&
89
+ pointer.x <= rect.right &&
90
+ pointer.y >= rect.top &&
91
+ pointer.y <= rect.bottom
92
+ );
93
+ }
94
+
95
+ function pointerIsHorizontallyInsideRect(
96
+ pointer: DashboardPointerCoordinates,
97
+ rect: DashboardClientRect,
98
+ ): boolean {
99
+ return pointer.x >= rect.left && pointer.x <= rect.right;
100
+ }
101
+
102
+ function closestDropSlotCandidate(
103
+ pointer: DashboardPointerCoordinates,
104
+ candidates: DashboardDropSlotCandidate[],
105
+ ): { candidate: DashboardDropSlotCandidate; distance: number } | null {
106
+ let closest: DashboardDropSlotCandidate | null = null;
107
+ let closestDistance = Number.MAX_VALUE;
108
+
109
+ for (const candidate of candidates) {
110
+ const distance = distanceFromPointerToRect(pointer, candidate.rect);
111
+ if (distance < closestDistance) {
112
+ closest = candidate;
113
+ closestDistance = distance;
114
+ }
115
+ }
116
+
117
+ return closest ? { candidate: closest, distance: closestDistance } : null;
118
+ }
119
+
120
+ export function preferredDropSlotId(
121
+ pointer: DashboardPointerCoordinates,
122
+ candidates: DashboardDropSlotCandidate[],
123
+ ): string | null {
124
+ if (candidates.length === 0) return null;
125
+
126
+ const columnSlotsUnderPointerX = candidates.filter(
127
+ (candidate) =>
128
+ candidate.slot.type === "column" &&
129
+ pointerIsHorizontallyInsideRect(pointer, candidate.rect),
130
+ );
131
+ const closestColumnSlot = closestDropSlotCandidate(
132
+ pointer,
133
+ columnSlotsUnderPointerX,
134
+ );
135
+ if (
136
+ closestColumnSlot &&
137
+ closestColumnSlot.distance <= COLUMN_SLOT_EDGE_PROXIMITY_PX
138
+ ) {
139
+ return closestColumnSlot.candidate.id;
140
+ }
141
+
142
+ const containingSlots = candidates.filter((candidate) =>
143
+ pointerIsInsideRect(pointer, candidate.rect),
144
+ );
145
+ const closestContainingSlot = closestDropSlotCandidate(
146
+ pointer,
147
+ containingSlots,
148
+ );
149
+ if (closestContainingSlot) return closestContainingSlot.candidate.id;
150
+
151
+ return closestDropSlotCandidate(pointer, candidates)?.candidate.id ?? null;
152
+ }
153
+
65
154
  export function rebalanceRowWidths(
66
155
  panels: SqlPanel[],
67
156
  columns: number,
@@ -276,7 +365,7 @@ export function isDropSlotAvailable(
276
365
  const rowContainsPanel = row.panels.some((panel) => panel.id === panelId);
277
366
  if (rowContainsPanel) return row.panels.length > 1;
278
367
 
279
- return row.panels.length < group.columns;
368
+ return row.panels.length < MAX_DASHBOARD_COLUMNS;
280
369
  }
281
370
 
282
371
  export function availableDropSlotIdsForPanel(
@@ -294,7 +383,9 @@ export function availableDropSlotIdsForPanel(
294
383
  for (let rowIndex = 0; rowIndex < group.rows.length; rowIndex++) {
295
384
  const row = group.rows[rowIndex];
296
385
  const rowContainsPanel = row.panels.some((panel) => panel.id === panelId);
297
- if (!rowContainsPanel && row.panels.length >= group.columns) continue;
386
+ if (!rowContainsPanel && row.panels.length >= MAX_DASHBOARD_COLUMNS) {
387
+ continue;
388
+ }
298
389
  if (rowContainsPanel && row.panels.length <= 1) continue;
299
390
 
300
391
  for (
@@ -317,6 +408,29 @@ export function availableDropSlotIdsForPanel(
317
408
  return ids;
318
409
  }
319
410
 
411
+ export function columnExpansionForDropSlot(
412
+ groups: DashboardPanelGroup[],
413
+ panelId: string,
414
+ slot: DashboardDropSlot,
415
+ ): DashboardColumnExpansion | null {
416
+ if (slot.type !== "column") return null;
417
+
418
+ const group = groups.find((item) => item.key === slot.groupKey);
419
+ const row = group?.rows[slot.rowIndex];
420
+ if (!group || !row) return null;
421
+
422
+ const rowContainsPanel = row.panels.some((panel) => panel.id === panelId);
423
+ const requiredColumns = rowContainsPanel
424
+ ? row.panels.length
425
+ : row.panels.length + 1;
426
+ if (requiredColumns <= group.columns) return null;
427
+
428
+ return {
429
+ columns: clampDashboardColumns(requiredColumns),
430
+ sectionPanelId: group.section?.id ?? null,
431
+ };
432
+ }
433
+
320
434
  export function movePanelToDropSlot(
321
435
  panels: SqlPanel[],
322
436
  panelId: string,
@@ -408,6 +522,10 @@ export function movePanelToDropSlot(
408
522
  0,
409
523
  movingPanel,
410
524
  );
525
+ targetGroup.columns = Math.max(
526
+ targetGroup.columns,
527
+ clampDashboardColumns(targetRow.panels.length),
528
+ );
411
529
  }
412
530
 
413
531
  return flattenGroups(nextGroups);
@@ -17,7 +17,6 @@ import {
17
17
  useDroppable,
18
18
  DndContext,
19
19
  DragOverlay,
20
- pointerWithin,
21
20
  PointerSensor,
22
21
  useSensor,
23
22
  useSensors,
@@ -106,9 +105,11 @@ import { DashboardSkeleton } from "../DashboardSkeleton";
106
105
  import {
107
106
  availableDropSlotIdsForPanel,
108
107
  buildDashboardPanelGroups,
108
+ columnExpansionForDropSlot,
109
109
  distanceFromPointerToRect,
110
110
  dropSlotId,
111
111
  movePanelToDropSlot,
112
+ preferredDropSlotId,
112
113
  readDropSlot,
113
114
  removePanelFromLayout,
114
115
  sameDropSlot,
@@ -978,6 +979,11 @@ export default function SqlDashboardPage() {
978
979
  const slot = readDropSlot(event.over?.data.current);
979
980
  if (!slot) return;
980
981
  const panelId = String(event.active.id);
982
+ const columnExpansion = columnExpansionForDropSlot(
983
+ buildDashboardPanelGroups(dashboard.panels, dashboardColumns),
984
+ panelId,
985
+ slot,
986
+ );
981
987
  const nextPanels = movePanelToDropSlot(
982
988
  dashboard.panels,
983
989
  panelId,
@@ -985,9 +991,19 @@ export default function SqlDashboardPage() {
985
991
  dashboardColumns,
986
992
  );
987
993
  if (nextPanels === dashboard.panels) return;
994
+ const persistedPanels = columnExpansion?.sectionPanelId
995
+ ? nextPanels.map((panel) =>
996
+ panel.id === columnExpansion.sectionPanelId
997
+ ? { ...panel, columns: columnExpansion.columns }
998
+ : panel,
999
+ )
1000
+ : nextPanels;
988
1001
  persist({
989
1002
  ...dashboard,
990
- panels: nextPanels,
1003
+ ...(columnExpansion && !columnExpansion.sectionPanelId
1004
+ ? { columns: columnExpansion.columns }
1005
+ : {}),
1006
+ panels: persistedPanels,
991
1007
  });
992
1008
  },
993
1009
  [dashboard, dashboardColumns, persist],
@@ -1154,36 +1170,43 @@ export default function SqlDashboardPage() {
1154
1170
 
1155
1171
  if (droppableContainers.length === 0) return [];
1156
1172
 
1157
- const exactCollisions = pointerWithin({
1158
- ...args,
1159
- droppableContainers,
1160
- });
1161
- if (exactCollisions.length > 0) return exactCollisions;
1162
-
1163
1173
  if (!args.pointerCoordinates) return [];
1164
1174
 
1165
- let closestId: string | null = null;
1166
- let closestContainer: (typeof droppableContainers)[number] | null = null;
1167
- let closestValue = Number.MAX_VALUE;
1168
- for (const droppableContainer of droppableContainers) {
1175
+ const candidates = droppableContainers.flatMap((droppableContainer) => {
1169
1176
  const rect = args.droppableRects.get(droppableContainer.id);
1170
- if (!rect) continue;
1171
-
1172
- const value = distanceFromPointerToRect(args.pointerCoordinates, rect);
1173
- if (value < closestValue) {
1174
- closestId = String(droppableContainer.id);
1175
- closestContainer = droppableContainer;
1176
- closestValue = value;
1177
- }
1178
- }
1179
-
1180
- return closestId && closestContainer
1177
+ const slot = readDropSlot(droppableContainer.data.current);
1178
+ if (!rect || !slot) return [];
1179
+ return [
1180
+ {
1181
+ id: String(droppableContainer.id),
1182
+ slot,
1183
+ rect,
1184
+ },
1185
+ ];
1186
+ });
1187
+ const preferredId = preferredDropSlotId(
1188
+ args.pointerCoordinates,
1189
+ candidates,
1190
+ );
1191
+ const preferredContainer =
1192
+ preferredId === null
1193
+ ? null
1194
+ : (droppableContainers.find(
1195
+ (container) => String(container.id) === preferredId,
1196
+ ) ?? null);
1197
+ const preferredRect =
1198
+ preferredContainer && args.droppableRects.get(preferredContainer.id);
1199
+
1200
+ return preferredId && preferredContainer && preferredRect
1181
1201
  ? [
1182
1202
  {
1183
- id: closestId,
1203
+ id: preferredId,
1184
1204
  data: {
1185
- droppableContainer: closestContainer,
1186
- value: closestValue,
1205
+ droppableContainer: preferredContainer,
1206
+ value: distanceFromPointerToRect(
1207
+ args.pointerCoordinates,
1208
+ preferredRect,
1209
+ ),
1187
1210
  },
1188
1211
  },
1189
1212
  ]