@myrialabs/clopen 0.0.1
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 +6 -0
- package/.github/workflows/release.yml +60 -0
- package/.github/workflows/test.yml +40 -0
- package/CONTRIBUTING.md +499 -0
- package/LICENSE +21 -0
- package/README.md +209 -0
- package/backend/index.ts +156 -0
- package/backend/lib/chat/helpers.ts +42 -0
- package/backend/lib/chat/index.ts +2 -0
- package/backend/lib/chat/stream-manager.ts +1126 -0
- package/backend/lib/database/README.md +77 -0
- package/backend/lib/database/index.ts +119 -0
- package/backend/lib/database/migrations/001_create_projects_table.ts +31 -0
- package/backend/lib/database/migrations/002_create_chat_sessions_table.ts +33 -0
- package/backend/lib/database/migrations/003_create_messages_table.ts +32 -0
- package/backend/lib/database/migrations/004_create_prompt_templates_table.ts +34 -0
- package/backend/lib/database/migrations/005_create_settings_table.ts +24 -0
- package/backend/lib/database/migrations/006_add_user_to_messages.ts +58 -0
- package/backend/lib/database/migrations/007_create_stream_states_table.ts +41 -0
- package/backend/lib/database/migrations/008_create_message_snapshots_table.ts +62 -0
- package/backend/lib/database/migrations/009_add_delta_snapshot_fields.ts +41 -0
- package/backend/lib/database/migrations/010_add_soft_delete_and_branch_support.ts +70 -0
- package/backend/lib/database/migrations/011_git_like_commit_graph.ts +156 -0
- package/backend/lib/database/migrations/012_add_file_change_statistics.ts +41 -0
- package/backend/lib/database/migrations/013_checkpoint_tree_state.ts +118 -0
- package/backend/lib/database/migrations/014_add_engine_to_sessions.ts +18 -0
- package/backend/lib/database/migrations/015_add_model_to_sessions.ts +18 -0
- package/backend/lib/database/migrations/016_create_user_projects_table.ts +34 -0
- package/backend/lib/database/migrations/017_add_current_session_to_user_projects.ts +32 -0
- package/backend/lib/database/migrations/018_create_claude_accounts_table.ts +24 -0
- package/backend/lib/database/migrations/019_add_claude_account_to_sessions.ts +18 -0
- package/backend/lib/database/migrations/020_add_snapshot_tree_hash.ts +32 -0
- package/backend/lib/database/migrations/021_drop_prompt_templates_table.ts +33 -0
- package/backend/lib/database/migrations/index.ts +154 -0
- package/backend/lib/database/queries/checkpoint-queries.ts +87 -0
- package/backend/lib/database/queries/engine-queries.ts +75 -0
- package/backend/lib/database/queries/index.ts +9 -0
- package/backend/lib/database/queries/message-queries.ts +472 -0
- package/backend/lib/database/queries/project-queries.ts +117 -0
- package/backend/lib/database/queries/session-queries.ts +271 -0
- package/backend/lib/database/queries/settings-queries.ts +34 -0
- package/backend/lib/database/queries/snapshot-queries.ts +326 -0
- package/backend/lib/database/queries/utils-queries.ts +59 -0
- package/backend/lib/database/seeders/index.ts +13 -0
- package/backend/lib/database/seeders/settings_seeder.ts +84 -0
- package/backend/lib/database/utils/connection.ts +174 -0
- package/backend/lib/database/utils/index.ts +4 -0
- package/backend/lib/database/utils/migration-runner.ts +118 -0
- package/backend/lib/database/utils/seeder-runner.ts +121 -0
- package/backend/lib/engine/adapters/claude/environment.ts +164 -0
- package/backend/lib/engine/adapters/claude/error-handler.ts +60 -0
- package/backend/lib/engine/adapters/claude/index.ts +1 -0
- package/backend/lib/engine/adapters/claude/path-utils.ts +38 -0
- package/backend/lib/engine/adapters/claude/stream.ts +177 -0
- package/backend/lib/engine/adapters/opencode/index.ts +2 -0
- package/backend/lib/engine/adapters/opencode/message-converter.ts +862 -0
- package/backend/lib/engine/adapters/opencode/server.ts +104 -0
- package/backend/lib/engine/adapters/opencode/stream.ts +755 -0
- package/backend/lib/engine/index.ts +196 -0
- package/backend/lib/engine/types.ts +58 -0
- package/backend/lib/files/file-operations.ts +478 -0
- package/backend/lib/files/file-reading.ts +308 -0
- package/backend/lib/files/file-watcher.ts +383 -0
- package/backend/lib/files/path-browsing.ts +382 -0
- package/backend/lib/git/git-executor.ts +88 -0
- package/backend/lib/git/git-parser.ts +411 -0
- package/backend/lib/git/git-service.ts +505 -0
- package/backend/lib/mcp/README.md +1144 -0
- package/backend/lib/mcp/config.ts +316 -0
- package/backend/lib/mcp/index.ts +35 -0
- package/backend/lib/mcp/project-context.ts +236 -0
- package/backend/lib/mcp/servers/browser-automation/actions.ts +156 -0
- package/backend/lib/mcp/servers/browser-automation/browser.ts +419 -0
- package/backend/lib/mcp/servers/browser-automation/index.ts +791 -0
- package/backend/lib/mcp/servers/browser-automation/inspection.ts +501 -0
- package/backend/lib/mcp/servers/helper.ts +143 -0
- package/backend/lib/mcp/servers/index.ts +45 -0
- package/backend/lib/mcp/servers/weather/get-temperature.ts +56 -0
- package/backend/lib/mcp/servers/weather/index.ts +31 -0
- package/backend/lib/mcp/stdio-server.ts +103 -0
- package/backend/lib/mcp/types.ts +65 -0
- package/backend/lib/preview/browser/browser-audio-capture.ts +86 -0
- package/backend/lib/preview/browser/browser-console-manager.ts +263 -0
- package/backend/lib/preview/browser/browser-dialog-handler.ts +222 -0
- package/backend/lib/preview/browser/browser-interaction-handler.ts +421 -0
- package/backend/lib/preview/browser/browser-mcp-control.ts +415 -0
- package/backend/lib/preview/browser/browser-native-ui-handler.ts +512 -0
- package/backend/lib/preview/browser/browser-navigation-tracker.ts +104 -0
- package/backend/lib/preview/browser/browser-pool.ts +357 -0
- package/backend/lib/preview/browser/browser-preview-service.ts +882 -0
- package/backend/lib/preview/browser/browser-tab-manager.ts +935 -0
- package/backend/lib/preview/browser/browser-video-capture.ts +695 -0
- package/backend/lib/preview/browser/scripts/audio-stream.ts +292 -0
- package/backend/lib/preview/browser/scripts/cursor-tracking.ts +85 -0
- package/backend/lib/preview/browser/scripts/video-stream.ts +438 -0
- package/backend/lib/preview/browser/types.ts +359 -0
- package/backend/lib/preview/index.ts +24 -0
- package/backend/lib/project/index.ts +2 -0
- package/backend/lib/project/status-manager.ts +182 -0
- package/backend/lib/shared/index.ts +2 -0
- package/backend/lib/shared/port-utils.ts +25 -0
- package/backend/lib/shared/process-manager.ts +281 -0
- package/backend/lib/snapshot/blob-store.ts +227 -0
- package/backend/lib/snapshot/gitignore.ts +307 -0
- package/backend/lib/snapshot/helpers.ts +397 -0
- package/backend/lib/snapshot/snapshot-service.ts +483 -0
- package/backend/lib/terminal/helpers.ts +14 -0
- package/backend/lib/terminal/index.ts +8 -0
- package/backend/lib/terminal/pty-manager.ts +4 -0
- package/backend/lib/terminal/pty-session-manager.ts +387 -0
- package/backend/lib/terminal/shell-utils.ts +313 -0
- package/backend/lib/terminal/stream-manager.ts +293 -0
- package/backend/lib/tunnel/global-tunnel-manager.ts +243 -0
- package/backend/lib/tunnel/project-tunnel-manager.ts +311 -0
- package/backend/lib/user/helpers.ts +87 -0
- package/backend/lib/utils/ws.ts +944 -0
- package/backend/lib/vite-dev.ts +353 -0
- package/backend/middleware/cors.ts +15 -0
- package/backend/middleware/error-handler.ts +49 -0
- package/backend/middleware/logger.ts +9 -0
- package/backend/types/api.ts +24 -0
- package/backend/ws/README.md +1505 -0
- package/backend/ws/chat/background.ts +198 -0
- package/backend/ws/chat/index.ts +21 -0
- package/backend/ws/chat/stream.ts +707 -0
- package/backend/ws/engine/claude/accounts.ts +401 -0
- package/backend/ws/engine/claude/index.ts +13 -0
- package/backend/ws/engine/claude/status.ts +43 -0
- package/backend/ws/engine/index.ts +14 -0
- package/backend/ws/engine/opencode/index.ts +11 -0
- package/backend/ws/engine/opencode/status.ts +30 -0
- package/backend/ws/engine/utils.ts +36 -0
- package/backend/ws/files/index.ts +30 -0
- package/backend/ws/files/read.ts +189 -0
- package/backend/ws/files/search.ts +453 -0
- package/backend/ws/files/watch.ts +124 -0
- package/backend/ws/files/write.ts +143 -0
- package/backend/ws/git/branch.ts +106 -0
- package/backend/ws/git/commit.ts +39 -0
- package/backend/ws/git/conflict.ts +68 -0
- package/backend/ws/git/diff.ts +69 -0
- package/backend/ws/git/index.ts +24 -0
- package/backend/ws/git/log.ts +41 -0
- package/backend/ws/git/remote.ts +214 -0
- package/backend/ws/git/staging.ts +84 -0
- package/backend/ws/git/status.ts +90 -0
- package/backend/ws/index.ts +69 -0
- package/backend/ws/mcp/index.ts +61 -0
- package/backend/ws/messages/crud.ts +74 -0
- package/backend/ws/messages/index.ts +14 -0
- package/backend/ws/preview/browser/cleanup.ts +129 -0
- package/backend/ws/preview/browser/console.ts +114 -0
- package/backend/ws/preview/browser/interact.ts +513 -0
- package/backend/ws/preview/browser/mcp.ts +129 -0
- package/backend/ws/preview/browser/native-ui.ts +235 -0
- package/backend/ws/preview/browser/stats.ts +55 -0
- package/backend/ws/preview/browser/tab-info.ts +126 -0
- package/backend/ws/preview/browser/tab.ts +166 -0
- package/backend/ws/preview/browser/webcodecs.ts +293 -0
- package/backend/ws/preview/index.ts +146 -0
- package/backend/ws/projects/crud.ts +113 -0
- package/backend/ws/projects/index.ts +25 -0
- package/backend/ws/projects/presence.ts +46 -0
- package/backend/ws/projects/status.ts +116 -0
- package/backend/ws/sessions/crud.ts +327 -0
- package/backend/ws/sessions/index.ts +33 -0
- package/backend/ws/settings/crud.ts +112 -0
- package/backend/ws/settings/index.ts +14 -0
- package/backend/ws/snapshot/index.ts +17 -0
- package/backend/ws/snapshot/restore.ts +173 -0
- package/backend/ws/snapshot/timeline.ts +141 -0
- package/backend/ws/system/index.ts +14 -0
- package/backend/ws/system/operations.ts +49 -0
- package/backend/ws/terminal/index.ts +40 -0
- package/backend/ws/terminal/persistence.ts +153 -0
- package/backend/ws/terminal/session.ts +382 -0
- package/backend/ws/terminal/stream.ts +79 -0
- package/backend/ws/tunnel/index.ts +14 -0
- package/backend/ws/tunnel/operations.ts +91 -0
- package/backend/ws/types.ts +20 -0
- package/backend/ws/user/crud.ts +156 -0
- package/backend/ws/user/index.ts +14 -0
- package/bin/clopen.ts +307 -0
- package/bun.lock +1352 -0
- package/frontend/App.svelte +34 -0
- package/frontend/app.css +313 -0
- package/frontend/lib/app-environment.ts +10 -0
- package/frontend/lib/components/chat/ChatInterface.svelte +407 -0
- package/frontend/lib/components/chat/formatters/ErrorMessage.svelte +57 -0
- package/frontend/lib/components/chat/formatters/MessageFormatter.svelte +224 -0
- package/frontend/lib/components/chat/formatters/TextMessage.svelte +395 -0
- package/frontend/lib/components/chat/formatters/Tools.svelte +70 -0
- package/frontend/lib/components/chat/formatters/index.ts +3 -0
- package/frontend/lib/components/chat/input/ChatInput.svelte +421 -0
- package/frontend/lib/components/chat/input/components/ChatInputActions.svelte +78 -0
- package/frontend/lib/components/chat/input/components/DragDropOverlay.svelte +30 -0
- package/frontend/lib/components/chat/input/components/EditModeIndicator.svelte +33 -0
- package/frontend/lib/components/chat/input/components/EngineModelPicker.svelte +619 -0
- package/frontend/lib/components/chat/input/components/FileAttachmentPreview.svelte +48 -0
- package/frontend/lib/components/chat/input/components/LoadingIndicator.svelte +31 -0
- package/frontend/lib/components/chat/input/composables/use-animations.svelte.ts +201 -0
- package/frontend/lib/components/chat/input/composables/use-chat-actions.svelte.ts +148 -0
- package/frontend/lib/components/chat/input/composables/use-file-handling.svelte.ts +216 -0
- package/frontend/lib/components/chat/input/composables/use-input-state.svelte.ts +357 -0
- package/frontend/lib/components/chat/input/composables/use-textarea-resize.svelte.ts +57 -0
- package/frontend/lib/components/chat/message/ChatMessage.svelte +478 -0
- package/frontend/lib/components/chat/message/ChatMessages.svelte +541 -0
- package/frontend/lib/components/chat/message/DateSeparator.svelte +86 -0
- package/frontend/lib/components/chat/message/MessageBubble.svelte +86 -0
- package/frontend/lib/components/chat/message/MessageHeader.svelte +157 -0
- package/frontend/lib/components/chat/modal/DebugModal.svelte +59 -0
- package/frontend/lib/components/chat/modal/TokenUsageModal.svelte +124 -0
- package/frontend/lib/components/chat/shared/index.ts +2 -0
- package/frontend/lib/components/chat/shared/utils.ts +116 -0
- package/frontend/lib/components/chat/tools/BashOutputTool.svelte +35 -0
- package/frontend/lib/components/chat/tools/BashTool.svelte +46 -0
- package/frontend/lib/components/chat/tools/CustomMcpTool.svelte +139 -0
- package/frontend/lib/components/chat/tools/EditTool.svelte +48 -0
- package/frontend/lib/components/chat/tools/ExitPlanModeTool.svelte +32 -0
- package/frontend/lib/components/chat/tools/GlobTool.svelte +51 -0
- package/frontend/lib/components/chat/tools/GrepTool.svelte +90 -0
- package/frontend/lib/components/chat/tools/KillShellTool.svelte +26 -0
- package/frontend/lib/components/chat/tools/ListMcpResourcesTool.svelte +31 -0
- package/frontend/lib/components/chat/tools/NotebookEditTool.svelte +38 -0
- package/frontend/lib/components/chat/tools/ReadMcpResourceTool.svelte +34 -0
- package/frontend/lib/components/chat/tools/ReadTool.svelte +41 -0
- package/frontend/lib/components/chat/tools/TaskTool.svelte +64 -0
- package/frontend/lib/components/chat/tools/TodoWriteTool.svelte +75 -0
- package/frontend/lib/components/chat/tools/WebFetchTool.svelte +35 -0
- package/frontend/lib/components/chat/tools/WebSearchTool.svelte +84 -0
- package/frontend/lib/components/chat/tools/WriteTool.svelte +33 -0
- package/frontend/lib/components/chat/tools/components/CodeBlock.svelte +79 -0
- package/frontend/lib/components/chat/tools/components/DiffBlock.svelte +408 -0
- package/frontend/lib/components/chat/tools/components/FileHeader.svelte +45 -0
- package/frontend/lib/components/chat/tools/components/InfoLine.svelte +19 -0
- package/frontend/lib/components/chat/tools/components/StatsBadges.svelte +27 -0
- package/frontend/lib/components/chat/tools/components/TerminalCommand.svelte +54 -0
- package/frontend/lib/components/chat/tools/components/index.ts +7 -0
- package/frontend/lib/components/chat/tools/index.ts +26 -0
- package/frontend/lib/components/chat/widgets/FloatingTodoList.svelte +249 -0
- package/frontend/lib/components/chat/widgets/TokenUsage.svelte +78 -0
- package/frontend/lib/components/checkpoint/TimelineModal.svelte +391 -0
- package/frontend/lib/components/checkpoint/timeline/TimelineEdge.svelte +26 -0
- package/frontend/lib/components/checkpoint/timeline/TimelineGraph.svelte +87 -0
- package/frontend/lib/components/checkpoint/timeline/TimelineNode.svelte +108 -0
- package/frontend/lib/components/checkpoint/timeline/TimelineVersionGroup.svelte +59 -0
- package/frontend/lib/components/checkpoint/timeline/animation.ts +168 -0
- package/frontend/lib/components/checkpoint/timeline/config.ts +44 -0
- package/frontend/lib/components/checkpoint/timeline/graph-builder.ts +304 -0
- package/frontend/lib/components/checkpoint/timeline/types.ts +65 -0
- package/frontend/lib/components/checkpoint/timeline/utils.ts +53 -0
- package/frontend/lib/components/common/Alert.svelte +139 -0
- package/frontend/lib/components/common/AvatarBubble.svelte +56 -0
- package/frontend/lib/components/common/Button.svelte +71 -0
- package/frontend/lib/components/common/Card.svelte +102 -0
- package/frontend/lib/components/common/Checkbox.svelte +48 -0
- package/frontend/lib/components/common/Dialog.svelte +249 -0
- package/frontend/lib/components/common/FolderBrowser.svelte +843 -0
- package/frontend/lib/components/common/Icon.svelte +58 -0
- package/frontend/lib/components/common/Input.svelte +72 -0
- package/frontend/lib/components/common/Lightbox.svelte +233 -0
- package/frontend/lib/components/common/LoadingScreen.svelte +52 -0
- package/frontend/lib/components/common/LoadingSpinner.svelte +48 -0
- package/frontend/lib/components/common/Modal.svelte +177 -0
- package/frontend/lib/components/common/ModalProvider.svelte +28 -0
- package/frontend/lib/components/common/ModelSelector.svelte +110 -0
- package/frontend/lib/components/common/MonacoEditor.svelte +569 -0
- package/frontend/lib/components/common/NotificationToast.svelte +113 -0
- package/frontend/lib/components/common/PageTemplate.svelte +76 -0
- package/frontend/lib/components/common/ProjectUserAvatars.svelte +79 -0
- package/frontend/lib/components/common/Select.svelte +98 -0
- package/frontend/lib/components/common/Textarea.svelte +80 -0
- package/frontend/lib/components/common/ThemeToggle.svelte +44 -0
- package/frontend/lib/components/common/lucide-icons.ts +1642 -0
- package/frontend/lib/components/common/material-icons.ts +1082 -0
- package/frontend/lib/components/common/xterm/XTerm.svelte +796 -0
- package/frontend/lib/components/common/xterm/index.ts +16 -0
- package/frontend/lib/components/common/xterm/terminal-config.ts +68 -0
- package/frontend/lib/components/common/xterm/types.ts +31 -0
- package/frontend/lib/components/common/xterm/xterm-service.ts +353 -0
- package/frontend/lib/components/files/FileNode.svelte +384 -0
- package/frontend/lib/components/files/FileTree.svelte +681 -0
- package/frontend/lib/components/files/FileViewer.svelte +728 -0
- package/frontend/lib/components/files/SearchResults.svelte +303 -0
- package/frontend/lib/components/git/BranchManager.svelte +458 -0
- package/frontend/lib/components/git/ChangesSection.svelte +107 -0
- package/frontend/lib/components/git/CommitForm.svelte +76 -0
- package/frontend/lib/components/git/ConflictResolver.svelte +158 -0
- package/frontend/lib/components/git/DiffViewer.svelte +364 -0
- package/frontend/lib/components/git/FileChangeItem.svelte +97 -0
- package/frontend/lib/components/git/GitButton.svelte +33 -0
- package/frontend/lib/components/git/GitLog.svelte +361 -0
- package/frontend/lib/components/git/GitModal.svelte +80 -0
- package/frontend/lib/components/history/HistoryModal.svelte +563 -0
- package/frontend/lib/components/history/HistoryView.svelte +615 -0
- package/frontend/lib/components/index.ts +34 -0
- package/frontend/lib/components/preview/browser/BrowserPreview.svelte +549 -0
- package/frontend/lib/components/preview/browser/components/Canvas.svelte +1058 -0
- package/frontend/lib/components/preview/browser/components/ConsolePanel.svelte +757 -0
- package/frontend/lib/components/preview/browser/components/Container.svelte +450 -0
- package/frontend/lib/components/preview/browser/components/ContextMenu.svelte +236 -0
- package/frontend/lib/components/preview/browser/components/SelectDropdown.svelte +224 -0
- package/frontend/lib/components/preview/browser/components/Toolbar.svelte +339 -0
- package/frontend/lib/components/preview/browser/components/VirtualCursor.svelte +36 -0
- package/frontend/lib/components/preview/browser/core/cleanup.svelte.ts +155 -0
- package/frontend/lib/components/preview/browser/core/coordinator.svelte.ts +837 -0
- package/frontend/lib/components/preview/browser/core/interactions.svelte.ts +113 -0
- package/frontend/lib/components/preview/browser/core/mcp-handlers.svelte.ts +296 -0
- package/frontend/lib/components/preview/browser/core/native-ui-handlers.svelte.ts +391 -0
- package/frontend/lib/components/preview/browser/core/stream-handler.svelte.ts +231 -0
- package/frontend/lib/components/preview/browser/core/tab-manager.svelte.ts +210 -0
- package/frontend/lib/components/preview/browser/core/tab-operations.svelte.ts +239 -0
- package/frontend/lib/components/preview/index.ts +2 -0
- package/frontend/lib/components/settings/SettingsModal.svelte +235 -0
- package/frontend/lib/components/settings/SettingsView.svelte +36 -0
- package/frontend/lib/components/settings/appearance/AppearanceSettings.svelte +51 -0
- package/frontend/lib/components/settings/appearance/LayoutPresetSettings.svelte +160 -0
- package/frontend/lib/components/settings/appearance/LayoutPreview.svelte +76 -0
- package/frontend/lib/components/settings/engines/AIEnginesSettings.svelte +917 -0
- package/frontend/lib/components/settings/general/AdvancedSettings.svelte +187 -0
- package/frontend/lib/components/settings/general/DataManagementSettings.svelte +203 -0
- package/frontend/lib/components/settings/general/GeneralSettings.svelte +10 -0
- package/frontend/lib/components/settings/model/ModelSettings.svelte +357 -0
- package/frontend/lib/components/settings/notifications/NotificationSettings.svelte +205 -0
- package/frontend/lib/components/settings/user/UserSettings.svelte +197 -0
- package/frontend/lib/components/terminal/Terminal.svelte +368 -0
- package/frontend/lib/components/terminal/TerminalTabs.svelte +87 -0
- package/frontend/lib/components/terminal/TerminalView.svelte +55 -0
- package/frontend/lib/components/tunnel/TunnelActive.svelte +142 -0
- package/frontend/lib/components/tunnel/TunnelButton.svelte +54 -0
- package/frontend/lib/components/tunnel/TunnelInactive.svelte +284 -0
- package/frontend/lib/components/tunnel/TunnelModal.svelte +47 -0
- package/frontend/lib/components/tunnel/TunnelQRCode.svelte +49 -0
- package/frontend/lib/components/workspace/DesktopNavigator.svelte +382 -0
- package/frontend/lib/components/workspace/MobileNavigator.svelte +403 -0
- package/frontend/lib/components/workspace/PanelContainer.svelte +100 -0
- package/frontend/lib/components/workspace/PanelHeader.svelte +505 -0
- package/frontend/lib/components/workspace/ViewMenu.svelte +162 -0
- package/frontend/lib/components/workspace/WorkspaceLayout.svelte +169 -0
- package/frontend/lib/components/workspace/layout/DesktopLayout.svelte +15 -0
- package/frontend/lib/components/workspace/layout/MobileLayout.svelte +17 -0
- package/frontend/lib/components/workspace/layout/split-pane/Container.svelte +42 -0
- package/frontend/lib/components/workspace/layout/split-pane/Handle.svelte +85 -0
- package/frontend/lib/components/workspace/layout/split-pane/Layout.svelte +37 -0
- package/frontend/lib/components/workspace/panels/ChatPanel.svelte +274 -0
- package/frontend/lib/components/workspace/panels/FilesPanel.svelte +1261 -0
- package/frontend/lib/components/workspace/panels/GitPanel.svelte +1560 -0
- package/frontend/lib/components/workspace/panels/PreviewPanel.svelte +150 -0
- package/frontend/lib/components/workspace/panels/TerminalPanel.svelte +73 -0
- package/frontend/lib/constants/preview.ts +45 -0
- package/frontend/lib/services/chat/chat.service.ts +704 -0
- package/frontend/lib/services/chat/index.ts +7 -0
- package/frontend/lib/services/notification/global-stream-monitor.ts +86 -0
- package/frontend/lib/services/notification/index.ts +8 -0
- package/frontend/lib/services/notification/push.service.ts +144 -0
- package/frontend/lib/services/notification/sound.service.ts +127 -0
- package/frontend/lib/services/preview/browser/browser-console.service.ts +61 -0
- package/frontend/lib/services/preview/browser/browser-webcodecs.service.ts +1499 -0
- package/frontend/lib/services/preview/browser/mcp-integration.svelte.ts +67 -0
- package/frontend/lib/services/preview/index.ts +23 -0
- package/frontend/lib/services/project/index.ts +8 -0
- package/frontend/lib/services/project/status.service.ts +159 -0
- package/frontend/lib/services/snapshot/snapshot.service.ts +47 -0
- package/frontend/lib/services/terminal/background/index.ts +130 -0
- package/frontend/lib/services/terminal/background/session-restore.ts +274 -0
- package/frontend/lib/services/terminal/background/stream-manager.ts +286 -0
- package/frontend/lib/services/terminal/index.ts +14 -0
- package/frontend/lib/services/terminal/persistence.service.ts +260 -0
- package/frontend/lib/services/terminal/project.service.ts +953 -0
- package/frontend/lib/services/terminal/session.service.ts +364 -0
- package/frontend/lib/services/terminal/terminal.service.ts +369 -0
- package/frontend/lib/stores/core/app.svelte.ts +117 -0
- package/frontend/lib/stores/core/files.svelte.ts +73 -0
- package/frontend/lib/stores/core/presence.svelte.ts +48 -0
- package/frontend/lib/stores/core/projects.svelte.ts +317 -0
- package/frontend/lib/stores/core/sessions.svelte.ts +383 -0
- package/frontend/lib/stores/features/claude-accounts.svelte.ts +58 -0
- package/frontend/lib/stores/features/models.svelte.ts +89 -0
- package/frontend/lib/stores/features/settings.svelte.ts +87 -0
- package/frontend/lib/stores/features/terminal.svelte.ts +701 -0
- package/frontend/lib/stores/features/tunnel.svelte.ts +161 -0
- package/frontend/lib/stores/features/user.svelte.ts +96 -0
- package/frontend/lib/stores/ui/chat-input.svelte.ts +57 -0
- package/frontend/lib/stores/ui/chat-model.svelte.ts +61 -0
- package/frontend/lib/stores/ui/dialog.svelte.ts +59 -0
- package/frontend/lib/stores/ui/edit-mode.svelte.ts +214 -0
- package/frontend/lib/stores/ui/notification.svelte.ts +166 -0
- package/frontend/lib/stores/ui/settings-modal.svelte.ts +88 -0
- package/frontend/lib/stores/ui/theme.svelte.ts +179 -0
- package/frontend/lib/stores/ui/workspace.svelte.ts +754 -0
- package/frontend/lib/types/native-ui.ts +73 -0
- package/frontend/lib/utils/chat/date-separator.ts +39 -0
- package/frontend/lib/utils/chat/message-grouper.ts +219 -0
- package/frontend/lib/utils/chat/message-processor.ts +135 -0
- package/frontend/lib/utils/chat/tool-handler.ts +161 -0
- package/frontend/lib/utils/chat/virtual-scroll.svelte.ts +142 -0
- package/frontend/lib/utils/click-outside.ts +20 -0
- package/frontend/lib/utils/context-manager.ts +257 -0
- package/frontend/lib/utils/file-icon-mappings.ts +769 -0
- package/frontend/lib/utils/folder-icon-mappings.ts +1030 -0
- package/frontend/lib/utils/git-status.ts +68 -0
- package/frontend/lib/utils/platform.ts +113 -0
- package/frontend/lib/utils/port-check.ts +65 -0
- package/frontend/lib/utils/terminalFormatter.ts +207 -0
- package/frontend/lib/utils/theme.ts +6 -0
- package/frontend/lib/utils/tree-visualizer.ts +320 -0
- package/frontend/lib/utils/ws.ts +44 -0
- package/frontend/main.ts +13 -0
- package/index.html +70 -0
- package/package.json +111 -0
- package/scripts/generate-icons.ts +87 -0
- package/scripts/pre-publish-check.sh +142 -0
- package/scripts/setup-hooks.sh +134 -0
- package/scripts/validate-branch-name.sh +47 -0
- package/scripts/validate-commit-msg.sh +42 -0
- package/shared/constants/engines.ts +134 -0
- package/shared/types/database/connection.ts +16 -0
- package/shared/types/database/index.ts +6 -0
- package/shared/types/database/schema.ts +141 -0
- package/shared/types/engine/index.ts +45 -0
- package/shared/types/filesystem/index.ts +22 -0
- package/shared/types/git.ts +171 -0
- package/shared/types/messaging/index.ts +239 -0
- package/shared/types/messaging/tool.ts +526 -0
- package/shared/types/network/api.ts +18 -0
- package/shared/types/network/index.ts +5 -0
- package/shared/types/stores/app.ts +23 -0
- package/shared/types/stores/dialog.ts +21 -0
- package/shared/types/stores/index.ts +3 -0
- package/shared/types/stores/settings.ts +15 -0
- package/shared/types/terminal/index.ts +44 -0
- package/shared/types/ui/components.ts +61 -0
- package/shared/types/ui/icons.ts +23 -0
- package/shared/types/ui/index.ts +22 -0
- package/shared/types/ui/notifications.ts +14 -0
- package/shared/types/ui/theme.ts +12 -0
- package/shared/types/websocket/index.ts +43 -0
- package/shared/types/window.d.ts +13 -0
- package/shared/utils/anonymous-user.ts +168 -0
- package/shared/utils/async.ts +10 -0
- package/shared/utils/diff-calculator.ts +184 -0
- package/shared/utils/file-type-detection.ts +166 -0
- package/shared/utils/logger.ts +158 -0
- package/shared/utils/message-formatter.ts +79 -0
- package/shared/utils/path.ts +47 -0
- package/shared/utils/ws-client.ts +768 -0
- package/shared/utils/ws-server.ts +660 -0
- package/static/audio/notification.ogg +0 -0
- package/static/favicon.svg +8 -0
- package/static/fonts/dm-sans/dm-sans-italic-latin-ext.woff2 +0 -0
- package/static/fonts/dm-sans/dm-sans-italic-latin.woff2 +0 -0
- package/static/fonts/dm-sans/dm-sans-normal-latin-ext.woff2 +0 -0
- package/static/fonts/dm-sans/dm-sans-normal-latin.woff2 +0 -0
- package/static/fonts/dm-sans.css +96 -0
- package/svelte.config.js +20 -0
- package/vite.config.ts +33 -0
|
@@ -0,0 +1,660 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WebSocket Server Core Library - Optimized
|
|
3
|
+
*
|
|
4
|
+
* High-performance WebSocket routing with:
|
|
5
|
+
* - Zero boilerplate configuration
|
|
6
|
+
* - 100% type inference from backend to frontend
|
|
7
|
+
* - Singleton TextEncoder/Decoder for performance
|
|
8
|
+
* - Pre-computed binary action detection
|
|
9
|
+
* - Context management (user/project)
|
|
10
|
+
* - TypeBox schema validation (bundled with Elysia)
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { Elysia } from 'elysia';
|
|
14
|
+
import { t, type TSchema, type Static } from 'elysia';
|
|
15
|
+
import { Value } from '@sinclair/typebox/value';
|
|
16
|
+
import { debug } from './logger';
|
|
17
|
+
|
|
18
|
+
// ============================================================================
|
|
19
|
+
// Singleton Encoders (Performance Optimization)
|
|
20
|
+
// ============================================================================
|
|
21
|
+
|
|
22
|
+
/** Singleton TextEncoder - reused across all encode operations */
|
|
23
|
+
const textEncoder = new TextEncoder();
|
|
24
|
+
|
|
25
|
+
/** Singleton TextDecoder - reused across all decode operations */
|
|
26
|
+
const textDecoder = new TextDecoder();
|
|
27
|
+
|
|
28
|
+
// ============================================================================
|
|
29
|
+
// Pre-computed Binary Actions (Performance Optimization)
|
|
30
|
+
// ============================================================================
|
|
31
|
+
|
|
32
|
+
/** Actions known to contain binary data - skip containsBinary() check */
|
|
33
|
+
const BINARY_ACTIONS = new Set<string>([
|
|
34
|
+
'preview:frame',
|
|
35
|
+
'file:upload',
|
|
36
|
+
'file:download',
|
|
37
|
+
'terminal:binary'
|
|
38
|
+
]);
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Register an action as binary (for custom binary handlers)
|
|
42
|
+
*/
|
|
43
|
+
export function registerBinaryAction(action: string): void {
|
|
44
|
+
BINARY_ACTIONS.add(action);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// ============================================================================
|
|
48
|
+
// WebSocket Connection Interface
|
|
49
|
+
// ============================================================================
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* WebSocket Connection with Context
|
|
53
|
+
* Extends Elysia WebSocket with custom context properties
|
|
54
|
+
*/
|
|
55
|
+
export interface WSConnection {
|
|
56
|
+
/** WebSocket ready state (0=CONNECTING, 1=OPEN, 2=CLOSING, 3=CLOSED) */
|
|
57
|
+
readyState: number;
|
|
58
|
+
/** Send message to client */
|
|
59
|
+
send: (data: string | ArrayBufferLike | Blob | ArrayBufferView) => void;
|
|
60
|
+
/** Close connection */
|
|
61
|
+
close: (code?: number, reason?: string) => void;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ============================================================================
|
|
65
|
+
// Route Configuration Types
|
|
66
|
+
// ============================================================================
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* WebSocket route configuration
|
|
70
|
+
*/
|
|
71
|
+
interface RouteConfig<TData extends TSchema> {
|
|
72
|
+
/** Incoming data schema (validates client → server) */
|
|
73
|
+
data: TData;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* HTTP-like route configuration for request-response pattern
|
|
78
|
+
*/
|
|
79
|
+
interface HTTPRouteConfig<
|
|
80
|
+
TData extends TSchema,
|
|
81
|
+
TResponse extends TSchema
|
|
82
|
+
> {
|
|
83
|
+
/** Request data schema (optional) */
|
|
84
|
+
data?: TData;
|
|
85
|
+
/** Response data schema (required) */
|
|
86
|
+
response: TResponse;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* HTTP route handler callback
|
|
91
|
+
*/
|
|
92
|
+
type HTTPHandler<
|
|
93
|
+
TData extends TSchema,
|
|
94
|
+
TResponse extends TSchema
|
|
95
|
+
> = (params: {
|
|
96
|
+
conn: WSConnection;
|
|
97
|
+
data: TData extends TSchema ? Static<TData> : never;
|
|
98
|
+
}) => Promise<Static<TResponse>> | Static<TResponse>;
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* WebSocket route handler callback
|
|
102
|
+
*/
|
|
103
|
+
type RouteHandler<TData extends TSchema> = (params: {
|
|
104
|
+
conn: WSConnection;
|
|
105
|
+
data: Static<TData>;
|
|
106
|
+
}) => void | Promise<void>;
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Internal route definition
|
|
110
|
+
*/
|
|
111
|
+
interface Route {
|
|
112
|
+
action: string;
|
|
113
|
+
dataSchema: TSchema;
|
|
114
|
+
handler: (params: { conn: WSConnection; data: any }) => void | Promise<void>;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Internal HTTP route definition
|
|
119
|
+
*/
|
|
120
|
+
interface HTTPRoute {
|
|
121
|
+
action: string;
|
|
122
|
+
dataSchema?: TSchema;
|
|
123
|
+
responseSchema: TSchema;
|
|
124
|
+
handler: (params: { conn: WSConnection; data: any }) => Promise<any> | any;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// ============================================================================
|
|
128
|
+
// Binary Message Utilities (Optimized)
|
|
129
|
+
// ============================================================================
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Check if payload contains binary data (Uint8Array or ArrayBuffer)
|
|
133
|
+
* Optimized with early return and iterative approach
|
|
134
|
+
*/
|
|
135
|
+
function containsBinary(obj: any): boolean {
|
|
136
|
+
if (obj instanceof Uint8Array || obj instanceof ArrayBuffer) return true;
|
|
137
|
+
if (typeof obj !== 'object' || obj === null) return false;
|
|
138
|
+
|
|
139
|
+
// Use stack-based iteration instead of recursion for deep objects
|
|
140
|
+
const stack = [obj];
|
|
141
|
+
while (stack.length > 0) {
|
|
142
|
+
const current = stack.pop();
|
|
143
|
+
for (const value of Object.values(current)) {
|
|
144
|
+
if (value instanceof Uint8Array || value instanceof ArrayBuffer) return true;
|
|
145
|
+
if (typeof value === 'object' && value !== null) {
|
|
146
|
+
stack.push(value);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return false;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Fast check using pre-computed binary actions
|
|
155
|
+
*/
|
|
156
|
+
function isBinaryAction(action: string, payload: any): boolean {
|
|
157
|
+
if (BINARY_ACTIONS.has(action)) return true;
|
|
158
|
+
return containsBinary(payload);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Extract binary field and metadata from payload
|
|
163
|
+
*/
|
|
164
|
+
function extractBinaryFields(payload: any): { binaryData: Uint8Array; metadata: Record<string, any> } {
|
|
165
|
+
const metadata: Record<string, any> = {};
|
|
166
|
+
let binaryData: Uint8Array = new Uint8Array(0);
|
|
167
|
+
|
|
168
|
+
for (const [key, value] of Object.entries(payload)) {
|
|
169
|
+
if (value instanceof Uint8Array) {
|
|
170
|
+
binaryData = value;
|
|
171
|
+
} else if (value instanceof ArrayBuffer) {
|
|
172
|
+
binaryData = new Uint8Array(value);
|
|
173
|
+
} else {
|
|
174
|
+
metadata[key] = value;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return { binaryData, metadata };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Encode a binary message with action and metadata
|
|
183
|
+
*
|
|
184
|
+
* Binary Message Format:
|
|
185
|
+
* ┌─────────────────┬────────────────┬─────────────────┬──────────────┬─────────────┐
|
|
186
|
+
* │ Action Length │ Action String │ Metadata Length │ Metadata JSON│ Binary Data │
|
|
187
|
+
* │ (1 byte) │ (N bytes) │ (4 bytes) │ (M bytes) │ (rest) │
|
|
188
|
+
* └─────────────────┴────────────────┴─────────────────┴──────────────┴─────────────┘
|
|
189
|
+
*/
|
|
190
|
+
function encodeBinaryMessage(action: string, payload: any): ArrayBuffer {
|
|
191
|
+
const { binaryData, metadata } = extractBinaryFields(payload);
|
|
192
|
+
|
|
193
|
+
const actionBytes = textEncoder.encode(action);
|
|
194
|
+
const metaBytes = textEncoder.encode(JSON.stringify(metadata));
|
|
195
|
+
|
|
196
|
+
// Calculate total length
|
|
197
|
+
const totalLength = 1 + actionBytes.length + 4 + metaBytes.length + binaryData.length;
|
|
198
|
+
const buffer = new ArrayBuffer(totalLength);
|
|
199
|
+
const view = new DataView(buffer);
|
|
200
|
+
const uint8 = new Uint8Array(buffer);
|
|
201
|
+
|
|
202
|
+
let offset = 0;
|
|
203
|
+
|
|
204
|
+
// Action length (1 byte, max 255 characters)
|
|
205
|
+
view.setUint8(offset, actionBytes.length);
|
|
206
|
+
offset += 1;
|
|
207
|
+
|
|
208
|
+
// Action string
|
|
209
|
+
uint8.set(actionBytes, offset);
|
|
210
|
+
offset += actionBytes.length;
|
|
211
|
+
|
|
212
|
+
// Metadata length (4 bytes, big-endian)
|
|
213
|
+
view.setUint32(offset, metaBytes.length);
|
|
214
|
+
offset += 4;
|
|
215
|
+
|
|
216
|
+
// Metadata JSON
|
|
217
|
+
uint8.set(metaBytes, offset);
|
|
218
|
+
offset += metaBytes.length;
|
|
219
|
+
|
|
220
|
+
// Binary data (rest of buffer)
|
|
221
|
+
uint8.set(binaryData, offset);
|
|
222
|
+
|
|
223
|
+
return buffer;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Decode a binary message back to action and payload
|
|
228
|
+
*/
|
|
229
|
+
function decodeBinaryMessage(buffer: ArrayBuffer): { action: string; payload: any } {
|
|
230
|
+
const view = new DataView(buffer);
|
|
231
|
+
const uint8 = new Uint8Array(buffer);
|
|
232
|
+
|
|
233
|
+
let offset = 0;
|
|
234
|
+
|
|
235
|
+
// Read action length (1 byte)
|
|
236
|
+
const actionLength = view.getUint8(offset);
|
|
237
|
+
offset += 1;
|
|
238
|
+
|
|
239
|
+
// Read action string
|
|
240
|
+
const actionBytes = uint8.slice(offset, offset + actionLength);
|
|
241
|
+
const action = textDecoder.decode(actionBytes);
|
|
242
|
+
offset += actionLength;
|
|
243
|
+
|
|
244
|
+
// Read metadata length (4 bytes)
|
|
245
|
+
const metaLength = view.getUint32(offset);
|
|
246
|
+
offset += 4;
|
|
247
|
+
|
|
248
|
+
// Read metadata JSON
|
|
249
|
+
const metaBytes = uint8.slice(offset, offset + metaLength);
|
|
250
|
+
const metadata = JSON.parse(textDecoder.decode(metaBytes));
|
|
251
|
+
offset += metaLength;
|
|
252
|
+
|
|
253
|
+
// Read binary data (rest of buffer)
|
|
254
|
+
const binaryData = uint8.slice(offset);
|
|
255
|
+
|
|
256
|
+
// Reconstruct payload with binary data
|
|
257
|
+
const payload = {
|
|
258
|
+
...metadata,
|
|
259
|
+
data: binaryData // Binary field always named 'data'
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
return { action, payload };
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// ============================================================================
|
|
266
|
+
// WebSocket Router
|
|
267
|
+
// ============================================================================
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* WebSocket Router with Builder Pattern
|
|
271
|
+
*
|
|
272
|
+
* Includes built-in context management for user/project tracking.
|
|
273
|
+
*/
|
|
274
|
+
export class WSRouter<
|
|
275
|
+
TClient extends Record<string, any> = {},
|
|
276
|
+
TServer extends Record<string, any> = {}
|
|
277
|
+
> {
|
|
278
|
+
private routes = new Map<string, Route>();
|
|
279
|
+
private httpRoutes = new Map<string, HTTPRoute>();
|
|
280
|
+
private eventSchemas = new Map<string, TSchema>();
|
|
281
|
+
|
|
282
|
+
constructor() {
|
|
283
|
+
// Register built-in context management route
|
|
284
|
+
this.registerContextHandler();
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Register built-in ws:set-context handler
|
|
289
|
+
* Allows frontend to sync user/project context
|
|
290
|
+
*/
|
|
291
|
+
private registerContextHandler(): void {
|
|
292
|
+
this.httpRoutes.set('ws:set-context', {
|
|
293
|
+
action: 'ws:set-context',
|
|
294
|
+
dataSchema: t.Object({
|
|
295
|
+
userId: t.Optional(t.Union([t.String(), t.Null()])),
|
|
296
|
+
projectId: t.Optional(t.Union([t.String(), t.Null()]))
|
|
297
|
+
}),
|
|
298
|
+
responseSchema: t.Object({
|
|
299
|
+
userId: t.Union([t.String(), t.Null()]),
|
|
300
|
+
projectId: t.Union([t.String(), t.Null()])
|
|
301
|
+
}),
|
|
302
|
+
handler: async ({ conn, data }) => {
|
|
303
|
+
// Import ws server to update context
|
|
304
|
+
const { ws: wsServer } = await import('$backend/lib/utils/ws');
|
|
305
|
+
|
|
306
|
+
if (data.userId !== undefined) {
|
|
307
|
+
wsServer.setUser(conn, data.userId);
|
|
308
|
+
}
|
|
309
|
+
if (data.projectId !== undefined) {
|
|
310
|
+
wsServer.setProject(conn, data.projectId);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Read back from connectionState (single source of truth)
|
|
314
|
+
const state = wsServer.getConnectionState(conn);
|
|
315
|
+
return {
|
|
316
|
+
userId: state?.userId ?? null,
|
|
317
|
+
projectId: state?.projectId ?? null
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Register a WebSocket route handler
|
|
325
|
+
*
|
|
326
|
+
* @param action - Action name (e.g., 'terminal:init', 'chat:send')
|
|
327
|
+
* @param config - Route configuration with data schema
|
|
328
|
+
* @param handler - Handler callback receiving { conn, data }
|
|
329
|
+
*/
|
|
330
|
+
on<TAction extends string, TData extends TSchema>(
|
|
331
|
+
action: TAction,
|
|
332
|
+
config: RouteConfig<TData>,
|
|
333
|
+
handler: RouteHandler<TData>
|
|
334
|
+
): WSRouter<TClient & { [K in TAction]: Static<TData> }, TServer> {
|
|
335
|
+
// Validate action format
|
|
336
|
+
if (!action || typeof action !== 'string') {
|
|
337
|
+
throw new Error(`Invalid action name: ${action}`);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// Check for duplicate routes
|
|
341
|
+
if (this.routes.has(action)) {
|
|
342
|
+
throw new Error(`Route already exists: ${action}`);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// Store route definition
|
|
346
|
+
this.routes.set(action, {
|
|
347
|
+
action,
|
|
348
|
+
dataSchema: config.data,
|
|
349
|
+
handler: handler as any
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
return this as any;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Register an HTTP-like route handler for request-response pattern
|
|
357
|
+
*
|
|
358
|
+
* This method provides a simplified API for request-response interactions,
|
|
359
|
+
* automatically wrapping responses in { success, data?, error? } format.
|
|
360
|
+
*
|
|
361
|
+
* The handler should:
|
|
362
|
+
* - Return data directly (will be wrapped as { success: true, data })
|
|
363
|
+
* - Throw errors (will be wrapped as { success: false, error: message })
|
|
364
|
+
*
|
|
365
|
+
* Response schema should ONLY define the data structure, not the wrapper.
|
|
366
|
+
*/
|
|
367
|
+
http<
|
|
368
|
+
TAction extends string,
|
|
369
|
+
TData extends TSchema = never,
|
|
370
|
+
TResponse extends TSchema = any
|
|
371
|
+
>(
|
|
372
|
+
action: TAction,
|
|
373
|
+
config: HTTPRouteConfig<TData, TResponse>,
|
|
374
|
+
handler: HTTPHandler<TData, TResponse>
|
|
375
|
+
): WSRouter<
|
|
376
|
+
TClient & {
|
|
377
|
+
[K in TAction]: {
|
|
378
|
+
data: TData extends TSchema ? Static<TData> : undefined;
|
|
379
|
+
};
|
|
380
|
+
},
|
|
381
|
+
TServer & {
|
|
382
|
+
[K in `${TAction}:response`]: {
|
|
383
|
+
success: boolean;
|
|
384
|
+
data?: Static<TResponse>;
|
|
385
|
+
error?: string;
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
> {
|
|
389
|
+
// Validate action format
|
|
390
|
+
if (!action || typeof action !== 'string') {
|
|
391
|
+
throw new Error(`Invalid action name: ${action}`);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// Check for duplicate routes
|
|
395
|
+
if (this.httpRoutes.has(action)) {
|
|
396
|
+
throw new Error(`HTTP route already exists: ${action}`);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// Store HTTP route definition
|
|
400
|
+
this.httpRoutes.set(action, {
|
|
401
|
+
action,
|
|
402
|
+
dataSchema: config.data,
|
|
403
|
+
responseSchema: config.response,
|
|
404
|
+
handler: handler as any
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
return this as any;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* Register SERVER → CLIENT event schema (independent)
|
|
412
|
+
*
|
|
413
|
+
* Declares event schemas that server can emit to clients.
|
|
414
|
+
* Events are independent from actions and can be emitted from anywhere using ws.emit()
|
|
415
|
+
*/
|
|
416
|
+
emit<TEvent extends string, TEventSchema extends TSchema>(
|
|
417
|
+
event: TEvent,
|
|
418
|
+
schema: TEventSchema
|
|
419
|
+
): WSRouter<
|
|
420
|
+
TClient,
|
|
421
|
+
TServer & { [K in TEvent]: Static<TEventSchema> }
|
|
422
|
+
> {
|
|
423
|
+
// Validate event format
|
|
424
|
+
if (!event || typeof event !== 'string') {
|
|
425
|
+
throw new Error(`Invalid event name: ${event}`);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// Check for duplicate event schemas
|
|
429
|
+
if (this.eventSchemas.has(event)) {
|
|
430
|
+
throw new Error(`Event schema already exists: ${event}`);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// Store event schema for type inference
|
|
434
|
+
this.eventSchemas.set(event, schema);
|
|
435
|
+
|
|
436
|
+
return this as any;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* Merge another router into this one
|
|
441
|
+
*/
|
|
442
|
+
merge<TC extends Record<string, any>, TS extends Record<string, any>>(
|
|
443
|
+
router: WSRouter<TC, TS>
|
|
444
|
+
): WSRouter<TClient & TC, TServer & TS> {
|
|
445
|
+
// Copy all routes from the other router
|
|
446
|
+
for (const [action, route] of router.routes.entries()) {
|
|
447
|
+
if (this.routes.has(action)) {
|
|
448
|
+
throw new Error(`Route conflict during merge: ${action}`);
|
|
449
|
+
}
|
|
450
|
+
this.routes.set(action, route);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// Copy all HTTP routes from the other router (except built-in)
|
|
454
|
+
for (const [action, route] of router.httpRoutes.entries()) {
|
|
455
|
+
if (action === 'ws:set-context') continue; // Skip built-in
|
|
456
|
+
if (this.httpRoutes.has(action)) {
|
|
457
|
+
throw new Error(`HTTP route conflict during merge: ${action}`);
|
|
458
|
+
}
|
|
459
|
+
this.httpRoutes.set(action, route);
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// Copy all event schemas from the other router
|
|
463
|
+
for (const [event, schema] of router.eventSchemas.entries()) {
|
|
464
|
+
if (this.eventSchemas.has(event)) {
|
|
465
|
+
throw new Error(`Event schema conflict during merge: ${event}`);
|
|
466
|
+
}
|
|
467
|
+
this.eventSchemas.set(event, schema);
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
return this as any;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
/**
|
|
474
|
+
* Handle HTTP-like request-response message
|
|
475
|
+
* @internal
|
|
476
|
+
*/
|
|
477
|
+
private async handleHTTPMessage(conn: any, action: string, payload: any, route: HTTPRoute) {
|
|
478
|
+
const responseAction = `${action}:response`;
|
|
479
|
+
// Extract requestId from payload to match request with response
|
|
480
|
+
const requestId = payload?.requestId;
|
|
481
|
+
|
|
482
|
+
try {
|
|
483
|
+
// Extract data from payload
|
|
484
|
+
const data = payload?.data || {};
|
|
485
|
+
|
|
486
|
+
// Validate request data schema (if exists)
|
|
487
|
+
let validatedData = {};
|
|
488
|
+
if (route.dataSchema) {
|
|
489
|
+
try {
|
|
490
|
+
// Check for binary BEFORE validation
|
|
491
|
+
const hasBinary = containsBinary(data);
|
|
492
|
+
validatedData = Value.Decode(route.dataSchema, data);
|
|
493
|
+
|
|
494
|
+
// Restore binary data if it was converted
|
|
495
|
+
if (hasBinary && data.data instanceof Uint8Array) {
|
|
496
|
+
(validatedData as any).data = data.data;
|
|
497
|
+
}
|
|
498
|
+
} catch (err) {
|
|
499
|
+
throw new Error(
|
|
500
|
+
`Request validation failed: ${err instanceof Error ? err.message : 'Unknown error'}`
|
|
501
|
+
);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// Execute handler (can throw)
|
|
506
|
+
const result = await route.handler({
|
|
507
|
+
conn: conn,
|
|
508
|
+
data: validatedData
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
// Validate response data schema
|
|
512
|
+
let validatedResponse;
|
|
513
|
+
try {
|
|
514
|
+
validatedResponse = Value.Decode(route.responseSchema, result);
|
|
515
|
+
} catch (err) {
|
|
516
|
+
debug.error('websocket', `Response validation failed for ${action}:`, err);
|
|
517
|
+
throw new Error(
|
|
518
|
+
`Response validation failed: ${err instanceof Error ? err.message : 'Unknown error'}`
|
|
519
|
+
);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// Check if response contains binary data
|
|
523
|
+
if (isBinaryAction(responseAction, validatedResponse)) {
|
|
524
|
+
// For binary responses, wrap in success envelope with requestId
|
|
525
|
+
const wrappedResponse = { success: true, data: validatedResponse, requestId };
|
|
526
|
+
const binaryMessage = encodeBinaryMessage(responseAction, wrappedResponse);
|
|
527
|
+
conn.send(Buffer.from(binaryMessage));
|
|
528
|
+
debug.log('websocket', 'HTTP success (binary):', action);
|
|
529
|
+
} else {
|
|
530
|
+
// For JSON responses, wrap in success envelope with requestId
|
|
531
|
+
const wrappedResponse = { success: true, data: validatedResponse, requestId };
|
|
532
|
+
conn.send(JSON.stringify({ action: responseAction, payload: wrappedResponse }));
|
|
533
|
+
debug.log('websocket', 'HTTP success (JSON):', action);
|
|
534
|
+
}
|
|
535
|
+
} catch (err) {
|
|
536
|
+
// Catch ANY error and send wrapped in { success: false, error, requestId }
|
|
537
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
538
|
+
|
|
539
|
+
const errorResponse = { success: false, error: errorMessage, requestId };
|
|
540
|
+
conn.send(JSON.stringify({ action: responseAction, payload: errorResponse }));
|
|
541
|
+
debug.error('websocket', `HTTP error [${action}]:`, errorMessage);
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
/**
|
|
546
|
+
* Handle incoming WebSocket message (JSON or Binary)
|
|
547
|
+
* @internal
|
|
548
|
+
*/
|
|
549
|
+
private async handleMessage(conn: any, message: any) {
|
|
550
|
+
try {
|
|
551
|
+
let action: string;
|
|
552
|
+
let payload: any;
|
|
553
|
+
|
|
554
|
+
// Check if message is binary (ArrayBuffer or Buffer)
|
|
555
|
+
if (message instanceof ArrayBuffer) {
|
|
556
|
+
// Decode binary message
|
|
557
|
+
const decoded = decodeBinaryMessage(message);
|
|
558
|
+
action = decoded.action;
|
|
559
|
+
payload = decoded.payload;
|
|
560
|
+
} else if (Buffer.isBuffer(message)) {
|
|
561
|
+
// Node.js Buffer - convert to ArrayBuffer
|
|
562
|
+
const arrayBuffer = new ArrayBuffer(message.byteLength);
|
|
563
|
+
new Uint8Array(arrayBuffer).set(message);
|
|
564
|
+
const decoded = decodeBinaryMessage(arrayBuffer);
|
|
565
|
+
action = decoded.action;
|
|
566
|
+
payload = decoded.payload;
|
|
567
|
+
} else {
|
|
568
|
+
// Parse JSON message
|
|
569
|
+
const parsed = typeof message === 'string' ? JSON.parse(message) : message;
|
|
570
|
+
action = parsed.action;
|
|
571
|
+
payload = parsed.payload;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
if (!action || typeof action !== 'string') {
|
|
575
|
+
debug.error('websocket', 'Invalid message format');
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
// Check if this is an HTTP route
|
|
580
|
+
const httpRoute = this.httpRoutes.get(action);
|
|
581
|
+
if (httpRoute) {
|
|
582
|
+
await this.handleHTTPMessage(conn, action, payload, httpRoute);
|
|
583
|
+
return;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
// Find regular WebSocket route
|
|
587
|
+
const route = this.routes.get(action);
|
|
588
|
+
if (!route) {
|
|
589
|
+
debug.warn('websocket', `Unknown action: ${action}`);
|
|
590
|
+
return;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
// Validate payload against schema
|
|
594
|
+
try {
|
|
595
|
+
// Use TypeBox Value.Decode (bundled with Elysia)
|
|
596
|
+
const validatedData = Value.Decode(route.dataSchema, payload);
|
|
597
|
+
|
|
598
|
+
// Execute handler
|
|
599
|
+
await route.handler({ conn: conn, data: validatedData });
|
|
600
|
+
} catch (err) {
|
|
601
|
+
debug.error('websocket', `Data validation failed for ${action}:`, err);
|
|
602
|
+
}
|
|
603
|
+
} catch (err) {
|
|
604
|
+
debug.error('websocket', 'Message handling error:', err);
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
/**
|
|
609
|
+
* Convert router to Elysia plugin
|
|
610
|
+
*/
|
|
611
|
+
asPlugin(path: string = '/ws') {
|
|
612
|
+
return (app: Elysia) => {
|
|
613
|
+
return app.ws(path, {
|
|
614
|
+
message: (conn, message) => {
|
|
615
|
+
this.handleMessage(conn, message);
|
|
616
|
+
},
|
|
617
|
+
open: async (conn) => {
|
|
618
|
+
debug.log('websocket', 'Client connected');
|
|
619
|
+
|
|
620
|
+
// Register connection with ws singleton
|
|
621
|
+
try {
|
|
622
|
+
const { ws: wsServer } = await import('$backend/lib/utils/ws');
|
|
623
|
+
wsServer.register(conn);
|
|
624
|
+
} catch (err) {
|
|
625
|
+
debug.error('websocket', 'Failed to register connection:', err);
|
|
626
|
+
}
|
|
627
|
+
},
|
|
628
|
+
close: async (conn) => {
|
|
629
|
+
debug.log('websocket', 'Client disconnected');
|
|
630
|
+
|
|
631
|
+
// Unregister connection from ws singleton
|
|
632
|
+
// All registered cleanups are called automatically by unregister()
|
|
633
|
+
try {
|
|
634
|
+
const { ws: wsServer } = await import('$backend/lib/utils/ws');
|
|
635
|
+
wsServer.unregister(conn);
|
|
636
|
+
} catch (err) {
|
|
637
|
+
debug.error('websocket', 'Failed to unregister connection:', err);
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
});
|
|
641
|
+
};
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
/**
|
|
645
|
+
* Phantom getter for type inference
|
|
646
|
+
*/
|
|
647
|
+
get $api(): { client: TClient; server: TServer } {
|
|
648
|
+
throw new Error('$api is a phantom getter for type inference only');
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
/**
|
|
653
|
+
* Create a new WebSocket router
|
|
654
|
+
*/
|
|
655
|
+
export function createRouter(): WSRouter {
|
|
656
|
+
return new WSRouter();
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
// Export binary utilities for advanced use cases
|
|
660
|
+
export { encodeBinaryMessage, decodeBinaryMessage, containsBinary, isBinaryAction };
|
|
Binary file
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
|
|
2
|
+
<!-- Background -->
|
|
3
|
+
<rect width="100" height="100" rx="22" fill="#7C3AED"/>
|
|
4
|
+
<!-- Shared outline: sharp square (visible on right half) -->
|
|
5
|
+
<rect x="24" y="24" width="52" height="52" fill="none" stroke="#fff" stroke-width="8"/>
|
|
6
|
+
<!-- Closed half: filled left side -->
|
|
7
|
+
<rect x="20" y="20" width="30" height="60" fill="#fff"/>
|
|
8
|
+
</svg>
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|