@interactive-inc/claude-funnel 0.7.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.
- package/README.md +155 -133
- package/dist/bin.js +1417 -0
- package/dist/gateway/daemon.js +513 -0
- package/dist/highlights-eq9cgrbb.scm +604 -0
- package/dist/highlights-ghv9g403.scm +205 -0
- package/dist/highlights-hk7bwhj4.scm +284 -0
- package/dist/highlights-r812a2qc.scm +150 -0
- package/dist/highlights-x6tmsnaa.scm +115 -0
- package/dist/injections-73j83es3.scm +27 -0
- package/dist/tree-sitter-javascript-nd0q4pe9.wasm +0 -0
- package/dist/tree-sitter-markdown-411r6y9b.wasm +0 -0
- package/dist/tree-sitter-markdown_inline-j5349f42.wasm +0 -0
- package/dist/tree-sitter-typescript-zxjzwt75.wasm +0 -0
- package/dist/tree-sitter-zig-e78zbjpm.wasm +0 -0
- package/lib/bin.ts +78 -0
- package/lib/{modules → cli}/router/to-request.ts +13 -20
- package/lib/cli/routes/channels.$channel.connectors.$connector.rename.$newName.ts +27 -0
- package/lib/cli/routes/channels.$channel.connectors.$connector.request.ts +40 -0
- package/lib/cli/routes/channels.$channel.connectors.$connector.schedules.add.$id.ts +41 -0
- package/lib/cli/routes/channels.$channel.connectors.$connector.schedules.remove.$id.ts +22 -0
- package/lib/cli/routes/channels.$channel.connectors.$connector.schedules.ts +23 -0
- package/lib/cli/routes/channels.$channel.connectors.$connector.ts +26 -0
- package/lib/cli/routes/channels.$channel.connectors.add.$connector.ts +92 -0
- package/lib/cli/routes/channels.$channel.connectors.remove.$connector.ts +22 -0
- package/lib/cli/routes/channels.$channel.connectors.set.$connector.ts +63 -0
- package/lib/cli/routes/channels.$channel.connectors.ts +26 -0
- package/lib/cli/routes/channels.$channel.rename.$newName.ts +22 -0
- package/lib/cli/routes/channels.$channel.set.delivery.$mode.ts +34 -0
- package/lib/cli/routes/channels.$channel.ts +34 -0
- package/lib/cli/routes/channels.add.$channel.ts +33 -0
- package/lib/cli/routes/channels.remove.$channel.ts +20 -0
- package/lib/cli/routes/channels.ts +39 -0
- package/lib/cli/routes/claude.ts +69 -0
- package/lib/cli/routes/gateway.listeners.ts +41 -0
- package/lib/cli/routes/gateway.logs.ts +123 -0
- package/lib/{routes/gateway/restart.ts → cli/routes/gateway.restart.ts} +20 -5
- package/lib/cli/routes/gateway.run.ts +41 -0
- package/lib/cli/routes/gateway.start.ts +50 -0
- package/lib/cli/routes/gateway.status.ts +19 -0
- package/lib/cli/routes/gateway.stop.ts +32 -0
- package/lib/cli/routes/gateway.ts +55 -0
- package/lib/cli/routes/index.ts +202 -0
- package/lib/cli/routes/profiles.$profile.as-default.ts +22 -0
- package/lib/cli/routes/profiles.$profile.rename.$newName.ts +22 -0
- package/lib/cli/routes/profiles.$profile.run.ts +36 -0
- package/lib/cli/routes/profiles.add.$profile.ts +46 -0
- package/lib/cli/routes/profiles.remove.$profile.ts +20 -0
- package/lib/cli/routes/profiles.set.$profile.ts +46 -0
- package/lib/cli/routes/profiles.ts +40 -0
- package/lib/cli/routes/status.ts +93 -0
- package/lib/cli/routes/update.ts +27 -0
- package/lib/connectors/connector-config-schema.ts +16 -0
- package/lib/connectors/connector-factory.ts +94 -0
- package/lib/connectors/connector-listener.ts +20 -0
- package/lib/{modules/connectors/funnel-discord-adapter.ts → connectors/discord-adapter.ts} +6 -11
- package/lib/{modules/connectors → connectors}/discord-connector-schema.ts +4 -1
- package/lib/connectors/discord-listener.ts +111 -0
- package/lib/{modules/connectors/funnel-gh-adapter.ts → connectors/gh-adapter.ts} +3 -6
- package/lib/{modules/connectors → connectors}/gh-connector-schema.ts +4 -1
- package/lib/{modules/connectors/funnel-gh-listener.ts → connectors/gh-listener.ts} +45 -19
- package/lib/{modules/connectors → connectors}/match-cron.ts +10 -4
- package/lib/connectors/schedule-connector-schema.ts +33 -0
- package/lib/connectors/schedule-listener.ts +207 -0
- package/lib/connectors/schedule-state-store.ts +54 -0
- package/lib/connectors/slack-adapter.ts +36 -0
- package/lib/{modules/connectors → connectors}/slack-connector-schema.ts +4 -1
- package/lib/{modules/connectors/funnel-slack-event-processor.ts → connectors/slack-event-processor.ts} +15 -9
- package/lib/{modules/connectors/funnel-slack-listener.ts → connectors/slack-listener.ts} +33 -14
- package/lib/engine/channels/channels.ts +520 -0
- package/lib/{modules/claude/funnel-claude.ts → engine/claude/claude.ts} +28 -55
- package/lib/engine/claude/gateway-controller.ts +4 -0
- package/lib/{modules/fs/funnel-file-system.ts → engine/fs/file-system.ts} +4 -0
- package/lib/{modules/fs/memory-funnel-file-system.ts → engine/fs/memory-file-system.ts} +20 -3
- package/lib/{modules/fs/node-funnel-file-system.ts → engine/fs/node-file-system.ts} +14 -2
- package/lib/{modules/http/memory-funnel-http-client.ts → engine/http/memory-http-client.ts} +1 -5
- package/lib/{modules/http/node-funnel-http-client.ts → engine/http/node-http-client.ts} +1 -5
- package/lib/{modules/id/memory-funnel-id-generator.ts → engine/id/memory-id-generator.ts} +1 -1
- package/lib/{modules/id/node-funnel-id-generator.ts → engine/id/node-id-generator.ts} +1 -1
- package/lib/{modules/logger/memory-funnel-logger.ts → engine/logger/memory-logger.ts} +1 -1
- package/lib/{modules/logger/node-funnel-logger.ts → engine/logger/node-logger.ts} +1 -1
- package/lib/{modules/logger/noop-funnel-logger.ts → engine/logger/noop-logger.ts} +1 -1
- package/lib/engine/mcp/channel-server.ts +204 -0
- package/lib/{modules/mcp/funnel-mcp.ts → engine/mcp/mcp.ts} +24 -10
- package/lib/{modules/process/memory-funnel-process-runner.ts → engine/process/memory-process-runner.ts} +1 -1
- package/lib/{modules/process/node-funnel-process-runner.ts → engine/process/node-process-runner.ts} +12 -21
- package/lib/engine/profiles/profile-channel-checker.ts +7 -0
- package/lib/{modules/profiles/funnel-profiles.ts → engine/profiles/profiles.ts} +41 -43
- package/lib/{modules/settings/mock-funnel-settings-reader.ts → engine/settings/mock-settings-reader.ts} +4 -3
- package/lib/{modules/settings/funnel-settings-reader.ts → engine/settings/settings-reader.ts} +1 -1
- package/lib/engine/settings/settings-schema.ts +46 -0
- package/lib/engine/settings/settings-store.ts +110 -0
- package/lib/{modules/time/memory-funnel-clock.ts → engine/time/memory-clock.ts} +1 -1
- package/lib/{modules/time/node-funnel-clock.ts → engine/time/node-clock.ts} +1 -1
- package/lib/funnel.ts +83 -78
- package/lib/gateway/auth-middleware.ts +44 -0
- package/lib/gateway/broadcaster.ts +319 -0
- package/lib/gateway/daemon.ts +47 -0
- package/lib/gateway/factory.ts +10 -0
- package/lib/gateway/funnel-event-store.ts +155 -0
- package/lib/gateway/gateway-server.ts +414 -0
- package/lib/gateway/gateway-token.ts +79 -0
- package/lib/{modules/gateway/funnel-gateway.ts → gateway/gateway.ts} +27 -13
- package/lib/{modules/gateway → gateway}/kill-competing-slack-gateways.ts +4 -4
- package/lib/gateway/listener-supervisor.ts +339 -0
- package/lib/gateway/listeners-client.ts +128 -0
- package/lib/gateway/resolve-daemon-script.ts +26 -0
- package/lib/gateway/routes/channels.connectors.call.ts +39 -0
- package/lib/gateway/routes/health.ts +13 -0
- package/lib/gateway/routes/index.ts +24 -0
- package/lib/gateway/routes/listeners.list.ts +6 -0
- package/lib/gateway/routes/listeners.restart.ts +15 -0
- package/lib/gateway/routes/listeners.start.ts +15 -0
- package/lib/gateway/routes/listeners.stop.ts +15 -0
- package/lib/gateway/routes/route-deps.ts +11 -0
- package/lib/gateway/routes/status.ts +15 -0
- package/lib/gateway/routes/validator.ts +17 -0
- package/lib/index.ts +50 -92
- package/lib/logger/leuco-human-file-writer.ts +65 -0
- package/lib/logger/leuco-human-logger.ts +98 -0
- package/lib/logger/leuco-human-record.ts +16 -0
- package/lib/logger/leuco-human-stdout-writer.ts +26 -0
- package/lib/logger/leuco-human-writer.ts +14 -0
- package/lib/logger/leuco-logger-memory-sink.ts +67 -0
- package/lib/logger/leuco-logger-record.ts +13 -0
- package/lib/logger/leuco-logger-sink.ts +33 -0
- package/lib/logger/leuco-logger-sqlite-sink.ts +355 -0
- package/lib/logger/leuco-logger.ts +135 -0
- package/lib/tui/app.tsx +357 -0
- package/lib/tui/components/add-row.tsx +18 -0
- package/lib/tui/components/brand.tsx +27 -0
- package/lib/tui/components/card.tsx +44 -0
- package/lib/tui/components/detail-bar.tsx +46 -0
- package/lib/tui/components/editable-field.tsx +33 -0
- package/lib/tui/components/empty-state.tsx +11 -0
- package/lib/tui/components/gateway-status.tsx +66 -0
- package/lib/tui/components/keymap.tsx +29 -0
- package/lib/tui/components/menu-item.tsx +73 -0
- package/lib/tui/components/menu.tsx +26 -0
- package/lib/tui/components/panel-header.tsx +22 -0
- package/lib/tui/components/readonly-field.tsx +18 -0
- package/lib/tui/components/section-header.tsx +25 -0
- package/lib/tui/components/selection-accent.tsx +32 -0
- package/lib/tui/components/session-item.tsx +33 -0
- package/lib/tui/components/session-list.tsx +33 -0
- package/lib/tui/components/ui/hascii/accordion-item.tsx +88 -0
- package/lib/tui/components/ui/hascii/accordion.tsx +96 -0
- package/lib/tui/components/ui/hascii/alert-dialog.tsx +43 -0
- package/lib/tui/components/ui/hascii/badge.tsx +51 -0
- package/lib/tui/components/ui/hascii/breadcrumb.tsx +58 -0
- package/lib/tui/components/ui/hascii/button.tsx +194 -0
- package/lib/tui/components/ui/hascii/card-content.tsx +14 -0
- package/lib/tui/components/ui/hascii/card-description.tsx +13 -0
- package/lib/tui/components/ui/hascii/card-footer.tsx +14 -0
- package/lib/tui/components/ui/hascii/card-header.tsx +14 -0
- package/lib/tui/components/ui/hascii/card-title.tsx +13 -0
- package/lib/tui/components/ui/hascii/card.tsx +27 -0
- package/lib/tui/components/ui/hascii/checkbox.tsx +65 -0
- package/lib/tui/components/ui/hascii/command.tsx +159 -0
- package/lib/tui/components/ui/hascii/dialog-content.tsx +14 -0
- package/lib/tui/components/ui/hascii/dialog-description.tsx +13 -0
- package/lib/tui/components/ui/hascii/dialog-footer.tsx +14 -0
- package/lib/tui/components/ui/hascii/dialog-header.tsx +14 -0
- package/lib/tui/components/ui/hascii/dialog-title.tsx +13 -0
- package/lib/tui/components/ui/hascii/dialog.tsx +27 -0
- package/lib/tui/components/ui/hascii/file-tree.tsx +142 -0
- package/lib/tui/components/ui/hascii/focus-group.tsx +62 -0
- package/lib/tui/components/ui/hascii/form-item.tsx +43 -0
- package/lib/tui/components/ui/hascii/input-otp.tsx +86 -0
- package/lib/tui/components/ui/hascii/input.tsx +130 -0
- package/lib/tui/components/ui/hascii/pagination.tsx +105 -0
- package/lib/tui/components/ui/hascii/progress.tsx +28 -0
- package/lib/tui/components/ui/hascii/select.tsx +131 -0
- package/lib/tui/components/ui/hascii/separator.tsx +35 -0
- package/lib/tui/components/ui/hascii/sidebar-content.tsx +23 -0
- package/lib/tui/components/ui/hascii/sidebar-header.tsx +14 -0
- package/lib/tui/components/ui/hascii/sidebar-menu-item.tsx +67 -0
- package/lib/tui/components/ui/hascii/sidebar.tsx +24 -0
- package/lib/tui/components/ui/hascii/skeleton.tsx +60 -0
- package/lib/tui/components/ui/hascii/slider.tsx +91 -0
- package/lib/tui/components/ui/hascii/snackbar.tsx +75 -0
- package/lib/tui/components/ui/hascii/sparkline.tsx +53 -0
- package/lib/tui/components/ui/hascii/spinner.tsx +47 -0
- package/lib/tui/components/ui/hascii/stepper.tsx +54 -0
- package/lib/tui/components/ui/hascii/switch.tsx +66 -0
- package/lib/tui/components/ui/hascii/table.tsx +95 -0
- package/lib/tui/components/ui/hascii/tabs.tsx +59 -0
- package/lib/tui/components/ui/hascii/toggle-group-item.tsx +45 -0
- package/lib/tui/components/ui/hascii/toggle-group.tsx +99 -0
- package/lib/tui/components/ui/hascii/tree.tsx +104 -0
- package/lib/tui/components/view-shell.tsx +44 -0
- package/lib/tui/filter-input.tsx +33 -0
- package/lib/tui/hooks/hascii/use-pressable.ts +54 -0
- package/lib/tui/parse-comma-list.ts +14 -0
- package/lib/tui/profile-launcher.tsx +61 -0
- package/lib/tui/scrollbar-options.ts +19 -0
- package/lib/tui/sidebar.tsx +50 -0
- package/lib/tui/theme.ts +40 -0
- package/lib/tui/tui.tsx +20 -0
- package/lib/tui/types.ts +38 -0
- package/lib/tui/unique-name.ts +18 -0
- package/lib/tui/use-event-stream.ts +133 -0
- package/lib/tui/use-snapshot.ts +99 -0
- package/lib/tui/utils/hascii/form-item-context.tsx +23 -0
- package/lib/tui/utils/hascii/input-focus-context.tsx +31 -0
- package/lib/tui/utils/hascii/theme-context.tsx +26 -0
- package/lib/tui/utils/hascii/theme.ts +176 -0
- package/lib/tui/views/channels-view.tsx +108 -0
- package/lib/tui/views/connectors-view.tsx +164 -0
- package/lib/tui/views/events-view.tsx +160 -0
- package/lib/tui/views/listeners-view.tsx +80 -0
- package/lib/tui/views/profiles-view.tsx +152 -0
- package/package.json +50 -44
- package/lib/api.ts +0 -54
- package/lib/modules/channels/channel-connector-ref-updater.ts +0 -4
- package/lib/modules/channels/funnel-channels.ts +0 -160
- package/lib/modules/connectors/connector-config-schema.ts +0 -16
- package/lib/modules/connectors/connector-existence-checker.ts +0 -3
- package/lib/modules/connectors/funnel-callable-connector-store.ts +0 -9
- package/lib/modules/connectors/funnel-connector-listener.ts +0 -5
- package/lib/modules/connectors/funnel-connector-stores.ts +0 -52
- package/lib/modules/connectors/funnel-connector-type-store.ts +0 -24
- package/lib/modules/connectors/funnel-connectors.ts +0 -151
- package/lib/modules/connectors/funnel-discord-listener.ts +0 -71
- package/lib/modules/connectors/funnel-discord-store.ts +0 -88
- package/lib/modules/connectors/funnel-gh-store.ts +0 -101
- package/lib/modules/connectors/funnel-json-connector-store.ts +0 -100
- package/lib/modules/connectors/funnel-schedule-listener.ts +0 -130
- package/lib/modules/connectors/funnel-schedule-store.ts +0 -195
- package/lib/modules/connectors/funnel-slack-adapter.ts +0 -31
- package/lib/modules/connectors/funnel-slack-store.ts +0 -90
- package/lib/modules/connectors/migrate-legacy-connectors.ts +0 -81
- package/lib/modules/connectors/schedule-connector-schema.ts +0 -18
- package/lib/modules/connectors/schedule-last-fired-store.ts +0 -48
- package/lib/modules/gateway/daemon.ts +0 -74
- package/lib/modules/gateway/funnel-broadcaster.ts +0 -37
- package/lib/modules/gateway/funnel-event-logger.ts +0 -59
- package/lib/modules/gateway/funnel-gateway-server.ts +0 -241
- package/lib/modules/mcp/channel-server.ts +0 -76
- package/lib/modules/profiles/profile-channel-checker.ts +0 -3
- package/lib/modules/profiles/profile-channel-ref-updater.ts +0 -3
- package/lib/modules/repos/funnel-repositories.ts +0 -112
- package/lib/modules/schedule/funnel-schedule.ts +0 -39
- package/lib/modules/settings/funnel-settings-store.ts +0 -56
- package/lib/modules/settings/settings-schema.ts +0 -33
- package/lib/modules/tui/app.tsx +0 -44
- package/lib/modules/tui/tui.tsx +0 -13
- package/lib/routes/channels/add.help.ts +0 -3
- package/lib/routes/channels/add.ts +0 -21
- package/lib/routes/channels/connectors-attach.help.ts +0 -3
- package/lib/routes/channels/connectors-attach.ts +0 -17
- package/lib/routes/channels/connectors-detach.help.ts +0 -3
- package/lib/routes/channels/connectors-detach.ts +0 -17
- package/lib/routes/channels/group.help.ts +0 -16
- package/lib/routes/channels/group.ts +0 -22
- package/lib/routes/channels/remove.help.ts +0 -3
- package/lib/routes/channels/remove.ts +0 -17
- package/lib/routes/channels/rename.help.ts +0 -5
- package/lib/routes/channels/rename.ts +0 -17
- package/lib/routes/channels/routes.ts +0 -19
- package/lib/routes/channels/show.help.ts +0 -1
- package/lib/routes/channels/show.ts +0 -26
- package/lib/routes/claude/claude.help.ts +0 -16
- package/lib/routes/claude/claude.ts +0 -76
- package/lib/routes/claude/routes.ts +0 -4
- package/lib/routes/connectors/add.help.ts +0 -28
- package/lib/routes/connectors/add.ts +0 -64
- package/lib/routes/connectors/group.help.ts +0 -14
- package/lib/routes/connectors/group.ts +0 -18
- package/lib/routes/connectors/remove.help.ts +0 -3
- package/lib/routes/connectors/remove.ts +0 -17
- package/lib/routes/connectors/rename.help.ts +0 -5
- package/lib/routes/connectors/rename.ts +0 -17
- package/lib/routes/connectors/routes.ts +0 -23
- package/lib/routes/connectors/schedules-add.help.ts +0 -11
- package/lib/routes/connectors/schedules-add.ts +0 -33
- package/lib/routes/connectors/schedules-group.help.ts +0 -1
- package/lib/routes/connectors/schedules-group.ts +0 -38
- package/lib/routes/connectors/schedules-remove.help.ts +0 -3
- package/lib/routes/connectors/schedules-remove.ts +0 -17
- package/lib/routes/connectors/set.help.ts +0 -8
- package/lib/routes/connectors/set.ts +0 -72
- package/lib/routes/connectors/show.help.ts +0 -1
- package/lib/routes/connectors/show.ts +0 -41
- package/lib/routes/gateway/group.help.ts +0 -15
- package/lib/routes/gateway/group.ts +0 -28
- package/lib/routes/gateway/logs.help.ts +0 -13
- package/lib/routes/gateway/logs.ts +0 -102
- package/lib/routes/gateway/restart.help.ts +0 -10
- package/lib/routes/gateway/routes.ts +0 -18
- package/lib/routes/gateway/run.help.ts +0 -12
- package/lib/routes/gateway/run.ts +0 -35
- package/lib/routes/gateway/start.help.ts +0 -15
- package/lib/routes/gateway/start.ts +0 -32
- package/lib/routes/gateway/status.help.ts +0 -9
- package/lib/routes/gateway/status.ts +0 -28
- package/lib/routes/gateway/stop.help.ts +0 -8
- package/lib/routes/gateway/stop.ts +0 -21
- package/lib/routes/profiles/add.help.ts +0 -3
- package/lib/routes/profiles/add.ts +0 -33
- package/lib/routes/profiles/group.help.ts +0 -16
- package/lib/routes/profiles/group.ts +0 -25
- package/lib/routes/profiles/launch.help.ts +0 -4
- package/lib/routes/profiles/launch.ts +0 -36
- package/lib/routes/profiles/remove.help.ts +0 -3
- package/lib/routes/profiles/remove.ts +0 -17
- package/lib/routes/profiles/rename.help.ts +0 -5
- package/lib/routes/profiles/rename.ts +0 -17
- package/lib/routes/profiles/routes.ts +0 -18
- package/lib/routes/profiles/set.help.ts +0 -5
- package/lib/routes/profiles/set.ts +0 -32
- package/lib/routes/repos/add.help.ts +0 -6
- package/lib/routes/repos/add.ts +0 -20
- package/lib/routes/repos/group.help.ts +0 -11
- package/lib/routes/repos/group.ts +0 -18
- package/lib/routes/repos/remove.help.ts +0 -3
- package/lib/routes/repos/remove.ts +0 -17
- package/lib/routes/repos/rename.help.ts +0 -5
- package/lib/routes/repos/rename.ts +0 -17
- package/lib/routes/repos/routes.ts +0 -17
- package/lib/routes/repos/set.help.ts +0 -5
- package/lib/routes/repos/set.ts +0 -21
- package/lib/routes/repos/show.help.ts +0 -1
- package/lib/routes/repos/show.ts +0 -19
- package/lib/routes/request/discord-help.ts +0 -9
- package/lib/routes/request/discord.help.ts +0 -19
- package/lib/routes/request/discord.ts +0 -65
- package/lib/routes/request/group.help.ts +0 -15
- package/lib/routes/request/group.ts +0 -9
- package/lib/routes/request/routes.ts +0 -14
- package/lib/routes/request/slack-help.ts +0 -9
- package/lib/routes/request/slack.help.ts +0 -19
- package/lib/routes/request/slack.ts +0 -61
- package/lib/routes/status/routes.ts +0 -4
- package/lib/routes/status/status.help.ts +0 -6
- package/lib/routes/status/status.ts +0 -77
- package/lib/routes/update/routes.ts +0 -4
- package/lib/routes/update/update.help.ts +0 -5
- package/lib/routes/update/update.ts +0 -21
- package/lib/routes.ts +0 -40
- /package/lib/{factory.ts → cli/factory.ts} +0 -0
- /package/lib/{modules → cli}/router/query-to-cli-args.ts +0 -0
- /package/lib/{modules → cli}/router/validator.ts +0 -0
- /package/lib/{modules/connectors/funnel-connector-adapter.ts → connectors/connector-adapter.ts} +0 -0
- /package/lib/{modules/connectors/funnel-discord-event-processor.ts → connectors/discord-event-processor.ts} +0 -0
- /package/lib/{modules/http/funnel-http-client.ts → engine/http/http-client.ts} +0 -0
- /package/lib/{modules/id/funnel-id-generator.ts → engine/id/id-generator.ts} +0 -0
- /package/lib/{modules/logger/funnel-logger.ts → engine/logger/logger.ts} +0 -0
- /package/lib/{modules/process/funnel-process-runner.ts → engine/process/process-runner.ts} +0 -0
- /package/lib/{modules/time/funnel-clock.ts → engine/time/clock.ts} +0 -0
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/** @jsxImportSource @opentui/react */
|
|
2
|
+
import { AddRow } from "@/tui/components/add-row"
|
|
3
|
+
import { Card } from "@/tui/components/card"
|
|
4
|
+
import { EditableField } from "@/tui/components/editable-field"
|
|
5
|
+
import { EmptyState } from "@/tui/components/empty-state"
|
|
6
|
+
import { PanelHeader } from "@/tui/components/panel-header"
|
|
7
|
+
import { ReadonlyField } from "@/tui/components/readonly-field"
|
|
8
|
+
import { ViewShell } from "@/tui/components/view-shell"
|
|
9
|
+
import type { Snapshot } from "@/tui/types"
|
|
10
|
+
import { uniqueName } from "@/tui/unique-name"
|
|
11
|
+
import type { Funnel } from "@/funnel"
|
|
12
|
+
|
|
13
|
+
type Props = {
|
|
14
|
+
snapshot: Snapshot
|
|
15
|
+
funnel: Funnel
|
|
16
|
+
refresh: () => void
|
|
17
|
+
focusedKey: string | null
|
|
18
|
+
setFocusedKey: (key: string | null) => void
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
type Channel = Snapshot["channels"][number]
|
|
22
|
+
|
|
23
|
+
const fieldKey = (name: string, field: string): string => `channels::${name}::${field}`
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Channel inspector — one Card per channel. Connectors live nested inside the
|
|
27
|
+
* channel and are managed in the connectors view; here only the channel's
|
|
28
|
+
* name and id (read-only) are shown along with a count of nested connectors.
|
|
29
|
+
*/
|
|
30
|
+
export function ChannelsView(props: Props) {
|
|
31
|
+
const channels = props.snapshot.channels
|
|
32
|
+
|
|
33
|
+
const commit = (channel: Channel, field: string, raw: string): void => {
|
|
34
|
+
try {
|
|
35
|
+
if (field === "name") {
|
|
36
|
+
const next = raw.trim()
|
|
37
|
+
|
|
38
|
+
if (next && next !== channel.name) props.funnel.channels.rename(channel.name, next)
|
|
39
|
+
}
|
|
40
|
+
} catch (error) {
|
|
41
|
+
props.funnel.logger.error(error instanceof Error ? error.message : String(error))
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
props.setFocusedKey(null)
|
|
45
|
+
props.refresh()
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const removeChannel = (name: string): void => {
|
|
49
|
+
try {
|
|
50
|
+
props.funnel.channels.remove(name)
|
|
51
|
+
} catch (error) {
|
|
52
|
+
props.funnel.logger.error(error instanceof Error ? error.message : String(error))
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
props.setFocusedKey(null)
|
|
56
|
+
props.refresh()
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const addChannel = (): void => {
|
|
60
|
+
const name = uniqueName(
|
|
61
|
+
channels.map((c) => c.name),
|
|
62
|
+
"channel",
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
const created = props.funnel.channels.add({ name })
|
|
67
|
+
props.setFocusedKey(fieldKey(created.name, "name"))
|
|
68
|
+
} catch (error) {
|
|
69
|
+
props.funnel.logger.error(error instanceof Error ? error.message : String(error))
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
props.refresh()
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return (
|
|
76
|
+
<ViewShell>
|
|
77
|
+
<PanelHeader label="channels" count={channels.length} />
|
|
78
|
+
|
|
79
|
+
{channels.length === 0 ? (
|
|
80
|
+
<EmptyState message="(none — use the button below to add one)" />
|
|
81
|
+
) : (
|
|
82
|
+
channels.map((channel) => (
|
|
83
|
+
<Card key={channel.id} title={channel.name} onDelete={() => removeChannel(channel.name)}>
|
|
84
|
+
<EditableField
|
|
85
|
+
label="name"
|
|
86
|
+
initialValue={channel.name}
|
|
87
|
+
focused={props.focusedKey === fieldKey(channel.name, "name")}
|
|
88
|
+
onFocus={() => props.setFocusedKey(fieldKey(channel.name, "name"))}
|
|
89
|
+
onCommit={(raw) => commit(channel, "name", raw)}
|
|
90
|
+
/>
|
|
91
|
+
<ReadonlyField label="id" value={channel.id} />
|
|
92
|
+
<ReadonlyField label="delivery" value={channel.delivery} />
|
|
93
|
+
<ReadonlyField
|
|
94
|
+
label="connectors"
|
|
95
|
+
value={
|
|
96
|
+
channel.connectors.length > 0
|
|
97
|
+
? channel.connectors.map((c) => `${c.name}:${c.type}`).join(", ")
|
|
98
|
+
: "(none)"
|
|
99
|
+
}
|
|
100
|
+
/>
|
|
101
|
+
</Card>
|
|
102
|
+
))
|
|
103
|
+
)}
|
|
104
|
+
|
|
105
|
+
<AddRow label="add channel" onClick={addChannel} />
|
|
106
|
+
</ViewShell>
|
|
107
|
+
)
|
|
108
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { AddRow } from "@/tui/components/add-row"
|
|
2
|
+
import { Card } from "@/tui/components/card"
|
|
3
|
+
import { EmptyState } from "@/tui/components/empty-state"
|
|
4
|
+
import { PanelHeader } from "@/tui/components/panel-header"
|
|
5
|
+
import { ReadonlyField } from "@/tui/components/readonly-field"
|
|
6
|
+
import { HasciiButton } from "@/tui/components/ui/hascii/button"
|
|
7
|
+
import { ViewShell } from "@/tui/components/view-shell"
|
|
8
|
+
import { funnel } from "@/tui/theme"
|
|
9
|
+
import type { Snapshot } from "@/tui/types"
|
|
10
|
+
import { uniqueName } from "@/tui/unique-name"
|
|
11
|
+
import type { Funnel } from "@/funnel"
|
|
12
|
+
|
|
13
|
+
type Props = {
|
|
14
|
+
snapshot: Snapshot
|
|
15
|
+
funnel: Funnel
|
|
16
|
+
refresh: () => void
|
|
17
|
+
focusedKey: string | null
|
|
18
|
+
setFocusedKey: (key: string | null) => void
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
type Connector = Snapshot["connectors"][number]
|
|
22
|
+
type ConnectorType = Connector["type"]
|
|
23
|
+
|
|
24
|
+
const formatTimestamp = (iso: string | undefined): string => {
|
|
25
|
+
if (!iso) return "—"
|
|
26
|
+
|
|
27
|
+
const d = new Date(iso)
|
|
28
|
+
|
|
29
|
+
if (Number.isNaN(d.getTime())) return "—"
|
|
30
|
+
|
|
31
|
+
const yyyy = d.getFullYear()
|
|
32
|
+
const mm = String(d.getMonth() + 1).padStart(2, "0")
|
|
33
|
+
const dd = String(d.getDate()).padStart(2, "0")
|
|
34
|
+
const hh = String(d.getHours()).padStart(2, "0")
|
|
35
|
+
const mn = String(d.getMinutes()).padStart(2, "0")
|
|
36
|
+
|
|
37
|
+
return `${yyyy}-${mm}-${dd} ${hh}:${mn}`
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Channel-scoped connector inspector. Reads `funnel.channels.listAllConnectors()`
|
|
42
|
+
* (already flattened with channelName / channelId tags) and lets the user delete
|
|
43
|
+
* each connector or quickly add a new one to the first available channel via the
|
|
44
|
+
* AddRow buttons. Editing values is intentionally read-only — token / pollInterval
|
|
45
|
+
* mutation belongs to `fnl channels <ch> connectors set <conn> ...` because the
|
|
46
|
+
* same connector name can exist in multiple channels and inline edits would have
|
|
47
|
+
* to disambiguate.
|
|
48
|
+
*/
|
|
49
|
+
export function ConnectorsView(props: Props) {
|
|
50
|
+
const connectors = props.snapshot.connectors
|
|
51
|
+
const channels = props.snapshot.channels
|
|
52
|
+
const targetChannel = channels[0] ?? null
|
|
53
|
+
|
|
54
|
+
const logError = (error: unknown): void => {
|
|
55
|
+
props.funnel.logger.error(error instanceof Error ? error.message : String(error))
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const removeConnector = (connector: Connector): void => {
|
|
59
|
+
props.funnel.listeners.stop(connector.channelName, connector.name).catch(logError)
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
props.funnel.channels.removeConnector(connector.channelName, connector.name)
|
|
63
|
+
} catch (error) {
|
|
64
|
+
logError(error)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
props.refresh()
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const addConnector = (type: ConnectorType): void => {
|
|
71
|
+
if (!targetChannel) {
|
|
72
|
+
logError(new Error("add a channel first before creating a connector"))
|
|
73
|
+
|
|
74
|
+
return
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const existingNames = connectors
|
|
78
|
+
.filter((c) => c.channelId === targetChannel.id)
|
|
79
|
+
.map((c) => c.name)
|
|
80
|
+
const name = uniqueName(existingNames, type)
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
if (type === "slack") {
|
|
84
|
+
props.funnel.channels.addConnector(targetChannel.name, {
|
|
85
|
+
type: "slack",
|
|
86
|
+
name,
|
|
87
|
+
botToken: "xoxb-PLACEHOLDER",
|
|
88
|
+
appToken: "xapp-PLACEHOLDER",
|
|
89
|
+
})
|
|
90
|
+
} else if (type === "gh") {
|
|
91
|
+
props.funnel.channels.addConnector(targetChannel.name, { type: "gh", name })
|
|
92
|
+
} else if (type === "discord") {
|
|
93
|
+
props.funnel.channels.addConnector(targetChannel.name, {
|
|
94
|
+
type: "discord",
|
|
95
|
+
name,
|
|
96
|
+
botToken: "PLACEHOLDER-PLACEHOLDER",
|
|
97
|
+
})
|
|
98
|
+
} else {
|
|
99
|
+
props.funnel.channels.addConnector(targetChannel.name, { type: "schedule", name })
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
props.funnel.listeners.start(targetChannel.name, name).catch(logError)
|
|
103
|
+
} catch (error) {
|
|
104
|
+
logError(error)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
props.refresh()
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return (
|
|
111
|
+
<ViewShell>
|
|
112
|
+
<PanelHeader label="connectors" count={connectors.length} />
|
|
113
|
+
|
|
114
|
+
{connectors.length === 0 ? (
|
|
115
|
+
<EmptyState message="(none — add via the buttons below or `fnl channels <ch> connectors add ...`)" />
|
|
116
|
+
) : (
|
|
117
|
+
connectors.map((connector) => (
|
|
118
|
+
<Card key={`${connector.channelId}::${connector.id}`} title={connector.name}>
|
|
119
|
+
<ReadonlyField label="channel" value={connector.channelName} />
|
|
120
|
+
<ReadonlyField label="type" value={connector.type} />
|
|
121
|
+
<ReadonlyField label="id" value={connector.id} />
|
|
122
|
+
{connector.type === "slack" ? (
|
|
123
|
+
<>
|
|
124
|
+
<ReadonlyField label="bot-token" value={connector.botToken} />
|
|
125
|
+
<ReadonlyField label="app-token" value={connector.appToken} />
|
|
126
|
+
</>
|
|
127
|
+
) : null}
|
|
128
|
+
{connector.type === "gh" ? (
|
|
129
|
+
<ReadonlyField label="poll" value={String(connector.pollInterval ?? 60)} />
|
|
130
|
+
) : null}
|
|
131
|
+
{connector.type === "discord" ? (
|
|
132
|
+
<ReadonlyField label="bot-token" value={connector.botToken} />
|
|
133
|
+
) : null}
|
|
134
|
+
{connector.type === "schedule" ? (
|
|
135
|
+
<ReadonlyField label="entries" value={String(connector.entries.length)} />
|
|
136
|
+
) : null}
|
|
137
|
+
<text fg={funnel.faint}>{`created ${formatTimestamp(connector.createdAt)}`}</text>
|
|
138
|
+
<box style={{ flexDirection: "row", justifyContent: "space-between" }}>
|
|
139
|
+
<text fg={funnel.faint}>{`updated ${formatTimestamp(connector.updatedAt)}`}</text>
|
|
140
|
+
<HasciiButton
|
|
141
|
+
variant="destructive"
|
|
142
|
+
size="sm"
|
|
143
|
+
onPress={() => removeConnector(connector)}
|
|
144
|
+
>
|
|
145
|
+
delete
|
|
146
|
+
</HasciiButton>
|
|
147
|
+
</box>
|
|
148
|
+
</Card>
|
|
149
|
+
))
|
|
150
|
+
)}
|
|
151
|
+
|
|
152
|
+
{targetChannel ? (
|
|
153
|
+
<text fg={funnel.faint}>{`add target channel: ${targetChannel.name}`}</text>
|
|
154
|
+
) : (
|
|
155
|
+
<text fg={funnel.warn}>add a channel first to enable the buttons below</text>
|
|
156
|
+
)}
|
|
157
|
+
|
|
158
|
+
<AddRow label="add slack" onClick={() => addConnector("slack")} />
|
|
159
|
+
<AddRow label="add gh" onClick={() => addConnector("gh")} />
|
|
160
|
+
<AddRow label="add discord" onClick={() => addConnector("discord")} />
|
|
161
|
+
<AddRow label="add schedule" onClick={() => addConnector("schedule")} />
|
|
162
|
+
</ViewShell>
|
|
163
|
+
)
|
|
164
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/** @jsxImportSource @opentui/react */
|
|
2
|
+
import { DetailBar } from "@/tui/components/detail-bar"
|
|
3
|
+
import { HasciiSeparator } from "@/tui/components/ui/hascii/separator"
|
|
4
|
+
import { EmptyState } from "@/tui/components/empty-state"
|
|
5
|
+
import { Keymap } from "@/tui/components/keymap"
|
|
6
|
+
import { PanelHeader } from "@/tui/components/panel-header"
|
|
7
|
+
import { ViewShell } from "@/tui/components/view-shell"
|
|
8
|
+
import { funnel } from "@/tui/theme"
|
|
9
|
+
import { useHasciiTheme } from "@/tui/utils/hascii/theme-context"
|
|
10
|
+
import type { StreamEvent, StreamStatus } from "@/tui/types"
|
|
11
|
+
|
|
12
|
+
type Props = {
|
|
13
|
+
events: StreamEvent[]
|
|
14
|
+
filter: string
|
|
15
|
+
selectedIndex: number
|
|
16
|
+
streamStatus: StreamStatus
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const streamLabel = (status: StreamStatus): string => {
|
|
20
|
+
if (status === "open") return "live"
|
|
21
|
+
if (status === "connecting") return "connecting…"
|
|
22
|
+
if (status === "closed") return "reconnecting…"
|
|
23
|
+
|
|
24
|
+
return "offline"
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const formatTime = (ms: number): string => {
|
|
28
|
+
const date = new Date(ms)
|
|
29
|
+
const hh = String(date.getHours()).padStart(2, "0")
|
|
30
|
+
const mm = String(date.getMinutes()).padStart(2, "0")
|
|
31
|
+
const ss = String(date.getSeconds()).padStart(2, "0")
|
|
32
|
+
|
|
33
|
+
return `${hh}:${mm}:${ss}`
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const truncate = (value: string, max: number): string => {
|
|
37
|
+
const flat = value.replace(/\s+/g, " ").trim()
|
|
38
|
+
|
|
39
|
+
if (flat.length <= max) return flat
|
|
40
|
+
|
|
41
|
+
return `${flat.slice(0, max - 1)}…`
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const matches = (event: StreamEvent, filter: string): boolean => {
|
|
45
|
+
if (!filter) return true
|
|
46
|
+
|
|
47
|
+
const needle = filter.toLowerCase()
|
|
48
|
+
const haystack = [
|
|
49
|
+
event.content,
|
|
50
|
+
event.meta.connector ?? "",
|
|
51
|
+
event.meta.event_type ?? "",
|
|
52
|
+
event.meta.channel ?? "",
|
|
53
|
+
]
|
|
54
|
+
.join(" ")
|
|
55
|
+
.toLowerCase()
|
|
56
|
+
|
|
57
|
+
return haystack.includes(needle)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const tryParseJson = (value: string): unknown => {
|
|
61
|
+
try {
|
|
62
|
+
return JSON.parse(value)
|
|
63
|
+
} catch {
|
|
64
|
+
return value
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const formatJson = (value: unknown): string => {
|
|
69
|
+
try {
|
|
70
|
+
return JSON.stringify(value, null, 2)
|
|
71
|
+
} catch {
|
|
72
|
+
return String(value)
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Live event stream + detail of the selected event.
|
|
78
|
+
*
|
|
79
|
+
* The events list lives inside `ViewShell` (padded canvas) while the
|
|
80
|
+
* detail strip is a sibling `DetailBar` so its background spans the
|
|
81
|
+
* full main column edge-to-edge and reads as a distinct elevated
|
|
82
|
+
* stratum below the list.
|
|
83
|
+
*/
|
|
84
|
+
export function EventsView(props: Props) {
|
|
85
|
+
const theme = useHasciiTheme()
|
|
86
|
+
const visible = props.events.filter((event) => matches(event, props.filter))
|
|
87
|
+
const selected = visible[props.selectedIndex] ?? null
|
|
88
|
+
|
|
89
|
+
return (
|
|
90
|
+
<box style={{ flexDirection: "column", flexGrow: 1 }}>
|
|
91
|
+
<ViewShell>
|
|
92
|
+
<PanelHeader
|
|
93
|
+
label="events"
|
|
94
|
+
count={visible.length}
|
|
95
|
+
hint={[
|
|
96
|
+
streamLabel(props.streamStatus),
|
|
97
|
+
`${props.events.length} total`,
|
|
98
|
+
props.filter ? `/${props.filter}/` : null,
|
|
99
|
+
]
|
|
100
|
+
.filter((part): part is string => part !== null)
|
|
101
|
+
.join(" · ")}
|
|
102
|
+
/>
|
|
103
|
+
|
|
104
|
+
{visible.length === 0 ? (
|
|
105
|
+
<EmptyState message="(no events yet — waiting for the first one)" />
|
|
106
|
+
) : (
|
|
107
|
+
visible.map((event, index) => {
|
|
108
|
+
const isSelected = index === props.selectedIndex
|
|
109
|
+
const connector = event.meta.connector ?? "system"
|
|
110
|
+
const eventType = event.meta.event_type ?? "?"
|
|
111
|
+
|
|
112
|
+
return (
|
|
113
|
+
<text key={event.id} bg={isSelected ? theme.color.muted : undefined}>
|
|
114
|
+
<span fg={theme.color.mutedForeground}>{formatTime(event.receivedAt)}</span>
|
|
115
|
+
<span fg={funnel.faint}> </span>
|
|
116
|
+
<span fg={theme.color.mutedForeground}>{eventType.padEnd(8)}</span>
|
|
117
|
+
<span fg={funnel.faint}>{" · "}</span>
|
|
118
|
+
<span fg={isSelected ? theme.color.foreground : theme.color.foreground}>
|
|
119
|
+
{connector.padEnd(14)}
|
|
120
|
+
</span>
|
|
121
|
+
<span fg={funnel.faint}> </span>
|
|
122
|
+
<span fg={isSelected ? theme.color.foreground : theme.color.mutedForeground}>
|
|
123
|
+
{truncate(event.content, 80)}
|
|
124
|
+
</span>
|
|
125
|
+
</text>
|
|
126
|
+
)
|
|
127
|
+
})
|
|
128
|
+
)}
|
|
129
|
+
|
|
130
|
+
<Keymap
|
|
131
|
+
hints={[
|
|
132
|
+
{ key: "j/k", label: "select" },
|
|
133
|
+
{ key: "/", label: "filter" },
|
|
134
|
+
]}
|
|
135
|
+
/>
|
|
136
|
+
</ViewShell>
|
|
137
|
+
|
|
138
|
+
<DetailBar>
|
|
139
|
+
<PanelHeader label="detail" />
|
|
140
|
+
|
|
141
|
+
{!selected ? (
|
|
142
|
+
<EmptyState message="(select an event with j/k to inspect)" />
|
|
143
|
+
) : (
|
|
144
|
+
<>
|
|
145
|
+
<text>
|
|
146
|
+
<span fg={theme.color.mutedForeground}>meta: </span>
|
|
147
|
+
<span fg={theme.color.foreground}>
|
|
148
|
+
{Object.entries(selected.meta)
|
|
149
|
+
.map(([key, value]) => `${key}=${value}`)
|
|
150
|
+
.join(" ")}
|
|
151
|
+
</span>
|
|
152
|
+
</text>
|
|
153
|
+
<HasciiSeparator />
|
|
154
|
+
<text fg={theme.color.foreground}>{formatJson(tryParseJson(selected.content))}</text>
|
|
155
|
+
</>
|
|
156
|
+
)}
|
|
157
|
+
</DetailBar>
|
|
158
|
+
</box>
|
|
159
|
+
)
|
|
160
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/** @jsxImportSource @opentui/react */
|
|
2
|
+
import { Card } from "@/tui/components/card"
|
|
3
|
+
import { EmptyState } from "@/tui/components/empty-state"
|
|
4
|
+
import { Keymap } from "@/tui/components/keymap"
|
|
5
|
+
import { PanelHeader } from "@/tui/components/panel-header"
|
|
6
|
+
import { ViewShell } from "@/tui/components/view-shell"
|
|
7
|
+
import { funnel } from "@/tui/theme"
|
|
8
|
+
import { useHasciiTheme } from "@/tui/utils/hascii/theme-context"
|
|
9
|
+
import type { Snapshot, StreamEvent } from "@/tui/types"
|
|
10
|
+
|
|
11
|
+
type Props = {
|
|
12
|
+
snapshot: Snapshot
|
|
13
|
+
events: StreamEvent[]
|
|
14
|
+
selectedIndex: number
|
|
15
|
+
busy: boolean
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const eventCountBy = (events: StreamEvent[], connectorName: string): number => {
|
|
19
|
+
let count = 0
|
|
20
|
+
|
|
21
|
+
for (const event of events) {
|
|
22
|
+
if (event.meta.connector === connectorName) count += 1
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return count
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Listener registry — one Card per listener. The Card title shows the
|
|
30
|
+
* listener's name; inside, a single status line carries the alive
|
|
31
|
+
* dot, the connector type, and the event count. Cursor selection is
|
|
32
|
+
* shown via the Card's `selected` accent. Listeners are runtime
|
|
33
|
+
* entities derived from connectors, so there is no add path here —
|
|
34
|
+
* register / remove a connector instead.
|
|
35
|
+
*/
|
|
36
|
+
export function ListenersView(props: Props) {
|
|
37
|
+
const theme = useHasciiTheme()
|
|
38
|
+
const listeners = props.snapshot.listeners
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<ViewShell>
|
|
42
|
+
<PanelHeader
|
|
43
|
+
label="listeners"
|
|
44
|
+
count={listeners.length}
|
|
45
|
+
hint={props.busy ? "working…" : undefined}
|
|
46
|
+
/>
|
|
47
|
+
|
|
48
|
+
{!props.snapshot.daemonReachable ? (
|
|
49
|
+
<EmptyState message="(gateway daemon offline — press G to start it)" />
|
|
50
|
+
) : listeners.length === 0 ? (
|
|
51
|
+
<EmptyState message="(no listeners — register a connector first)" />
|
|
52
|
+
) : (
|
|
53
|
+
listeners.map((entry, index) => {
|
|
54
|
+
const aliveColor = entry.alive ? funnel.alive : funnel.dead
|
|
55
|
+
const count = eventCountBy(props.events, entry.name)
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<Card key={entry.name} title={entry.name} selected={index === props.selectedIndex}>
|
|
59
|
+
<text>
|
|
60
|
+
<span fg={aliveColor}>{entry.alive ? "●" : "○"}</span>
|
|
61
|
+
<span fg={funnel.faint}> </span>
|
|
62
|
+
<span fg={theme.color.mutedForeground}>{entry.type}</span>
|
|
63
|
+
{count > 0 ? <span fg={theme.color.mutedForeground}>{` ${count}↓`}</span> : null}
|
|
64
|
+
</text>
|
|
65
|
+
</Card>
|
|
66
|
+
)
|
|
67
|
+
})
|
|
68
|
+
)}
|
|
69
|
+
|
|
70
|
+
<Keymap
|
|
71
|
+
hints={[
|
|
72
|
+
{ key: "j/k", label: "select" },
|
|
73
|
+
{ key: "s", label: "start" },
|
|
74
|
+
{ key: "x", label: "stop" },
|
|
75
|
+
{ key: "R", label: "restart" },
|
|
76
|
+
]}
|
|
77
|
+
/>
|
|
78
|
+
</ViewShell>
|
|
79
|
+
)
|
|
80
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/** @jsxImportSource @opentui/react */
|
|
2
|
+
import { AddRow } from "@/tui/components/add-row"
|
|
3
|
+
import { Card } from "@/tui/components/card"
|
|
4
|
+
import { EditableField } from "@/tui/components/editable-field"
|
|
5
|
+
import { EmptyState } from "@/tui/components/empty-state"
|
|
6
|
+
import { Keymap } from "@/tui/components/keymap"
|
|
7
|
+
import { PanelHeader } from "@/tui/components/panel-header"
|
|
8
|
+
import { ViewShell } from "@/tui/components/view-shell"
|
|
9
|
+
import type { Snapshot } from "@/tui/types"
|
|
10
|
+
import { uniqueName } from "@/tui/unique-name"
|
|
11
|
+
import type { Funnel } from "@/funnel"
|
|
12
|
+
|
|
13
|
+
type Props = {
|
|
14
|
+
snapshot: Snapshot
|
|
15
|
+
selectedIndex: number
|
|
16
|
+
funnel: Funnel
|
|
17
|
+
refresh: () => void
|
|
18
|
+
focusedKey: string | null
|
|
19
|
+
setFocusedKey: (key: string | null) => void
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
type Profile = Snapshot["profiles"][number]
|
|
23
|
+
|
|
24
|
+
const fieldKey = (name: string, field: string): string => `profiles::${name}::${field}`
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Profile list — one Card per profile. Selection (j/k cursor) shows the
|
|
28
|
+
* `▏` primary rule via the Card's `selected` prop; pressing `c`
|
|
29
|
+
* launches Claude Code with the selected profile.
|
|
30
|
+
*
|
|
31
|
+
* `+ add profile` at the foot creates a new profile pointed at the
|
|
32
|
+
* first existing channel (or an empty string if there are none, which
|
|
33
|
+
* the user must then edit before launching).
|
|
34
|
+
*/
|
|
35
|
+
export function ProfilesView(props: Props) {
|
|
36
|
+
const profiles = props.snapshot.profiles
|
|
37
|
+
const channels = props.snapshot.channels
|
|
38
|
+
|
|
39
|
+
const commit = (profile: Profile, field: string, raw: string): void => {
|
|
40
|
+
try {
|
|
41
|
+
if (field === "name") {
|
|
42
|
+
const next = raw.trim()
|
|
43
|
+
|
|
44
|
+
if (next && next !== profile.name) props.funnel.profiles.rename(profile.name, next)
|
|
45
|
+
} else if (field === "channel") {
|
|
46
|
+
const next = raw.trim()
|
|
47
|
+
|
|
48
|
+
if (next) props.funnel.profiles.update(profile.name, { channelId: next })
|
|
49
|
+
} else if (field === "path") {
|
|
50
|
+
const next = raw.trim()
|
|
51
|
+
|
|
52
|
+
if (next) props.funnel.profiles.update(profile.name, { path: next })
|
|
53
|
+
} else if (field === "sub-agent") {
|
|
54
|
+
const next = raw.trim()
|
|
55
|
+
|
|
56
|
+
if (next) props.funnel.profiles.update(profile.name, { subAgent: next })
|
|
57
|
+
}
|
|
58
|
+
} catch (error) {
|
|
59
|
+
props.funnel.logger.error(error instanceof Error ? error.message : String(error))
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
props.setFocusedKey(null)
|
|
63
|
+
props.refresh()
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const removeProfile = (name: string): void => {
|
|
67
|
+
try {
|
|
68
|
+
props.funnel.profiles.remove(name)
|
|
69
|
+
} catch (error) {
|
|
70
|
+
props.funnel.logger.error(error instanceof Error ? error.message : String(error))
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
props.setFocusedKey(null)
|
|
74
|
+
props.refresh()
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const addProfile = (): void => {
|
|
78
|
+
const name = uniqueName(
|
|
79
|
+
profiles.map((p) => p.name),
|
|
80
|
+
"profile",
|
|
81
|
+
)
|
|
82
|
+
const channelId = channels[0]?.name ?? ""
|
|
83
|
+
|
|
84
|
+
try {
|
|
85
|
+
props.funnel.profiles.add({ name, path: "", subAgent: "", channelId })
|
|
86
|
+
props.setFocusedKey(fieldKey(name, "name"))
|
|
87
|
+
} catch (error) {
|
|
88
|
+
props.funnel.logger.error(error instanceof Error ? error.message : String(error))
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
props.refresh()
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return (
|
|
95
|
+
<ViewShell>
|
|
96
|
+
<PanelHeader label="profiles" count={profiles.length} />
|
|
97
|
+
|
|
98
|
+
{profiles.length === 0 ? (
|
|
99
|
+
<EmptyState message="(none — use the button below to add one)" />
|
|
100
|
+
) : (
|
|
101
|
+
profiles.map((profile, index) => (
|
|
102
|
+
<Card
|
|
103
|
+
key={profile.name}
|
|
104
|
+
title={profile.name}
|
|
105
|
+
selected={index === props.selectedIndex}
|
|
106
|
+
onDelete={() => removeProfile(profile.name)}
|
|
107
|
+
>
|
|
108
|
+
<EditableField
|
|
109
|
+
label="name"
|
|
110
|
+
initialValue={profile.name}
|
|
111
|
+
focused={props.focusedKey === fieldKey(profile.name, "name")}
|
|
112
|
+
onFocus={() => props.setFocusedKey(fieldKey(profile.name, "name"))}
|
|
113
|
+
onCommit={(raw) => commit(profile, "name", raw)}
|
|
114
|
+
/>
|
|
115
|
+
<EditableField
|
|
116
|
+
label="path"
|
|
117
|
+
initialValue={profile.path}
|
|
118
|
+
focused={props.focusedKey === fieldKey(profile.name, "path")}
|
|
119
|
+
onFocus={() => props.setFocusedKey(fieldKey(profile.name, "path"))}
|
|
120
|
+
onCommit={(raw) => commit(profile, "path", raw)}
|
|
121
|
+
placeholder="repository path"
|
|
122
|
+
/>
|
|
123
|
+
<EditableField
|
|
124
|
+
label="sub-agent"
|
|
125
|
+
initialValue={profile.subAgent}
|
|
126
|
+
focused={props.focusedKey === fieldKey(profile.name, "sub-agent")}
|
|
127
|
+
onFocus={() => props.setFocusedKey(fieldKey(profile.name, "sub-agent"))}
|
|
128
|
+
onCommit={(raw) => commit(profile, "sub-agent", raw)}
|
|
129
|
+
placeholder="claude --agent value"
|
|
130
|
+
/>
|
|
131
|
+
<EditableField
|
|
132
|
+
label="channel"
|
|
133
|
+
initialValue={profile.channelId}
|
|
134
|
+
focused={props.focusedKey === fieldKey(profile.name, "channel")}
|
|
135
|
+
onFocus={() => props.setFocusedKey(fieldKey(profile.name, "channel"))}
|
|
136
|
+
onCommit={(raw) => commit(profile, "channel", raw)}
|
|
137
|
+
/>
|
|
138
|
+
</Card>
|
|
139
|
+
))
|
|
140
|
+
)}
|
|
141
|
+
|
|
142
|
+
<AddRow label="add profile" onClick={addProfile} />
|
|
143
|
+
|
|
144
|
+
<Keymap
|
|
145
|
+
hints={[
|
|
146
|
+
{ key: "j/k", label: "select" },
|
|
147
|
+
{ key: "c", label: "launch" },
|
|
148
|
+
]}
|
|
149
|
+
/>
|
|
150
|
+
</ViewShell>
|
|
151
|
+
)
|
|
152
|
+
}
|