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,31 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { createContext, useContext } from 'react';
|
|
4
|
+
import type { LogEntry } from './types';
|
|
5
|
+
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
// Context — shared state for sidebar panels and the main log area
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
|
|
10
|
+
export interface NatsViewerContextValue {
|
|
11
|
+
subscriptions: Set<string>;
|
|
12
|
+
addSubscription: (subject: string) => void;
|
|
13
|
+
removeSubscription: (subject: string) => void;
|
|
14
|
+
buffer: {
|
|
15
|
+
entries: LogEntry[];
|
|
16
|
+
push: (entry: Omit<LogEntry, 'id' | 'timestamp'>) => void;
|
|
17
|
+
clear: () => void;
|
|
18
|
+
};
|
|
19
|
+
filter: string;
|
|
20
|
+
setFilter: (f: string) => void;
|
|
21
|
+
paused: boolean;
|
|
22
|
+
setPaused: (p: boolean) => void;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const NatsViewerContext = createContext<NatsViewerContextValue | null>(null);
|
|
26
|
+
|
|
27
|
+
export function useNatsViewer(): NatsViewerContextValue {
|
|
28
|
+
const ctx = useContext(NatsViewerContext);
|
|
29
|
+
if (!ctx) throw new Error('useNatsViewer must be used within <NatsViewer>');
|
|
30
|
+
return ctx;
|
|
31
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { useCallback, useReducer } from 'react';
|
|
2
|
+
import type { LogEntry } from './types';
|
|
3
|
+
|
|
4
|
+
const DEFAULT_CAPACITY = 1000;
|
|
5
|
+
|
|
6
|
+
type Action =
|
|
7
|
+
| { type: 'push'; entry: Omit<LogEntry, 'id' | 'timestamp'> }
|
|
8
|
+
| { type: 'clear' }
|
|
9
|
+
| { type: 'set-capacity'; capacity: number };
|
|
10
|
+
|
|
11
|
+
interface State {
|
|
12
|
+
entries: LogEntry[];
|
|
13
|
+
capacity: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function reducer(state: State, action: Action): State {
|
|
17
|
+
switch (action.type) {
|
|
18
|
+
case 'push': {
|
|
19
|
+
const entry: LogEntry = {
|
|
20
|
+
...action.entry,
|
|
21
|
+
id: crypto.randomUUID(),
|
|
22
|
+
timestamp: Date.now(),
|
|
23
|
+
};
|
|
24
|
+
const next = [...state.entries, entry];
|
|
25
|
+
// Drop oldest entries when over capacity
|
|
26
|
+
if (next.length > state.capacity) {
|
|
27
|
+
return { ...state, entries: next.slice(next.length - state.capacity) };
|
|
28
|
+
}
|
|
29
|
+
return { ...state, entries: next };
|
|
30
|
+
}
|
|
31
|
+
case 'clear':
|
|
32
|
+
return { ...state, entries: [] };
|
|
33
|
+
case 'set-capacity': {
|
|
34
|
+
const entries =
|
|
35
|
+
state.entries.length > action.capacity
|
|
36
|
+
? state.entries.slice(state.entries.length - action.capacity)
|
|
37
|
+
: state.entries;
|
|
38
|
+
return { capacity: action.capacity, entries };
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function useMessageBuffer(capacity = DEFAULT_CAPACITY) {
|
|
44
|
+
const [state, dispatch] = useReducer(reducer, { entries: [], capacity });
|
|
45
|
+
|
|
46
|
+
const push = useCallback((entry: Omit<LogEntry, 'id' | 'timestamp'>) => {
|
|
47
|
+
dispatch({ type: 'push', entry });
|
|
48
|
+
}, []);
|
|
49
|
+
|
|
50
|
+
const clear = useCallback(() => {
|
|
51
|
+
dispatch({ type: 'clear' });
|
|
52
|
+
}, []);
|
|
53
|
+
|
|
54
|
+
return { entries: state.entries, push, clear };
|
|
55
|
+
}
|
|
@@ -0,0 +1,492 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { Bell, Command, Info, Monitor, Radio } from 'lucide-react';
|
|
4
|
+
import { useEffect, useState } from 'react';
|
|
5
|
+
import { EmptyState, PropertyPanel, SectionHeader, SidebarNav, SplitPane, StatusBar } from '@/components/os-primitives';
|
|
6
|
+
import { Button } from '@/components/ui/button';
|
|
7
|
+
import { Input } from '@/components/ui/input';
|
|
8
|
+
import { Note } from '@/components/ui/note';
|
|
9
|
+
import { Separator } from '@/components/ui/separator';
|
|
10
|
+
import { Toggle } from '@/components/ui/switch';
|
|
11
|
+
import { ThemeSwitcher } from '@/components/ui/theme-switcher';
|
|
12
|
+
import { useNats } from '@/lib/hooks/use-nats';
|
|
13
|
+
import type { KeyCombo, ModifierKey, ShortcutCategory } from '@/lib/keyboard/types';
|
|
14
|
+
import { comboToSymbols } from '@/lib/keyboard/types';
|
|
15
|
+
import { SUBJECTS } from '@/lib/subjects';
|
|
16
|
+
import { useKeybindStore } from '@/stores/keybind-store';
|
|
17
|
+
import type { DesktopNotifMode } from '@/stores/notification-store';
|
|
18
|
+
import { useNotificationStore } from '@/stores/notification-store';
|
|
19
|
+
import { useThemeStore } from '@/stores/theme-store';
|
|
20
|
+
|
|
21
|
+
const IS_DEV = process.env.NODE_ENV === 'development';
|
|
22
|
+
|
|
23
|
+
type SettingsTab = 'appearance' | 'notifications' | 'keyboard' | 'about' | 'nats';
|
|
24
|
+
|
|
25
|
+
const NAV_ITEMS: Array<{ id: SettingsTab; label: string; icon: React.ComponentType; devOnly?: boolean }> = [
|
|
26
|
+
{ id: 'appearance', label: 'Appearance', icon: Monitor },
|
|
27
|
+
{ id: 'notifications', label: 'Notifications', icon: Bell },
|
|
28
|
+
{ id: 'keyboard', label: 'Keyboard Shortcuts', icon: Command },
|
|
29
|
+
{ id: 'about', label: 'About', icon: Info },
|
|
30
|
+
{ id: 'nats', label: 'NATS Echo Test', icon: Radio, devOnly: true },
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
export function Settings(_props: { windowId: string; meta?: Record<string, unknown> }) {
|
|
34
|
+
const [tab, setTab] = useState<SettingsTab>('appearance');
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<div className="flex h-full flex-col bg-background-100">
|
|
38
|
+
<div className="flex-1 overflow-hidden">
|
|
39
|
+
<SplitPane defaultSize={160} min={130} max={220} collapseBelow={400}>
|
|
40
|
+
<SplitPane.Panel className="bg-gray-alpha-50">
|
|
41
|
+
<SidebarNav label="Settings" title="Settings">
|
|
42
|
+
{NAV_ITEMS.filter((item) => !item.devOnly || IS_DEV).map((item) => {
|
|
43
|
+
const Icon = item.icon;
|
|
44
|
+
return (
|
|
45
|
+
<SidebarNav.Item
|
|
46
|
+
key={item.id}
|
|
47
|
+
active={tab === item.id}
|
|
48
|
+
onClick={() => setTab(item.id)}
|
|
49
|
+
icon={<Icon />}
|
|
50
|
+
>
|
|
51
|
+
{item.label}
|
|
52
|
+
</SidebarNav.Item>
|
|
53
|
+
);
|
|
54
|
+
})}
|
|
55
|
+
</SidebarNav>
|
|
56
|
+
</SplitPane.Panel>
|
|
57
|
+
<SplitPane.Panel className="overflow-auto p-6">
|
|
58
|
+
{tab === 'appearance' && <AppearanceTab />}
|
|
59
|
+
{tab === 'notifications' && <NotificationsTab />}
|
|
60
|
+
{tab === 'keyboard' && <KeyboardShortcutsTab />}
|
|
61
|
+
{tab === 'about' && <AboutTab />}
|
|
62
|
+
{tab === 'nats' && <NatsEchoTab />}
|
|
63
|
+
</SplitPane.Panel>
|
|
64
|
+
</SplitPane>
|
|
65
|
+
</div>
|
|
66
|
+
<StatusBar>
|
|
67
|
+
<StatusBar.Item>Khal</StatusBar.Item>
|
|
68
|
+
<StatusBar.Spacer />
|
|
69
|
+
<StatusBar.Item variant="success">local</StatusBar.Item>
|
|
70
|
+
</StatusBar>
|
|
71
|
+
</div>
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// ---------------------------------------------------------------------------
|
|
76
|
+
// Appearance Tab
|
|
77
|
+
// ---------------------------------------------------------------------------
|
|
78
|
+
|
|
79
|
+
function ReduceMotionToggle() {
|
|
80
|
+
const reduceMotion = useThemeStore((s) => s.reduceMotion);
|
|
81
|
+
const setReduceMotion = useThemeStore((s) => s.setReduceMotion);
|
|
82
|
+
|
|
83
|
+
return (
|
|
84
|
+
<div className="flex items-center justify-between rounded-lg border border-gray-alpha-200 bg-background-100 px-4 py-3">
|
|
85
|
+
<div>
|
|
86
|
+
<p className="text-copy-13 font-medium text-gray-1000">Reduce motion</p>
|
|
87
|
+
<p className="text-copy-12 text-gray-800">
|
|
88
|
+
{reduceMotion ? 'Animations are disabled' : 'Animations are enabled'}
|
|
89
|
+
</p>
|
|
90
|
+
</div>
|
|
91
|
+
<Toggle checked={reduceMotion} onChange={() => setReduceMotion(!reduceMotion)} />
|
|
92
|
+
</div>
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function AppearanceTab() {
|
|
97
|
+
return (
|
|
98
|
+
<div className="flex max-w-2xl flex-col gap-8 text-gray-1000">
|
|
99
|
+
<section>
|
|
100
|
+
<SectionHeader title="Mode" description="Choose light, dark, or system." />
|
|
101
|
+
<ThemeSwitcher />
|
|
102
|
+
</section>
|
|
103
|
+
|
|
104
|
+
<Separator />
|
|
105
|
+
|
|
106
|
+
<section>
|
|
107
|
+
<SectionHeader title="Motion" description="Control animation preferences." />
|
|
108
|
+
<ReduceMotionToggle />
|
|
109
|
+
</section>
|
|
110
|
+
</div>
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// ---------------------------------------------------------------------------
|
|
115
|
+
// Notifications Tab
|
|
116
|
+
// ---------------------------------------------------------------------------
|
|
117
|
+
|
|
118
|
+
const NOTIF_MODE_OPTIONS: Array<{
|
|
119
|
+
value: DesktopNotifMode;
|
|
120
|
+
label: string;
|
|
121
|
+
description: string;
|
|
122
|
+
}> = [
|
|
123
|
+
{ value: 'background', label: 'When in background', description: 'Only show when the tab is not visible' },
|
|
124
|
+
{ value: 'always', label: 'Always', description: 'Show for every notification' },
|
|
125
|
+
{ value: 'off', label: 'Off', description: 'Never send browser notifications' },
|
|
126
|
+
];
|
|
127
|
+
|
|
128
|
+
function NotificationsTab() {
|
|
129
|
+
const doNotDisturb = useNotificationStore((s) => s.doNotDisturb);
|
|
130
|
+
const setDoNotDisturb = useNotificationStore((s) => s.setDoNotDisturb);
|
|
131
|
+
const desktopNotifMode = useNotificationStore((s) => s.desktopNotifMode);
|
|
132
|
+
const setDesktopNotifMode = useNotificationStore((s) => s.setDesktopNotifMode);
|
|
133
|
+
const browserPermission = useNotificationStore((s) => s.browserPermission);
|
|
134
|
+
const requestBrowserPermission = useNotificationStore((s) => s.requestBrowserPermission);
|
|
135
|
+
const syncBrowserPermission = useNotificationStore((s) => s.syncBrowserPermission);
|
|
136
|
+
const history = useNotificationStore((s) => s.history);
|
|
137
|
+
const clearHistory = useNotificationStore((s) => s.clearHistory);
|
|
138
|
+
|
|
139
|
+
useEffect(() => {
|
|
140
|
+
syncBrowserPermission();
|
|
141
|
+
}, [syncBrowserPermission]);
|
|
142
|
+
|
|
143
|
+
const needsPermission = browserPermission !== 'granted' && desktopNotifMode !== 'off';
|
|
144
|
+
|
|
145
|
+
return (
|
|
146
|
+
<div className="flex max-w-2xl flex-col gap-8 text-gray-1000">
|
|
147
|
+
<section>
|
|
148
|
+
<SectionHeader title="Do Not Disturb" description="Suppress in-app toast notifications." />
|
|
149
|
+
<div className="flex items-center justify-between rounded-lg border border-gray-alpha-200 bg-background-100 px-4 py-3">
|
|
150
|
+
<div>
|
|
151
|
+
<p className="text-copy-13 font-medium text-gray-1000">Do Not Disturb</p>
|
|
152
|
+
<p className="text-copy-12 text-gray-800">
|
|
153
|
+
{doNotDisturb ? 'Toasts are hidden' : 'Toasts are shown normally'}
|
|
154
|
+
</p>
|
|
155
|
+
</div>
|
|
156
|
+
<Toggle checked={doNotDisturb} onChange={() => setDoNotDisturb(!doNotDisturb)} />
|
|
157
|
+
</div>
|
|
158
|
+
</section>
|
|
159
|
+
|
|
160
|
+
<Separator />
|
|
161
|
+
|
|
162
|
+
<section>
|
|
163
|
+
<SectionHeader title="Desktop Notifications" description="Bridge notifications to your OS." />
|
|
164
|
+
<div className="flex flex-col gap-3">
|
|
165
|
+
{NOTIF_MODE_OPTIONS.map((opt) => (
|
|
166
|
+
<button
|
|
167
|
+
key={opt.value}
|
|
168
|
+
onClick={() => setDesktopNotifMode(opt.value)}
|
|
169
|
+
className={`flex items-center gap-3 rounded-lg border px-4 py-3 text-left transition-colors ${
|
|
170
|
+
desktopNotifMode === opt.value
|
|
171
|
+
? 'border-blue-400 bg-blue-100'
|
|
172
|
+
: 'border-gray-alpha-200 bg-background-100 hover:border-gray-alpha-300'
|
|
173
|
+
}`}
|
|
174
|
+
>
|
|
175
|
+
<div
|
|
176
|
+
className={`h-3 w-3 shrink-0 rounded-full border-2 ${desktopNotifMode === opt.value ? 'border-blue-600 bg-blue-600' : 'border-gray-alpha-400'}`}
|
|
177
|
+
/>
|
|
178
|
+
<div>
|
|
179
|
+
<p className="text-copy-13 font-medium text-gray-1000">{opt.label}</p>
|
|
180
|
+
<p className="text-copy-12 text-gray-800">{opt.description}</p>
|
|
181
|
+
</div>
|
|
182
|
+
</button>
|
|
183
|
+
))}
|
|
184
|
+
{needsPermission && (
|
|
185
|
+
<Note type="warning" size="small">
|
|
186
|
+
<div className="flex items-center justify-between gap-4">
|
|
187
|
+
<span>{browserPermission === 'denied' ? 'Blocked by browser.' : 'Permission required.'}</span>
|
|
188
|
+
{browserPermission !== 'denied' && (
|
|
189
|
+
<Button size="small" variant="secondary" onClick={() => requestBrowserPermission()}>
|
|
190
|
+
Allow
|
|
191
|
+
</Button>
|
|
192
|
+
)}
|
|
193
|
+
</div>
|
|
194
|
+
</Note>
|
|
195
|
+
)}
|
|
196
|
+
</div>
|
|
197
|
+
</section>
|
|
198
|
+
|
|
199
|
+
<Separator />
|
|
200
|
+
|
|
201
|
+
<section>
|
|
202
|
+
<SectionHeader
|
|
203
|
+
title="Notification History"
|
|
204
|
+
description={`${history.length} notification${history.length !== 1 ? 's' : ''}`}
|
|
205
|
+
>
|
|
206
|
+
{history.length > 0 && (
|
|
207
|
+
<Button size="small" variant="secondary" onClick={clearHistory}>
|
|
208
|
+
Clear History
|
|
209
|
+
</Button>
|
|
210
|
+
)}
|
|
211
|
+
</SectionHeader>
|
|
212
|
+
</section>
|
|
213
|
+
</div>
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// ---------------------------------------------------------------------------
|
|
218
|
+
// About Tab
|
|
219
|
+
// ---------------------------------------------------------------------------
|
|
220
|
+
|
|
221
|
+
function AboutTab() {
|
|
222
|
+
return (
|
|
223
|
+
<div className="flex max-w-2xl flex-col gap-8 text-gray-1000">
|
|
224
|
+
<section>
|
|
225
|
+
<SectionHeader title="Khal" description="Desktop-in-browser OS shell." />
|
|
226
|
+
<PropertyPanel className="rounded-lg border border-gray-alpha-200">
|
|
227
|
+
<PropertyPanel.Section>
|
|
228
|
+
<PropertyPanel.Row label="Version">v2-dev</PropertyPanel.Row>
|
|
229
|
+
<PropertyPanel.Row label="Framework">Next.js</PropertyPanel.Row>
|
|
230
|
+
<PropertyPanel.Row label="Runtime">
|
|
231
|
+
{typeof navigator !== 'undefined' ? navigator.userAgent.split(' ').pop() : '--'}
|
|
232
|
+
</PropertyPanel.Row>
|
|
233
|
+
</PropertyPanel.Section>
|
|
234
|
+
</PropertyPanel>
|
|
235
|
+
</section>
|
|
236
|
+
</div>
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// ---------------------------------------------------------------------------
|
|
241
|
+
// NATS Echo Test Tab (dev only)
|
|
242
|
+
// ---------------------------------------------------------------------------
|
|
243
|
+
|
|
244
|
+
function NatsEchoTab() {
|
|
245
|
+
const { connected, request, orgId } = useNats();
|
|
246
|
+
const [message, setMessage] = useState('');
|
|
247
|
+
const [response, setResponse] = useState<string | null>(null);
|
|
248
|
+
const [loading, setLoading] = useState(false);
|
|
249
|
+
const [error, setError] = useState<string | null>(null);
|
|
250
|
+
|
|
251
|
+
const handleSendEcho = async () => {
|
|
252
|
+
if (!message.trim() || !orgId) return;
|
|
253
|
+
setLoading(true);
|
|
254
|
+
setError(null);
|
|
255
|
+
setResponse(null);
|
|
256
|
+
try {
|
|
257
|
+
const reply = await request(SUBJECTS.echo(orgId), { message });
|
|
258
|
+
setResponse(JSON.stringify(reply, null, 2));
|
|
259
|
+
} catch (err) {
|
|
260
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
261
|
+
} finally {
|
|
262
|
+
setLoading(false);
|
|
263
|
+
}
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
return (
|
|
267
|
+
<div className="flex max-w-2xl flex-col gap-8 text-gray-1000">
|
|
268
|
+
<section>
|
|
269
|
+
<SectionHeader title="NATS Echo Test" description="Send a message to the echo service and see the response.">
|
|
270
|
+
<span
|
|
271
|
+
className={`inline-flex items-center gap-1.5 rounded-full px-2 py-0.5 text-copy-12 font-medium ${
|
|
272
|
+
connected ? 'bg-green-100 text-green-900' : 'bg-red-100 text-red-900'
|
|
273
|
+
}`}
|
|
274
|
+
>
|
|
275
|
+
<span className={`h-1.5 w-1.5 rounded-full ${connected ? 'bg-green-600' : 'bg-red-600'}`} />
|
|
276
|
+
{connected ? 'Connected' : 'Disconnected'}
|
|
277
|
+
</span>
|
|
278
|
+
</SectionHeader>
|
|
279
|
+
|
|
280
|
+
<div className="flex flex-col gap-4">
|
|
281
|
+
<div className="flex gap-2">
|
|
282
|
+
<Input
|
|
283
|
+
size="small"
|
|
284
|
+
placeholder="Type a message..."
|
|
285
|
+
value={message}
|
|
286
|
+
onChange={(e) => setMessage(e.target.value)}
|
|
287
|
+
onKeyDown={(e) => {
|
|
288
|
+
if (e.key === 'Enter') handleSendEcho();
|
|
289
|
+
}}
|
|
290
|
+
aria-label="Echo message"
|
|
291
|
+
/>
|
|
292
|
+
<Button size="small" variant="secondary" onClick={handleSendEcho} disabled={loading || !connected}>
|
|
293
|
+
{loading ? 'Sending...' : 'Send Echo'}
|
|
294
|
+
</Button>
|
|
295
|
+
</div>
|
|
296
|
+
|
|
297
|
+
{error && (
|
|
298
|
+
<Note type="error" size="small">
|
|
299
|
+
{error}
|
|
300
|
+
</Note>
|
|
301
|
+
)}
|
|
302
|
+
|
|
303
|
+
{response && (
|
|
304
|
+
<div className="rounded-lg border border-gray-alpha-200 bg-background-100 p-4">
|
|
305
|
+
<p className="mb-2 text-copy-12 font-medium text-gray-800">Response:</p>
|
|
306
|
+
<pre className="overflow-auto whitespace-pre-wrap break-all font-mono text-copy-13 text-gray-1000">
|
|
307
|
+
{response}
|
|
308
|
+
</pre>
|
|
309
|
+
</div>
|
|
310
|
+
)}
|
|
311
|
+
</div>
|
|
312
|
+
</section>
|
|
313
|
+
</div>
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// ---------------------------------------------------------------------------
|
|
318
|
+
// Keyboard Shortcuts Tab
|
|
319
|
+
// ---------------------------------------------------------------------------
|
|
320
|
+
|
|
321
|
+
const CATEGORY_LABELS: Record<ShortcutCategory, string> = {
|
|
322
|
+
window: 'Window Management',
|
|
323
|
+
workspace: 'Workspaces',
|
|
324
|
+
launcher: 'App Launcher',
|
|
325
|
+
terminal: 'Terminal',
|
|
326
|
+
system: 'System',
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
const CATEGORY_ORDER: ShortcutCategory[] = ['window', 'launcher', 'terminal', 'system'];
|
|
330
|
+
|
|
331
|
+
function ShortcutRecorder({
|
|
332
|
+
value,
|
|
333
|
+
onChange,
|
|
334
|
+
onReset,
|
|
335
|
+
isDefault,
|
|
336
|
+
}: {
|
|
337
|
+
value: KeyCombo | null;
|
|
338
|
+
onChange: (combo: KeyCombo) => void;
|
|
339
|
+
onReset: () => void;
|
|
340
|
+
isDefault: boolean;
|
|
341
|
+
}) {
|
|
342
|
+
const [recording, setRecording] = useState(false);
|
|
343
|
+
const setSuspended = useKeybindStore((s) => s.setSuspended);
|
|
344
|
+
|
|
345
|
+
useEffect(() => {
|
|
346
|
+
if (!recording) {
|
|
347
|
+
setSuspended(false);
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
setSuspended(true);
|
|
351
|
+
|
|
352
|
+
const handler = (e: KeyboardEvent) => {
|
|
353
|
+
e.preventDefault();
|
|
354
|
+
e.stopPropagation();
|
|
355
|
+
if (e.key === 'Escape') {
|
|
356
|
+
setRecording(false);
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
if (['Meta', 'Control', 'Alt', 'Shift'].includes(e.key)) return;
|
|
360
|
+
const modifiers: ModifierKey[] = [];
|
|
361
|
+
if (e.metaKey) modifiers.push('meta');
|
|
362
|
+
if (e.ctrlKey) modifiers.push('ctrl');
|
|
363
|
+
if (e.altKey) modifiers.push('alt');
|
|
364
|
+
if (e.shiftKey) modifiers.push('shift');
|
|
365
|
+
onChange({ key: e.key, modifiers });
|
|
366
|
+
setRecording(false);
|
|
367
|
+
};
|
|
368
|
+
const handleBlur = () => setRecording(false);
|
|
369
|
+
window.addEventListener('keydown', handler, { capture: true });
|
|
370
|
+
window.addEventListener('blur', handleBlur);
|
|
371
|
+
return () => {
|
|
372
|
+
window.removeEventListener('keydown', handler, { capture: true });
|
|
373
|
+
window.removeEventListener('blur', handleBlur);
|
|
374
|
+
setSuspended(false);
|
|
375
|
+
};
|
|
376
|
+
}, [recording, onChange, setSuspended]);
|
|
377
|
+
|
|
378
|
+
if (recording) {
|
|
379
|
+
return (
|
|
380
|
+
<div className="flex items-center gap-2">
|
|
381
|
+
<span className="animate-pulse rounded border border-blue-400 bg-blue-100 px-2 py-0.5 text-copy-13 text-blue-900">
|
|
382
|
+
Press a key combo...
|
|
383
|
+
</span>
|
|
384
|
+
<button className="text-copy-12 text-gray-700 hover:text-gray-1000" onClick={() => setRecording(false)}>
|
|
385
|
+
Cancel
|
|
386
|
+
</button>
|
|
387
|
+
</div>
|
|
388
|
+
);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
return (
|
|
392
|
+
<div className="flex items-center gap-2">
|
|
393
|
+
<button
|
|
394
|
+
className="rounded border border-gray-alpha-300 bg-background-100 px-2 py-0.5 font-mono text-copy-13 text-gray-1000 transition-colors hover:border-gray-alpha-400 hover:bg-gray-alpha-100"
|
|
395
|
+
onClick={() => setRecording(true)}
|
|
396
|
+
title="Click to rebind"
|
|
397
|
+
>
|
|
398
|
+
{value ? comboToSymbols(value) : <span className="text-gray-600">Disabled</span>}
|
|
399
|
+
</button>
|
|
400
|
+
{!isDefault && (
|
|
401
|
+
<button className="text-copy-12 text-gray-700 hover:text-gray-1000" onClick={onReset} title="Reset to default">
|
|
402
|
+
Reset
|
|
403
|
+
</button>
|
|
404
|
+
)}
|
|
405
|
+
</div>
|
|
406
|
+
);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function KeyboardShortcutsTab() {
|
|
410
|
+
const definitions = useKeybindStore((s) => s.definitions);
|
|
411
|
+
const overrides = useKeybindStore((s) => s.overrides);
|
|
412
|
+
const getBinding = useKeybindStore((s) => s.getBinding);
|
|
413
|
+
const setBinding = useKeybindStore((s) => s.setBinding);
|
|
414
|
+
const resetBinding = useKeybindStore((s) => s.resetBinding);
|
|
415
|
+
const resetAll = useKeybindStore((s) => s.resetAll);
|
|
416
|
+
const [search, setSearch] = useState('');
|
|
417
|
+
|
|
418
|
+
const hasOverrides = Object.keys(overrides).length > 0;
|
|
419
|
+
const filtered = search.trim()
|
|
420
|
+
? definitions.filter(
|
|
421
|
+
(d) =>
|
|
422
|
+
d.label.toLowerCase().includes(search.toLowerCase()) ||
|
|
423
|
+
d.description.toLowerCase().includes(search.toLowerCase())
|
|
424
|
+
)
|
|
425
|
+
: definitions;
|
|
426
|
+
|
|
427
|
+
const grouped = CATEGORY_ORDER.map((cat) => ({
|
|
428
|
+
category: cat,
|
|
429
|
+
label: CATEGORY_LABELS[cat],
|
|
430
|
+
shortcuts: filtered.filter((d) => d.category === cat),
|
|
431
|
+
})).filter((g) => g.shortcuts.length > 0);
|
|
432
|
+
|
|
433
|
+
return (
|
|
434
|
+
<div className="flex max-w-2xl flex-col gap-6 text-gray-1000">
|
|
435
|
+
<section>
|
|
436
|
+
<SectionHeader title="Keyboard Shortcuts" description="Customize keybindings. Click a shortcut to rebind it.">
|
|
437
|
+
{hasOverrides && (
|
|
438
|
+
<Button
|
|
439
|
+
size="small"
|
|
440
|
+
variant="secondary"
|
|
441
|
+
onClick={() => {
|
|
442
|
+
if (globalThis.confirm('Reset all shortcuts to defaults?')) resetAll();
|
|
443
|
+
}}
|
|
444
|
+
>
|
|
445
|
+
Reset All
|
|
446
|
+
</Button>
|
|
447
|
+
)}
|
|
448
|
+
</SectionHeader>
|
|
449
|
+
<Input
|
|
450
|
+
size="small"
|
|
451
|
+
placeholder="Search shortcuts..."
|
|
452
|
+
value={search}
|
|
453
|
+
onChange={(e) => setSearch(e.target.value)}
|
|
454
|
+
aria-label="Search keyboard shortcuts"
|
|
455
|
+
/>
|
|
456
|
+
</section>
|
|
457
|
+
|
|
458
|
+
{grouped.map((group) => (
|
|
459
|
+
<section key={group.category}>
|
|
460
|
+
<h3 className="mb-2 text-copy-13 font-medium text-gray-900">{group.label}</h3>
|
|
461
|
+
<div className="rounded-lg border border-gray-alpha-200 bg-background-100">
|
|
462
|
+
{group.shortcuts.map((def, i) => {
|
|
463
|
+
const binding = getBinding(def.id);
|
|
464
|
+
const isDefault = !(def.id in overrides);
|
|
465
|
+
return (
|
|
466
|
+
<div
|
|
467
|
+
key={def.id}
|
|
468
|
+
className={`flex items-center justify-between px-4 py-2.5 ${i > 0 ? 'border-t border-gray-alpha-100' : ''}`}
|
|
469
|
+
>
|
|
470
|
+
<div className="min-w-0 flex-1">
|
|
471
|
+
<p className="text-copy-13 text-gray-1000">{def.label}</p>
|
|
472
|
+
<p className="text-copy-12 text-gray-700">{def.description}</p>
|
|
473
|
+
</div>
|
|
474
|
+
<ShortcutRecorder
|
|
475
|
+
value={binding}
|
|
476
|
+
onChange={(combo) => setBinding(def.id, combo)}
|
|
477
|
+
onReset={() => resetBinding(def.id)}
|
|
478
|
+
isDefault={isDefault}
|
|
479
|
+
/>
|
|
480
|
+
</div>
|
|
481
|
+
);
|
|
482
|
+
})}
|
|
483
|
+
</div>
|
|
484
|
+
</section>
|
|
485
|
+
))}
|
|
486
|
+
|
|
487
|
+
{grouped.length === 0 && search && (
|
|
488
|
+
<EmptyState title="No matching shortcuts" description={`No shortcuts match "${search}".`} compact />
|
|
489
|
+
)}
|
|
490
|
+
</div>
|
|
491
|
+
);
|
|
492
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { type Static, Type } from '@sinclair/typebox';
|
|
2
|
+
|
|
3
|
+
// Note: userId is NOT included in request schemas.
|
|
4
|
+
// The WS bridge injects `_authUserId` from the authenticated session
|
|
5
|
+
// into all outbound NATS messages. Services read that field instead.
|
|
6
|
+
|
|
7
|
+
export const PtyCreateRequest = Type.Object({
|
|
8
|
+
sessionId: Type.Optional(Type.String()),
|
|
9
|
+
cols: Type.Optional(Type.Number()),
|
|
10
|
+
rows: Type.Optional(Type.Number()),
|
|
11
|
+
});
|
|
12
|
+
export type PtyCreateRequest = Static<typeof PtyCreateRequest>;
|
|
13
|
+
|
|
14
|
+
export const PtyCreateResponse = Type.Object({
|
|
15
|
+
sessionId: Type.String(),
|
|
16
|
+
created: Type.Boolean(),
|
|
17
|
+
});
|
|
18
|
+
export type PtyCreateResponse = Static<typeof PtyCreateResponse>;
|
|
19
|
+
|
|
20
|
+
export const PtyListRequest = Type.Object({});
|
|
21
|
+
export type PtyListRequest = Static<typeof PtyListRequest>;
|
|
22
|
+
|
|
23
|
+
export const PtyDestroyRequest = Type.Object({
|
|
24
|
+
sessionId: Type.String(),
|
|
25
|
+
});
|
|
26
|
+
export type PtyDestroyRequest = Static<typeof PtyDestroyRequest>;
|
|
27
|
+
|
|
28
|
+
export const PtyResizeRequest = Type.Object({
|
|
29
|
+
sessionId: Type.String(),
|
|
30
|
+
cols: Type.Number(),
|
|
31
|
+
rows: Type.Number(),
|
|
32
|
+
});
|
|
33
|
+
export type PtyResizeRequest = Static<typeof PtyResizeRequest>;
|
|
34
|
+
|
|
35
|
+
export const PtyReplayRequest = Type.Object({
|
|
36
|
+
sessionId: Type.String(),
|
|
37
|
+
});
|
|
38
|
+
export type PtyReplayRequest = Static<typeof PtyReplayRequest>;
|
|
39
|
+
|
|
40
|
+
export const PtyListResponse = Type.Object({
|
|
41
|
+
sessions: Type.Array(
|
|
42
|
+
Type.Object({
|
|
43
|
+
sessionId: Type.String(),
|
|
44
|
+
createdAt: Type.Number(),
|
|
45
|
+
lastActivity: Type.Number(),
|
|
46
|
+
bufferBytes: Type.Number(),
|
|
47
|
+
connected: Type.Boolean(),
|
|
48
|
+
})
|
|
49
|
+
),
|
|
50
|
+
});
|
|
51
|
+
export type PtyListResponse = Static<typeof PtyListResponse>;
|
|
52
|
+
|
|
53
|
+
export const PtyDataMessage = Type.Object({
|
|
54
|
+
sessionId: Type.String(),
|
|
55
|
+
data: Type.String(), // base64 encoded
|
|
56
|
+
});
|
|
57
|
+
export type PtyDataMessage = Static<typeof PtyDataMessage>;
|
|
58
|
+
|
|
59
|
+
export const PtyInputMessage = Type.Object({
|
|
60
|
+
sessionId: Type.String(),
|
|
61
|
+
data: Type.String(), // raw text
|
|
62
|
+
});
|
|
63
|
+
export type PtyInputMessage = Static<typeof PtyInputMessage>;
|
|
64
|
+
|
|
65
|
+
export const PtyExitMessage = Type.Object({
|
|
66
|
+
sessionId: Type.String(),
|
|
67
|
+
code: Type.Number(),
|
|
68
|
+
signal: Type.Optional(Type.Number()),
|
|
69
|
+
});
|
|
70
|
+
export type PtyExitMessage = Static<typeof PtyExitMessage>;
|
|
71
|
+
|
|
72
|
+
export const PtyBufferMessage = Type.Object({
|
|
73
|
+
sessionId: Type.String(),
|
|
74
|
+
data: Type.String(), // base64 encoded chunk
|
|
75
|
+
});
|
|
76
|
+
export type PtyBufferMessage = Static<typeof PtyBufferMessage>;
|
|
77
|
+
|
|
78
|
+
export const PtyBufferEndMessage = Type.Object({
|
|
79
|
+
sessionId: Type.String(),
|
|
80
|
+
error: Type.Optional(Type.String()),
|
|
81
|
+
});
|
|
82
|
+
export type PtyBufferEndMessage = Static<typeof PtyBufferEndMessage>;
|