@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,546 @@
1
+ import { serverOf, type ToolCapability, type ToolServer } from "@contract/tool.ts";
2
+ import { useQuery } from "@tanstack/react-query";
3
+ import { useRouterState } from "@tanstack/react-router";
4
+ import {
5
+ AlertTriangle,
6
+ ChevronRight,
7
+ HelpCircle,
8
+ PlugZap,
9
+ ShieldOff,
10
+ Timer,
11
+ Wrench,
12
+ } from "lucide-react";
13
+ import * as React from "react";
14
+ import { Button } from "@/components/ui/button";
15
+ import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
16
+ import type { I18n } from "@/i18n/i18n.types.ts";
17
+ import { useI18n } from "@/i18n/i18n-context.tsx";
18
+ import { useIntelRouterContext } from "@/router/router-context.ts";
19
+ import { selectedFrom } from "@/router/selection-search.ts";
20
+
21
+ interface ToolGroup {
22
+ // The handle the portal named, or `null` for the box holding what no named server carries.
23
+ handle: string | null;
24
+ label: string;
25
+ items: ToolCapability[];
26
+ }
27
+
28
+ /**
29
+ * The tools of one catalog, under the servers the portal itself named (#212).
30
+ *
31
+ * ⚠️ The grouping comes from `GET /api/v1/tools/servers`, never from the tool name alone. The name
32
+ * cannot say where the server part ends — `intel_flow_get` reads as `intel` + `flow_get` and as
33
+ * `intel_flow` + `get` — so this screen matches the portal's own handles with the same `serverOf`
34
+ * the delegation is cut with, longest match wins. The earlier reading, a cut at a separator, was
35
+ * shipped twice and was wrong twice: at `__` it put all 92 tools of the running installation in one
36
+ * box, because the portal writes a single underscore (#106), and at `_` it would break the first
37
+ * server whose slug carries one (#107, closed for that reason).
38
+ *
39
+ * ⚠️ A tool that matches no named handle is NOT dropped. It goes into its own box: the portal's
40
+ * management tools (`portal_list_servers`) live there, because the portal does not list itself as
41
+ * an upstream, and so does anything from a server the directory failed to mention. Hiding those
42
+ * would make the screen quietly disagree with `tools/list` — and the count on the lid is what the
43
+ * reader compares against the portal.
44
+ *
45
+ * The order is the portal's own: a group appears where its first tool appears, because the portal
46
+ * returns a server's tools together and sorting would replace its answer with a preference. Only
47
+ * the unattributed box is forced last — it is a remainder, not a server, and it must not lead.
48
+ */
49
+ function groupByServer(
50
+ items: ToolCapability[],
51
+ servers: ToolServer[],
52
+ unlistedLabel: string,
53
+ ): ToolGroup[] {
54
+ const named = new Map(servers.map((server) => [server.handle, server.name]));
55
+ const groups = new Map<string, ToolGroup>();
56
+ const unlisted: ToolGroup = { handle: null, label: unlistedLabel, items: [] };
57
+ for (const item of items) {
58
+ const handle = serverOf(item.name, named.keys());
59
+ if (handle === null) {
60
+ unlisted.items.push(item);
61
+ continue;
62
+ }
63
+ // The server's display name, not its handle: `notion` is what the wire says, and what the
64
+ // portal calls it is what the reader knows it by.
65
+ const group = groups.get(handle) ?? { handle, label: named.get(handle) ?? handle, items: [] };
66
+ group.items.push(item);
67
+ groups.set(handle, group);
68
+ }
69
+ return unlisted.items.length > 0 ? [...groups.values(), unlisted] : [...groups.values()];
70
+ }
71
+
72
+ // Signed in to Intel is signed in to the portal, so the sign-in is attempted at most once per
73
+ // visit. A second attempt after a refusal would be a redirect loop with an unchanging answer, and
74
+ // the marker in the URL only survives until the next navigation — the browser session remembers it
75
+ // instead (#60).
76
+ //
77
+ // ⚠️ It records that an attempt was STARTED, never that one was answered (#426). A sign-in that
78
+ // crashed on the way leaves exactly the same note as one that came back, so nothing may read it as
79
+ // a verdict about anybody: it only keeps the screen from walking in a circle on its own, and what
80
+ // the reader is told comes from the marker in the URL instead. That is the whole distinction — the
81
+ // note is never cleared to make a second attempt possible; the button is what makes one possible.
82
+ const SignInAttemptKey = "intel.portal-sign-in-attempted";
83
+
84
+ /**
85
+ * What came back from the silent walk — and "nothing" is one of the answers (#426).
86
+ *
87
+ * The marker in the URL is the only thing that ever says why the walk ended, and it is absent in two
88
+ * situations that look identical from here: the attempt never ran, or it broke before it could
89
+ * return. Neither is a decision about access, so neither may borrow the sentence that describes one
90
+ * — the rule `packages/intel/ui/CLAUDE.md` keeps from #350: **a missing statement is a reason to say less,
91
+ * never to refuse more.** Saying "no access" for `none` sent a reader hunting through portal
92
+ * policies while the sign-in had answered 500 (#425).
93
+ */
94
+ type PortalAnswer = "refused" | "expired" | "failed" | "none";
95
+
96
+ function portalAnswerOf(connectError: string | null): PortalAnswer {
97
+ if (connectError === null) return "none";
98
+ if (connectError === "portal_sign_in_expired") return "expired";
99
+ // The two codes Intel itself writes when somebody was actually turned away — by the portal, or by
100
+ // Gate before the portal was ever asked. Every other marker is machinery, not policy.
101
+ return connectError === "portal_sign_in_refused" || connectError === "permission_required"
102
+ ? "refused"
103
+ : "failed";
104
+ }
105
+
106
+ // Reading `sessionStorage` throws outright in a few privacy modes, so the guard is a try, not a
107
+ // feature check. Losing the note costs one extra redirect, and the marker the refusal leaves in the
108
+ // URL still ends the walk — it must never cost the screen.
109
+ function attemptStore(): Storage | null {
110
+ try {
111
+ return typeof window === "undefined" ? null : window.sessionStorage;
112
+ } catch {
113
+ return null;
114
+ }
115
+ }
116
+
117
+ // The catalog is one live `tools/list` with the signed-in user's own portal token. Nothing here is
118
+ // stored, mirrored or administered — the screen shows what the portal answers and nothing else.
119
+ export function Tools() {
120
+ const { data } = useIntelRouterContext();
121
+ const i18n = useI18n();
122
+ const catalog = useQuery({ queryKey: ["tools"], queryFn: () => data.listTools() });
123
+ // The servers behind that same live list (D30). Its own query, and its own failure: a portal that
124
+ // answers tools but no directory still has a usable screen, it just has no boxes to put them in.
125
+ // ⚠️ No retry on this one, and that is a decision about the screen rather than about the request.
126
+ // The tools are held back until the directory has answered, so every further attempt is time
127
+ // somebody spends in front of the word "Loading" — and a retry does not even have to happen: a
128
+ // React Query retry is PAUSED while the tab is unfocused, so a screen waiting on one can stay
129
+ // "Loading" for as long as the reader is looking elsewhere. A portal that offers no directory
130
+ // answers the same 502 every time anyway; one attempt settles it, and what follows is the list
131
+ // plus the sentence saying why it is not sorted.
132
+ const servers = useQuery({
133
+ queryKey: ["tool-servers"],
134
+ queryFn: () => data.listToolServers(),
135
+ retry: false,
136
+ });
137
+ // A hit from the header search arrives as `?select=<tool name>`, and the row it names opens with
138
+ // the list. Nothing else about the row changes: it is still only a disclosure.
139
+ const requested = useRouterState({ select: (state) => selectedFrom(state.location.search) });
140
+ // The silent sign-in returns here with a marker when it failed. The value comes from outside
141
+ // and could carry provider detail, so it is never rendered — it is only compared against the
142
+ // codes Intel itself writes, to tell "you may not" from "the sign-in broke" (#93): the first is
143
+ // a sentence about access, the second must not masquerade as one.
144
+ const connectError =
145
+ typeof window === "undefined"
146
+ ? null
147
+ : new URLSearchParams(window.location.search).get("connectError");
148
+ const answer = portalAnswerOf(connectError);
149
+ // What the screen DID about that answer, which is a third thing beside the catalog and the marker.
150
+ //
151
+ // ⚠️ Only `none` starts undecided. `useEffect` runs after the paint, so a state that always began
152
+ // at `deciding` would push a "Loading" frame in front of all four notices — and three of them are
153
+ // already settled at the first render, because their answer stands in the URL. It would flash at
154
+ // exactly the readers who just arrived out of a redirect. `none` is the one that genuinely has to
155
+ // wait: its walk starts one tick after the catalog arrives, and "we do not know" in between is a
156
+ // sentence the screen takes back immediately.
157
+ const [walk, setWalk] = React.useState<"deciding" | "running" | "stopped">(
158
+ answer === "none" ? "deciding" : "stopped",
159
+ );
160
+
161
+ const startSignIn = React.useCallback(() => {
162
+ // ⚠️ `setItem`, and the marker is not cleared anywhere on this path. Pressing the button STARTS
163
+ // an attempt, which is the only thing the marker ever claims — clearing it would buy the click
164
+ // nothing (no code in this path reads it) and cost the next visit: an attempt that dies without
165
+ // ever coming back with a marker would be followed by a silent redirect nobody asked for, on a
166
+ // screen that has just said it does not understand what is happening. The second attempt this
167
+ // browser session owes its reader is this button, never a re-armed guard.
168
+ attemptStore()?.setItem(SignInAttemptKey, "1");
169
+ setWalk("running");
170
+ data.startPortalSignIn("/tools");
171
+ }, [data]);
172
+
173
+ // ⚠️ Nobody is asked to start this. The portal sign-in is a browser redirect, so the screen
174
+ // begins it itself the moment the catalog says there is no portal session yet — that is the whole
175
+ // of what replaced the "Connect the portal" button.
176
+ React.useEffect(() => {
177
+ if (!catalog.data) return;
178
+ const attempts = attemptStore();
179
+ if (catalog.data.portalConnected) {
180
+ // An answered sign-in frees the next one: when the token later expires beyond renewal, the
181
+ // same silent walk runs again rather than stopping at a screen.
182
+ attempts?.removeItem(SignInAttemptKey);
183
+ return;
184
+ }
185
+ if (answer !== "none" || attempts?.getItem(SignInAttemptKey)) {
186
+ setWalk("stopped");
187
+ return;
188
+ }
189
+ attempts?.setItem(SignInAttemptKey, "1");
190
+ setWalk("running");
191
+ data.startPortalSignIn("/tools");
192
+ }, [catalog.data, data, answer]);
193
+
194
+ return (
195
+ <div className="min-h-full p-8">
196
+ <div className="mx-auto w-full max-w-4xl space-y-5">
197
+ {catalog.isPending ? (
198
+ <p className="text-sm text-muted-foreground">{i18n.t("common.loading")}</p>
199
+ ) : catalog.isError ? (
200
+ // Unreachable is not empty: the portal did not answer, so Intel knows nothing about this
201
+ // user's tools rather than knowing there are none.
202
+ <Notice
203
+ alert
204
+ icon={<PlugZap aria-hidden="true" className="mx-auto size-8 text-destructive" />}
205
+ title={i18n.t("tools.unreachable")}
206
+ help={i18n.t("tools.unreachableHelp")}
207
+ >
208
+ <Button variant="outline" onClick={() => void catalog.refetch()}>
209
+ {i18n.t("common.retry")}
210
+ </Button>
211
+ </Notice>
212
+ ) : !catalog.data.portalConnected ? (
213
+ walk === "deciding" ? (
214
+ <p className="text-sm text-muted-foreground">{i18n.t("common.loading")}</p>
215
+ ) : walk === "running" ? (
216
+ <p className="text-sm text-muted-foreground">{i18n.t("tools.signingIn")}</p>
217
+ ) : (
218
+ <PortalNotice answer={answer} i18n={i18n} onRetry={startSignIn} />
219
+ )
220
+ ) : catalog.data.items.length === 0 ? (
221
+ <Notice
222
+ icon={<Wrench aria-hidden="true" className="mx-auto size-8 text-muted-foreground" />}
223
+ title={i18n.t("tools.empty")}
224
+ help={i18n.t("tools.emptyHelp")}
225
+ />
226
+ ) : servers.isPending ? (
227
+ // The tools are here and the servers are not yet. Showing the flat list first and
228
+ // reshuffling it a moment later would be a screen that moves under the reader's hand.
229
+ <p className="text-sm text-muted-foreground">{i18n.t("common.loading")}</p>
230
+ ) : (
231
+ <ToolCatalogView
232
+ items={catalog.data.items}
233
+ servers={servers.isError ? [] : servers.data.items}
234
+ i18n={i18n}
235
+ requested={requested}
236
+ />
237
+ )}
238
+ </div>
239
+ </div>
240
+ );
241
+ }
242
+
243
+ /**
244
+ * The end of the silent walk, in the words of whatever ended it — one sentence per answer, and no
245
+ * sentence shared between two of them (#426).
246
+ *
247
+ * ⚠️ Only `refused` is a statement about ACCESS, and only it is a dead end: there is nothing the
248
+ * reader could press that would change the answer, so nothing is offered. The other two are the
249
+ * machinery and the silence, and both are worth another attempt — which is the button the screen
250
+ * lacked entirely while the marker in `sessionStorage` made the first attempt the only one.
251
+ */
252
+ function PortalNotice({
253
+ answer,
254
+ i18n,
255
+ onRetry,
256
+ }: {
257
+ answer: PortalAnswer;
258
+ i18n: I18n;
259
+ onRetry: () => void;
260
+ }) {
261
+ if (answer === "refused") {
262
+ return (
263
+ <Notice
264
+ alert
265
+ icon={<ShieldOff aria-hidden="true" className="mx-auto size-8 text-muted-foreground" />}
266
+ title={i18n.t("tools.noAccess")}
267
+ help={i18n.t("tools.noAccessHelp")}
268
+ />
269
+ );
270
+ }
271
+ // Ran out of time on the consent screen. Nothing is broken and nobody was refused, so neither of
272
+ // the other sentences fits (#95).
273
+ if (answer === "expired") {
274
+ return (
275
+ <Notice
276
+ icon={<Timer aria-hidden="true" className="mx-auto size-8 text-muted-foreground" />}
277
+ title={i18n.t("tools.connectExpired")}
278
+ help={i18n.t("tools.connectExpiredHelp")}
279
+ >
280
+ <Button variant="outline" onClick={onRetry}>
281
+ {i18n.t("tools.connectRetry")}
282
+ </Button>
283
+ </Notice>
284
+ );
285
+ }
286
+ // A marker that is neither of the two access answers is machinery, not policy: the sign-in broke
287
+ // before anyone could be allowed or refused. Saying "no access" here sent the last reader hunting
288
+ // through Access policies while the real fault sat in a failed client registration (#93).
289
+ if (answer === "failed") {
290
+ return (
291
+ <Notice
292
+ alert
293
+ icon={<PlugZap aria-hidden="true" className="mx-auto size-8 text-destructive" />}
294
+ title={i18n.t("tools.connectFailed")}
295
+ help={i18n.t("tools.connectFailedHelp")}
296
+ >
297
+ <Button variant="outline" onClick={onRetry}>
298
+ {i18n.t("tools.connectRetry")}
299
+ </Button>
300
+ </Notice>
301
+ );
302
+ }
303
+ // No marker at all, and an attempt already made in this browser session: it never came back. The
304
+ // screen says exactly that — not "no access", which is a decision nobody made here.
305
+ return (
306
+ <Notice
307
+ icon={<HelpCircle aria-hidden="true" className="mx-auto size-8 text-muted-foreground" />}
308
+ title={i18n.t("tools.connectUnknown")}
309
+ help={i18n.t("tools.connectUnknownHelp")}
310
+ >
311
+ <Button variant="outline" onClick={onRetry}>
312
+ {i18n.t("tools.connectRetry")}
313
+ </Button>
314
+ </Notice>
315
+ );
316
+ }
317
+
318
+ /**
319
+ * The catalog, under the portal's servers where there are any.
320
+ *
321
+ * ⚠️ No directory, no boxes — and the screen says why instead of inventing them. A portal that does
322
+ * not offer `portal_list_servers`, or whose answer Intel cannot read, makes `/tools/servers` fail,
323
+ * and every alternative to this sentence is a guess: a cut at an underscore, or the one box called
324
+ * "Company portal" that held all 92 tools of the running installation (#106). The tools are all
325
+ * here, in the order the portal answered them; only the grouping is missing.
326
+ */
327
+ function ToolCatalogView({
328
+ items,
329
+ servers,
330
+ i18n,
331
+ requested,
332
+ }: {
333
+ items: ToolCapability[];
334
+ servers: ToolServer[];
335
+ i18n: I18n;
336
+ requested: string | null;
337
+ }) {
338
+ if (servers.length === 0) {
339
+ return (
340
+ <>
341
+ <section className="rounded-xl border border-dashed bg-card px-5 py-4">
342
+ <h2 className="text-sm font-semibold">{i18n.t("tools.ungrouped")}</h2>
343
+ <p className="mt-1 text-sm text-muted-foreground">{i18n.t("tools.ungroupedHelp")}</p>
344
+ </section>
345
+ {/* Open, not a lid: with no server to name, a closed box would hide the whole catalog
346
+ behind a word that says nothing. */}
347
+ <section className="overflow-hidden rounded-xl border bg-card shadow-sm">
348
+ <ToolRows items={items} i18n={i18n} requested={requested} />
349
+ </section>
350
+ <p className="text-xs text-muted-foreground">{i18n.t("tools.liveNote")}</p>
351
+ </>
352
+ );
353
+ }
354
+ return (
355
+ <>
356
+ <div className="space-y-3">
357
+ {groupByServer(items, servers, i18n.t("tools.originUnlisted")).map((group) => (
358
+ <ServerGroup
359
+ key={group.handle ?? ""}
360
+ group={group}
361
+ note={group.handle === null ? i18n.t("tools.originUnlistedHelp") : null}
362
+ i18n={i18n}
363
+ requested={requested}
364
+ />
365
+ ))}
366
+ </div>
367
+ <p className="text-xs text-muted-foreground">{i18n.t("tools.liveNote")}</p>
368
+ </>
369
+ );
370
+ }
371
+
372
+ function Notice({
373
+ icon,
374
+ title,
375
+ help,
376
+ alert,
377
+ children,
378
+ }: {
379
+ icon: React.ReactNode;
380
+ title: string;
381
+ help: string;
382
+ alert?: boolean;
383
+ children?: React.ReactNode;
384
+ }) {
385
+ return (
386
+ <section
387
+ {...(alert ? { role: "alert" } : {})}
388
+ className="rounded-xl border border-dashed bg-card p-12 text-center"
389
+ >
390
+ {icon}
391
+ <h2 className="mt-4 font-semibold">{title}</h2>
392
+ <p className="mx-auto mt-2 max-w-lg text-sm text-muted-foreground">{help}</p>
393
+ {children ? <div className="mt-5">{children}</div> : null}
394
+ </section>
395
+ );
396
+ }
397
+
398
+ // One server, one box, closed until asked. Closed is the point: a hundred rows spread over a
399
+ // handful of servers is a list nobody reads, and which server a tool speaks to is the first thing
400
+ // somebody wants to know about it (#99).
401
+ function ServerGroup({
402
+ group,
403
+ note,
404
+ i18n,
405
+ requested,
406
+ }: {
407
+ group: ToolGroup;
408
+ // Why this box exists at all, for the one box that is not a server. A group named by the portal
409
+ // needs no explanation; the remainder does, or it reads as a server called something odd.
410
+ note: string | null;
411
+ i18n: I18n;
412
+ requested: string | null;
413
+ }) {
414
+ // A hit from the header search names a tool, not a server, and the box it sits in has to open
415
+ // with it — otherwise Enter on a hit lands on a screen where nothing happened.
416
+ const holdsRequested = group.items.some((item) => item.name === requested);
417
+ const destructive = group.items.filter((item) => item.annotations.destructiveHint).length;
418
+ // ⚠️ One key, not a `…One` beside a `…Many` (#605): the catalog carries a wording that fits every
419
+ // number, so nothing here branches on the count. The branch that used to stand here is what the
420
+ // rule in `packages/intel/ui/CLAUDE.md` forbids — it puts the inflection into the component and into
421
+ // every future language.
422
+ const count = i18n.t("tools.toolCount", { count: group.items.length });
423
+ // The badges beside the name read as one run-on word when a screen reader concatenates them
424
+ // ("wiki1 tool"), so the control says its own name instead of being read off its contents.
425
+ const spoken = [`${i18n.t("tools.origin")}: ${group.label}`, count];
426
+ if (destructive > 0) spoken.push(i18n.t("tools.destructiveCount", { count: destructive }));
427
+
428
+ return (
429
+ <Collapsible defaultOpen={holdsRequested} asChild>
430
+ <section className="overflow-hidden rounded-xl border bg-card shadow-sm">
431
+ <CollapsibleTrigger
432
+ aria-label={spoken.join(", ")}
433
+ className="group flex w-full items-center gap-3 px-5 py-4 text-left outline-none hover:bg-muted/50 focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring"
434
+ >
435
+ <ChevronRight
436
+ aria-hidden="true"
437
+ className="size-4 shrink-0 text-muted-foreground transition-transform group-data-[state=open]:rotate-90"
438
+ />
439
+ <span className="min-w-0 flex-1">
440
+ <span className="block truncate text-sm font-semibold">{group.label}</span>
441
+ </span>
442
+ {/* Closed, the box hides every destructive badge inside it. The count says so on the lid
443
+ rather than making somebody open each server to find out. */}
444
+ {destructive > 0 && (
445
+ <span className="inline-flex shrink-0 items-center gap-1.5 rounded-full bg-destructive/10 px-2 py-0.5 text-xs text-destructive">
446
+ <AlertTriangle aria-hidden="true" className="size-3.5" />
447
+ {i18n.t("tools.destructiveCount", { count: destructive })}
448
+ </span>
449
+ )}
450
+ <span className="shrink-0 text-xs text-muted-foreground">{count}</span>
451
+ </CollapsibleTrigger>
452
+ <CollapsibleContent className="border-t">
453
+ {note && (
454
+ <p className="border-b bg-muted/50 px-5 py-3 text-xs text-muted-foreground">{note}</p>
455
+ )}
456
+ <ToolRows items={group.items} i18n={i18n} requested={requested} />
457
+ </CollapsibleContent>
458
+ </section>
459
+ </Collapsible>
460
+ );
461
+ }
462
+
463
+ // The rows themselves, with or without a box around them: the ungrouped screen shows the same list
464
+ // without a lid, and duplicating it there is how the two would drift apart.
465
+ function ToolRows({
466
+ items,
467
+ i18n,
468
+ requested,
469
+ }: {
470
+ items: ToolCapability[];
471
+ i18n: I18n;
472
+ requested: string | null;
473
+ }) {
474
+ return (
475
+ <ul className="divide-y">
476
+ {items.map((capability) => (
477
+ <li key={capability.name}>
478
+ <ToolEntry capability={capability} i18n={i18n} open={capability.name === requested} />
479
+ </li>
480
+ ))}
481
+ </ul>
482
+ );
483
+ }
484
+
485
+ // One tool, one disclosure. The row is the only control in the list: it opens the schema and does
486
+ // nothing else — enabling, sharing or permitting a tool happens in the portal, never here.
487
+ function ToolEntry({
488
+ capability,
489
+ i18n,
490
+ open,
491
+ }: {
492
+ capability: ToolCapability;
493
+ i18n: I18n;
494
+ open: boolean;
495
+ }) {
496
+ return (
497
+ <Collapsible defaultOpen={open}>
498
+ <CollapsibleTrigger className="group flex w-full items-start gap-4 px-5 py-3 text-left outline-none hover:bg-muted/50 focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring">
499
+ <ChevronRight
500
+ aria-hidden="true"
501
+ className="mt-0.5 size-4 shrink-0 text-muted-foreground transition-transform group-data-[state=open]:rotate-90"
502
+ />
503
+ <span className="min-w-0 flex-1">
504
+ <span className="flex flex-wrap items-center gap-2">
505
+ <span className="text-sm font-medium">{capability.title ?? capability.name}</span>
506
+ {capability.annotations.destructiveHint && (
507
+ <span className="inline-flex items-center gap-1.5 rounded-full bg-destructive/10 px-2 py-0.5 text-xs text-destructive">
508
+ <AlertTriangle aria-hidden="true" className="size-3.5" />
509
+ {i18n.t("tools.destructiveShort")}
510
+ </span>
511
+ )}
512
+ </span>
513
+ {/* The namespaced name is what a flow references, so it stays readable — but only where
514
+ it is not already the heading of the row. */}
515
+ {capability.title && (
516
+ <span className="mt-1 block truncate font-mono text-xs text-muted-foreground">
517
+ {capability.name}
518
+ </span>
519
+ )}
520
+ </span>
521
+ </CollapsibleTrigger>
522
+ {/* ⚠️ The description belongs here, not in the closed row. Held open it was the whole reason
523
+ the list could not be scanned: a paragraph per tool, a hundred times over. */}
524
+ <CollapsibleContent className="space-y-3 px-5 pb-4 pl-13">
525
+ {capability.description && (
526
+ <p className="text-xs text-muted-foreground">{capability.description}</p>
527
+ )}
528
+ <Schema label={i18n.t("tools.inputSchema")} value={capability.inputSchema} />
529
+ {capability.outputSchema && (
530
+ <Schema label={i18n.t("tools.outputSchema")} value={capability.outputSchema} />
531
+ )}
532
+ </CollapsibleContent>
533
+ </Collapsible>
534
+ );
535
+ }
536
+
537
+ function Schema({ label, value }: { label: string; value: Record<string, unknown> }) {
538
+ return (
539
+ <div>
540
+ <p className="text-xs font-medium">{label}</p>
541
+ <pre className="mt-1 max-h-80 overflow-auto rounded-md bg-muted p-3 text-xs">
542
+ {JSON.stringify(value, null, 2)}
543
+ </pre>
544
+ </div>
545
+ );
546
+ }
@@ -0,0 +1,61 @@
1
+ import type { SessionUser } from "@contract/contract.ts";
2
+ import { useQuery } from "@tanstack/react-query";
3
+ import { useIntelRouterContext } from "@/router/router-context.ts";
4
+
5
+ /**
6
+ * The signed-in person, from the one query the shell already makes.
7
+ *
8
+ * ⚠️ The same query key the user footer uses, so asking here costs no second request — and the two
9
+ * can never disagree about who is signed in.
10
+ */
11
+ export function useSessionUser(): SessionUser | null {
12
+ const { data } = useIntelRouterContext();
13
+ const session = useQuery({ queryKey: ["session"], queryFn: () => data.getSession() });
14
+ return session.data ?? null;
15
+ }
16
+
17
+ /** What a person is CALLED, by the same rule the user footer follows: the name, or the address. */
18
+ export function displayNameOf(user: SessionUser): string {
19
+ return user.name ?? user.email;
20
+ }
21
+
22
+ /**
23
+ * The display name behind a user id, WITHOUT a request — or `null` where this browser cannot say
24
+ * from what it already holds.
25
+ *
26
+ * ⚠️ It answers for exactly one id, and that is a choice rather than a limit: the signed-in
27
+ * person's, out of the session the shell already fetched. Intel CAN name a foreign id — the bundled
28
+ * resolution over `directory_resolve` does it (`#700`) — but not from here. A hook that resolved a
29
+ * single id would be called once per row and once per circle, and one screen would open one request
30
+ * per person instead of one for all of them.
31
+ *
32
+ * ⚠️ So a caller that needs foreign names asks for them where the ids are already in one place, and
33
+ * layers the two: this hook first, the resolved map second. `useAssigneeLabel` is the worked
34
+ * example.
35
+ *
36
+ * ⚠️ Never fall back to the id. An id under a name is not an answer to "who made this", it is the
37
+ * question again in smaller type — and it would leak an identifier onto a screen that has no reason
38
+ * to carry one. Callers show `node.someUser` instead (#258).
39
+ */
40
+ export function useUserName(userId: string | null): string | null {
41
+ const me = useSessionUser();
42
+ if (userId === null || me === null || me.id !== userId) return null;
43
+ return displayNameOf(me);
44
+ }
45
+
46
+ /**
47
+ * The one or two letters that stand for a name in a circle.
48
+ *
49
+ * ⚠️ Lives here rather than beside the circle that first needed it: two surfaces now draw the same
50
+ * circle — the access summary and a task's assignee — and two copies of this would drift into two
51
+ * different abbreviations of the same person.
52
+ */
53
+ export function initials(value: string): string {
54
+ const parts = value
55
+ .replace(/@.*$/, "")
56
+ .split(/[\s._+-]+/)
57
+ .filter(Boolean);
58
+ if (parts.length === 0) return "?";
59
+ if (parts.length === 1) return (parts[0]?.slice(0, 2) ?? "?").toUpperCase();
60
+ return `${parts[0]?.[0] ?? ""}${parts.at(-1)?.[0] ?? ""}`.toUpperCase();
61
+ }
@@ -0,0 +1,68 @@
1
+ import { createReactBlockSpec } from "@blocknote/react";
2
+ import { createContext, useContext } from "react";
3
+
4
+ /**
5
+ * A piece of the document the editor cannot show, kept exactly as it was written (#826).
6
+ *
7
+ * ⚠️ **This is the frontmatter's way, generalised.** #657 gave the head its own path because
8
+ * `blocksToMarkdownLossy` had destroyed it: what the editor does not understand it rewrites into
9
+ * what it does, and `payload.markdown` is what the search reads. The head was the only construct
10
+ * with that protection; everything else the editor did not know still lost its shape on the first
11
+ * open-and-save, and nothing anywhere went red.
12
+ *
13
+ * ⚠️ **`raw` is the exact source text, never something parsed.** Whatever went in comes back out,
14
+ * byte for byte. A block that carried a parsed structure would turn every gap in the parser into
15
+ * data loss at the next save, which is the trap the head already avoids.
16
+ */
17
+ export const VerbatimBlockType = "verbatim";
18
+
19
+ const VERBATIM_PROPS = { raw: { default: "" } } as const;
20
+
21
+ /**
22
+ * The one word the block says. It travels in a context for the same reason the head's two do: a
23
+ * block is rendered by the editor, not by a view that could reach the catalogue.
24
+ */
25
+ const VerbatimContext = createContext<string>("Kept as written");
26
+
27
+ export const VerbatimProvider = VerbatimContext.Provider;
28
+
29
+ export function VerbatimBlock({ raw }: { raw: string }) {
30
+ const label = useContext(VerbatimContext);
31
+ return (
32
+ <div
33
+ // The editor owns the text around this; the block itself is not typed into. What is in here
34
+ // is the author's own text, and an editor that cannot draw it must not let anybody change it
35
+ // by accident either.
36
+ contentEditable={false}
37
+ data-verbatim=""
38
+ className="w-full overflow-x-auto rounded-md bg-muted/40 px-3 py-2"
39
+ >
40
+ {/* ⚠️ Named rather than silent, which is the whole difference to the state before: a piece
41
+ that vanishes without a word is what #826 is about. */}
42
+ <p className="mb-1 text-xs text-muted-foreground">{label}</p>
43
+ <pre className="whitespace-pre font-mono text-xs">
44
+ <code>{raw}</code>
45
+ </pre>
46
+ </div>
47
+ );
48
+ }
49
+
50
+ export const verbatimSpec = createReactBlockSpec(
51
+ {
52
+ type: VerbatimBlockType,
53
+ propSchema: VERBATIM_PROPS,
54
+ content: "none",
55
+ },
56
+ {
57
+ render: (props) => <VerbatimBlock raw={String(props.block.props.raw)} />,
58
+ // ⚠️ The net under `blocksToMarkdown`, for every path that does not go through it. Without a
59
+ // serialiser BlockNote falls back to the RENDERED DOM, and the label would end up in the
60
+ // document in place of the text. `<pre><code>` rather than a bare `<pre>`: measured in #657, a
61
+ // bare one survives the HTML export and is then dropped by the HTML-to-markdown step.
62
+ toExternalHTML: (props) => (
63
+ <pre>
64
+ <code>{String(props.block.props.raw)}</code>
65
+ </pre>
66
+ ),
67
+ },
68
+ );