@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,768 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WebSocket Client Core Library - Optimized
|
|
3
|
+
*
|
|
4
|
+
* High-performance WebSocket client with:
|
|
5
|
+
* - 100% type inference from backend API
|
|
6
|
+
* - Singleton TextEncoder/Decoder for performance
|
|
7
|
+
* - Automatic context sync (user/project)
|
|
8
|
+
* - Automatic reconnection with exponential backoff
|
|
9
|
+
* - Binary message support for efficient data transfer
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { debug } from './logger';
|
|
13
|
+
|
|
14
|
+
// ============================================================================
|
|
15
|
+
// Singleton Encoders (Performance Optimization)
|
|
16
|
+
// ============================================================================
|
|
17
|
+
|
|
18
|
+
/** Singleton TextEncoder - reused across all encode operations */
|
|
19
|
+
const textEncoder = new TextEncoder();
|
|
20
|
+
|
|
21
|
+
/** Singleton TextDecoder - reused across all decode operations */
|
|
22
|
+
const textDecoder = new TextDecoder();
|
|
23
|
+
|
|
24
|
+
// ============================================================================
|
|
25
|
+
// Pre-computed Binary Actions
|
|
26
|
+
// ============================================================================
|
|
27
|
+
|
|
28
|
+
/** Actions known to contain binary data - skip containsBinary() check */
|
|
29
|
+
const BINARY_ACTIONS = new Set<string>([
|
|
30
|
+
'preview:frame',
|
|
31
|
+
'file:upload',
|
|
32
|
+
'file:download',
|
|
33
|
+
'terminal:binary'
|
|
34
|
+
]);
|
|
35
|
+
|
|
36
|
+
// ============================================================================
|
|
37
|
+
// Client Options
|
|
38
|
+
// ============================================================================
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* WebSocket client options
|
|
42
|
+
*/
|
|
43
|
+
export interface WSClientOptions {
|
|
44
|
+
/** Auto-reconnect on connection loss */
|
|
45
|
+
autoReconnect?: boolean;
|
|
46
|
+
/** Maximum reconnection attempts (0 = infinite) */
|
|
47
|
+
maxReconnectAttempts?: number;
|
|
48
|
+
/** Initial reconnect delay in ms */
|
|
49
|
+
reconnectDelay?: number;
|
|
50
|
+
/** Maximum reconnect delay in ms */
|
|
51
|
+
maxReconnectDelay?: number;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ============================================================================
|
|
55
|
+
// Binary Message Utilities (Optimized)
|
|
56
|
+
// ============================================================================
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Check if payload contains binary data
|
|
60
|
+
* Optimized with early return and iterative approach
|
|
61
|
+
*/
|
|
62
|
+
function containsBinary(obj: any): boolean {
|
|
63
|
+
if (obj instanceof Uint8Array || obj instanceof ArrayBuffer) return true;
|
|
64
|
+
if (typeof obj !== 'object' || obj === null) return false;
|
|
65
|
+
|
|
66
|
+
const stack = [obj];
|
|
67
|
+
while (stack.length > 0) {
|
|
68
|
+
const current = stack.pop();
|
|
69
|
+
for (const value of Object.values(current)) {
|
|
70
|
+
if (value instanceof Uint8Array || value instanceof ArrayBuffer) return true;
|
|
71
|
+
if (typeof value === 'object' && value !== null) {
|
|
72
|
+
stack.push(value);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Fast check using pre-computed binary actions
|
|
81
|
+
*/
|
|
82
|
+
function isBinaryAction(action: string, payload: any): boolean {
|
|
83
|
+
if (BINARY_ACTIONS.has(action)) return true;
|
|
84
|
+
return containsBinary(payload);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Extract binary field and metadata from payload
|
|
89
|
+
*/
|
|
90
|
+
function extractBinaryFields(payload: any): { binaryData: Uint8Array; metadata: Record<string, any> } {
|
|
91
|
+
const metadata: Record<string, any> = {};
|
|
92
|
+
let binaryData: Uint8Array = new Uint8Array(0);
|
|
93
|
+
|
|
94
|
+
for (const [key, value] of Object.entries(payload)) {
|
|
95
|
+
if (value instanceof Uint8Array) {
|
|
96
|
+
binaryData = value;
|
|
97
|
+
} else if (value instanceof ArrayBuffer) {
|
|
98
|
+
binaryData = new Uint8Array(value);
|
|
99
|
+
} else {
|
|
100
|
+
metadata[key] = value;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return { binaryData, metadata };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Encode a binary message with action and metadata
|
|
109
|
+
*
|
|
110
|
+
* Binary Message Format:
|
|
111
|
+
* ┌─────────────────┬────────────────┬─────────────────┬──────────────┬─────────────┐
|
|
112
|
+
* │ Action Length │ Action String │ Metadata Length │ Metadata JSON│ Binary Data │
|
|
113
|
+
* │ (1 byte) │ (N bytes) │ (4 bytes) │ (M bytes) │ (rest) │
|
|
114
|
+
* └─────────────────┴────────────────┴─────────────────┴──────────────┴─────────────┘
|
|
115
|
+
*/
|
|
116
|
+
function encodeBinaryMessage(action: string, payload: any): ArrayBuffer {
|
|
117
|
+
const { binaryData, metadata } = extractBinaryFields(payload);
|
|
118
|
+
|
|
119
|
+
const actionBytes = textEncoder.encode(action);
|
|
120
|
+
const metaBytes = textEncoder.encode(JSON.stringify(metadata));
|
|
121
|
+
|
|
122
|
+
const totalLength = 1 + actionBytes.length + 4 + metaBytes.length + binaryData.length;
|
|
123
|
+
const buffer = new ArrayBuffer(totalLength);
|
|
124
|
+
const view = new DataView(buffer);
|
|
125
|
+
const uint8 = new Uint8Array(buffer);
|
|
126
|
+
|
|
127
|
+
let offset = 0;
|
|
128
|
+
|
|
129
|
+
// Action length (1 byte)
|
|
130
|
+
view.setUint8(offset, actionBytes.length);
|
|
131
|
+
offset += 1;
|
|
132
|
+
|
|
133
|
+
// Action string
|
|
134
|
+
uint8.set(actionBytes, offset);
|
|
135
|
+
offset += actionBytes.length;
|
|
136
|
+
|
|
137
|
+
// Metadata length (4 bytes)
|
|
138
|
+
view.setUint32(offset, metaBytes.length);
|
|
139
|
+
offset += 4;
|
|
140
|
+
|
|
141
|
+
// Metadata JSON
|
|
142
|
+
uint8.set(metaBytes, offset);
|
|
143
|
+
offset += metaBytes.length;
|
|
144
|
+
|
|
145
|
+
// Binary data
|
|
146
|
+
uint8.set(binaryData, offset);
|
|
147
|
+
|
|
148
|
+
return buffer;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Decode a binary message back to action and payload
|
|
153
|
+
*/
|
|
154
|
+
function decodeBinaryMessage(buffer: ArrayBuffer): { action: string; payload: any } {
|
|
155
|
+
const view = new DataView(buffer);
|
|
156
|
+
const uint8 = new Uint8Array(buffer);
|
|
157
|
+
|
|
158
|
+
let offset = 0;
|
|
159
|
+
|
|
160
|
+
// Read action length (1 byte)
|
|
161
|
+
const actionLength = view.getUint8(offset);
|
|
162
|
+
offset += 1;
|
|
163
|
+
|
|
164
|
+
// Read action string
|
|
165
|
+
const actionBytes = uint8.slice(offset, offset + actionLength);
|
|
166
|
+
const action = textDecoder.decode(actionBytes);
|
|
167
|
+
offset += actionLength;
|
|
168
|
+
|
|
169
|
+
// Read metadata length (4 bytes)
|
|
170
|
+
const metaLength = view.getUint32(offset);
|
|
171
|
+
offset += 4;
|
|
172
|
+
|
|
173
|
+
// Read metadata JSON
|
|
174
|
+
const metaBytes = uint8.slice(offset, offset + metaLength);
|
|
175
|
+
const metadata = JSON.parse(textDecoder.decode(metaBytes));
|
|
176
|
+
offset += metaLength;
|
|
177
|
+
|
|
178
|
+
// Read binary data (rest of buffer)
|
|
179
|
+
const binaryData = uint8.slice(offset);
|
|
180
|
+
|
|
181
|
+
// Reconstruct payload with binary data
|
|
182
|
+
const payload = {
|
|
183
|
+
...metadata,
|
|
184
|
+
data: binaryData // Binary field always named 'data'
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
return { action, payload };
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// ============================================================================
|
|
191
|
+
// WebSocket Client
|
|
192
|
+
// ============================================================================
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Type-safe WebSocket Client with Binary Support and Context Sync
|
|
196
|
+
*/
|
|
197
|
+
export class WSClient<TAPI extends { client: any; server: any }> {
|
|
198
|
+
private ws: WebSocket | null = null;
|
|
199
|
+
private url: string;
|
|
200
|
+
private options: Required<WSClientOptions>;
|
|
201
|
+
private reconnectAttempts = 0;
|
|
202
|
+
private reconnectTimeout: ReturnType<typeof setTimeout> | null = null;
|
|
203
|
+
private listeners = new Map<string, Set<(payload: any) => void>>();
|
|
204
|
+
private messageQueue: Array<{ action: string; payload: any }> = [];
|
|
205
|
+
private isConnected = false;
|
|
206
|
+
private shouldReconnect = true;
|
|
207
|
+
|
|
208
|
+
/** Current context (synced with server) */
|
|
209
|
+
private context: {
|
|
210
|
+
userId: string | null;
|
|
211
|
+
projectId: string | null;
|
|
212
|
+
} = {
|
|
213
|
+
userId: null,
|
|
214
|
+
projectId: null
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
/** Pending context sync (for reconnection) */
|
|
218
|
+
private pendingContextSync = false;
|
|
219
|
+
|
|
220
|
+
/** Resolvers waiting for connection to be fully ready */
|
|
221
|
+
private connectResolvers: Array<() => void> = [];
|
|
222
|
+
|
|
223
|
+
constructor(url: string, options: WSClientOptions = {}) {
|
|
224
|
+
this.url = url;
|
|
225
|
+
this.options = {
|
|
226
|
+
autoReconnect: options.autoReconnect ?? true,
|
|
227
|
+
maxReconnectAttempts: options.maxReconnectAttempts ?? 5,
|
|
228
|
+
reconnectDelay: options.reconnectDelay ?? 1000,
|
|
229
|
+
maxReconnectDelay: options.maxReconnectDelay ?? 30000
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
this.connect();
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Establish WebSocket connection
|
|
237
|
+
*/
|
|
238
|
+
private connect(): void {
|
|
239
|
+
try {
|
|
240
|
+
// CRITICAL: Close any existing connection before creating a new one
|
|
241
|
+
// This prevents zombie connections from accumulating on the server
|
|
242
|
+
// (e.g., during reconnection or HMR, old connections may linger)
|
|
243
|
+
if (this.ws) {
|
|
244
|
+
try {
|
|
245
|
+
this.ws.onclose = null; // Prevent triggering reconnect from this close
|
|
246
|
+
this.ws.onerror = null;
|
|
247
|
+
this.ws.onmessage = null;
|
|
248
|
+
this.ws.close();
|
|
249
|
+
} catch {
|
|
250
|
+
// Ignore close errors on stale socket
|
|
251
|
+
}
|
|
252
|
+
this.ws = null;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
debug.log('websocket', 'Connecting to', this.url);
|
|
256
|
+
this.ws = new WebSocket(this.url);
|
|
257
|
+
|
|
258
|
+
// IMPORTANT: Enable binary message handling
|
|
259
|
+
this.ws.binaryType = 'arraybuffer';
|
|
260
|
+
|
|
261
|
+
this.ws.onopen = async () => {
|
|
262
|
+
debug.log('websocket', 'Connected');
|
|
263
|
+
this.isConnected = true;
|
|
264
|
+
this.reconnectAttempts = 0;
|
|
265
|
+
|
|
266
|
+
// Sync context on reconnection - MUST await before flushing queue
|
|
267
|
+
if (this.context.userId || this.context.projectId) {
|
|
268
|
+
try {
|
|
269
|
+
await this.syncContext();
|
|
270
|
+
debug.log('websocket', 'Context synced after reconnection');
|
|
271
|
+
} catch (err) {
|
|
272
|
+
debug.error('websocket', 'Failed to sync context on reconnection:', err);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Flush queued messages AFTER context is synced
|
|
277
|
+
while (this.messageQueue.length > 0) {
|
|
278
|
+
const msg = this.messageQueue.shift();
|
|
279
|
+
if (msg) {
|
|
280
|
+
this.sendRaw(msg.action, msg.payload);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Resolve waitUntilConnected() callers AFTER context sync + queue flush
|
|
285
|
+
for (const resolve of this.connectResolvers) {
|
|
286
|
+
resolve();
|
|
287
|
+
}
|
|
288
|
+
this.connectResolvers = [];
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
this.ws.onmessage = (event) => {
|
|
292
|
+
try {
|
|
293
|
+
if (event.data instanceof ArrayBuffer) {
|
|
294
|
+
// Binary message
|
|
295
|
+
debug.log('websocket', `Received ArrayBuffer: ${event.data.byteLength} bytes`);
|
|
296
|
+
this.handleBinaryMessage(event.data);
|
|
297
|
+
} else if (event.data instanceof Blob) {
|
|
298
|
+
// Blob message - convert to ArrayBuffer
|
|
299
|
+
debug.log('websocket', `Received Blob: ${event.data.size} bytes, converting to ArrayBuffer`);
|
|
300
|
+
event.data.arrayBuffer().then((buffer) => {
|
|
301
|
+
this.handleBinaryMessage(buffer);
|
|
302
|
+
});
|
|
303
|
+
} else {
|
|
304
|
+
// JSON message
|
|
305
|
+
this.handleTextMessage(event.data);
|
|
306
|
+
}
|
|
307
|
+
} catch (err) {
|
|
308
|
+
debug.error('websocket', 'Message handling error:', err);
|
|
309
|
+
}
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
this.ws.onerror = (error) => {
|
|
313
|
+
debug.error('websocket', 'WebSocket error:', error);
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
this.ws.onclose = () => {
|
|
317
|
+
debug.log('websocket', 'Disconnected');
|
|
318
|
+
this.isConnected = false;
|
|
319
|
+
this.ws = null;
|
|
320
|
+
|
|
321
|
+
// Auto-reconnect
|
|
322
|
+
if (this.shouldReconnect && this.options.autoReconnect) {
|
|
323
|
+
this.scheduleReconnect();
|
|
324
|
+
}
|
|
325
|
+
};
|
|
326
|
+
} catch (err) {
|
|
327
|
+
debug.error('websocket', 'Connection error:', err);
|
|
328
|
+
if (this.shouldReconnect && this.options.autoReconnect) {
|
|
329
|
+
this.scheduleReconnect();
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Handle text/JSON message from server
|
|
336
|
+
*/
|
|
337
|
+
private handleTextMessage(data: string): void {
|
|
338
|
+
// Check if data looks like binary (not valid JSON start)
|
|
339
|
+
if (data && data.length > 0 && data[0] !== '{' && data[0] !== '[') {
|
|
340
|
+
debug.warn('websocket', `Received non-JSON text data (length: ${data.length}), first char code: ${data.charCodeAt(0)}`);
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
const parsed = JSON.parse(data);
|
|
345
|
+
const { action, payload } = parsed;
|
|
346
|
+
|
|
347
|
+
if (!action) {
|
|
348
|
+
debug.log('websocket', 'Invalid message format:', parsed);
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
this.dispatchToListeners(action, payload);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Handle binary message from server
|
|
357
|
+
*/
|
|
358
|
+
private handleBinaryMessage(buffer: ArrayBuffer): void {
|
|
359
|
+
const { action, payload } = decodeBinaryMessage(buffer);
|
|
360
|
+
|
|
361
|
+
if (!action) {
|
|
362
|
+
debug.log('websocket', 'Invalid binary message format');
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
debug.log('websocket', 'Received binary:', action, `(${buffer.byteLength} bytes)`);
|
|
367
|
+
this.dispatchToListeners(action, payload);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* Dispatch message to registered listeners
|
|
372
|
+
*/
|
|
373
|
+
private dispatchToListeners(action: string, payload: any): void {
|
|
374
|
+
const callbacks = this.listeners.get(action);
|
|
375
|
+
if (callbacks) {
|
|
376
|
+
callbacks.forEach((cb) => {
|
|
377
|
+
try {
|
|
378
|
+
cb(payload);
|
|
379
|
+
} catch (err) {
|
|
380
|
+
debug.error('websocket', `Listener error for ${action}:`, err);
|
|
381
|
+
}
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* Schedule reconnection with exponential backoff
|
|
388
|
+
*/
|
|
389
|
+
private scheduleReconnect(): void {
|
|
390
|
+
if (this.options.maxReconnectAttempts > 0 && this.reconnectAttempts >= this.options.maxReconnectAttempts) {
|
|
391
|
+
debug.error('websocket', 'Max reconnect attempts reached');
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
this.reconnectAttempts++;
|
|
396
|
+
const delay = Math.min(
|
|
397
|
+
this.options.reconnectDelay * Math.pow(2, this.reconnectAttempts - 1),
|
|
398
|
+
this.options.maxReconnectDelay
|
|
399
|
+
);
|
|
400
|
+
|
|
401
|
+
debug.log('websocket', `Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts})`);
|
|
402
|
+
|
|
403
|
+
this.reconnectTimeout = setTimeout(() => {
|
|
404
|
+
this.connect();
|
|
405
|
+
}, delay);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Emit message to server (type-safe)
|
|
410
|
+
* Automatically detects binary data and sends as binary message
|
|
411
|
+
*/
|
|
412
|
+
emit<TEvent extends keyof TAPI['client']>(
|
|
413
|
+
action: TEvent,
|
|
414
|
+
payload: TAPI['client'][TEvent]
|
|
415
|
+
): void {
|
|
416
|
+
if (this.isConnected && this.ws) {
|
|
417
|
+
this.sendRaw(action as string, payload);
|
|
418
|
+
} else {
|
|
419
|
+
// Queue message for sending when connected
|
|
420
|
+
debug.log('websocket', 'Queueing message (not connected):', action);
|
|
421
|
+
this.messageQueue.push({ action: action as string, payload });
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* Send raw message (JSON or Binary based on payload content)
|
|
427
|
+
*/
|
|
428
|
+
private sendRaw(action: string, payload: any): void {
|
|
429
|
+
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
430
|
+
debug.log('websocket', 'Cannot send, socket not ready:', action);
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
try {
|
|
435
|
+
if (isBinaryAction(action, payload)) {
|
|
436
|
+
// Send as binary message
|
|
437
|
+
const binaryMessage = encodeBinaryMessage(action, payload);
|
|
438
|
+
this.ws.send(binaryMessage);
|
|
439
|
+
debug.log('websocket', 'Sent binary:', action, `(${binaryMessage.byteLength} bytes)`);
|
|
440
|
+
} else {
|
|
441
|
+
// Send as JSON message
|
|
442
|
+
this.ws.send(JSON.stringify({ action, payload }));
|
|
443
|
+
debug.log('websocket', 'Sent JSON:', action);
|
|
444
|
+
}
|
|
445
|
+
} catch (err) {
|
|
446
|
+
debug.error('websocket', 'Send error:', err);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* Listen for server messages (type-safe)
|
|
452
|
+
*/
|
|
453
|
+
on<TEvent extends keyof TAPI['server']>(
|
|
454
|
+
action: TEvent,
|
|
455
|
+
callback: (payload: TAPI['server'][TEvent]) => void
|
|
456
|
+
): () => void {
|
|
457
|
+
const actionStr = action as string;
|
|
458
|
+
|
|
459
|
+
if (!this.listeners.has(actionStr)) {
|
|
460
|
+
this.listeners.set(actionStr, new Set());
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
this.listeners.get(actionStr)!.add(callback);
|
|
464
|
+
debug.log('websocket', 'Listener added:', actionStr);
|
|
465
|
+
|
|
466
|
+
// Return unsubscribe function
|
|
467
|
+
return () => {
|
|
468
|
+
const callbacks = this.listeners.get(actionStr);
|
|
469
|
+
if (callbacks) {
|
|
470
|
+
callbacks.delete(callback);
|
|
471
|
+
if (callbacks.size === 0) {
|
|
472
|
+
this.listeners.delete(actionStr);
|
|
473
|
+
}
|
|
474
|
+
debug.log('websocket', 'Listener removed:', actionStr);
|
|
475
|
+
}
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
/**
|
|
480
|
+
* Remove all listeners for an action
|
|
481
|
+
*/
|
|
482
|
+
off<TEvent extends keyof TAPI['server']>(action: TEvent): void {
|
|
483
|
+
const actionStr = action as string;
|
|
484
|
+
this.listeners.delete(actionStr);
|
|
485
|
+
debug.log('websocket', 'All listeners removed:', actionStr);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/**
|
|
489
|
+
* Disconnect and cleanup
|
|
490
|
+
*/
|
|
491
|
+
disconnect(): void {
|
|
492
|
+
this.shouldReconnect = false;
|
|
493
|
+
|
|
494
|
+
if (this.reconnectTimeout) {
|
|
495
|
+
clearTimeout(this.reconnectTimeout);
|
|
496
|
+
this.reconnectTimeout = null;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
if (this.ws) {
|
|
500
|
+
this.ws.close();
|
|
501
|
+
this.ws = null;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
this.listeners.clear();
|
|
505
|
+
this.messageQueue = [];
|
|
506
|
+
this.isConnected = false;
|
|
507
|
+
|
|
508
|
+
debug.log('websocket', 'Disconnected and cleaned up');
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
/**
|
|
512
|
+
* Check if connected
|
|
513
|
+
*/
|
|
514
|
+
connected(): boolean {
|
|
515
|
+
return this.isConnected;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
/**
|
|
519
|
+
* Wait until WebSocket is fully connected and ready (context synced, queue flushed).
|
|
520
|
+
* Resolves immediately if already connected.
|
|
521
|
+
*/
|
|
522
|
+
waitUntilConnected(timeout = 10000): Promise<void> {
|
|
523
|
+
if (this.isConnected) return Promise.resolve();
|
|
524
|
+
|
|
525
|
+
return new Promise<void>((resolve, reject) => {
|
|
526
|
+
const timer = setTimeout(() => {
|
|
527
|
+
const idx = this.connectResolvers.indexOf(doResolve);
|
|
528
|
+
if (idx >= 0) this.connectResolvers.splice(idx, 1);
|
|
529
|
+
reject(new Error(`WebSocket connection timeout (${timeout}ms)`));
|
|
530
|
+
}, timeout);
|
|
531
|
+
|
|
532
|
+
const doResolve = () => {
|
|
533
|
+
clearTimeout(timer);
|
|
534
|
+
resolve();
|
|
535
|
+
};
|
|
536
|
+
|
|
537
|
+
this.connectResolvers.push(doResolve);
|
|
538
|
+
});
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
/**
|
|
542
|
+
* Manual reconnect
|
|
543
|
+
*/
|
|
544
|
+
reconnect(): void {
|
|
545
|
+
this.disconnect();
|
|
546
|
+
this.shouldReconnect = true;
|
|
547
|
+
this.reconnectAttempts = 0;
|
|
548
|
+
this.connect();
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
// =========================================================================
|
|
552
|
+
// Context Management
|
|
553
|
+
// =========================================================================
|
|
554
|
+
|
|
555
|
+
/** Promise for ongoing context sync */
|
|
556
|
+
private contextSyncPromise: Promise<void> | null = null;
|
|
557
|
+
|
|
558
|
+
/**
|
|
559
|
+
* Set user context (auto-syncs with server)
|
|
560
|
+
* @returns Promise that resolves when context is synced with server
|
|
561
|
+
*/
|
|
562
|
+
async setUser(userId: string | null): Promise<void> {
|
|
563
|
+
if (this.context.userId === userId) return;
|
|
564
|
+
|
|
565
|
+
this.context.userId = userId;
|
|
566
|
+
debug.log('websocket', 'Context: user set to', userId);
|
|
567
|
+
|
|
568
|
+
if (this.isConnected) {
|
|
569
|
+
await this.syncContext();
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
/**
|
|
574
|
+
* Set project context (auto-syncs with server)
|
|
575
|
+
* @returns Promise that resolves when context is synced with server
|
|
576
|
+
*/
|
|
577
|
+
async setProject(projectId: string | null): Promise<void> {
|
|
578
|
+
if (this.context.projectId === projectId) return;
|
|
579
|
+
|
|
580
|
+
this.context.projectId = projectId;
|
|
581
|
+
debug.log('websocket', 'Context: project set to', projectId);
|
|
582
|
+
|
|
583
|
+
if (this.isConnected) {
|
|
584
|
+
await this.syncContext();
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
/**
|
|
589
|
+
* Get current context
|
|
590
|
+
*/
|
|
591
|
+
getContext(): { userId: string | null; projectId: string | null } {
|
|
592
|
+
return { ...this.context };
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
/**
|
|
596
|
+
* Wait for any pending context sync to complete
|
|
597
|
+
*/
|
|
598
|
+
async waitForContextSync(): Promise<void> {
|
|
599
|
+
if (this.contextSyncPromise) {
|
|
600
|
+
await this.contextSyncPromise;
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
/**
|
|
605
|
+
* Sync context with server (awaitable)
|
|
606
|
+
* Waits for server confirmation before resolving
|
|
607
|
+
*/
|
|
608
|
+
private async syncContext(): Promise<void> {
|
|
609
|
+
// If there's already a pending sync, wait for it
|
|
610
|
+
if (this.contextSyncPromise) {
|
|
611
|
+
await this.contextSyncPromise;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
// Create a new sync promise
|
|
615
|
+
this.contextSyncPromise = this.doSyncContext();
|
|
616
|
+
|
|
617
|
+
try {
|
|
618
|
+
await this.contextSyncPromise;
|
|
619
|
+
} finally {
|
|
620
|
+
this.contextSyncPromise = null;
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
/**
|
|
625
|
+
* Actually perform the context sync
|
|
626
|
+
*/
|
|
627
|
+
private async doSyncContext(): Promise<void> {
|
|
628
|
+
const requestId = `context-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
629
|
+
|
|
630
|
+
return new Promise<void>((resolve, reject) => {
|
|
631
|
+
let unsubResponse: (() => void) | null = null;
|
|
632
|
+
let timeoutId: ReturnType<typeof setTimeout> | null = null;
|
|
633
|
+
|
|
634
|
+
const cleanup = () => {
|
|
635
|
+
unsubResponse?.();
|
|
636
|
+
if (timeoutId) {
|
|
637
|
+
clearTimeout(timeoutId);
|
|
638
|
+
timeoutId = null;
|
|
639
|
+
}
|
|
640
|
+
};
|
|
641
|
+
|
|
642
|
+
// Response handler
|
|
643
|
+
const handleResponse = (response: any) => {
|
|
644
|
+
if (response?.requestId !== requestId) return;
|
|
645
|
+
|
|
646
|
+
cleanup();
|
|
647
|
+
|
|
648
|
+
if (response.success) {
|
|
649
|
+
debug.log('websocket', 'Context sync confirmed by server:', response.data);
|
|
650
|
+
resolve();
|
|
651
|
+
} else {
|
|
652
|
+
debug.error('websocket', 'Context sync failed:', response.error);
|
|
653
|
+
reject(new Error(response.error || 'Context sync failed'));
|
|
654
|
+
}
|
|
655
|
+
};
|
|
656
|
+
|
|
657
|
+
// Timeout handler (2 seconds — localhost round-trip should be near-instant)
|
|
658
|
+
timeoutId = setTimeout(() => {
|
|
659
|
+
cleanup();
|
|
660
|
+
debug.warn('websocket', 'Context sync timeout, assuming success');
|
|
661
|
+
// Don't reject on timeout - just warn and continue
|
|
662
|
+
resolve();
|
|
663
|
+
}, 2000);
|
|
664
|
+
|
|
665
|
+
// Register response listener
|
|
666
|
+
unsubResponse = this.on('ws:set-context:response' as any, handleResponse);
|
|
667
|
+
|
|
668
|
+
// Send context sync request
|
|
669
|
+
this.emit('ws:set-context' as any, {
|
|
670
|
+
requestId,
|
|
671
|
+
data: {
|
|
672
|
+
userId: this.context.userId,
|
|
673
|
+
projectId: this.context.projectId
|
|
674
|
+
}
|
|
675
|
+
} as any);
|
|
676
|
+
|
|
677
|
+
debug.log('websocket', 'Context sync sent:', this.context);
|
|
678
|
+
});
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
// =========================================================================
|
|
682
|
+
// HTTP-like Request-Response Pattern
|
|
683
|
+
// =========================================================================
|
|
684
|
+
|
|
685
|
+
/**
|
|
686
|
+
* HTTP-like request-response pattern over WebSocket
|
|
687
|
+
*
|
|
688
|
+
* This method provides a simplified API for request-response pattern.
|
|
689
|
+
* The server always returns `{ success, data?, error? }` response.
|
|
690
|
+
* This method unwraps the response:
|
|
691
|
+
* - If `success: true` → returns `data` directly
|
|
692
|
+
* - If `success: false` → throws Error with `error` message
|
|
693
|
+
* - Timeout → throws Error
|
|
694
|
+
*/
|
|
695
|
+
http<TAction extends keyof TAPI['client']>(
|
|
696
|
+
action: TAction,
|
|
697
|
+
data?: TAPI['client'][TAction] extends { data: infer D } ? D : never,
|
|
698
|
+
timeout: number = 30000
|
|
699
|
+
): Promise<
|
|
700
|
+
TAPI['server'][`${TAction & string}:response`] extends { success: boolean; data?: infer TData }
|
|
701
|
+
? TData
|
|
702
|
+
: any
|
|
703
|
+
> {
|
|
704
|
+
const responseAction = `${action as string}:response` as any;
|
|
705
|
+
// Generate unique request ID to match request with response
|
|
706
|
+
const requestId = `${action as string}-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
707
|
+
|
|
708
|
+
return new Promise((resolve, reject) => {
|
|
709
|
+
let unsubResponse: (() => void) | null = null;
|
|
710
|
+
let timeoutId: ReturnType<typeof setTimeout> | null = null;
|
|
711
|
+
|
|
712
|
+
// Cleanup function
|
|
713
|
+
const cleanup = () => {
|
|
714
|
+
unsubResponse?.();
|
|
715
|
+
if (timeoutId) {
|
|
716
|
+
clearTimeout(timeoutId);
|
|
717
|
+
timeoutId = null;
|
|
718
|
+
}
|
|
719
|
+
};
|
|
720
|
+
|
|
721
|
+
// Response handler - unwrap { success, data, error } response
|
|
722
|
+
const handleResponse = (response: any) => {
|
|
723
|
+
// Only handle response that matches this request ID
|
|
724
|
+
if (response && response.requestId !== requestId) {
|
|
725
|
+
return; // Ignore responses for other requests
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
cleanup();
|
|
729
|
+
|
|
730
|
+
// Check if response has success field
|
|
731
|
+
if (response && typeof response === 'object' && 'success' in response) {
|
|
732
|
+
if (response.success) {
|
|
733
|
+
// Success: return data directly (unwrapped)
|
|
734
|
+
resolve(response.data);
|
|
735
|
+
} else {
|
|
736
|
+
// Failure: throw error with message from response.error
|
|
737
|
+
reject(new Error(response.error || 'Unknown error'));
|
|
738
|
+
}
|
|
739
|
+
} else {
|
|
740
|
+
// Legacy response format or unexpected structure
|
|
741
|
+
resolve(response);
|
|
742
|
+
}
|
|
743
|
+
};
|
|
744
|
+
|
|
745
|
+
// Timeout handler
|
|
746
|
+
if (timeout > 0) {
|
|
747
|
+
timeoutId = setTimeout(() => {
|
|
748
|
+
cleanup();
|
|
749
|
+
reject(new Error(`Request timeout: ${action as string} (${timeout}ms)`));
|
|
750
|
+
}, timeout);
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
// Register response listener
|
|
754
|
+
unsubResponse = this.on(responseAction, handleResponse);
|
|
755
|
+
|
|
756
|
+
// Send request with data structure and requestId
|
|
757
|
+
const payload = {
|
|
758
|
+
requestId,
|
|
759
|
+
data: data || {}
|
|
760
|
+
};
|
|
761
|
+
|
|
762
|
+
this.emit(action as any, payload as any);
|
|
763
|
+
});
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
// Export binary utilities for advanced use cases
|
|
768
|
+
export { encodeBinaryMessage, decodeBinaryMessage, containsBinary, isBinaryAction };
|