khal-os 1.260324.2
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/.env.example +23 -0
- package/.genie/mailbox/cli-sent.jsonl +3 -0
- package/.genie/mailbox/ds1-wave2-engineer-1.json +15 -0
- package/.genie/mailbox/ds1-wave2-engineer-2.json +15 -0
- package/.genie/mailbox/ds1-wave2-engineer-3.json +15 -0
- package/.genie/state/os-observability.json +39 -0
- package/.genie/state/tmux-control-mode-terminal.json +28 -0
- package/.genie/wishes/genieos-one-theme/WISH.md +417 -0
- package/.genie/wishes/workos-prod-rbac/WISH.md +345 -0
- package/.github/workflows/ci.yml +39 -0
- package/.github/workflows/release.yml +78 -0
- package/.github/workflows/version.yml +122 -0
- package/.husky/pre-commit +1 -0
- package/.pnpm-approve-builds.json +1 -0
- package/CLAUDE.md +117 -0
- package/LICENSE +21 -0
- package/README.md +38 -0
- package/biome.json +124 -0
- package/bun.lock +1249 -0
- package/docs/workos-setup.md +116 -0
- package/ecosystem.config.cjs +26 -0
- package/instrumentation.ts +8 -0
- package/knip.json +35 -0
- package/nats.conf +7 -0
- package/next.config.ts +25 -0
- package/package.json +78 -0
- package/packages/dev3000-app/components.ts +12 -0
- package/packages/dev3000-app/manifest.ts +19 -0
- package/packages/dev3000-app/package.json +23 -0
- package/packages/dev3000-app/views/dev3000/Dev3000App.tsx +758 -0
- package/packages/dev3000-app/views/dev3000/ErrorsPanel.tsx +160 -0
- package/packages/dev3000-app/views/dev3000/dev3000-context.tsx +21 -0
- package/packages/dev3000-app/views/dev3000/index.ts +4 -0
- package/packages/dev3000-app/views/dev3000/schema.ts +55 -0
- package/packages/dev3000-app/views/dev3000/service/index.ts +358 -0
- package/packages/dev3000-app/views/dev3000/service/runtime +1 -0
- package/packages/dev3000-app/views/dev3000/subjects.ts +9 -0
- package/packages/dev3000-app/views/dev3000/types.ts +77 -0
- package/packages/files-app/components.ts +12 -0
- package/packages/files-app/manifest.ts +19 -0
- package/packages/files-app/package.json +23 -0
- package/packages/files-app/views/files/ContextMenu.tsx +151 -0
- package/packages/files-app/views/files/DeleteConfirmDialog.tsx +39 -0
- package/packages/files-app/views/files/FileItem.tsx +128 -0
- package/packages/files-app/views/files/FilesApp.tsx +509 -0
- package/packages/files-app/views/files/FilesListView.tsx +201 -0
- package/packages/files-app/views/files/FilesToolbar.tsx +117 -0
- package/packages/files-app/views/files/GridView.tsx +90 -0
- package/packages/files-app/views/files/InlineInput.tsx +131 -0
- package/packages/files-app/views/files/UploadOverlay.tsx +61 -0
- package/packages/files-app/views/files/schema.ts +49 -0
- package/packages/files-app/views/files/service/index.ts +184 -0
- package/packages/files-app/views/files/service/runtime +1 -0
- package/packages/files-app/views/files/use-files.ts +201 -0
- package/packages/files-app/views/files/use-upload.ts +105 -0
- package/packages/genie-app/components.ts +12 -0
- package/packages/genie-app/lib/subjects.ts +87 -0
- package/packages/genie-app/manifest.ts +19 -0
- package/packages/genie-app/package.json +29 -0
- package/packages/genie-app/views/genie/service/agent-lifecycle.ts +136 -0
- package/packages/genie-app/views/genie/service/cli.ts +114 -0
- package/packages/genie-app/views/genie/service/comms.ts +141 -0
- package/packages/genie-app/views/genie/service/directory.ts +167 -0
- package/packages/genie-app/views/genie/service/index.ts +219 -0
- package/packages/genie-app/views/genie/service/system.ts +123 -0
- package/packages/genie-app/views/genie/service/teams.ts +191 -0
- package/packages/genie-app/views/genie/service/terminal-proxy.ts +184 -0
- package/packages/genie-app/views/genie/service/tmux-control.ts +318 -0
- package/packages/genie-app/views/genie/service/wishes.ts +270 -0
- package/packages/genie-app/views/genie/ui/GenieApp.tsx +5 -0
- package/packages/genie-app/views/genie/ui/PaneCard.tsx +307 -0
- package/packages/genie-app/views/genie/ui/Sidebar.tsx +212 -0
- package/packages/genie-app/views/genie/ui/TabBar.tsx +70 -0
- package/packages/genie-app/views/genie/ui/WorkspaceCanvas.tsx +343 -0
- package/packages/genie-app/views/genie/ui/XTermPane.tsx +306 -0
- package/packages/genie-app/views/genie/ui/hooks/useNatsAction.ts +54 -0
- package/packages/genie-app/views/genie/ui/hooks/useNatsRequest.ts +68 -0
- package/packages/genie-app/views/genie/ui/panels/AgentsPanel.tsx +399 -0
- package/packages/genie-app/views/genie/ui/panels/ChatPanel.tsx +351 -0
- package/packages/genie-app/views/genie/ui/panels/SystemPanel.tsx +195 -0
- package/packages/genie-app/views/genie/ui/panels/TeamsPanel.tsx +560 -0
- package/packages/genie-app/views/genie/ui/panels/WishesPanel.tsx +424 -0
- package/packages/nats-viewer-app/components.ts +12 -0
- package/packages/nats-viewer-app/manifest.ts +18 -0
- package/packages/nats-viewer-app/package.json +14 -0
- package/packages/nats-viewer-app/views/nats-viewer/ActiveSubs.tsx +34 -0
- package/packages/nats-viewer-app/views/nats-viewer/MessageLog.tsx +247 -0
- package/packages/nats-viewer-app/views/nats-viewer/NatsViewer.tsx +209 -0
- package/packages/nats-viewer-app/views/nats-viewer/PublishPanel.tsx +111 -0
- package/packages/nats-viewer-app/views/nats-viewer/RequestPanel.tsx +165 -0
- package/packages/nats-viewer-app/views/nats-viewer/Sidebar.tsx +59 -0
- package/packages/nats-viewer-app/views/nats-viewer/SubjectCatalog.tsx +63 -0
- package/packages/nats-viewer-app/views/nats-viewer/SubscribeInput.tsx +59 -0
- package/packages/nats-viewer-app/views/nats-viewer/index.ts +5 -0
- package/packages/nats-viewer-app/views/nats-viewer/nats-viewer-context.tsx +31 -0
- package/packages/nats-viewer-app/views/nats-viewer/types.ts +7 -0
- package/packages/nats-viewer-app/views/nats-viewer/use-message-buffer.ts +55 -0
- package/packages/os-cli/package.json +18 -0
- package/packages/os-cli/src/commands/events.ts +176 -0
- package/packages/os-cli/src/commands/logs.ts +96 -0
- package/packages/os-cli/src/commands/status.ts +53 -0
- package/packages/os-cli/src/commands/traces.ts +115 -0
- package/packages/os-cli/src/index.ts +15 -0
- package/packages/os-cli/src/lib/formatter.ts +123 -0
- package/packages/os-cli/src/lib/nats.ts +16 -0
- package/packages/os-cli/src/lib/trace-tree.ts +144 -0
- package/packages/os-cli/tsconfig.json +12 -0
- package/packages/os-sdk/package.json +27 -0
- package/packages/os-sdk/src/api/handler.ts +67 -0
- package/packages/os-sdk/src/config.ts +68 -0
- package/packages/os-sdk/src/db/factory.test.ts +42 -0
- package/packages/os-sdk/src/db/factory.ts +72 -0
- package/packages/os-sdk/src/db/migrate.ts +140 -0
- package/packages/os-sdk/src/db/provision.ts +44 -0
- package/packages/os-sdk/src/index.ts +36 -0
- package/packages/os-sdk/src/service/console-intercept.ts +60 -0
- package/packages/os-sdk/src/service/logger.ts +88 -0
- package/packages/os-sdk/src/service/o11y-streams.ts +88 -0
- package/packages/os-sdk/src/service/runtime.ts +259 -0
- package/packages/os-sdk/src/service/trace.ts +71 -0
- package/packages/os-sdk/tsconfig.json +16 -0
- package/packages/os-ui/package.json +13 -0
- package/packages/os-ui/src/index.ts +29 -0
- package/packages/os-ui/src/server.ts +4 -0
- package/packages/os-ui/tsconfig.json +19 -0
- package/packages/settings-app/components.ts +12 -0
- package/packages/settings-app/manifest.ts +18 -0
- package/packages/settings-app/package.json +14 -0
- package/packages/settings-app/views/settings/Settings.tsx +492 -0
- package/packages/terminal-app/components.ts +12 -0
- package/packages/terminal-app/manifest.ts +20 -0
- package/packages/terminal-app/package.json +23 -0
- package/packages/terminal-app/views/terminal/schema.ts +82 -0
- package/packages/terminal-app/views/terminal/service/index.ts +133 -0
- package/packages/terminal-app/views/terminal/service/runtime +1 -0
- package/packages/terminal-app/views/terminal/service/session.ts +290 -0
- package/packages/terminal-app/views/terminal/service/shell-hooks/bashrc-hook.sh +21 -0
- package/packages/terminal-app/views/terminal/types.ts +26 -0
- package/packages/terminal-app/views/terminal/ui/MultiTerminalApp.tsx +615 -0
- package/packages/terminal-app/views/terminal/ui/SplitDragHandle.tsx +91 -0
- package/packages/terminal-app/views/terminal/ui/SplitPaneRenderer.tsx +112 -0
- package/packages/terminal-app/views/terminal/ui/TerminalPane.tsx +478 -0
- package/packages/terminal-app/views/terminal/ui/TerminalTabBar.tsx +131 -0
- package/pnpm-workspace.yaml +9 -0
- package/postcss.config.mjs +7 -0
- package/public/file.svg +1 -0
- package/public/globe.svg +1 -0
- package/public/icons/code-server.svg +6 -0
- package/public/icons/default.svg +5 -0
- package/public/icons/dusk/1password.svg +1 -0
- package/public/icons/dusk/activity_monitor.svg +1 -0
- package/public/icons/dusk/app_store.svg +1 -0
- package/public/icons/dusk/atom.svg +1 -0
- package/public/icons/dusk/brave.svg +1 -0
- package/public/icons/dusk/calculator.svg +1 -0
- package/public/icons/dusk/calendar.svg +1 -0
- package/public/icons/dusk/chrome.svg +1 -0
- package/public/icons/dusk/chrome2.svg +1 -0
- package/public/icons/dusk/dashboard.svg +13 -0
- package/public/icons/dusk/discord.svg +1 -0
- package/public/icons/dusk/dropbox.svg +1 -0
- package/public/icons/dusk/electron.svg +1 -0
- package/public/icons/dusk/figma.svg +1 -0
- package/public/icons/dusk/finder.svg +1 -0
- package/public/icons/dusk/finder2.svg +1 -0
- package/public/icons/dusk/finder3.svg +1 -0
- package/public/icons/dusk/firefox.svg +1 -0
- package/public/icons/dusk/framer.svg +1 -0
- package/public/icons/dusk/gimp.svg +1 -0
- package/public/icons/dusk/github_desktop.svg +1 -0
- package/public/icons/dusk/hyper.svg +1 -0
- package/public/icons/dusk/hyper3.svg +1 -0
- package/public/icons/dusk/intellij.svg +1 -0
- package/public/icons/dusk/iterm2.svg +1 -0
- package/public/icons/dusk/itunes.svg +1 -0
- package/public/icons/dusk/mail.svg +1 -0
- package/public/icons/dusk/messenger.svg +1 -0
- package/public/icons/dusk/mongodb.svg +1 -0
- package/public/icons/dusk/notes.svg +1 -0
- package/public/icons/dusk/notion.svg +1 -0
- package/public/icons/dusk/obs.svg +1 -0
- package/public/icons/dusk/pages.svg +1 -0
- package/public/icons/dusk/photos.svg +1 -0
- package/public/icons/dusk/postman.svg +1 -0
- package/public/icons/dusk/preview.svg +1 -0
- package/public/icons/dusk/reminders.svg +1 -0
- package/public/icons/dusk/safari.svg +1 -0
- package/public/icons/dusk/sequel_pro.svg +1 -0
- package/public/icons/dusk/sketch.svg +1 -0
- package/public/icons/dusk/skype.svg +1 -0
- package/public/icons/dusk/slack.svg +1 -0
- package/public/icons/dusk/slack2.svg +1 -0
- package/public/icons/dusk/spotify.svg +1 -0
- package/public/icons/dusk/steam.svg +1 -0
- package/public/icons/dusk/system_preferences.svg +1 -0
- package/public/icons/dusk/tableplus.svg +1 -0
- package/public/icons/dusk/teams.svg +1 -0
- package/public/icons/dusk/telegram.svg +1 -0
- package/public/icons/dusk/terminal.svg +1 -0
- package/public/icons/dusk/todoist.svg +1 -0
- package/public/icons/dusk/trash.svg +1 -0
- package/public/icons/dusk/trello.svg +1 -0
- package/public/icons/dusk/vivaldi.svg +1 -0
- package/public/icons/dusk/vlc.svg +1 -0
- package/public/icons/dusk/vscode.svg +1 -0
- package/public/icons/dusk/whatsapp.svg +1 -0
- package/public/icons/dusk/xeyes.svg +1 -0
- package/public/icons/dusk/zoom.svg +1 -0
- package/public/icons/files.svg +5 -0
- package/public/icons/pwa/icon-192.png +0 -0
- package/public/icons/pwa/icon-512.png +0 -0
- package/public/icons/settings.svg +14 -0
- package/public/icons/terminal.svg +5 -0
- package/public/icons/text-editor.svg +7 -0
- package/public/manifest.json +38 -0
- package/public/next.svg +1 -0
- package/public/sw.js +41 -0
- package/public/vercel.svg +1 -0
- package/public/wallpapers/default.svg +10 -0
- package/public/window.svg +1 -0
- package/scripts/generate-pwa-icons.mjs +33 -0
- package/scripts/install-nats.sh +37 -0
- package/sentry.client.config.ts +21 -0
- package/sentry.edge.config.ts +12 -0
- package/sentry.server.config.ts +12 -0
- package/src/app/api/files/download/route.ts +81 -0
- package/src/app/api/files/download-zip/route.ts +102 -0
- package/src/app/api/files/upload/route.ts +58 -0
- package/src/app/api/webhooks/workos/route.ts +98 -0
- package/src/app/auth/callback/route.ts +16 -0
- package/src/app/auth/logout/route.ts +15 -0
- package/src/app/desktop/desktop-shell.tsx +110 -0
- package/src/app/desktop/layout.tsx +8 -0
- package/src/app/desktop/page.tsx +24 -0
- package/src/app/favicon.ico +0 -0
- package/src/app/globals.css +7 -0
- package/src/app/layout.tsx +64 -0
- package/src/app/offline/page.tsx +83 -0
- package/src/app/page.tsx +5 -0
- package/src/app/standalone/[appId]/page.tsx +28 -0
- package/src/app/standalone/layout.tsx +10 -0
- package/src/components/app-icon.tsx +55 -0
- package/src/components/apps/_echo/schema.ts +14 -0
- package/src/components/apps/_echo/service/index.ts +42 -0
- package/src/components/apps/app-manifest.ts +97 -0
- package/src/components/apps/app-registry.ts +55 -0
- package/src/components/apps/dev3000/Dev3000App.tsx +224 -0
- package/src/components/apps/dev3000/ErrorsPanel.tsx +160 -0
- package/src/components/apps/dev3000/Sidebar.tsx +41 -0
- package/src/components/apps/dev3000/TimelineLog.tsx +173 -0
- package/src/components/apps/dev3000/dev3000-context.tsx +29 -0
- package/src/components/apps/dev3000/index.ts +4 -0
- package/src/components/apps/dev3000/schema.ts +48 -0
- package/src/components/apps/dev3000/service/index.ts +520 -0
- package/src/components/apps/dev3000/service/runtime +1 -0
- package/src/components/apps/dev3000/types.ts +15 -0
- package/src/components/apps/dev3000/use-message-buffer.ts +46 -0
- package/src/components/apps/files/ContextMenu.tsx +151 -0
- package/src/components/apps/files/DeleteConfirmDialog.tsx +78 -0
- package/src/components/apps/files/FileItem.tsx +128 -0
- package/src/components/apps/files/FilesApp.tsx +509 -0
- package/src/components/apps/files/FilesListView.tsx +201 -0
- package/src/components/apps/files/FilesToolbar.tsx +117 -0
- package/src/components/apps/files/GridView.tsx +90 -0
- package/src/components/apps/files/InlineInput.tsx +131 -0
- package/src/components/apps/files/UploadOverlay.tsx +61 -0
- package/src/components/apps/files/schema.ts +49 -0
- package/src/components/apps/files/service/index.ts +227 -0
- package/src/components/apps/files/service/runtime +1 -0
- package/src/components/apps/files/use-files.ts +201 -0
- package/src/components/apps/files/use-upload.ts +105 -0
- package/src/components/apps/nats-viewer/ActiveSubs.tsx +34 -0
- package/src/components/apps/nats-viewer/MessageLog.tsx +247 -0
- package/src/components/apps/nats-viewer/NatsViewer.tsx +209 -0
- package/src/components/apps/nats-viewer/PublishPanel.tsx +113 -0
- package/src/components/apps/nats-viewer/RequestPanel.tsx +167 -0
- package/src/components/apps/nats-viewer/Sidebar.tsx +62 -0
- package/src/components/apps/nats-viewer/SubjectCatalog.tsx +64 -0
- package/src/components/apps/nats-viewer/SubscribeInput.tsx +59 -0
- package/src/components/apps/nats-viewer/index.ts +5 -0
- package/src/components/apps/nats-viewer/nats-viewer-context.tsx +31 -0
- package/src/components/apps/nats-viewer/types.ts +7 -0
- package/src/components/apps/nats-viewer/use-message-buffer.ts +55 -0
- package/src/components/apps/settings/Settings.tsx +492 -0
- package/src/components/apps/terminal/schema.ts +82 -0
- package/src/components/apps/terminal/service/index.ts +189 -0
- package/src/components/apps/terminal/service/runtime +1 -0
- package/src/components/apps/terminal/service/session.ts +296 -0
- package/src/components/apps/terminal/service/shell-hooks/bashrc-hook.sh +21 -0
- package/src/components/apps/terminal/types.ts +26 -0
- package/src/components/apps/terminal/ui/MultiTerminalApp.tsx +617 -0
- package/src/components/apps/terminal/ui/SplitDragHandle.tsx +91 -0
- package/src/components/apps/terminal/ui/SplitPaneRenderer.tsx +112 -0
- package/src/components/apps/terminal/ui/TerminalPane.tsx +476 -0
- package/src/components/apps/terminal/ui/TerminalTabBar.tsx +131 -0
- package/src/components/desktop/AnimatedBackground.tsx +69 -0
- package/src/components/desktop/Desktop.tsx +79 -0
- package/src/components/desktop/DesktopBackground.tsx +16 -0
- package/src/components/desktop/DesktopIcon.tsx +49 -0
- package/src/components/desktop/ShortcutViewer.tsx +136 -0
- package/src/components/desktop/WindowRenderer.tsx +34 -0
- package/src/components/desktop/WindowSwitcher.tsx +42 -0
- package/src/components/notifications/NotificationCenter.tsx +153 -0
- package/src/components/notifications/NotificationToasts.tsx +66 -0
- package/src/components/notifications/OrphanSessionToast.tsx +293 -0
- package/src/components/os-primitives/collapsible-sidebar.tsx +226 -0
- package/src/components/os-primitives/dialog.tsx +76 -0
- package/src/components/os-primitives/empty-state.tsx +43 -0
- package/src/components/os-primitives/index.ts +21 -0
- package/src/components/os-primitives/list-view.tsx +155 -0
- package/src/components/os-primitives/property-panel.tsx +108 -0
- package/src/components/os-primitives/section-header.tsx +19 -0
- package/src/components/os-primitives/sidebar-nav.tsx +110 -0
- package/src/components/os-primitives/split-pane.tsx +146 -0
- package/src/components/os-primitives/status-badge.tsx +10 -0
- package/src/components/os-primitives/status-bar.tsx +100 -0
- package/src/components/os-primitives/toolbar.tsx +152 -0
- package/src/components/taskbar/AppLauncher.tsx +114 -0
- package/src/components/taskbar/RunningApps.tsx +71 -0
- package/src/components/taskbar/SystemTray.tsx +134 -0
- package/src/components/taskbar/Taskbar.tsx +45 -0
- package/src/components/taskbar/UserMenu.tsx +138 -0
- package/src/components/taskbar/WorkspaceSwitcher.tsx +9 -0
- package/src/components/ui/ContextMenu.tsx +130 -0
- package/src/components/ui/badge.tsx +39 -0
- package/src/components/ui/button.tsx +102 -0
- package/src/components/ui/command.tsx +165 -0
- package/src/components/ui/dropdown-menu.tsx +233 -0
- package/src/components/ui/input.tsx +48 -0
- package/src/components/ui/note.tsx +55 -0
- package/src/components/ui/separator.tsx +25 -0
- package/src/components/ui/spinner.tsx +42 -0
- package/src/components/ui/switch.tsx +36 -0
- package/src/components/ui/theme-provider.tsx +24 -0
- package/src/components/ui/theme-switcher.tsx +51 -0
- package/src/components/ui/tooltip.tsx +62 -0
- package/src/components/window/MobileWindowStack.tsx +218 -0
- package/src/components/window/SnapPreview.tsx +37 -0
- package/src/components/window/StandaloneFrame.tsx +170 -0
- package/src/components/window/Window.tsx +423 -0
- package/src/components/window/WindowContent.tsx +14 -0
- package/src/components/window/WindowControlsOverlay.tsx +89 -0
- package/src/components/window/WindowFrame.tsx +124 -0
- package/src/lib/auth/index.ts +27 -0
- package/src/lib/auth/roles.ts +50 -0
- package/src/lib/auth/types.ts +32 -0
- package/src/lib/auth/use-auth.ts +53 -0
- package/src/lib/auth/webhook-handler.ts +87 -0
- package/src/lib/auth/workos.ts +67 -0
- package/src/lib/constants.ts +1 -0
- package/src/lib/desktop/dedup.ts +57 -0
- package/src/lib/desktop/schema.ts +55 -0
- package/src/lib/files/filename-validation.ts +41 -0
- package/src/lib/files/safe-path.ts +49 -0
- package/src/lib/hooks/use-desktop-nats.ts +438 -0
- package/src/lib/hooks/use-is-mobile.ts +23 -0
- package/src/lib/hooks/use-launch-app.ts +79 -0
- package/src/lib/hooks/use-nats-notifications.ts +84 -0
- package/src/lib/hooks/use-nats.ts +60 -0
- package/src/lib/hooks/use-visual-viewport.ts +72 -0
- package/src/lib/icons/resolve-window-icon.ts +10 -0
- package/src/lib/keyboard/defaults.ts +146 -0
- package/src/lib/keyboard/types.ts +52 -0
- package/src/lib/keyboard/use-global-keybinds.ts +231 -0
- package/src/lib/nats-client.ts +255 -0
- package/src/lib/nats.ts +35 -0
- package/src/lib/notifications/schema.ts +12 -0
- package/src/lib/service-loader.ts +171 -0
- package/src/lib/subjects.ts +64 -0
- package/src/lib/utils.ts +6 -0
- package/src/lib/ws-bridge.ts +288 -0
- package/src/lib/ws-protocol.ts +53 -0
- package/src/lib/ws-server.ts +167 -0
- package/src/middleware.ts +57 -0
- package/src/stores/desktop-store.ts +112 -0
- package/src/stores/keybind-store.ts +66 -0
- package/src/stores/notification-store.ts +271 -0
- package/src/stores/theme-store.ts +25 -0
- package/src/stores/window-store.ts +294 -0
- package/src/theme/animations.css +68 -0
- package/src/theme/base.css +123 -0
- package/src/theme/controls.css +35 -0
- package/src/theme/design-tokens.css +276 -0
- package/src/theme/index.css +23 -0
- package/src/theme/menus.css +45 -0
- package/src/theme/status.css +41 -0
- package/src/theme/surfaces.css +94 -0
- package/src/theme/tailwind-map.css +138 -0
- package/src/theme/taskbar.css +25 -0
- package/src/theme/terminal.css +55 -0
- package/src/theme/typography.css +26 -0
- package/src/theme/utilities.css +156 -0
- package/src/theme/window.css +103 -0
- package/src/types/desktop-entry.ts +12 -0
- package/src/types/use-descendants.d.ts +13 -0
- package/src/types/window.ts +28 -0
- package/src/types.d.ts +9 -0
- package/tauri/Cargo.lock +5464 -0
- package/tauri/Cargo.toml +19 -0
- package/tauri/build.rs +3 -0
- package/tauri/capabilities/default.json +36 -0
- package/tauri/icons/128x128.png +0 -0
- package/tauri/icons/128x128@2x.png +0 -0
- package/tauri/icons/32x32.png +0 -0
- package/tauri/icons/icon.png +0 -0
- package/tauri/src/main.rs +396 -0
- package/tauri/tauri.conf.json +23 -0
- package/tsconfig.json +43 -0
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { Filter, Inbox, Pause, Play, Radio, Send, Trash2 } from 'lucide-react';
|
|
4
|
+
import { useCallback, useMemo, useRef, useState } from 'react';
|
|
5
|
+
import { SectionHeader, SplitPane, StatusBar, Toolbar } from '@/components/os-primitives';
|
|
6
|
+
import { SidebarNav } from '@/components/os-primitives/sidebar-nav';
|
|
7
|
+
import { useNats } from '@/lib/hooks/use-nats';
|
|
8
|
+
import { MessageLog } from './MessageLog';
|
|
9
|
+
import type { NatsViewerContextValue } from './nats-viewer-context';
|
|
10
|
+
import { NatsViewerContext } from './nats-viewer-context';
|
|
11
|
+
import { PublishPanel } from './PublishPanel';
|
|
12
|
+
import { RequestPanel } from './RequestPanel';
|
|
13
|
+
import { Sidebar } from './Sidebar';
|
|
14
|
+
import { useMessageBuffer } from './use-message-buffer';
|
|
15
|
+
|
|
16
|
+
export type { NatsViewerContextValue } from './nats-viewer-context';
|
|
17
|
+
export { useNatsViewer } from './nats-viewer-context';
|
|
18
|
+
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
// NatsViewer component
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
|
|
23
|
+
type SidebarSection = 'subjects' | 'publish' | 'request';
|
|
24
|
+
|
|
25
|
+
export function NatsViewer(_props: { windowId: string; meta?: Record<string, unknown> }) {
|
|
26
|
+
const [activeSection, setActiveSection] = useState<SidebarSection>('subjects');
|
|
27
|
+
const [paused, setPaused] = useState(false);
|
|
28
|
+
const [filter, setFilter] = useState('');
|
|
29
|
+
|
|
30
|
+
const { connected, subscribe } = useNats();
|
|
31
|
+
const buffer = useMessageBuffer();
|
|
32
|
+
|
|
33
|
+
// Track active subscriptions: subject -> unsub function
|
|
34
|
+
const unsubMapRef = useRef<Map<string, () => void>>(new Map());
|
|
35
|
+
const [subscriptions, setSubscriptions] = useState<Set<string>>(new Set());
|
|
36
|
+
|
|
37
|
+
// Stable ref for buffer.push so the subscribe callback never goes stale
|
|
38
|
+
const pushRef = useRef(buffer.push);
|
|
39
|
+
pushRef.current = buffer.push;
|
|
40
|
+
|
|
41
|
+
const addSubscription = useCallback(
|
|
42
|
+
(subject: string) => {
|
|
43
|
+
if (unsubMapRef.current.has(subject)) return;
|
|
44
|
+
|
|
45
|
+
const unsub = subscribe(subject, (data: unknown, actualSubject: string) => {
|
|
46
|
+
pushRef.current({ subject: actualSubject, payload: data, direction: 'in' });
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
unsubMapRef.current.set(subject, unsub);
|
|
50
|
+
setSubscriptions((prev) => new Set(prev).add(subject));
|
|
51
|
+
},
|
|
52
|
+
[subscribe]
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
const removeSubscription = useCallback((subject: string) => {
|
|
56
|
+
const unsub = unsubMapRef.current.get(subject);
|
|
57
|
+
if (unsub) {
|
|
58
|
+
unsub();
|
|
59
|
+
unsubMapRef.current.delete(subject);
|
|
60
|
+
}
|
|
61
|
+
setSubscriptions((prev) => {
|
|
62
|
+
const next = new Set(prev);
|
|
63
|
+
next.delete(subject);
|
|
64
|
+
return next;
|
|
65
|
+
});
|
|
66
|
+
}, []);
|
|
67
|
+
|
|
68
|
+
// Filtered count for toolbar display
|
|
69
|
+
const filteredEntries = useMemo(() => {
|
|
70
|
+
if (!filter) return buffer.entries;
|
|
71
|
+
const lower = filter.toLowerCase();
|
|
72
|
+
return buffer.entries.filter((e) => {
|
|
73
|
+
if (e.subject.toLowerCase().includes(lower)) return true;
|
|
74
|
+
try {
|
|
75
|
+
return JSON.stringify(e.payload).toLowerCase().includes(lower);
|
|
76
|
+
} catch {
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
}, [buffer.entries, filter]);
|
|
81
|
+
|
|
82
|
+
const messageCountLabel = filter
|
|
83
|
+
? `${filteredEntries.length}/${buffer.entries.length} messages`
|
|
84
|
+
: `${buffer.entries.length} messages`;
|
|
85
|
+
|
|
86
|
+
// Context value
|
|
87
|
+
const ctxValue = useMemo<NatsViewerContextValue>(
|
|
88
|
+
() => ({
|
|
89
|
+
subscriptions,
|
|
90
|
+
addSubscription,
|
|
91
|
+
removeSubscription,
|
|
92
|
+
buffer,
|
|
93
|
+
filter,
|
|
94
|
+
setFilter,
|
|
95
|
+
paused,
|
|
96
|
+
setPaused,
|
|
97
|
+
}),
|
|
98
|
+
[subscriptions, addSubscription, removeSubscription, buffer, filter, paused]
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
return (
|
|
102
|
+
<NatsViewerContext.Provider value={ctxValue}>
|
|
103
|
+
<div className="flex h-full flex-col bg-background-100">
|
|
104
|
+
<div className="flex-1 overflow-hidden">
|
|
105
|
+
<SplitPane defaultSize={250} min={180} max={360} collapseBelow={500}>
|
|
106
|
+
{/* ---- Sidebar ---- */}
|
|
107
|
+
<SplitPane.Panel className="bg-gray-alpha-50">
|
|
108
|
+
<SidebarNav label="NATS Viewer" title="NATS Viewer">
|
|
109
|
+
<SidebarNav.Group title="Subscriptions">
|
|
110
|
+
<SidebarNav.Item
|
|
111
|
+
active={activeSection === 'subjects'}
|
|
112
|
+
onClick={() => setActiveSection('subjects')}
|
|
113
|
+
icon={<Radio />}
|
|
114
|
+
>
|
|
115
|
+
Subjects
|
|
116
|
+
</SidebarNav.Item>
|
|
117
|
+
</SidebarNav.Group>
|
|
118
|
+
|
|
119
|
+
<SidebarNav.Group title="Tools">
|
|
120
|
+
<SidebarNav.Item
|
|
121
|
+
active={activeSection === 'publish'}
|
|
122
|
+
onClick={() => setActiveSection('publish')}
|
|
123
|
+
icon={<Send />}
|
|
124
|
+
>
|
|
125
|
+
Publish
|
|
126
|
+
</SidebarNav.Item>
|
|
127
|
+
<SidebarNav.Item
|
|
128
|
+
active={activeSection === 'request'}
|
|
129
|
+
onClick={() => setActiveSection('request')}
|
|
130
|
+
icon={<Inbox />}
|
|
131
|
+
>
|
|
132
|
+
Request
|
|
133
|
+
</SidebarNav.Item>
|
|
134
|
+
</SidebarNav.Group>
|
|
135
|
+
</SidebarNav>
|
|
136
|
+
|
|
137
|
+
{/* Sidebar detail panel */}
|
|
138
|
+
<div className="flex-1 overflow-y-auto border-t border-gray-alpha-200 p-3">
|
|
139
|
+
{activeSection === 'subjects' && <Sidebar />}
|
|
140
|
+
{activeSection === 'publish' && (
|
|
141
|
+
<div className="flex flex-col gap-2">
|
|
142
|
+
<SectionHeader title="Publish" description="Send a message to a subject." />
|
|
143
|
+
<PublishPanel onPublish={buffer.push} />
|
|
144
|
+
</div>
|
|
145
|
+
)}
|
|
146
|
+
{activeSection === 'request' && (
|
|
147
|
+
<div className="flex flex-col gap-2">
|
|
148
|
+
<SectionHeader title="Request" description="Send a request and view the reply." />
|
|
149
|
+
<RequestPanel onMessage={buffer.push} />
|
|
150
|
+
</div>
|
|
151
|
+
)}
|
|
152
|
+
</div>
|
|
153
|
+
</SplitPane.Panel>
|
|
154
|
+
|
|
155
|
+
{/* ---- Main log area ---- */}
|
|
156
|
+
<SplitPane.Panel>
|
|
157
|
+
<div className="flex h-full flex-col">
|
|
158
|
+
{/* Toolbar */}
|
|
159
|
+
<Toolbar>
|
|
160
|
+
<Toolbar.Group>
|
|
161
|
+
<Toolbar.Button
|
|
162
|
+
tooltip={paused ? 'Resume' : 'Pause'}
|
|
163
|
+
onClick={() => setPaused(!paused)}
|
|
164
|
+
active={paused}
|
|
165
|
+
>
|
|
166
|
+
{paused ? <Play /> : <Pause />}
|
|
167
|
+
</Toolbar.Button>
|
|
168
|
+
<Toolbar.Button tooltip="Clear log" onClick={() => buffer.clear()}>
|
|
169
|
+
<Trash2 />
|
|
170
|
+
</Toolbar.Button>
|
|
171
|
+
</Toolbar.Group>
|
|
172
|
+
<Toolbar.Separator />
|
|
173
|
+
<Toolbar.Group>
|
|
174
|
+
<Toolbar.Button tooltip="Filter">
|
|
175
|
+
<Filter />
|
|
176
|
+
</Toolbar.Button>
|
|
177
|
+
</Toolbar.Group>
|
|
178
|
+
<Toolbar.Input
|
|
179
|
+
placeholder="Filter by subject or payload..."
|
|
180
|
+
value={filter}
|
|
181
|
+
onChange={(e) => setFilter(e.target.value)}
|
|
182
|
+
/>
|
|
183
|
+
<Toolbar.Spacer />
|
|
184
|
+
<Toolbar.Text>{messageCountLabel}</Toolbar.Text>
|
|
185
|
+
</Toolbar>
|
|
186
|
+
|
|
187
|
+
{/* Message log */}
|
|
188
|
+
<div className="flex-1 overflow-hidden">
|
|
189
|
+
<MessageLog entries={buffer.entries} filter={filter} paused={paused} />
|
|
190
|
+
</div>
|
|
191
|
+
</div>
|
|
192
|
+
</SplitPane.Panel>
|
|
193
|
+
</SplitPane>
|
|
194
|
+
</div>
|
|
195
|
+
|
|
196
|
+
{/* Status bar */}
|
|
197
|
+
<StatusBar>
|
|
198
|
+
<StatusBar.Item>NATS Viewer</StatusBar.Item>
|
|
199
|
+
<StatusBar.Separator />
|
|
200
|
+
<StatusBar.Item>{subscriptions.size} subscription(s)</StatusBar.Item>
|
|
201
|
+
<StatusBar.Spacer />
|
|
202
|
+
<StatusBar.Item variant={connected ? 'success' : 'default'}>
|
|
203
|
+
{connected ? 'connected' : 'disconnected'}
|
|
204
|
+
</StatusBar.Item>
|
|
205
|
+
</StatusBar>
|
|
206
|
+
</div>
|
|
207
|
+
</NatsViewerContext.Provider>
|
|
208
|
+
);
|
|
209
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { Send } from 'lucide-react';
|
|
4
|
+
import { useCallback, useMemo, useRef, useState } from 'react';
|
|
5
|
+
import { Button } from '@/components/ui/button';
|
|
6
|
+
import { Input } from '@/components/ui/input';
|
|
7
|
+
import { useKhalAuth } from '@/lib/auth/use-auth';
|
|
8
|
+
import { useNats } from '@/lib/hooks/use-nats';
|
|
9
|
+
import { SUBJECTS } from '@/lib/subjects';
|
|
10
|
+
import type { LogEntry } from './types';
|
|
11
|
+
|
|
12
|
+
/** Build quick-pick subjects using the current orgId. */
|
|
13
|
+
function buildQuickPickSubjects(orgId: string): string[] {
|
|
14
|
+
if (!orgId) return [];
|
|
15
|
+
return [SUBJECTS.echo(orgId), SUBJECTS.pty.create(orgId), SUBJECTS.pty.list(orgId), SUBJECTS.notify.broadcast(orgId)];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface PublishPanelProps {
|
|
19
|
+
/** Optional callback to push an entry to the message buffer. */
|
|
20
|
+
onPublish?: (entry: Omit<LogEntry, 'id' | 'timestamp'>) => void;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function PublishPanel({ onPublish }: PublishPanelProps) {
|
|
24
|
+
const { connected, publish } = useNats();
|
|
25
|
+
const auth = useKhalAuth();
|
|
26
|
+
const orgId = auth?.orgId ?? '';
|
|
27
|
+
const quickPicks = useMemo(() => buildQuickPickSubjects(orgId), [orgId]);
|
|
28
|
+
|
|
29
|
+
const [subject, setSubject] = useState('');
|
|
30
|
+
const [payload, setPayload] = useState('');
|
|
31
|
+
const [error, setError] = useState<string | null>(null);
|
|
32
|
+
const [sent, setSent] = useState(false);
|
|
33
|
+
const sentTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
34
|
+
|
|
35
|
+
const handleSend = useCallback(() => {
|
|
36
|
+
setError(null);
|
|
37
|
+
|
|
38
|
+
let parsed: unknown;
|
|
39
|
+
if (payload.trim()) {
|
|
40
|
+
try {
|
|
41
|
+
parsed = JSON.parse(payload);
|
|
42
|
+
} catch (e) {
|
|
43
|
+
setError(`Invalid JSON: ${(e as Error).message}`);
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
publish(subject, parsed);
|
|
49
|
+
|
|
50
|
+
// Push to buffer as outgoing message
|
|
51
|
+
onPublish?.({ subject, payload: parsed, direction: 'out' });
|
|
52
|
+
|
|
53
|
+
// Show "Sent!" indicator
|
|
54
|
+
setSent(true);
|
|
55
|
+
if (sentTimer.current) clearTimeout(sentTimer.current);
|
|
56
|
+
sentTimer.current = setTimeout(() => setSent(false), 1000);
|
|
57
|
+
}, [subject, payload, publish, onPublish]);
|
|
58
|
+
|
|
59
|
+
const canSend = subject.trim().length > 0 && connected;
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
<div className="flex flex-col gap-2">
|
|
63
|
+
{/* Subject input */}
|
|
64
|
+
<Input
|
|
65
|
+
size="small"
|
|
66
|
+
placeholder={orgId ? SUBJECTS.echo(orgId) : 'khal.<orgId>.echo'}
|
|
67
|
+
value={subject}
|
|
68
|
+
onChange={(e) => {
|
|
69
|
+
setSubject(e.target.value);
|
|
70
|
+
setError(null);
|
|
71
|
+
}}
|
|
72
|
+
/>
|
|
73
|
+
|
|
74
|
+
{/* Quick-pick subjects */}
|
|
75
|
+
<div className="flex flex-wrap gap-1">
|
|
76
|
+
{quickPicks.map((s) => (
|
|
77
|
+
<button
|
|
78
|
+
key={s}
|
|
79
|
+
type="button"
|
|
80
|
+
className="rounded border border-gray-alpha-300 px-1.5 py-0.5 text-[10px] font-mono text-gray-800 hover:bg-gray-alpha-100 transition-colors"
|
|
81
|
+
onClick={() => setSubject(s)}
|
|
82
|
+
>
|
|
83
|
+
{s}
|
|
84
|
+
</button>
|
|
85
|
+
))}
|
|
86
|
+
</div>
|
|
87
|
+
|
|
88
|
+
{/* Payload textarea */}
|
|
89
|
+
<textarea
|
|
90
|
+
className="w-full rounded-md border border-gray-alpha-400 bg-background-100 px-2 py-1.5 font-mono text-xs text-gray-1000 placeholder:text-gray-700 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-700 focus-visible:ring-offset-1 resize-none"
|
|
91
|
+
rows={4}
|
|
92
|
+
placeholder='{"key": "value"}'
|
|
93
|
+
value={payload}
|
|
94
|
+
onChange={(e) => {
|
|
95
|
+
setPayload(e.target.value);
|
|
96
|
+
setError(null);
|
|
97
|
+
}}
|
|
98
|
+
/>
|
|
99
|
+
|
|
100
|
+
{/* Error message */}
|
|
101
|
+
{error && <p className="text-[11px] text-red-600">{error}</p>}
|
|
102
|
+
|
|
103
|
+
{/* Send button + Sent indicator */}
|
|
104
|
+
<div className="flex items-center gap-2">
|
|
105
|
+
<Button size="small" disabled={!canSend} onClick={handleSend} prefix={<Send className="h-3 w-3" />}>
|
|
106
|
+
Send
|
|
107
|
+
</Button>
|
|
108
|
+
{sent && <span className="text-[11px] font-medium text-green-600 animate-pulse">Sent!</span>}
|
|
109
|
+
{!connected && <span className="text-[11px] text-gray-600">Not connected</span>}
|
|
110
|
+
</div>
|
|
111
|
+
</div>
|
|
112
|
+
);
|
|
113
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { Inbox } from 'lucide-react';
|
|
4
|
+
import { useCallback, useMemo, useState } from 'react';
|
|
5
|
+
import { Button } from '@/components/ui/button';
|
|
6
|
+
import { Input } from '@/components/ui/input';
|
|
7
|
+
import { Spinner } from '@/components/ui/spinner';
|
|
8
|
+
import { useKhalAuth } from '@/lib/auth/use-auth';
|
|
9
|
+
import { useNats } from '@/lib/hooks/use-nats';
|
|
10
|
+
import { SUBJECTS } from '@/lib/subjects';
|
|
11
|
+
import type { LogEntry } from './types';
|
|
12
|
+
|
|
13
|
+
/** Build quick-pick subjects using the current orgId. */
|
|
14
|
+
function buildQuickPickSubjects(orgId: string): string[] {
|
|
15
|
+
if (!orgId) return [];
|
|
16
|
+
return [SUBJECTS.echo(orgId), SUBJECTS.pty.create(orgId), SUBJECTS.pty.list(orgId)];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
type RequestState =
|
|
20
|
+
| { status: 'idle' }
|
|
21
|
+
| { status: 'loading' }
|
|
22
|
+
| { status: 'success'; data: unknown }
|
|
23
|
+
| { status: 'error'; message: string };
|
|
24
|
+
|
|
25
|
+
interface RequestPanelProps {
|
|
26
|
+
/** Optional callback to push an entry to the message buffer. */
|
|
27
|
+
onMessage?: (entry: Omit<LogEntry, 'id' | 'timestamp'>) => void;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function RequestPanel({ onMessage }: RequestPanelProps) {
|
|
31
|
+
const { connected, request } = useNats();
|
|
32
|
+
const auth = useKhalAuth();
|
|
33
|
+
const orgId = auth?.orgId ?? '';
|
|
34
|
+
const quickPicks = useMemo(() => buildQuickPickSubjects(orgId), [orgId]);
|
|
35
|
+
|
|
36
|
+
const [subject, setSubject] = useState('');
|
|
37
|
+
const [payload, setPayload] = useState('');
|
|
38
|
+
const [timeout, setTimeout_] = useState(5000);
|
|
39
|
+
const [jsonError, setJsonError] = useState<string | null>(null);
|
|
40
|
+
const [reqState, setReqState] = useState<RequestState>({ status: 'idle' });
|
|
41
|
+
|
|
42
|
+
const handleSend = useCallback(async () => {
|
|
43
|
+
setJsonError(null);
|
|
44
|
+
|
|
45
|
+
let parsed: unknown;
|
|
46
|
+
if (payload.trim()) {
|
|
47
|
+
try {
|
|
48
|
+
parsed = JSON.parse(payload);
|
|
49
|
+
} catch (e) {
|
|
50
|
+
setJsonError(`Invalid JSON: ${(e as Error).message}`);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Clear previous response and start loading
|
|
56
|
+
setReqState({ status: 'loading' });
|
|
57
|
+
|
|
58
|
+
// Push outgoing request to buffer
|
|
59
|
+
onMessage?.({ subject, payload: parsed, direction: 'out' });
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
const response = await request(subject, parsed, timeout);
|
|
63
|
+
setReqState({ status: 'success', data: response });
|
|
64
|
+
|
|
65
|
+
// Push response to buffer
|
|
66
|
+
onMessage?.({ subject: `${subject} (reply)`, payload: response, direction: 'in' });
|
|
67
|
+
} catch (e) {
|
|
68
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
69
|
+
setReqState({ status: 'error', message });
|
|
70
|
+
}
|
|
71
|
+
}, [subject, payload, timeout, request, onMessage]);
|
|
72
|
+
|
|
73
|
+
const canSend = subject.trim().length > 0 && connected && reqState.status !== 'loading';
|
|
74
|
+
|
|
75
|
+
return (
|
|
76
|
+
<div className="flex flex-col gap-2">
|
|
77
|
+
{/* Subject input */}
|
|
78
|
+
<Input
|
|
79
|
+
size="small"
|
|
80
|
+
placeholder={orgId ? SUBJECTS.echo(orgId) : 'khal.<orgId>.echo'}
|
|
81
|
+
value={subject}
|
|
82
|
+
onChange={(e) => {
|
|
83
|
+
setSubject(e.target.value);
|
|
84
|
+
setJsonError(null);
|
|
85
|
+
}}
|
|
86
|
+
/>
|
|
87
|
+
|
|
88
|
+
{/* Quick-pick subjects */}
|
|
89
|
+
<div className="flex flex-wrap gap-1">
|
|
90
|
+
{quickPicks.map((s) => (
|
|
91
|
+
<button
|
|
92
|
+
key={s}
|
|
93
|
+
type="button"
|
|
94
|
+
className="rounded border border-gray-alpha-300 px-1.5 py-0.5 text-[10px] font-mono text-gray-800 hover:bg-gray-alpha-100 transition-colors"
|
|
95
|
+
onClick={() => setSubject(s)}
|
|
96
|
+
>
|
|
97
|
+
{s}
|
|
98
|
+
</button>
|
|
99
|
+
))}
|
|
100
|
+
</div>
|
|
101
|
+
|
|
102
|
+
{/* Payload textarea */}
|
|
103
|
+
<textarea
|
|
104
|
+
className="w-full rounded-md border border-gray-alpha-400 bg-background-100 px-2 py-1.5 font-mono text-xs text-gray-1000 placeholder:text-gray-700 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-700 focus-visible:ring-offset-1 resize-none"
|
|
105
|
+
rows={4}
|
|
106
|
+
placeholder="{}"
|
|
107
|
+
value={payload}
|
|
108
|
+
onChange={(e) => {
|
|
109
|
+
setPayload(e.target.value);
|
|
110
|
+
setJsonError(null);
|
|
111
|
+
}}
|
|
112
|
+
/>
|
|
113
|
+
|
|
114
|
+
{/* Timeout input */}
|
|
115
|
+
<div className="flex items-center gap-2">
|
|
116
|
+
<label className="text-[11px] text-gray-800 shrink-0">Timeout</label>
|
|
117
|
+
<Input
|
|
118
|
+
size="small"
|
|
119
|
+
type="number"
|
|
120
|
+
className="w-20 text-xs"
|
|
121
|
+
value={timeout}
|
|
122
|
+
onChange={(e) => setTimeout_(Number(e.target.value) || 5000)}
|
|
123
|
+
/>
|
|
124
|
+
<span className="text-[11px] text-gray-700">ms</span>
|
|
125
|
+
</div>
|
|
126
|
+
|
|
127
|
+
{/* JSON error */}
|
|
128
|
+
{jsonError && <p className="text-[11px] text-red-600">{jsonError}</p>}
|
|
129
|
+
|
|
130
|
+
{/* Send button */}
|
|
131
|
+
<div className="flex items-center gap-2">
|
|
132
|
+
<Button
|
|
133
|
+
size="small"
|
|
134
|
+
disabled={!canSend}
|
|
135
|
+
onClick={handleSend}
|
|
136
|
+
loading={reqState.status === 'loading'}
|
|
137
|
+
prefix={<Inbox className="h-3 w-3" />}
|
|
138
|
+
>
|
|
139
|
+
Send Request
|
|
140
|
+
</Button>
|
|
141
|
+
{!connected && <span className="text-[11px] text-gray-600">Not connected</span>}
|
|
142
|
+
</div>
|
|
143
|
+
|
|
144
|
+
{/* Response area */}
|
|
145
|
+
{reqState.status === 'loading' && (
|
|
146
|
+
<div className="flex items-center gap-2 rounded border border-gray-alpha-300 bg-gray-alpha-50 px-2 py-2">
|
|
147
|
+
<Spinner size="sm" />
|
|
148
|
+
<span className="text-[11px] text-gray-800">Waiting for reply...</span>
|
|
149
|
+
</div>
|
|
150
|
+
)}
|
|
151
|
+
|
|
152
|
+
{reqState.status === 'success' && (
|
|
153
|
+
<div className="rounded border border-gray-alpha-400 bg-gray-alpha-50 p-2 overflow-auto max-h-48">
|
|
154
|
+
<pre className="font-mono text-xs text-gray-1000 whitespace-pre-wrap break-all">
|
|
155
|
+
{typeof reqState.data === 'string' ? reqState.data : JSON.stringify(reqState.data, null, 2)}
|
|
156
|
+
</pre>
|
|
157
|
+
</div>
|
|
158
|
+
)}
|
|
159
|
+
|
|
160
|
+
{reqState.status === 'error' && (
|
|
161
|
+
<div className="rounded border border-red-300 bg-red-50 dark:bg-red-950/20 p-2">
|
|
162
|
+
<p className="font-mono text-[11px] text-red-600 break-all">{reqState.message}</p>
|
|
163
|
+
</div>
|
|
164
|
+
)}
|
|
165
|
+
</div>
|
|
166
|
+
);
|
|
167
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { Radio } from 'lucide-react';
|
|
4
|
+
import { Separator } from '@/components/ui/separator';
|
|
5
|
+
import { useKhalAuth } from '@/lib/auth/use-auth';
|
|
6
|
+
import { ActiveSubs } from './ActiveSubs';
|
|
7
|
+
import { useNatsViewer } from './nats-viewer-context';
|
|
8
|
+
import { SubjectCatalog } from './SubjectCatalog';
|
|
9
|
+
import { SubscribeInput } from './SubscribeInput';
|
|
10
|
+
|
|
11
|
+
export function Sidebar() {
|
|
12
|
+
const { subscriptions, addSubscription, removeSubscription } = useNatsViewer();
|
|
13
|
+
const auth = useKhalAuth();
|
|
14
|
+
const orgId = auth?.orgId ?? '';
|
|
15
|
+
const catchAll = orgId ? `khal.${orgId}.>` : 'khal.>';
|
|
16
|
+
const catchAllActive = subscriptions.has(catchAll);
|
|
17
|
+
|
|
18
|
+
const toggleCatchAll = () => {
|
|
19
|
+
if (catchAllActive) {
|
|
20
|
+
removeSubscription(catchAll);
|
|
21
|
+
} else {
|
|
22
|
+
addSubscription(catchAll);
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<div className="flex flex-col gap-3 overflow-y-auto">
|
|
28
|
+
{/* Catch-all toggle */}
|
|
29
|
+
<button
|
|
30
|
+
onClick={toggleCatchAll}
|
|
31
|
+
className={`flex items-center gap-2 rounded-md px-2 py-1.5 text-xs font-medium transition-colors ${
|
|
32
|
+
catchAllActive
|
|
33
|
+
? 'bg-green-500/15 text-green-700 hover:bg-green-500/25'
|
|
34
|
+
: 'bg-gray-alpha-100 text-gray-900 hover:bg-gray-alpha-200'
|
|
35
|
+
}`}
|
|
36
|
+
>
|
|
37
|
+
<Radio className="h-3.5 w-3.5" />
|
|
38
|
+
<span className="font-mono">{catchAll}</span>
|
|
39
|
+
<span className="ml-auto text-[11px]">{catchAllActive ? 'ON' : 'OFF'}</span>
|
|
40
|
+
</button>
|
|
41
|
+
|
|
42
|
+
{/* Custom subscribe input */}
|
|
43
|
+
<SubscribeInput />
|
|
44
|
+
|
|
45
|
+
<Separator />
|
|
46
|
+
|
|
47
|
+
{/* Known Subjects */}
|
|
48
|
+
<div>
|
|
49
|
+
<h3 className="mb-1.5 text-[11px] font-medium uppercase tracking-wider text-gray-700">Known Subjects</h3>
|
|
50
|
+
<SubjectCatalog />
|
|
51
|
+
</div>
|
|
52
|
+
|
|
53
|
+
<Separator />
|
|
54
|
+
|
|
55
|
+
{/* Active Subscriptions */}
|
|
56
|
+
<div>
|
|
57
|
+
<h3 className="mb-1.5 text-[11px] font-medium uppercase tracking-wider text-gray-700">Active Subscriptions</h3>
|
|
58
|
+
<ActiveSubs />
|
|
59
|
+
</div>
|
|
60
|
+
</div>
|
|
61
|
+
);
|
|
62
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useMemo } from 'react';
|
|
4
|
+
import { useKhalAuth } from '@/lib/auth/use-auth';
|
|
5
|
+
import { SUBJECTS } from '@/lib/subjects';
|
|
6
|
+
import { useNatsViewer } from './nats-viewer-context';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Build a list of known static subjects using the current orgId.
|
|
10
|
+
* Only includes subjects that don't require dynamic parameters
|
|
11
|
+
* beyond orgId (i.e., skips session-scoped subjects like pty.data).
|
|
12
|
+
*/
|
|
13
|
+
function buildKnownSubjects(orgId: string, userId: string): string[] {
|
|
14
|
+
if (!orgId) return [];
|
|
15
|
+
const subjects = [
|
|
16
|
+
SUBJECTS.echo(orgId),
|
|
17
|
+
SUBJECTS.system.health(orgId),
|
|
18
|
+
SUBJECTS.pty.create(orgId),
|
|
19
|
+
SUBJECTS.pty.destroy(orgId),
|
|
20
|
+
SUBJECTS.pty.list(orgId),
|
|
21
|
+
SUBJECTS.fs.list(orgId),
|
|
22
|
+
SUBJECTS.fs.read(orgId),
|
|
23
|
+
SUBJECTS.fs.write(orgId),
|
|
24
|
+
SUBJECTS.fs.search(orgId),
|
|
25
|
+
SUBJECTS.notify.broadcast(orgId),
|
|
26
|
+
];
|
|
27
|
+
if (userId) {
|
|
28
|
+
subjects.push(SUBJECTS.desktop.cmd.all(orgId, userId), SUBJECTS.desktop.event.all(orgId, userId));
|
|
29
|
+
}
|
|
30
|
+
return subjects.sort();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function SubjectCatalog() {
|
|
34
|
+
const { subscriptions, addSubscription, removeSubscription } = useNatsViewer();
|
|
35
|
+
const auth = useKhalAuth();
|
|
36
|
+
const orgId = auth?.orgId ?? '';
|
|
37
|
+
const userId = auth?.userId ?? '';
|
|
38
|
+
|
|
39
|
+
const knownSubjects = useMemo(() => buildKnownSubjects(orgId, userId), [orgId, userId]);
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<div className="flex flex-col gap-0.5">
|
|
43
|
+
{knownSubjects.map((subject) => {
|
|
44
|
+
const active = subscriptions.has(subject);
|
|
45
|
+
return (
|
|
46
|
+
<button
|
|
47
|
+
key={subject}
|
|
48
|
+
onClick={() => (active ? removeSubscription(subject) : addSubscription(subject))}
|
|
49
|
+
className="group flex items-center gap-2 rounded px-1.5 py-0.5 text-left transition-colors hover:bg-gray-alpha-100"
|
|
50
|
+
>
|
|
51
|
+
<span
|
|
52
|
+
className={`inline-block h-2 w-2 shrink-0 rounded-full transition-colors ${
|
|
53
|
+
active ? 'bg-green-500' : 'bg-gray-400 group-hover:bg-gray-500'
|
|
54
|
+
}`}
|
|
55
|
+
/>
|
|
56
|
+
<span className="min-w-0 truncate font-mono text-xs text-gray-900 group-hover:text-gray-1000">
|
|
57
|
+
{subject}
|
|
58
|
+
</span>
|
|
59
|
+
</button>
|
|
60
|
+
);
|
|
61
|
+
})}
|
|
62
|
+
</div>
|
|
63
|
+
);
|
|
64
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { Plus } from 'lucide-react';
|
|
4
|
+
import { useState } from 'react';
|
|
5
|
+
import { useNatsViewer } from './nats-viewer-context';
|
|
6
|
+
|
|
7
|
+
export function SubscribeInput() {
|
|
8
|
+
const { addSubscription } = useNatsViewer();
|
|
9
|
+
const [value, setValue] = useState('');
|
|
10
|
+
const [error, setError] = useState('');
|
|
11
|
+
|
|
12
|
+
const handleSubmit = () => {
|
|
13
|
+
const trimmed = value.trim();
|
|
14
|
+
if (!trimmed) return;
|
|
15
|
+
|
|
16
|
+
if (!trimmed.startsWith('khal.')) {
|
|
17
|
+
setError('Subject must start with "khal."');
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
setError('');
|
|
22
|
+
addSubscription(trimmed);
|
|
23
|
+
setValue('');
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const handleKeyDown = (e: React.KeyboardEvent) => {
|
|
27
|
+
if (e.key === 'Enter') {
|
|
28
|
+
e.preventDefault();
|
|
29
|
+
handleSubmit();
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<div className="flex flex-col gap-1">
|
|
35
|
+
<div className="flex items-center gap-1">
|
|
36
|
+
<input
|
|
37
|
+
type="text"
|
|
38
|
+
value={value}
|
|
39
|
+
onChange={(e) => {
|
|
40
|
+
setValue(e.target.value);
|
|
41
|
+
if (error) setError('');
|
|
42
|
+
}}
|
|
43
|
+
onKeyDown={handleKeyDown}
|
|
44
|
+
placeholder="khal.custom.subject"
|
|
45
|
+
className="h-7 flex-1 rounded border border-gray-alpha-400 bg-background-100 px-2 font-mono text-xs text-gray-1000 placeholder:text-gray-600 focus:outline-none focus:ring-1 focus:ring-blue-700"
|
|
46
|
+
/>
|
|
47
|
+
<button
|
|
48
|
+
onClick={handleSubmit}
|
|
49
|
+
disabled={!value.trim()}
|
|
50
|
+
className="flex h-7 shrink-0 items-center gap-1 rounded border border-gray-alpha-400 bg-background-100 px-2 text-xs text-gray-900 transition-colors hover:bg-gray-alpha-100 hover:text-gray-1000 disabled:opacity-40 disabled:pointer-events-none"
|
|
51
|
+
>
|
|
52
|
+
<Plus className="h-3 w-3" />
|
|
53
|
+
Sub
|
|
54
|
+
</button>
|
|
55
|
+
</div>
|
|
56
|
+
{error && <p className="px-0.5 text-[11px] text-red-600">{error}</p>}
|
|
57
|
+
</div>
|
|
58
|
+
);
|
|
59
|
+
}
|