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,438 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useEffect, useRef } from 'react';
|
|
4
|
+
import { APP_MANIFEST, type AppId } from '@/components/apps/app-manifest';
|
|
5
|
+
import { APP_REGISTRY } from '@/components/apps/app-registry';
|
|
6
|
+
import { useKhalAuth } from '@/lib/auth/use-auth';
|
|
7
|
+
import { claimCommand, deriveCommandId } from '@/lib/desktop/dedup';
|
|
8
|
+
import type { DesktopCmdNotify, DesktopCmdOpen, DesktopCmdWindow } from '@/lib/desktop/schema';
|
|
9
|
+
import { hasAppPermission } from '@/lib/hooks/use-launch-app';
|
|
10
|
+
import { useNats } from '@/lib/hooks/use-nats';
|
|
11
|
+
import { SUBJECTS } from '@/lib/subjects';
|
|
12
|
+
import { useNotificationStore } from '@/stores/notification-store';
|
|
13
|
+
import { useWindowStore } from '@/stores/window-store';
|
|
14
|
+
import type { WindowState } from '@/types/window';
|
|
15
|
+
|
|
16
|
+
let nextDesktopNotifId = 200_000;
|
|
17
|
+
|
|
18
|
+
function parseJson(data: unknown): Record<string, unknown> | null {
|
|
19
|
+
let obj = data;
|
|
20
|
+
if (typeof obj === 'string') {
|
|
21
|
+
try {
|
|
22
|
+
obj = JSON.parse(obj);
|
|
23
|
+
} catch {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
if (typeof obj !== 'object' || obj === null) return null;
|
|
28
|
+
return obj as Record<string, unknown>;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function windowEventPayload(win: WindowState) {
|
|
32
|
+
return {
|
|
33
|
+
windowId: win.id,
|
|
34
|
+
appId: win.appId,
|
|
35
|
+
title: win.title,
|
|
36
|
+
x: win.position.x,
|
|
37
|
+
y: win.position.y,
|
|
38
|
+
width: win.size.width,
|
|
39
|
+
height: win.size.height,
|
|
40
|
+
meta: win.meta,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// ── Command handlers (extracted for complexity) ──
|
|
45
|
+
|
|
46
|
+
function handleCmdOpen(payload: Record<string, unknown> | null, permissions: string[], cmdId: string) {
|
|
47
|
+
if (!payload) return;
|
|
48
|
+
const p = payload as unknown as DesktopCmdOpen;
|
|
49
|
+
if (!p.appId || !(p.appId in APP_REGISTRY)) return;
|
|
50
|
+
if (!hasAppPermission(p.appId, permissions)) return;
|
|
51
|
+
|
|
52
|
+
// Deterministic window ID from cmdId — all tabs derive the same ID,
|
|
53
|
+
// so even if dedup races and multiple tabs process the command,
|
|
54
|
+
// they all create the same window and duplicates are caught.
|
|
55
|
+
const windowId = `cmd-${cmdId}`;
|
|
56
|
+
const store = useWindowStore.getState();
|
|
57
|
+
if (store.getWindows().some((w) => w.id === windowId)) return;
|
|
58
|
+
|
|
59
|
+
const manifest = APP_MANIFEST[p.appId as AppId];
|
|
60
|
+
store.openWindow({
|
|
61
|
+
id: windowId,
|
|
62
|
+
title: p.title ?? manifest.label,
|
|
63
|
+
appId: p.appId,
|
|
64
|
+
width: p.width ?? manifest.defaultSize.width,
|
|
65
|
+
height: p.height ?? manifest.defaultSize.height,
|
|
66
|
+
meta: { ...p.meta, _cmdId: cmdId },
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function handleCmdWindow(payload: Record<string, unknown> | null, action: (id: string) => void) {
|
|
71
|
+
if (!payload) return;
|
|
72
|
+
const p = payload as unknown as DesktopCmdWindow;
|
|
73
|
+
if (!p.windowId) return;
|
|
74
|
+
action(p.windowId);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function handleCmdNotify(payload: Record<string, unknown> | null) {
|
|
78
|
+
if (!payload) return;
|
|
79
|
+
const p = payload as unknown as DesktopCmdNotify;
|
|
80
|
+
if (!p.summary) return;
|
|
81
|
+
useNotificationStore.getState().addNotification({
|
|
82
|
+
id: ++nextDesktopNotifId,
|
|
83
|
+
replacesId: 0,
|
|
84
|
+
summary: p.summary,
|
|
85
|
+
body: p.body ?? '',
|
|
86
|
+
icon: null,
|
|
87
|
+
actions: [],
|
|
88
|
+
expires: 0,
|
|
89
|
+
appName: p.appName,
|
|
90
|
+
urgency: p.urgency,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function handleCmdSync(orgId: string, userId: string, publish: (subject: string, data?: unknown) => void) {
|
|
95
|
+
const windows = useWindowStore.getState().getWindows();
|
|
96
|
+
publish(SUBJECTS.desktop.event.state(orgId, userId), {
|
|
97
|
+
windows: windows.map((w) => ({
|
|
98
|
+
id: w.id,
|
|
99
|
+
appId: w.appId,
|
|
100
|
+
title: w.title,
|
|
101
|
+
minimized: w.minimized,
|
|
102
|
+
maximized: w.maximized,
|
|
103
|
+
focused: w.focused,
|
|
104
|
+
position: w.position,
|
|
105
|
+
size: w.size,
|
|
106
|
+
zIndex: w.zIndex,
|
|
107
|
+
meta: w.meta,
|
|
108
|
+
})),
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// ── Event diffing (extracted for complexity) ──
|
|
113
|
+
|
|
114
|
+
function publishWindowEvents(
|
|
115
|
+
curr: WindowState[],
|
|
116
|
+
prev: WindowState[],
|
|
117
|
+
orgId: string,
|
|
118
|
+
userId: string,
|
|
119
|
+
publish: (subject: string, data?: unknown) => void
|
|
120
|
+
) {
|
|
121
|
+
const currMap = new Map(curr.map((w) => [w.id, w]));
|
|
122
|
+
const prevMap = new Map(prev.map((w) => [w.id, w]));
|
|
123
|
+
|
|
124
|
+
for (const w of curr) {
|
|
125
|
+
if (!prevMap.has(w.id) && !w.closing) {
|
|
126
|
+
publish(SUBJECTS.desktop.event.opened(orgId, userId), windowEventPayload(w));
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
for (const w of prev) {
|
|
131
|
+
if (!currMap.has(w.id)) {
|
|
132
|
+
publish(SUBJECTS.desktop.event.closed(orgId, userId), windowEventPayload(w));
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
for (const w of curr) {
|
|
137
|
+
const p = prevMap.get(w.id);
|
|
138
|
+
if (!p) continue;
|
|
139
|
+
publishStateChanges(w, p, orgId, userId, publish);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function publishStateChanges(
|
|
144
|
+
w: WindowState,
|
|
145
|
+
p: WindowState,
|
|
146
|
+
orgId: string,
|
|
147
|
+
userId: string,
|
|
148
|
+
publish: (subject: string, data?: unknown) => void
|
|
149
|
+
) {
|
|
150
|
+
const ev = windowEventPayload(w);
|
|
151
|
+
if (w.focused && !p.focused) {
|
|
152
|
+
publish(SUBJECTS.desktop.event.focused(orgId, userId), ev);
|
|
153
|
+
}
|
|
154
|
+
if (w.minimized && !p.minimized) {
|
|
155
|
+
publish(SUBJECTS.desktop.event.minimized(orgId, userId), ev);
|
|
156
|
+
}
|
|
157
|
+
if (w.maximized && !p.maximized) {
|
|
158
|
+
publish(SUBJECTS.desktop.event.maximized(orgId, userId), ev);
|
|
159
|
+
}
|
|
160
|
+
if ((!w.minimized && p.minimized) || (!w.maximized && p.maximized)) {
|
|
161
|
+
publish(SUBJECTS.desktop.event.restored(orgId, userId), ev);
|
|
162
|
+
}
|
|
163
|
+
if (w.position.x !== p.position.x || w.position.y !== p.position.y) {
|
|
164
|
+
publish(SUBJECTS.desktop.event.moved(orgId, userId), {
|
|
165
|
+
windowId: w.id,
|
|
166
|
+
x: w.position.x,
|
|
167
|
+
y: w.position.y,
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
if (w.size.width !== p.size.width || w.size.height !== p.size.height) {
|
|
171
|
+
publish(SUBJECTS.desktop.event.resized(orgId, userId), {
|
|
172
|
+
windowId: w.id,
|
|
173
|
+
width: w.size.width,
|
|
174
|
+
height: w.size.height,
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// ── Remote event handling (cross-tab sync) ──
|
|
180
|
+
|
|
181
|
+
type StoreActions = ReturnType<typeof useWindowStore.getState>;
|
|
182
|
+
|
|
183
|
+
interface RemoteWindow {
|
|
184
|
+
id: string;
|
|
185
|
+
appId: string;
|
|
186
|
+
title: string;
|
|
187
|
+
minimized: boolean;
|
|
188
|
+
maximized: boolean;
|
|
189
|
+
focused: boolean;
|
|
190
|
+
position?: { x: number; y: number };
|
|
191
|
+
size?: { width: number; height: number };
|
|
192
|
+
zIndex?: number;
|
|
193
|
+
meta?: Record<string, unknown>;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function handleRemoteOpened(payload: Record<string, unknown>, store: StoreActions) {
|
|
197
|
+
const windowId = payload.windowId as string;
|
|
198
|
+
if (!windowId) return;
|
|
199
|
+
const windows = store.getWindows();
|
|
200
|
+
|
|
201
|
+
// Skip if exact window ID already exists
|
|
202
|
+
if (windows.some((w) => w.id === windowId)) return;
|
|
203
|
+
|
|
204
|
+
// Skip if a window from the same command already exists (dedup race)
|
|
205
|
+
const meta = payload.meta as Record<string, unknown> | undefined;
|
|
206
|
+
const cmdId = meta?._cmdId;
|
|
207
|
+
if (cmdId && windows.some((w) => w.meta?._cmdId === cmdId)) return;
|
|
208
|
+
|
|
209
|
+
store.openWindow({
|
|
210
|
+
id: windowId,
|
|
211
|
+
appId: payload.appId as string,
|
|
212
|
+
title: payload.title as string,
|
|
213
|
+
width: (payload.width as number) || undefined,
|
|
214
|
+
height: (payload.height as number) || undefined,
|
|
215
|
+
meta: { ...meta, _awaitingMeta: true },
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function handleRemoteWindowAction(payload: Record<string, unknown>, action: (id: string) => void) {
|
|
220
|
+
const windowId = payload.windowId as string;
|
|
221
|
+
if (windowId) action(windowId);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function applyStateSync(payload: Record<string, unknown>, store: StoreActions) {
|
|
225
|
+
const remoteWindows = (payload.windows ?? []) as RemoteWindow[];
|
|
226
|
+
const localWindows = store.getWindows();
|
|
227
|
+
const localIds = new Set(localWindows.map((w) => w.id));
|
|
228
|
+
const remoteIds = new Set(remoteWindows.map((w) => w.id));
|
|
229
|
+
|
|
230
|
+
for (const rw of remoteWindows) {
|
|
231
|
+
if (!localIds.has(rw.id)) {
|
|
232
|
+
store.openWindow({
|
|
233
|
+
id: rw.id,
|
|
234
|
+
appId: rw.appId,
|
|
235
|
+
title: rw.title,
|
|
236
|
+
width: rw.size?.width,
|
|
237
|
+
height: rw.size?.height,
|
|
238
|
+
x: rw.position?.x,
|
|
239
|
+
y: rw.position?.y,
|
|
240
|
+
meta: rw.meta,
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
for (const lw of localWindows) {
|
|
246
|
+
if (!remoteIds.has(lw.id)) store.removeWindowImmediate(lw.id);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
reconcileFlags(remoteWindows, localIds, store);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function reconcileFlags(remoteWindows: RemoteWindow[], localIds: Set<string>, store: StoreActions) {
|
|
253
|
+
for (const rw of remoteWindows) {
|
|
254
|
+
if (!localIds.has(rw.id)) continue;
|
|
255
|
+
if (rw.focused) store.focusWindow(rw.id);
|
|
256
|
+
if (rw.minimized) store.minimizeWindow(rw.id);
|
|
257
|
+
if (rw.maximized) store.maximizeWindow(rw.id);
|
|
258
|
+
if (rw.position) store.moveWindow(rw.id, rw.position);
|
|
259
|
+
if (rw.size) store.resizeWindow(rw.id, rw.size);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function handleRemoteMetaUpdated(payload: Record<string, unknown>, store: StoreActions) {
|
|
264
|
+
const windowId = payload.windowId as string;
|
|
265
|
+
if (!windowId) return;
|
|
266
|
+
const meta = payload.meta as Record<string, unknown> | undefined;
|
|
267
|
+
if (!meta) return;
|
|
268
|
+
// Must explicitly set to undefined (not delete) so the spread merge
|
|
269
|
+
// in updateWindowMeta overrides the existing `true` value
|
|
270
|
+
meta._awaitingMeta = undefined;
|
|
271
|
+
store.updateWindowMeta(windowId, meta);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function handleRemoteEvent(event: string | undefined, payload: Record<string, unknown>, store: StoreActions) {
|
|
275
|
+
switch (event) {
|
|
276
|
+
case 'opened':
|
|
277
|
+
handleRemoteOpened(payload, store);
|
|
278
|
+
break;
|
|
279
|
+
case 'closed':
|
|
280
|
+
handleRemoteWindowAction(payload, store.removeWindowImmediate);
|
|
281
|
+
break;
|
|
282
|
+
case 'focused':
|
|
283
|
+
handleRemoteWindowAction(payload, store.focusWindow);
|
|
284
|
+
break;
|
|
285
|
+
case 'minimized':
|
|
286
|
+
handleRemoteWindowAction(payload, store.minimizeWindow);
|
|
287
|
+
break;
|
|
288
|
+
case 'maximized':
|
|
289
|
+
handleRemoteWindowAction(payload, store.maximizeWindow);
|
|
290
|
+
break;
|
|
291
|
+
case 'restored':
|
|
292
|
+
handleRemoteWindowAction(payload, store.restoreWindow);
|
|
293
|
+
break;
|
|
294
|
+
case 'moved':
|
|
295
|
+
handleRemoteWindowAction(payload, (id) =>
|
|
296
|
+
store.moveWindow(id, { x: payload.x as number, y: payload.y as number })
|
|
297
|
+
);
|
|
298
|
+
break;
|
|
299
|
+
case 'resized':
|
|
300
|
+
handleRemoteWindowAction(payload, (id) =>
|
|
301
|
+
store.resizeWindow(id, { width: payload.width as number, height: payload.height as number })
|
|
302
|
+
);
|
|
303
|
+
break;
|
|
304
|
+
case 'state':
|
|
305
|
+
applyStateSync(payload, store);
|
|
306
|
+
break;
|
|
307
|
+
case 'meta-updated':
|
|
308
|
+
handleRemoteMetaUpdated(payload, store);
|
|
309
|
+
break;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Bridges NATS commands → Zustand actions and Zustand state changes → NATS events.
|
|
315
|
+
* Mount once in DesktopShell.
|
|
316
|
+
*/
|
|
317
|
+
export function useDesktopNats() {
|
|
318
|
+
const { subscribe, publish, orgId, userId } = useNats();
|
|
319
|
+
const auth = useKhalAuth();
|
|
320
|
+
const permissions = auth?.permissions ?? [];
|
|
321
|
+
|
|
322
|
+
// Suppress event re-publishing when applying remote events
|
|
323
|
+
const syncingRef = useRef(false);
|
|
324
|
+
|
|
325
|
+
// Stable refs so the Zustand subscriber doesn't re-attach on every render
|
|
326
|
+
const publishRef = useRef(publish);
|
|
327
|
+
const orgIdRef = useRef(orgId);
|
|
328
|
+
const userIdRef = useRef(userId);
|
|
329
|
+
const permissionsRef = useRef(permissions);
|
|
330
|
+
useEffect(() => {
|
|
331
|
+
publishRef.current = publish;
|
|
332
|
+
}, [publish]);
|
|
333
|
+
useEffect(() => {
|
|
334
|
+
orgIdRef.current = orgId;
|
|
335
|
+
}, [orgId]);
|
|
336
|
+
useEffect(() => {
|
|
337
|
+
userIdRef.current = userId;
|
|
338
|
+
}, [userId]);
|
|
339
|
+
useEffect(() => {
|
|
340
|
+
permissionsRef.current = permissions;
|
|
341
|
+
}, [permissions]);
|
|
342
|
+
|
|
343
|
+
// ── Command handling (NATS → Zustand) ──
|
|
344
|
+
useEffect(() => {
|
|
345
|
+
if (!orgId || !userId) return;
|
|
346
|
+
|
|
347
|
+
const subject = SUBJECTS.desktop.cmd.all(orgId, userId);
|
|
348
|
+
const store = useWindowStore.getState();
|
|
349
|
+
|
|
350
|
+
const unsub = subscribe(subject, (data: unknown, fullSubject: string) => {
|
|
351
|
+
const cmd = fullSubject.split('.').pop();
|
|
352
|
+
const payload = parseJson(data);
|
|
353
|
+
|
|
354
|
+
// Dedup: derive or extract cmdId, claim via localStorage
|
|
355
|
+
const rawPayload = typeof data === 'string' ? data : JSON.stringify(data);
|
|
356
|
+
const cmdId =
|
|
357
|
+
((payload as Record<string, unknown> | null)?._cmdId as string) ?? deriveCommandId(fullSubject, rawPayload);
|
|
358
|
+
if (!claimCommand(cmdId)) return;
|
|
359
|
+
|
|
360
|
+
switch (cmd) {
|
|
361
|
+
case 'open':
|
|
362
|
+
handleCmdOpen(payload, permissionsRef.current, cmdId);
|
|
363
|
+
break;
|
|
364
|
+
case 'close':
|
|
365
|
+
handleCmdWindow(payload, store.closeWindow);
|
|
366
|
+
break;
|
|
367
|
+
case 'focus':
|
|
368
|
+
handleCmdWindow(payload, store.focusWindow);
|
|
369
|
+
break;
|
|
370
|
+
case 'minimize':
|
|
371
|
+
handleCmdWindow(payload, store.minimizeWindow);
|
|
372
|
+
break;
|
|
373
|
+
case 'maximize':
|
|
374
|
+
handleCmdWindow(payload, store.maximizeWindow);
|
|
375
|
+
break;
|
|
376
|
+
case 'restore':
|
|
377
|
+
handleCmdWindow(payload, store.restoreWindow);
|
|
378
|
+
break;
|
|
379
|
+
case 'notify':
|
|
380
|
+
handleCmdNotify(payload);
|
|
381
|
+
break;
|
|
382
|
+
case 'sync': {
|
|
383
|
+
const org = orgIdRef.current;
|
|
384
|
+
const uid = userIdRef.current;
|
|
385
|
+
if (org && uid) handleCmdSync(org, uid, publishRef.current);
|
|
386
|
+
break;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
return unsub;
|
|
392
|
+
}, [orgId, userId, subscribe]);
|
|
393
|
+
|
|
394
|
+
// ── Event publishing (Zustand → NATS) ──
|
|
395
|
+
useEffect(() => {
|
|
396
|
+
if (!orgId || !userId) return;
|
|
397
|
+
|
|
398
|
+
const unsub = useWindowStore.subscribe((state, prevState) => {
|
|
399
|
+
if (syncingRef.current) return;
|
|
400
|
+
const org = orgIdRef.current;
|
|
401
|
+
const uid = userIdRef.current;
|
|
402
|
+
if (!org || !uid) return;
|
|
403
|
+
const wsId = state.activeWorkspaceId;
|
|
404
|
+
if (!wsId) return;
|
|
405
|
+
|
|
406
|
+
const curr = state.windowsByWorkspace[wsId] ?? [];
|
|
407
|
+
const prev = prevState.windowsByWorkspace[wsId] ?? [];
|
|
408
|
+
publishWindowEvents(curr, prev, org, uid, publishRef.current);
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
return unsub;
|
|
412
|
+
}, [orgId, userId]);
|
|
413
|
+
|
|
414
|
+
// ── Remote event subscription (cross-tab sync) ──
|
|
415
|
+
useEffect(() => {
|
|
416
|
+
if (!orgId || !userId) return;
|
|
417
|
+
|
|
418
|
+
const subject = SUBJECTS.desktop.event.all(orgId, userId);
|
|
419
|
+
const store = useWindowStore.getState();
|
|
420
|
+
|
|
421
|
+
const unsub = subscribe(subject, (data: unknown, fullSubject: string) => {
|
|
422
|
+
const payload = parseJson(data);
|
|
423
|
+
if (!payload) return;
|
|
424
|
+
|
|
425
|
+
syncingRef.current = true;
|
|
426
|
+
try {
|
|
427
|
+
handleRemoteEvent(fullSubject.split('.').pop(), payload, store);
|
|
428
|
+
} finally {
|
|
429
|
+
syncingRef.current = false;
|
|
430
|
+
}
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
// Request full state from existing tabs
|
|
434
|
+
publishRef.current(SUBJECTS.desktop.cmd.sync(orgId, userId), {});
|
|
435
|
+
|
|
436
|
+
return unsub;
|
|
437
|
+
}, [orgId, userId, subscribe]);
|
|
438
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useSyncExternalStore } from 'react';
|
|
4
|
+
|
|
5
|
+
const MOBILE_BREAKPOINT = 768;
|
|
6
|
+
|
|
7
|
+
function subscribe(callback: () => void) {
|
|
8
|
+
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`);
|
|
9
|
+
mql.addEventListener('change', callback);
|
|
10
|
+
return () => mql.removeEventListener('change', callback);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function getSnapshot() {
|
|
14
|
+
return window.innerWidth < MOBILE_BREAKPOINT;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function getServerSnapshot() {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function useIsMobile() {
|
|
22
|
+
return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
|
|
23
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { useCallback } from 'react';
|
|
2
|
+
import type { AppId } from '@/components/apps/app-registry';
|
|
3
|
+
import { APP_REGISTRY } from '@/components/apps/app-registry';
|
|
4
|
+
import { useKhalAuth } from '@/lib/auth/use-auth';
|
|
5
|
+
import { useNotificationStore } from '@/stores/notification-store';
|
|
6
|
+
import { useWindowStore } from '@/stores/window-store';
|
|
7
|
+
import type { DesktopEntry } from '@/types/desktop-entry';
|
|
8
|
+
|
|
9
|
+
/** Detect whether we are running inside a Tauri webview. */
|
|
10
|
+
function isTauri(): boolean {
|
|
11
|
+
return typeof window !== 'undefined' && '__TAURI__' in window;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Resolve the canonical app identifier for a desktop entry.
|
|
16
|
+
*/
|
|
17
|
+
export function getAppId(entry: DesktopEntry): string {
|
|
18
|
+
return entry.component || entry.exec || entry.id;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const DEFAULT_WINDOW_SIZE = { width: 800, height: 600 };
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Check whether the given permissions list allows opening an app.
|
|
25
|
+
* Returns true if the app has no requiredPermission or the permission is present.
|
|
26
|
+
*/
|
|
27
|
+
export function hasAppPermission(appId: string, permissions: string[]): boolean {
|
|
28
|
+
const registryEntry = APP_REGISTRY[appId as AppId];
|
|
29
|
+
if (!registryEntry?.requiredPermission) return true;
|
|
30
|
+
return permissions.includes(registryEntry.requiredPermission);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Hook that returns a stable callback to launch any desktop entry.
|
|
35
|
+
* Only builtin apps (registered in APP_REGISTRY) are supported.
|
|
36
|
+
* Checks user permissions before opening; shows a notification if blocked.
|
|
37
|
+
*/
|
|
38
|
+
export function useLaunchApp() {
|
|
39
|
+
const openWindow = useWindowStore((s) => s.openWindow);
|
|
40
|
+
const auth = useKhalAuth();
|
|
41
|
+
const permissions = auth?.permissions ?? [];
|
|
42
|
+
|
|
43
|
+
const launch = useCallback(
|
|
44
|
+
(entry: DesktopEntry) => {
|
|
45
|
+
const appId = getAppId(entry);
|
|
46
|
+
|
|
47
|
+
// Permission gate: block if user lacks required permission
|
|
48
|
+
if (!hasAppPermission(appId, permissions)) {
|
|
49
|
+
useNotificationStore.getState().addNotification({
|
|
50
|
+
id: Date.now(),
|
|
51
|
+
replacesId: 0,
|
|
52
|
+
summary: 'Permission Denied',
|
|
53
|
+
body: `You do not have permission to open ${entry.name}.`,
|
|
54
|
+
icon: null,
|
|
55
|
+
actions: [],
|
|
56
|
+
expires: 4000,
|
|
57
|
+
urgency: 'normal',
|
|
58
|
+
});
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (appId in APP_REGISTRY) {
|
|
63
|
+
// Tauri standalone: delegate window creation to the native shell
|
|
64
|
+
if (isTauri()) {
|
|
65
|
+
// biome-ignore lint/suspicious/noExplicitAny: Tauri global is untyped
|
|
66
|
+
(window as any).__TAURI__.core.invoke('open_app_window', { appId, title: entry.name });
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const registryEntry = APP_REGISTRY[appId as AppId];
|
|
71
|
+
const { width, height } = registryEntry?.defaultSize ?? DEFAULT_WINDOW_SIZE;
|
|
72
|
+
openWindow({ title: entry.name, appId, width, height });
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
[openWindow, permissions]
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
return launch;
|
|
79
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useCallback, useEffect, useRef } from 'react';
|
|
4
|
+
import { useKhalAuth } from '@/lib/auth/use-auth';
|
|
5
|
+
import { useNats } from '@/lib/hooks/use-nats';
|
|
6
|
+
import type { NatsNotification } from '@/lib/notifications/schema';
|
|
7
|
+
import { SUBJECTS } from '@/lib/subjects';
|
|
8
|
+
import { useNotificationStore } from '@/stores/notification-store';
|
|
9
|
+
|
|
10
|
+
let nextNatsNotifId = 100_000;
|
|
11
|
+
|
|
12
|
+
function parseNotification(data: unknown): NatsNotification | null {
|
|
13
|
+
let obj = data;
|
|
14
|
+
// Bridge may deliver payload as a JSON string — parse it
|
|
15
|
+
if (typeof obj === 'string') {
|
|
16
|
+
try {
|
|
17
|
+
obj = JSON.parse(obj);
|
|
18
|
+
} catch {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
if (typeof obj !== 'object' || obj === null) return null;
|
|
23
|
+
const rec = obj as Record<string, unknown>;
|
|
24
|
+
if (typeof rec.summary !== 'string' || rec.summary.length === 0) return null;
|
|
25
|
+
return rec as unknown as NatsNotification;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function useNatsNotifications() {
|
|
29
|
+
const { subscribe, orgId } = useNats();
|
|
30
|
+
const auth = useKhalAuth();
|
|
31
|
+
const userId = auth?.userId ?? '';
|
|
32
|
+
|
|
33
|
+
const addNotification = useNotificationStore((s) => s.addNotification);
|
|
34
|
+
|
|
35
|
+
const handleMessage = useCallback(
|
|
36
|
+
(data: unknown) => {
|
|
37
|
+
const notif = parseNotification(data);
|
|
38
|
+
if (!notif) {
|
|
39
|
+
// biome-ignore lint/suspicious/noConsole: intentional warning for invalid NATS payloads
|
|
40
|
+
console.warn('[nats-notifications] invalid payload, ignoring:', data);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const id = ++nextNatsNotifId;
|
|
45
|
+
addNotification({
|
|
46
|
+
id,
|
|
47
|
+
replacesId: 0,
|
|
48
|
+
summary: notif.summary,
|
|
49
|
+
body: notif.body ?? '',
|
|
50
|
+
icon: notif.icon ?? null,
|
|
51
|
+
actions: [],
|
|
52
|
+
expires: 0,
|
|
53
|
+
appName: notif.appName,
|
|
54
|
+
urgency: notif.urgency,
|
|
55
|
+
category: notif.category,
|
|
56
|
+
transient: notif.transient,
|
|
57
|
+
});
|
|
58
|
+
},
|
|
59
|
+
[addNotification]
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
const unsubsRef = useRef<Array<() => void>>([]);
|
|
63
|
+
|
|
64
|
+
useEffect(() => {
|
|
65
|
+
if (!orgId) return;
|
|
66
|
+
|
|
67
|
+
const unsubs: Array<() => void> = [];
|
|
68
|
+
|
|
69
|
+
// Broadcast — all users in the org
|
|
70
|
+
unsubs.push(subscribe(SUBJECTS.notify.broadcast(orgId), handleMessage));
|
|
71
|
+
|
|
72
|
+
// User-specific — only for the authenticated user
|
|
73
|
+
if (userId) {
|
|
74
|
+
unsubs.push(subscribe(SUBJECTS.notify.user(orgId, userId), handleMessage));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
unsubsRef.current = unsubs;
|
|
78
|
+
|
|
79
|
+
return () => {
|
|
80
|
+
for (const unsub of unsubs) unsub();
|
|
81
|
+
unsubsRef.current = [];
|
|
82
|
+
};
|
|
83
|
+
}, [orgId, userId, subscribe, handleMessage]);
|
|
84
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useCallback, useEffect, useSyncExternalStore } from 'react';
|
|
4
|
+
import { useKhalAuth } from '@/lib/auth/use-auth';
|
|
5
|
+
import { getNatsClient } from '@/lib/nats-client';
|
|
6
|
+
|
|
7
|
+
export function useNats() {
|
|
8
|
+
const client = getNatsClient();
|
|
9
|
+
const auth = useKhalAuth();
|
|
10
|
+
const orgId = auth?.orgId ?? '';
|
|
11
|
+
const userId = auth?.userId ?? '';
|
|
12
|
+
|
|
13
|
+
// Keep the client's identity in sync with auth state
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
if (orgId) client.setOrgId(orgId);
|
|
16
|
+
if (userId) client.setUserId(userId);
|
|
17
|
+
}, [client, orgId, userId]);
|
|
18
|
+
|
|
19
|
+
// Track connection status reactively
|
|
20
|
+
const connected = useSyncExternalStore(
|
|
21
|
+
(callback) => client.onStatusChange(callback),
|
|
22
|
+
() => client.connected,
|
|
23
|
+
() => false // SSR snapshot
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
// subscribe: wraps client.subscribe, auto-unsubscribes on unmount
|
|
27
|
+
const subscribe = useCallback(
|
|
28
|
+
(subject: string, callback: (data: unknown, subject: string) => void) => {
|
|
29
|
+
return client.subscribe(subject, callback);
|
|
30
|
+
},
|
|
31
|
+
[client]
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
// publish: fire-and-forget
|
|
35
|
+
const publish = useCallback(
|
|
36
|
+
(subject: string, data?: unknown) => {
|
|
37
|
+
client.publish(subject, data);
|
|
38
|
+
},
|
|
39
|
+
[client]
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
// request: returns Promise<unknown>
|
|
43
|
+
const request = useCallback(
|
|
44
|
+
(subject: string, data?: unknown, timeoutMs?: number) => {
|
|
45
|
+
return client.request(subject, data, timeoutMs);
|
|
46
|
+
},
|
|
47
|
+
[client]
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
return { connected, subscribe, publish, request, orgId, userId };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Convenience hook for subscribing in useEffect with auto-cleanup
|
|
54
|
+
export function useNatsSubscription(subject: string, callback: (data: unknown, subject: string) => void) {
|
|
55
|
+
const { subscribe } = useNats();
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
const unsub = subscribe(subject, callback);
|
|
58
|
+
return unsub;
|
|
59
|
+
}, [subject, callback, subscribe]);
|
|
60
|
+
}
|