@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,695 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser Video Capture Handler
|
|
3
|
+
*
|
|
4
|
+
* Handles WebCodecs-based video streaming with WebRTC DataChannel transport.
|
|
5
|
+
*
|
|
6
|
+
* Video Architecture:
|
|
7
|
+
* 1. Puppeteer CDP captures JPEG frames via Page.screencastFrame
|
|
8
|
+
* 2. Decode JPEG to ImageBitmap in browser
|
|
9
|
+
* 3. Encode with VideoEncoder (VP8) in browser
|
|
10
|
+
* 4. Send encoded chunks via RTCDataChannel
|
|
11
|
+
*
|
|
12
|
+
* Audio Architecture:
|
|
13
|
+
* 1. AudioContext interception (handled by BrowserAudioCapture)
|
|
14
|
+
* 2. Audio encoded with AudioEncoder (Opus) in headless browser
|
|
15
|
+
* 3. Encoded chunks sent via sendAudioChunk() to same DataChannel
|
|
16
|
+
*
|
|
17
|
+
* Client:
|
|
18
|
+
* - Receives video + audio chunks via DataChannel
|
|
19
|
+
* - Decodes with VideoDecoder + AudioDecoder
|
|
20
|
+
* - Renders video to canvas, plays audio with proper scheduling
|
|
21
|
+
*
|
|
22
|
+
* Benefits vs Canvas + WebRTC:
|
|
23
|
+
* - Lower bandwidth (500-800 Kbps vs 1 Mbps)
|
|
24
|
+
* - More control over codec selection
|
|
25
|
+
* - Skip canvas rendering overhead
|
|
26
|
+
* - DataChannel = lower latency than video track
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import { EventEmitter } from 'events';
|
|
30
|
+
import type { Page } from 'puppeteer';
|
|
31
|
+
import type { BrowserTab, StreamingConfig } from './types';
|
|
32
|
+
import { DEFAULT_STREAMING_CONFIG } from './types';
|
|
33
|
+
import { videoEncoderScript } from './scripts/video-stream';
|
|
34
|
+
import { debug } from '$shared/utils/logger';
|
|
35
|
+
|
|
36
|
+
interface VideoStreamSession {
|
|
37
|
+
sessionId: string;
|
|
38
|
+
isActive: boolean;
|
|
39
|
+
clientConnected: boolean;
|
|
40
|
+
headlessReady: boolean;
|
|
41
|
+
pendingCandidates: RTCIceCandidateInit[];
|
|
42
|
+
scriptInjected: boolean; // Track if persistent script was injected
|
|
43
|
+
stats: {
|
|
44
|
+
videoBytesSent: number;
|
|
45
|
+
audioBytesSent: number;
|
|
46
|
+
videoFramesEncoded: number;
|
|
47
|
+
audioFramesEncoded: number;
|
|
48
|
+
connectionState: string;
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export class BrowserVideoCapture extends EventEmitter {
|
|
53
|
+
private sessions = new Map<string, VideoStreamSession>();
|
|
54
|
+
|
|
55
|
+
constructor() {
|
|
56
|
+
super();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Start video streaming for a session
|
|
61
|
+
*/
|
|
62
|
+
async startStreaming(
|
|
63
|
+
sessionId: string,
|
|
64
|
+
session: BrowserTab,
|
|
65
|
+
isValidSession: () => boolean
|
|
66
|
+
): Promise<boolean> {
|
|
67
|
+
debug.log('webcodecs', `Starting streaming for session ${sessionId}`);
|
|
68
|
+
|
|
69
|
+
// If session exists, stop it first
|
|
70
|
+
if (this.sessions.has(sessionId)) {
|
|
71
|
+
debug.log('webcodecs', `Session ${sessionId} exists, stopping for restart`);
|
|
72
|
+
await this.stopStreaming(sessionId, session);
|
|
73
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (!session.page || session.page.isClosed()) {
|
|
77
|
+
debug.error('webcodecs', `Cannot start: page is closed`);
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
try {
|
|
82
|
+
const page = session.page;
|
|
83
|
+
const viewport = page.viewport()!;
|
|
84
|
+
const config = DEFAULT_STREAMING_CONFIG;
|
|
85
|
+
|
|
86
|
+
// Get scale from session (default to 1 if not set)
|
|
87
|
+
const scale = session.scale || 1;
|
|
88
|
+
debug.log('webcodecs', `Using scale: ${scale} for session ${sessionId}`);
|
|
89
|
+
|
|
90
|
+
// Get or create session tracking
|
|
91
|
+
let videoSession = this.sessions.get(sessionId);
|
|
92
|
+
const isRestart = !!videoSession;
|
|
93
|
+
|
|
94
|
+
if (!videoSession) {
|
|
95
|
+
videoSession = {
|
|
96
|
+
sessionId,
|
|
97
|
+
isActive: false,
|
|
98
|
+
clientConnected: false,
|
|
99
|
+
headlessReady: false,
|
|
100
|
+
pendingCandidates: [],
|
|
101
|
+
scriptInjected: false,
|
|
102
|
+
stats: {
|
|
103
|
+
videoBytesSent: 0,
|
|
104
|
+
audioBytesSent: 0,
|
|
105
|
+
videoFramesEncoded: 0,
|
|
106
|
+
audioFramesEncoded: 0,
|
|
107
|
+
connectionState: 'new'
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
this.sessions.set(sessionId, videoSession);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Check if bindings exist
|
|
114
|
+
const bindingsExist = await page.evaluate(() => {
|
|
115
|
+
return typeof (window as any).__sendIceCandidate === 'function';
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
// Expose signaling functions (persists across navigations)
|
|
119
|
+
if (!bindingsExist) {
|
|
120
|
+
await page.exposeFunction('__sendIceCandidate', (candidate: RTCIceCandidateInit) => {
|
|
121
|
+
const activeSession = Array.from(this.sessions.values()).find(s => s.isActive);
|
|
122
|
+
if (!activeSession) return;
|
|
123
|
+
this.emit('ice-candidate', { sessionId: activeSession.sessionId, candidate, from: 'headless' });
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
await page.exposeFunction('__sendConnectionState', (state: string) => {
|
|
127
|
+
const activeSession = Array.from(this.sessions.values()).find(s => s.isActive);
|
|
128
|
+
if (activeSession) {
|
|
129
|
+
activeSession.stats.connectionState = state;
|
|
130
|
+
this.emit('connection-state', { sessionId: activeSession.sessionId, state });
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
await page.exposeFunction('__sendCursorChange', (cursor: string) => {
|
|
135
|
+
const activeSession = Array.from(this.sessions.values()).find(s => s.isActive);
|
|
136
|
+
if (activeSession) {
|
|
137
|
+
this.emit('cursor-change', { sessionId: activeSession.sessionId, cursor });
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Calculate scaled dimensions
|
|
143
|
+
const scaledWidth = Math.round(viewport.width * scale);
|
|
144
|
+
const scaledHeight = Math.round(viewport.height * scale);
|
|
145
|
+
|
|
146
|
+
const videoConfig: StreamingConfig['video'] = {
|
|
147
|
+
...config.video,
|
|
148
|
+
width: scaledWidth,
|
|
149
|
+
height: scaledHeight
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
// Store config globally for persistent script access
|
|
153
|
+
await page.evaluate((cfg) => {
|
|
154
|
+
(window as any).__videoEncoderConfig = cfg;
|
|
155
|
+
}, videoConfig);
|
|
156
|
+
|
|
157
|
+
// Inject persistent video encoder script (survives navigation)
|
|
158
|
+
// Only inject once per page instance
|
|
159
|
+
if (!videoSession.scriptInjected) {
|
|
160
|
+
await page.evaluateOnNewDocument(videoEncoderScript, videoConfig);
|
|
161
|
+
videoSession.scriptInjected = true;
|
|
162
|
+
debug.log('webcodecs', `Persistent video encoder script injected for ${sessionId}`);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Also inject immediately for current page context
|
|
166
|
+
// (evaluateOnNewDocument only runs on NEXT navigation)
|
|
167
|
+
await page.evaluate(videoEncoderScript, videoConfig);
|
|
168
|
+
|
|
169
|
+
// Verify peer was created
|
|
170
|
+
const peerExists = await page.evaluate(() => {
|
|
171
|
+
return typeof (window as any).__webCodecsPeer?.startStreaming === 'function';
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
if (!peerExists) {
|
|
175
|
+
debug.error('webcodecs', `Peer script injected but __webCodecsPeer not available`);
|
|
176
|
+
this.sessions.delete(sessionId);
|
|
177
|
+
return false;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
videoSession.isActive = true;
|
|
181
|
+
|
|
182
|
+
// Wait for page to be fully loaded
|
|
183
|
+
try {
|
|
184
|
+
const loadState = await page.evaluate(() => document.readyState);
|
|
185
|
+
if (loadState !== 'complete') {
|
|
186
|
+
debug.log('webcodecs', `Waiting for page load...`);
|
|
187
|
+
await page.waitForFunction(() => document.readyState === 'complete', { timeout: 60000 });
|
|
188
|
+
}
|
|
189
|
+
} catch (loadError) {
|
|
190
|
+
debug.warn('webcodecs', 'Page load wait timed out, proceeding anyway');
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Start video streaming
|
|
194
|
+
const started = await page.evaluate(() => {
|
|
195
|
+
return (window as any).__webCodecsPeer?.startStreaming();
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
if (!started) {
|
|
199
|
+
debug.error('webcodecs', `startStreaming returned false`);
|
|
200
|
+
this.sessions.delete(sessionId);
|
|
201
|
+
return false;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Initialize and start audio encoder (from AudioContext interception)
|
|
205
|
+
const audioEncoderAvailable = await page.evaluate(() => {
|
|
206
|
+
return typeof (window as any).__audioEncoder?.init === 'function';
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
if (audioEncoderAvailable) {
|
|
210
|
+
debug.log('webcodecs', 'Initializing audio encoder from AudioContext interception...');
|
|
211
|
+
|
|
212
|
+
const audioInitialized = await page.evaluate(async () => {
|
|
213
|
+
const encoder = (window as any).__audioEncoder;
|
|
214
|
+
if (!encoder) return false;
|
|
215
|
+
|
|
216
|
+
const initiated = await encoder.init();
|
|
217
|
+
if (initiated) {
|
|
218
|
+
return encoder.start();
|
|
219
|
+
}
|
|
220
|
+
return false;
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
if (audioInitialized) {
|
|
224
|
+
debug.log('webcodecs', 'Audio encoder initialized and started');
|
|
225
|
+
} else {
|
|
226
|
+
debug.warn('webcodecs', 'Audio encoder initialization failed, continuing with video only');
|
|
227
|
+
}
|
|
228
|
+
} else {
|
|
229
|
+
debug.warn('webcodecs', 'Audio encoder not available (AudioEncoder API may not be supported)');
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
videoSession.headlessReady = true;
|
|
233
|
+
|
|
234
|
+
// Setup CDP screencast to feed frames to encoder
|
|
235
|
+
await this.setupFrameFeeder(sessionId, session, config, isValidSession);
|
|
236
|
+
|
|
237
|
+
debug.log('webcodecs', `Streaming started for ${sessionId}`);
|
|
238
|
+
return true;
|
|
239
|
+
} catch (error) {
|
|
240
|
+
debug.error('webcodecs', `Failed to start streaming:`, error);
|
|
241
|
+
this.sessions.delete(sessionId);
|
|
242
|
+
throw error;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Setup CDP screencast to feed JPEG frames to VideoEncoder
|
|
248
|
+
*/
|
|
249
|
+
private async setupFrameFeeder(
|
|
250
|
+
sessionId: string,
|
|
251
|
+
session: BrowserTab,
|
|
252
|
+
config: StreamingConfig,
|
|
253
|
+
isValidSession: () => boolean
|
|
254
|
+
): Promise<void> {
|
|
255
|
+
const page = session.page;
|
|
256
|
+
const viewport = page.viewport()!;
|
|
257
|
+
|
|
258
|
+
// Get scale from session (default to 1 if not set)
|
|
259
|
+
const scale = session.scale || 1;
|
|
260
|
+
|
|
261
|
+
const cdp = await page.createCDPSession();
|
|
262
|
+
|
|
263
|
+
let cdpFrameCount = 0;
|
|
264
|
+
|
|
265
|
+
cdp.on('Page.screencastFrame', async (event: any) => {
|
|
266
|
+
cdpFrameCount++;
|
|
267
|
+
|
|
268
|
+
const videoSession = this.sessions.get(sessionId);
|
|
269
|
+
if (!videoSession?.isActive || session.isDestroyed) {
|
|
270
|
+
cdp.send('Page.screencastFrameAck', { sessionId: event.sessionId }).catch(() => {});
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if (!isValidSession()) {
|
|
275
|
+
this.stopStreaming(sessionId);
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// ACK immediately
|
|
280
|
+
cdp.send('Page.screencastFrameAck', { sessionId: event.sessionId }).catch(() => {});
|
|
281
|
+
|
|
282
|
+
// Send frame to encoder
|
|
283
|
+
page.evaluate((frameData) => {
|
|
284
|
+
const peer = (window as any).__webCodecsPeer;
|
|
285
|
+
if (!peer) return false;
|
|
286
|
+
peer.encodeFrame(frameData);
|
|
287
|
+
return true;
|
|
288
|
+
}, event.data).catch((err) => {
|
|
289
|
+
if (cdpFrameCount <= 5) {
|
|
290
|
+
debug.warn('webcodecs', `Frame delivery error (frame ${cdpFrameCount}):`, err.message);
|
|
291
|
+
}
|
|
292
|
+
});
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
// Start screencast with scaled dimensions
|
|
296
|
+
const scaledWidth = Math.round(viewport.width * scale);
|
|
297
|
+
const scaledHeight = Math.round(viewport.height * scale);
|
|
298
|
+
|
|
299
|
+
await cdp.send('Page.startScreencast', {
|
|
300
|
+
format: 'jpeg',
|
|
301
|
+
quality: config.video.screenshotQuality,
|
|
302
|
+
maxWidth: scaledWidth,
|
|
303
|
+
maxHeight: scaledHeight,
|
|
304
|
+
everyNthFrame: 1
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
debug.log('webcodecs', `CDP screencast started with scaled dimensions: ${scaledWidth}x${scaledHeight} (scale: ${scale})`);
|
|
308
|
+
(session as any).__webCodecsCdp = cdp;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Create offer from headless browser
|
|
313
|
+
*/
|
|
314
|
+
async createOffer(sessionId: string, session: BrowserTab): Promise<RTCSessionDescriptionInit | null> {
|
|
315
|
+
const videoSession = this.sessions.get(sessionId);
|
|
316
|
+
if (!videoSession?.isActive || !session.page) {
|
|
317
|
+
return null;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const maxRetries = 5;
|
|
321
|
+
const retryDelay = 100;
|
|
322
|
+
|
|
323
|
+
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
324
|
+
try {
|
|
325
|
+
const peerReady = await session.page.evaluate(() => {
|
|
326
|
+
return typeof (window as any).__webCodecsPeer?.createOffer === 'function';
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
if (!peerReady) {
|
|
330
|
+
if (attempt < maxRetries - 1) {
|
|
331
|
+
await new Promise(resolve => setTimeout(resolve, retryDelay));
|
|
332
|
+
continue;
|
|
333
|
+
}
|
|
334
|
+
return null;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const offer = await session.page.evaluate(() => {
|
|
338
|
+
return (window as any).__webCodecsPeer?.createOffer();
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
if (offer) return offer;
|
|
342
|
+
|
|
343
|
+
if (attempt < maxRetries - 1) {
|
|
344
|
+
await new Promise(resolve => setTimeout(resolve, retryDelay));
|
|
345
|
+
}
|
|
346
|
+
} catch (error) {
|
|
347
|
+
debug.error('webcodecs', `Create offer error (attempt ${attempt + 1}):`, error);
|
|
348
|
+
if (attempt < maxRetries - 1) {
|
|
349
|
+
await new Promise(resolve => setTimeout(resolve, retryDelay));
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
return null;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Handle answer from client
|
|
359
|
+
*/
|
|
360
|
+
async handleAnswer(
|
|
361
|
+
sessionId: string,
|
|
362
|
+
session: BrowserTab,
|
|
363
|
+
answer: RTCSessionDescriptionInit
|
|
364
|
+
): Promise<boolean> {
|
|
365
|
+
const videoSession = this.sessions.get(sessionId);
|
|
366
|
+
if (!videoSession?.isActive || !session.page) {
|
|
367
|
+
return false;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
try {
|
|
371
|
+
const success = await session.page.evaluate((ans) => {
|
|
372
|
+
return (window as any).__webCodecsPeer?.handleAnswer(ans);
|
|
373
|
+
}, answer);
|
|
374
|
+
|
|
375
|
+
if (success) {
|
|
376
|
+
videoSession.clientConnected = true;
|
|
377
|
+
|
|
378
|
+
// Process pending ICE candidates
|
|
379
|
+
for (const candidate of videoSession.pendingCandidates) {
|
|
380
|
+
await this.addIceCandidate(sessionId, session, candidate);
|
|
381
|
+
}
|
|
382
|
+
videoSession.pendingCandidates = [];
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
return success;
|
|
386
|
+
} catch (error) {
|
|
387
|
+
debug.error('webcodecs', `Handle answer error:`, error);
|
|
388
|
+
return false;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Add ICE candidate from client
|
|
394
|
+
*/
|
|
395
|
+
async addIceCandidate(
|
|
396
|
+
sessionId: string,
|
|
397
|
+
session: BrowserTab,
|
|
398
|
+
candidate: RTCIceCandidateInit
|
|
399
|
+
): Promise<boolean> {
|
|
400
|
+
const videoSession = this.sessions.get(sessionId);
|
|
401
|
+
if (!videoSession?.isActive || !session.page) {
|
|
402
|
+
return false;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// Queue if not connected yet
|
|
406
|
+
if (!videoSession.clientConnected) {
|
|
407
|
+
videoSession.pendingCandidates.push(candidate);
|
|
408
|
+
return true;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
try {
|
|
412
|
+
return await session.page.evaluate((cand) => {
|
|
413
|
+
return (window as any).__webCodecsPeer?.addIceCandidate(cand);
|
|
414
|
+
}, candidate);
|
|
415
|
+
} catch (error) {
|
|
416
|
+
debug.error('webcodecs', `Add ICE candidate error:`, error);
|
|
417
|
+
return false;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* Update viewport and scale without reconnection (hot-swap)
|
|
423
|
+
*/
|
|
424
|
+
async updateViewport(sessionId: string, session: BrowserTab, width: number, height: number, newScale: number): Promise<boolean> {
|
|
425
|
+
const videoSession = this.sessions.get(sessionId);
|
|
426
|
+
if (!videoSession?.isActive || !session.page || session.page.isClosed()) {
|
|
427
|
+
debug.warn('webcodecs', `Cannot update viewport: session not active`);
|
|
428
|
+
return false;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
try {
|
|
432
|
+
const page = session.page;
|
|
433
|
+
const config = DEFAULT_STREAMING_CONFIG;
|
|
434
|
+
|
|
435
|
+
// Calculate scaled dimensions
|
|
436
|
+
const scaledWidth = Math.round(width * newScale);
|
|
437
|
+
const scaledHeight = Math.round(height * newScale);
|
|
438
|
+
|
|
439
|
+
debug.log('webcodecs', `🔄 Hot-swapping viewport to ${width}x${height} (scaled: ${scaledWidth}x${scaledHeight}, scale: ${newScale})`);
|
|
440
|
+
|
|
441
|
+
// Step 1: Update viewport via CDP (without page reload)
|
|
442
|
+
await page.setViewport({ width, height });
|
|
443
|
+
|
|
444
|
+
// Step 2: Reconfigure VideoEncoder with scaled dimensions
|
|
445
|
+
const reconfigured = await page.evaluate((dimensions) => {
|
|
446
|
+
const peer = (window as any).__webCodecsPeer;
|
|
447
|
+
if (!peer || !peer.reconfigureEncoder) return false;
|
|
448
|
+
return peer.reconfigureEncoder(dimensions.width, dimensions.height);
|
|
449
|
+
}, { width: scaledWidth, height: scaledHeight });
|
|
450
|
+
|
|
451
|
+
if (!reconfigured) {
|
|
452
|
+
debug.error('webcodecs', `Failed to reconfigure encoder`);
|
|
453
|
+
return false;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// Step 3: Restart CDP screencast with new dimensions
|
|
457
|
+
const cdp = (session as any).__webCodecsCdp;
|
|
458
|
+
if (cdp) {
|
|
459
|
+
await cdp.send('Page.stopScreencast').catch(() => {});
|
|
460
|
+
await cdp.send('Page.startScreencast', {
|
|
461
|
+
format: 'jpeg',
|
|
462
|
+
quality: config.video.screenshotQuality,
|
|
463
|
+
maxWidth: scaledWidth,
|
|
464
|
+
maxHeight: scaledHeight,
|
|
465
|
+
everyNthFrame: 1
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
debug.log('webcodecs', `✅ Viewport hot-swapped successfully to ${width}x${height} (scale: ${newScale})`);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
return true;
|
|
472
|
+
} catch (error) {
|
|
473
|
+
debug.error('webcodecs', `Failed to update viewport:`, error);
|
|
474
|
+
return false;
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/**
|
|
479
|
+
* Update scale without reconnection (hot-swap)
|
|
480
|
+
*/
|
|
481
|
+
async updateScale(sessionId: string, session: BrowserTab, newScale: number): Promise<boolean> {
|
|
482
|
+
const videoSession = this.sessions.get(sessionId);
|
|
483
|
+
if (!videoSession?.isActive || !session.page || session.page.isClosed()) {
|
|
484
|
+
debug.warn('webcodecs', `Cannot update scale: session not active`);
|
|
485
|
+
return false;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
try {
|
|
489
|
+
const page = session.page;
|
|
490
|
+
const viewport = page.viewport()!;
|
|
491
|
+
const config = DEFAULT_STREAMING_CONFIG;
|
|
492
|
+
|
|
493
|
+
// Calculate new scaled dimensions
|
|
494
|
+
const scaledWidth = Math.round(viewport.width * newScale);
|
|
495
|
+
const scaledHeight = Math.round(viewport.height * newScale);
|
|
496
|
+
|
|
497
|
+
debug.log('webcodecs', `🔄 Hot-swapping resolution to ${scaledWidth}x${scaledHeight} (scale: ${newScale})`);
|
|
498
|
+
|
|
499
|
+
// Step 1: Reconfigure VideoEncoder in headless browser
|
|
500
|
+
const reconfigured = await page.evaluate((dimensions) => {
|
|
501
|
+
const peer = (window as any).__webCodecsPeer;
|
|
502
|
+
if (!peer || !peer.reconfigureEncoder) return false;
|
|
503
|
+
return peer.reconfigureEncoder(dimensions.width, dimensions.height);
|
|
504
|
+
}, { width: scaledWidth, height: scaledHeight });
|
|
505
|
+
|
|
506
|
+
if (!reconfigured) {
|
|
507
|
+
debug.error('webcodecs', `Failed to reconfigure encoder`);
|
|
508
|
+
return false;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// Step 2: Restart CDP screencast with new dimensions
|
|
512
|
+
const cdp = (session as any).__webCodecsCdp;
|
|
513
|
+
if (cdp) {
|
|
514
|
+
// Stop current screencast
|
|
515
|
+
await cdp.send('Page.stopScreencast').catch(() => {});
|
|
516
|
+
|
|
517
|
+
// Start with new dimensions
|
|
518
|
+
await cdp.send('Page.startScreencast', {
|
|
519
|
+
format: 'jpeg',
|
|
520
|
+
quality: config.video.screenshotQuality,
|
|
521
|
+
maxWidth: scaledWidth,
|
|
522
|
+
maxHeight: scaledHeight,
|
|
523
|
+
everyNthFrame: 1
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
debug.log('webcodecs', `✅ Scale hot-swapped successfully to ${scaledWidth}x${scaledHeight}`);
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
return true;
|
|
530
|
+
} catch (error) {
|
|
531
|
+
debug.error('webcodecs', `Failed to update scale:`, error);
|
|
532
|
+
return false;
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
/**
|
|
537
|
+
* Handle navigation - re-inject peer script and restart CDP screencast
|
|
538
|
+
* Called after page navigation to restore video streaming without full reconnection
|
|
539
|
+
*/
|
|
540
|
+
async handleNavigation(sessionId: string, session: BrowserTab): Promise<boolean> {
|
|
541
|
+
const videoSession = this.sessions.get(sessionId);
|
|
542
|
+
if (!videoSession?.isActive || !session.page || session.page.isClosed()) {
|
|
543
|
+
debug.warn('webcodecs', `Cannot handle navigation: session not active`);
|
|
544
|
+
return false;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
try {
|
|
548
|
+
const page = session.page;
|
|
549
|
+
const viewport = page.viewport()!;
|
|
550
|
+
const config = DEFAULT_STREAMING_CONFIG;
|
|
551
|
+
const scale = session.scale || 1;
|
|
552
|
+
|
|
553
|
+
debug.log('webcodecs', `🔄 Handling navigation for ${sessionId} - re-injecting peer script and restarting screencast`);
|
|
554
|
+
|
|
555
|
+
// Calculate scaled dimensions
|
|
556
|
+
const scaledWidth = Math.round(viewport.width * scale);
|
|
557
|
+
const scaledHeight = Math.round(viewport.height * scale);
|
|
558
|
+
|
|
559
|
+
const videoConfig: StreamingConfig['video'] = {
|
|
560
|
+
...config.video,
|
|
561
|
+
width: scaledWidth,
|
|
562
|
+
height: scaledHeight
|
|
563
|
+
};
|
|
564
|
+
|
|
565
|
+
// Re-inject video encoder script to new page context
|
|
566
|
+
// (evaluateOnNewDocument doesn't run for the current navigation, only future ones)
|
|
567
|
+
await page.evaluate((cfg) => {
|
|
568
|
+
(window as any).__videoEncoderConfig = cfg;
|
|
569
|
+
}, videoConfig);
|
|
570
|
+
|
|
571
|
+
await page.evaluate(videoEncoderScript, videoConfig);
|
|
572
|
+
|
|
573
|
+
// Verify peer was re-created
|
|
574
|
+
const peerExists = await page.evaluate(() => {
|
|
575
|
+
return typeof (window as any).__webCodecsPeer?.startStreaming === 'function';
|
|
576
|
+
});
|
|
577
|
+
|
|
578
|
+
if (!peerExists) {
|
|
579
|
+
debug.error('webcodecs', `Peer script re-injection failed - peer not available`);
|
|
580
|
+
return false;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
// Start video streaming on new page
|
|
584
|
+
const started = await page.evaluate(() => {
|
|
585
|
+
return (window as any).__webCodecsPeer?.startStreaming();
|
|
586
|
+
});
|
|
587
|
+
|
|
588
|
+
if (!started) {
|
|
589
|
+
debug.error('webcodecs', `Failed to start streaming on new page`);
|
|
590
|
+
return false;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
// Restart CDP screencast
|
|
594
|
+
const cdp = (session as any).__webCodecsCdp;
|
|
595
|
+
if (cdp) {
|
|
596
|
+
// Stop current screencast
|
|
597
|
+
await cdp.send('Page.stopScreencast').catch(() => {});
|
|
598
|
+
|
|
599
|
+
// Start with current dimensions
|
|
600
|
+
await cdp.send('Page.startScreencast', {
|
|
601
|
+
format: 'jpeg',
|
|
602
|
+
quality: config.video.screenshotQuality,
|
|
603
|
+
maxWidth: scaledWidth,
|
|
604
|
+
maxHeight: scaledHeight,
|
|
605
|
+
everyNthFrame: 1
|
|
606
|
+
});
|
|
607
|
+
|
|
608
|
+
debug.log('webcodecs', `✅ Navigation handled - screencast restarted at ${scaledWidth}x${scaledHeight}`);
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
// Emit event to notify frontend that streaming is ready
|
|
612
|
+
this.emit('navigation-streaming-ready', { sessionId });
|
|
613
|
+
|
|
614
|
+
return true;
|
|
615
|
+
} catch (error) {
|
|
616
|
+
debug.error('webcodecs', `Failed to handle navigation:`, error);
|
|
617
|
+
return false;
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
/**
|
|
622
|
+
* Stop video streaming
|
|
623
|
+
*/
|
|
624
|
+
async stopStreaming(sessionId: string, session?: BrowserTab): Promise<void> {
|
|
625
|
+
const videoSession = this.sessions.get(sessionId);
|
|
626
|
+
if (!videoSession) return;
|
|
627
|
+
|
|
628
|
+
debug.log('webcodecs', `Stopping streaming for ${sessionId}`);
|
|
629
|
+
|
|
630
|
+
videoSession.isActive = false;
|
|
631
|
+
|
|
632
|
+
if (session?.page && !session.page.isClosed()) {
|
|
633
|
+
try {
|
|
634
|
+
// Stop peer
|
|
635
|
+
await session.page.evaluate(() => {
|
|
636
|
+
(window as any).__webCodecsPeer?.stopStreaming();
|
|
637
|
+
});
|
|
638
|
+
|
|
639
|
+
// Stop CDP screencast
|
|
640
|
+
const cdp = (session as any).__webCodecsCdp;
|
|
641
|
+
if (cdp) {
|
|
642
|
+
await cdp.send('Page.stopScreencast').catch(() => {});
|
|
643
|
+
await cdp.detach().catch(() => {});
|
|
644
|
+
(session as any).__webCodecsCdp = null;
|
|
645
|
+
}
|
|
646
|
+
} catch (error) {
|
|
647
|
+
debug.warn('webcodecs', `Error during cleanup: ${error}`);
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
this.sessions.delete(sessionId);
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
/**
|
|
655
|
+
* Check if streaming is active
|
|
656
|
+
*/
|
|
657
|
+
isStreaming(sessionId: string): boolean {
|
|
658
|
+
return this.sessions.get(sessionId)?.isActive ?? false;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
/**
|
|
662
|
+
* Get session stats
|
|
663
|
+
*/
|
|
664
|
+
async getStats(sessionId: string, session: BrowserTab): Promise<VideoStreamSession['stats'] | null> {
|
|
665
|
+
const videoSession = this.sessions.get(sessionId);
|
|
666
|
+
if (!videoSession?.isActive || !session.page) {
|
|
667
|
+
return videoSession?.stats || null;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
try {
|
|
671
|
+
const stats = await session.page.evaluate(() => {
|
|
672
|
+
return (window as any).__webCodecsPeer?.getStats();
|
|
673
|
+
});
|
|
674
|
+
|
|
675
|
+
if (stats) {
|
|
676
|
+
videoSession.stats = stats;
|
|
677
|
+
}
|
|
678
|
+
return videoSession.stats;
|
|
679
|
+
} catch (error) {
|
|
680
|
+
return videoSession.stats;
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
/**
|
|
685
|
+
* Cleanup all sessions
|
|
686
|
+
*/
|
|
687
|
+
async cleanup(): Promise<void> {
|
|
688
|
+
debug.log('webcodecs', 'Cleaning up all sessions');
|
|
689
|
+
|
|
690
|
+
const sessionIds = Array.from(this.sessions.keys());
|
|
691
|
+
await Promise.all(sessionIds.map((id) => this.stopStreaming(id)));
|
|
692
|
+
|
|
693
|
+
this.sessions.clear();
|
|
694
|
+
}
|
|
695
|
+
}
|