@agent-native/core 0.123.0 → 0.123.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 (334) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +24 -0
  3. package/corpus/core/package.json +1 -1
  4. package/corpus/core/src/cli/templates-meta.ts +12 -0
  5. package/corpus/core/src/deploy/build.ts +8 -0
  6. package/corpus/core/src/server/builder-browser.ts +76 -0
  7. package/corpus/core/src/server/core-routes-plugin.ts +15 -5
  8. package/corpus/templates/assets/app/global.css +0 -4
  9. package/corpus/templates/assets/app/routes/brand-kits.$id.tsx +195 -167
  10. package/corpus/templates/assets/changelog/2026-07-23-drafts-preview-panel-now-fills-the-full-width-with-a-large-p.md +6 -0
  11. package/corpus/templates/content/actions/_builder-cms-source-adapter.ts +1 -0
  12. package/corpus/templates/content/actions/_builder-cms-write-client.ts +55 -9
  13. package/corpus/templates/content/actions/_database-source-utils.ts +756 -182
  14. package/corpus/templates/content/actions/change-content-database-source-role.ts +21 -20
  15. package/corpus/templates/content/actions/execute-builder-source-execution.ts +49 -8
  16. package/corpus/templates/content/actions/materialize-builder-required-fields.ts +42 -9
  17. package/corpus/templates/content/actions/prepare-builder-source-review.ts +88 -90
  18. package/corpus/templates/content/actions/preview-builder-source-review.ts +4 -3
  19. package/corpus/templates/content/actions/refresh-content-database-source.ts +36 -9
  20. package/corpus/templates/content/actions/set-content-database-source-write-mode.ts +28 -24
  21. package/corpus/templates/content/app/components/editor/BubbleToolbar.tsx +62 -12
  22. package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +91 -16
  23. package/corpus/templates/content/app/components/editor/DocumentProperties.tsx +15 -1
  24. package/corpus/templates/content/app/components/editor/SlashCommandMenu.tsx +234 -44
  25. package/corpus/templates/content/app/components/editor/VisualEditor.tsx +194 -16
  26. package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +95 -42
  27. package/corpus/templates/content/app/components/editor/database-sources/BuilderSourceReviewDialog.tsx +4 -2
  28. package/corpus/templates/content/app/components/editor/extensions/CodeBlockNode.tsx +10 -166
  29. package/corpus/templates/content/app/components/editor/extensions/VideoBlock.tsx +32 -8
  30. package/corpus/templates/content/app/components/editor/extensions/VideoNode.ts +14 -0
  31. package/corpus/templates/content/app/components/editor/registrySlashItems.ts +5 -1
  32. package/corpus/templates/content/app/hooks/use-content-database.ts +59 -2
  33. package/corpus/templates/content/app/hooks/use-documents.ts +18 -26
  34. package/corpus/templates/content/app/root.tsx +1 -6
  35. package/corpus/templates/content/changelog/2026-07-24-builder-can-now-publish-approved-updates-to-entries-that-are.md +6 -0
  36. package/corpus/templates/content/changelog/2026-07-24-builder-collection-loading-no-longer-restarts-from-the-begin.md +6 -0
  37. package/corpus/templates/content/changelog/2026-07-24-builder-connections-now-complete-securely-from-netlify-deplo.md +6 -0
  38. package/corpus/templates/content/changelog/2026-07-24-builder-draft-creation-now-scopes-execution-to-the-selected-.md +6 -0
  39. package/corpus/templates/content/changelog/2026-07-24-builder-review-now-distinguishes-preparing-a-safe-dry-run-from.md +6 -0
  40. package/corpus/templates/content/changelog/2026-07-24-builder-source-imports-now-recover-from-a-transient-paginati.md +6 -0
  41. package/corpus/templates/content/changelog/2026-07-24-builder-source-refresh-now-preserves-the-provider-bound-fiel.md +6 -0
  42. package/corpus/templates/content/changelog/2026-07-24-builder-update-reviews-now-open-reliably-on-large-collection.md +6 -0
  43. package/corpus/templates/content/changelog/2026-07-24-builder-write-policy-controls-now-reflect-successful-changes.md +6 -0
  44. package/corpus/templates/content/changelog/2026-07-24-date-properties-now-save-reliably-when-selected-with-the-nat.md +6 -0
  45. package/corpus/templates/content/changelog/2026-07-24-editor-slash-search-now-keeps-multi-word-block-names-open.md +6 -0
  46. package/corpus/templates/content/changelog/2026-07-24-exact-editor-slash-commands-now-execute-on-enter-even-if-the.md +6 -0
  47. package/corpus/templates/content/changelog/2026-07-24-new-collaborative-pages-now-save-their-first-edits.md +6 -0
  48. package/corpus/templates/content/changelog/2026-07-24-new-local-builder-rows-no-longer-remain-locked-behind-a-none.md +6 -0
  49. package/corpus/templates/content/changelog/2026-07-24-opening-or-closing-a-collaborative-document-no-longer-lets-u.md +6 -0
  50. package/corpus/templates/content/changelog/2026-07-24-selected-text-now-opens-the-link-editor-with-command-k-or-co.md +6 -0
  51. package/corpus/templates/content/changelog/2026-07-24-slash-menu-block-choices-no-longer-stall-pointer-interaction.md +6 -0
  52. package/corpus/templates/content/changelog/2026-07-24-uploaded-and-embedded-media-now-stay-intact-while-collaborative-document-drafts-save.md +6 -0
  53. package/corpus/templates/content/changelog/2026-07-24-video-link-insertion-now-keeps-its-source-panel-open-while-t.md +6 -0
  54. package/corpus/templates/content/changelog/2026-07-26-builder-publishing-now-shows-safe-validation-details-instead.md +6 -0
  55. package/corpus/templates/content/changelog/2026-07-26-builder-reviews-stay-responsive-on-large-collections-and-pre.md +6 -0
  56. package/corpus/templates/content/changelog/2026-07-26-collaborative-documents-no-longer-flash-duplicated-recent-bl.md +6 -0
  57. package/corpus/templates/content/changelog/2026-07-26-corrected-builder-updates-are-now-reviewed-separately-from-f.md +6 -0
  58. package/corpus/templates/content/changelog/2026-07-26-documents-now-remain-read-only-until-their-live-collaborativ.md +6 -0
  59. package/corpus/templates/content/changelog/2026-07-26-slash-command-menus-now-stay-reachable-when-editing-near-the.md +6 -0
  60. package/corpus/templates/content/changelog/2026-07-26-the-slash-menu-now-shows-one-clear-callout-choice-instead-of.md +6 -0
  61. package/corpus/templates/content/shared/api.ts +1 -0
  62. package/corpus/templates/content/shared/builder-mdx.ts +73 -16
  63. package/corpus/templates/crm/.agents/skills/agent-native-docs/SKILL.md +115 -0
  64. package/corpus/templates/crm/.agents/skills/agent-native-toolkit/SKILL.md +177 -0
  65. package/corpus/templates/crm/.agents/skills/crm/SKILL.md +205 -0
  66. package/corpus/templates/crm/.agents/skills/customizing-agent-native/SKILL.md +220 -0
  67. package/corpus/templates/crm/.agents/skills/feature-flags/SKILL.md +169 -0
  68. package/corpus/templates/crm/.agents/skills/sharing/SKILL.md +228 -0
  69. package/corpus/templates/crm/.agents/skills/storing-data/SKILL.md +178 -0
  70. package/corpus/templates/crm/.agents/skills/upgrade-agent-native/SKILL.md +122 -0
  71. package/corpus/templates/crm/.env.example +5 -0
  72. package/corpus/templates/crm/AGENTS.md +82 -0
  73. package/corpus/templates/crm/CHANGELOG.md +25 -0
  74. package/corpus/templates/crm/DEVELOPING.md +66 -0
  75. package/corpus/templates/crm/README.md +122 -0
  76. package/corpus/templates/crm/_gitignore +12 -0
  77. package/corpus/templates/crm/actions/_crm-action-utils.ts +98 -0
  78. package/corpus/templates/crm/actions/_crm-attribute-utils.ts +219 -0
  79. package/corpus/templates/crm/actions/_crm-dashboard.ts +69 -0
  80. package/corpus/templates/crm/actions/_crm-data-program-actions.ts +15 -0
  81. package/corpus/templates/crm/actions/_crm-list-utils.ts +775 -0
  82. package/corpus/templates/crm/actions/_crm-signal-utils.ts +79 -0
  83. package/corpus/templates/crm/actions/add-crm-record-to-list.ts +164 -0
  84. package/corpus/templates/crm/actions/apply-crm-proposals.ts +291 -0
  85. package/corpus/templates/crm/actions/archive-crm-attribute.ts +69 -0
  86. package/corpus/templates/crm/actions/attach-call-evidence.ts +170 -0
  87. package/corpus/templates/crm/actions/configure-crm-connection.ts +126 -0
  88. package/corpus/templates/crm/actions/configure-native-crm.ts +44 -0
  89. package/corpus/templates/crm/actions/create-crm-attribute.ts +350 -0
  90. package/corpus/templates/crm/actions/create-crm-list.ts +194 -0
  91. package/corpus/templates/crm/actions/create-crm-record.ts +148 -0
  92. package/corpus/templates/crm/actions/create-crm-signal-tracker.ts +58 -0
  93. package/corpus/templates/crm/actions/delete-crm-saved-view.ts +68 -0
  94. package/corpus/templates/crm/actions/delete-staged-dataset.ts +14 -0
  95. package/corpus/templates/crm/actions/estimate-crm-enrichment.ts +229 -0
  96. package/corpus/templates/crm/actions/find-crm-duplicates.ts +87 -0
  97. package/corpus/templates/crm/actions/get-crm-automation-recipe.ts +36 -0
  98. package/corpus/templates/crm/actions/get-crm-dashboard-panel.ts +35 -0
  99. package/corpus/templates/crm/actions/get-crm-dashboard.ts +14 -0
  100. package/corpus/templates/crm/actions/get-crm-overview.ts +13 -0
  101. package/corpus/templates/crm/actions/get-crm-pipeline-data.ts +61 -0
  102. package/corpus/templates/crm/actions/get-crm-record-page.ts +287 -0
  103. package/corpus/templates/crm/actions/get-crm-record.ts +75 -0
  104. package/corpus/templates/crm/actions/get-crm-workspace.ts +364 -0
  105. package/corpus/templates/crm/actions/install-crm-pipeline-dashboard.ts +81 -0
  106. package/corpus/templates/crm/actions/list-crm-attributes.ts +71 -0
  107. package/corpus/templates/crm/actions/list-crm-connections.ts +68 -0
  108. package/corpus/templates/crm/actions/list-crm-dashboard-revisions.ts +15 -0
  109. package/corpus/templates/crm/actions/list-crm-dashboards.ts +15 -0
  110. package/corpus/templates/crm/actions/list-crm-enrichment-slots.ts +28 -0
  111. package/corpus/templates/crm/actions/list-crm-list-entries.ts +192 -0
  112. package/corpus/templates/crm/actions/list-crm-lists.ts +80 -0
  113. package/corpus/templates/crm/actions/list-crm-proposals.ts +55 -0
  114. package/corpus/templates/crm/actions/list-crm-record-field-history.ts +201 -0
  115. package/corpus/templates/crm/actions/list-crm-record-values.ts +200 -0
  116. package/corpus/templates/crm/actions/list-crm-records.ts +103 -0
  117. package/corpus/templates/crm/actions/list-crm-saved-views.ts +66 -0
  118. package/corpus/templates/crm/actions/list-crm-signal-hits.ts +43 -0
  119. package/corpus/templates/crm/actions/list-crm-signal-trackers.ts +32 -0
  120. package/corpus/templates/crm/actions/list-crm-tasks.ts +22 -0
  121. package/corpus/templates/crm/actions/list-staged-datasets.ts +14 -0
  122. package/corpus/templates/crm/actions/list-workspace-connections.ts +47 -0
  123. package/corpus/templates/crm/actions/manage-crm-attribute-option.ts +272 -0
  124. package/corpus/templates/crm/actions/manage-crm-signal-tracker.ts +87 -0
  125. package/corpus/templates/crm/actions/manage-crm-task.ts +132 -0
  126. package/corpus/templates/crm/actions/merge-crm-records.ts +691 -0
  127. package/corpus/templates/crm/actions/navigate.ts +54 -0
  128. package/corpus/templates/crm/actions/provider-api-catalog.ts +22 -0
  129. package/corpus/templates/crm/actions/provider-api-docs.ts +54 -0
  130. package/corpus/templates/crm/actions/provider-api-request.ts +92 -0
  131. package/corpus/templates/crm/actions/query-staged-dataset.ts +44 -0
  132. package/corpus/templates/crm/actions/record-crm-call-insight.ts +160 -0
  133. package/corpus/templates/crm/actions/record-crm-smart-signal.ts +150 -0
  134. package/corpus/templates/crm/actions/remove-crm-list-entry.ts +79 -0
  135. package/corpus/templates/crm/actions/restore-crm-dashboard-revision.ts +23 -0
  136. package/corpus/templates/crm/actions/review-crm-signal.ts +31 -0
  137. package/corpus/templates/crm/actions/run-crm-attribute-fill.ts +443 -0
  138. package/corpus/templates/crm/actions/run-crm-enrichment.ts +647 -0
  139. package/corpus/templates/crm/actions/run-crm-saved-view-program.ts +103 -0
  140. package/corpus/templates/crm/actions/run-crm-signal-trackers.ts +265 -0
  141. package/corpus/templates/crm/actions/run.ts +7 -0
  142. package/corpus/templates/crm/actions/save-crm-dashboard.ts +51 -0
  143. package/corpus/templates/crm/actions/save-crm-saved-view.ts +296 -0
  144. package/corpus/templates/crm/actions/sync-crm.ts +146 -0
  145. package/corpus/templates/crm/actions/update-crm-attribute.ts +111 -0
  146. package/corpus/templates/crm/actions/update-crm-list-entry.ts +102 -0
  147. package/corpus/templates/crm/actions/update-crm-list.ts +97 -0
  148. package/corpus/templates/crm/actions/update-crm-record.ts +543 -0
  149. package/corpus/templates/crm/actions/view-screen.ts +324 -0
  150. package/corpus/templates/crm/agent-native.app-skill.json +69 -0
  151. package/corpus/templates/crm/app/components/crm/CreateCrmRecordDialog.tsx +158 -0
  152. package/corpus/templates/crm/app/components/crm/CrmDashboardPanel.tsx +78 -0
  153. package/corpus/templates/crm/app/components/crm/CrmSignalsPanel.tsx +220 -0
  154. package/corpus/templates/crm/app/components/crm/IntelligenceSettings.tsx +414 -0
  155. package/corpus/templates/crm/app/components/crm/RecordActions.tsx +648 -0
  156. package/corpus/templates/crm/app/components/crm/RecordGrid.tsx +488 -0
  157. package/corpus/templates/crm/app/components/crm/RecordWorkspace.tsx +232 -0
  158. package/corpus/templates/crm/app/components/crm/SavedViewDataProgram.tsx +93 -0
  159. package/corpus/templates/crm/app/components/crm/Surface.tsx +97 -0
  160. package/corpus/templates/crm/app/components/crm/WorkOverview.tsx +180 -0
  161. package/corpus/templates/crm/app/components/crm/board/CrmBoard.tsx +988 -0
  162. package/corpus/templates/crm/app/components/crm/board/board-model.ts +399 -0
  163. package/corpus/templates/crm/app/components/crm/board/entry-filter.ts +150 -0
  164. package/corpus/templates/crm/app/components/crm/board/view-draft.ts +227 -0
  165. package/corpus/templates/crm/app/components/crm/grid/CrmGrid.tsx +1033 -0
  166. package/corpus/templates/crm/app/components/crm/grid/GridCell.tsx +620 -0
  167. package/corpus/templates/crm/app/components/crm/grid/clipboard.ts +84 -0
  168. package/corpus/templates/crm/app/components/crm/grid/model.ts +304 -0
  169. package/corpus/templates/crm/app/components/crm/grid/navigation.ts +220 -0
  170. package/corpus/templates/crm/app/components/crm/grid/query.ts +214 -0
  171. package/corpus/templates/crm/app/components/crm/record/AttributePanel.tsx +192 -0
  172. package/corpus/templates/crm/app/components/crm/record/FieldHistory.tsx +179 -0
  173. package/corpus/templates/crm/app/components/crm/record/RecordHeader.tsx +103 -0
  174. package/corpus/templates/crm/app/components/crm/record/RecordLists.tsx +235 -0
  175. package/corpus/templates/crm/app/components/crm/record/RecordTabs.tsx +206 -0
  176. package/corpus/templates/crm/app/components/crm/record/field-editors.tsx +245 -0
  177. package/corpus/templates/crm/app/components/crm/record/record-data.ts +603 -0
  178. package/corpus/templates/crm/app/components/crm/settings/AdvancedSettings.tsx +49 -0
  179. package/corpus/templates/crm/app/components/crm/settings/AttributeOptionsEditor.tsx +441 -0
  180. package/corpus/templates/crm/app/components/crm/settings/ConnectionSettings.tsx +169 -0
  181. package/corpus/templates/crm/app/components/crm/settings/FieldsSettings.tsx +935 -0
  182. package/corpus/templates/crm/app/components/crm/settings/ListsSettings.tsx +339 -0
  183. package/corpus/templates/crm/app/components/crm/settings/settings-admin.ts +377 -0
  184. package/corpus/templates/crm/app/components/crm/shared/AttributeValueParts.tsx +69 -0
  185. package/corpus/templates/crm/app/components/crm/shared/attribute-value.ts +591 -0
  186. package/corpus/templates/crm/app/components/layout/CrmCommandMenu.tsx +556 -0
  187. package/corpus/templates/crm/app/components/layout/CrmLayout.tsx +130 -0
  188. package/corpus/templates/crm/app/components/layout/CrmSidebar.tsx +407 -0
  189. package/corpus/templates/crm/app/components/layout/command-palette-search.ts +126 -0
  190. package/corpus/templates/crm/app/components/layout/crm-ui-intents.ts +41 -0
  191. package/corpus/templates/crm/app/components/layout/sidebar-collapse.ts +40 -0
  192. package/corpus/templates/crm/app/components/layout/sidebar-lists.ts +109 -0
  193. package/corpus/templates/crm/app/components/ui/alert-dialog.tsx +1 -0
  194. package/corpus/templates/crm/app/components/ui/badge.tsx +1 -0
  195. package/corpus/templates/crm/app/components/ui/button.tsx +1 -0
  196. package/corpus/templates/crm/app/components/ui/card.tsx +1 -0
  197. package/corpus/templates/crm/app/components/ui/checkbox.tsx +1 -0
  198. package/corpus/templates/crm/app/components/ui/dialog.tsx +1 -0
  199. package/corpus/templates/crm/app/components/ui/dropdown-menu.tsx +1 -0
  200. package/corpus/templates/crm/app/components/ui/hover-card.tsx +1 -0
  201. package/corpus/templates/crm/app/components/ui/input.tsx +1 -0
  202. package/corpus/templates/crm/app/components/ui/label.tsx +1 -0
  203. package/corpus/templates/crm/app/components/ui/popover.tsx +1 -0
  204. package/corpus/templates/crm/app/components/ui/select.tsx +1 -0
  205. package/corpus/templates/crm/app/components/ui/sheet.tsx +1 -0
  206. package/corpus/templates/crm/app/components/ui/skeleton.tsx +1 -0
  207. package/corpus/templates/crm/app/components/ui/switch.tsx +1 -0
  208. package/corpus/templates/crm/app/components/ui/table.tsx +1 -0
  209. package/corpus/templates/crm/app/components/ui/tabs.tsx +1 -0
  210. package/corpus/templates/crm/app/components/ui/textarea.tsx +1 -0
  211. package/corpus/templates/crm/app/components/ui/toolkit-provider.tsx +17 -0
  212. package/corpus/templates/crm/app/components/ui/tooltip.tsx +1 -0
  213. package/corpus/templates/crm/app/design-system.ts +3 -0
  214. package/corpus/templates/crm/app/entry.client.tsx +19 -0
  215. package/corpus/templates/crm/app/entry.server.tsx +10 -0
  216. package/corpus/templates/crm/app/global.css +167 -0
  217. package/corpus/templates/crm/app/hooks/use-keyboard-shortcuts.ts +206 -0
  218. package/corpus/templates/crm/app/hooks/use-navigation-state.ts +48 -0
  219. package/corpus/templates/crm/app/i18n/en-US.ts +684 -0
  220. package/corpus/templates/crm/app/i18n/index.ts +34 -0
  221. package/corpus/templates/crm/app/lib/dashboard.ts +13 -0
  222. package/corpus/templates/crm/app/lib/navigation.ts +17 -0
  223. package/corpus/templates/crm/app/lib/tab-id.ts +1 -0
  224. package/corpus/templates/crm/app/lib/types.ts +172 -0
  225. package/corpus/templates/crm/app/lib/utils.ts +1 -0
  226. package/corpus/templates/crm/app/root.tsx +93 -0
  227. package/corpus/templates/crm/app/routes/_index.tsx +26 -0
  228. package/corpus/templates/crm/app/routes/accounts.tsx +41 -0
  229. package/corpus/templates/crm/app/routes/agent.tsx +5 -0
  230. package/corpus/templates/crm/app/routes/ask.tsx +53 -0
  231. package/corpus/templates/crm/app/routes/dashboard.tsx +118 -0
  232. package/corpus/templates/crm/app/routes/lists.tsx +228 -0
  233. package/corpus/templates/crm/app/routes/opportunities.tsx +41 -0
  234. package/corpus/templates/crm/app/routes/people.tsx +41 -0
  235. package/corpus/templates/crm/app/routes/proposals.tsx +388 -0
  236. package/corpus/templates/crm/app/routes/records.$recordId.tsx +53 -0
  237. package/corpus/templates/crm/app/routes/records.tsx +64 -0
  238. package/corpus/templates/crm/app/routes/settings.tsx +118 -0
  239. package/corpus/templates/crm/app/routes/setup.tsx +308 -0
  240. package/corpus/templates/crm/app/routes/tasks.tsx +252 -0
  241. package/corpus/templates/crm/app/routes/views.tsx +683 -0
  242. package/corpus/templates/crm/app/routes.ts +19 -0
  243. package/corpus/templates/crm/app/vite-env.d.ts +6 -0
  244. package/corpus/templates/crm/changelog/2026-07-26-any-list-or-view-can-be-shown-as-a-board-grouped-by-stage-so.md +6 -0
  245. package/corpus/templates/crm/changelog/2026-07-26-crm-now-finds-likely-duplicate-records-and-tells-you-exactly.md +6 -0
  246. package/corpus/templates/crm/changelog/2026-07-26-enrichment-now-shows-what-a-run-will-cost-before-it-starts-g.md +6 -0
  247. package/corpus/templates/crm/changelog/2026-07-26-lists-turn-any-set-of-records-into-a-pipeline-with-its-own-s.md +6 -0
  248. package/corpus/templates/crm/changelog/2026-07-26-moving-a-record-or-list-entry-into-a-stage-that-was-retired-.md +6 -0
  249. package/corpus/templates/crm/changelog/2026-07-26-opening-a-record-now-shows-one-page-with-its-fields-the-list.md +6 -0
  250. package/corpus/templates/crm/changelog/2026-07-26-press-cmd-k-to-jump-to-any-record-list-view-or-setting-and-u.md +6 -0
  251. package/corpus/templates/crm/changelog/2026-07-26-provider-record-edits-now-show-an-exact-before-after-diff-an.md +6 -0
  252. package/corpus/templates/crm/changelog/2026-07-26-record-page-attribute-labels-now-read-in-full-instead-of-bei.md +6 -0
  253. package/corpus/templates/crm/changelog/2026-07-26-records-now-use-typed-fields-currency-dates-ratings-status-s.md +6 -0
  254. package/corpus/templates/crm/changelog/2026-07-26-retyping-a-value-on-the-record-page-now-replaces-it-instead-.md +6 -0
  255. package/corpus/templates/crm/changelog/2026-07-26-settings-now-has-fields-and-lists-tabs-for-adding-fields-edi.md +6 -0
  256. package/corpus/templates/crm/changelog/2026-07-26-the-proposal-review-now-shows-the-field-that-actually-change.md +6 -0
  257. package/corpus/templates/crm/changelog/2026-07-26-the-record-grid-is-now-a-real-spreadsheet-filter-sort-and-pa.md +6 -0
  258. package/corpus/templates/crm/components.json +20 -0
  259. package/corpus/templates/crm/docs/architecture/crm-contract.md +214 -0
  260. package/corpus/templates/crm/docs/features/README.md +55 -0
  261. package/corpus/templates/crm/docs/features/f1-records-and-fields.mdx +52 -0
  262. package/corpus/templates/crm/docs/features/f2-lists-pipelines-and-views.mdx +48 -0
  263. package/corpus/templates/crm/docs/features/f3-work-and-record-quality.mdx +40 -0
  264. package/corpus/templates/crm/docs/features/f4-evidence-and-chat.mdx +47 -0
  265. package/corpus/templates/crm/learnings.defaults.md +5 -0
  266. package/corpus/templates/crm/netlify.toml +11 -0
  267. package/corpus/templates/crm/package.json +61 -0
  268. package/corpus/templates/crm/public/favicon.svg +1 -0
  269. package/corpus/templates/crm/public/icon-180.svg +1 -0
  270. package/corpus/templates/crm/public/manifest.json +11 -0
  271. package/corpus/templates/crm/react-router.config.ts +7 -0
  272. package/corpus/templates/crm/server/crm/adapter.ts +32 -0
  273. package/corpus/templates/crm/server/crm/crm-field-firewall.ts +64 -0
  274. package/corpus/templates/crm/server/crm/crm-mirror.ts +721 -0
  275. package/corpus/templates/crm/server/crm/field-policy-attributes.ts +48 -0
  276. package/corpus/templates/crm/server/crm/hubspot-adapter.ts +1146 -0
  277. package/corpus/templates/crm/server/crm/native-adapter.ts +1810 -0
  278. package/corpus/templates/crm/server/crm/provider-record-link.ts +264 -0
  279. package/corpus/templates/crm/server/crm/read-through.ts +207 -0
  280. package/corpus/templates/crm/server/crm/salesforce-adapter.ts +1255 -0
  281. package/corpus/templates/crm/server/db/crm-store.ts +1112 -0
  282. package/corpus/templates/crm/server/db/index.ts +191 -0
  283. package/corpus/templates/crm/server/db/schema.ts +647 -0
  284. package/corpus/templates/crm/server/lib/crm-query.ts +1558 -0
  285. package/corpus/templates/crm/server/lib/dedupe.ts +714 -0
  286. package/corpus/templates/crm/server/lib/enrichment-cost.ts +381 -0
  287. package/corpus/templates/crm/server/lib/enrichment-slots.ts +519 -0
  288. package/corpus/templates/crm/server/lib/intelligence/default-detectors.ts +65 -0
  289. package/corpus/templates/crm/server/lib/intelligence/evidence.ts +105 -0
  290. package/corpus/templates/crm/server/lib/intelligence/keyword-detector.ts +94 -0
  291. package/corpus/templates/crm/server/lib/intelligence/smart-detector.ts +39 -0
  292. package/corpus/templates/crm/server/lib/intelligence/summary.ts +19 -0
  293. package/corpus/templates/crm/server/lib/lifecycle.ts +634 -0
  294. package/corpus/templates/crm/server/lib/provider-api.ts +64 -0
  295. package/corpus/templates/crm/server/middleware/auth.ts +4 -0
  296. package/corpus/templates/crm/server/plugins/agent-chat.ts +65 -0
  297. package/corpus/templates/crm/server/plugins/auth.ts +14 -0
  298. package/corpus/templates/crm/server/plugins/core-routes.ts +17 -0
  299. package/corpus/templates/crm/server/plugins/db.ts +738 -0
  300. package/corpus/templates/crm/server/routes/[...page].get.ts +5 -0
  301. package/corpus/templates/crm/shared/crm-attributes.ts +469 -0
  302. package/corpus/templates/crm/shared/crm-automation-recipes.ts +132 -0
  303. package/corpus/templates/crm/shared/crm-contract.ts +417 -0
  304. package/corpus/templates/crm/shared/crm-navigation.ts +190 -0
  305. package/corpus/templates/crm/shared/crm-sales-config.ts +63 -0
  306. package/corpus/templates/crm/ssr-entry.ts +11 -0
  307. package/corpus/templates/crm/tsconfig.json +24 -0
  308. package/corpus/templates/crm/vite.config.ts +15 -0
  309. package/corpus/templates/crm/vitest.config.ts +5 -0
  310. package/corpus/templates/design/app/i18n-data.ts +12 -12
  311. package/corpus/templates/design/changelog/2026-07-26-apply-design-updates-from-a-centered-top-toolbar-without-cov.md +6 -0
  312. package/corpus/toolkit/CHANGELOG.md +6 -0
  313. package/corpus/toolkit/package.json +1 -1
  314. package/corpus/toolkit/src/editor/useCollabReconcile.ts +14 -1
  315. package/dist/cli/templates-meta.d.ts.map +1 -1
  316. package/dist/cli/templates-meta.js +12 -0
  317. package/dist/cli/templates-meta.js.map +1 -1
  318. package/dist/collab/routes.d.ts +2 -2
  319. package/dist/deploy/build.js +6 -0
  320. package/dist/deploy/build.js.map +1 -1
  321. package/dist/observability/routes.d.ts +3 -3
  322. package/dist/server/builder-browser.d.ts +13 -0
  323. package/dist/server/builder-browser.d.ts.map +1 -1
  324. package/dist/server/builder-browser.js +59 -0
  325. package/dist/server/builder-browser.js.map +1 -1
  326. package/dist/server/core-routes-plugin.d.ts.map +1 -1
  327. package/dist/server/core-routes-plugin.js +10 -5
  328. package/dist/server/core-routes-plugin.js.map +1 -1
  329. package/dist/server/transcribe-voice.d.ts +1 -1
  330. package/package.json +2 -2
  331. package/src/cli/templates-meta.ts +12 -0
  332. package/src/deploy/build.ts +8 -0
  333. package/src/server/builder-browser.ts +76 -0
  334. package/src/server/core-routes-plugin.ts +15 -5
@@ -0,0 +1,603 @@
1
+ /**
2
+ * Pure logic behind the record page: what an attribute value looks like, who is
3
+ * allowed to edit it inline, how typed input is parsed, and how bitemporal rows
4
+ * become "changed from X to Y".
5
+ *
6
+ * It lives apart from the components because the template's vitest config is
7
+ * node-only (no DOM, `.test.ts` only), so this is the layer that can be tested.
8
+ * Anything here that returns a value must keep "absent" and "unreadable"
9
+ * distinguishable — a field the mirror never carried is not an empty field.
10
+ */
11
+
12
+ import {
13
+ ATTRIBUTE_TYPE_SPECS,
14
+ type CrmAttributeType,
15
+ } from "../../../../shared/crm-attributes";
16
+ import type {
17
+ CrmAttributeDefinition,
18
+ CrmAttributeOption,
19
+ CrmValue,
20
+ } from "../../../../shared/crm-contract";
21
+ import {
22
+ ATTRIBUTE_VALUE_SPECS,
23
+ formatAttributeValue,
24
+ parseAttributeValue,
25
+ valueTokens,
26
+ type CrmAttributeControl,
27
+ } from "../shared/attribute-value";
28
+
29
+ export { attributeInputValue as fieldInputValue } from "../shared/attribute-value";
30
+
31
+ /** Up to six pinned attributes sit above the rest of the panel. */
32
+ export const MAX_HIGHLIGHTS = 6;
33
+
34
+ export interface CrmRecordPageEntry {
35
+ id: string;
36
+ listId: string;
37
+ recordId: string;
38
+ position: number;
39
+ createdAt: string;
40
+ createdByActorType: string;
41
+ createdByActorId: string | null;
42
+ values: Record<string, CrmValue>;
43
+ valuesSince: Record<string, string>;
44
+ }
45
+
46
+ export interface CrmRecordPageListAttribute {
47
+ id: string;
48
+ apiSlug: string;
49
+ label: string;
50
+ description: string | null;
51
+ attributeType: CrmAttributeType;
52
+ multi: boolean;
53
+ required: boolean;
54
+ position: number;
55
+ usesOptions: boolean;
56
+ options: CrmAttributeOption[];
57
+ }
58
+
59
+ export interface CrmRecordPageList {
60
+ id: string;
61
+ name: string;
62
+ apiSlug: string;
63
+ parentObjectType: string;
64
+ attributes: CrmRecordPageListAttribute[];
65
+ entries: CrmRecordPageEntry[];
66
+ }
67
+
68
+ export interface CrmRecordPageValueMeta {
69
+ since: string;
70
+ actorType: string;
71
+ actorId: string | null;
72
+ }
73
+
74
+ export interface CrmRecordPage {
75
+ record: {
76
+ id: string;
77
+ connectionId: string;
78
+ provider: string;
79
+ objectType: string;
80
+ kind: string;
81
+ displayName: string;
82
+ remoteRevision: string | null;
83
+ updatedAt: string;
84
+ };
85
+ attributes: CrmAttributeDefinition[];
86
+ values: Record<string, CrmValue>;
87
+ valueMeta: Record<string, CrmRecordPageValueMeta>;
88
+ lists: CrmRecordPageList[];
89
+ listMembershipsTruncated: boolean;
90
+ recordUrl: string | null;
91
+ recordUrlUnavailableReason: string | null;
92
+ }
93
+
94
+ // ---------------------------------------------------------------------------
95
+ // Editability
96
+ // ---------------------------------------------------------------------------
97
+
98
+ /**
99
+ * Controls this panel implements. The registry says a `record-reference` is
100
+ * edited with a record picker; a labelled panel row has no picker yet, so the
101
+ * field is honestly locked here instead of offering a text input that would
102
+ * store whatever display name the user typed.
103
+ */
104
+ const PANEL_CONTROLS: ReadonlySet<CrmAttributeControl> = new Set([
105
+ "text",
106
+ "number",
107
+ "checkbox",
108
+ "date",
109
+ "datetime",
110
+ "options",
111
+ "rating",
112
+ ]);
113
+
114
+ export type FieldLockReason =
115
+ | "archived"
116
+ | "read-only"
117
+ | "redacted"
118
+ | "provider-owned"
119
+ | "derived"
120
+ | "unsupported-type";
121
+
122
+ export type FieldEditability =
123
+ | { editable: true }
124
+ | { editable: false; reason: FieldLockReason };
125
+
126
+ /**
127
+ * Mirrors what `update-crm-record` will actually accept for `target: "local"`.
128
+ * Offering an editor the action would reject is the same class of lie as a
129
+ * success-shaped failure.
130
+ */
131
+ export function fieldEditability(
132
+ attribute: Pick<
133
+ CrmAttributeDefinition,
134
+ "attributeType" | "archived" | "storagePolicy" | "updateable"
135
+ >,
136
+ ): FieldEditability {
137
+ if (attribute.archived) return { editable: false, reason: "archived" };
138
+ if (attribute.storagePolicy === "redacted")
139
+ return { editable: false, reason: "redacted" };
140
+ if (!attribute.updateable) return { editable: false, reason: "read-only" };
141
+ if (attribute.storagePolicy === "derived-local")
142
+ return { editable: false, reason: "derived" };
143
+ if (attribute.storagePolicy !== "local-authoritative")
144
+ return { editable: false, reason: "provider-owned" };
145
+ if (
146
+ !PANEL_CONTROLS.has(ATTRIBUTE_VALUE_SPECS[attribute.attributeType].control)
147
+ )
148
+ return { editable: false, reason: "unsupported-type" };
149
+ return { editable: true };
150
+ }
151
+
152
+ // ---------------------------------------------------------------------------
153
+ // Display
154
+ // ---------------------------------------------------------------------------
155
+
156
+ export type FieldDisplay =
157
+ | { kind: "empty" }
158
+ | { kind: "boolean"; value: boolean }
159
+ | { kind: "text"; text: string }
160
+ | { kind: "tokens"; tokens: Array<{ label: string; color?: string }> }
161
+ | { kind: "structured"; text: string };
162
+
163
+ const STRUCTURED_PREVIEW_LIMIT = 200;
164
+
165
+ /**
166
+ * The panel's own display union. Which chips a value produces, how a currency
167
+ * or a timestamp reads, and which option a value resolves to all come from the
168
+ * shared registry — only the union the panel renders against lives here.
169
+ */
170
+ export function formatFieldValue(
171
+ attribute: Pick<
172
+ CrmAttributeDefinition,
173
+ "attributeType" | "multi" | "options" | "config"
174
+ >,
175
+ value: CrmValue | undefined,
176
+ ): FieldDisplay {
177
+ if (value === undefined || value === null) return { kind: "empty" };
178
+
179
+ if (Array.isArray(value)) {
180
+ const tokens = valueTokens(attribute, value);
181
+ return tokens.length ? { kind: "tokens", tokens } : { kind: "empty" };
182
+ }
183
+
184
+ if (typeof value === "boolean") return { kind: "boolean", value };
185
+
186
+ if (typeof value === "object") {
187
+ const text = formatAttributeValue(attribute, value);
188
+ return {
189
+ kind: "structured",
190
+ text:
191
+ text.length > STRUCTURED_PREVIEW_LIMIT
192
+ ? `${text.slice(0, STRUCTURED_PREVIEW_LIMIT)}…`
193
+ : text,
194
+ };
195
+ }
196
+
197
+ if (value === "") return { kind: "empty" };
198
+ if (ATTRIBUTE_TYPE_SPECS[attribute.attributeType].usesOptions)
199
+ return { kind: "tokens", tokens: valueTokens(attribute, value) };
200
+ return { kind: "text", text: formatAttributeValue(attribute, value) };
201
+ }
202
+
203
+ // ---------------------------------------------------------------------------
204
+ // Input parsing
205
+ // ---------------------------------------------------------------------------
206
+
207
+ export type FieldParseResult =
208
+ | { ok: true; value: CrmValue }
209
+ | {
210
+ ok: false;
211
+ code: "not-a-number" | "not-a-date" | "unknown-option" | "not-editable";
212
+ };
213
+
214
+ /**
215
+ * Parse a raw editor value into the typed `CrmValue` the action expects.
216
+ * `multi` attributes take a comma-separated list.
217
+ *
218
+ * The editability gate runs first, so this panel never parses a field
219
+ * `update-crm-record` would refuse; everything past it is the shared parser.
220
+ *
221
+ * ponytail: comma-separated text for multi values instead of a token input —
222
+ * upgrade to a real token editor when a customer keeps commas inside a value.
223
+ */
224
+ export function parseFieldInput(
225
+ attribute: Pick<
226
+ CrmAttributeDefinition,
227
+ | "attributeType"
228
+ | "multi"
229
+ | "options"
230
+ | "archived"
231
+ | "storagePolicy"
232
+ | "updateable"
233
+ >,
234
+ raw: string | boolean,
235
+ ): FieldParseResult {
236
+ if (!fieldEditability(attribute).editable)
237
+ return { ok: false, code: "not-editable" };
238
+ if (typeof raw === "boolean") return { ok: true, value: raw };
239
+
240
+ const parsed = parseAttributeValue(attribute, raw);
241
+ if (parsed.ok) return { ok: true, value: parsed.value };
242
+ // `read-only` cannot come back here — the gate above already refused those —
243
+ // but mapping it keeps a future registry change from turning into a wrong
244
+ // "invalid number" message.
245
+ return {
246
+ ok: false,
247
+ code: parsed.reason === "read-only" ? "not-editable" : parsed.reason,
248
+ };
249
+ }
250
+
251
+ /**
252
+ * A list attribute in the shape the field editors take. List attributes are
253
+ * local-authoritative on every backend by construction — `loadCrmListAttributes`
254
+ * refuses to return one that is not — so they carry no provider authority to
255
+ * check, and the summary the action returns simply omits those columns.
256
+ */
257
+ export function entryAttributeAsEditable(
258
+ attribute: CrmRecordPageListAttribute,
259
+ ): Pick<
260
+ CrmAttributeDefinition,
261
+ | "apiSlug"
262
+ | "label"
263
+ | "attributeType"
264
+ | "multi"
265
+ | "options"
266
+ | "config"
267
+ | "archived"
268
+ | "storagePolicy"
269
+ | "updateable"
270
+ | "required"
271
+ > {
272
+ return {
273
+ apiSlug: attribute.apiSlug,
274
+ label: attribute.label,
275
+ attributeType: attribute.attributeType,
276
+ multi: attribute.multi,
277
+ options: attribute.options,
278
+ config: {},
279
+ archived: false,
280
+ storagePolicy: "local-authoritative",
281
+ updateable: true,
282
+ required: attribute.required,
283
+ };
284
+ }
285
+
286
+ // ---------------------------------------------------------------------------
287
+ // Highlights
288
+ // ---------------------------------------------------------------------------
289
+
290
+ /**
291
+ * Curated highlight order per object `kind`. An attribute this app's schema
292
+ * does not declare for that kind (e.g. `people` has no `name`) is simply
293
+ * skipped by `splitHighlights`, which is how "name, or firstName+lastName
294
+ * when there is no name" falls out of one flat list instead of a branch.
295
+ */
296
+ const HIGHLIGHT_ORDER: Record<string, readonly string[]> = {
297
+ account: ["name", "domain", "industry", "ownerName", "nextContactAt"],
298
+ person: [
299
+ "name",
300
+ "firstName",
301
+ "lastName",
302
+ "email",
303
+ "title",
304
+ "accountId",
305
+ "ownerName",
306
+ ],
307
+ opportunity: ["name", "amount", "stage", "closeDate", "ownerName"],
308
+ };
309
+
310
+ function hasHighlightValue(value: CrmValue | undefined): boolean {
311
+ if (value === undefined || value === null || value === "") return false;
312
+ return !Array.isArray(value) || value.length > 0;
313
+ }
314
+
315
+ /**
316
+ * Highlights are a curated order for `account`/`person`/`opportunity`, and the
317
+ * plain position order for `custom` or any other kind. There is no pinned
318
+ * column on `crm_field_policies`, and inventing one here would be a schema
319
+ * change in a table another slice owns — reordering the attribute is how a
320
+ * user pins one today.
321
+ *
322
+ * A curated slot the schema does not declare is skipped, and leftover slots
323
+ * backfill from position order, preferring an attribute that already carries
324
+ * a value — otherwise a schema with a short curated list would fill its
325
+ * remaining slots with guaranteed-empty attributes, reproducing the "mostly
326
+ * Empty" bug this replaces.
327
+ */
328
+ export function splitHighlights<
329
+ T extends { apiSlug: string; position: number },
330
+ >(
331
+ attributes: T[],
332
+ options: {
333
+ kind?: string;
334
+ values?: Record<string, CrmValue>;
335
+ max?: number;
336
+ } = {},
337
+ ): { highlights: T[]; rest: T[] } {
338
+ const { kind = "custom", values = {}, max = MAX_HIGHLIGHTS } = options;
339
+ const ordered = [...attributes].sort((a, b) => a.position - b.position);
340
+ const curated = HIGHLIGHT_ORDER[kind];
341
+ if (!curated) {
342
+ return { highlights: ordered.slice(0, max), rest: ordered.slice(max) };
343
+ }
344
+
345
+ const byApiSlug = new Map(
346
+ ordered.map((attribute) => [attribute.apiSlug, attribute]),
347
+ );
348
+ const picked: T[] = [];
349
+ const pickedSlugs = new Set<string>();
350
+ for (const slug of curated) {
351
+ if (picked.length >= max) break;
352
+ const attribute = byApiSlug.get(slug);
353
+ if (!attribute) continue;
354
+ picked.push(attribute);
355
+ pickedSlugs.add(slug);
356
+ }
357
+ if (picked.length < max) {
358
+ const filler = ordered
359
+ .filter((attribute) => !pickedSlugs.has(attribute.apiSlug))
360
+ .sort((a, b) => {
361
+ const byValue =
362
+ Number(hasHighlightValue(values[b.apiSlug])) -
363
+ Number(hasHighlightValue(values[a.apiSlug]));
364
+ return byValue !== 0 ? byValue : a.position - b.position;
365
+ });
366
+ for (const attribute of filler) {
367
+ if (picked.length >= max) break;
368
+ picked.push(attribute);
369
+ pickedSlugs.add(attribute.apiSlug);
370
+ }
371
+ }
372
+ return {
373
+ highlights: picked,
374
+ rest: ordered.filter((attribute) => !pickedSlugs.has(attribute.apiSlug)),
375
+ };
376
+ }
377
+
378
+ // ---------------------------------------------------------------------------
379
+ // Duplicate attributes
380
+ // ---------------------------------------------------------------------------
381
+
382
+ function sameCrmValue(a: CrmValue, b: CrmValue | undefined): boolean {
383
+ if (a === b) return true;
384
+ if (a === null || b === null || b === undefined) return false;
385
+ if (typeof a === "object" || typeof b === "object") {
386
+ return JSON.stringify(a) === JSON.stringify(b);
387
+ }
388
+ return false;
389
+ }
390
+
391
+ /**
392
+ * `displayName` duplicates `name` when the native adapter minted both from
393
+ * the same write (`server/crm/native-adapter.ts` has stopped minting
394
+ * `displayName` going forward, but existing rows still carry both). Suppress
395
+ * only when `name` has a real value equal to `displayName` — an absent or
396
+ * cleared `name` must not read the same as a suppressed duplicate, so
397
+ * `displayName` keeps showing.
398
+ */
399
+ export function isSuppressedDuplicateAttribute(
400
+ apiSlug: string,
401
+ values: Record<string, CrmValue>,
402
+ ): boolean {
403
+ if (apiSlug !== "displayName") return false;
404
+ const name = values.name;
405
+ if (!name) return false;
406
+ return sameCrmValue(name, values.displayName);
407
+ }
408
+
409
+ /** Drop attributes the panel should not render twice — see above. */
410
+ export function withoutSuppressedDuplicates<T extends { apiSlug: string }>(
411
+ attributes: T[],
412
+ values: Record<string, CrmValue>,
413
+ ): T[] {
414
+ return attributes.filter(
415
+ (attribute) => !isSuppressedDuplicateAttribute(attribute.apiSlug, values),
416
+ );
417
+ }
418
+
419
+ // ---------------------------------------------------------------------------
420
+ // History
421
+ // ---------------------------------------------------------------------------
422
+
423
+ export interface FieldHistoryChange {
424
+ id: string;
425
+ value: CrmValue;
426
+ activeFrom: string;
427
+ activeUntil: string | null;
428
+ current: boolean;
429
+ actorType: string;
430
+ actorId: string | null;
431
+ }
432
+
433
+ export interface FieldHistoryResponse {
434
+ recordId: string;
435
+ entryId: string | null;
436
+ apiSlug: string;
437
+ label: string;
438
+ attributeType: CrmAttributeType;
439
+ multi: boolean;
440
+ historyTracked: boolean;
441
+ changes: FieldHistoryChange[];
442
+ }
443
+
444
+ export interface FieldHistoryTransition {
445
+ id: string;
446
+ from: CrmValue | undefined;
447
+ to: CrmValue;
448
+ at: string;
449
+ actorType: string;
450
+ actorId: string | null;
451
+ }
452
+
453
+ /**
454
+ * Turn the newest-first rows into "from → to" transitions. The oldest row has
455
+ * no predecessor, so its `from` is `undefined` — deliberately not `null`, which
456
+ * is a real stored value meaning the field was cleared.
457
+ */
458
+ export function historyTransitions(
459
+ changes: FieldHistoryChange[],
460
+ ): FieldHistoryTransition[] {
461
+ return changes.map((change, index) => {
462
+ const previous = changes[index + 1];
463
+ return {
464
+ id: change.id,
465
+ from: previous ? previous.value : undefined,
466
+ to: change.value,
467
+ at: change.activeFrom,
468
+ actorType: change.actorType,
469
+ actorId: change.actorId,
470
+ };
471
+ });
472
+ }
473
+
474
+ // ---------------------------------------------------------------------------
475
+ // Activity
476
+ // ---------------------------------------------------------------------------
477
+
478
+ export interface CrmActivityItem {
479
+ id: string;
480
+ title: string;
481
+ summary?: string;
482
+ occurredAt?: string;
483
+ actor?: string;
484
+ }
485
+
486
+ /**
487
+ * Nothing in this app writes `crm_interactions` yet, so an empty Activity tab
488
+ * means "we are not ingesting" and never "nothing happened". The UI must print
489
+ * that difference instead of an ambiguous "No activity".
490
+ */
491
+ export type ActivityState =
492
+ | { kind: "not-ingested" }
493
+ | { kind: "items"; items: CrmActivityItem[] };
494
+
495
+ export function resolveActivityState(
496
+ items: CrmActivityItem[] | undefined,
497
+ ): ActivityState {
498
+ return items && items.length
499
+ ? { kind: "items", items }
500
+ : { kind: "not-ingested" };
501
+ }
502
+
503
+ // ---------------------------------------------------------------------------
504
+ // Optimistic value edits
505
+ // ---------------------------------------------------------------------------
506
+
507
+ export interface OptimisticFieldEdit {
508
+ apiSlug: string;
509
+ previousValue: CrmValue | undefined;
510
+ previousMeta: CrmRecordPageValueMeta | undefined;
511
+ }
512
+
513
+ /**
514
+ * Apply an edit to a cached page and hand back what is needed to undo it. The
515
+ * caller restores with `rollbackFieldValue` when the action rejects, so a
516
+ * failed write never leaves the panel showing a value the server does not have.
517
+ */
518
+ export function applyFieldValue(
519
+ page: CrmRecordPage,
520
+ apiSlug: string,
521
+ value: CrmValue,
522
+ meta: CrmRecordPageValueMeta,
523
+ ): { page: CrmRecordPage; edit: OptimisticFieldEdit } {
524
+ return {
525
+ page: {
526
+ ...page,
527
+ values: { ...page.values, [apiSlug]: value },
528
+ valueMeta: { ...page.valueMeta, [apiSlug]: meta },
529
+ },
530
+ edit: {
531
+ apiSlug,
532
+ previousValue: page.values[apiSlug],
533
+ previousMeta: page.valueMeta[apiSlug],
534
+ },
535
+ };
536
+ }
537
+
538
+ export function rollbackFieldValue(
539
+ page: CrmRecordPage,
540
+ edit: OptimisticFieldEdit,
541
+ ): CrmRecordPage {
542
+ const values = { ...page.values };
543
+ const valueMeta = { ...page.valueMeta };
544
+ if (edit.previousValue === undefined) delete values[edit.apiSlug];
545
+ else values[edit.apiSlug] = edit.previousValue;
546
+ if (edit.previousMeta === undefined) delete valueMeta[edit.apiSlug];
547
+ else valueMeta[edit.apiSlug] = edit.previousMeta;
548
+ return { ...page, values, valueMeta };
549
+ }
550
+
551
+ export function applyEntryValue(
552
+ page: CrmRecordPage,
553
+ entryId: string,
554
+ apiSlug: string,
555
+ value: CrmValue,
556
+ ): { page: CrmRecordPage; previousValue: CrmValue | undefined } {
557
+ let previousValue: CrmValue | undefined;
558
+ const lists = page.lists.map((list) => ({
559
+ ...list,
560
+ entries: list.entries.map((entry) => {
561
+ if (entry.id !== entryId) return entry;
562
+ previousValue = entry.values[apiSlug];
563
+ return { ...entry, values: { ...entry.values, [apiSlug]: value } };
564
+ }),
565
+ }));
566
+ return { page: { ...page, lists }, previousValue };
567
+ }
568
+
569
+ export function rollbackEntryValue(
570
+ page: CrmRecordPage,
571
+ entryId: string,
572
+ apiSlug: string,
573
+ previousValue: CrmValue | undefined,
574
+ ): CrmRecordPage {
575
+ return {
576
+ ...page,
577
+ lists: page.lists.map((list) => ({
578
+ ...list,
579
+ entries: list.entries.map((entry) => {
580
+ if (entry.id !== entryId) return entry;
581
+ const values = { ...entry.values };
582
+ if (previousValue === undefined) delete values[apiSlug];
583
+ else values[apiSlug] = previousValue;
584
+ return { ...entry, values };
585
+ }),
586
+ })),
587
+ };
588
+ }
589
+
590
+ // ---------------------------------------------------------------------------
591
+ // Tabs
592
+ // ---------------------------------------------------------------------------
593
+
594
+ export const RECORD_TABS = ["activity", "notes", "tasks", "related"] as const;
595
+
596
+ export type RecordTab = (typeof RECORD_TABS)[number];
597
+
598
+ export function isRecordTab(value: unknown): value is RecordTab {
599
+ return (
600
+ typeof value === "string" &&
601
+ (RECORD_TABS as readonly string[]).includes(value)
602
+ );
603
+ }
@@ -0,0 +1,49 @@
1
+ import { useT } from "@agent-native/core/client/i18n";
2
+ import { IconDatabaseCog, IconRefresh } from "@tabler/icons-react";
3
+ import { Link } from "react-router";
4
+
5
+ import { Button } from "@/components/ui/button";
6
+
7
+ /**
8
+ * The irreversible-looking corner of CRM settings. It is deliberately thin:
9
+ * CRM has no delete path. Archiving an attribute, an option, or a list keeps
10
+ * every stored value, which is the one thing worth stating here.
11
+ */
12
+ export function AdvancedSettings() {
13
+ const t = useT();
14
+ return (
15
+ <div className="mx-auto w-full max-w-2xl">
16
+ <h1 className="text-xl font-semibold tracking-tight">
17
+ {t("advanced.title")}
18
+ </h1>
19
+ <p className="mt-1 text-sm leading-6 text-muted-foreground">
20
+ {t("advanced.description")}
21
+ </p>
22
+
23
+ <div className="mt-6 grid gap-3">
24
+ <section className="flex flex-wrap items-center gap-4 rounded-lg border border-border/70 bg-card px-4 py-3.5">
25
+ <IconRefresh className="size-4 shrink-0 text-muted-foreground" />
26
+ <div className="min-w-0 flex-1">
27
+ <p className="text-sm font-medium">{t("advanced.reconfigure")}</p>
28
+ <p className="mt-1 text-xs leading-5 text-muted-foreground">
29
+ {t("advanced.reconfigureHelp")}
30
+ </p>
31
+ </div>
32
+ <Button asChild variant="outline" size="sm">
33
+ <Link to="/setup">{t("advanced.openSetup")}</Link>
34
+ </Button>
35
+ </section>
36
+
37
+ <section className="flex flex-wrap items-center gap-4 rounded-lg border border-border/70 bg-card px-4 py-3.5">
38
+ <IconDatabaseCog className="size-4 shrink-0 text-muted-foreground" />
39
+ <div className="min-w-0 flex-1">
40
+ <p className="text-sm font-medium">{t("advanced.retention")}</p>
41
+ <p className="mt-1 text-xs leading-5 text-muted-foreground">
42
+ {t("advanced.retentionHelp")}
43
+ </p>
44
+ </div>
45
+ </section>
46
+ </div>
47
+ </div>
48
+ );
49
+ }