@agent-native/core 0.123.0 → 0.123.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 (263) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +16 -0
  3. package/corpus/core/package.json +1 -1
  4. package/corpus/core/src/cli/templates-meta.ts +12 -0
  5. package/corpus/templates/crm/.agents/skills/agent-native-docs/SKILL.md +115 -0
  6. package/corpus/templates/crm/.agents/skills/agent-native-toolkit/SKILL.md +177 -0
  7. package/corpus/templates/crm/.agents/skills/crm/SKILL.md +205 -0
  8. package/corpus/templates/crm/.agents/skills/customizing-agent-native/SKILL.md +220 -0
  9. package/corpus/templates/crm/.agents/skills/feature-flags/SKILL.md +169 -0
  10. package/corpus/templates/crm/.agents/skills/sharing/SKILL.md +228 -0
  11. package/corpus/templates/crm/.agents/skills/storing-data/SKILL.md +178 -0
  12. package/corpus/templates/crm/.agents/skills/upgrade-agent-native/SKILL.md +122 -0
  13. package/corpus/templates/crm/.env.example +5 -0
  14. package/corpus/templates/crm/AGENTS.md +82 -0
  15. package/corpus/templates/crm/CHANGELOG.md +25 -0
  16. package/corpus/templates/crm/DEVELOPING.md +66 -0
  17. package/corpus/templates/crm/README.md +122 -0
  18. package/corpus/templates/crm/_gitignore +12 -0
  19. package/corpus/templates/crm/actions/_crm-action-utils.ts +98 -0
  20. package/corpus/templates/crm/actions/_crm-attribute-utils.ts +219 -0
  21. package/corpus/templates/crm/actions/_crm-dashboard.ts +69 -0
  22. package/corpus/templates/crm/actions/_crm-data-program-actions.ts +15 -0
  23. package/corpus/templates/crm/actions/_crm-list-utils.ts +775 -0
  24. package/corpus/templates/crm/actions/_crm-signal-utils.ts +79 -0
  25. package/corpus/templates/crm/actions/add-crm-record-to-list.ts +164 -0
  26. package/corpus/templates/crm/actions/apply-crm-proposals.ts +291 -0
  27. package/corpus/templates/crm/actions/archive-crm-attribute.ts +69 -0
  28. package/corpus/templates/crm/actions/attach-call-evidence.ts +170 -0
  29. package/corpus/templates/crm/actions/configure-crm-connection.ts +126 -0
  30. package/corpus/templates/crm/actions/configure-native-crm.ts +44 -0
  31. package/corpus/templates/crm/actions/create-crm-attribute.ts +350 -0
  32. package/corpus/templates/crm/actions/create-crm-list.ts +194 -0
  33. package/corpus/templates/crm/actions/create-crm-record.ts +148 -0
  34. package/corpus/templates/crm/actions/create-crm-signal-tracker.ts +58 -0
  35. package/corpus/templates/crm/actions/delete-crm-saved-view.ts +68 -0
  36. package/corpus/templates/crm/actions/delete-staged-dataset.ts +14 -0
  37. package/corpus/templates/crm/actions/estimate-crm-enrichment.ts +229 -0
  38. package/corpus/templates/crm/actions/find-crm-duplicates.ts +87 -0
  39. package/corpus/templates/crm/actions/get-crm-automation-recipe.ts +36 -0
  40. package/corpus/templates/crm/actions/get-crm-dashboard-panel.ts +35 -0
  41. package/corpus/templates/crm/actions/get-crm-dashboard.ts +14 -0
  42. package/corpus/templates/crm/actions/get-crm-overview.ts +13 -0
  43. package/corpus/templates/crm/actions/get-crm-pipeline-data.ts +61 -0
  44. package/corpus/templates/crm/actions/get-crm-record-page.ts +287 -0
  45. package/corpus/templates/crm/actions/get-crm-record.ts +75 -0
  46. package/corpus/templates/crm/actions/get-crm-workspace.ts +364 -0
  47. package/corpus/templates/crm/actions/install-crm-pipeline-dashboard.ts +81 -0
  48. package/corpus/templates/crm/actions/list-crm-attributes.ts +71 -0
  49. package/corpus/templates/crm/actions/list-crm-connections.ts +68 -0
  50. package/corpus/templates/crm/actions/list-crm-dashboard-revisions.ts +15 -0
  51. package/corpus/templates/crm/actions/list-crm-dashboards.ts +15 -0
  52. package/corpus/templates/crm/actions/list-crm-enrichment-slots.ts +28 -0
  53. package/corpus/templates/crm/actions/list-crm-list-entries.ts +192 -0
  54. package/corpus/templates/crm/actions/list-crm-lists.ts +80 -0
  55. package/corpus/templates/crm/actions/list-crm-proposals.ts +55 -0
  56. package/corpus/templates/crm/actions/list-crm-record-field-history.ts +201 -0
  57. package/corpus/templates/crm/actions/list-crm-record-values.ts +200 -0
  58. package/corpus/templates/crm/actions/list-crm-records.ts +103 -0
  59. package/corpus/templates/crm/actions/list-crm-saved-views.ts +66 -0
  60. package/corpus/templates/crm/actions/list-crm-signal-hits.ts +43 -0
  61. package/corpus/templates/crm/actions/list-crm-signal-trackers.ts +32 -0
  62. package/corpus/templates/crm/actions/list-crm-tasks.ts +22 -0
  63. package/corpus/templates/crm/actions/list-staged-datasets.ts +14 -0
  64. package/corpus/templates/crm/actions/list-workspace-connections.ts +47 -0
  65. package/corpus/templates/crm/actions/manage-crm-attribute-option.ts +272 -0
  66. package/corpus/templates/crm/actions/manage-crm-signal-tracker.ts +87 -0
  67. package/corpus/templates/crm/actions/manage-crm-task.ts +132 -0
  68. package/corpus/templates/crm/actions/merge-crm-records.ts +691 -0
  69. package/corpus/templates/crm/actions/navigate.ts +54 -0
  70. package/corpus/templates/crm/actions/provider-api-catalog.ts +22 -0
  71. package/corpus/templates/crm/actions/provider-api-docs.ts +54 -0
  72. package/corpus/templates/crm/actions/provider-api-request.ts +92 -0
  73. package/corpus/templates/crm/actions/query-staged-dataset.ts +44 -0
  74. package/corpus/templates/crm/actions/record-crm-call-insight.ts +160 -0
  75. package/corpus/templates/crm/actions/record-crm-smart-signal.ts +150 -0
  76. package/corpus/templates/crm/actions/remove-crm-list-entry.ts +79 -0
  77. package/corpus/templates/crm/actions/restore-crm-dashboard-revision.ts +23 -0
  78. package/corpus/templates/crm/actions/review-crm-signal.ts +31 -0
  79. package/corpus/templates/crm/actions/run-crm-attribute-fill.ts +443 -0
  80. package/corpus/templates/crm/actions/run-crm-enrichment.ts +647 -0
  81. package/corpus/templates/crm/actions/run-crm-saved-view-program.ts +103 -0
  82. package/corpus/templates/crm/actions/run-crm-signal-trackers.ts +265 -0
  83. package/corpus/templates/crm/actions/run.ts +7 -0
  84. package/corpus/templates/crm/actions/save-crm-dashboard.ts +51 -0
  85. package/corpus/templates/crm/actions/save-crm-saved-view.ts +296 -0
  86. package/corpus/templates/crm/actions/sync-crm.ts +146 -0
  87. package/corpus/templates/crm/actions/update-crm-attribute.ts +111 -0
  88. package/corpus/templates/crm/actions/update-crm-list-entry.ts +102 -0
  89. package/corpus/templates/crm/actions/update-crm-list.ts +97 -0
  90. package/corpus/templates/crm/actions/update-crm-record.ts +543 -0
  91. package/corpus/templates/crm/actions/view-screen.ts +324 -0
  92. package/corpus/templates/crm/agent-native.app-skill.json +69 -0
  93. package/corpus/templates/crm/app/components/crm/CreateCrmRecordDialog.tsx +158 -0
  94. package/corpus/templates/crm/app/components/crm/CrmDashboardPanel.tsx +78 -0
  95. package/corpus/templates/crm/app/components/crm/CrmSignalsPanel.tsx +220 -0
  96. package/corpus/templates/crm/app/components/crm/IntelligenceSettings.tsx +414 -0
  97. package/corpus/templates/crm/app/components/crm/RecordActions.tsx +648 -0
  98. package/corpus/templates/crm/app/components/crm/RecordGrid.tsx +488 -0
  99. package/corpus/templates/crm/app/components/crm/RecordWorkspace.tsx +232 -0
  100. package/corpus/templates/crm/app/components/crm/SavedViewDataProgram.tsx +93 -0
  101. package/corpus/templates/crm/app/components/crm/Surface.tsx +97 -0
  102. package/corpus/templates/crm/app/components/crm/WorkOverview.tsx +180 -0
  103. package/corpus/templates/crm/app/components/crm/board/CrmBoard.tsx +988 -0
  104. package/corpus/templates/crm/app/components/crm/board/board-model.ts +399 -0
  105. package/corpus/templates/crm/app/components/crm/board/entry-filter.ts +150 -0
  106. package/corpus/templates/crm/app/components/crm/board/view-draft.ts +227 -0
  107. package/corpus/templates/crm/app/components/crm/grid/CrmGrid.tsx +1033 -0
  108. package/corpus/templates/crm/app/components/crm/grid/GridCell.tsx +620 -0
  109. package/corpus/templates/crm/app/components/crm/grid/clipboard.ts +84 -0
  110. package/corpus/templates/crm/app/components/crm/grid/model.ts +304 -0
  111. package/corpus/templates/crm/app/components/crm/grid/navigation.ts +220 -0
  112. package/corpus/templates/crm/app/components/crm/grid/query.ts +214 -0
  113. package/corpus/templates/crm/app/components/crm/record/AttributePanel.tsx +192 -0
  114. package/corpus/templates/crm/app/components/crm/record/FieldHistory.tsx +179 -0
  115. package/corpus/templates/crm/app/components/crm/record/RecordHeader.tsx +103 -0
  116. package/corpus/templates/crm/app/components/crm/record/RecordLists.tsx +235 -0
  117. package/corpus/templates/crm/app/components/crm/record/RecordTabs.tsx +206 -0
  118. package/corpus/templates/crm/app/components/crm/record/field-editors.tsx +245 -0
  119. package/corpus/templates/crm/app/components/crm/record/record-data.ts +603 -0
  120. package/corpus/templates/crm/app/components/crm/settings/AdvancedSettings.tsx +49 -0
  121. package/corpus/templates/crm/app/components/crm/settings/AttributeOptionsEditor.tsx +441 -0
  122. package/corpus/templates/crm/app/components/crm/settings/ConnectionSettings.tsx +169 -0
  123. package/corpus/templates/crm/app/components/crm/settings/FieldsSettings.tsx +935 -0
  124. package/corpus/templates/crm/app/components/crm/settings/ListsSettings.tsx +339 -0
  125. package/corpus/templates/crm/app/components/crm/settings/settings-admin.ts +377 -0
  126. package/corpus/templates/crm/app/components/crm/shared/AttributeValueParts.tsx +69 -0
  127. package/corpus/templates/crm/app/components/crm/shared/attribute-value.ts +591 -0
  128. package/corpus/templates/crm/app/components/layout/CrmCommandMenu.tsx +556 -0
  129. package/corpus/templates/crm/app/components/layout/CrmLayout.tsx +130 -0
  130. package/corpus/templates/crm/app/components/layout/CrmSidebar.tsx +407 -0
  131. package/corpus/templates/crm/app/components/layout/command-palette-search.ts +126 -0
  132. package/corpus/templates/crm/app/components/layout/crm-ui-intents.ts +41 -0
  133. package/corpus/templates/crm/app/components/layout/sidebar-collapse.ts +40 -0
  134. package/corpus/templates/crm/app/components/layout/sidebar-lists.ts +109 -0
  135. package/corpus/templates/crm/app/components/ui/alert-dialog.tsx +1 -0
  136. package/corpus/templates/crm/app/components/ui/badge.tsx +1 -0
  137. package/corpus/templates/crm/app/components/ui/button.tsx +1 -0
  138. package/corpus/templates/crm/app/components/ui/card.tsx +1 -0
  139. package/corpus/templates/crm/app/components/ui/checkbox.tsx +1 -0
  140. package/corpus/templates/crm/app/components/ui/dialog.tsx +1 -0
  141. package/corpus/templates/crm/app/components/ui/dropdown-menu.tsx +1 -0
  142. package/corpus/templates/crm/app/components/ui/hover-card.tsx +1 -0
  143. package/corpus/templates/crm/app/components/ui/input.tsx +1 -0
  144. package/corpus/templates/crm/app/components/ui/label.tsx +1 -0
  145. package/corpus/templates/crm/app/components/ui/popover.tsx +1 -0
  146. package/corpus/templates/crm/app/components/ui/select.tsx +1 -0
  147. package/corpus/templates/crm/app/components/ui/sheet.tsx +1 -0
  148. package/corpus/templates/crm/app/components/ui/skeleton.tsx +1 -0
  149. package/corpus/templates/crm/app/components/ui/switch.tsx +1 -0
  150. package/corpus/templates/crm/app/components/ui/table.tsx +1 -0
  151. package/corpus/templates/crm/app/components/ui/tabs.tsx +1 -0
  152. package/corpus/templates/crm/app/components/ui/textarea.tsx +1 -0
  153. package/corpus/templates/crm/app/components/ui/toolkit-provider.tsx +17 -0
  154. package/corpus/templates/crm/app/components/ui/tooltip.tsx +1 -0
  155. package/corpus/templates/crm/app/design-system.ts +3 -0
  156. package/corpus/templates/crm/app/entry.client.tsx +19 -0
  157. package/corpus/templates/crm/app/entry.server.tsx +10 -0
  158. package/corpus/templates/crm/app/global.css +167 -0
  159. package/corpus/templates/crm/app/hooks/use-keyboard-shortcuts.ts +206 -0
  160. package/corpus/templates/crm/app/hooks/use-navigation-state.ts +48 -0
  161. package/corpus/templates/crm/app/i18n/en-US.ts +684 -0
  162. package/corpus/templates/crm/app/i18n/index.ts +34 -0
  163. package/corpus/templates/crm/app/lib/dashboard.ts +13 -0
  164. package/corpus/templates/crm/app/lib/navigation.ts +17 -0
  165. package/corpus/templates/crm/app/lib/tab-id.ts +1 -0
  166. package/corpus/templates/crm/app/lib/types.ts +172 -0
  167. package/corpus/templates/crm/app/lib/utils.ts +1 -0
  168. package/corpus/templates/crm/app/root.tsx +93 -0
  169. package/corpus/templates/crm/app/routes/_index.tsx +26 -0
  170. package/corpus/templates/crm/app/routes/accounts.tsx +41 -0
  171. package/corpus/templates/crm/app/routes/agent.tsx +5 -0
  172. package/corpus/templates/crm/app/routes/ask.tsx +53 -0
  173. package/corpus/templates/crm/app/routes/dashboard.tsx +118 -0
  174. package/corpus/templates/crm/app/routes/lists.tsx +228 -0
  175. package/corpus/templates/crm/app/routes/opportunities.tsx +41 -0
  176. package/corpus/templates/crm/app/routes/people.tsx +41 -0
  177. package/corpus/templates/crm/app/routes/proposals.tsx +388 -0
  178. package/corpus/templates/crm/app/routes/records.$recordId.tsx +53 -0
  179. package/corpus/templates/crm/app/routes/records.tsx +64 -0
  180. package/corpus/templates/crm/app/routes/settings.tsx +118 -0
  181. package/corpus/templates/crm/app/routes/setup.tsx +308 -0
  182. package/corpus/templates/crm/app/routes/tasks.tsx +252 -0
  183. package/corpus/templates/crm/app/routes/views.tsx +683 -0
  184. package/corpus/templates/crm/app/routes.ts +19 -0
  185. package/corpus/templates/crm/app/vite-env.d.ts +6 -0
  186. 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
  187. package/corpus/templates/crm/changelog/2026-07-26-crm-now-finds-likely-duplicate-records-and-tells-you-exactly.md +6 -0
  188. package/corpus/templates/crm/changelog/2026-07-26-enrichment-now-shows-what-a-run-will-cost-before-it-starts-g.md +6 -0
  189. package/corpus/templates/crm/changelog/2026-07-26-lists-turn-any-set-of-records-into-a-pipeline-with-its-own-s.md +6 -0
  190. package/corpus/templates/crm/changelog/2026-07-26-moving-a-record-or-list-entry-into-a-stage-that-was-retired-.md +6 -0
  191. package/corpus/templates/crm/changelog/2026-07-26-opening-a-record-now-shows-one-page-with-its-fields-the-list.md +6 -0
  192. 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
  193. package/corpus/templates/crm/changelog/2026-07-26-provider-record-edits-now-show-an-exact-before-after-diff-an.md +6 -0
  194. package/corpus/templates/crm/changelog/2026-07-26-record-page-attribute-labels-now-read-in-full-instead-of-bei.md +6 -0
  195. package/corpus/templates/crm/changelog/2026-07-26-records-now-use-typed-fields-currency-dates-ratings-status-s.md +6 -0
  196. package/corpus/templates/crm/changelog/2026-07-26-retyping-a-value-on-the-record-page-now-replaces-it-instead-.md +6 -0
  197. package/corpus/templates/crm/changelog/2026-07-26-settings-now-has-fields-and-lists-tabs-for-adding-fields-edi.md +6 -0
  198. package/corpus/templates/crm/changelog/2026-07-26-the-proposal-review-now-shows-the-field-that-actually-change.md +6 -0
  199. package/corpus/templates/crm/changelog/2026-07-26-the-record-grid-is-now-a-real-spreadsheet-filter-sort-and-pa.md +6 -0
  200. package/corpus/templates/crm/components.json +20 -0
  201. package/corpus/templates/crm/docs/architecture/crm-contract.md +214 -0
  202. package/corpus/templates/crm/docs/features/README.md +55 -0
  203. package/corpus/templates/crm/docs/features/f1-records-and-fields.mdx +52 -0
  204. package/corpus/templates/crm/docs/features/f2-lists-pipelines-and-views.mdx +48 -0
  205. package/corpus/templates/crm/docs/features/f3-work-and-record-quality.mdx +40 -0
  206. package/corpus/templates/crm/docs/features/f4-evidence-and-chat.mdx +47 -0
  207. package/corpus/templates/crm/learnings.defaults.md +5 -0
  208. package/corpus/templates/crm/netlify.toml +11 -0
  209. package/corpus/templates/crm/package.json +61 -0
  210. package/corpus/templates/crm/public/favicon.svg +1 -0
  211. package/corpus/templates/crm/public/icon-180.svg +1 -0
  212. package/corpus/templates/crm/public/manifest.json +11 -0
  213. package/corpus/templates/crm/react-router.config.ts +7 -0
  214. package/corpus/templates/crm/server/crm/adapter.ts +32 -0
  215. package/corpus/templates/crm/server/crm/crm-field-firewall.ts +64 -0
  216. package/corpus/templates/crm/server/crm/crm-mirror.ts +721 -0
  217. package/corpus/templates/crm/server/crm/field-policy-attributes.ts +48 -0
  218. package/corpus/templates/crm/server/crm/hubspot-adapter.ts +1146 -0
  219. package/corpus/templates/crm/server/crm/native-adapter.ts +1810 -0
  220. package/corpus/templates/crm/server/crm/provider-record-link.ts +264 -0
  221. package/corpus/templates/crm/server/crm/read-through.ts +207 -0
  222. package/corpus/templates/crm/server/crm/salesforce-adapter.ts +1255 -0
  223. package/corpus/templates/crm/server/db/crm-store.ts +1112 -0
  224. package/corpus/templates/crm/server/db/index.ts +191 -0
  225. package/corpus/templates/crm/server/db/schema.ts +647 -0
  226. package/corpus/templates/crm/server/lib/crm-query.ts +1558 -0
  227. package/corpus/templates/crm/server/lib/dedupe.ts +714 -0
  228. package/corpus/templates/crm/server/lib/enrichment-cost.ts +381 -0
  229. package/corpus/templates/crm/server/lib/enrichment-slots.ts +519 -0
  230. package/corpus/templates/crm/server/lib/intelligence/default-detectors.ts +65 -0
  231. package/corpus/templates/crm/server/lib/intelligence/evidence.ts +105 -0
  232. package/corpus/templates/crm/server/lib/intelligence/keyword-detector.ts +94 -0
  233. package/corpus/templates/crm/server/lib/intelligence/smart-detector.ts +39 -0
  234. package/corpus/templates/crm/server/lib/intelligence/summary.ts +19 -0
  235. package/corpus/templates/crm/server/lib/lifecycle.ts +634 -0
  236. package/corpus/templates/crm/server/lib/provider-api.ts +64 -0
  237. package/corpus/templates/crm/server/middleware/auth.ts +4 -0
  238. package/corpus/templates/crm/server/plugins/agent-chat.ts +65 -0
  239. package/corpus/templates/crm/server/plugins/auth.ts +14 -0
  240. package/corpus/templates/crm/server/plugins/core-routes.ts +17 -0
  241. package/corpus/templates/crm/server/plugins/db.ts +738 -0
  242. package/corpus/templates/crm/server/routes/[...page].get.ts +5 -0
  243. package/corpus/templates/crm/shared/crm-attributes.ts +469 -0
  244. package/corpus/templates/crm/shared/crm-automation-recipes.ts +132 -0
  245. package/corpus/templates/crm/shared/crm-contract.ts +417 -0
  246. package/corpus/templates/crm/shared/crm-navigation.ts +190 -0
  247. package/corpus/templates/crm/shared/crm-sales-config.ts +63 -0
  248. package/corpus/templates/crm/ssr-entry.ts +11 -0
  249. package/corpus/templates/crm/tsconfig.json +24 -0
  250. package/corpus/templates/crm/vite.config.ts +15 -0
  251. package/corpus/templates/crm/vitest.config.ts +5 -0
  252. package/corpus/templates/design/app/i18n-data.ts +12 -12
  253. package/corpus/templates/design/changelog/2026-07-26-apply-design-updates-from-a-centered-top-toolbar-without-cov.md +6 -0
  254. package/dist/cli/templates-meta.d.ts.map +1 -1
  255. package/dist/cli/templates-meta.js +12 -0
  256. package/dist/cli/templates-meta.js.map +1 -1
  257. package/dist/collab/routes.d.ts +1 -1
  258. package/dist/observability/routes.d.ts +3 -3
  259. package/dist/resources/handlers.d.ts +1 -1
  260. package/dist/server/realtime-token.d.ts +1 -1
  261. package/dist/server/transcribe-voice.d.ts +1 -1
  262. package/package.json +1 -1
  263. package/src/cli/templates-meta.ts +12 -0
@@ -0,0 +1,214 @@
1
+ /**
2
+ * Grid state → server request, and the optimistic cache patch that goes with a
3
+ * cell write. Kept pure: the grid must never narrow a page it already has, and
4
+ * that is only provable if the request builder is testable on its own.
5
+ */
6
+
7
+ import type { CrmCellValue, CrmGridAttribute } from "./model";
8
+
9
+ export interface CrmGridSortEntry {
10
+ attributeId?: string;
11
+ field?: string;
12
+ direction: "asc" | "desc";
13
+ }
14
+
15
+ export interface CrmGridFilterLeaf {
16
+ attributeId?: string;
17
+ field?: string;
18
+ condition: string;
19
+ value?: string | number | boolean | null | Array<string | number | boolean>;
20
+ }
21
+
22
+ export interface CrmGridFilter {
23
+ op: "and" | "or";
24
+ conditions: CrmGridFilterLeaf[];
25
+ }
26
+
27
+ export interface CrmGridQueryState {
28
+ kind?: "account" | "person" | "opportunity";
29
+ connectionId?: string;
30
+ viewId?: string;
31
+ /** Display-name search. Sent to the server; never applied to a loaded page. */
32
+ search?: string;
33
+ filter?: CrmGridFilter;
34
+ sort?: CrmGridSortEntry[];
35
+ pageSize?: number;
36
+ }
37
+
38
+ export const CRM_GRID_PAGE_SIZE = 50;
39
+
40
+ /**
41
+ * Parameters for `list-crm-records`. Every narrowing input is on the wire —
42
+ * the grid has no local filter path to fall back to, by construction.
43
+ */
44
+ export function listRecordsParams(
45
+ state: CrmGridQueryState,
46
+ cursor?: string,
47
+ ): Record<string, unknown> {
48
+ const search = state.search?.trim();
49
+ return {
50
+ ...(state.kind ? { kind: state.kind } : {}),
51
+ ...(state.connectionId ? { connectionId: state.connectionId } : {}),
52
+ ...(state.viewId ? { viewId: state.viewId } : {}),
53
+ ...(search ? { query: search } : {}),
54
+ // A saved view owns its filter; sending both is a 422 at the action.
55
+ ...(state.filter && state.filter.conditions.length && !state.viewId
56
+ ? { filter: state.filter }
57
+ : {}),
58
+ ...(state.sort && state.sort.length ? { sort: state.sort } : {}),
59
+ limit: state.pageSize ?? CRM_GRID_PAGE_SIZE,
60
+ ...(cursor ? { cursor } : {}),
61
+ };
62
+ }
63
+
64
+ // ---------------------------------------------------------------------------
65
+ // Column configuration — the saved view's `columns_json`
66
+ // ---------------------------------------------------------------------------
67
+
68
+ export interface CrmGridColumn {
69
+ attributeId: string;
70
+ width?: number;
71
+ hidden?: boolean;
72
+ }
73
+
74
+ const MIN_COLUMN_WIDTH = 80;
75
+ const MAX_COLUMN_WIDTH = 720;
76
+
77
+ /** Accepts both the legacy `string[]` and the typed `columns_json` shape. */
78
+ export function normalizeGridColumns(raw: unknown): CrmGridColumn[] {
79
+ if (!Array.isArray(raw)) return [];
80
+ const columns: CrmGridColumn[] = [];
81
+ for (const entry of raw) {
82
+ if (typeof entry === "string" && entry) {
83
+ columns.push({ attributeId: entry });
84
+ continue;
85
+ }
86
+ if (!entry || typeof entry !== "object") continue;
87
+ const record = entry as Record<string, unknown>;
88
+ const attributeId =
89
+ typeof record.attributeId === "string" ? record.attributeId : null;
90
+ if (!attributeId) continue;
91
+ const width =
92
+ typeof record.width === "number" && Number.isFinite(record.width)
93
+ ? Math.min(Math.max(record.width, MIN_COLUMN_WIDTH), MAX_COLUMN_WIDTH)
94
+ : undefined;
95
+ columns.push({
96
+ attributeId,
97
+ ...(width === undefined ? {} : { width }),
98
+ ...(record.hidden === true ? { hidden: true } : {}),
99
+ });
100
+ }
101
+ return columns;
102
+ }
103
+
104
+ /**
105
+ * The grid's column order: saved columns first, in their saved order, then any
106
+ * attribute the saved view has never seen. A saved column whose attribute no
107
+ * longer exists is dropped rather than rendered as a blank column.
108
+ */
109
+ export function resolveGridColumns(
110
+ saved: CrmGridColumn[],
111
+ attributes: CrmGridAttribute[],
112
+ ): CrmGridColumn[] {
113
+ const known = new Map(
114
+ attributes.map((attribute) => [attribute.apiSlug, attribute]),
115
+ );
116
+ const seen = new Set<string>();
117
+ const ordered: CrmGridColumn[] = [];
118
+ for (const column of saved) {
119
+ if (!known.has(column.attributeId) || seen.has(column.attributeId))
120
+ continue;
121
+ seen.add(column.attributeId);
122
+ ordered.push(column);
123
+ }
124
+ for (const attribute of attributes) {
125
+ if (seen.has(attribute.apiSlug)) continue;
126
+ ordered.push({ attributeId: attribute.apiSlug });
127
+ }
128
+ return ordered;
129
+ }
130
+
131
+ export function setGridColumnWidth(
132
+ columns: CrmGridColumn[],
133
+ attributeId: string,
134
+ width: number,
135
+ ): CrmGridColumn[] {
136
+ const clamped = Math.min(
137
+ Math.max(Math.round(width), MIN_COLUMN_WIDTH),
138
+ MAX_COLUMN_WIDTH,
139
+ );
140
+ return columns.map((column) =>
141
+ column.attributeId === attributeId ? { ...column, width: clamped } : column,
142
+ );
143
+ }
144
+
145
+ export function setGridColumnHidden(
146
+ columns: CrmGridColumn[],
147
+ attributeId: string,
148
+ hidden: boolean,
149
+ ): CrmGridColumn[] {
150
+ return columns.map((column) =>
151
+ column.attributeId === attributeId
152
+ ? hidden
153
+ ? { ...column, hidden: true }
154
+ : {
155
+ attributeId: column.attributeId,
156
+ ...(column.width ? { width: column.width } : {}),
157
+ }
158
+ : column,
159
+ );
160
+ }
161
+
162
+ export function moveGridColumn(
163
+ columns: CrmGridColumn[],
164
+ attributeId: string,
165
+ toIndex: number,
166
+ ): CrmGridColumn[] {
167
+ const from = columns.findIndex(
168
+ (column) => column.attributeId === attributeId,
169
+ );
170
+ if (from < 0) return columns;
171
+ const next = [...columns];
172
+ const [moved] = next.splice(from, 1);
173
+ if (!moved) return columns;
174
+ next.splice(Math.max(0, Math.min(toIndex, next.length)), 0, moved);
175
+ return next;
176
+ }
177
+
178
+ // ---------------------------------------------------------------------------
179
+ // Optimistic cell writes
180
+ // ---------------------------------------------------------------------------
181
+
182
+ export interface CrmRecordValuesEntry {
183
+ recordId: string;
184
+ remoteRevision?: string | null;
185
+ values: Record<string, CrmCellValue>;
186
+ valuesSince?: Record<string, string>;
187
+ }
188
+
189
+ export interface CrmRecordValuesPayload {
190
+ records: CrmRecordValuesEntry[];
191
+ }
192
+
193
+ /**
194
+ * Immutably set one cell in a `list-crm-record-values` payload.
195
+ *
196
+ * Returns the SAME object when no record matched, so a caller can tell "wrote
197
+ * optimistically" from "the row is not in this cache" instead of assuming the
198
+ * patch landed. Rollback is restoring the previous payload.
199
+ */
200
+ export function patchRecordValues(
201
+ payload: CrmRecordValuesPayload | undefined,
202
+ recordId: string,
203
+ attributeSlug: string,
204
+ value: CrmCellValue,
205
+ ): CrmRecordValuesPayload | undefined {
206
+ if (!payload) return payload;
207
+ let matched = false;
208
+ const records = payload.records.map((entry) => {
209
+ if (entry.recordId !== recordId) return entry;
210
+ matched = true;
211
+ return { ...entry, values: { ...entry.values, [attributeSlug]: value } };
212
+ });
213
+ return matched ? { ...payload, records } : payload;
214
+ }
@@ -0,0 +1,192 @@
1
+ /**
2
+ * The left pane: highlights, then the rest of the object's typed attributes,
3
+ * each inline-editable when `update-crm-record` would actually accept it.
4
+ */
5
+
6
+ import { ExtensionSlot } from "@agent-native/core/client/extensions";
7
+ import { useT } from "@agent-native/core/client/i18n";
8
+ import { IconChevronRight } from "@tabler/icons-react";
9
+ import { useState } from "react";
10
+
11
+ import type {
12
+ CrmAttributeDefinition,
13
+ CrmValue,
14
+ } from "../../../../shared/crm-contract";
15
+ import { FieldEditor, FieldLockNote, FieldValueDisplay } from "./field-editors";
16
+ import { FieldHistoryButton } from "./FieldHistory";
17
+ import {
18
+ fieldEditability,
19
+ splitHighlights,
20
+ withoutSuppressedDuplicates,
21
+ type CrmRecordPage,
22
+ type CrmRecordPageValueMeta,
23
+ } from "./record-data";
24
+
25
+ export function AttributePanel({
26
+ page,
27
+ onCommit,
28
+ }: {
29
+ page: CrmRecordPage;
30
+ onCommit: (attribute: CrmAttributeDefinition, value: CrmValue) => void;
31
+ }) {
32
+ const t = useT();
33
+ const [editingSlug, setEditingSlug] = useState<string | null>(null);
34
+ const attributes = withoutSuppressedDuplicates(page.attributes, page.values);
35
+ const { highlights, rest } = splitHighlights(attributes, {
36
+ kind: page.record.kind,
37
+ values: page.values,
38
+ });
39
+
40
+ return (
41
+ <aside className="flex min-h-full w-full flex-col gap-6 border-border/70 p-5 lg:w-[22rem] lg:shrink-0 lg:border-r">
42
+ <section aria-labelledby="crm-record-highlights">
43
+ <h2
44
+ id="crm-record-highlights"
45
+ className="text-xs font-medium uppercase tracking-[0.12em] text-muted-foreground"
46
+ >
47
+ {t("record.highlights")}
48
+ </h2>
49
+ <div className="mt-3 grid gap-0.5">
50
+ {highlights.length ? (
51
+ highlights.map((attribute) => (
52
+ <AttributeRow
53
+ key={attribute.id}
54
+ recordId={page.record.id}
55
+ attribute={attribute}
56
+ value={page.values[attribute.apiSlug]}
57
+ meta={page.valueMeta[attribute.apiSlug]}
58
+ isEditing={editingSlug === attribute.apiSlug}
59
+ onStartEdit={() => setEditingSlug(attribute.apiSlug)}
60
+ onDone={() => setEditingSlug(null)}
61
+ onCommit={(value) => onCommit(attribute, value)}
62
+ />
63
+ ))
64
+ ) : (
65
+ <p className="py-3 text-sm text-muted-foreground">
66
+ {t("record.noAttributes")}
67
+ </p>
68
+ )}
69
+ </div>
70
+ </section>
71
+
72
+ {rest.length ? (
73
+ <details open className="group/details">
74
+ <summary className="flex cursor-pointer list-none items-center gap-1.5 text-xs font-medium uppercase tracking-[0.12em] text-muted-foreground">
75
+ <IconChevronRight className="size-3.5 transition-transform group-open/details:rotate-90" />
76
+ {t("record.allAttributes")}
77
+ </summary>
78
+ <div className="mt-3 grid gap-0.5">
79
+ {rest.map((attribute) => (
80
+ <AttributeRow
81
+ key={attribute.id}
82
+ recordId={page.record.id}
83
+ attribute={attribute}
84
+ value={page.values[attribute.apiSlug]}
85
+ meta={page.valueMeta[attribute.apiSlug]}
86
+ isEditing={editingSlug === attribute.apiSlug}
87
+ onStartEdit={() => setEditingSlug(attribute.apiSlug)}
88
+ onDone={() => setEditingSlug(null)}
89
+ onCommit={(value) => onCommit(attribute, value)}
90
+ />
91
+ ))}
92
+ </div>
93
+ </details>
94
+ ) : null}
95
+
96
+ {/* Stable extension contract: changing this slot id or context is a migration. */}
97
+ <ExtensionSlot
98
+ id="crm.record-sidebar.bottom"
99
+ context={{
100
+ recordId: page.record.id,
101
+ objectType: page.record.objectType,
102
+ kind: page.record.kind,
103
+ displayName: page.record.displayName,
104
+ provider: page.record.provider,
105
+ values: page.values,
106
+ }}
107
+ />
108
+ </aside>
109
+ );
110
+ }
111
+
112
+ function AttributeRow({
113
+ recordId,
114
+ attribute,
115
+ value,
116
+ meta,
117
+ isEditing,
118
+ onStartEdit,
119
+ onDone,
120
+ onCommit,
121
+ }: {
122
+ recordId: string;
123
+ attribute: CrmAttributeDefinition;
124
+ value: CrmValue | undefined;
125
+ meta: CrmRecordPageValueMeta | undefined;
126
+ isEditing: boolean;
127
+ onStartEdit: () => void;
128
+ onDone: () => void;
129
+ onCommit: (value: CrmValue) => void;
130
+ }) {
131
+ const t = useT();
132
+ const editability = fieldEditability(attribute);
133
+ // Options and checkboxes are always live controls; typing controls swap in
134
+ // only once the row is activated, so the panel stays readable at rest.
135
+ const alwaysLive =
136
+ editability.editable &&
137
+ (attribute.attributeType === "checkbox" ||
138
+ (!attribute.multi &&
139
+ (attribute.attributeType === "status" ||
140
+ attribute.attributeType === "select")));
141
+
142
+ return (
143
+ <div className="group grid grid-cols-[minmax(6.5rem,0.8fr)_minmax(0,1.2fr)] items-start gap-3 rounded-md px-2 py-1.5 hover:bg-muted/50">
144
+ <div className="flex min-w-0 items-start gap-1 pt-1">
145
+ {/* Wrapped, not truncated: a label column this narrow truncates long
146
+ attribute names (e.g. "Desired Cadence Days") past readability, and
147
+ a hover-only tooltip would hide the same text at rest. Wrapping
148
+ keeps the two-column grid and just grows the row. */}
149
+ <span
150
+ className="text-sm text-muted-foreground"
151
+ title={attribute.description ?? attribute.label}
152
+ >
153
+ {attribute.label}
154
+ </span>
155
+ {attribute.historyTracked ? (
156
+ <FieldHistoryButton recordId={recordId} attribute={attribute} />
157
+ ) : null}
158
+ </div>
159
+ <div className="min-w-0 text-sm">
160
+ {alwaysLive || isEditing ? (
161
+ <FieldEditor
162
+ attribute={attribute}
163
+ value={value}
164
+ autoFocus={isEditing}
165
+ onCommit={onCommit}
166
+ onDone={onDone}
167
+ />
168
+ ) : editability.editable ? (
169
+ <button
170
+ type="button"
171
+ className="w-full cursor-pointer rounded px-1 py-1 text-left hover:bg-background"
172
+ onClick={onStartEdit}
173
+ >
174
+ <FieldValueDisplay attribute={attribute} value={value} />
175
+ </button>
176
+ ) : (
177
+ <div className="grid gap-1 px-1 py-1">
178
+ <FieldValueDisplay attribute={attribute} value={value} />
179
+ <FieldLockNote reason={editability.reason} />
180
+ </div>
181
+ )}
182
+ {meta && !isEditing ? (
183
+ <p className="mt-0.5 px-1 text-[11px] text-muted-foreground opacity-0 transition-opacity group-hover:opacity-100">
184
+ {t("record.lastSetBy", {
185
+ actor: meta.actorId ?? meta.actorType,
186
+ })}
187
+ </p>
188
+ ) : null}
189
+ </div>
190
+ </div>
191
+ );
192
+ }
@@ -0,0 +1,179 @@
1
+ /**
2
+ * The "changed from X to Y" affordance next to a history-tracked attribute.
3
+ *
4
+ * A dialog rather than a hover card: the template ships no popover primitive,
5
+ * and the house rule forbids hand-rolling one. History is fetched only when the
6
+ * dialog opens — the panel would otherwise issue one query per attribute.
7
+ */
8
+
9
+ import { useActionQuery } from "@agent-native/core/client/hooks";
10
+ import { useT } from "@agent-native/core/client/i18n";
11
+ import { IconHistory } from "@tabler/icons-react";
12
+ import { useState } from "react";
13
+
14
+ import { Button } from "@/components/ui/button";
15
+ import {
16
+ Dialog,
17
+ DialogContent,
18
+ DialogDescription,
19
+ DialogHeader,
20
+ DialogTitle,
21
+ DialogTrigger,
22
+ } from "@/components/ui/dialog";
23
+ import { Skeleton } from "@/components/ui/skeleton";
24
+
25
+ import type { CrmAttributeDefinition } from "../../../../shared/crm-contract";
26
+ import { FieldValueDisplay } from "./field-editors";
27
+ import { historyTransitions, type FieldHistoryResponse } from "./record-data";
28
+
29
+ export function FieldHistoryButton({
30
+ recordId,
31
+ entryId,
32
+ attribute,
33
+ }: {
34
+ recordId: string;
35
+ entryId?: string;
36
+ attribute: Pick<
37
+ CrmAttributeDefinition,
38
+ | "apiSlug"
39
+ | "label"
40
+ | "attributeType"
41
+ | "multi"
42
+ | "options"
43
+ | "config"
44
+ | "archived"
45
+ | "storagePolicy"
46
+ | "updateable"
47
+ | "required"
48
+ >;
49
+ }) {
50
+ const t = useT();
51
+ const [open, setOpen] = useState(false);
52
+ const query = useActionQuery<FieldHistoryResponse>(
53
+ "list-crm-record-field-history" as never,
54
+ {
55
+ recordId,
56
+ apiSlug: attribute.apiSlug,
57
+ ...(entryId ? { entryId } : {}),
58
+ } as never,
59
+ { enabled: open },
60
+ );
61
+
62
+ return (
63
+ <Dialog open={open} onOpenChange={setOpen}>
64
+ <DialogTrigger asChild>
65
+ <Button
66
+ variant="ghost"
67
+ size="icon"
68
+ className="size-6 shrink-0 cursor-pointer text-muted-foreground opacity-0 group-hover:opacity-100 focus-visible:opacity-100"
69
+ aria-label={t("record.viewHistory", { field: attribute.label })}
70
+ >
71
+ <IconHistory className="size-3.5" />
72
+ </Button>
73
+ </DialogTrigger>
74
+ <DialogContent className="sm:max-w-lg">
75
+ <DialogHeader>
76
+ <DialogTitle>
77
+ {t("record.historyTitle", { field: attribute.label })}
78
+ </DialogTitle>
79
+ <DialogDescription>
80
+ {t("record.historyDescription")}
81
+ </DialogDescription>
82
+ </DialogHeader>
83
+ <FieldHistoryBody attribute={attribute} query={query} />
84
+ </DialogContent>
85
+ </Dialog>
86
+ );
87
+ }
88
+
89
+ function FieldHistoryBody({
90
+ attribute,
91
+ query,
92
+ }: {
93
+ attribute: React.ComponentProps<typeof FieldValueDisplay>["attribute"] & {
94
+ label: string;
95
+ };
96
+ query: {
97
+ data?: FieldHistoryResponse;
98
+ isLoading: boolean;
99
+ error: unknown;
100
+ };
101
+ }) {
102
+ const t = useT();
103
+ if (query.isLoading) return <Skeleton className="h-24 w-full" />;
104
+ if (query.error)
105
+ return (
106
+ <p className="text-sm text-destructive">
107
+ {query.error instanceof Error
108
+ ? query.error.message
109
+ : t("record.historyLoadFailed")}
110
+ </p>
111
+ );
112
+ const data = query.data;
113
+ if (!data) return null;
114
+ // Not tracked and no changes are different answers: one says history is off
115
+ // for this attribute, the other says it is on and empty.
116
+ if (!data.historyTracked)
117
+ return (
118
+ <p className="text-sm text-muted-foreground">
119
+ {t("record.historyNotTracked")}
120
+ </p>
121
+ );
122
+ const transitions = historyTransitions(data.changes);
123
+ if (!transitions.length)
124
+ return (
125
+ <p className="text-sm text-muted-foreground">
126
+ {t("record.historyEmpty")}
127
+ </p>
128
+ );
129
+
130
+ return (
131
+ <ol className="divide-y divide-border/70 overflow-y-auto rounded-md border border-border/70">
132
+ {transitions.map((transition) => (
133
+ <li key={transition.id} className="grid gap-1 px-3 py-2.5 text-sm">
134
+ <div className="flex flex-wrap items-center gap-2">
135
+ {transition.from === undefined ? (
136
+ <span className="text-muted-foreground">
137
+ {t("record.historySet")}
138
+ </span>
139
+ ) : (
140
+ <>
141
+ <span className="text-muted-foreground">
142
+ <FieldValueDisplay
143
+ attribute={attribute}
144
+ value={transition.from}
145
+ />
146
+ </span>
147
+ <span aria-hidden className="text-muted-foreground">
148
+
149
+ </span>
150
+ </>
151
+ )}
152
+ <span className="font-medium">
153
+ <FieldValueDisplay attribute={attribute} value={transition.to} />
154
+ </span>
155
+ </div>
156
+ <p className="text-xs text-muted-foreground">
157
+ {t("record.historyBy", {
158
+ actor: transition.actorId ?? transition.actorType,
159
+ when: formatWhen(transition.at),
160
+ })}
161
+ </p>
162
+ </li>
163
+ ))}
164
+ </ol>
165
+ );
166
+ }
167
+
168
+ function formatWhen(value: string): string {
169
+ const date = new Date(value);
170
+ return Number.isNaN(date.valueOf())
171
+ ? value
172
+ : date.toLocaleString(undefined, {
173
+ month: "short",
174
+ day: "numeric",
175
+ year: "numeric",
176
+ hour: "numeric",
177
+ minute: "2-digit",
178
+ });
179
+ }
@@ -0,0 +1,103 @@
1
+ /**
2
+ * Record header: identity, provenance, the upstream deep link, quick actions.
3
+ */
4
+
5
+ import { useT } from "@agent-native/core/client/i18n";
6
+ import { IconExternalLink } from "@tabler/icons-react";
7
+
8
+ import { Badge } from "@/components/ui/badge";
9
+ import { Button } from "@/components/ui/button";
10
+ import type { CrmRecordDetail } from "@/lib/types";
11
+
12
+ const PROVIDER_LABELS: Record<string, string> = {
13
+ hubspot: "HubSpot",
14
+ salesforce: "Salesforce",
15
+ };
16
+
17
+ export function RecordHeader({
18
+ record,
19
+ recordUrl,
20
+ recordUrlUnavailableReason,
21
+ actions,
22
+ }: {
23
+ record: CrmRecordDetail;
24
+ recordUrl: string | null;
25
+ recordUrlUnavailableReason: string | null;
26
+ actions?: React.ReactNode;
27
+ }) {
28
+ const t = useT();
29
+ const providerLabel = record.provider
30
+ ? PROVIDER_LABELS[record.provider]
31
+ : undefined;
32
+
33
+ return (
34
+ <header className="border-b border-border/70 px-5 py-4 sm:px-6">
35
+ <div className="flex flex-wrap items-start justify-between gap-4">
36
+ <div className="flex min-w-0 items-center gap-3">
37
+ <RecordAvatar name={record.displayName} kind={record.kind} />
38
+ <div className="min-w-0">
39
+ <h1 className="truncate text-xl font-semibold tracking-tight">
40
+ {record.displayName}
41
+ </h1>
42
+ <div className="mt-1 flex flex-wrap items-center gap-1.5 text-xs text-muted-foreground">
43
+ <span className="capitalize">{record.kind}</span>
44
+ {record.stage ? (
45
+ <Badge variant="secondary" className="font-normal">
46
+ {record.stage}
47
+ </Badge>
48
+ ) : null}
49
+ {providerLabel ? (
50
+ <Badge variant="outline" className="font-normal">
51
+ {t("record.mirroredFrom", { provider: providerLabel })}
52
+ </Badge>
53
+ ) : null}
54
+ {record.owner ? (
55
+ <span>{t("record.ownedBy", { owner: record.owner })}</span>
56
+ ) : null}
57
+ </div>
58
+ </div>
59
+ </div>
60
+ <div className="flex flex-wrap items-center gap-2">
61
+ {/* A native record has no upstream record at all, so it shows no link
62
+ and no "unavailable" note — absent and unavailable are different. */}
63
+ {providerLabel && recordUrl ? (
64
+ <Button asChild variant="outline" size="sm" className="gap-1.5">
65
+ <a href={recordUrl} target="_blank" rel="noopener noreferrer">
66
+ {t("record.openUpstream", { provider: providerLabel })}
67
+ <IconExternalLink className="size-4" />
68
+ </a>
69
+ </Button>
70
+ ) : null}
71
+ {providerLabel && !recordUrl && recordUrlUnavailableReason ? (
72
+ <span className="text-xs text-muted-foreground">
73
+ {t("record.upstreamLinkUnavailable", {
74
+ provider: providerLabel,
75
+ reason: recordUrlUnavailableReason,
76
+ })}
77
+ </span>
78
+ ) : null}
79
+ {actions}
80
+ </div>
81
+ </div>
82
+ </header>
83
+ );
84
+ }
85
+
86
+ function RecordAvatar({ name, kind }: { name: string; kind: string }) {
87
+ const initials = name
88
+ .split(/\s+/)
89
+ .filter(Boolean)
90
+ .slice(0, 2)
91
+ .map((part) => part[0]?.toUpperCase() ?? "")
92
+ .join("");
93
+ return (
94
+ <div
95
+ aria-hidden
96
+ className={`grid size-10 shrink-0 place-items-center text-sm font-semibold text-muted-foreground ${
97
+ kind === "person" ? "rounded-full" : "rounded-lg"
98
+ } bg-muted`}
99
+ >
100
+ {initials || "?"}
101
+ </div>
102
+ );
103
+ }