@anchrd/intel 0.59.0

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 (364) hide show
  1. package/README.md +496 -0
  2. package/bin/intel.mjs +134 -0
  3. package/dist/adapters/cloudflare/cloudflare-flow-workflow.d.ts +42 -0
  4. package/dist/adapters/cloudflare/cloudflare-flow-workflow.js +89 -0
  5. package/dist/adapters/cloudflare/cloudflare.d.ts +29 -0
  6. package/dist/adapters/cloudflare/cloudflare.js +416 -0
  7. package/dist/adapters/cloudflare/cloudflare.types.d.ts +31 -0
  8. package/dist/adapters/cloudflare/cloudflare.types.js +1 -0
  9. package/dist/adapters/content/content.d.ts +3 -0
  10. package/dist/adapters/content/content.js +128 -0
  11. package/dist/adapters/content/content.types.d.ts +38 -0
  12. package/dist/adapters/content/content.types.js +1 -0
  13. package/dist/adapters/db/db-audit.d.ts +62 -0
  14. package/dist/adapters/db/db-audit.js +150 -0
  15. package/dist/adapters/db/db-boards.d.ts +6 -0
  16. package/dist/adapters/db/db-boards.js +324 -0
  17. package/dist/adapters/db/db-feed.d.ts +30 -0
  18. package/dist/adapters/db/db-feed.js +276 -0
  19. package/dist/adapters/db/db-flows.d.ts +42 -0
  20. package/dist/adapters/db/db-flows.js +1311 -0
  21. package/dist/adapters/db/db-grants.d.ts +133 -0
  22. package/dist/adapters/db/db-grants.js +269 -0
  23. package/dist/adapters/db/db-indexing.d.ts +3 -0
  24. package/dist/adapters/db/db-indexing.js +200 -0
  25. package/dist/adapters/db/db-oauth.d.ts +3 -0
  26. package/dist/adapters/db/db-oauth.js +26 -0
  27. package/dist/adapters/db/db-prompts.d.ts +46 -0
  28. package/dist/adapters/db/db-prompts.js +144 -0
  29. package/dist/adapters/db/db.d.ts +6 -0
  30. package/dist/adapters/db/db.js +1687 -0
  31. package/dist/adapters/db/db.types.d.ts +16 -0
  32. package/dist/adapters/db/db.types.js +1 -0
  33. package/dist/adapters/document-converter/document-converter.d.ts +2 -0
  34. package/dist/adapters/document-converter/document-converter.js +26 -0
  35. package/dist/adapters/document-converter/document-converter.types.d.ts +17 -0
  36. package/dist/adapters/document-converter/document-converter.types.js +1 -0
  37. package/dist/adapters/flow-runtime/flow-runtime.d.ts +3 -0
  38. package/dist/adapters/flow-runtime/flow-runtime.js +22 -0
  39. package/dist/adapters/flow-runtime/flow-runtime.types.d.ts +19 -0
  40. package/dist/adapters/flow-runtime/flow-runtime.types.js +1 -0
  41. package/dist/adapters/index-queue/index-queue.d.ts +10 -0
  42. package/dist/adapters/index-queue/index-queue.js +12 -0
  43. package/dist/adapters/index-queue/index-queue.types.d.ts +11 -0
  44. package/dist/adapters/index-queue/index-queue.types.js +1 -0
  45. package/dist/adapters/json-schema/json-schema.d.ts +2 -0
  46. package/dist/adapters/json-schema/json-schema.js +77 -0
  47. package/dist/adapters/openid/openid.d.ts +5 -0
  48. package/dist/adapters/openid/openid.js +301 -0
  49. package/dist/adapters/portal-tokens/portal-tokens.d.ts +8 -0
  50. package/dist/adapters/portal-tokens/portal-tokens.js +79 -0
  51. package/dist/adapters/remote-tools/remote-tools.d.ts +5 -0
  52. package/dist/adapters/remote-tools/remote-tools.js +129 -0
  53. package/dist/adapters/semantic-index/semantic-index.d.ts +2 -0
  54. package/dist/adapters/semantic-index/semantic-index.js +139 -0
  55. package/dist/adapters/semantic-index/semantic-index.types.d.ts +56 -0
  56. package/dist/adapters/semantic-index/semantic-index.types.js +1 -0
  57. package/dist/adapters/session-cookie/session-cookie.d.ts +5 -0
  58. package/dist/adapters/session-cookie/session-cookie.js +87 -0
  59. package/dist/adapters/tool-source-policy/tool-source-policy.d.ts +1 -0
  60. package/dist/adapters/tool-source-policy/tool-source-policy.js +22 -0
  61. package/dist/audit/audit.d.ts +2 -0
  62. package/dist/audit/audit.js +87 -0
  63. package/dist/audit/audit.types.d.ts +23 -0
  64. package/dist/audit/audit.types.js +1 -0
  65. package/dist/auth/auth.d.ts +2 -0
  66. package/dist/auth/auth.js +284 -0
  67. package/dist/auth/auth.types.d.ts +108 -0
  68. package/dist/auth/auth.types.js +1 -0
  69. package/dist/boards/boards.d.ts +44 -0
  70. package/dist/boards/boards.js +447 -0
  71. package/dist/boards/boards.types.d.ts +117 -0
  72. package/dist/boards/boards.types.js +1 -0
  73. package/dist/build/build.d.ts +2 -0
  74. package/dist/build/build.js +192 -0
  75. package/dist/build/build.types.d.ts +17 -0
  76. package/dist/build/build.types.js +1 -0
  77. package/dist/bundle/bundle.d.ts +4 -0
  78. package/dist/bundle/bundle.js +1194 -0
  79. package/dist/bundle/bundle.types.d.ts +33 -0
  80. package/dist/bundle/bundle.types.js +1 -0
  81. package/dist/cli/cli.d.ts +4 -0
  82. package/dist/cli/cli.js +262 -0
  83. package/dist/cli/cli.types.d.ts +14 -0
  84. package/dist/cli/cli.types.js +1 -0
  85. package/dist/contract/audit.d.ts +50 -0
  86. package/dist/contract/audit.js +72 -0
  87. package/dist/contract/board.d.ts +150 -0
  88. package/dist/contract/board.js +262 -0
  89. package/dist/contract/bundle.d.ts +147 -0
  90. package/dist/contract/bundle.js +139 -0
  91. package/dist/contract/contract.d.ts +112 -0
  92. package/dist/contract/contract.js +143 -0
  93. package/dist/contract/feed.d.ts +126 -0
  94. package/dist/contract/feed.js +149 -0
  95. package/dist/contract/flow-run.d.ts +346 -0
  96. package/dist/contract/flow-run.js +194 -0
  97. package/dist/contract/flow.d.ts +1057 -0
  98. package/dist/contract/flow.js +552 -0
  99. package/dist/contract/node.d.ts +551 -0
  100. package/dist/contract/node.js +456 -0
  101. package/dist/contract/share.d.ts +216 -0
  102. package/dist/contract/share.js +134 -0
  103. package/dist/contract/table.d.ts +171 -0
  104. package/dist/contract/table.js +117 -0
  105. package/dist/contract/tool.d.ts +122 -0
  106. package/dist/contract/tool.js +172 -0
  107. package/dist/feed/feed.d.ts +2 -0
  108. package/dist/feed/feed.js +77 -0
  109. package/dist/feed/feed.types.d.ts +23 -0
  110. package/dist/feed/feed.types.js +1 -0
  111. package/dist/flows/flows.d.ts +41 -0
  112. package/dist/flows/flows.js +2216 -0
  113. package/dist/flows/flows.types.d.ts +339 -0
  114. package/dist/flows/flows.types.js +1 -0
  115. package/dist/http/http.d.ts +9 -0
  116. package/dist/http/http.js +874 -0
  117. package/dist/http/http.types.d.ts +22 -0
  118. package/dist/http/http.types.js +1 -0
  119. package/dist/indexing/indexing.d.ts +6 -0
  120. package/dist/indexing/indexing.js +239 -0
  121. package/dist/indexing/indexing.types.d.ts +11 -0
  122. package/dist/indexing/indexing.types.js +1 -0
  123. package/dist/intel/intel.d.ts +3 -0
  124. package/dist/intel/intel.js +194 -0
  125. package/dist/intel/intel.types.d.ts +24 -0
  126. package/dist/intel/intel.types.js +1 -0
  127. package/dist/mcp/mcp.d.ts +2 -0
  128. package/dist/mcp/mcp.js +1292 -0
  129. package/dist/mcp/mcp.types.d.ts +25 -0
  130. package/dist/mcp/mcp.types.js +1 -0
  131. package/dist/nodes/document-links/document-links.d.ts +20 -0
  132. package/dist/nodes/document-links/document-links.js +57 -0
  133. package/dist/nodes/nodes.d.ts +2 -0
  134. package/dist/nodes/nodes.js +1667 -0
  135. package/dist/nodes/nodes.types.d.ts +511 -0
  136. package/dist/nodes/nodes.types.js +1 -0
  137. package/dist/prepare/prepare.d.ts +4 -0
  138. package/dist/prepare/prepare.js +16 -0
  139. package/dist/prepare/prepare.types.d.ts +9 -0
  140. package/dist/prepare/prepare.types.js +1 -0
  141. package/dist/prompts/prompts.d.ts +2 -0
  142. package/dist/prompts/prompts.js +65 -0
  143. package/dist/prompts/prompts.types.d.ts +71 -0
  144. package/dist/prompts/prompts.types.js +1 -0
  145. package/dist/shared/callers/callers.d.ts +28 -0
  146. package/dist/shared/callers/callers.js +30 -0
  147. package/dist/shared/csv/csv.d.ts +13 -0
  148. package/dist/shared/csv/csv.js +85 -0
  149. package/dist/shared/document-text/document-text.d.ts +21 -0
  150. package/dist/shared/document-text/document-text.js +31 -0
  151. package/dist/shared/gate-authorization/gate-authorization.d.ts +5 -0
  152. package/dist/shared/gate-authorization/gate-authorization.js +26 -0
  153. package/dist/shared/grant-expiry/grant-expiry.d.ts +19 -0
  154. package/dist/shared/grant-expiry/grant-expiry.js +26 -0
  155. package/dist/shared/intel-error/intel-error.d.ts +22 -0
  156. package/dist/shared/intel-error/intel-error.js +27 -0
  157. package/dist/shared/plain-title/plain-title.d.ts +16 -0
  158. package/dist/shared/plain-title/plain-title.js +45 -0
  159. package/dist/shared/problem-details/problem-details.d.ts +30 -0
  160. package/dist/shared/problem-details/problem-details.js +18 -0
  161. package/dist/shared/report-unexpected-error/report-unexpected-error.d.ts +1 -0
  162. package/dist/shared/report-unexpected-error/report-unexpected-error.js +37 -0
  163. package/dist/shared/safe-return-path/safe-return-path.d.ts +2 -0
  164. package/dist/shared/safe-return-path/safe-return-path.js +11 -0
  165. package/dist/shared/sha256/sha256.d.ts +2 -0
  166. package/dist/shared/sha256/sha256.js +14 -0
  167. package/dist/tools/tool-servers/tool-servers.d.ts +46 -0
  168. package/dist/tools/tool-servers/tool-servers.js +177 -0
  169. package/dist/tools/tools.d.ts +2 -0
  170. package/dist/tools/tools.js +207 -0
  171. package/dist/tools/tools.types.d.ts +56 -0
  172. package/dist/tools/tools.types.js +1 -0
  173. package/examples/branding/favicon.svg +5 -0
  174. package/examples/branding/logo.svg +5 -0
  175. package/examples/branding/theme.css +16 -0
  176. package/examples/dev.vars.example +7 -0
  177. package/examples/intel.json +9 -0
  178. package/migrations/0000_intel_foundation.sql +203 -0
  179. package/migrations/0001_portal_native_tools.sql +21 -0
  180. package/migrations/0002_flows_in_the_knowledge_tree.sql +34 -0
  181. package/migrations/0003_folder_permissions.sql +211 -0
  182. package/migrations/0004_subflow_runs.sql +14 -0
  183. package/migrations/0005_flow_node_cleanup.sql +28 -0
  184. package/migrations/0005_tables_in_the_knowledge_tree.sql +68 -0
  185. package/migrations/0006_links_are_written_in_the_text.sql +20 -0
  186. package/migrations/0007_no_node_waits.sql +69 -0
  187. package/migrations/0008_three_layers.sql +130 -0
  188. package/migrations/0009_no_context_policy.sql +48 -0
  189. package/migrations/0010_no_run_waits_a_year.sql +27 -0
  190. package/migrations/0011_one_name_for_the_tree.sql +53 -0
  191. package/migrations/0012_table_snapshots.sql +29 -0
  192. package/migrations/0017_a_vector_per_card.sql +38 -0
  193. package/migrations/0018_no_context_policy_at_last.sql +97 -0
  194. package/migrations/0019_one_name_for_the_grants.sql +52 -0
  195. package/migrations/0020_cascade_purge_replay.sql +11 -0
  196. package/migrations/0021_a_flow_carries_its_own_grant.sql +44 -0
  197. package/migrations/0022_a_cursor_over_the_journal.sql +14 -0
  198. package/migrations/0023_a_board_and_its_tasks.sql +116 -0
  199. package/migrations/0024_the_archive_is_a_column.sql +33 -0
  200. package/migrations/0025_archiving_takes_the_subtree.sql +29 -0
  201. package/migrations/0026_one_feed_over_two_kinds.sql +23 -0
  202. package/migrations/0027_the_runs_of_every_flow.sql +27 -0
  203. package/migrations/0028_a_prompt_name_over_two_kinds.sql +70 -0
  204. package/package.json +118 -0
  205. package/src/contract/CLAUDE.md +152 -0
  206. package/src/contract/README.md +102 -0
  207. package/src/contract/audit.ts +85 -0
  208. package/src/contract/board.ts +306 -0
  209. package/src/contract/bundle.ts +157 -0
  210. package/src/contract/contract.ts +161 -0
  211. package/src/contract/feed.ts +165 -0
  212. package/src/contract/flow-run.ts +241 -0
  213. package/src/contract/flow.ts +675 -0
  214. package/src/contract/node.ts +583 -0
  215. package/src/contract/share.ts +196 -0
  216. package/src/contract/table.ts +178 -0
  217. package/src/contract/tool.ts +202 -0
  218. package/ui/README.md +110 -0
  219. package/ui/components.json +21 -0
  220. package/ui/index.html +13 -0
  221. package/ui/package.json +11 -0
  222. package/ui/scripts/lint-tokens.mjs +187 -0
  223. package/ui/src/access-summary/access-summary.tsx +385 -0
  224. package/ui/src/app/action-slot/action-slot.tsx +27 -0
  225. package/ui/src/app/app-sidebar/app-sidebar.tsx +71 -0
  226. package/ui/src/app/app-tree/app-tree.tsx +1175 -0
  227. package/ui/src/app/app.tsx +87 -0
  228. package/ui/src/app/header-search/header-search.tsx +294 -0
  229. package/ui/src/app/reindex-dialog/reindex-dialog.tsx +79 -0
  230. package/ui/src/app/settings-dialog/settings-dialog.tsx +135 -0
  231. package/ui/src/app/sidebar-preferences/sidebar-preferences.ts +68 -0
  232. package/ui/src/app/sidebar-preferences/sidebar-preferences.types.ts +15 -0
  233. package/ui/src/app/sidebar-resize-handle/sidebar-resize-handle.tsx +86 -0
  234. package/ui/src/app/tree-expansion/tree-expansion.ts +107 -0
  235. package/ui/src/app/tree-expansion/tree-expansion.types.ts +31 -0
  236. package/ui/src/app/tree-move/tree-move.tsx +427 -0
  237. package/ui/src/app/tree-reveal/tree-reveal.ts +64 -0
  238. package/ui/src/app/tree-reveal/tree-reveal.types.ts +16 -0
  239. package/ui/src/app/user-footer/user-footer.tsx +188 -0
  240. package/ui/src/app/view-menu/view-menu.tsx +115 -0
  241. package/ui/src/app-root/app-root.tsx +109 -0
  242. package/ui/src/app-root/app-root.types.ts +27 -0
  243. package/ui/src/archive/archive.tsx +417 -0
  244. package/ui/src/attachment-detail/attachment-detail.ts +52 -0
  245. package/ui/src/attachment-viewer/attachment-viewer.tsx +166 -0
  246. package/ui/src/blocknote-view/blocknote-view.tsx +30 -0
  247. package/ui/src/board/board-assignee/board-assignee-picker.tsx +132 -0
  248. package/ui/src/board/board-assignee/board-assignee.ts +48 -0
  249. package/ui/src/board/board-assignee/board-assignee.tsx +75 -0
  250. package/ui/src/board/board-chip/board-chip.tsx +46 -0
  251. package/ui/src/board/board-crumbs/board-crumbs.ts +56 -0
  252. package/ui/src/board/board-crumbs/board-crumbs.tsx +111 -0
  253. package/ui/src/board/board-data/board-data.ts +195 -0
  254. package/ui/src/board/board-data/board-data.types.ts +38 -0
  255. package/ui/src/board/board-dates/board-dates.ts +40 -0
  256. package/ui/src/board/board-dates/board-dates.tsx +105 -0
  257. package/ui/src/board/board-kanban/board-kanban.ts +162 -0
  258. package/ui/src/board/board-kanban/board-kanban.tsx +680 -0
  259. package/ui/src/board/board-open-task/board-open-task.ts +32 -0
  260. package/ui/src/board/board-panel/board-panel.tsx +259 -0
  261. package/ui/src/board/board-settings/board-settings.tsx +226 -0
  262. package/ui/src/board/board-status/board-status.tsx +60 -0
  263. package/ui/src/board/board-stripes/board-stripes.ts +128 -0
  264. package/ui/src/board/board-table/board-table.ts +53 -0
  265. package/ui/src/board/board-table/board-table.tsx +610 -0
  266. package/ui/src/board/board-task/board-task.ts +108 -0
  267. package/ui/src/board/board-task/board-task.tsx +335 -0
  268. package/ui/src/board/board-title-row/board-title-row.tsx +66 -0
  269. package/ui/src/branding/branding.tsx +17 -0
  270. package/ui/src/branding/custom-logo.ts +1 -0
  271. package/ui/src/branding/favicon.default.svg +5 -0
  272. package/ui/src/branding/favicon.svg +5 -0
  273. package/ui/src/components/ui/avatar.tsx +39 -0
  274. package/ui/src/components/ui/button.tsx +64 -0
  275. package/ui/src/components/ui/collapsible.tsx +20 -0
  276. package/ui/src/components/ui/command.tsx +160 -0
  277. package/ui/src/components/ui/dialog.tsx +143 -0
  278. package/ui/src/components/ui/dropdown-menu.tsx +187 -0
  279. package/ui/src/components/ui/input.tsx +21 -0
  280. package/ui/src/components/ui/popover.tsx +41 -0
  281. package/ui/src/components/ui/select.tsx +163 -0
  282. package/ui/src/components/ui/separator.tsx +26 -0
  283. package/ui/src/components/ui/sheet.tsx +136 -0
  284. package/ui/src/components/ui/sidebar.tsx +693 -0
  285. package/ui/src/components/ui/skeleton.tsx +13 -0
  286. package/ui/src/components/ui/switch.tsx +25 -0
  287. package/ui/src/components/ui/table.tsx +82 -0
  288. package/ui/src/components/ui/tabs.tsx +52 -0
  289. package/ui/src/components/ui/tooltip.tsx +51 -0
  290. package/ui/src/data/intel-data-provider/intel-data-provider.ts +685 -0
  291. package/ui/src/data/intel-data-provider/intel-data-provider.types.ts +266 -0
  292. package/ui/src/data/query-client.ts +18 -0
  293. package/ui/src/data/request-refusal/refusal-notice.tsx +36 -0
  294. package/ui/src/data/request-refusal/request-refusal.ts +51 -0
  295. package/ui/src/data/sign-in/sign-in.ts +97 -0
  296. package/ui/src/data/sign-in/sign-in.types.ts +15 -0
  297. package/ui/src/document-link/document-link.tsx +114 -0
  298. package/ui/src/document-markdown/document-markdown.ts +195 -0
  299. package/ui/src/editor-schema/editor-schema.ts +23 -0
  300. package/ui/src/entry-picker/entry-picker.tsx +238 -0
  301. package/ui/src/feed/feed.tsx +344 -0
  302. package/ui/src/file-preview/file-preview-view.tsx +496 -0
  303. package/ui/src/file-preview/file-preview.tsx +28 -0
  304. package/ui/src/file-preview/pdf-file-preview.tsx +5 -0
  305. package/ui/src/file-preview/presentation-file-preview.tsx +21 -0
  306. package/ui/src/file-preview/spreadsheet-file-preview.tsx +5 -0
  307. package/ui/src/file-preview/word-file-preview.tsx +5 -0
  308. package/ui/src/flow-runs/flow-runs.tsx +238 -0
  309. package/ui/src/flows/flows.tsx +1646 -0
  310. package/ui/src/flows/node-icon/node-icon.ts +34 -0
  311. package/ui/src/flows/node-palette/node-palette.tsx +214 -0
  312. package/ui/src/flows/node-palette/node-palette.types.ts +15 -0
  313. package/ui/src/folder-contents/folder-contents.tsx +321 -0
  314. package/ui/src/folder-index/folder-index.ts +55 -0
  315. package/ui/src/frontmatter/frontmatter.tsx +193 -0
  316. package/ui/src/graph-pane/graph-pane.tsx +53 -0
  317. package/ui/src/hooks/use-mobile.ts +19 -0
  318. package/ui/src/i18n/custom.json +4 -0
  319. package/ui/src/i18n/de.json +609 -0
  320. package/ui/src/i18n/en.json +609 -0
  321. package/ui/src/i18n/es.json +609 -0
  322. package/ui/src/i18n/i18n-context.tsx +65 -0
  323. package/ui/src/i18n/i18n-languages/i18n-languages.ts +83 -0
  324. package/ui/src/i18n/i18n.ts +32 -0
  325. package/ui/src/i18n/i18n.types.ts +49 -0
  326. package/ui/src/kind-icon.ts +97 -0
  327. package/ui/src/lib/utils.ts +6 -0
  328. package/ui/src/main.tsx +68 -0
  329. package/ui/src/modal/modal.tsx +53 -0
  330. package/ui/src/node-details/node-details.tsx +545 -0
  331. package/ui/src/node-editor/node-editor.tsx +304 -0
  332. package/ui/src/node-graph/graph-notice.tsx +21 -0
  333. package/ui/src/node-graph/node-graph.ts +214 -0
  334. package/ui/src/node-graph/node-graph.tsx +295 -0
  335. package/ui/src/node-import/node-import.tsx +418 -0
  336. package/ui/src/node-save-error.ts +46 -0
  337. package/ui/src/node-source/node-source.tsx +128 -0
  338. package/ui/src/node-table/node-table.tsx +107 -0
  339. package/ui/src/nodes/nodes.tsx +240 -0
  340. package/ui/src/rename-resource/rename-resource.ts +80 -0
  341. package/ui/src/resolved-names/resolved-names.tsx +67 -0
  342. package/ui/src/resource-error.ts +33 -0
  343. package/ui/src/resource-menu/resource-menu.tsx +1156 -0
  344. package/ui/src/router/router-context.ts +6 -0
  345. package/ui/src/router/router.tsx +67 -0
  346. package/ui/src/router/router.types.ts +8 -0
  347. package/ui/src/router/selection-search.ts +88 -0
  348. package/ui/src/save-button/save-button.tsx +94 -0
  349. package/ui/src/section-hint/section-hint.tsx +40 -0
  350. package/ui/src/sign-in-refused/sign-in-refused.tsx +35 -0
  351. package/ui/src/styles.css +189 -0
  352. package/ui/src/table-columns/table-columns.tsx +379 -0
  353. package/ui/src/theme/custom.css +1 -0
  354. package/ui/src/theme/theme-context.tsx +89 -0
  355. package/ui/src/theme/theme.ts +70 -0
  356. package/ui/src/time/relative-time.tsx +41 -0
  357. package/ui/src/time/time-context.tsx +78 -0
  358. package/ui/src/time/time.ts +165 -0
  359. package/ui/src/title-row/title-row.tsx +185 -0
  360. package/ui/src/tools/tools.tsx +546 -0
  361. package/ui/src/user-name/user-name.ts +61 -0
  362. package/ui/src/verbatim/verbatim.tsx +68 -0
  363. package/ui/tsconfig.json +17 -0
  364. package/ui/vite.config.ts +129 -0
@@ -0,0 +1,107 @@
1
+ import type { Node } from "@contract/node.ts";
2
+ import { useQuery } from "@tanstack/react-query";
3
+ import { ActionSlot } from "@/app/action-slot/action-slot.tsx";
4
+ import { useI18n } from "@/i18n/i18n-context.tsx";
5
+ import { useIntelRouterContext } from "@/router/router-context.ts";
6
+ import { TitleRowScrollArea } from "@/title-row/title-row.tsx";
7
+
8
+ /**
9
+ * A table as a grid (#40).
10
+ *
11
+ * Reading is the whole of the first cut: there is no cell editor, and rows arrive through
12
+ * `node_table_row_create` — the point of the kind is that a flow or an MCP client fills it
13
+ * without a person retyping anything. Editing in the grid is anchrd/intel#165.
14
+ *
15
+ * ⚠️ The rows are the server's answer, parsed by the server's one CSV reader. A second reader here
16
+ * would eventually disagree with it about a quoted comma, and the grid would then show something
17
+ * the export does not contain.
18
+ */
19
+ export function NodeTablePanel({ node }: { node: Node }) {
20
+ const { data } = useIntelRouterContext();
21
+ const i18n = useI18n();
22
+ const table = useQuery({
23
+ queryKey: ["node-table", node.id],
24
+ queryFn: () => data.getNodeTable(node.id),
25
+ });
26
+ return (
27
+ <div className="flex min-h-0 flex-1 flex-col">
28
+ {/* ⚠️ This used to be a second bar of its own, directly under the title line (#54): two
29
+ headers stacked, and the table starting a whole row lower for it. A table is one node kind
30
+ among four, not a screen with its own chrome — so the count goes beside the title as a
31
+ quiet word. It is rendered from here rather than from the screen because this is where the
32
+ query lives; the screen would otherwise have to load a table it does not show.
33
+
34
+ ⚠️ The CSV export used to stand here too, as a button WITH TEXT beside two icon buttons.
35
+ #454 moved it into the title line's three-dot menu, and #532 dropped it there: the bundle
36
+ export beside it already writes the table into the zip as `<title>.csv` (`bundle.ts`), so
37
+ the two entries handed out the same bytes. Taking a table home is `Export`, here and
38
+ nowhere else. */}
39
+ <ActionSlot name="title-meta">
40
+ {table.data
41
+ ? i18n.t("node.table.summary", {
42
+ rows: table.data.rows.length,
43
+ columns: table.data.columns.length,
44
+ })
45
+ : ""}
46
+ </ActionSlot>
47
+ {table.isPending ? (
48
+ <p className="p-6 text-sm text-muted-foreground">{i18n.t("common.loading")}</p>
49
+ ) : table.isError ? (
50
+ <div role="alert" className="space-y-3 p-6 text-sm">
51
+ <p className="text-destructive">{i18n.t("node.loadFailed")}</p>
52
+ <button
53
+ type="button"
54
+ onClick={() => void table.refetch()}
55
+ className="rounded-md border px-3 py-2 text-sm outline-none hover:bg-muted focus-visible:ring-2 focus-visible:ring-ring"
56
+ >
57
+ {i18n.t("common.retry")}
58
+ </button>
59
+ </div>
60
+ ) : table.data.columns.length === 0 ? (
61
+ <p className="grid flex-1 place-items-center p-8 text-center text-sm text-muted-foreground">
62
+ {i18n.t("node.table.undefined")}
63
+ </p>
64
+ ) : (
65
+ <TitleRowScrollArea className="min-h-0 flex-1 overflow-auto">
66
+ <table className="w-full border-collapse text-sm">
67
+ <caption className="sr-only">{node.title}</caption>
68
+ <thead className="sticky top-0 bg-card">
69
+ <tr>
70
+ {table.data.columns.map((column) => (
71
+ // Column names are distinct on every path that writes a table — `DefineTableInput`,
72
+ // `RedefineTableInput` and, since #322, the bundle import, which used to walk past
73
+ // both — so the name is the key and nothing has to fall back to a position.
74
+ <th
75
+ key={column}
76
+ scope="col"
77
+ className="border-b border-r px-3 py-2 text-left font-medium whitespace-nowrap"
78
+ >
79
+ {column}
80
+ </th>
81
+ ))}
82
+ </tr>
83
+ </thead>
84
+ <tbody>
85
+ {table.data.rows.map((row, rowIndex) => (
86
+ // ⚠️ Rows carry no identity of their own. A table is an append-only list of text
87
+ // and its position is the only thing that names a row — there is no id to key on,
88
+ // and inventing one would be inventing data.
89
+ // biome-ignore lint/suspicious/noArrayIndexKey: an appended row has no other key
90
+ <tr key={rowIndex} className="even:bg-muted/40">
91
+ {table.data.columns.map((column, cellIndex) => (
92
+ <td key={column} className="border-b border-r px-3 py-2 align-top">
93
+ {row[cellIndex] ?? ""}
94
+ </td>
95
+ ))}
96
+ </tr>
97
+ ))}
98
+ </tbody>
99
+ </table>
100
+ {table.data.rows.length === 0 ? (
101
+ <p className="p-6 text-sm text-muted-foreground">{i18n.t("node.table.empty")}</p>
102
+ ) : null}
103
+ </TitleRowScrollArea>
104
+ )}
105
+ </div>
106
+ );
107
+ }
@@ -0,0 +1,240 @@
1
+ import type { Node } from "@contract/node.ts";
2
+ import { useQuery, useQueryClient } from "@tanstack/react-query";
3
+ import { useNavigate, useRouterState } from "@tanstack/react-router";
4
+ import { lazy, Suspense } from "react";
5
+ import { treeLevelKey } from "@/app/tree-move/tree-move.tsx";
6
+ import { ViewMenu } from "@/app/view-menu/view-menu.tsx";
7
+ import { BoardPanel } from "@/board/board-panel/board-panel.tsx";
8
+ import { BoardTitleRow } from "@/board/board-title-row/board-title-row.tsx";
9
+ import { RefusalNotice } from "@/data/request-refusal/refusal-notice.tsx";
10
+ import { refusalOf } from "@/data/request-refusal/request-refusal.ts";
11
+ import { FolderContents } from "@/folder-contents/folder-contents.tsx";
12
+ import { GraphPane } from "@/graph-pane/graph-pane.tsx";
13
+ import { useI18n } from "@/i18n/i18n-context.tsx";
14
+ import { NodeTablePanel } from "@/node-table/node-table.tsx";
15
+ import { useIntelRouterContext } from "@/router/router-context.ts";
16
+ import { graphViewFrom, selectedFrom, viewOf } from "@/router/selection-search.ts";
17
+ import { TitleRow, TitleRowFrame } from "@/title-row/title-row.tsx";
18
+
19
+ const NodeEditor = lazy(async () => ({
20
+ default: (await import("@/node-editor/node-editor.tsx")).NodeEditor,
21
+ }));
22
+ const NodeSource = lazy(async () => ({
23
+ default: (await import("@/node-source/node-source.tsx")).NodeSource,
24
+ }));
25
+ const AttachmentViewer = lazy(async () => ({
26
+ default: (await import("@/attachment-viewer/attachment-viewer.tsx")).AttachmentViewer,
27
+ }));
28
+
29
+ /**
30
+ * A document is markdown, and since #820 it can be read and written as what it is. The editor
31
+ * stands first: it is the view a plain link means.
32
+ */
33
+ const DocumentViews = ["editor", "code"] as const;
34
+
35
+ /**
36
+ * Which kinds are a document, and therefore have a source at all.
37
+ *
38
+ * ⚠️ The same list the panel below falls through to: a folder draws its contents, an attachment its
39
+ * preview, a table and a board their own panel, and what is left is a text. A card is one too — it
40
+ * is not reached from this screen in the ordinary way, but a link straight to it lands here, and a
41
+ * switch that appeared for it without a view behind it would be worse than no switch.
42
+ */
43
+ function writesMarkdown(kind: Node["kind"]): boolean {
44
+ return kind === "document" || kind === "task";
45
+ }
46
+
47
+ export function Nodes() {
48
+ const { data } = useIntelRouterContext();
49
+ const i18n = useI18n();
50
+ const queryClient = useQueryClient();
51
+ const navigate = useNavigate();
52
+ // The tree in the sidebar and the header search both name what to open through `?select=`, so this
53
+ // screen has no selection of its own to keep in step with them.
54
+ const selection = useRouterState({
55
+ select: (state) => ({
56
+ id: selectedFrom(state.location.search),
57
+ graph: graphViewFrom(state.location.search),
58
+ // Which of a document's two views is showing (#820). The editor is the first of them and
59
+ // therefore the absence of the parameter, so every link written before it keeps its meaning.
60
+ view: viewOf(state.location.search, DocumentViews),
61
+ }),
62
+ });
63
+ const selectedId = selection.id;
64
+ const document = useQuery({
65
+ queryKey: ["node", selectedId],
66
+ queryFn: () => data.getNode(selectedId ?? ""),
67
+ enabled: Boolean(selectedId),
68
+ });
69
+ const selected = document.data?.node ?? null;
70
+ // ⚠️ Off the ERROR, not off the absence of data: a query that has not answered is not a query that
71
+ // answered "no" (`packages/intel/ui/CLAUDE.md`). While the request is in flight there is no refusal to
72
+ // report, and the loading line below stays.
73
+ const refusal = refusalOf(document.error);
74
+ /**
75
+ * The top level of the tree — the same query the sidebar already holds, by the same key and the
76
+ * same function, so this is a second reader of one cache entry rather than a second request.
77
+ *
78
+ * ⚠️ The screen needs a source of its own because the one above cannot answer here: the refusal
79
+ * branch hangs on `["node", selectedId]`, and without a selection that query is not enabled at
80
+ * all. The empty state was therefore reached BEFORE anything had been asked — and it invited the
81
+ * reader to select something, which is exactly what somebody without `intel.nodes.read` cannot
82
+ * do. The sidebar beside it has said so since #430; two surfaces, two answers (#445).
83
+ */
84
+ const rootLevel = useQuery({
85
+ queryKey: treeLevelKey(null),
86
+ queryFn: () => data.listTreeChildren(null),
87
+ enabled: selectedId === null,
88
+ });
89
+ // ⚠️ Off the ERROR, and here that is half the ticket rather than a detail. A sentence about a
90
+ // missing permission, shown while the answer is still travelling, claims a refusal nobody has
91
+ // spoken yet — the mistake from #350, and `packages/intel/ui/CLAUDE.md` has the rule: a missing answer
92
+ // is a reason to say less, never to refuse more. So the negative sentence hangs off the refusal;
93
+ // until one arrives, what stands is the invitation, which claims nothing about permission.
94
+ const rootRefusal = refusalOf(rootLevel.error);
95
+ // A folder shows its contents as a graph, and so does the root of the tree (#19). A document has
96
+ // nothing to draw — which is why the switch is not offered on one rather than offered and empty.
97
+ const graphable = selectedId === null || selected?.kind === "folder";
98
+ const relations = useQuery({
99
+ queryKey: ["relation-graph", "folder", selectedId],
100
+ queryFn: () => data.getRelationGraph({ of: "folder", folderId: selectedId }),
101
+ enabled: graphable && selection.graph,
102
+ });
103
+ return (
104
+ <TitleRowFrame className="flex h-full min-h-0 flex-col">
105
+ <section className="relative flex min-h-0 min-w-0 flex-1 flex-col bg-card">
106
+ {/* ⚠️ Only the root draws its graph on its own. A folder's graph hangs *under* the folder's
107
+ title line rather than in place of it (#58): the line is the one place its menu lives,
108
+ and a folder that could only be renamed while the graph happened to be switched off
109
+ would be a folder one cannot rename. */}
110
+ {/* ⚠️ #454 took the view switch out of the GLOBAL header — there it applied to the whole
111
+ application although it is a view onto the selected item. For an item it now stands in
112
+ that item's title line.
113
+
114
+ ⚠️ The root has no title line, and that is exactly why it gets a row of its own here.
115
+ Without it the graph of the WHOLE tree would have been unreachable after the move — the
116
+ switch was its only way in and out. Moving a control must not abolish a view; that would
117
+ be a decision the ticket did not contain. */}
118
+ {selectedId === null ? (
119
+ <div className="flex shrink-0 items-center justify-end gap-2 border-b px-6 py-2">
120
+ <ViewMenu />
121
+ </div>
122
+ ) : null}
123
+ {selectedId === null && selection.graph ? (
124
+ <GraphPane
125
+ query={relations}
126
+ select={(node) =>
127
+ void navigate({
128
+ to: node.kind === "flow" ? "/flows" : "/nodes",
129
+ search: { select: node.id },
130
+ })
131
+ }
132
+ />
133
+ ) : !selectedId ? (
134
+ rootRefusal ? (
135
+ <RefusalNotice refusal={rootRefusal} />
136
+ ) : (
137
+ <div className="grid flex-1 place-items-center p-8 text-center text-sm text-muted-foreground">
138
+ {i18n.t("node.select")}
139
+ </div>
140
+ )
141
+ ) : document.isPending ? (
142
+ <p className="p-6 text-sm text-muted-foreground">{i18n.t("common.loading")}</p>
143
+ ) : refusal ? (
144
+ <RefusalNotice refusal={refusal} />
145
+ ) : document.isError || !selected ? (
146
+ <div role="alert" className="grid flex-1 place-items-center p-8 text-center text-sm">
147
+ <div className="space-y-3">
148
+ <p className="text-destructive">{i18n.t("node.loadFailed")}</p>
149
+ <button
150
+ type="button"
151
+ onClick={() => void document.refetch()}
152
+ className="rounded-md border px-3 py-2 text-sm outline-none hover:bg-muted focus-visible:ring-2 focus-visible:ring-ring"
153
+ >
154
+ {i18n.t("common.retry")}
155
+ </button>
156
+ </div>
157
+ </div>
158
+ ) : (
159
+ <>
160
+ {/* The document's own line: its name, and beside it only what acts on this one
161
+ document. What the four loose icons used to do is now one menu (#24) — and since #58
162
+ this line is the only place that menu appears at all, for every kind including a
163
+ folder, whose whole screen is this line.
164
+ ⚠️ #454: the view switch now stands HERE. It used to stand in the global header,
165
+ beside the search — where what applies everywhere stands. But a view is a view onto
166
+ THIS item, and each switch is drawn only where its views exist: the graph where
167
+ there is a graph, the source where there is markdown. A kind with one view gets
168
+ none, or the menu would promise a choice it does not have (#818). */}
169
+ {/* ⚠️ A board brings its OWN title row, because an open card changes what the line
170
+ identifies: not the board any more, but where the card sits on it. It is the same
171
+ `TitleRow` underneath, only with a path in place of the name (#692). */}
172
+ {selected.kind === "board" ? (
173
+ <BoardTitleRow node={selected} />
174
+ ) : (
175
+ <TitleRow title={selected.title} target={{ type: "node", node: selected }}>
176
+ {graphable ? <ViewMenu /> : null}
177
+ {/* ⚠️ A document has no graph and a folder has no source, so the two switches are
178
+ never both drawn — which is also why neither has to know about the other. */}
179
+ {writesMarkdown(selected.kind) ? <ViewMenu views={DocumentViews} /> : null}
180
+ </TitleRow>
181
+ )}
182
+ {graphable && selection.graph ? (
183
+ <GraphPane
184
+ query={relations}
185
+ select={(node) =>
186
+ void navigate({
187
+ to: node.kind === "flow" ? "/flows" : "/nodes",
188
+ search: { select: node.id },
189
+ })
190
+ }
191
+ />
192
+ ) : selected.kind === "folder" ? (
193
+ <FolderContents folderId={selected.id} />
194
+ ) : selected.kind === "attachment" ? (
195
+ <Suspense
196
+ fallback={
197
+ <p className="p-6 text-sm text-muted-foreground">{i18n.t("common.loading")}</p>
198
+ }
199
+ >
200
+ <AttachmentViewer document={document.data} />
201
+ </Suspense>
202
+ ) : selected.kind === "table" ? (
203
+ <NodeTablePanel node={selected} />
204
+ ) : selected.kind === "board" ? (
205
+ <BoardPanel node={selected} />
206
+ ) : document.data ? (
207
+ <Suspense
208
+ fallback={
209
+ <p className="p-6 text-sm text-muted-foreground">{i18n.t("common.loading")}</p>
210
+ }
211
+ >
212
+ {selection.view === "code" ? (
213
+ <NodeSource
214
+ key={document.data.node.id}
215
+ data={data}
216
+ document={document.data}
217
+ i18n={i18n}
218
+ onSaved={(saved) => {
219
+ queryClient.setQueryData(["node", selected.id], saved);
220
+ }}
221
+ />
222
+ ) : (
223
+ <NodeEditor
224
+ key={document.data.node.id}
225
+ data={data}
226
+ document={document.data}
227
+ i18n={i18n}
228
+ onSaved={(saved) => {
229
+ queryClient.setQueryData(["node", selected.id], saved);
230
+ }}
231
+ />
232
+ )}
233
+ </Suspense>
234
+ ) : null}
235
+ </>
236
+ )}
237
+ </section>
238
+ </TitleRowFrame>
239
+ );
240
+ }
@@ -0,0 +1,80 @@
1
+ import type { Flow } from "@contract/flow.ts";
2
+ import type { Node } from "@contract/node.ts";
3
+ import { useMutation, useQueryClient } from "@tanstack/react-query";
4
+ import { allTreeLevelsKey } from "@/app/tree-move/tree-move.tsx";
5
+ import { useIntelRouterContext } from "@/router/router-context.ts";
6
+
7
+ /** The two things that carry a title, and the two doors that change one. */
8
+ export type RenameTarget = { type: "node"; node: Node } | { type: "flow"; flow: Flow };
9
+
10
+ /**
11
+ * Everything a change to one resource can make stale (#823).
12
+ *
13
+ * The row sits in one level of the tree, the open screen reads the record, and the flat collections
14
+ * behind the search, the link picker and the relation view all carry the title as well — a rename
15
+ * that only reached the tree would leave yesterday's name standing on three other screens.
16
+ *
17
+ * ⚠️ Every level rather than the one this row sits in: archiving the last row of a folder takes
18
+ * that folder's arrow away, and the arrow is drawn from the folder's own row one level higher
19
+ * (`allTreeLevelsKey`, #519). Renaming needs no more than the one level — but two invalidations
20
+ * that differ per entry are how one of them goes missing.
21
+ *
22
+ * ⚠️ It lives here rather than in either caller because there are now TWO ways to rename: the
23
+ * menu's dialog and the tree's own row (#823). A copy of this list is a second place for one rule
24
+ * to drift, and the direction it drifts in is silent — a name that stays right on the screen the
25
+ * renamer is looking at and wrong on the three they are not.
26
+ */
27
+ export function useResourceRefresh(target: RenameTarget): () => Promise<void> {
28
+ const queryClient = useQueryClient();
29
+ const id = target.type === "flow" ? target.flow.id : target.node.id;
30
+ return async () => {
31
+ await Promise.all([
32
+ queryClient.invalidateQueries({ queryKey: allTreeLevelsKey }),
33
+ queryClient.invalidateQueries({
34
+ queryKey: target.type === "flow" ? ["flow", id] : ["node", id],
35
+ }),
36
+ queryClient.invalidateQueries({
37
+ queryKey: [target.type === "flow" ? "flows" : "node-graph"],
38
+ }),
39
+ queryClient.invalidateQueries({ queryKey: ["relation-graph"] }),
40
+ ]);
41
+ };
42
+ }
43
+
44
+ /**
45
+ * One new title, through the door the thing behind it has (#823).
46
+ *
47
+ * ⚠️ `baseUpdatedAt` is not optional decoration. Renaming takes the same conflict route as every
48
+ * other change, or two people renaming at once end with one of them silently overwritten — and the
49
+ * one who loses never learns that they did.
50
+ *
51
+ * ⚠️ A flow and a document are two doors, not one with a flag: `updateFlow` and `updateNode` write
52
+ * different tables and answer to different capabilities (ADR-0004 §1).
53
+ */
54
+ export function useResourceRename(target: RenameTarget, onRenamed?: () => void) {
55
+ const { data } = useIntelRouterContext();
56
+ const refresh = useResourceRefresh(target);
57
+ return useMutation({
58
+ mutationFn: async (next: string) => {
59
+ if (target.type === "flow") {
60
+ await data.updateFlow({
61
+ flowId: target.flow.id,
62
+ baseUpdatedAt: target.flow.updatedAt,
63
+ title: next,
64
+ idempotencyKey: crypto.randomUUID(),
65
+ });
66
+ return;
67
+ }
68
+ await data.updateNode({
69
+ nodeId: target.node.id,
70
+ baseUpdatedAt: target.node.updatedAt,
71
+ title: next,
72
+ idempotencyKey: crypto.randomUUID(),
73
+ });
74
+ },
75
+ onSuccess: async () => {
76
+ onRenamed?.();
77
+ await refresh();
78
+ },
79
+ });
80
+ }
@@ -0,0 +1,67 @@
1
+ import { useQuery } from "@tanstack/react-query";
2
+ import { createContext, type ReactNode, useContext, useMemo } from "react";
3
+ import { useIntelRouterContext } from "@/router/router-context.ts";
4
+
5
+ /**
6
+ * The names behind a set of user ids a screen is currently showing (#700).
7
+ *
8
+ * ⚠️ **One request for the whole screen, not one per person.** A hook that resolved a single id
9
+ * would be called once per row and once per circle, and a board with forty cards would open forty
10
+ * requests for four distinct people. The ids are collected where they are already all in one
11
+ * place — the board view, the access summary — and asked for together.
12
+ *
13
+ * ⚠️ **Not a board thing**, though a board was the first to need it: `directory_resolve` names ids
14
+ * the caller already holds, and any screen that draws people can wrap itself in this (#803). It
15
+ * lives outside `board/` for that reason — the name that suggested otherwise cost a duplicated
16
+ * door once already (#751).
17
+ *
18
+ * ⚠️ **Absent is not an error.** An id that cannot be named is missing from the answer, and the
19
+ * map simply has no entry for it. Callers fall back to the same wording they used before names
20
+ * existed at all; nobody ever draws the raw id (#258).
21
+ */
22
+ const ResolvedNames = createContext<ReadonlyMap<string, string> | null>(null);
23
+
24
+ export function ResolvedNamesProvider({
25
+ ids,
26
+ children,
27
+ }: {
28
+ ids: readonly (string | null)[];
29
+ children: ReactNode;
30
+ }) {
31
+ const { data } = useIntelRouterContext();
32
+ // Sorted and de-duplicated so that the same screen produces the same key twice running: an
33
+ // unstable key would refetch on every render that reorders its rows, which a drag does
34
+ // constantly.
35
+ const wanted = useMemo(
36
+ () => [...new Set(ids.filter((id): id is string => id !== null))].sort(),
37
+ [ids],
38
+ );
39
+ const query = useQuery({
40
+ queryKey: ["resolved-names", wanted],
41
+ // ⚠️ Not `enabled: wanted.length > 0` alone — the queryFn must never be reached with an empty
42
+ // list, because the contract refuses it (`min(1)`) and the refusal would surface as a broken
43
+ // board rather than as the empty answer it means.
44
+ enabled: wanted.length > 0,
45
+ queryFn: async () => await data.resolveNames(wanted),
46
+ staleTime: 5 * 60 * 1000,
47
+ });
48
+ const names = useMemo(() => {
49
+ const map = new Map<string, string>();
50
+ for (const person of query.data?.items ?? []) map.set(person.id, person.name);
51
+ return map;
52
+ }, [query.data]);
53
+ return <ResolvedNames.Provider value={names}>{children}</ResolvedNames.Provider>;
54
+ }
55
+
56
+ /**
57
+ * The name behind one id, or `null` where this browser cannot say.
58
+ *
59
+ * ⚠️ Outside a provider this answers `null` for everybody, which is exactly the behaviour every
60
+ * caller already handles. A screen that forgets the provider shows what it showed before this
61
+ * existed; it does not break, and it does not leak an id.
62
+ */
63
+ export function useResolvedName(id: string | null): string | null {
64
+ const names = useContext(ResolvedNames);
65
+ if (id === null) return null;
66
+ return names?.get(id) ?? null;
67
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Which sentence a refused change to a resource is told with.
3
+ *
4
+ * ⚠️ A conflict is the one that must not be swallowed: every write in the product names the state it
5
+ * was made against — `baseUpdatedAt` for a record, `baseVersionId` for a table — and turns a parallel
6
+ * edit into a refusal. A refusal nobody words is indistinguishable from a change that went through.
7
+ *
8
+ * ⚠️ Flat and on its own since #531, where the table's column dialog became the second reader. It
9
+ * used to live in `resource-menu.tsx`; importing it from there would have made the menu and the
10
+ * dialog import each other, and a cycle is a thing that works until the bundler splits differently.
11
+ */
12
+ export function resourceErrorKey(error: unknown): string {
13
+ const code =
14
+ typeof error === "object" && error !== null && "code" in error
15
+ ? String((error as { code: unknown }).code)
16
+ : null;
17
+ switch (code) {
18
+ // ⚠️ `version_conflict` is the table's word for the same event (`nodes.ts`, `tableStateFor`):
19
+ // the positions or the mapping were read from a version that is no longer the newest. It is the
20
+ // conflict sentence and not the general one — "reload it and try again" is exactly the way out,
21
+ // and letting it fall through to `resource.failed` would tell somebody to reload in the same
22
+ // breath as claiming nobody knows what happened.
23
+ case "version_conflict":
24
+ case "update_conflict":
25
+ case "flow_update_conflict":
26
+ return "resource.conflict";
27
+ case "node_forbidden":
28
+ case "flow_edit_forbidden":
29
+ return "resource.forbidden";
30
+ default:
31
+ return "resource.failed";
32
+ }
33
+ }