@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,34 @@
1
+ import type { FlowNode } from "@contract/flow.ts";
2
+ import {
3
+ CircleDot,
4
+ CirclePlay,
5
+ FileText,
6
+ Folder,
7
+ GitBranch,
8
+ Paperclip,
9
+ Sparkles,
10
+ Table,
11
+ Workflow,
12
+ Wrench,
13
+ } from "lucide-react";
14
+ import type { ComponentType, SVGProps } from "react";
15
+
16
+ // The canvas card and the palette entry have to show the same symbol for the same kind — a step that
17
+ // looks one way in the bar and another on the graph is two things to learn instead of one. The map
18
+ // lives apart from both so neither has to import the other.
19
+ export type NodeIcon = ComponentType<SVGProps<SVGSVGElement>>;
20
+
21
+ export const nodeIcon: Record<FlowNode["kind"], NodeIcon> = {
22
+ trigger: CirclePlay,
23
+ instruction: Sparkles,
24
+ condition: GitBranch,
25
+ subflow: Workflow,
26
+ // The four links wear the symbol their kind wears in the tree, so a document is the same shape
27
+ // wherever it is seen — the sidebar, the picker and the canvas do not each teach their own.
28
+ folder: Folder,
29
+ document: FileText,
30
+ upload: Paperclip,
31
+ table: Table,
32
+ tool: Wrench,
33
+ output: CircleDot,
34
+ };
@@ -0,0 +1,214 @@
1
+ import { flowNodeLayer } from "@contract/flow.ts";
2
+ import { Plus, X } from "lucide-react";
3
+ import { Fragment, useId, useRef, useState } from "react";
4
+ import { type NodeIcon, nodeIcon } from "@/flows/node-icon/node-icon.ts";
5
+ import { useI18n } from "@/i18n/i18n-context.tsx";
6
+ import { cn } from "@/lib/utils.ts";
7
+ import type { NodePaletteProps, PaletteKind } from "./node-palette.types.ts";
8
+
9
+ const storageKey = "intel.flow-palette";
10
+
11
+ // ⚠️ Reading `localStorage` throws outright in a few privacy modes, so the guard is a try, not a
12
+ // typeof check — the same reason `sidebar-preferences` guards it that way. Losing the preference is
13
+ // acceptable; losing the editor is not.
14
+ function readOpen(): boolean {
15
+ try {
16
+ return typeof window !== "undefined" && window.localStorage.getItem(storageKey) === "open";
17
+ } catch {
18
+ return false;
19
+ }
20
+ }
21
+
22
+ function writeOpen(open: boolean): void {
23
+ try {
24
+ if (typeof window !== "undefined")
25
+ window.localStorage.setItem(storageKey, open ? "open" : "closed");
26
+ } catch {}
27
+ }
28
+
29
+ // Closed is the default, and the preference is written through: the bar is chrome, exactly like the
30
+ // sidebar whose open state already survives a reload. Someone who builds flows all day would
31
+ // otherwise re-open it on every load, and the ticket's own reason for collapsing it — the canvas
32
+ // belongs to the graph — is just as true for someone who never opens it.
33
+ export function usePaletteOpen(): [boolean, (open: boolean) => void] {
34
+ const [open, setOpen] = useState(readOpen);
35
+ return [
36
+ open,
37
+ (next: boolean) => {
38
+ setOpen(next);
39
+ writeOpen(next);
40
+ },
41
+ ];
42
+ }
43
+
44
+ export function NodePalette<K extends PaletteKind>({
45
+ kinds,
46
+ open,
47
+ setOpen,
48
+ add,
49
+ disabledReason,
50
+ }: NodePaletteProps<K>) {
51
+ const i18n = useI18n();
52
+ const listId = useId();
53
+ const triggerRef = useRef<HTMLButtonElement>(null);
54
+ const listRef = useRef<HTMLDivElement>(null);
55
+ // A toolbar carries one tab stop, so the group is entered with a single Tab and left with the
56
+ // next one instead of costing seven.
57
+ const [active, setActive] = useState(0);
58
+
59
+ // The focus goes back where it came from, so closing does not drop the caret at the top of the
60
+ // document and make the next Tab start over from the page.
61
+ function close() {
62
+ setOpen(false);
63
+ triggerRef.current?.focus();
64
+ }
65
+
66
+ function entries(): HTMLButtonElement[] {
67
+ return [
68
+ ...(listRef.current?.querySelectorAll<HTMLButtonElement>("button[data-palette-item]") ?? []),
69
+ ];
70
+ }
71
+
72
+ // A disabled entry is stepped over rather than focused: its reason is already on the entry, and
73
+ // stopping there would only be a dead end for the keyboard.
74
+ function focusEntry(from: number, delta: number) {
75
+ const items = entries();
76
+ if (items.length === 0) return;
77
+ for (let step = 1; step <= items.length; step += 1) {
78
+ const index = (((from + delta * step) % items.length) + items.length) % items.length;
79
+ const item = items[index];
80
+ if (item && !item.disabled) {
81
+ setActive(index);
82
+ item.focus();
83
+ return;
84
+ }
85
+ }
86
+ }
87
+
88
+ function onListKeyDown(event: React.KeyboardEvent<HTMLDivElement>) {
89
+ if (event.key === "Escape") {
90
+ event.stopPropagation();
91
+ close();
92
+ return;
93
+ }
94
+ const forward = event.key === "ArrowDown" || event.key === "ArrowRight";
95
+ const backward = event.key === "ArrowUp" || event.key === "ArrowLeft";
96
+ if (!forward && !backward) return;
97
+ event.preventDefault();
98
+ const items = entries();
99
+ const current = items.indexOf(document.activeElement as HTMLButtonElement);
100
+ focusEntry(current < 0 ? 0 : current, forward ? 1 : -1);
101
+ }
102
+
103
+ function onTriggerKeyDown(event: React.KeyboardEvent<HTMLButtonElement>) {
104
+ if (!open) return;
105
+ if (event.key === "Escape") {
106
+ event.stopPropagation();
107
+ close();
108
+ return;
109
+ }
110
+ if (event.key === "ArrowDown" || event.key === "ArrowRight") {
111
+ event.preventDefault();
112
+ focusEntry(-1, 1);
113
+ }
114
+ }
115
+
116
+ // Open, the trigger is the first segment of the bar itself rather than a second box above it: one
117
+ // closes a bar where it is. Two details carry that and neither is decoration:
118
+ //
119
+ // ⚠️ The negative margin is the frame's own padding plus its border. Opening hands the frame from
120
+ // the trigger to the row around it, and a frame drawn inside would push the trigger down and right
121
+ // by exactly that sum. The margin lets the row grow outwards instead, so the 36px one clicks stays
122
+ // on the same pixel open and collapsed. Change `p-1` and this has to change with it.
123
+ //
124
+ // ⚠️ The toolbar stays an element of its own inside the row: it is what `aria-controls` names and
125
+ // what the arrow keys walk, and the trigger must not become an eighth entry in that ring.
126
+ //
127
+ // The `w-max` that used to stand here went with the structure it guarded. While the bar was a
128
+ // block below the trigger, the shrink-wrapped box took the collapsed 36px as its maximum and
129
+ // folded the entries into a column; one flex row is sized from its content in either state, which
130
+ // was measured at 1280px and at 420px with the class and without it (#55).
131
+ return (
132
+ <div
133
+ className={cn(
134
+ "absolute left-4 top-4 z-10 max-w-[calc(100%-2rem)]",
135
+ open &&
136
+ "-m-[calc(0.25rem+1px)] flex flex-wrap items-center gap-1 rounded-lg border bg-card/95 p-1 shadow-sm backdrop-blur",
137
+ )}
138
+ >
139
+ <button
140
+ ref={triggerRef}
141
+ type="button"
142
+ aria-expanded={open}
143
+ aria-controls={listId}
144
+ onClick={() => (open ? close() : setOpen(true))}
145
+ onKeyDown={onTriggerKeyDown}
146
+ className={cn(
147
+ "inline-flex size-9 shrink-0 items-center justify-center rounded-lg text-card-foreground outline-none hover:bg-accent focus-visible:ring-2 focus-visible:ring-ring",
148
+ !open && "border bg-card/95 shadow-sm backdrop-blur",
149
+ )}
150
+ >
151
+ {open ? (
152
+ <X aria-hidden="true" className="size-4" />
153
+ ) : (
154
+ <Plus aria-hidden="true" className="size-4" />
155
+ )}
156
+ <span className="sr-only">{i18n.t(open ? "flows.closePalette" : "flows.addNode")}</span>
157
+ </button>
158
+ {/* Over the canvas rather than pushing it: the graph is a spatial workspace, and resizing the
159
+ viewport would shift every node under the pointer each time the bar opens. It is dismissed
160
+ instead — the trigger, Escape, or a click on the canvas. */}
161
+ {open && (
162
+ <div
163
+ ref={listRef}
164
+ id={listId}
165
+ role="toolbar"
166
+ aria-orientation="horizontal"
167
+ aria-label={i18n.t("flows.nodePalette")}
168
+ onKeyDown={onListKeyDown}
169
+ // `min-w-0` so a narrow bar makes the entries wrap among themselves instead of forcing the
170
+ // whole row wider than the canvas allows.
171
+ className="flex min-w-0 max-w-full flex-wrap gap-1"
172
+ >
173
+ {kinds.map((kind, index) => {
174
+ const Icon: NodeIcon = nodeIcon[kind];
175
+ const reason = disabledReason?.(kind) ?? null;
176
+ // ⚠️ A separator before the first entry of each layer after the first (D25). It is
177
+ // decoration, not structure: the toolbar's keyboard walks `[data-palette-item]`, so a
178
+ // divider in between must not be one — otherwise the arrows would stop on a line.
179
+ const opensLayer =
180
+ index > 0 && flowNodeLayer[kind] !== flowNodeLayer[kinds[index - 1] ?? kind];
181
+ return (
182
+ <Fragment key={kind}>
183
+ {opensLayer ? (
184
+ <span
185
+ aria-hidden="true"
186
+ className="mx-1 my-1.5 w-px self-stretch bg-border"
187
+ title={i18n.t(`flows.layer.${flowNodeLayer[kind]}`)}
188
+ />
189
+ ) : null}
190
+ <button
191
+ type="button"
192
+ data-palette-item=""
193
+ disabled={reason !== null}
194
+ title={reason ?? undefined}
195
+ tabIndex={index === active ? 0 : -1}
196
+ onClick={() => {
197
+ setActive(index);
198
+ add(kind);
199
+ }}
200
+ // `h-9` is the trigger's height: sharing one row only reads as one row if the entries
201
+ // start on the trigger's top edge instead of floating in the middle of it.
202
+ className="inline-flex h-9 items-center gap-1.5 rounded-md px-2 text-xs outline-none hover:bg-muted focus-visible:ring-2 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 disabled:hover:bg-transparent"
203
+ >
204
+ <Icon aria-hidden="true" className="size-3.5" />
205
+ {i18n.t(`flows.node.${kind}`)}
206
+ </button>
207
+ </Fragment>
208
+ );
209
+ })}
210
+ </div>
211
+ )}
212
+ </div>
213
+ );
214
+ }
@@ -0,0 +1,15 @@
1
+ import type { FlowNode } from "@contract/flow.ts";
2
+
3
+ export type PaletteKind = FlowNode["kind"];
4
+
5
+ // Generic over the kinds it is given: the caller decides which subset the bar offers, and `add` is
6
+ // handed back exactly that subset rather than "any node kind" it would have to re-narrow.
7
+ export interface NodePaletteProps<K extends PaletteKind = PaletteKind> {
8
+ kinds: readonly K[];
9
+ open: boolean;
10
+ setOpen(open: boolean): void;
11
+ add(kind: K): void;
12
+ // Why an entry is off, in words, or `null` when it is not. One function rather than a flag and a
13
+ // string: an entry that is greyed out without a reason is just a broken button.
14
+ disabledReason?(kind: K): string | null;
15
+ }
@@ -0,0 +1,321 @@
1
+ import { useQueries, useQuery } from "@tanstack/react-query";
2
+ import { useNavigate } from "@tanstack/react-router";
3
+ import { type ReactNode, useEffect, useState } from "react";
4
+ import { effectiveAccessQuery, ResourceAccessSummary } from "@/access-summary/access-summary.tsx";
5
+ import { treeLevelKey } from "@/app/tree-move/tree-move.tsx";
6
+ import { Skeleton } from "@/components/ui/skeleton.tsx";
7
+ import {
8
+ Table,
9
+ TableBody,
10
+ TableCell,
11
+ TableHead,
12
+ TableHeader,
13
+ TableRow,
14
+ } from "@/components/ui/table.tsx";
15
+ import type { TreeEntry } from "@/data/intel-data-provider/intel-data-provider.types.ts";
16
+ import { folderContentsOf } from "@/folder-index/folder-index.ts";
17
+ import { useI18n } from "@/i18n/i18n-context.tsx";
18
+ import { iconFor } from "@/kind-icon.ts";
19
+ import { ResourceMenu, type ResourceTarget } from "@/resource-menu/resource-menu.tsx";
20
+ import { useIntelRouterContext } from "@/router/router-context.ts";
21
+ import { RelativeTime } from "@/time/relative-time.tsx";
22
+ import { TitleRowScrollArea } from "@/title-row/title-row.tsx";
23
+
24
+ // The same record the menu is handed everywhere else, read off a level row. A re-labelling, never a
25
+ // second source of truth — a rename works on the whole record, not on a summary of it.
26
+ function targetOf(entry: TreeEntry): ResourceTarget {
27
+ return entry.type === "flow"
28
+ ? { type: "flow", flow: entry.flow }
29
+ : { type: "node", node: entry.node };
30
+ }
31
+
32
+ /**
33
+ * Whose access a level row shows (#817).
34
+ *
35
+ * The function exists because two places need the same answer — the cell that draws the summary,
36
+ * and the table that waits for every one of them before it draws itself. Two expressions would
37
+ * share nothing but their author's intention, and one of them would eventually watch a key the
38
+ * other never asks for.
39
+ *
40
+ * ⚠️ For a FLOW row it answers with the folder the flow lies in, and that is carried over from
41
+ * before this ticket rather than decided in it. It is not the whole truth any more: since D51
42
+ * (`anchrd/intel#530`, amendment to ADR-0004) a flow carries grants of its own, and
43
+ * `listEffectiveFlowAccess` is the call that would name them. Changing what this column shows is a
44
+ * different question from making the table load in one piece, so it has a ticket of its own (#844)
45
+ * rather than a quiet fix in the middle of this one.
46
+ */
47
+ function accessResourceId(entry: TreeEntry, folderId: string): string {
48
+ return entry.type === "flow" ? (entry.flow.parentId ?? folderId) : entry.id;
49
+ }
50
+
51
+ /**
52
+ * The placeholder rows, and how many there are (#817).
53
+ *
54
+ * The count IS this array's length — a separate constant beside it is joined to the widths by
55
+ * nothing but agreement, and the row that outlives the agreement has no width. The bars shrink from
56
+ * row to row so the block does not read as one grey rectangle. Same three widths, same `h-4` bar and
57
+ * same one-skeleton-per-column shape as the Gate half (`anchrd/gate#440`), because the two tables
58
+ * are supposed to look alike.
59
+ */
60
+ const skeletonRowWidths = ["w-full", "w-5/6", "w-2/3"];
61
+
62
+ /**
63
+ * How long the table holds itself back for the access cells (#817).
64
+ *
65
+ * ⚠️ A ceiling, not a timeout: nothing is cancelled when it passes. The table simply stops waiting
66
+ * and draws itself with whatever the access cells have — which for an undecided row is an empty
67
+ * cell that fills itself the moment its answer lands. Without the ceiling one slow row would keep a
68
+ * whole folder off the screen, and the reader would be worse off than with the sentence this
69
+ * replaced.
70
+ */
71
+ export const accessWaitMs = 1500;
72
+
73
+ /**
74
+ * Whether the table may draw itself yet (#817).
75
+ *
76
+ * ⚠️ DECIDED, not answered. The access queries run with `retry: false`, so a refusal is a final
77
+ * state — waiting for `success` would leave a whole folder off the screen forever because one row
78
+ * may not be inspected. `status !== "pending"` is both outcomes, and that word is the trap this
79
+ * ticket is about.
80
+ *
81
+ * ⚠️ It is a function of its own because a rendered table cannot tell the two apart. A build that
82
+ * waited for `success` looks identical on screen — the ceiling below rescues it a second and a half
83
+ * later and every cell ends up in the same place. Only the CLOCK differs, and a clock is what a
84
+ * test under load may not measure. Here the difference is a value, and a value can be asserted.
85
+ */
86
+ export function tableIsReady(
87
+ accessStatuses: Array<"pending" | "error" | "success">,
88
+ ceilingPassed: boolean,
89
+ ): boolean {
90
+ return ceilingPassed || accessStatuses.every((status) => status !== "pending");
91
+ }
92
+
93
+ /**
94
+ * A folder's own screen: what lies directly in it, as a table.
95
+ *
96
+ * ⚠️ It reads the SAME query the sidebar reads for the same folder — `treeLevelKey`, filled by
97
+ * `listTreeChildren`. Not a second call and not a second cache entry: two readers of one folder that
98
+ * fetch separately show it differently the moment one of them is stale, and that is the kind of
99
+ * difference a customer finds before anybody here does. The other half of the bargain is free —
100
+ * every invalidation the tree already does after a create, a move or an archive lands here too, and
101
+ * that now includes the ones this screen's own menu makes: since #519 they all refresh every tree
102
+ * level (`allTreeLevelsKey`), and this level is one of them.
103
+ *
104
+ * ⚠️ There is no "Kind" column since #101. The icon says what a row is, in the same picture the
105
+ * sidebar draws two centimetres to its left, and `sr-only` says it in words for everyone who does
106
+ * not read pictures. A column repeating the icon in prose costs the width the title needs.
107
+ *
108
+ * ⚠️ The table appears as a whole (#817). Every row used to ask for its own effective access and
109
+ * draw nothing until the answer arrived, so six rows were six answers trickling in one after
110
+ * another. The waiting is done here instead, over the very same query keys, so nothing is asked
111
+ * twice — `useQueries` and the cells below share one cache entry per resource.
112
+ */
113
+ export function FolderContents({ folderId }: { folderId: string }) {
114
+ const { data } = useIntelRouterContext();
115
+ const i18n = useI18n();
116
+ const navigate = useNavigate();
117
+ const level = useQuery({
118
+ queryKey: treeLevelKey(folderId),
119
+ queryFn: () => data.listTreeChildren(folderId),
120
+ });
121
+ // The folder's own text is not one of its contents (#824) — the same reading the tree does, from
122
+ // the same function, because two answers to "what is in this folder" is how they come to differ.
123
+ // ⚠️ It has to happen HERE, above the access queries: the folder text is not a row, so it must
124
+ // not be one of the answers the table waits for either.
125
+ const entries = folderContentsOf(folderId, level.data ?? []);
126
+ // ⚠️ The same options object the cells use, from the same factory. The point is the shared cache
127
+ // entry: this starts the requests while the placeholders are on screen, and the cell that renders
128
+ // afterwards reads the answer rather than asking again.
129
+ const access = useQueries({
130
+ queries: entries.map((entry) =>
131
+ effectiveAccessQuery(data, {
132
+ resourceId: accessResourceId(entry, folderId),
133
+ kind: "node",
134
+ }),
135
+ ),
136
+ });
137
+ // ⚠️ Both pieces of state below remember WHICH folder they are about rather than a bare `true`,
138
+ // so opening the next folder invalidates them by itself. A boolean would need a reset beside it,
139
+ // and a reset that runs one commit late shows the new folder through the old folder's decision.
140
+ const entryIds = entries.map((entry) => entry.id).join(",");
141
+
142
+ // The clock starts when the rows are known, not when the screen mounts: otherwise a slow folder
143
+ // listing would spend the access budget before a single access query had been sent.
144
+ const [ceilingFor, setCeilingFor] = useState("");
145
+ useEffect(() => {
146
+ if (entryIds === "") return;
147
+ const timer = setTimeout(() => setCeilingFor(entryIds), accessWaitMs);
148
+ return () => clearTimeout(timer);
149
+ }, [entryIds]);
150
+
151
+ /**
152
+ * ⚠️ Once the table is up it stays up, and that is not a nicety (#817).
153
+ *
154
+ * Waiting for every row is what the FIRST appearance is about. Afterwards it would be the very
155
+ * thing this ticket is against: the access queries go pending again whenever the effective-grants
156
+ * prefix is invalidated, which a single share does to every summary on screen, and an added row
157
+ * puts one pending query beside four settled ones. Either way the whole folder would blink back
158
+ * to placeholders — a table that disappears as a whole, which is worse than the trickle it
159
+ * replaced, because the rows that were readable a moment ago are gone too.
160
+ *
161
+ * ⚠️ It is keyed on the FOLDER, not on the row set. Keyed on the rows it dissolves the moment
162
+ * somebody creates a document in the open folder, which is exactly the case it exists for; the
163
+ * new row then arrives with an empty access cell that fills itself, and nothing else moves.
164
+ */
165
+ const [revealedFor, setRevealedFor] = useState("");
166
+ const ready =
167
+ revealedFor === folderId ||
168
+ tableIsReady(
169
+ access.map((query) => query.status),
170
+ ceilingFor === entryIds,
171
+ );
172
+ useEffect(() => {
173
+ if (ready && entryIds !== "") setRevealedFor(folderId);
174
+ }, [ready, entryIds, folderId]);
175
+
176
+ if (level.isError) {
177
+ return (
178
+ <p role="alert" className="p-6 text-sm text-destructive">
179
+ {i18n.t("node.folderFailed")}
180
+ </p>
181
+ );
182
+ }
183
+ // ⚠️ An empty folder keeps a sentence, unlike an empty row in the tree (#59). A folder IS this
184
+ // screen: a blank rectangle leaves someone who just created it with nowhere to go, which is the
185
+ // same reason `tree.empty` survives at the root.
186
+ if (!level.isPending && entries.length === 0) {
187
+ return (
188
+ <div className="grid flex-1 place-items-center p-8 text-sm text-muted-foreground">
189
+ {i18n.t("node.folderEmpty")}
190
+ </div>
191
+ );
192
+ }
193
+
194
+ if (level.isPending || !ready) {
195
+ return (
196
+ <LevelTable i18n={i18n}>
197
+ {/* The word the sentence used to carry, kept for the readers a pulsing rectangle says
198
+ nothing to. The placeholder rows themselves are hidden from them: they are a picture of
199
+ a table, and an accessibility tree that lists twelve empty cells is worse than silence. */}
200
+ <TableRow className="hover:bg-transparent">
201
+ <TableCell colSpan={4} className="h-0 p-0">
202
+ <span role="status" className="sr-only">
203
+ {i18n.t("common.loading")}
204
+ </span>
205
+ </TableCell>
206
+ </TableRow>
207
+ {skeletonRowWidths.map((width) => (
208
+ <TableRow key={width} aria-hidden="true" className="hover:bg-transparent">
209
+ <TableCell className="px-4 py-2.5">
210
+ <Skeleton className={`h-4 ${width}`} />
211
+ </TableCell>
212
+ <TableCell className="px-4">
213
+ <Skeleton className={`h-4 ${width}`} />
214
+ </TableCell>
215
+ <TableCell className="px-4">
216
+ <Skeleton className={`h-4 ${width}`} />
217
+ </TableCell>
218
+ <TableCell className="w-14 px-3">
219
+ <Skeleton className="h-4 w-4" />
220
+ </TableCell>
221
+ </TableRow>
222
+ ))}
223
+ </LevelTable>
224
+ );
225
+ }
226
+
227
+ return (
228
+ <LevelTable i18n={i18n}>
229
+ {entries.map((entry) => {
230
+ const changed = entry.type === "flow" ? entry.flow.updatedAt : entry.node.updatedAt;
231
+ const Icon = iconFor(entry);
232
+ return (
233
+ <TableRow key={entry.id} className="group/row hover:bg-muted">
234
+ {/* ⚠️ The button carries the whole cell rather than the cell carrying an onClick.
235
+ A row that only reacts to a mouse is a row nobody can reach with a keyboard, and
236
+ a `<tr onClick>` has no name to read out either. */}
237
+ <TableCell className="p-0">
238
+ <button
239
+ type="button"
240
+ onClick={() =>
241
+ void navigate({
242
+ to: entry.type === "flow" ? "/flows" : "/nodes",
243
+ search: { select: entry.id },
244
+ })
245
+ }
246
+ className="flex w-full min-w-0 items-center gap-3 px-4 py-2.5 text-left outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-inset"
247
+ >
248
+ {/* The icon gets a tile of its own so five kinds under one another read as a
249
+ column, instead of five loose glyphs at five different widths. */}
250
+ <span
251
+ aria-hidden="true"
252
+ className="grid size-8 shrink-0 place-items-center rounded-md bg-muted/50 text-muted-foreground transition-colors group-hover/row:bg-accent group-hover/row:text-accent-foreground"
253
+ >
254
+ <Icon className="size-4" />
255
+ </span>
256
+ <span className="min-w-0 truncate font-medium group-hover/row:underline">
257
+ {entry.title}
258
+ </span>
259
+ {/* The icon carries the type on screen; this carries it everywhere else, and
260
+ it is why a flow needs no suffix in its name to be told from a document. */}
261
+ <span className="sr-only">{i18n.t(`node.kind.${entry.kind}`)}</span>
262
+ </button>
263
+ </TableCell>
264
+ <TableCell className="px-4 text-right text-sm text-muted-foreground tabular-nums">
265
+ <RelativeTime value={changed} />
266
+ </TableCell>
267
+ <TableCell className="px-4">
268
+ <ResourceAccessSummary
269
+ resourceId={accessResourceId(entry, folderId)}
270
+ ownerId={entry.type === "flow" ? entry.flow.ownerId : entry.node.ownerId}
271
+ />
272
+ </TableCell>
273
+ {/* ⚠️ The same menu as the title line of the open thing, not a shorter one built
274
+ for here. #58 collected every action in one place so that place is always the
275
+ same; a folder listing offering three of the six would be a second place. */}
276
+ <TableCell className="px-3 text-right">
277
+ <ResourceMenu target={targetOf(entry)} variant="row" />
278
+ </TableCell>
279
+ </TableRow>
280
+ );
281
+ })}
282
+ </LevelTable>
283
+ );
284
+ }
285
+
286
+ /**
287
+ * The chrome both states are drawn in (#817).
288
+ *
289
+ * ⚠️ One shell rather than two, and that is what keeps the columns from jumping: the header row
290
+ * governs the widths and it is the SAME header in both states, over a body whose cell count matches.
291
+ * Two tables that merely look alike would be two places to change a column in.
292
+ */
293
+ function LevelTable({ i18n, children }: { i18n: ReturnType<typeof useI18n>; children: ReactNode }) {
294
+ return (
295
+ <TitleRowScrollArea className="min-h-0 flex-1 overflow-y-auto p-6">
296
+ <div className="overflow-hidden rounded-lg border bg-card">
297
+ <Table>
298
+ <TableHeader className="[&_tr]:bg-muted/40">
299
+ <TableRow className="hover:bg-transparent">
300
+ <TableHead className="h-9 px-4 text-xs tracking-wide uppercase">
301
+ {i18n.t("common.title")}
302
+ </TableHead>
303
+ <TableHead className="h-9 px-4 text-right text-xs tracking-wide uppercase">
304
+ {i18n.t("node.changed")}
305
+ </TableHead>
306
+ <TableHead className="h-9 px-4 text-xs tracking-wide uppercase">
307
+ {i18n.t("node.access")}
308
+ </TableHead>
309
+ {/* A `<th>` with nothing in it leaves the column unnamed for anyone reading the table
310
+ by its headers. The word is there; only the eye is spared it. */}
311
+ <TableHead className="h-9 w-14">
312
+ <span className="sr-only">{i18n.t("node.rowActions")}</span>
313
+ </TableHead>
314
+ </TableRow>
315
+ </TableHeader>
316
+ <TableBody>{children}</TableBody>
317
+ </Table>
318
+ </div>
319
+ </TitleRowScrollArea>
320
+ );
321
+ }
@@ -0,0 +1,55 @@
1
+ import { isFolderIndexTitle } from "@contract/node.ts";
2
+ import type { TreeEntry } from "@/data/intel-data-provider/intel-data-provider.types.ts";
3
+
4
+ /**
5
+ * Which row of a folder is the folder's own text, and which rows are its contents (#824).
6
+ *
7
+ * ⚠️ **The split happens here and not in the data provider.** The provider answers what the server
8
+ * said, and the server hides nothing — over MCP an `index` is a document like any other, which is
9
+ * the point of doing this in the view. A provider that filtered would make the browser's cache a
10
+ * second, quieter truth about the tree, and the folder's own details panel — which needs the very
11
+ * row the tree drops — would have to ask again for what it already had.
12
+ *
13
+ * ⚠️ **Only a document**, never a folder, a table or a board of that name. What is being served is
14
+ * a convention about TEXT, and a folder called `index` disappearing into its parent's details is
15
+ * the same word meaning two things.
16
+ */
17
+ export function folderIndexOf(
18
+ parentId: string | null,
19
+ entries: readonly TreeEntry[],
20
+ ): TreeEntry | null {
21
+ /**
22
+ * ⚠️ **The top level is not a folder, and the convention does not reach it** (found in review of
23
+ * #843). A folder shows its own text in its details; the root has no node, no details panel and
24
+ * no menu — so a document called `index` up there would lose its only row and gain nothing in
25
+ * exchange, reachable afterwards through the search or a link somebody already had. The rule
26
+ * lives here rather than in the two callers, because a rule that has to be remembered twice is
27
+ * one that will be remembered once.
28
+ */
29
+ if (parentId === null) return null;
30
+ const candidates = entries.filter(
31
+ (entry) =>
32
+ entry.type === "node" && entry.kind === "document" && isFolderIndexTitle(entry.title),
33
+ );
34
+ /**
35
+ * ⚠️ **Two documents of that name can exist, and exactly ONE of them becomes the folder's text:
36
+ * the oldest.** The ids are ULIDs, so the smallest is the one written first — the one whose
37
+ * author meant the convention, before anybody added a second.
38
+ *
39
+ * ⚠️ And the others STAY IN THE TREE. That is the answer to "then one of them would be
40
+ * invisible": a row that no list draws and no menu reaches is a document nobody can move,
41
+ * archive or even find, and nothing in the folder would say it exists. Two rows called `index`,
42
+ * one of them the folder's text and one of them an ordinary document, is a state somebody can
43
+ * see and put right.
44
+ */
45
+ return candidates.sort((left, right) => left.id.localeCompare(right.id))[0] ?? null;
46
+ }
47
+
48
+ /** Everything a folder shows as its contents: its rows, minus the one that is its own text. */
49
+ export function folderContentsOf(
50
+ parentId: string | null,
51
+ entries: readonly TreeEntry[],
52
+ ): TreeEntry[] {
53
+ const index = folderIndexOf(parentId, entries);
54
+ return index === null ? [...entries] : entries.filter((entry) => entry.id !== index.id);
55
+ }