@interactive-inc/claude-funnel 0.4.1 → 0.8.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 (349) hide show
  1. package/README.md +233 -111
  2. package/dist/bin.js +1417 -0
  3. package/dist/gateway/daemon.js +513 -0
  4. package/dist/highlights-eq9cgrbb.scm +604 -0
  5. package/dist/highlights-ghv9g403.scm +205 -0
  6. package/dist/highlights-hk7bwhj4.scm +284 -0
  7. package/dist/highlights-r812a2qc.scm +150 -0
  8. package/dist/highlights-x6tmsnaa.scm +115 -0
  9. package/dist/injections-73j83es3.scm +27 -0
  10. package/dist/tree-sitter-javascript-nd0q4pe9.wasm +0 -0
  11. package/dist/tree-sitter-markdown-411r6y9b.wasm +0 -0
  12. package/dist/tree-sitter-markdown_inline-j5349f42.wasm +0 -0
  13. package/dist/tree-sitter-typescript-zxjzwt75.wasm +0 -0
  14. package/dist/tree-sitter-zig-e78zbjpm.wasm +0 -0
  15. package/lib/bin.ts +78 -0
  16. package/lib/{modules → cli}/router/to-request.ts +13 -20
  17. package/lib/cli/routes/channels.$channel.connectors.$connector.rename.$newName.ts +27 -0
  18. package/lib/cli/routes/channels.$channel.connectors.$connector.request.ts +40 -0
  19. package/lib/cli/routes/channels.$channel.connectors.$connector.schedules.add.$id.ts +41 -0
  20. package/lib/cli/routes/channels.$channel.connectors.$connector.schedules.remove.$id.ts +22 -0
  21. package/lib/cli/routes/channels.$channel.connectors.$connector.schedules.ts +23 -0
  22. package/lib/cli/routes/channels.$channel.connectors.$connector.ts +26 -0
  23. package/lib/cli/routes/channels.$channel.connectors.add.$connector.ts +92 -0
  24. package/lib/cli/routes/channels.$channel.connectors.remove.$connector.ts +22 -0
  25. package/lib/cli/routes/channels.$channel.connectors.set.$connector.ts +63 -0
  26. package/lib/cli/routes/channels.$channel.connectors.ts +26 -0
  27. package/lib/cli/routes/channels.$channel.rename.$newName.ts +22 -0
  28. package/lib/cli/routes/channels.$channel.set.delivery.$mode.ts +34 -0
  29. package/lib/cli/routes/channels.$channel.ts +34 -0
  30. package/lib/cli/routes/channels.add.$channel.ts +33 -0
  31. package/lib/cli/routes/channels.remove.$channel.ts +20 -0
  32. package/lib/cli/routes/channels.ts +39 -0
  33. package/lib/cli/routes/claude.ts +69 -0
  34. package/lib/cli/routes/gateway.listeners.ts +41 -0
  35. package/lib/cli/routes/gateway.logs.ts +123 -0
  36. package/lib/{routes/gateway/restart.ts → cli/routes/gateway.restart.ts} +20 -5
  37. package/lib/cli/routes/gateway.run.ts +41 -0
  38. package/lib/cli/routes/gateway.start.ts +50 -0
  39. package/lib/cli/routes/gateway.status.ts +19 -0
  40. package/lib/cli/routes/gateway.stop.ts +32 -0
  41. package/lib/cli/routes/gateway.ts +55 -0
  42. package/lib/cli/routes/index.ts +202 -0
  43. package/lib/cli/routes/profiles.$profile.as-default.ts +22 -0
  44. package/lib/cli/routes/profiles.$profile.rename.$newName.ts +22 -0
  45. package/lib/cli/routes/profiles.$profile.run.ts +36 -0
  46. package/lib/cli/routes/profiles.add.$profile.ts +46 -0
  47. package/lib/cli/routes/profiles.remove.$profile.ts +20 -0
  48. package/lib/cli/routes/profiles.set.$profile.ts +46 -0
  49. package/lib/cli/routes/profiles.ts +40 -0
  50. package/lib/cli/routes/status.ts +93 -0
  51. package/lib/cli/routes/update.ts +27 -0
  52. package/lib/connectors/connector-config-schema.ts +16 -0
  53. package/lib/connectors/connector-factory.ts +94 -0
  54. package/lib/connectors/connector-listener.ts +20 -0
  55. package/lib/{modules/connectors/funnel-discord-adapter.ts → connectors/discord-adapter.ts} +6 -11
  56. package/lib/{modules/connectors → connectors}/discord-connector-schema.ts +4 -1
  57. package/lib/connectors/discord-listener.ts +111 -0
  58. package/lib/{modules/connectors/funnel-gh-adapter.ts → connectors/gh-adapter.ts} +3 -6
  59. package/lib/{modules/connectors → connectors}/gh-connector-schema.ts +4 -1
  60. package/lib/{modules/connectors/funnel-gh-listener.ts → connectors/gh-listener.ts} +57 -22
  61. package/lib/{modules/connectors → connectors}/match-cron.ts +10 -4
  62. package/lib/connectors/schedule-connector-schema.ts +33 -0
  63. package/lib/connectors/schedule-listener.ts +207 -0
  64. package/lib/connectors/schedule-state-store.ts +54 -0
  65. package/lib/connectors/slack-adapter.ts +36 -0
  66. package/lib/{modules/connectors → connectors}/slack-connector-schema.ts +4 -1
  67. package/lib/{modules/connectors/funnel-slack-event-processor.ts → connectors/slack-event-processor.ts} +15 -9
  68. package/lib/{modules/connectors/funnel-slack-listener.ts → connectors/slack-listener.ts} +39 -14
  69. package/lib/engine/channels/channels.ts +520 -0
  70. package/lib/{modules/claude/funnel-claude.ts → engine/claude/claude.ts} +47 -62
  71. package/lib/engine/claude/gateway-controller.ts +4 -0
  72. package/lib/{modules/fs/funnel-file-system.ts → engine/fs/file-system.ts} +9 -0
  73. package/lib/{modules/fs/memory-funnel-file-system.ts → engine/fs/memory-file-system.ts} +20 -3
  74. package/lib/{modules/fs/node-funnel-file-system.ts → engine/fs/node-file-system.ts} +14 -2
  75. package/lib/{modules/http/memory-funnel-http-client.ts → engine/http/memory-http-client.ts} +1 -5
  76. package/lib/{modules/http/node-funnel-http-client.ts → engine/http/node-http-client.ts} +1 -5
  77. package/lib/engine/id/id-generator.ts +7 -0
  78. package/lib/engine/id/memory-id-generator.ts +20 -0
  79. package/lib/engine/id/node-id-generator.ts +7 -0
  80. package/lib/engine/logger/logger.ts +11 -0
  81. package/lib/engine/logger/memory-logger.ts +28 -0
  82. package/lib/engine/logger/node-logger.ts +49 -0
  83. package/lib/engine/logger/noop-logger.ts +9 -0
  84. package/lib/engine/mcp/channel-server.ts +204 -0
  85. package/lib/{modules/mcp/funnel-mcp.ts → engine/mcp/mcp.ts} +29 -10
  86. package/lib/{modules/process/memory-funnel-process-runner.ts → engine/process/memory-process-runner.ts} +1 -1
  87. package/lib/{modules/process/node-funnel-process-runner.ts → engine/process/node-process-runner.ts} +12 -21
  88. package/lib/{modules/process/funnel-process-runner.ts → engine/process/process-runner.ts} +5 -0
  89. package/lib/engine/profiles/profile-channel-checker.ts +7 -0
  90. package/lib/engine/profiles/profiles.ts +126 -0
  91. package/lib/{modules/settings/mock-funnel-settings-reader.ts → engine/settings/mock-settings-reader.ts} +4 -3
  92. package/lib/{modules/settings/funnel-settings-reader.ts → engine/settings/settings-reader.ts} +1 -1
  93. package/lib/engine/settings/settings-schema.ts +46 -0
  94. package/lib/engine/settings/settings-store.ts +110 -0
  95. package/lib/engine/time/clock.ts +15 -0
  96. package/lib/engine/time/memory-clock.ts +26 -0
  97. package/lib/engine/time/node-clock.ts +7 -0
  98. package/lib/funnel.ts +148 -56
  99. package/lib/gateway/auth-middleware.ts +44 -0
  100. package/lib/gateway/broadcaster.ts +319 -0
  101. package/lib/gateway/daemon.ts +47 -0
  102. package/lib/gateway/factory.ts +10 -0
  103. package/lib/gateway/funnel-event-store.ts +155 -0
  104. package/lib/gateway/gateway-server.ts +414 -0
  105. package/lib/gateway/gateway-token.ts +79 -0
  106. package/lib/{modules/gateway/funnel-gateway.ts → gateway/gateway.ts} +70 -27
  107. package/lib/{modules/gateway → gateway}/kill-competing-slack-gateways.ts +7 -3
  108. package/lib/gateway/listener-supervisor.ts +339 -0
  109. package/lib/gateway/listeners-client.ts +128 -0
  110. package/lib/gateway/resolve-daemon-script.ts +26 -0
  111. package/lib/gateway/routes/channels.connectors.call.ts +39 -0
  112. package/lib/gateway/routes/health.ts +13 -0
  113. package/lib/gateway/routes/index.ts +24 -0
  114. package/lib/gateway/routes/listeners.list.ts +6 -0
  115. package/lib/gateway/routes/listeners.restart.ts +15 -0
  116. package/lib/gateway/routes/listeners.start.ts +15 -0
  117. package/lib/gateway/routes/listeners.stop.ts +15 -0
  118. package/lib/gateway/routes/route-deps.ts +11 -0
  119. package/lib/gateway/routes/status.ts +15 -0
  120. package/lib/gateway/routes/validator.ts +17 -0
  121. package/lib/index.ts +50 -92
  122. package/lib/logger/leuco-human-file-writer.ts +65 -0
  123. package/lib/logger/leuco-human-logger.ts +98 -0
  124. package/lib/logger/leuco-human-record.ts +16 -0
  125. package/lib/logger/leuco-human-stdout-writer.ts +26 -0
  126. package/lib/logger/leuco-human-writer.ts +14 -0
  127. package/lib/logger/leuco-logger-memory-sink.ts +67 -0
  128. package/lib/logger/leuco-logger-record.ts +13 -0
  129. package/lib/logger/leuco-logger-sink.ts +33 -0
  130. package/lib/logger/leuco-logger-sqlite-sink.ts +355 -0
  131. package/lib/logger/leuco-logger.ts +135 -0
  132. package/lib/tui/app.tsx +357 -0
  133. package/lib/tui/components/add-row.tsx +18 -0
  134. package/lib/tui/components/brand.tsx +27 -0
  135. package/lib/tui/components/card.tsx +44 -0
  136. package/lib/tui/components/detail-bar.tsx +46 -0
  137. package/lib/tui/components/editable-field.tsx +33 -0
  138. package/lib/tui/components/empty-state.tsx +11 -0
  139. package/lib/tui/components/gateway-status.tsx +66 -0
  140. package/lib/tui/components/keymap.tsx +29 -0
  141. package/lib/tui/components/menu-item.tsx +73 -0
  142. package/lib/tui/components/menu.tsx +26 -0
  143. package/lib/tui/components/panel-header.tsx +22 -0
  144. package/lib/tui/components/readonly-field.tsx +18 -0
  145. package/lib/tui/components/section-header.tsx +25 -0
  146. package/lib/tui/components/selection-accent.tsx +32 -0
  147. package/lib/tui/components/session-item.tsx +33 -0
  148. package/lib/tui/components/session-list.tsx +33 -0
  149. package/lib/tui/components/ui/hascii/accordion-item.tsx +88 -0
  150. package/lib/tui/components/ui/hascii/accordion.tsx +96 -0
  151. package/lib/tui/components/ui/hascii/alert-dialog.tsx +43 -0
  152. package/lib/tui/components/ui/hascii/badge.tsx +51 -0
  153. package/lib/tui/components/ui/hascii/breadcrumb.tsx +58 -0
  154. package/lib/tui/components/ui/hascii/button.tsx +194 -0
  155. package/lib/tui/components/ui/hascii/card-content.tsx +14 -0
  156. package/lib/tui/components/ui/hascii/card-description.tsx +13 -0
  157. package/lib/tui/components/ui/hascii/card-footer.tsx +14 -0
  158. package/lib/tui/components/ui/hascii/card-header.tsx +14 -0
  159. package/lib/tui/components/ui/hascii/card-title.tsx +13 -0
  160. package/lib/tui/components/ui/hascii/card.tsx +27 -0
  161. package/lib/tui/components/ui/hascii/checkbox.tsx +65 -0
  162. package/lib/tui/components/ui/hascii/command.tsx +159 -0
  163. package/lib/tui/components/ui/hascii/dialog-content.tsx +14 -0
  164. package/lib/tui/components/ui/hascii/dialog-description.tsx +13 -0
  165. package/lib/tui/components/ui/hascii/dialog-footer.tsx +14 -0
  166. package/lib/tui/components/ui/hascii/dialog-header.tsx +14 -0
  167. package/lib/tui/components/ui/hascii/dialog-title.tsx +13 -0
  168. package/lib/tui/components/ui/hascii/dialog.tsx +27 -0
  169. package/lib/tui/components/ui/hascii/file-tree.tsx +142 -0
  170. package/lib/tui/components/ui/hascii/focus-group.tsx +62 -0
  171. package/lib/tui/components/ui/hascii/form-item.tsx +43 -0
  172. package/lib/tui/components/ui/hascii/input-otp.tsx +86 -0
  173. package/lib/tui/components/ui/hascii/input.tsx +130 -0
  174. package/lib/tui/components/ui/hascii/pagination.tsx +105 -0
  175. package/lib/tui/components/ui/hascii/progress.tsx +28 -0
  176. package/lib/tui/components/ui/hascii/select.tsx +131 -0
  177. package/lib/tui/components/ui/hascii/separator.tsx +35 -0
  178. package/lib/tui/components/ui/hascii/sidebar-content.tsx +23 -0
  179. package/lib/tui/components/ui/hascii/sidebar-header.tsx +14 -0
  180. package/lib/tui/components/ui/hascii/sidebar-menu-item.tsx +67 -0
  181. package/lib/tui/components/ui/hascii/sidebar.tsx +24 -0
  182. package/lib/tui/components/ui/hascii/skeleton.tsx +60 -0
  183. package/lib/tui/components/ui/hascii/slider.tsx +91 -0
  184. package/lib/tui/components/ui/hascii/snackbar.tsx +75 -0
  185. package/lib/tui/components/ui/hascii/sparkline.tsx +53 -0
  186. package/lib/tui/components/ui/hascii/spinner.tsx +47 -0
  187. package/lib/tui/components/ui/hascii/stepper.tsx +54 -0
  188. package/lib/tui/components/ui/hascii/switch.tsx +66 -0
  189. package/lib/tui/components/ui/hascii/table.tsx +95 -0
  190. package/lib/tui/components/ui/hascii/tabs.tsx +59 -0
  191. package/lib/tui/components/ui/hascii/toggle-group-item.tsx +45 -0
  192. package/lib/tui/components/ui/hascii/toggle-group.tsx +99 -0
  193. package/lib/tui/components/ui/hascii/tree.tsx +104 -0
  194. package/lib/tui/components/view-shell.tsx +44 -0
  195. package/lib/tui/filter-input.tsx +33 -0
  196. package/lib/tui/hooks/hascii/use-pressable.ts +54 -0
  197. package/lib/tui/parse-comma-list.ts +14 -0
  198. package/lib/tui/profile-launcher.tsx +61 -0
  199. package/lib/tui/scrollbar-options.ts +19 -0
  200. package/lib/tui/sidebar.tsx +50 -0
  201. package/lib/tui/theme.ts +40 -0
  202. package/lib/tui/tui.tsx +20 -0
  203. package/lib/tui/types.ts +38 -0
  204. package/lib/tui/unique-name.ts +18 -0
  205. package/lib/tui/use-event-stream.ts +133 -0
  206. package/lib/tui/use-snapshot.ts +99 -0
  207. package/lib/tui/utils/hascii/form-item-context.tsx +23 -0
  208. package/lib/tui/utils/hascii/input-focus-context.tsx +31 -0
  209. package/lib/tui/utils/hascii/theme-context.tsx +26 -0
  210. package/lib/tui/utils/hascii/theme.ts +176 -0
  211. package/lib/tui/views/channels-view.tsx +108 -0
  212. package/lib/tui/views/connectors-view.tsx +164 -0
  213. package/lib/tui/views/events-view.tsx +160 -0
  214. package/lib/tui/views/listeners-view.tsx +80 -0
  215. package/lib/tui/views/profiles-view.tsx +152 -0
  216. package/package.json +51 -34
  217. package/lib/modules/channels/channel-connector-ref-updater.ts +0 -4
  218. package/lib/modules/channels/funnel-channels.ts +0 -155
  219. package/lib/modules/connectors/connector-config-schema.ts +0 -16
  220. package/lib/modules/connectors/connector-existence-checker.ts +0 -3
  221. package/lib/modules/connectors/funnel-callable-connector-store.ts +0 -9
  222. package/lib/modules/connectors/funnel-connector-listener.ts +0 -5
  223. package/lib/modules/connectors/funnel-connector-stores.ts +0 -24
  224. package/lib/modules/connectors/funnel-connector-type-store.ts +0 -24
  225. package/lib/modules/connectors/funnel-connectors.ts +0 -145
  226. package/lib/modules/connectors/funnel-discord-listener.ts +0 -65
  227. package/lib/modules/connectors/funnel-discord-store.ts +0 -84
  228. package/lib/modules/connectors/funnel-gh-store.ts +0 -84
  229. package/lib/modules/connectors/funnel-json-connector-store.ts +0 -100
  230. package/lib/modules/connectors/funnel-schedule-listener.ts +0 -124
  231. package/lib/modules/connectors/funnel-schedule-store.ts +0 -178
  232. package/lib/modules/connectors/funnel-slack-adapter.ts +0 -31
  233. package/lib/modules/connectors/funnel-slack-store.ts +0 -86
  234. package/lib/modules/connectors/migrate-legacy-connectors.ts +0 -77
  235. package/lib/modules/connectors/schedule-connector-schema.ts +0 -18
  236. package/lib/modules/connectors/schedule-last-fired-store.ts +0 -48
  237. package/lib/modules/gateway/daemon.ts +0 -207
  238. package/lib/modules/gateway/funnel-broadcaster.ts +0 -37
  239. package/lib/modules/gateway/funnel-event-logger.ts +0 -59
  240. package/lib/modules/logger.ts +0 -26
  241. package/lib/modules/mcp/channel-server.ts +0 -76
  242. package/lib/modules/profiles/funnel-profiles.ts +0 -123
  243. package/lib/modules/profiles/profile-channel-checker.ts +0 -3
  244. package/lib/modules/profiles/profile-channel-ref-updater.ts +0 -3
  245. package/lib/modules/repos/funnel-repositories.ts +0 -107
  246. package/lib/modules/schedule/funnel-schedule.ts +0 -34
  247. package/lib/modules/settings/funnel-settings-store.ts +0 -56
  248. package/lib/modules/settings/settings-schema.ts +0 -33
  249. package/lib/modules/tui/app.tsx +0 -44
  250. package/lib/modules/tui/tui.tsx +0 -13
  251. package/lib/routes/channels/add.help.ts +0 -3
  252. package/lib/routes/channels/add.ts +0 -21
  253. package/lib/routes/channels/connectors-attach.help.ts +0 -3
  254. package/lib/routes/channels/connectors-attach.ts +0 -17
  255. package/lib/routes/channels/connectors-detach.help.ts +0 -3
  256. package/lib/routes/channels/connectors-detach.ts +0 -17
  257. package/lib/routes/channels/group.help.ts +0 -16
  258. package/lib/routes/channels/group.ts +0 -22
  259. package/lib/routes/channels/remove.help.ts +0 -3
  260. package/lib/routes/channels/remove.ts +0 -17
  261. package/lib/routes/channels/rename.help.ts +0 -5
  262. package/lib/routes/channels/rename.ts +0 -17
  263. package/lib/routes/channels/routes.ts +0 -19
  264. package/lib/routes/channels/show.help.ts +0 -1
  265. package/lib/routes/channels/show.ts +0 -26
  266. package/lib/routes/claude/claude.help.ts +0 -16
  267. package/lib/routes/claude/claude.ts +0 -76
  268. package/lib/routes/claude/routes.ts +0 -4
  269. package/lib/routes/connectors/add.help.ts +0 -28
  270. package/lib/routes/connectors/add.ts +0 -64
  271. package/lib/routes/connectors/group.help.ts +0 -14
  272. package/lib/routes/connectors/group.ts +0 -18
  273. package/lib/routes/connectors/remove.help.ts +0 -3
  274. package/lib/routes/connectors/remove.ts +0 -17
  275. package/lib/routes/connectors/rename.help.ts +0 -5
  276. package/lib/routes/connectors/rename.ts +0 -17
  277. package/lib/routes/connectors/routes.ts +0 -23
  278. package/lib/routes/connectors/schedules-add.help.ts +0 -11
  279. package/lib/routes/connectors/schedules-add.ts +0 -33
  280. package/lib/routes/connectors/schedules-group.help.ts +0 -1
  281. package/lib/routes/connectors/schedules-group.ts +0 -38
  282. package/lib/routes/connectors/schedules-remove.help.ts +0 -3
  283. package/lib/routes/connectors/schedules-remove.ts +0 -17
  284. package/lib/routes/connectors/set.help.ts +0 -8
  285. package/lib/routes/connectors/set.ts +0 -72
  286. package/lib/routes/connectors/show.help.ts +0 -1
  287. package/lib/routes/connectors/show.ts +0 -41
  288. package/lib/routes/gateway/group.help.ts +0 -15
  289. package/lib/routes/gateway/group.ts +0 -28
  290. package/lib/routes/gateway/logs.help.ts +0 -13
  291. package/lib/routes/gateway/logs.ts +0 -100
  292. package/lib/routes/gateway/restart.help.ts +0 -10
  293. package/lib/routes/gateway/routes.ts +0 -18
  294. package/lib/routes/gateway/run.help.ts +0 -12
  295. package/lib/routes/gateway/run.ts +0 -35
  296. package/lib/routes/gateway/start.help.ts +0 -15
  297. package/lib/routes/gateway/start.ts +0 -32
  298. package/lib/routes/gateway/status.help.ts +0 -9
  299. package/lib/routes/gateway/status.ts +0 -28
  300. package/lib/routes/gateway/stop.help.ts +0 -8
  301. package/lib/routes/gateway/stop.ts +0 -21
  302. package/lib/routes/profiles/add.help.ts +0 -3
  303. package/lib/routes/profiles/add.ts +0 -33
  304. package/lib/routes/profiles/group.help.ts +0 -16
  305. package/lib/routes/profiles/group.ts +0 -25
  306. package/lib/routes/profiles/launch.help.ts +0 -4
  307. package/lib/routes/profiles/launch.ts +0 -36
  308. package/lib/routes/profiles/remove.help.ts +0 -3
  309. package/lib/routes/profiles/remove.ts +0 -17
  310. package/lib/routes/profiles/rename.help.ts +0 -5
  311. package/lib/routes/profiles/rename.ts +0 -17
  312. package/lib/routes/profiles/routes.ts +0 -18
  313. package/lib/routes/profiles/set.help.ts +0 -5
  314. package/lib/routes/profiles/set.ts +0 -32
  315. package/lib/routes/repos/add.help.ts +0 -6
  316. package/lib/routes/repos/add.ts +0 -20
  317. package/lib/routes/repos/group.help.ts +0 -11
  318. package/lib/routes/repos/group.ts +0 -18
  319. package/lib/routes/repos/remove.help.ts +0 -3
  320. package/lib/routes/repos/remove.ts +0 -17
  321. package/lib/routes/repos/rename.help.ts +0 -5
  322. package/lib/routes/repos/rename.ts +0 -17
  323. package/lib/routes/repos/routes.ts +0 -17
  324. package/lib/routes/repos/set.help.ts +0 -5
  325. package/lib/routes/repos/set.ts +0 -21
  326. package/lib/routes/repos/show.help.ts +0 -1
  327. package/lib/routes/repos/show.ts +0 -19
  328. package/lib/routes/request/discord-help.ts +0 -9
  329. package/lib/routes/request/discord.help.ts +0 -19
  330. package/lib/routes/request/discord.ts +0 -65
  331. package/lib/routes/request/group.help.ts +0 -15
  332. package/lib/routes/request/group.ts +0 -9
  333. package/lib/routes/request/routes.ts +0 -14
  334. package/lib/routes/request/slack-help.ts +0 -9
  335. package/lib/routes/request/slack.help.ts +0 -19
  336. package/lib/routes/request/slack.ts +0 -61
  337. package/lib/routes/status/routes.ts +0 -4
  338. package/lib/routes/status/status.help.ts +0 -6
  339. package/lib/routes/status/status.ts +0 -77
  340. package/lib/routes/update/routes.ts +0 -4
  341. package/lib/routes/update/update.help.ts +0 -5
  342. package/lib/routes/update/update.ts +0 -21
  343. package/lib/routes.ts +0 -40
  344. /package/lib/{factory.ts → cli/factory.ts} +0 -0
  345. /package/lib/{modules → cli}/router/query-to-cli-args.ts +0 -0
  346. /package/lib/{modules → cli}/router/validator.ts +0 -0
  347. /package/lib/{modules/connectors/funnel-connector-adapter.ts → connectors/connector-adapter.ts} +0 -0
  348. /package/lib/{modules/connectors/funnel-discord-event-processor.ts → connectors/discord-event-processor.ts} +0 -0
  349. /package/lib/{modules/http/funnel-http-client.ts → engine/http/http-client.ts} +0 -0
@@ -0,0 +1,66 @@
1
+ import { useState } from "react"
2
+ import { useHasciiTheme } from "@/tui/utils/hascii/theme-context"
3
+ import { usePressable } from "@/tui/hooks/hascii/use-pressable"
4
+
5
+ export type Props = {
6
+ isChecked?: boolean
7
+ defaultChecked?: boolean
8
+ isDisabled?: boolean
9
+ onChange?: (next: boolean) => void
10
+ }
11
+
12
+ /** Two-cell on/off switch. The thumb sits at the left or right edge of a 3-cell track. */
13
+ export function HasciiSwitch(props: Props) {
14
+ const isDisabled = props.isDisabled ?? false
15
+ const theme = useHasciiTheme()
16
+
17
+ const internalState = useState(props.defaultChecked ?? false)
18
+ const internal = internalState[0]
19
+ const setInternal = internalState[1]
20
+
21
+ const isChecked = props.isChecked ?? internal
22
+
23
+ const toggle = () => {
24
+ const next = !isChecked
25
+
26
+ if (props.isChecked === undefined) setInternal(next)
27
+ props.onChange?.(next)
28
+ }
29
+
30
+ const press = usePressable({ isDisabled, onPress: toggle })
31
+
32
+ const trackBg = isDisabled
33
+ ? theme.color.muted
34
+ : isChecked
35
+ ? press.isPressed
36
+ ? theme.color.primaryActive
37
+ : press.isHovered
38
+ ? theme.color.primaryHover
39
+ : theme.color.primary
40
+ : press.isPressed
41
+ ? theme.color.secondaryActive
42
+ : press.isHovered
43
+ ? theme.color.secondaryActive
44
+ : theme.color.popover
45
+
46
+ const thumbFg = isDisabled
47
+ ? theme.color.mutedForeground
48
+ : isChecked
49
+ ? theme.color.primaryForeground
50
+ : theme.color.foreground
51
+
52
+ return (
53
+ <box
54
+ width={3}
55
+ height={1}
56
+ backgroundColor={trackBg}
57
+ flexDirection="row"
58
+ alignItems="center"
59
+ justifyContent={isChecked ? "flex-end" : "flex-start"}
60
+ paddingRight={isChecked ? 1 : 0}
61
+ {...press.bind}
62
+ >
63
+ <text fg={thumbFg}>▮</text>
64
+ </box>
65
+ )
66
+ }
@@ -0,0 +1,95 @@
1
+ import { useHasciiTheme } from "@/tui/utils/hascii/theme-context"
2
+
3
+ export type TableColumn = {
4
+ key: string
5
+ label: string
6
+ width?: number
7
+ align?: "left" | "right"
8
+ }
9
+
10
+ export type TableRow = Record<string, string | number>
11
+
12
+ export type Props = {
13
+ columns: TableColumn[]
14
+ rows: TableRow[]
15
+ selectedIndex?: number
16
+ onSelect?: (index: number) => void
17
+ }
18
+
19
+ const padCell = (text: string, width: number, align: "left" | "right"): string => {
20
+ const truncated = text.length > width ? `${text.slice(0, Math.max(0, width - 1))}…` : text
21
+ const pad = " ".repeat(Math.max(0, width - truncated.length))
22
+ return align === "right" ? `${pad}${truncated}` : `${truncated}${pad}`
23
+ }
24
+
25
+ const resolveColumnWidth = (column: TableColumn, rows: TableRow[]): number => {
26
+ if (column.width !== undefined) return column.width
27
+
28
+ let width = column.label.length
29
+
30
+ for (const row of rows) {
31
+ const value = String(row[column.key] ?? "")
32
+ if (value.length > width) width = value.length
33
+ }
34
+
35
+ return width
36
+ }
37
+
38
+ /** Static row/column table. Cell text is padded to the column width; rows are clickable when onSelect is provided. */
39
+ export function HasciiTable(props: Props) {
40
+ const theme = useHasciiTheme()
41
+
42
+ const widths = props.columns.map((column) => resolveColumnWidth(column, props.rows))
43
+
44
+ return (
45
+ <box flexDirection="column">
46
+ <box flexDirection="row" gap={2} paddingLeft={1} paddingRight={1} height={1}>
47
+ {props.columns.map((column, columnIndex) => (
48
+ <text key={column.key} fg={theme.color.mutedForeground}>
49
+ {padCell(
50
+ column.label,
51
+ widths[columnIndex] ?? column.label.length,
52
+ column.align ?? "left",
53
+ )}
54
+ </text>
55
+ ))}
56
+ </box>
57
+ <box flexDirection="row" paddingLeft={1} paddingRight={1} height={1}>
58
+ <text fg={theme.color.border}>
59
+ {"─".repeat(
60
+ widths.reduce((sum, width) => sum + width, 0) +
61
+ Math.max(0, props.columns.length - 1) * 2,
62
+ )}
63
+ </text>
64
+ </box>
65
+ {props.rows.map((row, rowIndex) => {
66
+ const isSelected = props.selectedIndex === rowIndex
67
+ const rowBg = isSelected ? theme.color.secondaryActive : undefined
68
+ const rowFg = isSelected ? theme.color.foreground : theme.color.foreground
69
+
70
+ return (
71
+ <box
72
+ key={`row-${rowIndex}`}
73
+ flexDirection="row"
74
+ gap={2}
75
+ paddingLeft={1}
76
+ paddingRight={1}
77
+ height={1}
78
+ backgroundColor={rowBg}
79
+ onMouseUp={props.onSelect !== undefined ? () => props.onSelect?.(rowIndex) : undefined}
80
+ >
81
+ {props.columns.map((column, columnIndex) => (
82
+ <text key={column.key} fg={rowFg}>
83
+ {padCell(
84
+ String(row[column.key] ?? ""),
85
+ widths[columnIndex] ?? 0,
86
+ column.align ?? "left",
87
+ )}
88
+ </text>
89
+ ))}
90
+ </box>
91
+ )
92
+ })}
93
+ </box>
94
+ )
95
+ }
@@ -0,0 +1,59 @@
1
+ import { useState } from "react"
2
+ import { useHasciiTheme } from "@/tui/utils/hascii/theme-context"
3
+
4
+ export type TabItem = {
5
+ value: string
6
+ label: string
7
+ }
8
+
9
+ export type Props = {
10
+ items: TabItem[]
11
+ value?: string
12
+ defaultValue?: string
13
+ onChange?: (value: string) => void
14
+ }
15
+
16
+ /** Single-row segmented tabs. Uncontrolled by default; pass value + onChange to control externally. */
17
+ export function HasciiTabs(props: Props) {
18
+ const theme = useHasciiTheme()
19
+
20
+ const initial = props.defaultValue ?? props.value ?? props.items[0]?.value ?? ""
21
+
22
+ const internalState = useState(initial)
23
+ const internal = internalState[0]
24
+ const setInternal = internalState[1]
25
+
26
+ const current = props.value ?? internal
27
+
28
+ const onSelect = (next: string) => {
29
+ if (props.value === undefined) setInternal(next)
30
+ props.onChange?.(next)
31
+ }
32
+
33
+ return (
34
+ <box flexDirection="row" gap={0} height={2}>
35
+ {props.items.map((item) => {
36
+ const isActive = item.value === current
37
+ const fg = isActive ? theme.color.foreground : theme.color.mutedForeground
38
+ const itemWidth = item.label.length + 4
39
+
40
+ return (
41
+ <box
42
+ key={item.value}
43
+ height={2}
44
+ paddingLeft={2}
45
+ paddingRight={2}
46
+ onMouseUp={() => onSelect(item.value)}
47
+ >
48
+ <text fg={fg}>{item.label}</text>
49
+ {isActive ? (
50
+ <box position="absolute" bottom={0} left={0} right={0}>
51
+ <text fg={theme.color.primary}>{"▁".repeat(itemWidth)}</text>
52
+ </box>
53
+ ) : null}
54
+ </box>
55
+ )
56
+ })}
57
+ </box>
58
+ )
59
+ }
@@ -0,0 +1,45 @@
1
+ import type { ReactNode } from "react"
2
+ import { useHasciiToggleGroup } from "@/tui/components/ui/hascii/toggle-group"
3
+ import { useHasciiTheme } from "@/tui/utils/hascii/theme-context"
4
+ import { usePressable } from "@/tui/hooks/hascii/use-pressable"
5
+
6
+ export type Props = {
7
+ value: string
8
+ children?: ReactNode
9
+ }
10
+
11
+ /** Pressable cell inside HasciiToggleGroup. Pressed state is controlled by the surrounding group. */
12
+ export function HasciiToggleGroupItem(props: Props) {
13
+ const theme = useHasciiTheme()
14
+ const ctx = useHasciiToggleGroup()
15
+
16
+ const isSelected = ctx?.isPressed(props.value) ?? false
17
+
18
+ const press = usePressable({
19
+ onPress: () => ctx?.toggle(props.value),
20
+ })
21
+
22
+ const bg = isSelected
23
+ ? press.isPressed
24
+ ? theme.color.primaryActive
25
+ : press.isHovered
26
+ ? theme.color.primaryHover
27
+ : theme.color.primary
28
+ : press.isPressed
29
+ ? theme.color.secondaryActive
30
+ : press.isHovered
31
+ ? theme.color.secondaryHover
32
+ : theme.color.popover
33
+
34
+ const fg = isSelected
35
+ ? theme.color.primaryForeground
36
+ : press.isHovered
37
+ ? theme.color.foreground
38
+ : theme.color.mutedForeground
39
+
40
+ return (
41
+ <box height={1} paddingLeft={2} paddingRight={2} backgroundColor={bg} {...press.bind}>
42
+ <text fg={fg}>{props.children}</text>
43
+ </box>
44
+ )
45
+ }
@@ -0,0 +1,99 @@
1
+ import { createContext, useContext, useState } from "react"
2
+ import type { ReactNode } from "react"
3
+ import { useHasciiTheme } from "@/tui/utils/hascii/theme-context"
4
+
5
+ type SelectionMode = "single" | "multiple"
6
+
7
+ type SingleProps = {
8
+ type?: "single"
9
+ value?: string
10
+ defaultValue?: string
11
+ onChange?: (value: string) => void
12
+ }
13
+
14
+ type MultipleProps = {
15
+ type: "multiple"
16
+ value?: string[]
17
+ defaultValue?: string[]
18
+ onChange?: (value: string[]) => void
19
+ }
20
+
21
+ export type Props = (SingleProps | MultipleProps) & {
22
+ children?: ReactNode
23
+ }
24
+
25
+ type ContextValue = {
26
+ mode: SelectionMode
27
+ isPressed: (value: string) => boolean
28
+ toggle: (value: string) => void
29
+ }
30
+
31
+ const ToggleGroupContext = createContext<ContextValue | null>(null)
32
+
33
+ /** Read the current ToggleGroup context. Returns null when called outside a HasciiToggleGroup. */
34
+ export function useHasciiToggleGroup(): ContextValue | null {
35
+ return useContext(ToggleGroupContext)
36
+ }
37
+
38
+ const isSingle = (props: Props): props is SingleProps & { children?: ReactNode } =>
39
+ props.type !== "multiple"
40
+
41
+ /** Segmented row of HasciiToggleGroupItem. type="single" is mutually exclusive; type="multiple" allows any subset. */
42
+ export function HasciiToggleGroup(props: Props) {
43
+ const theme = useHasciiTheme()
44
+ const internalSingleState = useState<string>(isSingle(props) ? (props.defaultValue ?? "") : "")
45
+ const internalMultipleState = useState<string[]>(
46
+ !isSingle(props) ? (props.defaultValue ?? []) : [],
47
+ )
48
+
49
+ if (isSingle(props)) {
50
+ const internal = internalSingleState[0]
51
+ const setInternal = internalSingleState[1]
52
+ const current = props.value ?? internal
53
+
54
+ const toggle = (value: string) => {
55
+ if (props.value === undefined) setInternal(value)
56
+ props.onChange?.(value)
57
+ }
58
+
59
+ const ctx: ContextValue = {
60
+ mode: "single",
61
+ isPressed: (value) => value === current,
62
+ toggle,
63
+ }
64
+
65
+ return (
66
+ <ToggleGroupContext.Provider value={ctx}>
67
+ <box flexDirection="row" gap={0} height={1} backgroundColor={theme.color.popover}>
68
+ {props.children}
69
+ </box>
70
+ </ToggleGroupContext.Provider>
71
+ )
72
+ }
73
+
74
+ const internal = internalMultipleState[0]
75
+ const setInternal = internalMultipleState[1]
76
+ const current = props.value ?? internal
77
+
78
+ const toggle = (value: string) => {
79
+ const next = current.includes(value)
80
+ ? current.filter((entry) => entry !== value)
81
+ : [...current, value]
82
+ if (props.value === undefined) setInternal(next)
83
+ props.onChange?.(next)
84
+ }
85
+
86
+ const ctx: ContextValue = {
87
+ mode: "multiple",
88
+ isPressed: (value) => current.includes(value),
89
+ toggle,
90
+ }
91
+
92
+ return (
93
+ <ToggleGroupContext.Provider value={ctx}>
94
+ <box flexDirection="row" gap={0} height={1}>
95
+ {props.children}
96
+ </box>
97
+ </ToggleGroupContext.Provider>
98
+ )
99
+ }
@@ -0,0 +1,104 @@
1
+ import { useState } from "react"
2
+ import { useHasciiTheme } from "@/tui/utils/hascii/theme-context"
3
+
4
+ export type TreeNode = {
5
+ id: string
6
+ label: string
7
+ children?: TreeNode[]
8
+ }
9
+
10
+ export type Props = {
11
+ nodes: TreeNode[]
12
+ indent?: number
13
+ }
14
+
15
+ type Row = {
16
+ node: TreeNode
17
+ prefix: string
18
+ }
19
+
20
+ type RowProps = {
21
+ row: Row
22
+ }
23
+
24
+ /** Internal row used by HasciiTree. Hover-only background; no click handler. */
25
+ function HasciiTreeRow(props: RowProps) {
26
+ const theme = useHasciiTheme()
27
+
28
+ const hoveredState = useState(false)
29
+ const isHovered = hoveredState[0]
30
+ const setHovered = hoveredState[1]
31
+
32
+ const bg = isHovered ? theme.color.secondaryHover : undefined
33
+
34
+ return (
35
+ <box
36
+ flexDirection="row"
37
+ alignItems="center"
38
+ paddingLeft={1}
39
+ paddingRight={1}
40
+ height={1}
41
+ backgroundColor={bg}
42
+ onMouseOver={() => setHovered(true)}
43
+ onMouseOut={() => setHovered(false)}
44
+ >
45
+ <text fg={theme.color.mutedForeground}>{props.row.prefix}</text>
46
+ <text fg={theme.color.foreground}>{props.row.node.label}</text>
47
+ </box>
48
+ )
49
+ }
50
+
51
+ const buildSegment = (
52
+ kind: "ancestor-bar" | "ancestor-blank" | "tee" | "elbow",
53
+ indent: number,
54
+ ): string => {
55
+ const head =
56
+ kind === "ancestor-bar" ? "│" : kind === "ancestor-blank" ? " " : kind === "tee" ? "├" : "└"
57
+ const tail =
58
+ kind === "tee" || kind === "elbow"
59
+ ? "─".repeat(Math.max(0, indent - 1))
60
+ : " ".repeat(Math.max(0, indent - 1))
61
+ return `${head}${tail}`
62
+ }
63
+
64
+ const flatten = (nodes: TreeNode[], ancestorsAreLast: boolean[], indent: number): Row[] => {
65
+ const rows: Row[] = []
66
+
67
+ for (let index = 0; index < nodes.length; index++) {
68
+ const node = nodes[index]
69
+ if (node === undefined) continue
70
+
71
+ const isLast = index === nodes.length - 1
72
+
73
+ let prefix = ""
74
+ for (const isAncestorLast of ancestorsAreLast) {
75
+ prefix += buildSegment(isAncestorLast ? "ancestor-blank" : "ancestor-bar", indent)
76
+ }
77
+ prefix += buildSegment(isLast ? "elbow" : "tee", indent)
78
+ prefix += " "
79
+
80
+ rows.push({ node, prefix })
81
+
82
+ if (node.children && node.children.length > 0) {
83
+ const childRows = flatten(node.children, [...ancestorsAreLast, isLast], indent)
84
+ for (const childRow of childRows) rows.push(childRow)
85
+ }
86
+ }
87
+
88
+ return rows
89
+ }
90
+
91
+ /** Static read-only file-tree drawn with ├/└/│ box-drawing characters. Hover highlights a row but rows are not clickable. */
92
+ export function HasciiTree(props: Props) {
93
+ const indent = props.indent ?? 2
94
+
95
+ const rows = flatten(props.nodes, [], indent)
96
+
97
+ return (
98
+ <box flexDirection="column">
99
+ {rows.map((row) => (
100
+ <HasciiTreeRow key={row.node.id} row={row} />
101
+ ))}
102
+ </box>
103
+ )
104
+ }
@@ -0,0 +1,44 @@
1
+ /** @jsxImportSource @opentui/react */
2
+ import type { ReactNode } from "react"
3
+ import { verticalScrollbarOptions } from "@/tui/scrollbar-options"
4
+ import { funnel } from "@/tui/theme"
5
+ import { useHasciiTheme } from "@/tui/utils/hascii/theme-context"
6
+
7
+ type Props = {
8
+ children: ReactNode
9
+ }
10
+
11
+ /**
12
+ * Outer wrapper every view renders into.
13
+ *
14
+ * Renders a vertical `<scrollbox>` so content that overflows the visible
15
+ * area scrolls instead of clipping the layout. Padding follows the
16
+ * uniform `funnel.paddingX/Y` rule and lives on the inner content
17
+ * container (via `contentOptions`); the outer scrollbox itself is
18
+ * transparent so multi-block views (`events` events list + DetailBar
19
+ * sibling) still stack cleanly.
20
+ *
21
+ * The vertical scrollbar's track and thumb pull from the theme so the
22
+ * widget reads as part of the surface palette instead of OpenTUI's
23
+ * default electric blue.
24
+ */
25
+ export function ViewShell(props: Props) {
26
+ const theme = useHasciiTheme()
27
+
28
+ return (
29
+ <scrollbox
30
+ style={{ flexGrow: 1 }}
31
+ contentOptions={{
32
+ flexDirection: "column",
33
+ paddingLeft: funnel.paddingX,
34
+ paddingRight: funnel.paddingX,
35
+ paddingTop: funnel.paddingY,
36
+ paddingBottom: funnel.paddingY,
37
+ gap: funnel.gap,
38
+ }}
39
+ verticalScrollbarOptions={verticalScrollbarOptions(theme)}
40
+ >
41
+ {props.children}
42
+ </scrollbox>
43
+ )
44
+ }
@@ -0,0 +1,33 @@
1
+ /** @jsxImportSource @opentui/react */
2
+ import { funnel } from "@/tui/theme"
3
+ import { useHasciiTheme } from "@/tui/utils/hascii/theme-context"
4
+
5
+ type Props = {
6
+ value: string
7
+ active: boolean
8
+ }
9
+
10
+ /** Inline filter overlay shown when the user presses `/`. */
11
+ export function FilterInput(props: Props) {
12
+ const theme = useHasciiTheme()
13
+
14
+ if (!props.active) return null
15
+
16
+ return (
17
+ <box
18
+ style={{
19
+ height: funnel.barHeight,
20
+ backgroundColor: theme.color.muted,
21
+ paddingLeft: funnel.paddingX,
22
+ paddingRight: funnel.paddingX,
23
+ }}
24
+ >
25
+ <text>
26
+ <span fg={theme.color.foreground}>/</span>
27
+ <span fg={theme.color.foreground}>{props.value}</span>
28
+ <span fg={theme.color.foreground}>█</span>
29
+ <span fg={theme.color.mutedForeground}>{" · Enter to apply · Esc to cancel"}</span>
30
+ </text>
31
+ </box>
32
+ )
33
+ }
@@ -0,0 +1,54 @@
1
+ import { useState } from "react"
2
+
3
+ export type Bindings = {
4
+ onMouseOver: () => void
5
+ onMouseOut: () => void
6
+ onMouseDown: () => void
7
+ onMouseUp: () => void
8
+ }
9
+
10
+ export type PressableState = {
11
+ isHovered: boolean
12
+ isPressed: boolean
13
+ bind: Bindings
14
+ }
15
+
16
+ export type Options = {
17
+ isDisabled?: boolean
18
+ onPress?: () => void
19
+ }
20
+
21
+ /** Tracks hover and press state for a focusable element and exposes mouse handlers ready for spread. */
22
+ export function usePressable(options?: Options): PressableState {
23
+ const isDisabled = options?.isDisabled ?? false
24
+ const onPress = options?.onPress
25
+
26
+ const hoveredState = useState(false)
27
+ const isHovered = hoveredState[0]
28
+ const setHovered = hoveredState[1]
29
+
30
+ const pressedState = useState(false)
31
+ const isPressed = pressedState[0]
32
+ const setPressed = pressedState[1]
33
+
34
+ const bind: Bindings = {
35
+ onMouseOver: () => {
36
+ if (!isDisabled) setHovered(true)
37
+ },
38
+ onMouseOut: () => {
39
+ setHovered(false)
40
+ setPressed(false)
41
+ },
42
+ onMouseDown: () => {
43
+ if (!isDisabled) setPressed(true)
44
+ },
45
+ onMouseUp: () => {
46
+ if (isDisabled) return
47
+
48
+ if (isPressed) onPress?.()
49
+ setPressed(false)
50
+ },
51
+ }
52
+
53
+ return { isHovered, isPressed, bind }
54
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Parse a comma-separated text input into a clean string array.
3
+ *
4
+ * Used by the editable channels (connector list) and profiles (env-files)
5
+ * views to turn the user's free-form input ("a, b , c,") into the
6
+ * actual list ["a", "b", "c"]. Empty entries and surrounding whitespace
7
+ * are dropped so trailing commas and stray spaces don't matter.
8
+ */
9
+ export function parseCommaList(raw: string): string[] {
10
+ return raw
11
+ .split(",")
12
+ .map((part) => part.trim())
13
+ .filter((part) => part.length > 0)
14
+ }
@@ -0,0 +1,61 @@
1
+ /** @jsxImportSource @opentui/react */
2
+ import { HasciiSeparator } from "@/tui/components/ui/hascii/separator"
3
+ import { EmptyState } from "@/tui/components/empty-state"
4
+ import { funnel } from "@/tui/theme"
5
+ import { useHasciiTheme } from "@/tui/utils/hascii/theme-context"
6
+ import type { ProfileConfig } from "@/engine/settings/settings-schema"
7
+
8
+ type Props = {
9
+ active: boolean
10
+ profiles: ProfileConfig[]
11
+ selectedIndex: number
12
+ }
13
+
14
+ /**
15
+ * Modal-style overlay: pick a profile and launch Claude Code via the same
16
+ * code path as `fnl claude --profile`. The launcher exits the TUI before
17
+ * exec'ing so Claude takes over the terminal.
18
+ */
19
+ export function ProfileLauncher(props: Props) {
20
+ const theme = useHasciiTheme()
21
+
22
+ if (!props.active) return null
23
+
24
+ return (
25
+ <box
26
+ style={{
27
+ flexDirection: "column",
28
+ backgroundColor: funnel.surface,
29
+ paddingLeft: funnel.paddingX,
30
+ paddingRight: funnel.paddingX,
31
+ paddingTop: funnel.paddingY,
32
+ paddingBottom: funnel.paddingY,
33
+ gap: funnel.gap,
34
+ position: "absolute",
35
+ top: funnel.modalTop,
36
+ left: funnel.modalInset,
37
+ right: funnel.modalInset,
38
+ }}
39
+ >
40
+ <text fg={theme.color.foreground}>launch claude with profile</text>
41
+ <HasciiSeparator />
42
+
43
+ {props.profiles.length === 0 ? (
44
+ <EmptyState message="(no profiles — `fnl profiles add` first)" />
45
+ ) : (
46
+ props.profiles.map((profile, index) => {
47
+ const selected = index === props.selectedIndex
48
+
49
+ return (
50
+ <text key={profile.name} bg={selected ? theme.color.muted : undefined}>
51
+ <span fg={theme.color.foreground}>{profile.name}</span>
52
+ <span fg={funnel.faint}>
53
+ {` → channel ${profile.channelId} · path ${profile.path} · sub-agent ${profile.subAgent}`}
54
+ </span>
55
+ </text>
56
+ )
57
+ })
58
+ )}
59
+ </box>
60
+ )
61
+ }