@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,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
|
+
}
|