@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,87 @@
1
+ import { Outlet } from "@tanstack/react-router";
2
+ import { useRef, useState } from "react";
3
+ import { AppSidebar } from "@/app/app-sidebar/app-sidebar.tsx";
4
+ import { HeaderSearch } from "@/app/header-search/header-search.tsx";
5
+ import { createSidebarPreferences } from "@/app/sidebar-preferences/sidebar-preferences.ts";
6
+ import { SidebarInset, SidebarProvider, SidebarTrigger } from "@/components/ui/sidebar";
7
+ import { useI18n } from "@/i18n/i18n-context.tsx";
8
+
9
+ // ⚠️ The breadcrumb is gone (#337). It was two levels deep and the first of them was constant —
10
+ // `Intel > Knowledge`, `Intel > Flows`, `Intel > Tools`. The product name stands in the sidebar
11
+ // anyway, the area is marked there as the active row, and what was left was a path that never grew
12
+ // past two links and carried no navigation that did not already exist. The `Separator` beside the
13
+ // sidebar trigger went with it: it separated the trigger from the breadcrumb, and there is nothing
14
+ // left on that side to separate it from.
15
+ export function App() {
16
+ const i18n = useI18n();
17
+
18
+ // Read once: the stored preference is the starting point, after which the shell's own state is
19
+ // the truth. Re-reading on every render would fight the drag.
20
+ const preferences = useRef(createSidebarPreferences()).current;
21
+ const stored = useRef(preferences.read()).current;
22
+ const [open, setOpen] = useState(stored.open);
23
+ const [width, setWidth] = useState(stored.width);
24
+
25
+ return (
26
+ // ⚠️ `h-svh` is what keeps the box inside the screen. The registry's wrapper only carries
27
+ // `min-h-svh`, so its height follows its content — and the inset, an `inset` variant with a
28
+ // margin all round, then stands 16px taller than the viewport at best and grows with a long
29
+ // document at worst. Both times the lower border and its two rounded corners end up under the
30
+ // fold and the whole page scrolls. With a definite height the inset stretches to
31
+ // `100svh - 16px`, and the overflow below belongs to the container around the outlet, which
32
+ // already carries `min-h-0 overflow-auto` for exactly that. Set here rather than in
33
+ // `components/ui/sidebar.tsx`: that is registry code and an edit in it is silently gone at the
34
+ // next sync.
35
+ <SidebarProvider
36
+ className="h-svh"
37
+ open={open}
38
+ onOpenChange={(next) => {
39
+ setOpen(next);
40
+ preferences.write({ open: next, width });
41
+ }}
42
+ style={{ "--sidebar-width": `${width}px` } as React.CSSProperties}
43
+ >
44
+ <AppSidebar
45
+ width={width}
46
+ onWidthChange={(next) => {
47
+ setWidth(next);
48
+ preferences.write({ open, width: next });
49
+ }}
50
+ />
51
+ {/* ⚠️ `min-w-0` is not cosmetic: the inset is a flex item beside the sidebar and carries
52
+ `min-width: auto`, so it never shrinks below its content's min-content width. One wide
53
+ child (a flow canvas, a table of raw JSON) would widen the page instead of scrolling
54
+ inside its own container. Learned in gate, GATE-76 D. */}
55
+ {/* `overflow-hidden` is the second half of the rounding: the box is rounded but nothing
56
+ clipped its children, so an opaque panel inside it — the flow editor's inspector carries
57
+ `bg-card` — painted its own square corner over the rounded one. Invisible in the light
58
+ theme, where card and background are the same white, and plain to see in the dark one.
59
+ Scrolling is unaffected: the container around the outlet below owns it. */}
60
+ <SidebarInset className="min-w-0 overflow-hidden md:border">
61
+ <header className="flex h-14 shrink-0 items-center gap-2 border-b px-4">
62
+ <SidebarTrigger className="-ml-1" aria-label={i18n.t("shell.toggleSidebar")} />
63
+ {/* The bar for area-owned actions. A screen renders into the inner box through
64
+ `ActionSlot`; search is the shell's own, because it has to open from every screen
65
+ alike and belongs to none of them. It stays last, against the right edge, and a
66
+ screen's actions line up to its left: the search is the one thing in this bar that is
67
+ on every screen, so it is the one whose place must not depend on what a screen happens
68
+ to bring. Pinned the other way round it was the constant that moved (#56).
69
+
70
+ ⚠️ Two boxes rather than one list, and that is the whole of the mechanism: a portal
71
+ only ever appends to its container, so with the search a sibling of the portalled
72
+ actions a screen that mounts them later — Flows does, once a flow is selected — would
73
+ land to its right. Giving the screens a container of their own takes mount order out
74
+ of the question entirely. `data-slot="header-actions"` therefore names the inner box:
75
+ it is the name every `ActionSlot` looks up. */}
76
+ <div data-slot="header-bar" className="ml-auto flex items-center gap-2">
77
+ <div data-slot="header-actions" className="flex items-center gap-2" />
78
+ <HeaderSearch />
79
+ </div>
80
+ </header>
81
+ <div className="flex min-h-0 flex-1 flex-col overflow-auto">
82
+ <Outlet />
83
+ </div>
84
+ </SidebarInset>
85
+ </SidebarProvider>
86
+ );
87
+ }
@@ -0,0 +1,294 @@
1
+ import type { Flow } from "@contract/flow.ts";
2
+ import type { ToolCapability } from "@contract/tool.ts";
3
+ import { useQuery } from "@tanstack/react-query";
4
+ import { useNavigate } from "@tanstack/react-router";
5
+ import { FileText, Search, Workflow, Wrench } from "lucide-react";
6
+ import { useEffect, useRef, useState } from "react";
7
+ import {
8
+ Command,
9
+ CommandEmpty,
10
+ CommandGroup,
11
+ CommandInput,
12
+ CommandItem,
13
+ CommandList,
14
+ } from "@/components/ui/command";
15
+ import {
16
+ Dialog,
17
+ DialogContent,
18
+ DialogDescription,
19
+ DialogHeader,
20
+ DialogTitle,
21
+ } from "@/components/ui/dialog";
22
+ import { useI18n } from "@/i18n/i18n-context.tsx";
23
+ import { useIntelRouterContext } from "@/router/router-context.ts";
24
+
25
+ const Kinds = ["node", "flow", "tool"] as const;
26
+ type Kind = (typeof Kinds)[number];
27
+
28
+ // Enough to answer "where was that again" without turning the dialog into a result page.
29
+ const ResultLimit = 8;
30
+
31
+ const areaFor = { node: "/nodes", flow: "/flows", tool: "/tools" } as const;
32
+
33
+ function isMac(): boolean {
34
+ return typeof navigator !== "undefined" && /mac/i.test(navigator.userAgent);
35
+ }
36
+
37
+ // ⚠️ The deliberate boundary of this ticket, named rather than hidden: Intel has a search endpoint
38
+ // for nodes only, so node hits come from `searchNodes()` and the server decides both
39
+ // what matches and what may be seen. Flows and Tools have no such endpoint — they are the same
40
+ // authorized lists the screens render, narrowed here in the client.
41
+ //
42
+ // That is safe in the one direction that matters: `listFlows()` returns what the caller may see
43
+ // (the flow repository's `listVisible`), and `listTools()` is one live `tools/list` with the user's
44
+ // own portal token. The client narrows a permitted set; it can never widen one, and no permission
45
+ // decision is taken here.
46
+ //
47
+ // When either list outgrows what a browser should hold, that is a new ticket for a search endpoint
48
+ // — not a silent regression of this one.
49
+ function matches(query: string, ...fields: (string | null)[]): boolean {
50
+ const needle = query.toLocaleLowerCase();
51
+ return fields.some((field) => field?.toLocaleLowerCase().includes(needle));
52
+ }
53
+
54
+ export function HeaderSearch() {
55
+ const { data } = useIntelRouterContext();
56
+ const i18n = useI18n();
57
+ const navigate = useNavigate();
58
+ const [open, setOpen] = useState(false);
59
+ const [text, setText] = useState("");
60
+ const [kind, setKind] = useState<Kind | null>(null);
61
+ const query = text.trim();
62
+ const restoreTo = useRef<HTMLElement | null>(null);
63
+
64
+ function change(next: boolean) {
65
+ if (next) restoreTo.current = document.activeElement as HTMLElement | null;
66
+ setOpen(next);
67
+ }
68
+
69
+ // The same combination opens and closes the dialog. Radix does not swallow the key while it is
70
+ // open, so one toggling listener on the window covers both directions from every screen.
71
+ useEffect(() => {
72
+ function onKeyDown(event: KeyboardEvent) {
73
+ if (event.key.toLowerCase() !== "k" || !(event.metaKey || event.ctrlKey) || event.altKey) {
74
+ return;
75
+ }
76
+ event.preventDefault();
77
+ if (!open) restoreTo.current = document.activeElement as HTMLElement | null;
78
+ setOpen(!open);
79
+ }
80
+ window.addEventListener("keydown", onKeyDown);
81
+ return () => window.removeEventListener("keydown", onKeyDown);
82
+ }, [open]);
83
+
84
+ const shows = (candidate: Kind) => kind === null || kind === candidate;
85
+ // A filter that excludes a kind does not ask for it either: no request, and therefore no group
86
+ // that could show up empty.
87
+ const nodes = useQuery({
88
+ queryKey: ["node-search", query],
89
+ queryFn: () => data.searchNodes({ query, limit: ResultLimit }),
90
+ enabled: open && shows("node") && query.length > 0,
91
+ });
92
+ // ⚠️ Deliberately the whole list, not one folder of the tree: a search that only saw the level
93
+ // someone happened to have open would miss the flow they are looking for. The sidebar tree asks
94
+ // per folder and keeps its own keys; only the Tools key is shared with the screen that fills it.
95
+ const flows = useQuery({
96
+ queryKey: ["flows"],
97
+ queryFn: () => data.listFlows(),
98
+ enabled: open && shows("flow") && query.length > 0,
99
+ });
100
+ const tools = useQuery({
101
+ queryKey: ["tools"],
102
+ queryFn: () => data.listTools(),
103
+ enabled: open && shows("tool") && query.length > 0,
104
+ });
105
+
106
+ const active = [
107
+ ...(shows("node") ? [nodes] : []),
108
+ ...(shows("flow") ? [flows] : []),
109
+ ...(shows("tool") ? [tools] : []),
110
+ ];
111
+ const searching = query.length > 0 && active.some((source) => source.isPending);
112
+ const failed = query.length > 0 && active.some((source) => source.isError);
113
+
114
+ const nodeHits = shows("node") ? (nodes.data?.items ?? []) : [];
115
+ const flowHits: Flow[] = shows("flow")
116
+ ? (flows.data?.items ?? [])
117
+ .filter((flow) => matches(query, flow.title, flow.description))
118
+ .slice(0, ResultLimit)
119
+ : [];
120
+ const toolHits: ToolCapability[] = shows("tool")
121
+ ? (tools.data?.items ?? [])
122
+ .filter((tool) => matches(query, tool.name, tool.title, tool.description))
123
+ .slice(0, ResultLimit)
124
+ : [];
125
+ const total = nodeHits.length + flowHits.length + toolHits.length;
126
+
127
+ function reach(hit: Kind, select: string) {
128
+ setOpen(false);
129
+ void navigate({ to: areaFor[hit], search: { select } });
130
+ }
131
+
132
+ return (
133
+ <>
134
+ <button
135
+ type="button"
136
+ onClick={() => change(true)}
137
+ aria-label={i18n.t("search.open")}
138
+ aria-keyshortcuts="Meta+K Control+K"
139
+ className="flex h-8 items-center gap-2 rounded-md border bg-background px-2.5 text-sm text-muted-foreground outline-none hover:bg-accent hover:text-accent-foreground focus-visible:ring-2 focus-visible:ring-ring sm:w-56"
140
+ >
141
+ <Search aria-hidden="true" className="size-4 shrink-0" />
142
+ <span className="truncate">{i18n.t("search.open")}</span>
143
+ {/* The hint repeats what `aria-keyshortcuts` already announces, so the accessible name
144
+ stays the label above rather than "Search ⌘K". */}
145
+ <kbd className="ml-auto hidden rounded border bg-muted px-1.5 font-sans text-[10px] sm:inline">
146
+ {isMac() ? "⌘K" : "Ctrl K"}
147
+ </kbd>
148
+ </button>
149
+
150
+ {/* ⚠️ Composed from `Dialog` and `Command` rather than from the registry's `CommandDialog`
151
+ for two reasons that both matter here: `CommandDialog` forwards its extra props to the
152
+ dialog root, so `shouldFilter` would never reach cmdk, and it renders its title outside
153
+ `DialogContent`, where it does not name the dialog for a screen reader. */}
154
+ <Dialog open={open} onOpenChange={change}>
155
+ <DialogContent
156
+ className="overflow-hidden p-0"
157
+ // ⚠️ Radix hands focus back to the dialog's trigger. This dialog is usually opened by a
158
+ // shortcut and has no trigger at all, so without this the caret would land on the body
159
+ // and a keyboard user would start over at the top of the page.
160
+ onCloseAutoFocus={(event) => {
161
+ event.preventDefault();
162
+ restoreTo.current?.focus();
163
+ }}
164
+ >
165
+ <DialogHeader className="sr-only">
166
+ <DialogTitle>{i18n.t("search.title")}</DialogTitle>
167
+ <DialogDescription>{i18n.t("search.description")}</DialogDescription>
168
+ </DialogHeader>
169
+ {/* Every source has already decided what matches — the server for nodes, the client
170
+ filter above for the two lists. Letting cmdk filter again would hide rows the server
171
+ returned, because a row's value is its identity rather than its text. */}
172
+ <Command shouldFilter={false}>
173
+ <CommandInput
174
+ value={text}
175
+ onValueChange={setText}
176
+ placeholder={i18n.t("search.placeholder")}
177
+ />
178
+ <fieldset className="flex flex-wrap gap-1 border-b px-3 py-2">
179
+ <legend className="sr-only">{i18n.t("search.filter")}</legend>
180
+ {[null, ...Kinds].map((candidate) => (
181
+ <button
182
+ key={candidate ?? "all"}
183
+ type="button"
184
+ aria-pressed={kind === candidate}
185
+ onClick={() => setKind(candidate)}
186
+ // ⚠️ cmdk turns Enter into "open the highlighted result" on the command root,
187
+ // without looking at where the key came from. Unstopped, Enter on a filter would
188
+ // leave the dialog for a hit instead of narrowing the list.
189
+ onKeyDown={(event) => {
190
+ if (event.key === "Enter" || event.key === " ") event.stopPropagation();
191
+ }}
192
+ className="rounded-full border px-2.5 py-1 text-xs outline-none aria-pressed:bg-primary aria-pressed:text-primary-foreground focus-visible:ring-2 focus-visible:ring-ring"
193
+ >
194
+ {i18n.t(candidate === null ? "search.all" : `search.kind.${candidate}`)}
195
+ </button>
196
+ ))}
197
+ </fieldset>
198
+ <CommandList>
199
+ {query.length === 0 ? (
200
+ <p className="py-6 text-center text-sm text-muted-foreground">
201
+ {i18n.t("search.hint")}
202
+ </p>
203
+ ) : null}
204
+ {/* Three states that never read as one another: still asking, could not ask, and asked
205
+ with nothing to show. A failing source is an alert even when another one answered. */}
206
+ {failed ? (
207
+ <div role="alert" className="space-y-3 px-3 py-6 text-center text-sm">
208
+ <p className="text-destructive">{i18n.t("search.failed")}</p>
209
+ <button
210
+ type="button"
211
+ onClick={() => {
212
+ for (const source of active) if (source.isError) void source.refetch();
213
+ }}
214
+ className="rounded-md border px-2.5 py-1 text-xs outline-none hover:bg-accent focus-visible:ring-2 focus-visible:ring-ring"
215
+ >
216
+ {i18n.t("common.retry")}
217
+ </button>
218
+ </div>
219
+ ) : null}
220
+ {searching ? (
221
+ <p role="status" className="py-6 text-center text-sm text-muted-foreground">
222
+ {i18n.t("search.searching")}
223
+ </p>
224
+ ) : null}
225
+ {!searching && !failed && query.length > 0 && total === 0 ? (
226
+ <CommandEmpty>{i18n.t("search.empty", { query })}</CommandEmpty>
227
+ ) : null}
228
+
229
+ {nodeHits.length > 0 ? (
230
+ <CommandGroup heading={i18n.t("search.kind.node")}>
231
+ {nodeHits.map((citation) => (
232
+ <CommandItem
233
+ key={`${citation.nodeId}:${citation.versionId}`}
234
+ value={`node:${citation.nodeId}:${citation.versionId}`}
235
+ onSelect={() => reach("node", citation.nodeId)}
236
+ >
237
+ <FileText aria-hidden="true" />
238
+ <span className="min-w-0 flex-1">
239
+ <span className="block truncate">{citation.title}</span>
240
+ <span className="block truncate text-xs text-muted-foreground">
241
+ {citation.passage}
242
+ </span>
243
+ </span>
244
+ </CommandItem>
245
+ ))}
246
+ </CommandGroup>
247
+ ) : null}
248
+ {flowHits.length > 0 ? (
249
+ <CommandGroup heading={i18n.t("search.kind.flow")}>
250
+ {flowHits.map((flow) => (
251
+ <CommandItem
252
+ key={flow.id}
253
+ value={`flow:${flow.id}`}
254
+ onSelect={() => reach("flow", flow.id)}
255
+ >
256
+ <Workflow aria-hidden="true" />
257
+ <span className="min-w-0 flex-1">
258
+ <span className="block truncate">{flow.title}</span>
259
+ {flow.description ? (
260
+ <span className="block truncate text-xs text-muted-foreground">
261
+ {flow.description}
262
+ </span>
263
+ ) : null}
264
+ </span>
265
+ </CommandItem>
266
+ ))}
267
+ </CommandGroup>
268
+ ) : null}
269
+ {toolHits.length > 0 ? (
270
+ <CommandGroup heading={i18n.t("search.kind.tool")}>
271
+ {toolHits.map((tool) => (
272
+ <CommandItem
273
+ key={tool.name}
274
+ value={`tool:${tool.name}`}
275
+ onSelect={() => reach("tool", tool.name)}
276
+ >
277
+ <Wrench aria-hidden="true" />
278
+ <span className="min-w-0 flex-1">
279
+ <span className="block truncate">{tool.title ?? tool.name}</span>
280
+ <span className="block truncate font-mono text-xs text-muted-foreground">
281
+ {tool.name}
282
+ </span>
283
+ </span>
284
+ </CommandItem>
285
+ ))}
286
+ </CommandGroup>
287
+ ) : null}
288
+ </CommandList>
289
+ </Command>
290
+ </DialogContent>
291
+ </Dialog>
292
+ </>
293
+ );
294
+ }
@@ -0,0 +1,79 @@
1
+ import { useMutation } from "@tanstack/react-query";
2
+ import { Button } from "@/components/ui/button";
3
+ import {
4
+ Dialog,
5
+ DialogContent,
6
+ DialogDescription,
7
+ DialogFooter,
8
+ DialogHeader,
9
+ DialogTitle,
10
+ } from "@/components/ui/dialog";
11
+ import { useI18n } from "@/i18n/i18n-context.tsx";
12
+ import { useIntelRouterContext } from "@/router/router-context.ts";
13
+
14
+ /**
15
+ * Rebuilding the search index, asked for before it happens (#416).
16
+ *
17
+ * ⚠️ Two clicks, not one, and the reason is not that it is dangerous — nothing is lost, the index is
18
+ * derived from D1 and R2 and can always be built again. It is that it is EXPENSIVE and invisible:
19
+ * every node is read and embedded again, which costs money and minutes, and the screen looks exactly
20
+ * the same afterwards. A one-click button next to "Preferences" invites a second press when the
21
+ * first one seems to have done nothing.
22
+ *
23
+ * ⚠️ It reports what was QUEUED, never "done". The answer comes back the moment the work is handed
24
+ * to the queue, and the index catches up behind it — writing "finished" here would be a sentence
25
+ * about something this screen cannot see. The number is the honest part: it says how much was
26
+ * accepted, so a repeat press can be compared against it.
27
+ *
28
+ * The dialog is opened from outside, like the settings one: its trigger is a `DropdownMenuItem`, and
29
+ * Radix unmounts the menu content in the same frame the item is chosen.
30
+ */
31
+ interface ReindexDialogProps {
32
+ open: boolean;
33
+ onOpenChange(open: boolean): void;
34
+ }
35
+
36
+ export function ReindexDialog({ open, onOpenChange }: ReindexDialogProps) {
37
+ const i18n = useI18n();
38
+ const { data } = useIntelRouterContext();
39
+ const reindex = useMutation({ mutationFn: () => data.reindexNodes() });
40
+
41
+ return (
42
+ <Dialog
43
+ open={open}
44
+ onOpenChange={(next) => {
45
+ // Closing forgets the last answer, so re-opening never shows a count from an earlier run as
46
+ // if it were this one's.
47
+ if (!next) reindex.reset();
48
+ onOpenChange(next);
49
+ }}
50
+ >
51
+ <DialogContent className="sm:max-w-md">
52
+ <DialogHeader>
53
+ <DialogTitle>{i18n.t("reindex.title")}</DialogTitle>
54
+ <DialogDescription>{i18n.t("reindex.description")}</DialogDescription>
55
+ </DialogHeader>
56
+ {reindex.isSuccess ? (
57
+ <p role="status" className="text-sm">
58
+ {i18n.t("reindex.queued", { count: reindex.data.queued })}
59
+ </p>
60
+ ) : null}
61
+ {reindex.isError ? (
62
+ <p role="alert" className="text-sm text-destructive">
63
+ {i18n.t("reindex.failed")}
64
+ </p>
65
+ ) : null}
66
+ <DialogFooter>
67
+ <Button variant="outline" onClick={() => onOpenChange(false)}>
68
+ {i18n.t(reindex.isSuccess ? "reindex.close" : "reindex.cancel")}
69
+ </Button>
70
+ {reindex.isSuccess ? null : (
71
+ <Button disabled={reindex.isPending} onClick={() => reindex.mutate()}>
72
+ {i18n.t(reindex.isPending ? "reindex.running" : "reindex.confirm")}
73
+ </Button>
74
+ )}
75
+ </DialogFooter>
76
+ </DialogContent>
77
+ </Dialog>
78
+ );
79
+ }
@@ -0,0 +1,135 @@
1
+ import {
2
+ Dialog,
3
+ DialogContent,
4
+ DialogDescription,
5
+ DialogHeader,
6
+ DialogTitle,
7
+ } from "@/components/ui/dialog";
8
+ import {
9
+ Select,
10
+ SelectContent,
11
+ SelectItem,
12
+ SelectTrigger,
13
+ SelectValue,
14
+ } from "@/components/ui/select";
15
+ import { useI18n, useLanguage } from "@/i18n/i18n-context.tsx";
16
+ import { languageName } from "@/i18n/i18n-languages/i18n-languages.ts";
17
+ import { type ThemeChoice, ThemeChoices } from "@/theme/theme.ts";
18
+ import { useTheme } from "@/theme/theme-context.tsx";
19
+ import { DeviceZone, deviceZone, ZoneOptions } from "@/time/time.ts";
20
+ import { useTimeZone } from "@/time/time-context.tsx";
21
+
22
+ // The settings that belong to the person rather than to the installation: language and appearance.
23
+ // Both are client state — they live in localStorage, not in the Intel HTTP surface — so they do not
24
+ // belong on a screen with a save button. A choice applies at once; a dialog that previews and only
25
+ // commits on "Save" would carry two states too many for two dropdowns.
26
+ //
27
+ // ⚠️ The third one, the reader's timezone, is BACK — and the sentence that sent it away is the
28
+ // reason it could come back (#467). It left with the Agents (#388) because it was a suggestion for
29
+ // a schedule and the lens its calendar was read through (#228); with both gone nothing read the
30
+ // value, "and a preference that changes nothing on screen is worse than none — it invites the
31
+ // reader to set it and then contradicts them. Reading times in a chosen zone is a feature, and it
32
+ // would come back as one."
33
+ //
34
+ // It came back as one, and in that order: FIRST every time on screen reads `useDateTime()`, THEN
35
+ // this dropdown exists. Whoever adds a surface that shows a time reads that hook — a second
36
+ // `toLocaleString` would be the old state again, only harder to find.
37
+ //
38
+ // The dialog is opened from outside (the user footer holds the state), because its trigger is a
39
+ // DropdownMenuItem: Radix unmounts the menu content when an item is chosen and would take a dialog
40
+ // nested inside it along in the same frame.
41
+ interface SettingsDialogProps {
42
+ open: boolean;
43
+ onOpenChange(open: boolean): void;
44
+ }
45
+
46
+ export function SettingsDialog({ open, onOpenChange }: SettingsDialogProps) {
47
+ const i18n = useI18n();
48
+ const language = useLanguage();
49
+ const theme = useTheme();
50
+ const zone = useTimeZone();
51
+
52
+ return (
53
+ <Dialog open={open} onOpenChange={onOpenChange}>
54
+ <DialogContent className="sm:max-w-md">
55
+ <DialogHeader>
56
+ <DialogTitle>{i18n.t("settings.title")}</DialogTitle>
57
+ <DialogDescription>{i18n.t("settings.description")}</DialogDescription>
58
+ </DialogHeader>
59
+ <div className="grid gap-6">
60
+ {/* Without a provider there is nothing to choose — then the block is absent rather than a
61
+ dead dropdown. The number of languages is deliberately NOT checked: there are always
62
+ at least the three built-in ones, so a condition on it would be dead code. */}
63
+ {language ? (
64
+ <div className="grid gap-2">
65
+ <label className="text-sm font-medium" htmlFor="settings-language">
66
+ {i18n.t("settings.language")}
67
+ </label>
68
+ <Select value={language.current} onValueChange={language.select}>
69
+ <SelectTrigger id="settings-language" className="w-full">
70
+ <SelectValue />
71
+ </SelectTrigger>
72
+ <SelectContent>
73
+ {language.codes.map((code) => (
74
+ <SelectItem key={code} value={code}>
75
+ {languageName(code)}
76
+ </SelectItem>
77
+ ))}
78
+ </SelectContent>
79
+ </Select>
80
+ </div>
81
+ ) : null}
82
+ {theme ? (
83
+ <div className="grid gap-2">
84
+ <label className="text-sm font-medium" htmlFor="settings-appearance">
85
+ {i18n.t("settings.appearance")}
86
+ </label>
87
+ {/* Radix hands out a raw string; the values come from the same list that fills the
88
+ menu, so the assertion here is the whole check. */}
89
+ <Select
90
+ value={theme.current}
91
+ onValueChange={(value) => theme.select(value as ThemeChoice)}
92
+ >
93
+ <SelectTrigger id="settings-appearance" className="w-full">
94
+ <SelectValue />
95
+ </SelectTrigger>
96
+ <SelectContent>
97
+ {ThemeChoices.map((choice) => (
98
+ <SelectItem key={choice} value={choice}>
99
+ {i18n.t(`settings.appearance.${choice}`)}
100
+ </SelectItem>
101
+ ))}
102
+ </SelectContent>
103
+ </Select>
104
+ </div>
105
+ ) : null}
106
+ {zone ? (
107
+ <div className="grid gap-2">
108
+ <label className="text-sm font-medium" htmlFor="settings-timezone">
109
+ {i18n.t("settings.timezone")}
110
+ </label>
111
+ <Select value={zone.current} onValueChange={zone.select}>
112
+ <SelectTrigger id="settings-timezone" className="w-full">
113
+ <SelectValue />
114
+ </SelectTrigger>
115
+ <SelectContent>
116
+ {/* The default is the device's zone and is NAMED as such — not an empty field
117
+ that would look as if nothing were set. */}
118
+ <SelectItem value={DeviceZone}>
119
+ {i18n.t("settings.timezone.device", { zone: deviceZone() })}
120
+ </SelectItem>
121
+ {ZoneOptions.map((option) => (
122
+ <SelectItem key={option} value={option}>
123
+ {option}
124
+ </SelectItem>
125
+ ))}
126
+ </SelectContent>
127
+ </Select>
128
+ <p className="text-xs text-muted-foreground">{i18n.t("settings.timezone.hint")}</p>
129
+ </div>
130
+ ) : null}
131
+ </div>
132
+ </DialogContent>
133
+ </Dialog>
134
+ );
135
+ }
@@ -0,0 +1,68 @@
1
+ import type {
2
+ SidebarPreferenceDeps,
3
+ SidebarPreferenceStore,
4
+ SidebarPreferences,
5
+ } from "./sidebar-preferences.types.ts";
6
+
7
+ // Below the minimum the tree is unreadable and the sidebar cannot be dragged back open by eye;
8
+ // above the maximum the content box stops being the larger half. Both are the reason the drag
9
+ // exists at all, so neither is configurable.
10
+ export const MinSidebarWidth = 180;
11
+ export const MaxSidebarWidth = 520;
12
+ export const DefaultSidebarWidth = 256;
13
+
14
+ const storageKey = "intel.sidebar";
15
+ const defaults: SidebarPreferences = { open: true, width: DefaultSidebarWidth };
16
+
17
+ export function clampSidebarWidth(width: number): number {
18
+ if (!Number.isFinite(width)) return DefaultSidebarWidth;
19
+ return Math.min(MaxSidebarWidth, Math.max(MinSidebarWidth, Math.round(width)));
20
+ }
21
+
22
+ function browserStorage(): Pick<Storage, "getItem" | "setItem"> | null {
23
+ // Reading `localStorage` throws outright in a few privacy modes, so the guard is a try, not a
24
+ // typeof check. Losing the preference is acceptable; losing the shell is not.
25
+ try {
26
+ return typeof window === "undefined" ? null : window.localStorage;
27
+ } catch {
28
+ return null;
29
+ }
30
+ }
31
+
32
+ // The shell's own client state: not server data, so it stays out of TanStack Query, but it has to
33
+ // survive a page change — which in a SPA includes a full reload — so it is written through.
34
+ export function createSidebarPreferences(deps: SidebarPreferenceDeps = {}): SidebarPreferenceStore {
35
+ const storage = deps.storage === undefined ? browserStorage() : deps.storage;
36
+
37
+ return {
38
+ read() {
39
+ let raw: string | null = null;
40
+ try {
41
+ raw = storage?.getItem(storageKey) ?? null;
42
+ } catch {
43
+ return defaults;
44
+ }
45
+ if (raw === null) return defaults;
46
+ let parsed: unknown;
47
+ try {
48
+ parsed = JSON.parse(raw);
49
+ } catch {
50
+ return defaults;
51
+ }
52
+ if (typeof parsed !== "object" || parsed === null) return defaults;
53
+ const value = parsed as Partial<SidebarPreferences>;
54
+ return {
55
+ open: typeof value.open === "boolean" ? value.open : defaults.open,
56
+ width: clampSidebarWidth(typeof value.width === "number" ? value.width : defaults.width),
57
+ };
58
+ },
59
+ write(preferences) {
60
+ try {
61
+ storage?.setItem(
62
+ storageKey,
63
+ JSON.stringify({ open: preferences.open, width: clampSidebarWidth(preferences.width) }),
64
+ );
65
+ } catch {}
66
+ },
67
+ };
68
+ }