@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,513 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser Interact WebSocket Handler
|
|
3
|
+
* Handles mouse/keyboard interactions with browser
|
|
4
|
+
* **PROJECT ISOLATION**: Uses project-specific BrowserPreviewService instances
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { t } from 'elysia';
|
|
8
|
+
import { createRouter } from '$shared/utils/ws-server';
|
|
9
|
+
import { browserPreviewServiceManager } from '../../../lib/preview/index';
|
|
10
|
+
import { ws } from '$backend/lib/utils/ws';
|
|
11
|
+
import type { KeyInput } from 'puppeteer';
|
|
12
|
+
import { debug } from '$shared/utils/logger';
|
|
13
|
+
import { sleep } from '$shared/utils/async';
|
|
14
|
+
|
|
15
|
+
// Helper function to check if error is navigation-related
|
|
16
|
+
function isNavigationError(error: Error): boolean {
|
|
17
|
+
const msg = error.message.toLowerCase();
|
|
18
|
+
return msg.includes('execution context was destroyed') ||
|
|
19
|
+
msg.includes('target closed') ||
|
|
20
|
+
msg.includes('page is loading') ||
|
|
21
|
+
msg.includes('waiting for navigation') ||
|
|
22
|
+
msg.includes('inspected target navigated') ||
|
|
23
|
+
msg.includes('cannot find context');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const interactPreviewHandler = createRouter()
|
|
27
|
+
// Action: Client requests browser interaction
|
|
28
|
+
.on('preview:browser-interact', {
|
|
29
|
+
data: t.Object({
|
|
30
|
+
action: t.Object({
|
|
31
|
+
type: t.String(),
|
|
32
|
+
x: t.Optional(t.Number()),
|
|
33
|
+
y: t.Optional(t.Number()),
|
|
34
|
+
startX: t.Optional(t.Number()),
|
|
35
|
+
startY: t.Optional(t.Number()),
|
|
36
|
+
endX: t.Optional(t.Number()),
|
|
37
|
+
endY: t.Optional(t.Number()),
|
|
38
|
+
currentX: t.Optional(t.Number()),
|
|
39
|
+
currentY: t.Optional(t.Number()),
|
|
40
|
+
text: t.Optional(t.String()),
|
|
41
|
+
key: t.Optional(t.String()),
|
|
42
|
+
button: t.Optional(t.String()),
|
|
43
|
+
delay: t.Optional(t.Number()),
|
|
44
|
+
steps: t.Optional(t.Number()),
|
|
45
|
+
deltaX: t.Optional(t.Number()),
|
|
46
|
+
deltaY: t.Optional(t.Number()),
|
|
47
|
+
selector: t.Optional(t.String()),
|
|
48
|
+
value: t.Optional(t.String()),
|
|
49
|
+
optionIndex: t.Optional(t.Number()),
|
|
50
|
+
ctrlKey: t.Optional(t.Boolean()),
|
|
51
|
+
metaKey: t.Optional(t.Boolean()),
|
|
52
|
+
altKey: t.Optional(t.Boolean()),
|
|
53
|
+
shiftKey: t.Optional(t.Boolean()),
|
|
54
|
+
clearFirst: t.Optional(t.Boolean()),
|
|
55
|
+
scale: t.Optional(t.Number()),
|
|
56
|
+
width: t.Optional(t.Number()),
|
|
57
|
+
height: t.Optional(t.Number()),
|
|
58
|
+
deviceSize: t.Optional(t.String()),
|
|
59
|
+
rotation: t.Optional(t.String())
|
|
60
|
+
}),
|
|
61
|
+
})
|
|
62
|
+
}, async ({ data, conn }) => {
|
|
63
|
+
const userId = ws.getUserId(conn);
|
|
64
|
+
const projectId = ws.getProjectId(conn);
|
|
65
|
+
|
|
66
|
+
// Get project-specific preview service
|
|
67
|
+
const previewService = browserPreviewServiceManager.getService(projectId);
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
const { action } = data;
|
|
71
|
+
|
|
72
|
+
// Get active tab (mirip MCP pattern)
|
|
73
|
+
const tab = previewService.getActiveTab();
|
|
74
|
+
if (!tab) {
|
|
75
|
+
debug.error('preview', `❌ NO ACTIVE TAB for project ${projectId} - Available tabs: ${previewService.getTabCount()}`);
|
|
76
|
+
ws.emit.user(userId, 'preview:browser-error', {
|
|
77
|
+
message: 'No active tab. Please open or switch to a tab first.'
|
|
78
|
+
});
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const session = tab;
|
|
83
|
+
const tabId = tab.id;
|
|
84
|
+
|
|
85
|
+
// Check if page is still open and valid
|
|
86
|
+
if (!session.page || session.page.isClosed()) {
|
|
87
|
+
debug.warn('preview', `⚠️ Cannot execute interaction: page is closed for tab ${tabId}`);
|
|
88
|
+
ws.emit.user(userId, 'preview:browser-error', {
|
|
89
|
+
message: 'Browser page is closed'
|
|
90
|
+
});
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Check if browser is still connected
|
|
95
|
+
if (!session.browser || !session.browser.connected) {
|
|
96
|
+
debug.warn('preview', `⚠️ Cannot execute interaction: browser disconnected for tab ${tabId}`);
|
|
97
|
+
ws.emit.user(userId, 'preview:browser-error', {
|
|
98
|
+
message: 'Browser is disconnected'
|
|
99
|
+
});
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Mark tab activity to prevent automatic cleanup
|
|
104
|
+
previewService.markActiveTabActivity();
|
|
105
|
+
|
|
106
|
+
// Execute the interaction based on type with navigation-safe error handling
|
|
107
|
+
try {
|
|
108
|
+
switch (action.type) {
|
|
109
|
+
case 'mousedown':
|
|
110
|
+
try {
|
|
111
|
+
// Reset mouse state first to ensure clean state
|
|
112
|
+
try { await session.page.mouse.up(); } catch { }
|
|
113
|
+
// Move to position and press button
|
|
114
|
+
await session.page.mouse.move(action.x!, action.y!, { steps: 1 });
|
|
115
|
+
await session.page.mouse.down({ button: action.button === 'right' ? 'right' : 'left' });
|
|
116
|
+
} catch (error) {
|
|
117
|
+
if (error instanceof Error && isNavigationError(error)) {
|
|
118
|
+
ws.emit.user(userId, 'preview:browser-interacted', { action: action.type, message: 'Action deferred (navigation)', deferred: true });
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
throw error;
|
|
122
|
+
}
|
|
123
|
+
break;
|
|
124
|
+
|
|
125
|
+
case 'mouseup':
|
|
126
|
+
try {
|
|
127
|
+
await session.page.mouse.up({ button: action.button === 'right' ? 'right' : 'left' });
|
|
128
|
+
} catch (error) {
|
|
129
|
+
if (error instanceof Error && isNavigationError(error)) {
|
|
130
|
+
ws.emit.user(userId, 'preview:browser-interacted', { action: action.type, message: 'Action deferred (navigation)', deferred: true });
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
// Ignore "not pressed" errors - might have been released already
|
|
134
|
+
if (error instanceof Error && error.message.includes('not pressed')) {
|
|
135
|
+
break;
|
|
136
|
+
}
|
|
137
|
+
throw error;
|
|
138
|
+
}
|
|
139
|
+
break;
|
|
140
|
+
|
|
141
|
+
case 'click':
|
|
142
|
+
try {
|
|
143
|
+
// Reset mouse state before click to prevent "already pressed" errors
|
|
144
|
+
// This ensures a clean state for each click operation
|
|
145
|
+
try {
|
|
146
|
+
await session.page.mouse.up();
|
|
147
|
+
} catch { /* Ignore - mouse might not be pressed */ }
|
|
148
|
+
|
|
149
|
+
// IMPORTANT: Check for select element BEFORE clicking
|
|
150
|
+
// If it's a select, we'll emit event to frontend instead of clicking
|
|
151
|
+
const selectInfo = await previewService.checkForSelectElement(session.id, action.x!, action.y!);
|
|
152
|
+
if (selectInfo) {
|
|
153
|
+
// Select element detected - event emitted by checkForSelectElement
|
|
154
|
+
// Don't perform regular click, frontend will show dropdown overlay
|
|
155
|
+
debug.log('preview', `✅ Select element detected at (${action.x}, ${action.y}), skipping regular click`);
|
|
156
|
+
break;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Perform the click - this does down + up atomically
|
|
160
|
+
await session.page.mouse.click(action.x!, action.y!);
|
|
161
|
+
} catch (error) {
|
|
162
|
+
if (error instanceof Error) {
|
|
163
|
+
if (isNavigationError(error)) {
|
|
164
|
+
ws.emit.user(userId, 'preview:browser-interacted', { action: action.type, message: 'Click triggered navigation', deferred: true });
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
// These errors are recoverable - the click likely worked
|
|
168
|
+
if (error.message.includes('already pressed') || error.message.includes('not pressed')) {
|
|
169
|
+
// Mouse state errors are recoverable, continue without fallback
|
|
170
|
+
break;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
throw error;
|
|
174
|
+
}
|
|
175
|
+
break;
|
|
176
|
+
|
|
177
|
+
case 'type':
|
|
178
|
+
if (action.text) {
|
|
179
|
+
try {
|
|
180
|
+
// For user manual typing, clearFirst defaults to false (append behavior)
|
|
181
|
+
// User can explicitly set clearFirst: true if they want to clear
|
|
182
|
+
const shouldClear = action.clearFirst === true; // default false for user
|
|
183
|
+
|
|
184
|
+
if (shouldClear) {
|
|
185
|
+
// Select all text first (Ctrl+A), then type will overwrite
|
|
186
|
+
await session.page.keyboard.down('Control');
|
|
187
|
+
await session.page.keyboard.press('KeyA');
|
|
188
|
+
await session.page.keyboard.up('Control');
|
|
189
|
+
await sleep(50); // Small delay for selection
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
await session.page.keyboard.type(action.text, {
|
|
193
|
+
delay: action.delay || 50
|
|
194
|
+
});
|
|
195
|
+
} catch (error) {
|
|
196
|
+
if (error instanceof Error && isNavigationError(error)) {
|
|
197
|
+
ws.emit.user(userId, 'preview:browser-interacted', { action: action.type, message: 'Action deferred (navigation)', deferred: true });
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
throw error;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
break;
|
|
204
|
+
|
|
205
|
+
case 'key':
|
|
206
|
+
if (action.key) {
|
|
207
|
+
try {
|
|
208
|
+
// Handle modifier keys (Ctrl+C, Ctrl+V, etc.)
|
|
209
|
+
const modifiers: string[] = [];
|
|
210
|
+
if (action.ctrlKey) modifiers.push('Control');
|
|
211
|
+
if (action.metaKey) modifiers.push('Meta');
|
|
212
|
+
if (action.altKey) modifiers.push('Alt');
|
|
213
|
+
if (action.shiftKey) modifiers.push('Shift');
|
|
214
|
+
|
|
215
|
+
if (modifiers.length > 0) {
|
|
216
|
+
// Press modifiers first
|
|
217
|
+
for (const mod of modifiers) {
|
|
218
|
+
await session.page.keyboard.down(mod as KeyInput);
|
|
219
|
+
}
|
|
220
|
+
// Press the main key
|
|
221
|
+
await session.page.keyboard.press(action.key as KeyInput);
|
|
222
|
+
// Release modifiers in reverse order
|
|
223
|
+
for (const mod of modifiers.reverse()) {
|
|
224
|
+
await session.page.keyboard.up(mod as KeyInput);
|
|
225
|
+
}
|
|
226
|
+
} else {
|
|
227
|
+
// No modifiers, just press the key
|
|
228
|
+
await session.page.keyboard.press(action.key as KeyInput);
|
|
229
|
+
}
|
|
230
|
+
} catch (error) {
|
|
231
|
+
if (error instanceof Error && isNavigationError(error)) {
|
|
232
|
+
ws.emit.user(userId, 'preview:browser-interacted', { action: action.type, message: 'Action deferred (navigation)', deferred: true });
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
throw error;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
break;
|
|
239
|
+
|
|
240
|
+
case 'mousemove':
|
|
241
|
+
try {
|
|
242
|
+
// Use minimal steps for smoother, faster mouse movement
|
|
243
|
+
await session.page.mouse.move(action.x!, action.y!, {
|
|
244
|
+
steps: action.steps || 1 // Reduced from 5 to 1 for faster response
|
|
245
|
+
});
|
|
246
|
+
// Update cursor position in browser context (fire-and-forget, don't await)
|
|
247
|
+
session.page.evaluate((data) => {
|
|
248
|
+
const { x, y } = data;
|
|
249
|
+
if ((window as any).__cursorInfo) {
|
|
250
|
+
(window as any).__cursorInfo.x = x;
|
|
251
|
+
(window as any).__cursorInfo.y = y;
|
|
252
|
+
(window as any).__cursorInfo.timestamp = Date.now();
|
|
253
|
+
}
|
|
254
|
+
}, { x: action.x!, y: action.y! }).catch(() => { /* Ignore evaluation errors */ });
|
|
255
|
+
} catch (error) {
|
|
256
|
+
if (error instanceof Error && isNavigationError(error)) {
|
|
257
|
+
ws.emit.user(userId, 'preview:browser-interacted', { action: action.type, message: 'Action deferred (navigation)', deferred: true });
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
throw error;
|
|
261
|
+
}
|
|
262
|
+
break;
|
|
263
|
+
|
|
264
|
+
case 'scroll':
|
|
265
|
+
try {
|
|
266
|
+
await session.page.mouse.wheel({ deltaX: action.deltaX || 0, deltaY: action.deltaY || 0 });
|
|
267
|
+
} catch (error) {
|
|
268
|
+
if (error instanceof Error && isNavigationError(error)) {
|
|
269
|
+
ws.emit.user(userId, 'preview:browser-interacted', { action: action.type, message: 'Action deferred (navigation)', deferred: true });
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
throw error;
|
|
273
|
+
}
|
|
274
|
+
break;
|
|
275
|
+
|
|
276
|
+
case 'doubleclick':
|
|
277
|
+
try {
|
|
278
|
+
// Reset mouse state first
|
|
279
|
+
try { await session.page.mouse.up(); } catch { }
|
|
280
|
+
await session.page.mouse.click(action.x!, action.y!, { clickCount: 2 });
|
|
281
|
+
} catch (error) {
|
|
282
|
+
if (error instanceof Error) {
|
|
283
|
+
if (isNavigationError(error)) {
|
|
284
|
+
ws.emit.user(userId, 'preview:browser-interacted', { action: action.type, message: 'Action deferred (navigation)', deferred: true });
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
if (error.message.includes('already pressed') || error.message.includes('not pressed')) break;
|
|
288
|
+
}
|
|
289
|
+
throw error;
|
|
290
|
+
}
|
|
291
|
+
break;
|
|
292
|
+
|
|
293
|
+
case 'rightclick':
|
|
294
|
+
try {
|
|
295
|
+
// Reset mouse state first
|
|
296
|
+
try { await session.page.mouse.up(); } catch { }
|
|
297
|
+
|
|
298
|
+
// IMPORTANT: Check for context menu
|
|
299
|
+
// We'll emit context menu event to frontend for custom overlay
|
|
300
|
+
const menuInfo = await previewService.checkForContextMenu(session.id, action.x!, action.y!);
|
|
301
|
+
if (menuInfo) {
|
|
302
|
+
// Context menu detected - event emitted by checkForContextMenu
|
|
303
|
+
debug.log('preview', `✅ Context menu detected at (${action.x}, ${action.y})`);
|
|
304
|
+
// Don't perform regular right-click, frontend will show context menu overlay
|
|
305
|
+
break;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
await session.page.mouse.click(action.x!, action.y!, { button: 'right' });
|
|
309
|
+
} catch (error) {
|
|
310
|
+
if (error instanceof Error) {
|
|
311
|
+
if (isNavigationError(error)) {
|
|
312
|
+
ws.emit.user(userId, 'preview:browser-interacted', { action: action.type, message: 'Action deferred (navigation)', deferred: true });
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
if (error.message.includes('already pressed') || error.message.includes('not pressed')) break;
|
|
316
|
+
}
|
|
317
|
+
throw error;
|
|
318
|
+
}
|
|
319
|
+
break;
|
|
320
|
+
|
|
321
|
+
case 'drag':
|
|
322
|
+
try {
|
|
323
|
+
// Ensure clean state before drag
|
|
324
|
+
try { await session.page.mouse.up(); } catch { }
|
|
325
|
+
|
|
326
|
+
// Move to start, press, drag, release
|
|
327
|
+
await session.page.mouse.move(action.startX!, action.startY!, { steps: 1 });
|
|
328
|
+
await session.page.mouse.down();
|
|
329
|
+
await session.page.mouse.move(action.endX!, action.endY!, { steps: 3 });
|
|
330
|
+
await session.page.mouse.up();
|
|
331
|
+
} catch (error) {
|
|
332
|
+
try { await session.page.mouse.up(); } catch { }
|
|
333
|
+
if (error instanceof Error && isNavigationError(error)) {
|
|
334
|
+
ws.emit.user(userId, 'preview:browser-interacted', { action: action.type, message: 'Drag during navigation', deferred: true });
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
break;
|
|
339
|
+
|
|
340
|
+
case 'dragmove':
|
|
341
|
+
try {
|
|
342
|
+
await session.page.mouse.move(action.currentX!, action.currentY!, { steps: 1 });
|
|
343
|
+
} catch { }
|
|
344
|
+
break;
|
|
345
|
+
|
|
346
|
+
case 'keynav':
|
|
347
|
+
if (action.key) {
|
|
348
|
+
const modifiers: string[] = [];
|
|
349
|
+
if (action.ctrlKey) modifiers.push('Control');
|
|
350
|
+
if (action.metaKey) modifiers.push('Meta');
|
|
351
|
+
if (action.altKey) modifiers.push('Alt');
|
|
352
|
+
if (action.shiftKey) modifiers.push('Shift');
|
|
353
|
+
|
|
354
|
+
if (modifiers.length > 0) {
|
|
355
|
+
for (const mod of modifiers) {
|
|
356
|
+
await session.page.keyboard.down(mod as KeyInput);
|
|
357
|
+
}
|
|
358
|
+
await session.page.keyboard.press(action.key as KeyInput);
|
|
359
|
+
for (const mod of modifiers.reverse()) {
|
|
360
|
+
await session.page.keyboard.up(mod as KeyInput);
|
|
361
|
+
}
|
|
362
|
+
} else {
|
|
363
|
+
await session.page.keyboard.press(action.key as KeyInput);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
if (['ArrowDown', 'ArrowUp'].includes(action.key)) {
|
|
367
|
+
try {
|
|
368
|
+
await sleep(50);
|
|
369
|
+
} catch { }
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
break;
|
|
373
|
+
|
|
374
|
+
case 'checkselectoptions':
|
|
375
|
+
if (action.x && action.y) {
|
|
376
|
+
try {
|
|
377
|
+
await session.page.evaluate((coordinates) => {
|
|
378
|
+
const { x, y } = coordinates;
|
|
379
|
+
const element = document.elementFromPoint(x, y);
|
|
380
|
+
|
|
381
|
+
if (!element) return null;
|
|
382
|
+
|
|
383
|
+
const isSelect = element.tagName === 'SELECT';
|
|
384
|
+
const isOption = element.tagName === 'OPTION' || element.getAttribute('role') === 'option';
|
|
385
|
+
const isDropdown = element.classList.contains('dropdown') ||
|
|
386
|
+
element.classList.contains('select') ||
|
|
387
|
+
element.getAttribute('role') === 'listbox';
|
|
388
|
+
|
|
389
|
+
let options: Array<{ index: number, value: string, text: string, selected: boolean }> = [];
|
|
390
|
+
if (isSelect || isDropdown || isOption) {
|
|
391
|
+
const selectElement = isSelect ? element :
|
|
392
|
+
element.closest('select') ||
|
|
393
|
+
element.closest('[role="listbox"]') ||
|
|
394
|
+
element.closest('.dropdown');
|
|
395
|
+
|
|
396
|
+
if (selectElement) {
|
|
397
|
+
if (selectElement.tagName === 'SELECT') {
|
|
398
|
+
options = Array.from(selectElement.querySelectorAll('option')).map((opt, index) => ({
|
|
399
|
+
index,
|
|
400
|
+
value: opt.value || '',
|
|
401
|
+
text: opt.textContent || '',
|
|
402
|
+
selected: opt.selected
|
|
403
|
+
}));
|
|
404
|
+
} else {
|
|
405
|
+
options = Array.from(selectElement.querySelectorAll('[role="option"], .option, .dropdown-item')).map((opt, index) => ({
|
|
406
|
+
index,
|
|
407
|
+
value: opt.getAttribute('data-value') || opt.textContent || '',
|
|
408
|
+
text: opt.textContent || '',
|
|
409
|
+
selected: opt.getAttribute('aria-selected') === 'true' || opt.classList.contains('selected')
|
|
410
|
+
}));
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
return {
|
|
416
|
+
elementType: element.tagName,
|
|
417
|
+
isSelect,
|
|
418
|
+
isOption,
|
|
419
|
+
isDropdown,
|
|
420
|
+
hasOptions: options.length > 0,
|
|
421
|
+
options: options.slice(0, 10),
|
|
422
|
+
elementId: element.id,
|
|
423
|
+
elementClass: element.className,
|
|
424
|
+
elementText: element.textContent?.slice(0, 100)
|
|
425
|
+
};
|
|
426
|
+
}, { x: action.x, y: action.y });
|
|
427
|
+
} catch (error) {
|
|
428
|
+
debug.error('preview', 'Error checking for select options:', error);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
break;
|
|
432
|
+
|
|
433
|
+
case 'scale-update':
|
|
434
|
+
// Handle scale update from frontend (hot-swap, no reconnection)
|
|
435
|
+
if (action.scale && action.scale > 0 && action.scale <= 1) {
|
|
436
|
+
session.scale = action.scale;
|
|
437
|
+
debug.log('preview', `📐 Scale updated for tab ${tabId}: ${action.scale}`);
|
|
438
|
+
|
|
439
|
+
// Hot-swap resolution without reconnection
|
|
440
|
+
const updated = await previewService.updateWebCodecsScale(session.id, action.scale);
|
|
441
|
+
if (!updated) {
|
|
442
|
+
debug.warn('preview', `⚠️ Hot-swap failed, falling back to restart`);
|
|
443
|
+
// Fallback: restart if hot-swap fails
|
|
444
|
+
await previewService.stopWebCodecsStreaming(session.id);
|
|
445
|
+
await previewService.startWebCodecsStreaming(session.id);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
break;
|
|
449
|
+
|
|
450
|
+
case 'viewport-update':
|
|
451
|
+
// Handle viewport change (device/rotation) without reconnection
|
|
452
|
+
if (action.width && action.height && action.scale && action.scale > 0 && action.scale <= 1) {
|
|
453
|
+
session.scale = action.scale;
|
|
454
|
+
if (action.deviceSize) session.deviceSize = action.deviceSize as any;
|
|
455
|
+
if (action.rotation) session.rotation = action.rotation as any;
|
|
456
|
+
|
|
457
|
+
debug.log('preview', `📱 Viewport updated for tab ${tabId}: ${action.width}x${action.height} (scale: ${action.scale})`);
|
|
458
|
+
|
|
459
|
+
// Hot-swap viewport without reconnection
|
|
460
|
+
const updated = await previewService.updateWebCodecsViewport(session.id, action.width, action.height, action.scale);
|
|
461
|
+
if (!updated) {
|
|
462
|
+
debug.warn('preview', `⚠️ Viewport hot-swap failed`);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
break;
|
|
466
|
+
|
|
467
|
+
default:
|
|
468
|
+
ws.emit.user(userId, 'preview:browser-error', {
|
|
469
|
+
message: `Unknown action type: ${action.type}`
|
|
470
|
+
});
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
} catch (interactionError) {
|
|
474
|
+
if (interactionError instanceof Error) {
|
|
475
|
+
const errorMsg = interactionError.message.toLowerCase();
|
|
476
|
+
|
|
477
|
+
if (errorMsg.includes('execution context was destroyed') ||
|
|
478
|
+
errorMsg.includes('target closed') ||
|
|
479
|
+
errorMsg.includes('page is loading') ||
|
|
480
|
+
errorMsg.includes('waiting for navigation')) {
|
|
481
|
+
|
|
482
|
+
ws.emit.user(userId, 'preview:browser-interacted', {
|
|
483
|
+
action: action.type,
|
|
484
|
+
message: `Action ${action.type} deferred due to page loading`,
|
|
485
|
+
deferred: true
|
|
486
|
+
});
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
throw interactionError;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
ws.emit.user(userId, 'preview:browser-interacted', {
|
|
494
|
+
action: action.type,
|
|
495
|
+
message: `Action ${action.type} executed successfully`
|
|
496
|
+
});
|
|
497
|
+
} catch (error) {
|
|
498
|
+
debug.error('preview', 'Error executing browser interaction:', error);
|
|
499
|
+
ws.emit.user(userId, 'preview:browser-error', {
|
|
500
|
+
message: error instanceof Error ? error.message : 'Unknown error'
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
})
|
|
504
|
+
|
|
505
|
+
// Server → Client Events (independent declarations)
|
|
506
|
+
.emit('preview:browser-interacted', t.Object({
|
|
507
|
+
action: t.String(),
|
|
508
|
+
message: t.String(),
|
|
509
|
+
deferred: t.Optional(t.Boolean())
|
|
510
|
+
}))
|
|
511
|
+
.emit('preview:browser-error', t.Object({
|
|
512
|
+
message: t.String()
|
|
513
|
+
}));
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP Tab Handlers
|
|
3
|
+
*
|
|
4
|
+
* HTTP endpoints for MCP tab coordination between backend and frontend.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { createRouter } from '$shared/utils/ws-server';
|
|
8
|
+
import { t } from 'elysia';
|
|
9
|
+
import { browserMcpControl } from '$backend/lib/preview';
|
|
10
|
+
import { debug } from '$shared/utils/logger';
|
|
11
|
+
|
|
12
|
+
// Tab response types
|
|
13
|
+
const TabSchema = t.Object({
|
|
14
|
+
id: t.String(),
|
|
15
|
+
url: t.String(),
|
|
16
|
+
title: t.String(),
|
|
17
|
+
sessionId: t.Union([t.String(), t.Null()]),
|
|
18
|
+
isActive: t.Boolean()
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
export const mcpPreviewHandler = createRouter()
|
|
22
|
+
// Tabs list
|
|
23
|
+
.http('preview:mcp-tab-list', {
|
|
24
|
+
data: t.Object({
|
|
25
|
+
requestId: t.String(),
|
|
26
|
+
tabs: t.Array(TabSchema)
|
|
27
|
+
}),
|
|
28
|
+
response: t.Object({
|
|
29
|
+
success: t.Boolean()
|
|
30
|
+
})
|
|
31
|
+
}, async ({ data }) => {
|
|
32
|
+
debug.log('mcp', `📋 Received tabs list for request: ${data.requestId}`);
|
|
33
|
+
|
|
34
|
+
const resolved = browserMcpControl.resolveTabRequest(data.requestId, {
|
|
35
|
+
tabs: data.tabs
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
return { success: resolved };
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
// Active tab
|
|
42
|
+
.http('preview:mcp-active-tab', {
|
|
43
|
+
data: t.Object({
|
|
44
|
+
requestId: t.String(),
|
|
45
|
+
tab: t.Union([TabSchema, t.Null()])
|
|
46
|
+
}),
|
|
47
|
+
response: t.Object({
|
|
48
|
+
success: t.Boolean()
|
|
49
|
+
})
|
|
50
|
+
}, async ({ data }) => {
|
|
51
|
+
debug.log('mcp', `📋 Received active tab for request: ${data.requestId}`);
|
|
52
|
+
|
|
53
|
+
const resolved = browserMcpControl.resolveTabRequest(data.requestId, {
|
|
54
|
+
tab: data.tab
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
return { success: resolved };
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
// Switch tab
|
|
61
|
+
.http('preview:mcp-switch-tab', {
|
|
62
|
+
data: t.Object({
|
|
63
|
+
requestId: t.String(),
|
|
64
|
+
success: t.Boolean(),
|
|
65
|
+
tab: t.Optional(TabSchema),
|
|
66
|
+
error: t.Optional(t.String())
|
|
67
|
+
}),
|
|
68
|
+
response: t.Object({
|
|
69
|
+
success: t.Boolean()
|
|
70
|
+
})
|
|
71
|
+
}, async ({ data }) => {
|
|
72
|
+
debug.log('mcp', `📋 Received switch tab for request: ${data.requestId}`);
|
|
73
|
+
|
|
74
|
+
const resolved = browserMcpControl.resolveTabRequest(data.requestId, {
|
|
75
|
+
success: data.success,
|
|
76
|
+
tab: data.tab,
|
|
77
|
+
error: data.error
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
return { success: resolved };
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
// Open tab
|
|
84
|
+
.http('preview:mcp-open-tab', {
|
|
85
|
+
data: t.Object({
|
|
86
|
+
requestId: t.String(),
|
|
87
|
+
success: t.Boolean(),
|
|
88
|
+
tab: t.Optional(TabSchema),
|
|
89
|
+
error: t.Optional(t.String())
|
|
90
|
+
}),
|
|
91
|
+
response: t.Object({
|
|
92
|
+
success: t.Boolean()
|
|
93
|
+
})
|
|
94
|
+
}, async ({ data }) => {
|
|
95
|
+
debug.log('mcp', `📋 Received open tab for request: ${data.requestId}`);
|
|
96
|
+
|
|
97
|
+
const resolved = browserMcpControl.resolveTabRequest(data.requestId, {
|
|
98
|
+
success: data.success,
|
|
99
|
+
tab: data.tab,
|
|
100
|
+
error: data.error
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
return { success: resolved };
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
// Close tab
|
|
107
|
+
.http('preview:mcp-close-tab', {
|
|
108
|
+
data: t.Object({
|
|
109
|
+
requestId: t.String(),
|
|
110
|
+
success: t.Boolean(),
|
|
111
|
+
closedTabId: t.Optional(t.String()),
|
|
112
|
+
newActiveTab: t.Optional(TabSchema),
|
|
113
|
+
error: t.Optional(t.String())
|
|
114
|
+
}),
|
|
115
|
+
response: t.Object({
|
|
116
|
+
success: t.Boolean()
|
|
117
|
+
})
|
|
118
|
+
}, async ({ data }) => {
|
|
119
|
+
debug.log('mcp', `📋 Received close tab for request: ${data.requestId}`);
|
|
120
|
+
|
|
121
|
+
const resolved = browserMcpControl.resolveTabRequest(data.requestId, {
|
|
122
|
+
success: data.success,
|
|
123
|
+
closedTabId: data.closedTabId,
|
|
124
|
+
newActiveTab: data.newActiveTab,
|
|
125
|
+
error: data.error
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
return { success: resolved };
|
|
129
|
+
});
|