@agent-native/core 0.90.11 → 0.91.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 (322) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +34 -0
  3. package/corpus/core/docs/content/toolkit-settings.mdx +56 -3
  4. package/corpus/core/package.json +2 -1
  5. package/corpus/core/src/agent/run-ownership.ts +29 -1
  6. package/corpus/core/src/changelog/parse.ts +78 -0
  7. package/corpus/core/src/chat-threads/schema.ts +43 -0
  8. package/corpus/core/src/chat-threads/store.ts +144 -12
  9. package/corpus/core/src/cli/design-connect.ts +53 -9
  10. package/corpus/core/src/cli/workspace-dev.ts +25 -5
  11. package/corpus/core/src/client/AgentPanel.tsx +79 -14
  12. package/corpus/core/src/client/AssistantChat.tsx +359 -425
  13. package/corpus/core/src/client/analytics-session.ts +70 -0
  14. package/corpus/core/src/client/analytics.ts +175 -49
  15. package/corpus/core/src/client/chat/message-components.tsx +22 -2
  16. package/corpus/core/src/client/components/ui/message-scroller.tsx +131 -0
  17. package/corpus/core/src/client/conversation/AgentConversation.tsx +63 -41
  18. package/corpus/core/src/client/error-capture.ts +616 -0
  19. package/corpus/core/src/client/index.ts +12 -0
  20. package/corpus/core/src/client/session-replay.ts +57 -7
  21. package/corpus/core/src/client/settings/SettingsPanel.tsx +123 -1
  22. package/corpus/core/src/client/settings/SettingsTabsPage.tsx +292 -45
  23. package/corpus/core/src/client/settings/index.ts +1 -0
  24. package/corpus/core/src/client/sharing/ShareButton.tsx +14 -11
  25. package/corpus/core/src/client/sharing/ShareDialog.tsx +13 -10
  26. package/corpus/core/src/mcp/builtin-tools.ts +29 -2
  27. package/corpus/core/src/server/agent-chat-plugin.ts +174 -79
  28. package/corpus/core/src/server/onboarding-html.ts +28 -16
  29. package/corpus/core/src/sharing/actions/list-resource-shares.ts +45 -1
  30. package/corpus/core/src/sharing/actions/share-resource.ts +9 -0
  31. package/corpus/core/src/templates/workspace-core/.agents/skills/agent-native-toolkit/SKILL.md +4 -2
  32. package/corpus/core/src/templates/workspace-core/.agents/skills/changelog/SKILL.md +9 -6
  33. package/corpus/core/src/vite/client.ts +217 -35
  34. package/corpus/templates/analytics/.agents/skills/adhoc-analysis/SKILL.md +8 -7
  35. package/corpus/templates/analytics/.agents/skills/analysis-workspace/SKILL.md +14 -10
  36. package/corpus/templates/analytics/.agents/skills/bigquery/SKILL.md +15 -15
  37. package/corpus/templates/analytics/.agents/skills/cross-source-analysis/SKILL.md +1 -0
  38. package/corpus/templates/analytics/.agents/skills/dashboard-management/SKILL.md +38 -22
  39. package/corpus/templates/analytics/.agents/skills/data-programs/SKILL.md +114 -61
  40. package/corpus/templates/analytics/.agents/skills/data-querying/SKILL.md +1 -0
  41. package/corpus/templates/analytics/.agents/skills/gong/SKILL.md +7 -7
  42. package/corpus/templates/analytics/.agents/skills/hubspot/SKILL.md +1 -0
  43. package/corpus/templates/analytics/.agents/skills/prometheus/SKILL.md +1 -1
  44. package/corpus/templates/analytics/.builder/skills/bigquery/SKILL.md +17 -17
  45. package/corpus/templates/analytics/.builder/skills/gcloud/SKILL.md +5 -5
  46. package/corpus/templates/analytics/.builder/skills/github/SKILL.md +7 -7
  47. package/corpus/templates/analytics/.builder/skills/gong/SKILL.md +12 -12
  48. package/corpus/templates/analytics/.builder/skills/grafana/SKILL.md +7 -7
  49. package/corpus/templates/analytics/.builder/skills/sentry/SKILL.md +7 -7
  50. package/corpus/templates/analytics/.builder/skills/slack/SKILL.md +6 -6
  51. package/corpus/templates/analytics/.builder/skills/stripe/SKILL.md +7 -7
  52. package/corpus/templates/analytics/AGENTS.md +47 -2
  53. package/corpus/templates/analytics/actions/add-status-page-monitor.ts +36 -0
  54. package/corpus/templates/analytics/actions/capture-test-error.ts +33 -0
  55. package/corpus/templates/analytics/actions/delete-monitor.ts +24 -0
  56. package/corpus/templates/analytics/actions/delete-status-page.ts +23 -0
  57. package/corpus/templates/analytics/actions/get-error-issue.ts +36 -0
  58. package/corpus/templates/analytics/actions/get-monitor-stats.ts +55 -0
  59. package/corpus/templates/analytics/actions/get-monitor.ts +27 -0
  60. package/corpus/templates/analytics/actions/get-public-status-page.ts +41 -0
  61. package/corpus/templates/analytics/actions/get-status-page.ts +29 -0
  62. package/corpus/templates/analytics/actions/list-error-issues.ts +40 -0
  63. package/corpus/templates/analytics/actions/list-monitors.ts +21 -0
  64. package/corpus/templates/analytics/actions/list-status-pages.ts +21 -0
  65. package/corpus/templates/analytics/actions/match-error-issues.ts +51 -0
  66. package/corpus/templates/analytics/actions/navigate.ts +58 -4
  67. package/corpus/templates/analytics/actions/remove-status-page-monitor.ts +22 -0
  68. package/corpus/templates/analytics/actions/reorder-status-page-monitors.ts +25 -0
  69. package/corpus/templates/analytics/actions/resolve-error-issue.ts +38 -0
  70. package/corpus/templates/analytics/actions/run-monitor-check.ts +23 -0
  71. package/corpus/templates/analytics/actions/save-analysis.ts +1 -0
  72. package/corpus/templates/analytics/actions/save-monitor.ts +145 -0
  73. package/corpus/templates/analytics/actions/save-status-page.ts +47 -0
  74. package/corpus/templates/analytics/actions/view-screen.ts +212 -0
  75. package/corpus/templates/analytics/app/components/layout/Header.tsx +1 -0
  76. package/corpus/templates/analytics/app/components/layout/Layout.tsx +10 -1
  77. package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +134 -39
  78. package/corpus/templates/analytics/app/components/monitoring/PublicStatusView.tsx +351 -0
  79. package/corpus/templates/analytics/app/components/monitoring/ResponseTimeChart.tsx +174 -0
  80. package/corpus/templates/analytics/app/components/monitoring/UptimeStatCards.tsx +85 -0
  81. package/corpus/templates/analytics/app/components/monitoring/UptimeTimelineBars.tsx +106 -0
  82. package/corpus/templates/analytics/app/components/monitoring/chart-utils.ts +101 -0
  83. package/corpus/templates/analytics/app/components/monitoring/index.ts +28 -0
  84. package/corpus/templates/analytics/app/components/monitoring/types.ts +42 -0
  85. package/corpus/templates/analytics/app/hooks/use-navigation-state.ts +34 -0
  86. package/corpus/templates/analytics/app/i18n/zh-TW.ts +6 -0
  87. package/corpus/templates/analytics/app/i18n-data.ts +33 -0
  88. package/corpus/templates/analytics/app/pages/DataSources.tsx +28 -0
  89. package/corpus/templates/analytics/app/pages/Settings.tsx +77 -9
  90. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/SqlChartCard.tsx +3 -3
  91. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/dashboard-layout.ts +7 -0
  92. package/corpus/templates/analytics/app/pages/monitoring/ErrorsPanel.tsx +205 -0
  93. package/corpus/templates/analytics/app/pages/monitoring/MonitoringPage.tsx +100 -0
  94. package/corpus/templates/analytics/app/pages/monitoring/UptimePanel.tsx +475 -0
  95. package/corpus/templates/analytics/app/pages/monitoring/errors/IssueDetail.tsx +372 -0
  96. package/corpus/templates/analytics/app/pages/monitoring/errors/IssueList.tsx +360 -0
  97. package/corpus/templates/analytics/app/pages/monitoring/errors/Sparkline.tsx +55 -0
  98. package/corpus/templates/analytics/app/pages/monitoring/errors/i18n.ts +106 -0
  99. package/corpus/templates/analytics/app/pages/monitoring/errors/types.ts +75 -0
  100. package/corpus/templates/analytics/app/pages/monitoring/errors/utils.ts +132 -0
  101. package/corpus/templates/analytics/app/pages/monitoring/uptime/MonitorDetail.tsx +535 -0
  102. package/corpus/templates/analytics/app/pages/monitoring/uptime/MonitorFormPage.tsx +1026 -0
  103. package/corpus/templates/analytics/app/pages/monitoring/uptime/MonitorList.tsx +311 -0
  104. package/corpus/templates/analytics/app/pages/monitoring/uptime/i18n.ts +235 -0
  105. package/corpus/templates/analytics/app/pages/monitoring/uptime/status-pages/StatusPageEditor.tsx +661 -0
  106. package/corpus/templates/analytics/app/pages/monitoring/uptime/status-pages/StatusPagesView.tsx +355 -0
  107. package/corpus/templates/analytics/app/pages/monitoring/uptime/status-pages/i18n.ts +106 -0
  108. package/corpus/templates/analytics/app/pages/monitoring/uptime/status-pages/slug.ts +24 -0
  109. package/corpus/templates/analytics/app/pages/monitoring/uptime/status-pages/types.ts +20 -0
  110. package/corpus/templates/analytics/app/pages/monitoring/uptime/status-summary.ts +87 -0
  111. package/corpus/templates/analytics/app/pages/monitoring/uptime/types.ts +161 -0
  112. package/corpus/templates/analytics/app/pages/monitoring/uptime/utils.ts +210 -0
  113. package/corpus/templates/analytics/app/pages/sessions/SessionDetailPage.tsx +76 -1
  114. package/corpus/templates/analytics/app/pages/sessions/SessionDevToolsPanel.tsx +74 -9
  115. package/corpus/templates/analytics/app/pages/sessions/SessionsPage.tsx +197 -20
  116. package/corpus/templates/analytics/app/root.tsx +33 -1
  117. package/corpus/templates/analytics/app/routes/monitoring._index.tsx +10 -0
  118. package/corpus/templates/analytics/app/routes/status.$slug.tsx +118 -0
  119. package/corpus/templates/analytics/changelog/2026-07-08-add-uptime-monitors-that-ping-your-urls-and-alert-you-when-t.md +6 -0
  120. package/corpus/templates/analytics/changelog/2026-07-08-analytics-chats-keep-large-data-dictionaries-compact-so-prov.md +6 -0
  121. package/corpus/templates/analytics/changelog/2026-07-08-create-public-status-pages-that-share-the-live-health-of-cho.md +6 -0
  122. package/corpus/templates/analytics/changelog/2026-07-08-dashboard-extension-panels-use-clearer-open-copy.md +6 -0
  123. package/corpus/templates/analytics/changelog/2026-07-08-error-capture.md +6 -0
  124. package/corpus/templates/analytics/changelog/2026-07-08-fixed-single-panel-dashboard-drags-so-they-no-longer-rewrite.md +6 -0
  125. package/corpus/templates/analytics/changelog/2026-07-08-jump-straight-from-an-error-in-a-session-recording-to-its-fu.md +6 -0
  126. package/corpus/templates/analytics/changelog/2026-07-08-monitor-detail-and-list-now-show-colorful-uptime-timelines-r.md +6 -0
  127. package/corpus/templates/analytics/changelog/2026-07-08-pinned-ask-chats-now-show-a-pin-icon-in-the-sidebar.md +6 -0
  128. package/corpus/templates/analytics/changelog/2026-07-08-publish-a-public-uptime-status-page-with-colorful-uptime-tim.md +6 -0
  129. package/corpus/templates/analytics/changelog/2026-07-08-sessions-now-hide-visitor-emails-in-demo-mode-and-show-clean.md +6 -0
  130. package/corpus/templates/analytics/changelog/2026-07-08-settings-are-cleaner-searchable-and-alerts-have-their-own-tab.md +5 -0
  131. package/corpus/templates/analytics/changelog/2026-07-08-sidebar-sections-stay-collapsed-until-opened.md +5 -0
  132. package/corpus/templates/analytics/changelog/2026-07-08-the-uptime-monitor-list-now-has-a-current-status-overview-wi.md +6 -0
  133. package/corpus/templates/analytics/docs/error-capture.md +172 -0
  134. package/corpus/templates/analytics/docs/uptime-monitoring.md +150 -0
  135. package/corpus/templates/analytics/server/db/schema-errors.ts +150 -0
  136. package/corpus/templates/analytics/server/db/schema-monitoring.ts +177 -0
  137. package/corpus/templates/analytics/server/db/schema.ts +6 -0
  138. package/corpus/templates/analytics/server/jobs/uptime-monitors.ts +123 -0
  139. package/corpus/templates/analytics/server/lib/agent-chat-plan-mode.ts +41 -0
  140. package/corpus/templates/analytics/server/lib/data-dictionary-context.ts +55 -12
  141. package/corpus/templates/analytics/server/lib/error-capture.ts +1331 -0
  142. package/corpus/templates/analytics/server/lib/first-party-analytics.ts +47 -1
  143. package/corpus/templates/analytics/server/lib/monitor-stats.ts +494 -0
  144. package/corpus/templates/analytics/server/lib/status-pages.ts +798 -0
  145. package/corpus/templates/analytics/server/lib/uptime-monitors.ts +1665 -0
  146. package/corpus/templates/analytics/server/plugins/agent-chat.ts +7 -37
  147. package/corpus/templates/analytics/server/plugins/auth.ts +11 -1
  148. package/corpus/templates/analytics/server/plugins/db.ts +408 -0
  149. package/corpus/templates/analytics/server/plugins/uptime-monitor-jobs.ts +63 -0
  150. package/corpus/templates/assets/app/routes/_index.tsx +1 -1
  151. package/corpus/templates/assets/app/routes/settings.tsx +22 -1
  152. package/corpus/templates/assets/changelog/2026-07-08-recent-drafts-keep-comfortable-side-padding.md +6 -0
  153. package/corpus/templates/assets/changelog/2026-07-08-settings-are-cleaner-and-searchable.md +5 -0
  154. package/corpus/templates/brain/app/routes/settings.tsx +43 -5
  155. package/corpus/templates/brain/changelog/2026-07-08-settings-are-cleaner-and-searchable.md +5 -0
  156. package/corpus/templates/calendar/app/pages/Settings.tsx +44 -6
  157. package/corpus/templates/calendar/changelog/2026-07-08-settings-are-cleaner-and-searchable.md +5 -0
  158. package/corpus/templates/chat/app/routes/settings.tsx +16 -1
  159. package/corpus/templates/chat/changelog/2026-07-08-settings-are-cleaner-and-searchable.md +5 -0
  160. package/corpus/templates/clips/actions/lib/audio-only-transcription.ts +79 -37
  161. package/corpus/templates/clips/app/components/player/player-controls.tsx +35 -0
  162. package/corpus/templates/clips/app/components/player/video-player.tsx +162 -18
  163. package/corpus/templates/clips/app/routes/_app.settings._index.tsx +63 -6
  164. package/corpus/templates/clips/app/routes/record.tsx +18 -1
  165. package/corpus/templates/clips/changelog/2026-07-07-shared-clips-start-playback-reliably-from-the-first-play-click.md +6 -0
  166. package/corpus/templates/clips/changelog/2026-07-08-chrome-extension-camera-and-microphone-menus-now-show-which-.md +6 -0
  167. package/corpus/templates/clips/changelog/2026-07-08-chrome-extension-copies-new-clip-links-after-saving.md +6 -0
  168. package/corpus/templates/clips/changelog/2026-07-08-chrome-extension-recordings-now-continue-after-granting-came.md +6 -0
  169. package/corpus/templates/clips/changelog/2026-07-08-mobile-shared-clips-now-have-loom-style-tap-and-skip-controls.md +6 -0
  170. package/corpus/templates/clips/changelog/2026-07-08-recordings-and-uploads-copy-links-after-saving.md +6 -0
  171. package/corpus/templates/clips/changelog/2026-07-08-settings-are-cleaner-searchable-and-collapse-s3-with-builder.md +5 -0
  172. package/corpus/templates/clips/changelog/2026-07-08-transcripts-are-more-reliable-for-longer-desktop-recordings-.md +6 -0
  173. package/corpus/templates/clips/chrome-extension/public/manifest.json +1 -0
  174. package/corpus/templates/clips/chrome-extension/src/background.ts +162 -13
  175. package/corpus/templates/clips/chrome-extension/src/content-script.ts +14 -0
  176. package/corpus/templates/clips/chrome-extension/src/offscreen.ts +36 -0
  177. package/corpus/templates/clips/chrome-extension/src/overlay.ts +99 -25
  178. package/corpus/templates/clips/chrome-extension/src/permission.ts +63 -14
  179. package/corpus/templates/clips/chrome-extension/src/popup.ts +123 -23
  180. package/corpus/templates/content/actions/_builder-cms-read-client.ts +47 -0
  181. package/corpus/templates/content/actions/_database-source-utils.ts +33 -4
  182. package/corpus/templates/content/actions/add-content-database-source-field-property.ts +214 -29
  183. package/corpus/templates/content/actions/attach-content-database-source.ts +3 -0
  184. package/corpus/templates/content/actions/change-content-database-source-role.ts +1 -0
  185. package/corpus/templates/content/app/components/editor/DocumentProperties.tsx +42 -32
  186. package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +352 -81
  187. package/corpus/templates/content/app/components/editor/database/settings.tsx +49 -24
  188. package/corpus/templates/content/app/routes/_app.settings.tsx +19 -1
  189. package/corpus/templates/content/changelog/2026-07-07-builder-source-tag-fields-now-import-as-multi-select.md +6 -0
  190. package/corpus/templates/content/changelog/2026-07-08-settings-are-cleaner-and-searchable.md +5 -0
  191. package/corpus/templates/content/shared/api.ts +3 -0
  192. package/corpus/templates/content/shared/properties.ts +42 -0
  193. package/corpus/templates/design/AGENTS.md +9 -0
  194. package/corpus/templates/design/actions/generate-design.ts +3 -0
  195. package/corpus/templates/design/actions/get-design-system.ts +147 -1
  196. package/corpus/templates/design/actions/request-localhost-write-consent.ts +141 -0
  197. package/corpus/templates/design/app/components/design/DesignCanvas.tsx +13 -19
  198. package/corpus/templates/design/app/routes/settings.tsx +16 -1
  199. package/corpus/templates/design/changelog/2026-07-08-design-generation-now-follows-hydrated-builder-design-system.md +6 -0
  200. package/corpus/templates/design/changelog/2026-07-08-settings-are-cleaner-and-searchable.md +5 -0
  201. package/corpus/templates/design/server/lib/verify-write-grant.ts +4 -2
  202. package/corpus/templates/dispatch/app/routes/settings.tsx +23 -2
  203. package/corpus/templates/dispatch/changelog/2026-07-08-settings-are-cleaner-and-searchable.md +5 -0
  204. package/corpus/templates/forms/app/routes/_app.settings.tsx +16 -1
  205. package/corpus/templates/forms/changelog/2026-07-08-settings-are-cleaner-and-searchable.md +5 -0
  206. package/corpus/templates/macros/app/routes/settings.tsx +16 -1
  207. package/corpus/templates/macros/changelog/2026-07-08-settings-are-cleaner-and-searchable.md +5 -0
  208. package/corpus/templates/mail/app/components/settings/GmailFiltersSection.tsx +1 -1
  209. package/corpus/templates/mail/app/components/settings/SnippetsSection.tsx +1 -1
  210. package/corpus/templates/mail/app/pages/SettingsPage.tsx +123 -105
  211. package/corpus/templates/mail/changelog/2026-07-08-settings-redesigned-with-a-consistent-searchable-layout.md +5 -0
  212. package/corpus/templates/plan/app/routes/settings.tsx +23 -2
  213. package/corpus/templates/plan/changelog/2026-07-08-settings-are-cleaner-and-searchable.md +5 -0
  214. package/corpus/templates/slides/actions/add-slide.ts +1 -0
  215. package/corpus/templates/slides/actions/get-design-system.ts +147 -1
  216. package/corpus/templates/slides/app/pages/Index.tsx +46 -1
  217. package/corpus/templates/slides/app/routes/settings.tsx +16 -1
  218. package/corpus/templates/slides/changelog/2026-07-08-deck-generation-now-follows-hydrated-builder-design-system-g.md +6 -0
  219. package/corpus/templates/slides/changelog/2026-07-08-settings-are-cleaner-and-searchable.md +5 -0
  220. package/dist/agent/run-ownership.d.ts +6 -0
  221. package/dist/agent/run-ownership.d.ts.map +1 -1
  222. package/dist/agent/run-ownership.js +11 -1
  223. package/dist/agent/run-ownership.js.map +1 -1
  224. package/dist/changelog/parse.d.ts +7 -0
  225. package/dist/changelog/parse.d.ts.map +1 -1
  226. package/dist/changelog/parse.js +64 -0
  227. package/dist/changelog/parse.js.map +1 -1
  228. package/dist/chat-threads/schema.d.ts +445 -0
  229. package/dist/chat-threads/schema.d.ts.map +1 -0
  230. package/dist/chat-threads/schema.js +39 -0
  231. package/dist/chat-threads/schema.js.map +1 -0
  232. package/dist/chat-threads/store.d.ts +12 -0
  233. package/dist/chat-threads/store.d.ts.map +1 -1
  234. package/dist/chat-threads/store.js +99 -10
  235. package/dist/chat-threads/store.js.map +1 -1
  236. package/dist/cli/design-connect.d.ts.map +1 -1
  237. package/dist/cli/design-connect.js +40 -10
  238. package/dist/cli/design-connect.js.map +1 -1
  239. package/dist/cli/workspace-dev.d.ts.map +1 -1
  240. package/dist/cli/workspace-dev.js +20 -5
  241. package/dist/cli/workspace-dev.js.map +1 -1
  242. package/dist/client/AgentPanel.d.ts.map +1 -1
  243. package/dist/client/AgentPanel.js +35 -7
  244. package/dist/client/AgentPanel.js.map +1 -1
  245. package/dist/client/AssistantChat.d.ts.map +1 -1
  246. package/dist/client/AssistantChat.js +105 -183
  247. package/dist/client/AssistantChat.js.map +1 -1
  248. package/dist/client/analytics-session.d.ts +3 -0
  249. package/dist/client/analytics-session.d.ts.map +1 -0
  250. package/dist/client/analytics-session.js +66 -0
  251. package/dist/client/analytics-session.js.map +1 -0
  252. package/dist/client/analytics.d.ts +32 -0
  253. package/dist/client/analytics.d.ts.map +1 -1
  254. package/dist/client/analytics.js +116 -46
  255. package/dist/client/analytics.js.map +1 -1
  256. package/dist/client/chat/message-components.d.ts +2 -1
  257. package/dist/client/chat/message-components.d.ts.map +1 -1
  258. package/dist/client/chat/message-components.js +16 -6
  259. package/dist/client/chat/message-components.js.map +1 -1
  260. package/dist/client/components/ui/message-scroller.d.ts +16 -0
  261. package/dist/client/components/ui/message-scroller.d.ts.map +1 -0
  262. package/dist/client/components/ui/message-scroller.js +25 -0
  263. package/dist/client/components/ui/message-scroller.js.map +1 -0
  264. package/dist/client/conversation/AgentConversation.d.ts +1 -1
  265. package/dist/client/conversation/AgentConversation.d.ts.map +1 -1
  266. package/dist/client/conversation/AgentConversation.js +3 -8
  267. package/dist/client/conversation/AgentConversation.js.map +1 -1
  268. package/dist/client/error-capture.d.ts +99 -0
  269. package/dist/client/error-capture.d.ts.map +1 -0
  270. package/dist/client/error-capture.js +437 -0
  271. package/dist/client/error-capture.js.map +1 -0
  272. package/dist/client/index.d.ts +2 -2
  273. package/dist/client/index.d.ts.map +1 -1
  274. package/dist/client/index.js +3 -1
  275. package/dist/client/index.js.map +1 -1
  276. package/dist/client/session-replay.d.ts +22 -0
  277. package/dist/client/session-replay.d.ts.map +1 -1
  278. package/dist/client/session-replay.js +45 -4
  279. package/dist/client/session-replay.js.map +1 -1
  280. package/dist/client/settings/SettingsPanel.d.ts.map +1 -1
  281. package/dist/client/settings/SettingsPanel.js +100 -5
  282. package/dist/client/settings/SettingsPanel.js.map +1 -1
  283. package/dist/client/settings/SettingsTabsPage.d.ts +49 -1
  284. package/dist/client/settings/SettingsTabsPage.d.ts.map +1 -1
  285. package/dist/client/settings/SettingsTabsPage.js +131 -19
  286. package/dist/client/settings/SettingsTabsPage.js.map +1 -1
  287. package/dist/client/settings/index.d.ts +1 -1
  288. package/dist/client/settings/index.d.ts.map +1 -1
  289. package/dist/client/settings/index.js.map +1 -1
  290. package/dist/client/sharing/ShareButton.d.ts.map +1 -1
  291. package/dist/client/sharing/ShareButton.js +13 -8
  292. package/dist/client/sharing/ShareButton.js.map +1 -1
  293. package/dist/client/sharing/ShareDialog.d.ts.map +1 -1
  294. package/dist/client/sharing/ShareDialog.js +12 -7
  295. package/dist/client/sharing/ShareDialog.js.map +1 -1
  296. package/dist/collab/routes.d.ts +1 -1
  297. package/dist/file-upload/actions/upload-image.d.ts +1 -1
  298. package/dist/mcp/builtin-tools.d.ts.map +1 -1
  299. package/dist/mcp/builtin-tools.js +21 -2
  300. package/dist/mcp/builtin-tools.js.map +1 -1
  301. package/dist/observability/routes.d.ts +1 -1
  302. package/dist/org/schema.d.ts +15 -15
  303. package/dist/provider-api/corpus-jobs.d.ts +2 -2
  304. package/dist/server/agent-chat-plugin.d.ts.map +1 -1
  305. package/dist/server/agent-chat-plugin.js +109 -73
  306. package/dist/server/agent-chat-plugin.js.map +1 -1
  307. package/dist/server/onboarding-html.d.ts.map +1 -1
  308. package/dist/server/onboarding-html.js +28 -16
  309. package/dist/server/onboarding-html.js.map +1 -1
  310. package/dist/sharing/actions/list-resource-shares.js +30 -1
  311. package/dist/sharing/actions/list-resource-shares.js.map +1 -1
  312. package/dist/sharing/actions/share-resource.js +6 -0
  313. package/dist/sharing/actions/share-resource.js.map +1 -1
  314. package/dist/templates/workspace-core/.agents/skills/agent-native-toolkit/SKILL.md +4 -2
  315. package/dist/templates/workspace-core/.agents/skills/changelog/SKILL.md +9 -6
  316. package/dist/vite/client.d.ts.map +1 -1
  317. package/dist/vite/client.js +192 -30
  318. package/dist/vite/client.js.map +1 -1
  319. package/docs/content/toolkit-settings.mdx +56 -3
  320. package/package.json +2 -1
  321. package/src/templates/workspace-core/.agents/skills/agent-native-toolkit/SKILL.md +4 -2
  322. package/src/templates/workspace-core/.agents/skills/changelog/SKILL.md +9 -6
@@ -0,0 +1,1026 @@
1
+ /**
2
+ * Full-page create / edit form for an uptime monitor. Renders inside the
3
+ * Monitoring → Uptime panel (not a modal), driven by query params so it is
4
+ * deep-linkable and back-button friendly.
5
+ *
6
+ * Progressive disclosure: only Name, URL, and Check interval are always
7
+ * visible. Response checks, Alerting, and Advanced live in collapsed sections
8
+ * with smart defaults, so a working monitor needs just a name + URL.
9
+ */
10
+ import { useActionMutation, useActionQuery } from "@agent-native/core/client";
11
+ import {
12
+ IconArrowLeft,
13
+ IconBell,
14
+ IconChevronDown,
15
+ IconLoader2,
16
+ IconPlus,
17
+ IconSettings,
18
+ IconShieldCheck,
19
+ IconTrash,
20
+ } from "@tabler/icons-react";
21
+ import { useEffect, useMemo, useRef, useState } from "react";
22
+ import { Controller, useFieldArray, useForm } from "react-hook-form";
23
+ import { toast } from "sonner";
24
+
25
+ import { Button } from "@/components/ui/button";
26
+ import { Checkbox } from "@/components/ui/checkbox";
27
+ import {
28
+ Collapsible,
29
+ CollapsibleContent,
30
+ CollapsibleTrigger,
31
+ } from "@/components/ui/collapsible";
32
+ import { Input } from "@/components/ui/input";
33
+ import { Label } from "@/components/ui/label";
34
+ import {
35
+ Select,
36
+ SelectContent,
37
+ SelectItem,
38
+ SelectTrigger,
39
+ SelectValue,
40
+ } from "@/components/ui/select";
41
+ import { Skeleton } from "@/components/ui/skeleton";
42
+ import { Switch } from "@/components/ui/switch";
43
+ import { Textarea } from "@/components/ui/textarea";
44
+ import { cn } from "@/lib/utils";
45
+
46
+ import { fmt, useUptimeT } from "./i18n";
47
+ import type {
48
+ AssertionType,
49
+ MonitorDetail,
50
+ MonitorMethod,
51
+ MonitorSeverity,
52
+ MonitorSummary,
53
+ SaveMonitorInput,
54
+ StatusMatcher,
55
+ } from "./types";
56
+ import { deriveMonitorName, describeMatcher, isHttpUrl } from "./utils";
57
+
58
+ const METHODS: MonitorMethod[] = [
59
+ "GET",
60
+ "HEAD",
61
+ "POST",
62
+ "PUT",
63
+ "PATCH",
64
+ "DELETE",
65
+ "OPTIONS",
66
+ ];
67
+
68
+ const INTERVAL_OPTIONS = [
69
+ 30, 60, 300, 600, 900, 1800, 3600, 21600, 86400,
70
+ ] as const;
71
+
72
+ const STATUS_CLASSES = ["2xx", "3xx", "4xx", "5xx"] as const;
73
+ const KNOWN_CHANNELS = ["inbox", "email", "slack", "webhook"] as const;
74
+ const ASSERTION_TYPES: AssertionType[] = [
75
+ "body_contains",
76
+ "body_absent",
77
+ "header_contains",
78
+ "header_equals",
79
+ "max_latency_ms",
80
+ ];
81
+
82
+ interface AssertionFieldValue {
83
+ type: AssertionType;
84
+ value: string;
85
+ header: string;
86
+ }
87
+
88
+ interface MonitorFormValues {
89
+ name: string;
90
+ url: string;
91
+ method: MonitorMethod;
92
+ intervalSeconds: number;
93
+ timeoutSeconds: number;
94
+ matcherMode: "class" | "list" | "range";
95
+ classes: Record<(typeof STATUS_CLASSES)[number], boolean>;
96
+ codes: string;
97
+ rangeMin: number;
98
+ rangeMax: number;
99
+ assertions: AssertionFieldValue[];
100
+ followRedirects: boolean;
101
+ severity: MonitorSeverity;
102
+ channels: Record<(typeof KNOWN_CHANNELS)[number], boolean>;
103
+ emailRecipients: string;
104
+ cooldownMinutes: number;
105
+ enabled: boolean;
106
+ }
107
+
108
+ function splitList(value: string): string[] {
109
+ return value
110
+ .split(/[\n,]+/)
111
+ .map((item) => item.trim())
112
+ .filter(Boolean);
113
+ }
114
+
115
+ function defaultValues(monitor: MonitorSummary | null): MonitorFormValues {
116
+ const matcher = monitor?.expectedStatus ?? {
117
+ mode: "class",
118
+ classes: ["2xx"],
119
+ };
120
+ const classFlags = {
121
+ "2xx": false,
122
+ "3xx": false,
123
+ "4xx": false,
124
+ "5xx": false,
125
+ } as Record<(typeof STATUS_CLASSES)[number], boolean>;
126
+ if (matcher.mode === "class") {
127
+ for (const cls of matcher.classes) {
128
+ if (cls in classFlags) {
129
+ classFlags[cls as (typeof STATUS_CLASSES)[number]] = true;
130
+ }
131
+ }
132
+ } else {
133
+ classFlags["2xx"] = true;
134
+ }
135
+
136
+ const channelSet = new Set(monitor?.channels ?? ["inbox"]);
137
+
138
+ return {
139
+ name: monitor?.name ?? "",
140
+ url: monitor?.url ?? "",
141
+ method: monitor?.method ?? "GET",
142
+ intervalSeconds: monitor?.intervalSeconds ?? 300,
143
+ timeoutSeconds: Math.round((monitor?.timeoutMs ?? 15000) / 1000),
144
+ matcherMode: matcher.mode,
145
+ classes: classFlags,
146
+ codes: matcher.mode === "list" ? matcher.codes.join(", ") : "200",
147
+ rangeMin: matcher.mode === "range" ? matcher.min : 200,
148
+ rangeMax: matcher.mode === "range" ? matcher.max : 299,
149
+ assertions: (monitor?.assertions ?? []).map((assertion) => ({
150
+ type: assertion.type,
151
+ value: String(assertion.value),
152
+ header: assertion.header ?? "",
153
+ })),
154
+ followRedirects: monitor?.followRedirects ?? true,
155
+ severity: monitor?.severity ?? "critical",
156
+ channels: {
157
+ inbox: channelSet.has("inbox"),
158
+ email: channelSet.has("email"),
159
+ slack: channelSet.has("slack"),
160
+ webhook: channelSet.has("webhook"),
161
+ },
162
+ emailRecipients: (monitor?.emailRecipients ?? []).join(", "),
163
+ cooldownMinutes: monitor?.cooldownMinutes ?? 15,
164
+ enabled: monitor?.enabled ?? true,
165
+ };
166
+ }
167
+
168
+ function buildMatcher(values: MonitorFormValues): StatusMatcher | undefined {
169
+ if (values.matcherMode === "class") {
170
+ const classes = STATUS_CLASSES.filter((cls) => values.classes[cls]);
171
+ if (classes.length === 0) return undefined;
172
+ return { mode: "class", classes };
173
+ }
174
+ if (values.matcherMode === "list") {
175
+ const codes = splitList(values.codes)
176
+ .map((code) => Number.parseInt(code, 10))
177
+ .filter((code) => Number.isFinite(code));
178
+ if (codes.length === 0) return undefined;
179
+ return { mode: "list", codes };
180
+ }
181
+ return {
182
+ mode: "range",
183
+ min: Math.round(values.rangeMin),
184
+ max: Math.round(values.rangeMax),
185
+ };
186
+ }
187
+
188
+ interface SectionState {
189
+ response: boolean;
190
+ alerting: boolean;
191
+ advanced: boolean;
192
+ }
193
+
194
+ /** Open a section by default when the monitor already customized it. */
195
+ function initialSections(monitor: MonitorSummary | null): SectionState {
196
+ if (!monitor) return { response: false, alerting: false, advanced: false };
197
+ const matcher = monitor.expectedStatus;
198
+ const isDefaultMatcher =
199
+ matcher.mode === "class" &&
200
+ matcher.classes.length === 1 &&
201
+ matcher.classes[0] === "2xx";
202
+ const isDefaultChannels =
203
+ monitor.channels.length === 1 && monitor.channels[0] === "inbox";
204
+ return {
205
+ response: monitor.assertions.length > 0 || !isDefaultMatcher,
206
+ alerting:
207
+ !isDefaultChannels ||
208
+ monitor.emailRecipients.length > 0 ||
209
+ monitor.severity !== "critical",
210
+ advanced:
211
+ monitor.method !== "GET" ||
212
+ monitor.timeoutMs !== 15000 ||
213
+ monitor.cooldownMinutes !== 15 ||
214
+ !monitor.followRedirects ||
215
+ !monitor.enabled,
216
+ };
217
+ }
218
+
219
+ export function MonitorFormPage({
220
+ monitorId,
221
+ initialMonitor,
222
+ onCancel,
223
+ onSaved,
224
+ }: {
225
+ monitorId: string | null;
226
+ initialMonitor?: MonitorSummary | null;
227
+ onCancel: () => void;
228
+ onSaved: (monitor: MonitorSummary) => void;
229
+ }) {
230
+ const t = useUptimeT();
231
+ const isEdit = monitorId != null;
232
+
233
+ const { data: detail, isLoading } = useActionQuery<MonitorDetail>(
234
+ "get-monitor",
235
+ { id: monitorId ?? "" },
236
+ { enabled: isEdit && !initialMonitor, staleTime: 5_000 },
237
+ );
238
+
239
+ const resolved: MonitorSummary | null = isEdit
240
+ ? (initialMonitor ?? detail?.monitor ?? null)
241
+ : null;
242
+
243
+ const saveMonitor = useActionMutation<MonitorSummary, SaveMonitorInput>(
244
+ "save-monitor",
245
+ );
246
+
247
+ const {
248
+ register,
249
+ control,
250
+ handleSubmit,
251
+ reset,
252
+ watch,
253
+ setValue,
254
+ setError,
255
+ formState: { errors },
256
+ } = useForm<MonitorFormValues>({
257
+ defaultValues: defaultValues(initialMonitor ?? null),
258
+ });
259
+
260
+ const { fields, append, remove } = useFieldArray({
261
+ control,
262
+ name: "assertions",
263
+ });
264
+
265
+ const [sections, setSections] = useState<SectionState>(() =>
266
+ initialSections(initialMonitor ?? null),
267
+ );
268
+
269
+ // Name auto-fills from the URL until the user edits it. In edit mode an
270
+ // existing name counts as "touched" so we never clobber it.
271
+ const nameTouchedRef = useRef<boolean>(Boolean(initialMonitor?.name?.trim()));
272
+
273
+ // Prefill exactly once per resolved monitor id so a late get-monitor refetch
274
+ // never clobbers in-progress edits.
275
+ const initedRef = useRef<string | null>(null);
276
+ useEffect(() => {
277
+ if (!isEdit) {
278
+ if (initedRef.current !== "new") {
279
+ reset(defaultValues(null));
280
+ setSections(initialSections(null));
281
+ nameTouchedRef.current = false;
282
+ initedRef.current = "new";
283
+ }
284
+ return;
285
+ }
286
+ if (resolved && initedRef.current !== resolved.id) {
287
+ reset(defaultValues(resolved));
288
+ setSections(initialSections(resolved));
289
+ nameTouchedRef.current = Boolean(resolved.name?.trim());
290
+ initedRef.current = resolved.id;
291
+ }
292
+ }, [isEdit, resolved, reset]);
293
+
294
+ const values = watch();
295
+
296
+ const responseSummary = useMemo(() => {
297
+ const matcher = buildMatcher(values);
298
+ const matcherText = matcher ? describeMatcher(matcher, t) : t.classRequired;
299
+ const count = values.assertions.length;
300
+ const assertionsText =
301
+ count === 0
302
+ ? t.noAssertionsSummary
303
+ : count === 1
304
+ ? t.oneAssertionSummary
305
+ : fmt(t.assertionsCountSummary, { count });
306
+ return `${matcherText} · ${assertionsText}`;
307
+ // eslint-disable-next-line react-hooks/exhaustive-deps
308
+ }, [
309
+ values.matcherMode,
310
+ values.classes,
311
+ values.codes,
312
+ values.rangeMin,
313
+ values.rangeMax,
314
+ values.assertions.length,
315
+ t,
316
+ ]);
317
+
318
+ const alertingSummary = useMemo(() => {
319
+ const selected = KNOWN_CHANNELS.filter((c) => values.channels[c]).map((c) =>
320
+ channelLabel(c, t),
321
+ );
322
+ const channelsText =
323
+ selected.length > 0 ? selected.join(", ") : t.channelsNoneSummary;
324
+ const severityText =
325
+ values.severity === "critical" ? t.severityCritical : t.severityWarning;
326
+ return `${channelsText} · ${severityText}`;
327
+ // eslint-disable-next-line react-hooks/exhaustive-deps
328
+ }, [values.channels, values.severity, t]);
329
+
330
+ const advancedSummary = useMemo(() => {
331
+ const parts = [
332
+ values.method,
333
+ `${values.timeoutSeconds || 15}s`,
334
+ `${values.cooldownMinutes ?? 15}m`,
335
+ ];
336
+ if (!values.enabled) parts.push(t.pausedBadge);
337
+ return parts.join(" · ");
338
+ // eslint-disable-next-line react-hooks/exhaustive-deps
339
+ }, [
340
+ values.method,
341
+ values.timeoutSeconds,
342
+ values.cooldownMinutes,
343
+ values.enabled,
344
+ t,
345
+ ]);
346
+
347
+ const submit = handleSubmit(async (formValues) => {
348
+ const matcher = buildMatcher(formValues);
349
+ if (!matcher) {
350
+ setSections((prev) => ({ ...prev, response: true }));
351
+ if (formValues.matcherMode === "class") {
352
+ setError("classes", { message: t.classRequired });
353
+ } else {
354
+ setError("codes", { message: t.codesRequired });
355
+ }
356
+ return;
357
+ }
358
+
359
+ const channels = KNOWN_CHANNELS.filter((c) => formValues.channels[c]);
360
+ if (channels.length === 0) {
361
+ setSections((prev) => ({ ...prev, alerting: true }));
362
+ setError("channels", { message: t.channelRequired });
363
+ return;
364
+ }
365
+
366
+ const assertions = formValues.assertions
367
+ .map((assertion) => {
368
+ if (assertion.type === "max_latency_ms") {
369
+ const num = Number.parseInt(assertion.value, 10);
370
+ if (!Number.isFinite(num) || num <= 0) return null;
371
+ return { type: assertion.type, value: num };
372
+ }
373
+ const value = assertion.value.trim();
374
+ if (!value) return null;
375
+ if (
376
+ assertion.type === "header_contains" ||
377
+ assertion.type === "header_equals"
378
+ ) {
379
+ const header = assertion.header.trim();
380
+ if (!header) return null;
381
+ return { type: assertion.type, value, header };
382
+ }
383
+ return { type: assertion.type, value };
384
+ })
385
+ .filter((a): a is NonNullable<typeof a> => a !== null);
386
+
387
+ const url = formValues.url.trim();
388
+ const payload: SaveMonitorInput = {
389
+ id: monitorId ?? undefined,
390
+ name: formValues.name.trim() || deriveMonitorName(url),
391
+ url,
392
+ method: formValues.method,
393
+ requestHeaders: resolved?.requestHeaders,
394
+ requestBody: resolved?.requestBody,
395
+ intervalSeconds: formValues.intervalSeconds,
396
+ timeoutMs: Math.max(1000, Math.round(formValues.timeoutSeconds * 1000)),
397
+ expectedStatus: matcher,
398
+ assertions,
399
+ followRedirects: formValues.followRedirects,
400
+ severity: formValues.severity,
401
+ channels,
402
+ emailRecipients: splitList(formValues.emailRecipients),
403
+ cooldownMinutes: formValues.cooldownMinutes,
404
+ enabled: formValues.enabled,
405
+ };
406
+
407
+ try {
408
+ const saved = await saveMonitor.mutateAsync(payload);
409
+ toast.success(t.saved);
410
+ onSaved(saved);
411
+ } catch (err) {
412
+ toast.error(
413
+ fmt(t.saveFailed, {
414
+ message: err instanceof Error ? err.message : String(err),
415
+ }),
416
+ );
417
+ }
418
+ });
419
+
420
+ // Edit deep-link before the monitor resolves.
421
+ if (isEdit && !resolved && isLoading) {
422
+ return (
423
+ <div className="space-y-4">
424
+ <Skeleton className="h-8 w-40" />
425
+ <Skeleton className="h-24 w-full" />
426
+ <Skeleton className="h-40 w-full" />
427
+ </div>
428
+ );
429
+ }
430
+
431
+ const matcherMode = values.matcherMode;
432
+ const saving = saveMonitor.isPending;
433
+
434
+ return (
435
+ <div>
436
+ {/* Header */}
437
+ <div className="mb-6">
438
+ <Button
439
+ variant="ghost"
440
+ size="sm"
441
+ className="-ms-2 mb-2 text-muted-foreground"
442
+ onClick={onCancel}
443
+ >
444
+ <IconArrowLeft className="size-3.5" />
445
+ {t.back}
446
+ </Button>
447
+ <h2 className="text-lg font-semibold">
448
+ {isEdit ? t.formEditTitle : t.formCreateTitle}
449
+ </h2>
450
+ <p className="mt-0.5 text-sm text-muted-foreground">
451
+ {isEdit ? t.formEditSubtitle : t.formCreateSubtitle}
452
+ </p>
453
+ </div>
454
+
455
+ <form id="monitor-form" onSubmit={submit} className="space-y-5">
456
+ {/* Essentials — always visible */}
457
+ <div className="space-y-4">
458
+ <div className="space-y-1.5">
459
+ <Label htmlFor="monitor-url">{t.fieldUrl}</Label>
460
+ <Input
461
+ id="monitor-url"
462
+ placeholder={t.fieldUrlPlaceholder}
463
+ inputMode="url"
464
+ autoFocus
465
+ {...register("url", {
466
+ validate: (value) => {
467
+ if (!value.trim()) return t.urlRequired;
468
+ return isHttpUrl(value.trim()) || t.urlInvalid;
469
+ },
470
+ onChange: (event) => {
471
+ // Auto-fill the name from the URL until the user edits it.
472
+ if (!nameTouchedRef.current) {
473
+ setValue("name", deriveMonitorName(event.target.value), {
474
+ shouldDirty: false,
475
+ });
476
+ }
477
+ },
478
+ })}
479
+ />
480
+ {errors.url ? (
481
+ <p className="text-xs text-destructive">{errors.url.message}</p>
482
+ ) : null}
483
+ </div>
484
+
485
+ <div className="space-y-1.5">
486
+ <Label htmlFor="monitor-name">{t.fieldName}</Label>
487
+ <Input
488
+ id="monitor-name"
489
+ placeholder={t.fieldNamePlaceholder}
490
+ {...register("name", {
491
+ onChange: () => {
492
+ nameTouchedRef.current = true;
493
+ },
494
+ })}
495
+ />
496
+ <p className="text-xs text-muted-foreground">{t.fieldNameHint}</p>
497
+ </div>
498
+
499
+ <div className="space-y-1.5">
500
+ <Label htmlFor="monitor-interval">{t.fieldInterval}</Label>
501
+ <Controller
502
+ control={control}
503
+ name="intervalSeconds"
504
+ render={({ field }) => (
505
+ <Select
506
+ value={String(field.value)}
507
+ onValueChange={(value) =>
508
+ field.onChange(Number.parseInt(value, 10))
509
+ }
510
+ >
511
+ <SelectTrigger id="monitor-interval">
512
+ <SelectValue />
513
+ </SelectTrigger>
514
+ <SelectContent>
515
+ {INTERVAL_OPTIONS.map((seconds) => (
516
+ <SelectItem key={seconds} value={String(seconds)}>
517
+ {t.intervals[String(seconds)] ?? `${seconds}s`}
518
+ </SelectItem>
519
+ ))}
520
+ </SelectContent>
521
+ </Select>
522
+ )}
523
+ />
524
+ </div>
525
+ </div>
526
+
527
+ {/* Response checks */}
528
+ <Section
529
+ icon={<IconShieldCheck className="size-4 text-muted-foreground" />}
530
+ title={t.sectionResponseChecks}
531
+ hint={t.sectionResponseChecksHint}
532
+ summary={responseSummary}
533
+ open={sections.response}
534
+ onOpenChange={(open) =>
535
+ setSections((prev) => ({ ...prev, response: open }))
536
+ }
537
+ >
538
+ <div className="space-y-2">
539
+ <div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
540
+ <Label>{t.fieldExpectedStatus}</Label>
541
+ <Controller
542
+ control={control}
543
+ name="matcherMode"
544
+ render={({ field }) => (
545
+ <Select
546
+ value={field.value}
547
+ onValueChange={(value) =>
548
+ field.onChange(value as MonitorFormValues["matcherMode"])
549
+ }
550
+ >
551
+ <SelectTrigger className="sm:w-44">
552
+ <SelectValue />
553
+ </SelectTrigger>
554
+ <SelectContent>
555
+ <SelectItem value="class">
556
+ {t.matcherModeClass}
557
+ </SelectItem>
558
+ <SelectItem value="list">{t.matcherModeList}</SelectItem>
559
+ <SelectItem value="range">
560
+ {t.matcherModeRange}
561
+ </SelectItem>
562
+ </SelectContent>
563
+ </Select>
564
+ )}
565
+ />
566
+ </div>
567
+
568
+ {matcherMode === "class" ? (
569
+ <Controller
570
+ control={control}
571
+ name="classes"
572
+ render={({ field }) => (
573
+ <div className="grid grid-cols-2 gap-2 sm:grid-cols-4">
574
+ {STATUS_CLASSES.map((cls) => (
575
+ <label
576
+ key={cls}
577
+ className="flex items-center gap-2 rounded-md border px-2.5 py-2 text-xs"
578
+ >
579
+ <Checkbox
580
+ checked={field.value[cls]}
581
+ onCheckedChange={(checked) =>
582
+ field.onChange({
583
+ ...field.value,
584
+ [cls]: checked === true,
585
+ })
586
+ }
587
+ />
588
+ {classLabel(cls, t)}
589
+ </label>
590
+ ))}
591
+ </div>
592
+ )}
593
+ />
594
+ ) : matcherMode === "list" ? (
595
+ <div className="space-y-1.5">
596
+ <Label htmlFor="monitor-codes" className="text-xs">
597
+ {t.fieldCodes}
598
+ </Label>
599
+ <Input
600
+ id="monitor-codes"
601
+ placeholder={t.fieldCodesPlaceholder}
602
+ {...register("codes")}
603
+ />
604
+ </div>
605
+ ) : (
606
+ <div className="grid grid-cols-2 gap-3">
607
+ <div className="space-y-1.5">
608
+ <Label htmlFor="monitor-range-min" className="text-xs">
609
+ {t.fieldRangeMin}
610
+ </Label>
611
+ <Input
612
+ id="monitor-range-min"
613
+ type="number"
614
+ {...register("rangeMin", { valueAsNumber: true })}
615
+ />
616
+ </div>
617
+ <div className="space-y-1.5">
618
+ <Label htmlFor="monitor-range-max" className="text-xs">
619
+ {t.fieldRangeMax}
620
+ </Label>
621
+ <Input
622
+ id="monitor-range-max"
623
+ type="number"
624
+ {...register("rangeMax", { valueAsNumber: true })}
625
+ />
626
+ </div>
627
+ </div>
628
+ )}
629
+ {errors.classes ? (
630
+ <p className="text-xs text-destructive">
631
+ {errors.classes.message}
632
+ </p>
633
+ ) : null}
634
+ {errors.codes ? (
635
+ <p className="text-xs text-destructive">{errors.codes.message}</p>
636
+ ) : null}
637
+ </div>
638
+
639
+ <div className="space-y-2 border-t border-border/60 pt-4">
640
+ <div className="flex items-center justify-between">
641
+ <Label>{t.fieldAssertions}</Label>
642
+ <Button
643
+ type="button"
644
+ variant="outline"
645
+ size="sm"
646
+ onClick={() =>
647
+ append({ type: "body_contains", value: "", header: "" })
648
+ }
649
+ >
650
+ <IconPlus className="size-3.5" />
651
+ {t.addAssertion}
652
+ </Button>
653
+ </div>
654
+ {fields.length === 0 ? (
655
+ <p className="text-xs text-muted-foreground">
656
+ {t.assertionsHint}
657
+ </p>
658
+ ) : (
659
+ <div className="space-y-2">
660
+ {fields.map((fieldItem, index) => {
661
+ const type = watch(`assertions.${index}.type`);
662
+ const needsHeader =
663
+ type === "header_contains" || type === "header_equals";
664
+ return (
665
+ <div
666
+ key={fieldItem.id}
667
+ className="grid grid-cols-1 gap-2 sm:grid-cols-[minmax(0,150px)_minmax(0,120px)_1fr_auto]"
668
+ >
669
+ <Controller
670
+ control={control}
671
+ name={`assertions.${index}.type`}
672
+ render={({ field }) => (
673
+ <Select
674
+ value={field.value}
675
+ onValueChange={(value) =>
676
+ field.onChange(value as AssertionType)
677
+ }
678
+ >
679
+ <SelectTrigger>
680
+ <SelectValue />
681
+ </SelectTrigger>
682
+ <SelectContent>
683
+ {ASSERTION_TYPES.map((assertionType) => (
684
+ <SelectItem
685
+ key={assertionType}
686
+ value={assertionType}
687
+ >
688
+ {assertionTypeLabel(assertionType, t)}
689
+ </SelectItem>
690
+ ))}
691
+ </SelectContent>
692
+ </Select>
693
+ )}
694
+ />
695
+ {needsHeader ? (
696
+ <Input
697
+ placeholder={t.assertionHeader}
698
+ {...register(`assertions.${index}.header`)}
699
+ />
700
+ ) : (
701
+ <div className="hidden sm:block" />
702
+ )}
703
+ <Input
704
+ placeholder={t.assertionValue}
705
+ inputMode={
706
+ type === "max_latency_ms" ? "numeric" : undefined
707
+ }
708
+ {...register(`assertions.${index}.value`)}
709
+ />
710
+ <Button
711
+ type="button"
712
+ variant="ghost"
713
+ size="icon"
714
+ className="text-muted-foreground hover:text-destructive"
715
+ onClick={() => remove(index)}
716
+ aria-label={t.removeAssertion}
717
+ >
718
+ <IconTrash className="size-3.5" />
719
+ </Button>
720
+ </div>
721
+ );
722
+ })}
723
+ </div>
724
+ )}
725
+ </div>
726
+ </Section>
727
+
728
+ {/* Alerting */}
729
+ <Section
730
+ icon={<IconBell className="size-4 text-muted-foreground" />}
731
+ title={t.sectionAlerting}
732
+ hint={t.sectionAlertingHint}
733
+ summary={alertingSummary}
734
+ open={sections.alerting}
735
+ onOpenChange={(open) =>
736
+ setSections((prev) => ({ ...prev, alerting: open }))
737
+ }
738
+ >
739
+ <div className="grid gap-4 sm:grid-cols-2">
740
+ <div className="space-y-1.5">
741
+ <Label htmlFor="monitor-severity">{t.fieldSeverity}</Label>
742
+ <Controller
743
+ control={control}
744
+ name="severity"
745
+ render={({ field }) => (
746
+ <Select
747
+ value={field.value}
748
+ onValueChange={(value) =>
749
+ field.onChange(value as MonitorSeverity)
750
+ }
751
+ >
752
+ <SelectTrigger id="monitor-severity">
753
+ <SelectValue />
754
+ </SelectTrigger>
755
+ <SelectContent>
756
+ <SelectItem value="warning">
757
+ {t.severityWarning}
758
+ </SelectItem>
759
+ <SelectItem value="critical">
760
+ {t.severityCritical}
761
+ </SelectItem>
762
+ </SelectContent>
763
+ </Select>
764
+ )}
765
+ />
766
+ </div>
767
+
768
+ <div className="space-y-1.5">
769
+ <Label>{t.fieldChannels}</Label>
770
+ <Controller
771
+ control={control}
772
+ name="channels"
773
+ render={({ field }) => (
774
+ <div className="grid grid-cols-2 gap-2">
775
+ {KNOWN_CHANNELS.map((channel) => (
776
+ <label
777
+ key={channel}
778
+ className="flex items-center gap-2 rounded-md border px-2.5 py-1.5 text-xs"
779
+ >
780
+ <Checkbox
781
+ checked={field.value[channel]}
782
+ onCheckedChange={(checked) =>
783
+ field.onChange({
784
+ ...field.value,
785
+ [channel]: checked === true,
786
+ })
787
+ }
788
+ />
789
+ {channelLabel(channel, t)}
790
+ </label>
791
+ ))}
792
+ </div>
793
+ )}
794
+ />
795
+ </div>
796
+
797
+ <div className="space-y-1.5 sm:col-span-2">
798
+ <Label htmlFor="monitor-emails">{t.fieldEmailRecipients}</Label>
799
+ <Textarea
800
+ id="monitor-emails"
801
+ className="min-h-[64px]"
802
+ placeholder={t.fieldEmailRecipientsPlaceholder}
803
+ {...register("emailRecipients")}
804
+ />
805
+ {errors.channels ? (
806
+ <p className="text-xs text-destructive">
807
+ {errors.channels.message}
808
+ </p>
809
+ ) : null}
810
+ </div>
811
+ </div>
812
+ </Section>
813
+
814
+ {/* Advanced */}
815
+ <Section
816
+ icon={<IconSettings className="size-4 text-muted-foreground" />}
817
+ title={t.sectionAdvanced}
818
+ hint={t.sectionAdvancedHint}
819
+ summary={advancedSummary}
820
+ open={sections.advanced}
821
+ onOpenChange={(open) =>
822
+ setSections((prev) => ({ ...prev, advanced: open }))
823
+ }
824
+ >
825
+ <div className="grid gap-4 sm:grid-cols-2">
826
+ <div className="space-y-1.5">
827
+ <Label htmlFor="monitor-method">{t.fieldMethod}</Label>
828
+ <Controller
829
+ control={control}
830
+ name="method"
831
+ render={({ field }) => (
832
+ <Select
833
+ value={field.value}
834
+ onValueChange={(value) =>
835
+ field.onChange(value as MonitorMethod)
836
+ }
837
+ >
838
+ <SelectTrigger id="monitor-method">
839
+ <SelectValue />
840
+ </SelectTrigger>
841
+ <SelectContent>
842
+ {METHODS.map((method) => (
843
+ <SelectItem key={method} value={method}>
844
+ {method}
845
+ </SelectItem>
846
+ ))}
847
+ </SelectContent>
848
+ </Select>
849
+ )}
850
+ />
851
+ </div>
852
+
853
+ <div className="space-y-1.5">
854
+ <Label htmlFor="monitor-timeout">{t.fieldTimeout}</Label>
855
+ <Input
856
+ id="monitor-timeout"
857
+ type="number"
858
+ min={1}
859
+ max={120}
860
+ {...register("timeoutSeconds", { valueAsNumber: true })}
861
+ />
862
+ </div>
863
+
864
+ <div className="space-y-1.5">
865
+ <Label htmlFor="monitor-cooldown">{t.fieldCooldown}</Label>
866
+ <Input
867
+ id="monitor-cooldown"
868
+ type="number"
869
+ min={0}
870
+ max={1440}
871
+ {...register("cooldownMinutes", { valueAsNumber: true })}
872
+ />
873
+ </div>
874
+ </div>
875
+
876
+ <div className="grid gap-3 sm:grid-cols-2">
877
+ <Controller
878
+ control={control}
879
+ name="followRedirects"
880
+ render={({ field }) => (
881
+ <div className="flex items-center justify-between gap-3 rounded-md border px-3 py-2">
882
+ <div className="min-w-0">
883
+ <Label>{t.fieldFollowRedirects}</Label>
884
+ <p className="text-xs text-muted-foreground">
885
+ {t.fieldFollowRedirectsHint}
886
+ </p>
887
+ </div>
888
+ <Switch
889
+ checked={field.value}
890
+ onCheckedChange={field.onChange}
891
+ />
892
+ </div>
893
+ )}
894
+ />
895
+ <Controller
896
+ control={control}
897
+ name="enabled"
898
+ render={({ field }) => (
899
+ <div className="flex items-center justify-between gap-3 rounded-md border px-3 py-2">
900
+ <div className="min-w-0">
901
+ <Label>{t.fieldEnabled}</Label>
902
+ <p className="text-xs text-muted-foreground">
903
+ {t.fieldEnabledHint}
904
+ </p>
905
+ </div>
906
+ <Switch
907
+ checked={field.value}
908
+ onCheckedChange={field.onChange}
909
+ />
910
+ </div>
911
+ )}
912
+ />
913
+ </div>
914
+ </Section>
915
+ </form>
916
+
917
+ {/* Sticky action bar — pinned to the content column, not over the sidebar */}
918
+ <div className="sticky bottom-0 z-10 mt-6 flex items-center justify-end gap-2 border-t border-border/60 bg-background/80 py-3 backdrop-blur supports-[backdrop-filter]:bg-background/60">
919
+ <Button type="button" variant="outline" onClick={onCancel}>
920
+ {t.cancel}
921
+ </Button>
922
+ <Button type="submit" form="monitor-form" disabled={saving}>
923
+ {saving ? <IconLoader2 className="size-3.5 animate-spin" /> : null}
924
+ {isEdit ? t.save : t.create}
925
+ </Button>
926
+ </div>
927
+ </div>
928
+ );
929
+ }
930
+
931
+ function Section({
932
+ icon,
933
+ title,
934
+ hint,
935
+ summary,
936
+ open,
937
+ onOpenChange,
938
+ children,
939
+ }: {
940
+ icon: React.ReactNode;
941
+ title: string;
942
+ hint: string;
943
+ summary: string;
944
+ open: boolean;
945
+ onOpenChange: (open: boolean) => void;
946
+ children: React.ReactNode;
947
+ }) {
948
+ return (
949
+ <Collapsible
950
+ open={open}
951
+ onOpenChange={onOpenChange}
952
+ className="rounded-lg border border-border/60"
953
+ >
954
+ <CollapsibleTrigger className="group flex w-full items-center gap-3 rounded-lg px-4 py-3 text-start transition-colors hover:bg-muted/30">
955
+ {icon}
956
+ <div className="min-w-0 flex-1">
957
+ <div className="text-sm font-medium">{title}</div>
958
+ <div className="truncate text-xs text-muted-foreground">
959
+ {open ? hint : summary}
960
+ </div>
961
+ </div>
962
+ <IconChevronDown
963
+ className={cn(
964
+ "size-4 shrink-0 text-muted-foreground transition-transform",
965
+ open && "rotate-180",
966
+ )}
967
+ />
968
+ </CollapsibleTrigger>
969
+ <CollapsibleContent className="space-y-4 border-t border-border/60 px-4 py-4">
970
+ {children}
971
+ </CollapsibleContent>
972
+ </Collapsible>
973
+ );
974
+ }
975
+
976
+ function assertionTypeLabel(
977
+ type: AssertionType,
978
+ t: ReturnType<typeof useUptimeT>,
979
+ ): string {
980
+ switch (type) {
981
+ case "body_contains":
982
+ return t.typeBodyContains;
983
+ case "body_absent":
984
+ return t.typeBodyAbsent;
985
+ case "header_contains":
986
+ return t.typeHeaderContains;
987
+ case "header_equals":
988
+ return t.typeHeaderEquals;
989
+ case "max_latency_ms":
990
+ return t.typeMaxLatency;
991
+ default:
992
+ return type;
993
+ }
994
+ }
995
+
996
+ function classLabel(
997
+ cls: (typeof STATUS_CLASSES)[number],
998
+ t: ReturnType<typeof useUptimeT>,
999
+ ): string {
1000
+ switch (cls) {
1001
+ case "2xx":
1002
+ return t.classLabel2xx;
1003
+ case "3xx":
1004
+ return t.classLabel3xx;
1005
+ case "4xx":
1006
+ return t.classLabel4xx;
1007
+ case "5xx":
1008
+ return t.classLabel5xx;
1009
+ }
1010
+ }
1011
+
1012
+ function channelLabel(
1013
+ channel: (typeof KNOWN_CHANNELS)[number],
1014
+ t: ReturnType<typeof useUptimeT>,
1015
+ ): string {
1016
+ switch (channel) {
1017
+ case "inbox":
1018
+ return t.channelInbox;
1019
+ case "email":
1020
+ return t.channelEmail;
1021
+ case "slack":
1022
+ return t.channelSlack;
1023
+ case "webhook":
1024
+ return t.channelWebhook;
1025
+ }
1026
+ }