@agent-native/core 0.120.0 → 0.120.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (207) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +16 -0
  3. package/corpus/core/docs/content/extensions.mdx +23 -4
  4. package/corpus/core/package.json +1 -1
  5. package/corpus/core/src/agent/production-agent.ts +4 -0
  6. package/corpus/core/src/agent/run-manager.ts +36 -10
  7. package/corpus/core/src/agent/types.ts +2 -0
  8. package/corpus/core/src/client/MultiTabAssistantChat.tsx +45 -0
  9. package/corpus/core/src/client/extensions/ExtensionsListPage.tsx +16 -3
  10. package/corpus/core/src/client/settings/ExtensionsSettingsContent.tsx +24 -0
  11. package/corpus/core/src/client/settings/SettingsPanel.tsx +10 -0
  12. package/corpus/core/src/client/use-chat-models.ts +45 -0
  13. package/corpus/core/src/deploy/build.ts +3 -0
  14. package/corpus/core/src/server/analytics.ts +164 -20
  15. package/corpus/core/src/server/auth.ts +17 -13
  16. package/corpus/core/src/server/better-auth-instance.ts +13 -1
  17. package/corpus/core/src/templates/chat/app/components/layout/Sidebar.tsx +35 -6
  18. package/corpus/core/src/templates/chat/app/routes/extensions._index.tsx +2 -2
  19. package/corpus/core/src/templates/chat/netlify.toml +1 -0
  20. package/corpus/core/src/templates/workspace-core/.agents/skills/extensions/SKILL.md +16 -0
  21. package/corpus/core/src/vite/client.ts +6 -0
  22. package/corpus/templates/analytics/.agents/skills/adhoc-analysis/SKILL.md +27 -43
  23. package/corpus/templates/analytics/.agents/skills/dashboard-management/SKILL.md +19 -8
  24. package/corpus/templates/analytics/.agents/skills/data-querying/SKILL.md +2 -2
  25. package/corpus/templates/analytics/AGENTS.md +20 -8
  26. package/corpus/templates/analytics/actions/get-explorer-dashboard.ts +3 -0
  27. package/corpus/templates/analytics/actions/list-dashboard-usage-stats.ts +140 -46
  28. package/corpus/templates/analytics/actions/migrate-analytics-artifacts.ts +48 -0
  29. package/corpus/templates/analytics/actions/save-analysis.ts +2 -3
  30. package/corpus/templates/analytics/actions/view-screen.ts +1 -1
  31. package/corpus/templates/analytics/app/components/dashboard/DashboardMetadata.tsx +77 -0
  32. package/corpus/templates/analytics/app/components/dashboard/DashboardPanelSkeleton.tsx +7 -1
  33. package/corpus/templates/analytics/app/components/layout/NewDashboardDialog.tsx +1 -0
  34. package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +103 -502
  35. package/corpus/templates/analytics/app/global.css +30 -0
  36. package/corpus/templates/analytics/app/hooks/use-navigation-state.ts +2 -2
  37. package/corpus/templates/analytics/app/i18n-data.ts +80 -0
  38. package/corpus/templates/analytics/app/pages/Agents.tsx +49 -2
  39. package/corpus/templates/analytics/app/pages/adhoc/explorer-dashboard/index.tsx +54 -11
  40. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/SqlChartCard.tsx +3 -53
  41. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx +23 -25
  42. package/corpus/templates/analytics/app/pages/analyses/AnalysisDetail.tsx +2 -2
  43. package/corpus/templates/analytics/app/routes/analyses._index.tsx +3 -2
  44. package/corpus/templates/analytics/app/routes/extensions._index.tsx +2 -2
  45. package/corpus/templates/analytics/changelog/2026-07-23-analytics-now-groups-saved-analyses-and-embedded-extensions-.md +6 -0
  46. package/corpus/templates/analytics/changelog/2026-07-23-dashboard-menus-show-who-created-and-last-updated-each-dashb.md +6 -0
  47. package/corpus/templates/analytics/changelog/2026-07-23-dashboard-usage-now-loads-reliably-for-organizations-with-ma.md +6 -0
  48. package/corpus/templates/analytics/changelog/2026-07-23-long-dashboard-building-requests-now-continue-without-stoppi.md +6 -0
  49. package/corpus/templates/analytics/netlify.toml +4 -0
  50. package/corpus/templates/analytics/server/db/schema.ts +4 -0
  51. package/corpus/templates/analytics/server/lib/agent-chat-plan-mode.ts +0 -1
  52. package/corpus/templates/analytics/server/lib/agent-readable-resource-context.ts +1 -0
  53. package/corpus/templates/analytics/server/lib/migrate-analytics-artifacts.ts +1062 -0
  54. package/corpus/templates/analytics/server/plugins/agent-chat.ts +6 -52
  55. package/corpus/templates/analytics/server/plugins/core-routes.ts +1 -1
  56. package/corpus/templates/analytics/server/plugins/db.ts +10 -0
  57. package/corpus/templates/assets/actions/generate-image-batch.ts +36 -1
  58. package/corpus/templates/assets/actions/generate-image.ts +28 -1
  59. package/corpus/templates/assets/app/components/layout/Sidebar.tsx +47 -9
  60. package/corpus/templates/assets/app/routes/extensions._index.tsx +2 -2
  61. package/corpus/templates/assets/changelog/2026-07-23-assets-image-generation-now-recovers-after-a-completed-tool-.md +6 -0
  62. package/corpus/templates/assets/changelog/2026-07-23-new-chat-aligns-with-sidebar-controls.md +6 -0
  63. package/corpus/templates/assets/netlify.toml +1 -0
  64. package/corpus/templates/brain/app/components/layout/Sidebar.tsx +38 -8
  65. package/corpus/templates/brain/app/routes/extensions._index.tsx +2 -2
  66. package/corpus/templates/brain/netlify.toml +1 -0
  67. package/corpus/templates/calendar/app/components/layout/Sidebar.tsx +43 -5
  68. package/corpus/templates/calendar/app/routes/_app.extensions._index.tsx +2 -16
  69. package/corpus/templates/calendar/netlify.toml +1 -0
  70. package/corpus/templates/chat/app/components/layout/Sidebar.tsx +35 -6
  71. package/corpus/templates/chat/app/routes/extensions._index.tsx +2 -2
  72. package/corpus/templates/chat/netlify.toml +1 -0
  73. package/corpus/templates/clips/app/components/library/library-layout.tsx +52 -6
  74. package/corpus/templates/clips/app/routes/_app.extensions._index.tsx +2 -16
  75. package/corpus/templates/clips/changelog/2026-07-23-agent-settings-are-clearly-labeled-manage-agent-in-the-sideb.md +6 -0
  76. package/corpus/templates/clips/changelog/2026-07-23-meeting-recording-meters-and-dismissed-prompts.md +6 -0
  77. package/corpus/templates/clips/desktop/src/hooks/useMicMeter.ts +1 -1
  78. package/corpus/templates/clips/desktop/src/lib/meeting-notification-dismissal.ts +2 -0
  79. package/corpus/templates/clips/desktop/src/lib/voice-dictation.ts +1 -3
  80. package/corpus/templates/clips/desktop/src/overlays/live-audio-bars.tsx +86 -0
  81. package/corpus/templates/clips/desktop/src/overlays/meeting-notification.tsx +24 -0
  82. package/corpus/templates/clips/desktop/src/overlays/recording-pill.tsx +5 -69
  83. package/corpus/templates/clips/desktop/src/styles.css +35 -3
  84. package/corpus/templates/clips/desktop/src-tauri/src/adhoc_meetings_watcher.rs +13 -44
  85. package/corpus/templates/clips/desktop/src-tauri/src/notifications.rs +119 -17
  86. package/corpus/templates/clips/netlify.toml +1 -0
  87. package/corpus/templates/content/app/components/sidebar/DocumentSidebar.tsx +3 -7
  88. package/corpus/templates/content/app/routes/_app.extensions._index.tsx +2 -2
  89. package/corpus/templates/content/changelog/2026-07-23-agent-settings-are-clearly-labeled-manage-agent-in-the-sideb.md +6 -0
  90. package/corpus/templates/content/netlify.toml +1 -0
  91. package/corpus/templates/design/app/components/layout/Sidebar.tsx +55 -17
  92. package/corpus/templates/design/app/routes/extensions._index.tsx +2 -2
  93. package/corpus/templates/design/changelog/2026-07-23-agent-settings-are-clearly-labeled-manage-agent-in-the-sideb.md +6 -0
  94. package/corpus/templates/design/netlify.toml +1 -0
  95. package/corpus/templates/dispatch/app/dispatch-extensions.tsx +1 -1
  96. package/corpus/templates/dispatch/changelog/2026-07-23-agent-settings-are-clearly-labeled-manage-agent-in-the-sideb.md +6 -0
  97. package/corpus/templates/dispatch/netlify.toml +1 -0
  98. package/corpus/templates/forms/.agents/skills/form-publishing/SKILL.md +9 -3
  99. package/corpus/templates/forms/AGENTS.md +11 -1
  100. package/corpus/templates/forms/README.md +24 -1
  101. package/corpus/templates/forms/actions/update-form.ts +12 -5
  102. package/corpus/templates/forms/app/components/builder/FieldPropertiesPanel.tsx +199 -1
  103. package/corpus/templates/forms/app/components/layout/Sidebar.tsx +41 -45
  104. package/corpus/templates/forms/app/i18n/en-US.ts +13 -1
  105. package/corpus/templates/forms/app/pages/FormBuilderPage.tsx +35 -4
  106. package/corpus/templates/forms/app/pages/FormFillPage.tsx +7 -16
  107. package/corpus/templates/forms/app/routes/_app.extensions._index.tsx +2 -2
  108. package/corpus/templates/forms/changelog/2026-07-23-agent-settings-are-clearly-labeled-manage-agent-in-the-sideb.md +6 -0
  109. package/corpus/templates/forms/changelog/2026-07-23-forms-can-branch-into-follow-up-questions-and-route-response.md +6 -0
  110. package/corpus/templates/forms/netlify.toml +1 -0
  111. package/corpus/templates/forms/server/handlers/submissions.ts +22 -31
  112. package/corpus/templates/forms/server/lib/integrations.ts +11 -2
  113. package/corpus/templates/forms/server/lib/public-form-ssr.ts +26 -5
  114. package/corpus/templates/forms/server/lib/validate-fields.ts +48 -11
  115. package/corpus/templates/forms/server/plugins/agent-chat.ts +1 -0
  116. package/corpus/templates/forms/shared/conditional.ts +67 -0
  117. package/corpus/templates/macros/app/components/layout/AppLayout.tsx +36 -5
  118. package/corpus/templates/macros/app/routes/extensions._index.tsx +2 -2
  119. package/corpus/templates/macros/netlify.toml +1 -0
  120. package/corpus/templates/mail/app/components/layout/AppLayout.tsx +0 -9
  121. package/corpus/templates/mail/app/routes/extensions._index.tsx +2 -2
  122. package/corpus/templates/mail/netlify.toml +1 -0
  123. package/corpus/templates/plan/app/components/layout/Sidebar.tsx +38 -5
  124. package/corpus/templates/plan/app/routes/extensions._index.tsx +2 -2
  125. package/corpus/templates/plan/netlify.toml +1 -0
  126. package/corpus/templates/slides/app/components/layout/Sidebar.tsx +47 -4
  127. package/corpus/templates/slides/app/routes/extensions._index.tsx +2 -2
  128. package/corpus/templates/slides/netlify.toml +1 -0
  129. package/corpus/templates/tasks/app/components/layout/Sidebar.tsx +37 -6
  130. package/corpus/templates/tasks/app/routes/extensions._index.tsx +2 -2
  131. package/corpus/templates/tasks/app/routes/settings.tsx +31 -0
  132. package/corpus/templates/tasks/netlify.toml +1 -0
  133. package/dist/agent/production-agent.d.ts.map +1 -1
  134. package/dist/agent/production-agent.js +4 -0
  135. package/dist/agent/production-agent.js.map +1 -1
  136. package/dist/agent/run-manager.d.ts +9 -0
  137. package/dist/agent/run-manager.d.ts.map +1 -1
  138. package/dist/agent/run-manager.js +25 -10
  139. package/dist/agent/run-manager.js.map +1 -1
  140. package/dist/agent/types.d.ts +2 -2
  141. package/dist/agent/types.d.ts.map +1 -1
  142. package/dist/agent/types.js.map +1 -1
  143. package/dist/client/MultiTabAssistantChat.d.ts.map +1 -1
  144. package/dist/client/MultiTabAssistantChat.js +35 -0
  145. package/dist/client/MultiTabAssistantChat.js.map +1 -1
  146. package/dist/client/extensions/ExtensionsListPage.d.ts +5 -1
  147. package/dist/client/extensions/ExtensionsListPage.d.ts.map +1 -1
  148. package/dist/client/extensions/ExtensionsListPage.js +5 -3
  149. package/dist/client/extensions/ExtensionsListPage.js.map +1 -1
  150. package/dist/client/settings/ExtensionsSettingsContent.d.ts +2 -0
  151. package/dist/client/settings/ExtensionsSettingsContent.d.ts.map +1 -0
  152. package/dist/client/settings/ExtensionsSettingsContent.js +10 -0
  153. package/dist/client/settings/ExtensionsSettingsContent.js.map +1 -0
  154. package/dist/client/settings/SettingsPanel.d.ts.map +1 -1
  155. package/dist/client/settings/SettingsPanel.js +10 -1
  156. package/dist/client/settings/SettingsPanel.js.map +1 -1
  157. package/dist/client/use-chat-models.d.ts +1 -0
  158. package/dist/client/use-chat-models.d.ts.map +1 -1
  159. package/dist/client/use-chat-models.js +33 -0
  160. package/dist/client/use-chat-models.js.map +1 -1
  161. package/dist/collab/awareness.d.ts +2 -2
  162. package/dist/collab/awareness.d.ts.map +1 -1
  163. package/dist/deploy/build.js +1 -0
  164. package/dist/deploy/build.js.map +1 -1
  165. package/dist/file-upload/actions/upload-image.d.ts +1 -1
  166. package/dist/observability/routes.d.ts +3 -3
  167. package/dist/provider-api/actions/custom-provider-registration.d.ts +4 -4
  168. package/dist/provider-api/actions/provider-api.d.ts +2 -2
  169. package/dist/server/analytics.d.ts +17 -4
  170. package/dist/server/analytics.d.ts.map +1 -1
  171. package/dist/server/analytics.js +122 -17
  172. package/dist/server/analytics.js.map +1 -1
  173. package/dist/server/auth.d.ts.map +1 -1
  174. package/dist/server/auth.js +5 -4
  175. package/dist/server/auth.js.map +1 -1
  176. package/dist/server/better-auth-instance.d.ts +8 -0
  177. package/dist/server/better-auth-instance.d.ts.map +1 -1
  178. package/dist/server/better-auth-instance.js +10 -1
  179. package/dist/server/better-auth-instance.js.map +1 -1
  180. package/dist/templates/chat/app/components/layout/Sidebar.tsx +35 -6
  181. package/dist/templates/chat/app/routes/extensions._index.tsx +2 -2
  182. package/dist/templates/chat/netlify.toml +1 -0
  183. package/dist/templates/workspace-core/.agents/skills/extensions/SKILL.md +16 -0
  184. package/dist/vite/client.d.ts.map +1 -1
  185. package/dist/vite/client.js +2 -0
  186. package/dist/vite/client.js.map +1 -1
  187. package/docs/content/extensions.mdx +23 -4
  188. package/package.json +1 -1
  189. package/src/agent/production-agent.ts +4 -0
  190. package/src/agent/run-manager.ts +36 -10
  191. package/src/agent/types.ts +2 -0
  192. package/src/client/MultiTabAssistantChat.tsx +45 -0
  193. package/src/client/extensions/ExtensionsListPage.tsx +16 -3
  194. package/src/client/settings/ExtensionsSettingsContent.tsx +24 -0
  195. package/src/client/settings/SettingsPanel.tsx +10 -0
  196. package/src/client/use-chat-models.ts +45 -0
  197. package/src/deploy/build.ts +3 -0
  198. package/src/server/analytics.ts +164 -20
  199. package/src/server/auth.ts +17 -13
  200. package/src/server/better-auth-instance.ts +13 -1
  201. package/src/templates/chat/app/components/layout/Sidebar.tsx +35 -6
  202. package/src/templates/chat/app/routes/extensions._index.tsx +2 -2
  203. package/src/templates/chat/netlify.toml +1 -0
  204. package/src/templates/workspace-core/.agents/skills/extensions/SKILL.md +16 -0
  205. package/src/vite/client.ts +6 -0
  206. package/corpus/templates/analytics/app/components/layout/NewAnalysisDialog.tsx +0 -165
  207. package/corpus/templates/analytics/app/pages/analyses/AnalysesList.tsx +0 -175
@@ -1,5 +1,5 @@
1
1
  import { useT } from "@agent-native/core/client/i18n";
2
- import type { FormField, FormFieldType } from "@shared/types";
2
+ import type { ConditionalRule, FormField, FormFieldType } from "@shared/types";
3
3
  import { IconPlus, IconX } from "@tabler/icons-react";
4
4
  import { useState } from "react";
5
5
 
@@ -19,6 +19,7 @@ import { Textarea } from "@/components/ui/textarea";
19
19
 
20
20
  interface FieldPropertiesPanelProps {
21
21
  field: FormField;
22
+ fields: FormField[];
22
23
  onChange: (field: FormField) => void;
23
24
  onDelete: () => void;
24
25
  }
@@ -39,13 +40,48 @@ const fieldTypeLabels: Record<FormFieldType, string> = {
39
40
 
40
41
  const hasOptions: FormFieldType[] = ["select", "multiselect", "radio"];
41
42
 
43
+ function conditionValueOptions(source: FormField | undefined): string[] {
44
+ if (!source) return [];
45
+ if (hasOptions.includes(source.type)) {
46
+ return (source.options || []).filter(
47
+ (option, index, options) =>
48
+ Boolean(option.trim()) && options.indexOf(option) === index,
49
+ );
50
+ }
51
+ if (source.type === "checkbox") return ["true", "false"];
52
+ if (source.type === "rating") return ["1", "2", "3", "4", "5"];
53
+ return [];
54
+ }
55
+
56
+ function defaultConditionOperator(
57
+ source: FormField,
58
+ ): ConditionalRule["operator"] {
59
+ return source.type === "multiselect" ? "contains" : "equals";
60
+ }
61
+
42
62
  export function FieldPropertiesPanel({
43
63
  field,
64
+ fields,
44
65
  onChange,
45
66
  onDelete,
46
67
  }: FieldPropertiesPanelProps) {
47
68
  const t = useT();
48
69
  const [newOption, setNewOption] = useState("");
70
+ const fieldIndex = fields.findIndex((candidate) => candidate.id === field.id);
71
+ const availableFields =
72
+ fieldIndex >= 0
73
+ ? fields.slice(0, fieldIndex)
74
+ : fields.filter((candidate) => candidate.id !== field.id);
75
+ const conditionSource =
76
+ availableFields.find(
77
+ (candidate) => candidate.id === field.conditional?.fieldId,
78
+ ) || availableFields[0];
79
+ const conditionOptions = conditionValueOptions(conditionSource);
80
+ const conditionFieldId = conditionSource?.id || "";
81
+ const conditionOperator =
82
+ field.conditional?.operator ||
83
+ (conditionSource ? defaultConditionOperator(conditionSource) : "equals");
84
+ const conditionValue = field.conditional?.value || conditionOptions[0] || "";
49
85
 
50
86
  function update(partial: Partial<FormField>) {
51
87
  onChange({ ...field, ...partial });
@@ -161,6 +197,168 @@ export function FieldPropertiesPanel({
161
197
  </Select>
162
198
  </div>
163
199
 
200
+ {availableFields.length > 0 && (
201
+ <>
202
+ <Separator />
203
+ <div className="space-y-3">
204
+ <div className="flex items-start justify-between gap-3">
205
+ <div className="space-y-0.5">
206
+ <Label className="text-xs">
207
+ {t("fieldProperties.conditionalVisibility")}
208
+ </Label>
209
+ <p className="text-[11px] leading-snug text-muted-foreground">
210
+ {t("fieldProperties.conditionalVisibilityDescription")}
211
+ </p>
212
+ </div>
213
+ <Switch
214
+ checked={Boolean(field.conditional)}
215
+ onCheckedChange={(checked) => {
216
+ if (!checked) {
217
+ update({ conditional: undefined });
218
+ return;
219
+ }
220
+ if (!conditionSource) return;
221
+ update({
222
+ conditional: {
223
+ fieldId: conditionSource.id,
224
+ operator: defaultConditionOperator(conditionSource),
225
+ value: conditionOptions[0] || "",
226
+ },
227
+ });
228
+ }}
229
+ />
230
+ </div>
231
+
232
+ {field.conditional && conditionSource && (
233
+ <div className="space-y-2 rounded-md border border-border/70 bg-muted/20 p-2.5">
234
+ <div className="space-y-1.5">
235
+ <Label className="text-xs">
236
+ {t("fieldProperties.conditionField")}
237
+ </Label>
238
+ <Select
239
+ value={conditionFieldId}
240
+ onValueChange={(value) => {
241
+ const nextSource = availableFields.find(
242
+ (candidate) => candidate.id === value,
243
+ );
244
+ if (!nextSource) return;
245
+ const nextOptions = conditionValueOptions(nextSource);
246
+ update({
247
+ conditional: {
248
+ fieldId: nextSource.id,
249
+ operator: defaultConditionOperator(nextSource),
250
+ value: nextOptions[0] || "",
251
+ },
252
+ });
253
+ }}
254
+ >
255
+ <SelectTrigger className="h-8 text-xs">
256
+ <SelectValue />
257
+ </SelectTrigger>
258
+ <SelectContent>
259
+ {availableFields.map((candidate) => (
260
+ <SelectItem
261
+ key={candidate.id}
262
+ value={candidate.id}
263
+ className="text-xs"
264
+ >
265
+ {candidate.label || candidate.id}
266
+ </SelectItem>
267
+ ))}
268
+ </SelectContent>
269
+ </Select>
270
+ </div>
271
+
272
+ <div className="space-y-1.5">
273
+ <Label className="text-xs">
274
+ {t("fieldProperties.conditionOperator")}
275
+ </Label>
276
+ <Select
277
+ value={conditionOperator}
278
+ onValueChange={(value) =>
279
+ update({
280
+ conditional: {
281
+ fieldId: conditionFieldId,
282
+ operator: value as ConditionalRule["operator"],
283
+ value: conditionValue,
284
+ },
285
+ })
286
+ }
287
+ >
288
+ <SelectTrigger className="h-8 text-xs">
289
+ <SelectValue />
290
+ </SelectTrigger>
291
+ <SelectContent>
292
+ <SelectItem value="equals" className="text-xs">
293
+ {t("fieldProperties.conditionEquals")}
294
+ </SelectItem>
295
+ <SelectItem value="not_equals" className="text-xs">
296
+ {t("fieldProperties.conditionNotEquals")}
297
+ </SelectItem>
298
+ <SelectItem value="contains" className="text-xs">
299
+ {t("fieldProperties.conditionContains")}
300
+ </SelectItem>
301
+ </SelectContent>
302
+ </Select>
303
+ </div>
304
+
305
+ <div className="space-y-1.5">
306
+ <Label className="text-xs">
307
+ {t("fieldProperties.conditionValue")}
308
+ </Label>
309
+ {conditionOptions.length > 0 ? (
310
+ <Select
311
+ value={conditionValue}
312
+ onValueChange={(value) =>
313
+ update({
314
+ conditional: {
315
+ fieldId: conditionFieldId,
316
+ operator: conditionOperator,
317
+ value,
318
+ },
319
+ })
320
+ }
321
+ >
322
+ <SelectTrigger className="h-8 text-xs">
323
+ <SelectValue />
324
+ </SelectTrigger>
325
+ <SelectContent>
326
+ {conditionOptions.map((option) => (
327
+ <SelectItem
328
+ key={option}
329
+ value={option}
330
+ className="text-xs"
331
+ >
332
+ {option}
333
+ </SelectItem>
334
+ ))}
335
+ </SelectContent>
336
+ </Select>
337
+ ) : (
338
+ <Input
339
+ value={conditionValue}
340
+ onChange={(event) =>
341
+ update({
342
+ conditional: {
343
+ fieldId: conditionFieldId,
344
+ operator: conditionOperator,
345
+ value: event.target.value,
346
+ },
347
+ })
348
+ }
349
+ placeholder={t(
350
+ "fieldProperties.conditionValuePlaceholder",
351
+ )}
352
+ className="h-8 text-xs"
353
+ />
354
+ )}
355
+ </div>
356
+ </div>
357
+ )}
358
+ </div>
359
+ </>
360
+ )}
361
+
164
362
  {/* Options (for select/radio/multiselect) */}
165
363
  {hasOptions.includes(field.type) && (
166
364
  <>
@@ -5,7 +5,6 @@ import {
5
5
  } from "@agent-native/core/client/agent-chat";
6
6
  import { appPath } from "@agent-native/core/client/api-path";
7
7
  import { DevDatabaseLink } from "@agent-native/core/client/db-admin";
8
- import { ExtensionsSidebarSection } from "@agent-native/core/client/extensions";
9
8
  import { useT } from "@agent-native/core/client/i18n";
10
9
  import { OrgSwitcher } from "@agent-native/core/client/org";
11
10
  import { FeedbackButton } from "@agent-native/core/client/ui";
@@ -277,45 +276,47 @@ export function Sidebar() {
277
276
  {newFormPopover}
278
277
  </Popover>
279
278
 
280
- <Tooltip>
281
- <TooltipTrigger asChild>
282
- <Link
283
- to="/agent"
284
- aria-label={t("navigation.agent")}
285
- className={cn(
286
- "forms-sidebar-nav-item flex size-10 items-center justify-center rounded-lg active:scale-[0.96] transition-[background-color,box-shadow,color,transform]",
287
- location.pathname.startsWith("/agent")
288
- ? "bg-accent text-accent-foreground"
289
- : "text-muted-foreground hover:bg-accent/50 hover:text-foreground",
290
- )}
291
- >
292
- <IconHierarchy2 className="h-4 w-4" />
293
- </Link>
294
- </TooltipTrigger>
295
- <TooltipContent side="right">
296
- {t("navigation.agent")}
297
- </TooltipContent>
298
- </Tooltip>
279
+ <div className="mt-auto flex flex-col items-center gap-1">
280
+ <Tooltip>
281
+ <TooltipTrigger asChild>
282
+ <Link
283
+ to="/agent"
284
+ aria-label={t("settings.agentTitle")}
285
+ className={cn(
286
+ "forms-sidebar-nav-item flex size-10 items-center justify-center rounded-lg active:scale-[0.96] transition-[background-color,box-shadow,color,transform]",
287
+ location.pathname.startsWith("/agent")
288
+ ? "bg-accent text-accent-foreground"
289
+ : "text-muted-foreground hover:bg-accent/50 hover:text-foreground",
290
+ )}
291
+ >
292
+ <IconHierarchy2 className="h-4 w-4" />
293
+ </Link>
294
+ </TooltipTrigger>
295
+ <TooltipContent side="right">
296
+ {t("settings.agentTitle")}
297
+ </TooltipContent>
298
+ </Tooltip>
299
299
 
300
- <Tooltip>
301
- <TooltipTrigger asChild>
302
- <Link
303
- to="/settings"
304
- aria-label={t("navigation.settings")}
305
- className={cn(
306
- "forms-sidebar-nav-item flex size-10 items-center justify-center rounded-lg active:scale-[0.96] transition-[background-color,box-shadow,color,transform]",
307
- location.pathname === "/settings"
308
- ? "bg-accent text-accent-foreground"
309
- : "text-muted-foreground hover:bg-accent/50 hover:text-foreground",
310
- )}
311
- >
312
- <IconSettings className="h-4 w-4" />
313
- </Link>
314
- </TooltipTrigger>
315
- <TooltipContent side="right">
316
- {t("navigation.settings")}
317
- </TooltipContent>
318
- </Tooltip>
300
+ <Tooltip>
301
+ <TooltipTrigger asChild>
302
+ <Link
303
+ to="/settings"
304
+ aria-label={t("navigation.settings")}
305
+ className={cn(
306
+ "forms-sidebar-nav-item flex size-10 items-center justify-center rounded-lg active:scale-[0.96] transition-[background-color,box-shadow,color,transform]",
307
+ location.pathname === "/settings"
308
+ ? "bg-accent text-accent-foreground"
309
+ : "text-muted-foreground hover:bg-accent/50 hover:text-foreground",
310
+ )}
311
+ >
312
+ <IconSettings className="h-4 w-4" />
313
+ </Link>
314
+ </TooltipTrigger>
315
+ <TooltipContent side="right">
316
+ {t("navigation.settings")}
317
+ </TooltipContent>
318
+ </Tooltip>
319
+ </div>
319
320
  </nav>
320
321
  </TooltipProvider>
321
322
  </div>
@@ -446,7 +447,7 @@ export function Sidebar() {
446
447
  )}
447
448
  >
448
449
  <IconHierarchy2 size={14} className="shrink-0" />
449
- <span>{t("navigation.agent")}</span>
450
+ <span>{t("settings.agentTitle")}</span>
450
451
  </Link>
451
452
 
452
453
  <Link
@@ -464,11 +465,6 @@ export function Sidebar() {
464
465
  </Link>
465
466
  </div>
466
467
 
467
- {/* Tools */}
468
- <div className="shrink-0 px-1.5 py-1.5">
469
- <ExtensionsSidebarSection />
470
- </div>
471
-
472
468
  {/* Footer */}
473
469
  <div className="shrink-0 space-y-2 px-3 py-2">
474
470
  <OrgSwitcher />
@@ -88,6 +88,16 @@ const messages = {
88
88
  addOption: "Add option",
89
89
  min: "Min",
90
90
  max: "Max",
91
+ conditionalVisibility: "Conditional visibility",
92
+ conditionalVisibilityDescription:
93
+ "Only show this field when an earlier answer matches.",
94
+ conditionField: "Earlier answer",
95
+ conditionOperator: "Condition",
96
+ conditionEquals: "is",
97
+ conditionNotEquals: "is not",
98
+ conditionContains: "contains",
99
+ conditionValue: "Answer",
100
+ conditionValuePlaceholder: "Enter an answer...",
91
101
  fieldTypes: {
92
102
  text: "Short Text",
93
103
  email: "Email",
@@ -144,6 +154,7 @@ const messages = {
144
154
  integrationsTab: "Integrations",
145
155
  settingsSaved: "Settings saved",
146
156
  integrationsSaved: "Integrations saved",
157
+ saveFailed: "Failed to save changes",
147
158
  formTitlePlaceholder: "Form Title",
148
159
  addDescriptionPlaceholder: "Add a description...",
149
160
  dragToReorder: "Drag to reorder",
@@ -210,7 +221,8 @@ const messages = {
210
221
  "Sends a JSON POST with submission data. Works with Zapier, Make, n8n, etc.",
211
222
  googleSheetsBlurb:
212
223
  "Mirror every response into a spreadsheet your team can share.",
213
- googleSheetsHelp: "Deploy an Apps Script web app that receives POST data",
224
+ googleSheetsHelp:
225
+ "Use a deployed Apps Script /exec URL. It must parse JSON from e.postData.contents and append the row.",
214
226
  slackLabel: "Slack",
215
227
  discordLabel: "Discord",
216
228
  webhookLabel: "Webhook",
@@ -295,6 +295,18 @@ export function FormBuilderPage() {
295
295
  [updateForm],
296
296
  );
297
297
 
298
+ const saveImmediately = useCallback(
299
+ async (data: Parameters<typeof updateForm.mutate>[0]) => {
300
+ clearTimeout(saveTimeout.current);
301
+ try {
302
+ await updateForm.mutateAsync(data);
303
+ } finally {
304
+ fieldsDirty.current = false;
305
+ }
306
+ },
307
+ [updateForm],
308
+ );
309
+
298
310
  // Debounced field-op save — uses patch-form-fields (server-side merge) so
299
311
  // concurrent edits to different fields both survive.
300
312
  const fieldOpTimeout = useRef<ReturnType<typeof setTimeout>>(undefined);
@@ -853,8 +865,17 @@ export function FormBuilderPage() {
853
865
  key={JSON.stringify(form.settings)}
854
866
  form={form}
855
867
  onSave={(settings) => {
856
- save({ id: form.id, settings });
857
- toast.success(t("builder.settingsSaved"));
868
+ void saveImmediately({ id: form.id, settings })
869
+ .then(() => toast.success(t("builder.settingsSaved")))
870
+ .catch((error: unknown) => {
871
+ toast.error(
872
+ error instanceof Error && error.message
873
+ ? error.message
874
+ : t("builder.saveFailed", {
875
+ defaultValue: "Failed to save changes",
876
+ }),
877
+ );
878
+ });
858
879
  }}
859
880
  />
860
881
  </div>
@@ -868,8 +889,17 @@ export function FormBuilderPage() {
868
889
  key={JSON.stringify(form.settings?.integrations)}
869
890
  form={form}
870
891
  onSave={(settings) => {
871
- save({ id: form.id, settings });
872
- toast.success(t("builder.integrationsSaved"));
892
+ void saveImmediately({ id: form.id, settings })
893
+ .then(() => toast.success(t("builder.integrationsSaved")))
894
+ .catch((error: unknown) => {
895
+ toast.error(
896
+ error instanceof Error && error.message
897
+ ? error.message
898
+ : t("builder.saveFailed", {
899
+ defaultValue: "Failed to save changes",
900
+ }),
901
+ );
902
+ });
873
903
  }}
874
904
  />
875
905
  </div>
@@ -1054,6 +1084,7 @@ function BuilderContent({
1054
1084
  >
1055
1085
  <FieldPropertiesPanel
1056
1086
  field={field}
1087
+ fields={fields}
1057
1088
  onChange={onUpdateField}
1058
1089
  onDelete={() => onDeleteField(field.id)}
1059
1090
  />
@@ -1,5 +1,6 @@
1
1
  import { useT } from "@agent-native/core/client/i18n";
2
2
  import { Turnstile, PoweredByBadge } from "@agent-native/core/client/ui";
3
+ import { isConditionalFieldVisible } from "@shared/conditional";
3
4
  import type { FormField, FormSettings } from "@shared/types";
4
5
  import { IconCircleCheck, IconRefresh } from "@tabler/icons-react";
5
6
  import { useState, useMemo, useEffect } from "react";
@@ -88,21 +89,7 @@ export function FormFillPage() {
88
89
 
89
90
  // Evaluate conditional visibility
90
91
  const visibleFields = useMemo(() => {
91
- return fields.filter((field) => {
92
- if (!field.conditional) return true;
93
- const { fieldId, operator, value: condValue } = field.conditional;
94
- const fieldVal = String(values[fieldId] ?? "");
95
- switch (operator) {
96
- case "equals":
97
- return fieldVal === condValue;
98
- case "not_equals":
99
- return fieldVal !== condValue;
100
- case "contains":
101
- return fieldVal.includes(condValue);
102
- default:
103
- return true;
104
- }
105
- });
92
+ return fields.filter((field) => isConditionalFieldVisible(field, values));
106
93
  }, [fields, values]);
107
94
 
108
95
  function handleChange(fieldId: string, value: unknown) {
@@ -166,7 +153,11 @@ export function FormFillPage() {
166
153
  submitForm.mutate(
167
154
  {
168
155
  formId: form.id,
169
- data: values,
156
+ data: Object.fromEntries(
157
+ visibleFields
158
+ .filter((field) => values[field.id] !== undefined)
159
+ .map((field) => [field.id, values[field.id]]),
160
+ ),
170
161
  captchaToken,
171
162
  _hp: honeypot,
172
163
  _t: pageLoadTime,
@@ -1,4 +1,4 @@
1
- import { ExtensionsListPage } from "@agent-native/core/client/extensions";
1
+ import { Navigate } from "react-router";
2
2
 
3
3
  import messages from "@/i18n/en-US";
4
4
 
@@ -7,5 +7,5 @@ export function meta() {
7
7
  }
8
8
 
9
9
  export default function ExtensionsRoute() {
10
- return <ExtensionsListPage />;
10
+ return <Navigate to="/settings#extensions" replace />;
11
11
  }
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: improved
3
+ date: 2026-07-23
4
+ ---
5
+
6
+ Agent settings are clearly labeled Manage agent in the sidebar.
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: improved
3
+ date: 2026-07-23
4
+ ---
5
+
6
+ Forms can branch into follow-up questions and route responses to configured destinations.
@@ -6,6 +6,7 @@ functions = "templates/forms/.netlify/functions-internal"
6
6
 
7
7
  [build.environment]
8
8
  GA_MEASUREMENT_ID = "G-ESF7FYXGN9"
9
+ GTM_CONTAINER_ID = "GTM-N3WSTXZ"
9
10
  NITRO_PRESET = "netlify"
10
11
  NPM_CONFIG_PRODUCTION = "false"
11
12
 
@@ -17,6 +17,10 @@ import {
17
17
  } from "h3";
18
18
  import { nanoid } from "nanoid";
19
19
 
20
+ import {
21
+ isConditionalFieldVisible,
22
+ sanitizeConditionalValues as sanitizeVisibleValues,
23
+ } from "../../shared/conditional.js";
20
24
  import {
21
25
  cleanSubmitterEmail,
22
26
  publicSubmitterEmail,
@@ -158,43 +162,26 @@ export const submitForm = defineEventHandler(async (event: H3Event) => {
158
162
  // Parse form fields and build whitelist of valid field IDs
159
163
  const fields: FormField[] = JSON.parse(form.fields);
160
164
  const fieldMap = new Map(fields.map((f) => [f.id, f]));
161
- const rawData =
165
+ const submittedData =
162
166
  body.data && typeof body.data === "object" && !Array.isArray(body.data)
163
167
  ? (body.data as Record<string, unknown>)
164
168
  : {};
165
169
 
166
170
  // Whitelist: only accept keys matching form field IDs
167
- const data: Record<string, unknown> = {};
168
- for (const [key, value] of Object.entries(rawData)) {
171
+ const whitelistedData: Record<string, unknown> = {};
172
+ for (const [key, value] of Object.entries(submittedData)) {
169
173
  const field = fieldMap.get(key);
170
174
  if (!field) continue; // Strip unknown fields
171
- data[key] = value;
175
+ whitelistedData[key] = value;
172
176
  }
173
177
 
178
+ const data = sanitizeVisibleValues(fields, whitelistedData);
179
+
174
180
  // Validate required fields and field-specific constraints. Recompute
175
181
  // conditional visibility on the server so direct POSTs cannot submit hidden
176
182
  // field values or bypass client-side validation.
177
- function isFieldVisible(field: FormField): boolean {
178
- if (!field.conditional) return true;
179
- const { fieldId, operator, value: condValue } = field.conditional;
180
- const fieldVal = String(data[fieldId] ?? "");
181
- switch (operator) {
182
- case "equals":
183
- return fieldVal === condValue;
184
- case "not_equals":
185
- return fieldVal !== condValue;
186
- case "contains":
187
- return fieldVal.includes(condValue);
188
- default:
189
- return true;
190
- }
191
- }
192
-
193
183
  for (const field of fields) {
194
- if (!isFieldVisible(field)) {
195
- delete data[field.id];
196
- continue;
197
- }
184
+ if (field.conditional && !isConditionalFieldVisible(field, data)) continue;
198
185
 
199
186
  const val = data[field.id];
200
187
  if (field.required && isEmptySubmissionValue(val)) {
@@ -268,13 +255,17 @@ export const submitForm = defineEventHandler(async (event: H3Event) => {
268
255
 
269
256
  if (settings.emailOnNewResponses === true && form.ownerEmail) {
270
257
  try {
271
- await sendNewResponseEmail({
272
- to: form.ownerEmail,
273
- formTitle: form.title,
274
- fields,
275
- data,
276
- submittedAt: now,
277
- });
258
+ await runWithRequestContext(
259
+ { userEmail: form.ownerEmail, orgId: form.orgId ?? undefined },
260
+ () =>
261
+ sendNewResponseEmail({
262
+ to: form.ownerEmail!,
263
+ formTitle: form.title,
264
+ fields,
265
+ data,
266
+ submittedAt: now,
267
+ }),
268
+ );
278
269
  } catch (error) {
279
270
  // Email is best-effort — a provider outage must not reject a public
280
271
  // submission that was already persisted successfully.
@@ -118,9 +118,14 @@ function formatFields(
118
118
  data: Record<string, unknown>,
119
119
  ): Record<string, unknown> {
120
120
  const out: Record<string, unknown> = {};
121
+ const usedLabels = new Set<string>();
121
122
  for (const field of fields) {
122
123
  if (data[field.id] !== undefined) {
123
- out[field.label] = data[field.id];
124
+ const label = field.label.trim() || field.id;
125
+ let key = label;
126
+ if (usedLabels.has(key)) key = `${label} (${field.id})`;
127
+ usedLabels.add(key);
128
+ out[key] = data[field.id];
124
129
  }
125
130
  }
126
131
  return out;
@@ -258,9 +263,13 @@ function buildDiscordPayload(submission: SubmissionPayload) {
258
263
  }
259
264
 
260
265
  /** Google Sheets (Apps Script web app) — flat key/value pairs */
261
- function buildGoogleSheetsPayload(submission: SubmissionPayload) {
266
+ export function buildGoogleSheetsPayload(submission: SubmissionPayload) {
262
267
  return {
268
+ event: "form_submission",
269
+ eventVersion: 1,
270
+ formId: submission.formId,
263
271
  formTitle: submission.formTitle,
272
+ responseId: submission.responseId,
264
273
  submittedAt: submission.submittedAt,
265
274
  submitterEmail: publicSubmitterEmail(submission.submitterEmail) ?? "",
266
275
  chatSessionIds: (submission.chatSessionIds ?? []).join(", "),