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,112 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState } from 'react';
|
|
4
|
+
import type { SplitNode } from '../types';
|
|
5
|
+
import { SplitDragHandle } from './SplitDragHandle';
|
|
6
|
+
import { TerminalPane } from './TerminalPane';
|
|
7
|
+
|
|
8
|
+
interface SplitPaneRendererProps {
|
|
9
|
+
node: SplitNode;
|
|
10
|
+
focusedPaneId: string;
|
|
11
|
+
onPaneFocus: (paneId: string) => void;
|
|
12
|
+
onSessionIdChange: (paneId: string, sessionId: string) => void;
|
|
13
|
+
onKeyboardShortcut?: (event: KeyboardEvent) => boolean;
|
|
14
|
+
onRatioChange?: (nodeId: string, ratio: number) => void;
|
|
15
|
+
onCwdChange?: (paneId: string, cwd: string) => void;
|
|
16
|
+
onLastCommandChange?: (paneId: string, command: string) => void;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Recursive renderer for split pane tree.
|
|
21
|
+
* Branch nodes render two children with a draggable divider.
|
|
22
|
+
* Leaf nodes render a TerminalPane component.
|
|
23
|
+
*/
|
|
24
|
+
export function SplitPaneRenderer({
|
|
25
|
+
node,
|
|
26
|
+
focusedPaneId,
|
|
27
|
+
onPaneFocus,
|
|
28
|
+
onSessionIdChange,
|
|
29
|
+
onKeyboardShortcut,
|
|
30
|
+
onRatioChange,
|
|
31
|
+
onCwdChange,
|
|
32
|
+
onLastCommandChange,
|
|
33
|
+
}: SplitPaneRendererProps) {
|
|
34
|
+
const [localRatio, setLocalRatio] = useState(0.5);
|
|
35
|
+
|
|
36
|
+
if (node.type === 'leaf') {
|
|
37
|
+
return (
|
|
38
|
+
<TerminalPane
|
|
39
|
+
paneId={node.id}
|
|
40
|
+
ptySessionId={node.ptySessionId}
|
|
41
|
+
isFocused={node.id === focusedPaneId}
|
|
42
|
+
onFocus={() => onPaneFocus(node.id)}
|
|
43
|
+
onSessionIdChange={(sessionId) => onSessionIdChange(node.id, sessionId)}
|
|
44
|
+
onKeyboardShortcut={onKeyboardShortcut}
|
|
45
|
+
onCwdChange={onCwdChange ? (cwd) => onCwdChange(node.id, cwd) : undefined}
|
|
46
|
+
onLastCommandChange={onLastCommandChange ? (cmd) => onLastCommandChange(node.id, cmd) : undefined}
|
|
47
|
+
/>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Branch node: render two children with divider
|
|
52
|
+
const ratio = node.ratio ?? localRatio;
|
|
53
|
+
const isVertical = node.direction === 'vertical';
|
|
54
|
+
|
|
55
|
+
const handleRatioChange = (newRatio: number) => {
|
|
56
|
+
setLocalRatio(newRatio);
|
|
57
|
+
if (onRatioChange) {
|
|
58
|
+
onRatioChange(node.id, newRatio);
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
return (
|
|
63
|
+
<div
|
|
64
|
+
className={`
|
|
65
|
+
split-container
|
|
66
|
+
flex ${isVertical ? 'flex-row' : 'flex-col'}
|
|
67
|
+
h-full w-full
|
|
68
|
+
`}
|
|
69
|
+
>
|
|
70
|
+
{/* First child */}
|
|
71
|
+
<div
|
|
72
|
+
style={{
|
|
73
|
+
flex: `0 0 ${ratio * 100}%`,
|
|
74
|
+
overflow: 'hidden',
|
|
75
|
+
}}
|
|
76
|
+
>
|
|
77
|
+
<SplitPaneRenderer
|
|
78
|
+
node={node.children[0]}
|
|
79
|
+
focusedPaneId={focusedPaneId}
|
|
80
|
+
onPaneFocus={onPaneFocus}
|
|
81
|
+
onSessionIdChange={onSessionIdChange}
|
|
82
|
+
onKeyboardShortcut={onKeyboardShortcut}
|
|
83
|
+
onRatioChange={onRatioChange}
|
|
84
|
+
onCwdChange={onCwdChange}
|
|
85
|
+
onLastCommandChange={onLastCommandChange}
|
|
86
|
+
/>
|
|
87
|
+
</div>
|
|
88
|
+
|
|
89
|
+
{/* Drag handle */}
|
|
90
|
+
<SplitDragHandle direction={node.direction} onRatioChange={handleRatioChange} />
|
|
91
|
+
|
|
92
|
+
{/* Second child */}
|
|
93
|
+
<div
|
|
94
|
+
style={{
|
|
95
|
+
flex: 1,
|
|
96
|
+
overflow: 'hidden',
|
|
97
|
+
}}
|
|
98
|
+
>
|
|
99
|
+
<SplitPaneRenderer
|
|
100
|
+
node={node.children[1]}
|
|
101
|
+
focusedPaneId={focusedPaneId}
|
|
102
|
+
onPaneFocus={onPaneFocus}
|
|
103
|
+
onSessionIdChange={onSessionIdChange}
|
|
104
|
+
onKeyboardShortcut={onKeyboardShortcut}
|
|
105
|
+
onRatioChange={onRatioChange}
|
|
106
|
+
onCwdChange={onCwdChange}
|
|
107
|
+
onLastCommandChange={onLastCommandChange}
|
|
108
|
+
/>
|
|
109
|
+
</div>
|
|
110
|
+
</div>
|
|
111
|
+
);
|
|
112
|
+
}
|
|
@@ -0,0 +1,478 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import type { FitAddon } from '@xterm/addon-fit';
|
|
4
|
+
import type { WebglAddon } from '@xterm/addon-webgl';
|
|
5
|
+
import type { Terminal } from '@xterm/xterm';
|
|
6
|
+
import { useEffect, useRef } from 'react';
|
|
7
|
+
import { useNats } from '@/lib/hooks/use-nats';
|
|
8
|
+
import { SUBJECTS } from '@/lib/subjects';
|
|
9
|
+
import { useThemeStore } from '@/stores/theme-store';
|
|
10
|
+
|
|
11
|
+
/** Resolve a CSS custom property from :root to its computed value. */
|
|
12
|
+
function resolveVar(name: string, fallback: string): string {
|
|
13
|
+
if (typeof document === 'undefined') return fallback;
|
|
14
|
+
const v = getComputedStyle(document.documentElement).getPropertyValue(name).trim();
|
|
15
|
+
return v || fallback;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Build xterm color theme from current CSS custom properties. */
|
|
19
|
+
function buildTerminalTheme(focused: boolean) {
|
|
20
|
+
const bg = resolveVar('--os-terminal-bg', '#0a0a0a');
|
|
21
|
+
const fg = resolveVar('--os-text-primary', '#e0e0e0');
|
|
22
|
+
const muted = resolveVar('--os-text-muted', '#808080');
|
|
23
|
+
const accent = resolveVar('--os-accent-primary', '#39ff14');
|
|
24
|
+
return {
|
|
25
|
+
background: bg,
|
|
26
|
+
foreground: focused ? fg : muted,
|
|
27
|
+
cursor: focused ? accent : muted,
|
|
28
|
+
cursorAccent: bg,
|
|
29
|
+
selectionBackground: `${accent}33`, // accent with ~20% alpha
|
|
30
|
+
selectionForeground: fg,
|
|
31
|
+
// ANSI palette from CSS vars
|
|
32
|
+
black: resolveVar('--os-ansi-black', '#0a0a0a'),
|
|
33
|
+
red: resolveVar('--os-ansi-red', '#ff5555'),
|
|
34
|
+
green: resolveVar('--os-ansi-green', '#39ff14'),
|
|
35
|
+
yellow: resolveVar('--os-ansi-yellow', '#f1fa8c'),
|
|
36
|
+
blue: resolveVar('--os-ansi-blue', '#6272a4'),
|
|
37
|
+
magenta: resolveVar('--os-ansi-magenta', '#ff79c6'),
|
|
38
|
+
cyan: resolveVar('--os-ansi-cyan', '#8be9fd'),
|
|
39
|
+
white: resolveVar('--os-ansi-white', '#e0e0e0'),
|
|
40
|
+
brightBlack: resolveVar('--os-ansi-bright-black', '#555555'),
|
|
41
|
+
brightRed: resolveVar('--os-ansi-bright-red', '#ff6e6e'),
|
|
42
|
+
brightGreen: resolveVar('--os-ansi-bright-green', '#69ff69'),
|
|
43
|
+
brightYellow: resolveVar('--os-ansi-bright-yellow', '#ffffa5'),
|
|
44
|
+
brightBlue: resolveVar('--os-ansi-bright-blue', '#d6acff'),
|
|
45
|
+
brightMagenta: resolveVar('--os-ansi-bright-magenta', '#ff92df'),
|
|
46
|
+
brightCyan: resolveVar('--os-ansi-bright-cyan', '#a4ffff'),
|
|
47
|
+
brightWhite: resolveVar('--os-ansi-bright-white', '#ffffff'),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function decodeBase64(b64: string): Uint8Array {
|
|
52
|
+
const binary = atob(b64);
|
|
53
|
+
const bytes = new Uint8Array(binary.length);
|
|
54
|
+
for (let i = 0; i < binary.length; i++) {
|
|
55
|
+
bytes[i] = binary.charCodeAt(i);
|
|
56
|
+
}
|
|
57
|
+
return bytes;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
interface TerminalPaneProps {
|
|
61
|
+
paneId: string;
|
|
62
|
+
ptySessionId: string | null;
|
|
63
|
+
isFocused: boolean;
|
|
64
|
+
onFocus: () => void;
|
|
65
|
+
onSessionIdChange: (sessionId: string) => void;
|
|
66
|
+
onKeyboardShortcut?: (event: KeyboardEvent) => boolean; // Return false to prevent xterm processing
|
|
67
|
+
onCwdChange?: (cwd: string) => void; // OSC 7 CWD updates
|
|
68
|
+
onLastCommandChange?: (command: string) => void; // Track last command entered
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Single terminal pane (leaf in split tree).
|
|
73
|
+
* Manages one xterm.js instance connected to one PTY session via NATS.
|
|
74
|
+
*/
|
|
75
|
+
export function TerminalPane({
|
|
76
|
+
paneId,
|
|
77
|
+
ptySessionId,
|
|
78
|
+
isFocused,
|
|
79
|
+
onFocus,
|
|
80
|
+
onSessionIdChange,
|
|
81
|
+
onKeyboardShortcut,
|
|
82
|
+
onCwdChange,
|
|
83
|
+
onLastCommandChange,
|
|
84
|
+
}: TerminalPaneProps) {
|
|
85
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
86
|
+
const terminalRef = useRef<Terminal | null>(null);
|
|
87
|
+
const fitAddonRef = useRef<FitAddon | null>(null);
|
|
88
|
+
const webglAddonRef = useRef<WebglAddon | null>(null);
|
|
89
|
+
const sessionIdRef = useRef<string | null>(null);
|
|
90
|
+
const unsubsRef = useRef<Array<() => void>>([]);
|
|
91
|
+
const bufferReplayedRef = useRef(false);
|
|
92
|
+
const inputBufferRef = useRef<string>(''); // Track input between Enter keystrokes
|
|
93
|
+
// Dedup + debounce resize to avoid rapid SIGWINCH bursts (each causes bash prompt redraw)
|
|
94
|
+
const lastSentDimsRef = useRef('');
|
|
95
|
+
const resizeTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
96
|
+
// Settle period: suppress all resize→PTY events for 500ms after creation.
|
|
97
|
+
// DOM layout can reflow multiple times as the window manager positions/sizes the
|
|
98
|
+
// window, each triggering fit()→onResize. Without suppression, each gets through
|
|
99
|
+
// the 150ms debounce if reflows are spread across multiple debounce windows,
|
|
100
|
+
// resulting in 2-3 SIGWINCHs → duplicate bash prompts on the same line.
|
|
101
|
+
const settleTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
102
|
+
|
|
103
|
+
// Latest-ref pattern: callbacks in refs prevent the main useEffect from re-running
|
|
104
|
+
// when parent re-renders with new function references
|
|
105
|
+
const onSessionIdChangeRef = useRef(onSessionIdChange);
|
|
106
|
+
onSessionIdChangeRef.current = onSessionIdChange;
|
|
107
|
+
const onKeyboardShortcutRef = useRef(onKeyboardShortcut);
|
|
108
|
+
onKeyboardShortcutRef.current = onKeyboardShortcut;
|
|
109
|
+
const onCwdChangeRef = useRef(onCwdChange);
|
|
110
|
+
onCwdChangeRef.current = onCwdChange;
|
|
111
|
+
const onLastCommandChangeRef = useRef(onLastCommandChange);
|
|
112
|
+
onLastCommandChangeRef.current = onLastCommandChange;
|
|
113
|
+
// Capture initial ptySessionId -- don't re-run effect when our own onSessionIdChange updates it
|
|
114
|
+
const ptySessionIdRef = useRef(ptySessionId);
|
|
115
|
+
|
|
116
|
+
const { subscribe, publish, request, orgId } = useNats();
|
|
117
|
+
|
|
118
|
+
// Setup terminal instance
|
|
119
|
+
useEffect(() => {
|
|
120
|
+
// Local cancelled flag -- each effect invocation gets its own closure.
|
|
121
|
+
// In React StrictMode (mount -> cleanup -> remount), the first invocation's
|
|
122
|
+
// cancelled=true prevents its async work from proceeding, while the second
|
|
123
|
+
// invocation's cancelled=false allows it to run cleanly.
|
|
124
|
+
let cancelled = false;
|
|
125
|
+
|
|
126
|
+
if (!containerRef.current) return;
|
|
127
|
+
|
|
128
|
+
(async () => {
|
|
129
|
+
const [xtermMod, fitMod] = await Promise.all([import('@xterm/xterm'), import('@xterm/addon-fit')]);
|
|
130
|
+
|
|
131
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
132
|
+
// @ts-ignore
|
|
133
|
+
await import('@xterm/xterm/css/xterm.css');
|
|
134
|
+
|
|
135
|
+
if (cancelled || !containerRef.current) return;
|
|
136
|
+
|
|
137
|
+
const terminal = new xtermMod.Terminal({
|
|
138
|
+
cursorBlink: false,
|
|
139
|
+
cursorStyle: 'block',
|
|
140
|
+
fontSize: 14,
|
|
141
|
+
fontFamily: "'JetBrains Mono', 'Fira Code', 'Cascadia Code', 'Menlo', monospace",
|
|
142
|
+
lineHeight: 1.2,
|
|
143
|
+
scrollback: 5000,
|
|
144
|
+
allowProposedApi: true,
|
|
145
|
+
theme: buildTerminalTheme(isFocused),
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
terminalRef.current = terminal;
|
|
149
|
+
|
|
150
|
+
const fitAddon = new fitMod.FitAddon();
|
|
151
|
+
fitAddonRef.current = fitAddon;
|
|
152
|
+
terminal.loadAddon(fitAddon);
|
|
153
|
+
|
|
154
|
+
// Attach custom key event handler
|
|
155
|
+
terminal.attachCustomKeyEventHandler((event) => {
|
|
156
|
+
return onKeyboardShortcutRef.current?.(event) ?? true;
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
terminal.open(containerRef.current);
|
|
160
|
+
|
|
161
|
+
// Register OSC 7 handler for CWD tracking (shell integration)
|
|
162
|
+
// OSC 7 format: ESC ] 7 ; file://host/path BEL
|
|
163
|
+
terminal.parser.registerOscHandler(7, (data) => {
|
|
164
|
+
try {
|
|
165
|
+
const url = new URL(data);
|
|
166
|
+
if (url.protocol === 'file:' && url.pathname) {
|
|
167
|
+
const cwd = decodeURIComponent(url.pathname);
|
|
168
|
+
onCwdChangeRef.current?.(cwd);
|
|
169
|
+
}
|
|
170
|
+
} catch {
|
|
171
|
+
// Invalid OSC 7 format, ignore
|
|
172
|
+
}
|
|
173
|
+
return true;
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
// Fit synchronously so proposeDimensions returns the ACTUAL fitted
|
|
177
|
+
// size. If we defer to rAF, the PTY is created with pre-fit dims
|
|
178
|
+
// and the subsequent fit triggers a resize → SIGWINCH → extra prompt.
|
|
179
|
+
try {
|
|
180
|
+
fitAddon.fit();
|
|
181
|
+
} catch {
|
|
182
|
+
// fit() can throw if container has zero dimensions
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Get terminal dimensions for PTY creation (after fit — matches reality)
|
|
186
|
+
const dims = fitAddon.proposeDimensions();
|
|
187
|
+
const cols = dims?.cols ?? 80;
|
|
188
|
+
const rows = dims?.rows ?? 24;
|
|
189
|
+
|
|
190
|
+
// Guard: StrictMode cleanup may have fired during the sync calls above
|
|
191
|
+
if (cancelled) return;
|
|
192
|
+
|
|
193
|
+
// Create or reattach PTY session via NATS
|
|
194
|
+
const response = (await request(SUBJECTS.pty.create(orgId), {
|
|
195
|
+
sessionId: ptySessionIdRef.current || undefined,
|
|
196
|
+
cols,
|
|
197
|
+
rows,
|
|
198
|
+
})) as { sessionId: string; created: boolean };
|
|
199
|
+
|
|
200
|
+
if (cancelled) return;
|
|
201
|
+
|
|
202
|
+
const resolvedSessionId = response.sessionId;
|
|
203
|
+
sessionIdRef.current = resolvedSessionId;
|
|
204
|
+
// Record dims sent to PTY so we skip redundant resizes (avoids extra SIGWINCH)
|
|
205
|
+
lastSentDimsRef.current = `${cols}x${rows}`;
|
|
206
|
+
|
|
207
|
+
// Start settle period: suppress resize→PTY for 500ms while DOM settles.
|
|
208
|
+
// After 500ms, sync final dimensions if they've drifted.
|
|
209
|
+
settleTimerRef.current = setTimeout(() => {
|
|
210
|
+
settleTimerRef.current = null;
|
|
211
|
+
if (cancelled || !sessionIdRef.current) return;
|
|
212
|
+
const settled = fitAddonRef.current?.proposeDimensions();
|
|
213
|
+
if (!settled) return;
|
|
214
|
+
const finalKey = `${settled.cols}x${settled.rows}`;
|
|
215
|
+
if (lastSentDimsRef.current === finalKey) return;
|
|
216
|
+
lastSentDimsRef.current = finalKey;
|
|
217
|
+
publish(SUBJECTS.pty.resize(orgId, sessionIdRef.current), {
|
|
218
|
+
sessionId: sessionIdRef.current,
|
|
219
|
+
cols: settled.cols,
|
|
220
|
+
rows: settled.rows,
|
|
221
|
+
});
|
|
222
|
+
}, 500);
|
|
223
|
+
|
|
224
|
+
if (response.created) {
|
|
225
|
+
onSessionIdChangeRef.current(resolvedSessionId);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Subscribe to data (live output from PTY)
|
|
229
|
+
const unsubData = subscribe(SUBJECTS.pty.data(orgId, resolvedSessionId), (msg: unknown) => {
|
|
230
|
+
if (cancelled) return;
|
|
231
|
+
const { data } = msg as { sessionId: string; data: string };
|
|
232
|
+
const bytes = decodeBase64(data);
|
|
233
|
+
terminal.write(bytes);
|
|
234
|
+
|
|
235
|
+
// For new sessions, load WebGL after first data
|
|
236
|
+
if (response.created && !bufferReplayedRef.current) {
|
|
237
|
+
bufferReplayedRef.current = true;
|
|
238
|
+
(async () => {
|
|
239
|
+
try {
|
|
240
|
+
const webglMod = await import('@xterm/addon-webgl');
|
|
241
|
+
if (cancelled || !terminalRef.current) return;
|
|
242
|
+
const webglAddon = new webglMod.WebglAddon();
|
|
243
|
+
webglAddon.onContextLoss(() => {
|
|
244
|
+
webglAddon.dispose();
|
|
245
|
+
});
|
|
246
|
+
terminal.loadAddon(webglAddon);
|
|
247
|
+
webglAddonRef.current = webglAddon;
|
|
248
|
+
} catch {
|
|
249
|
+
// WebGL not available
|
|
250
|
+
}
|
|
251
|
+
})();
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
unsubsRef.current.push(unsubData);
|
|
255
|
+
|
|
256
|
+
// Buffer replay subscriptions — only for reattach.
|
|
257
|
+
// For new sessions, live data arrives via pty.data. Buffer replay is
|
|
258
|
+
// a broadcast (pty.buffer.{sessionId}), so if another tab reattaches
|
|
259
|
+
// and triggers replay, ALL subscribers receive it. Skipping these subs
|
|
260
|
+
// for new sessions prevents the creator from re-displaying already-visible
|
|
261
|
+
// output when other tabs replay.
|
|
262
|
+
if (!response.created) {
|
|
263
|
+
const unsubBuffer = subscribe(SUBJECTS.pty.buffer(orgId, resolvedSessionId), (msg: unknown) => {
|
|
264
|
+
if (cancelled) return;
|
|
265
|
+
const { data } = msg as { sessionId: string; data: string };
|
|
266
|
+
const bytes = decodeBase64(data);
|
|
267
|
+
terminal.write(bytes);
|
|
268
|
+
});
|
|
269
|
+
unsubsRef.current.push(unsubBuffer);
|
|
270
|
+
|
|
271
|
+
const unsubBufferEnd = subscribe(SUBJECTS.pty.bufferEnd(orgId, resolvedSessionId), (msg: unknown) => {
|
|
272
|
+
if (cancelled) return;
|
|
273
|
+
const { error } = (msg as { error?: string }) || {};
|
|
274
|
+
if (error) {
|
|
275
|
+
terminal.write(`\r\n\x1b[31m[Buffer replay denied: ${error}]\x1b[0m\r\n`);
|
|
276
|
+
}
|
|
277
|
+
bufferReplayedRef.current = true;
|
|
278
|
+
|
|
279
|
+
// Sync dimensions after replay — debounced to collapse with
|
|
280
|
+
// any ResizeObserver-triggered resizes during DOM settle
|
|
281
|
+
const dimsKey = `${cols}x${rows}`;
|
|
282
|
+
if (lastSentDimsRef.current !== dimsKey) {
|
|
283
|
+
if (resizeTimerRef.current) clearTimeout(resizeTimerRef.current);
|
|
284
|
+
resizeTimerRef.current = setTimeout(() => {
|
|
285
|
+
if (cancelled || !sessionIdRef.current) return;
|
|
286
|
+
lastSentDimsRef.current = dimsKey;
|
|
287
|
+
publish(SUBJECTS.pty.resize(orgId, resolvedSessionId), {
|
|
288
|
+
sessionId: resolvedSessionId,
|
|
289
|
+
cols,
|
|
290
|
+
rows,
|
|
291
|
+
});
|
|
292
|
+
}, 150);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Load WebGL addon after buffer replay
|
|
296
|
+
(async () => {
|
|
297
|
+
try {
|
|
298
|
+
const webglMod = await import('@xterm/addon-webgl');
|
|
299
|
+
if (cancelled || !terminalRef.current) return;
|
|
300
|
+
const webglAddon = new webglMod.WebglAddon();
|
|
301
|
+
webglAddon.onContextLoss(() => {
|
|
302
|
+
webglAddon.dispose();
|
|
303
|
+
});
|
|
304
|
+
terminal.loadAddon(webglAddon);
|
|
305
|
+
webglAddonRef.current = webglAddon;
|
|
306
|
+
} catch {
|
|
307
|
+
// WebGL not available
|
|
308
|
+
}
|
|
309
|
+
})();
|
|
310
|
+
});
|
|
311
|
+
unsubsRef.current.push(unsubBufferEnd);
|
|
312
|
+
|
|
313
|
+
// Request buffer replay now that subscriptions are in place
|
|
314
|
+
publish(SUBJECTS.pty.replay(orgId, resolvedSessionId), {
|
|
315
|
+
sessionId: resolvedSessionId,
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Subscribe to exit (always — both new and reattached sessions)
|
|
320
|
+
const unsubExit = subscribe(SUBJECTS.pty.exit(orgId, resolvedSessionId), (msg: unknown) => {
|
|
321
|
+
if (cancelled) return;
|
|
322
|
+
const { code } = msg as { sessionId: string; code: number; signal?: string };
|
|
323
|
+
terminal.write(`\r\n\x1b[33m[Process exited with code ${code ?? 0}]\x1b[0m\r\n`);
|
|
324
|
+
});
|
|
325
|
+
unsubsRef.current.push(unsubExit);
|
|
326
|
+
|
|
327
|
+
// Terminal input -> NATS
|
|
328
|
+
terminal.onData((data) => {
|
|
329
|
+
if (cancelled) return;
|
|
330
|
+
publish(SUBJECTS.pty.input(orgId, resolvedSessionId), {
|
|
331
|
+
sessionId: resolvedSessionId,
|
|
332
|
+
data,
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
// Track input for last command detection
|
|
336
|
+
if (data === '\r') {
|
|
337
|
+
const command = inputBufferRef.current.trim();
|
|
338
|
+
if (command && onLastCommandChangeRef.current) {
|
|
339
|
+
onLastCommandChangeRef.current(command);
|
|
340
|
+
}
|
|
341
|
+
inputBufferRef.current = '';
|
|
342
|
+
} else if (data === '\x7f' || data === '\x08') {
|
|
343
|
+
inputBufferRef.current = inputBufferRef.current.slice(0, -1);
|
|
344
|
+
} else if (data === '\x03') {
|
|
345
|
+
inputBufferRef.current = '';
|
|
346
|
+
} else if (data === '\x15') {
|
|
347
|
+
inputBufferRef.current = '';
|
|
348
|
+
} else if (data.charCodeAt(0) >= 32) {
|
|
349
|
+
inputBufferRef.current += data;
|
|
350
|
+
}
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
terminal.onBinary((data) => {
|
|
354
|
+
if (cancelled) return;
|
|
355
|
+
publish(SUBJECTS.pty.input(orgId, resolvedSessionId), {
|
|
356
|
+
sessionId: resolvedSessionId,
|
|
357
|
+
data,
|
|
358
|
+
});
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
terminal.onResize(({ cols, rows }) => {
|
|
362
|
+
if (cancelled) return;
|
|
363
|
+
if (!sessionIdRef.current) return;
|
|
364
|
+
// During settle period, skip — the settle timer will sync at the end
|
|
365
|
+
if (settleTimerRef.current) return;
|
|
366
|
+
const dimsKey = `${cols}x${rows}`;
|
|
367
|
+
if (lastSentDimsRef.current === dimsKey) return;
|
|
368
|
+
// Debounce: DOM reflow can trigger several rapid resizes as
|
|
369
|
+
// the window layout settles; each sends SIGWINCH → bash redraws
|
|
370
|
+
// the prompt. Collapse them into a single resize after 150ms.
|
|
371
|
+
if (resizeTimerRef.current) clearTimeout(resizeTimerRef.current);
|
|
372
|
+
resizeTimerRef.current = setTimeout(() => {
|
|
373
|
+
if (cancelled || !sessionIdRef.current) return;
|
|
374
|
+
lastSentDimsRef.current = dimsKey;
|
|
375
|
+
publish(SUBJECTS.pty.resize(orgId, sessionIdRef.current), {
|
|
376
|
+
sessionId: sessionIdRef.current,
|
|
377
|
+
cols,
|
|
378
|
+
rows,
|
|
379
|
+
});
|
|
380
|
+
}, 150);
|
|
381
|
+
});
|
|
382
|
+
})();
|
|
383
|
+
|
|
384
|
+
return () => {
|
|
385
|
+
cancelled = true;
|
|
386
|
+
|
|
387
|
+
// Unsubscribe all NATS subscriptions
|
|
388
|
+
for (const unsub of unsubsRef.current) {
|
|
389
|
+
unsub();
|
|
390
|
+
}
|
|
391
|
+
unsubsRef.current = [];
|
|
392
|
+
sessionIdRef.current = null;
|
|
393
|
+
|
|
394
|
+
if (webglAddonRef.current) {
|
|
395
|
+
try {
|
|
396
|
+
webglAddonRef.current.dispose();
|
|
397
|
+
} catch {
|
|
398
|
+
// ignore
|
|
399
|
+
}
|
|
400
|
+
webglAddonRef.current = null;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
if (terminalRef.current) {
|
|
404
|
+
terminalRef.current.dispose();
|
|
405
|
+
terminalRef.current = null;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
fitAddonRef.current = null;
|
|
409
|
+
bufferReplayedRef.current = false;
|
|
410
|
+
lastSentDimsRef.current = '';
|
|
411
|
+
if (resizeTimerRef.current) {
|
|
412
|
+
clearTimeout(resizeTimerRef.current);
|
|
413
|
+
resizeTimerRef.current = null;
|
|
414
|
+
}
|
|
415
|
+
if (settleTimerRef.current) {
|
|
416
|
+
clearTimeout(settleTimerRef.current);
|
|
417
|
+
settleTimerRef.current = null;
|
|
418
|
+
}
|
|
419
|
+
};
|
|
420
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
421
|
+
}, [paneId, subscribe, publish, request]);
|
|
422
|
+
|
|
423
|
+
// Update theme and DOM focus when focus changes
|
|
424
|
+
const mode = useThemeStore((s) => s.mode);
|
|
425
|
+
useEffect(() => {
|
|
426
|
+
const terminal = terminalRef.current;
|
|
427
|
+
if (!terminal) return;
|
|
428
|
+
|
|
429
|
+
terminal.options.cursorBlink = false;
|
|
430
|
+
terminal.options.theme = {
|
|
431
|
+
...terminal.options.theme,
|
|
432
|
+
...buildTerminalTheme(isFocused),
|
|
433
|
+
};
|
|
434
|
+
|
|
435
|
+
if (isFocused) {
|
|
436
|
+
terminal.focus();
|
|
437
|
+
} else {
|
|
438
|
+
terminal.blur();
|
|
439
|
+
}
|
|
440
|
+
}, [isFocused, mode]);
|
|
441
|
+
|
|
442
|
+
// Trigger fit when pane is resized
|
|
443
|
+
useEffect(() => {
|
|
444
|
+
const handleResize = () => {
|
|
445
|
+
if (fitAddonRef.current) {
|
|
446
|
+
requestAnimationFrame(() => {
|
|
447
|
+
try {
|
|
448
|
+
fitAddonRef.current?.fit();
|
|
449
|
+
} catch {
|
|
450
|
+
// ignore
|
|
451
|
+
}
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
};
|
|
455
|
+
|
|
456
|
+
const observer = new ResizeObserver(handleResize);
|
|
457
|
+
if (containerRef.current) {
|
|
458
|
+
observer.observe(containerRef.current);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
return () => {
|
|
462
|
+
observer.disconnect();
|
|
463
|
+
};
|
|
464
|
+
}, []);
|
|
465
|
+
|
|
466
|
+
return (
|
|
467
|
+
<div
|
|
468
|
+
ref={containerRef}
|
|
469
|
+
className="h-full w-full"
|
|
470
|
+
style={{
|
|
471
|
+
padding: '4px',
|
|
472
|
+
opacity: isFocused ? 1 : 0.7,
|
|
473
|
+
transition: 'opacity 0.2s',
|
|
474
|
+
}}
|
|
475
|
+
onClick={onFocus}
|
|
476
|
+
/>
|
|
477
|
+
);
|
|
478
|
+
}
|