@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,935 @@
|
|
|
1
|
+
import type { Browser, BrowserContext, Page } from 'puppeteer';
|
|
2
|
+
import { EventEmitter } from 'events';
|
|
3
|
+
import { getViewportDimensions } from '$frontend/lib/constants/preview.js';
|
|
4
|
+
import type { BrowserTab, BrowserTabInfo, DeviceSize, Rotation } from './types';
|
|
5
|
+
import { DEFAULT_STREAMING_CONFIG } from './types';
|
|
6
|
+
import { browserPool } from './browser-pool';
|
|
7
|
+
import { BrowserAudioCapture } from './browser-audio-capture';
|
|
8
|
+
import { cursorTrackingScript } from './scripts/cursor-tracking';
|
|
9
|
+
import { browserMcpControl } from './browser-mcp-control';
|
|
10
|
+
import { debug } from '$shared/utils/logger';
|
|
11
|
+
|
|
12
|
+
// Tab cleanup configuration
|
|
13
|
+
const INACTIVE_TIMEOUT = 5 * 60 * 1000; // 5 minutes
|
|
14
|
+
const CLEANUP_INTERVAL = 60 * 1000; // Check every minute
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Browser Tab Manager
|
|
18
|
+
*
|
|
19
|
+
* Tab-centric architecture where each tab represents a complete browser instance.
|
|
20
|
+
* Manages tab lifecycle, creation, navigation, and cleanup.
|
|
21
|
+
*
|
|
22
|
+
* ARCHITECTURE:
|
|
23
|
+
* - Tabs are the primary unit (no separate "session" concept)
|
|
24
|
+
* - Each tab has its own isolated browser context + page from the pool
|
|
25
|
+
* - 1 shared browser + isolated contexts = ~20 MB per tab
|
|
26
|
+
* - Active tab tracking for operations
|
|
27
|
+
* - Event-driven for frontend sync
|
|
28
|
+
* - **PROJECT ISOLATION**: Sessions are prefixed with projectId
|
|
29
|
+
*
|
|
30
|
+
* ISOLATION GUARANTEE:
|
|
31
|
+
* Each tab gets its own BrowserContext which provides:
|
|
32
|
+
* - Separate cookies
|
|
33
|
+
* - Separate localStorage/sessionStorage
|
|
34
|
+
* - Separate cache
|
|
35
|
+
* - Separate service workers
|
|
36
|
+
* - No data leakage between tabs
|
|
37
|
+
* - No data leakage between projects (via projectId-prefixed sessionIds)
|
|
38
|
+
*/
|
|
39
|
+
export class BrowserTabManager extends EventEmitter {
|
|
40
|
+
private tabs = new Map<string, BrowserTab>();
|
|
41
|
+
private activeTabId: string | null = null;
|
|
42
|
+
private nextTabNumber = 1;
|
|
43
|
+
|
|
44
|
+
// Tab activity tracking for cleanup
|
|
45
|
+
private tabActivity = new Map<string, number>();
|
|
46
|
+
private cleanupInterval: NodeJS.Timeout | null = null;
|
|
47
|
+
|
|
48
|
+
// Audio capture manager
|
|
49
|
+
private audioCapture = new BrowserAudioCapture();
|
|
50
|
+
|
|
51
|
+
// Project ID for session isolation (REQUIRED)
|
|
52
|
+
private projectId: string;
|
|
53
|
+
|
|
54
|
+
constructor(projectId: string) {
|
|
55
|
+
super();
|
|
56
|
+
|
|
57
|
+
if (!projectId) {
|
|
58
|
+
throw new Error('projectId is required for BrowserTabManager');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
this.projectId = projectId;
|
|
62
|
+
// Initialize periodic cleanup
|
|
63
|
+
this.initializeCleanup();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Create a new tab with optional URL
|
|
68
|
+
*
|
|
69
|
+
* If URL is provided, navigate to it immediately.
|
|
70
|
+
* If URL is not provided, create blank tab (about:blank).
|
|
71
|
+
*
|
|
72
|
+
* Default rotation depends on device size:
|
|
73
|
+
* - Desktop/laptop: landscape
|
|
74
|
+
* - Tablet/mobile: portrait
|
|
75
|
+
*/
|
|
76
|
+
async createTab(
|
|
77
|
+
url?: string,
|
|
78
|
+
deviceSize: DeviceSize = 'laptop',
|
|
79
|
+
rotation: Rotation = 'landscape',
|
|
80
|
+
options?: {
|
|
81
|
+
setActive?: boolean;
|
|
82
|
+
preNavigationSetup?: (page: Page) => Promise<void>;
|
|
83
|
+
}
|
|
84
|
+
): Promise<BrowserTab> {
|
|
85
|
+
const tabId = `tab-${this.nextTabNumber++}`;
|
|
86
|
+
const finalUrl = url || 'about:blank';
|
|
87
|
+
|
|
88
|
+
debug.log('preview', `🟡🟡🟡 Creating new tab: ${tabId} for project: ${this.projectId} 🟡🟡🟡`);
|
|
89
|
+
debug.log('preview', `📁 Tab URL: ${finalUrl}, deviceSize: ${deviceSize}, rotation: ${rotation}`);
|
|
90
|
+
|
|
91
|
+
let browser: Browser;
|
|
92
|
+
let context: BrowserContext;
|
|
93
|
+
let page: Page;
|
|
94
|
+
|
|
95
|
+
try {
|
|
96
|
+
// Create project-scoped sessionId for isolation
|
|
97
|
+
// Format: "projectId:tabId" ensures complete isolation between projects
|
|
98
|
+
const sessionId = `${this.projectId}:${tabId}`;
|
|
99
|
+
|
|
100
|
+
// Create isolated context via puppeteer-cluster
|
|
101
|
+
// This provides full isolation: cookies, localStorage, sessionStorage, cache
|
|
102
|
+
const pooledSession = await browserPool.createSession(sessionId);
|
|
103
|
+
browser = await browserPool.getBrowser();
|
|
104
|
+
context = pooledSession.context;
|
|
105
|
+
page = pooledSession.page;
|
|
106
|
+
|
|
107
|
+
debug.log('preview', `🔐 Session ID: ${sessionId} (project-scoped)`);
|
|
108
|
+
} catch (poolError) {
|
|
109
|
+
debug.error('preview', `❌ Browser pool error:`, poolError);
|
|
110
|
+
throw poolError;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
debug.log('preview', `✅ Isolated context created for tab: ${tabId}`);
|
|
114
|
+
|
|
115
|
+
// Setup page (viewport, headers, etc.)
|
|
116
|
+
debug.log('preview', `⚙️ Setting up page...`);
|
|
117
|
+
await this.setupPage(page, deviceSize, rotation);
|
|
118
|
+
debug.log('preview', `✅ Page setup complete`);
|
|
119
|
+
|
|
120
|
+
// Run pre-navigation setup if provided (e.g., dialog handling)
|
|
121
|
+
if (options?.preNavigationSetup) {
|
|
122
|
+
debug.log('preview', `🔧 Running pre-navigation setup...`);
|
|
123
|
+
await options.preNavigationSetup(page);
|
|
124
|
+
debug.log('preview', `✅ Pre-navigation setup complete`);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Navigate to URL (or about:blank)
|
|
128
|
+
debug.log('preview', `🌐 Navigating to: ${finalUrl}`);
|
|
129
|
+
const actualUrl = await this.navigateWithRetry(page, finalUrl);
|
|
130
|
+
debug.log('preview', `✅ Navigation complete - final URL: ${actualUrl}`);
|
|
131
|
+
|
|
132
|
+
// Get title from URL
|
|
133
|
+
const title = this.getTitleFromUrl(actualUrl);
|
|
134
|
+
|
|
135
|
+
// Create tab object
|
|
136
|
+
const tab: BrowserTab = {
|
|
137
|
+
// Identity
|
|
138
|
+
id: tabId,
|
|
139
|
+
url: actualUrl,
|
|
140
|
+
title,
|
|
141
|
+
isActive: false,
|
|
142
|
+
|
|
143
|
+
// Browser instances
|
|
144
|
+
browser,
|
|
145
|
+
context,
|
|
146
|
+
page,
|
|
147
|
+
|
|
148
|
+
// Streaming
|
|
149
|
+
isStreaming: false,
|
|
150
|
+
quality: 'good',
|
|
151
|
+
|
|
152
|
+
// Device
|
|
153
|
+
deviceSize,
|
|
154
|
+
rotation,
|
|
155
|
+
|
|
156
|
+
// Console
|
|
157
|
+
consoleLogs: [],
|
|
158
|
+
consoleEnabled: true,
|
|
159
|
+
|
|
160
|
+
// Navigation
|
|
161
|
+
isLoading: false,
|
|
162
|
+
canGoBack: false,
|
|
163
|
+
canGoForward: false,
|
|
164
|
+
currentUrl: actualUrl,
|
|
165
|
+
|
|
166
|
+
// Timestamps
|
|
167
|
+
createdAt: Date.now(),
|
|
168
|
+
lastAccessedAt: Date.now(),
|
|
169
|
+
|
|
170
|
+
// Internal
|
|
171
|
+
isDestroyed: false,
|
|
172
|
+
lastFrameHash: undefined,
|
|
173
|
+
duplicateFrameCount: 0,
|
|
174
|
+
lastInteractionTime: undefined,
|
|
175
|
+
lastNavigationTime: undefined
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
this.tabs.set(tabId, tab);
|
|
179
|
+
this.setupBrowserHandlers(tabId, browser, context, page);
|
|
180
|
+
|
|
181
|
+
// Mark tab as active immediately
|
|
182
|
+
this.markTabActivity(tabId);
|
|
183
|
+
|
|
184
|
+
// Set as active if requested or if it's the first tab
|
|
185
|
+
if (options?.setActive !== false) {
|
|
186
|
+
this.setActiveTab(tabId);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Emit tab created event with device info
|
|
190
|
+
const tabOpenedEvent = {
|
|
191
|
+
tabId,
|
|
192
|
+
url: actualUrl,
|
|
193
|
+
title,
|
|
194
|
+
isActive: tab.isActive,
|
|
195
|
+
deviceSize: tab.deviceSize,
|
|
196
|
+
rotation: tab.rotation,
|
|
197
|
+
timestamp: Date.now()
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
debug.log('preview', `📤 Emitting preview:browser-tab-opened event:`, tabOpenedEvent);
|
|
201
|
+
this.emit('preview:browser-tab-opened', tabOpenedEvent);
|
|
202
|
+
|
|
203
|
+
debug.log('preview', `✅ Tab created: ${tabId} (active: ${tab.isActive})`);
|
|
204
|
+
|
|
205
|
+
// Log pool stats
|
|
206
|
+
const stats = browserPool.getStats();
|
|
207
|
+
debug.log('preview', `📊 Pool stats: ${stats.activeSessions}/${stats.maxConcurrency} tabs active`);
|
|
208
|
+
|
|
209
|
+
return tab;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Navigate tab to a new URL
|
|
214
|
+
*/
|
|
215
|
+
async navigateTab(tabId: string, url: string): Promise<string> {
|
|
216
|
+
const tab = this.tabs.get(tabId);
|
|
217
|
+
if (!tab) {
|
|
218
|
+
throw new Error(`Tab not found: ${tabId}`);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
debug.log('preview', `🌐 Navigating tab ${tabId} to: ${url}`);
|
|
222
|
+
|
|
223
|
+
// Mark as loading
|
|
224
|
+
tab.isLoading = true;
|
|
225
|
+
|
|
226
|
+
try {
|
|
227
|
+
// Navigate (streaming continues, handlers reused)
|
|
228
|
+
const actualUrl = await this.navigateWithRetry(tab.page, url);
|
|
229
|
+
|
|
230
|
+
// Update tab properties
|
|
231
|
+
tab.url = actualUrl;
|
|
232
|
+
tab.currentUrl = actualUrl;
|
|
233
|
+
tab.title = this.getTitleFromUrl(actualUrl);
|
|
234
|
+
tab.lastNavigationTime = Date.now();
|
|
235
|
+
tab.isLoading = false;
|
|
236
|
+
|
|
237
|
+
// Update navigation state
|
|
238
|
+
tab.canGoBack = (await tab.page.evaluate(() => window.history.length)) > 1;
|
|
239
|
+
tab.canGoForward = false;
|
|
240
|
+
|
|
241
|
+
// Mark activity
|
|
242
|
+
this.markTabActivity(tabId);
|
|
243
|
+
|
|
244
|
+
// Emit navigation event
|
|
245
|
+
this.emit('preview:browser-tab-navigated', {
|
|
246
|
+
tabId,
|
|
247
|
+
url: actualUrl,
|
|
248
|
+
title: tab.title,
|
|
249
|
+
timestamp: Date.now()
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
debug.log('preview', `✅ Tab ${tabId} navigated to: ${actualUrl}`);
|
|
253
|
+
|
|
254
|
+
return actualUrl;
|
|
255
|
+
} catch (error) {
|
|
256
|
+
tab.isLoading = false;
|
|
257
|
+
throw error;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Close a tab and cleanup its resources
|
|
263
|
+
*/
|
|
264
|
+
async closeTab(tabId: string): Promise<{ success: boolean; newActiveTabId: string | null }> {
|
|
265
|
+
const tab = this.tabs.get(tabId);
|
|
266
|
+
if (!tab) {
|
|
267
|
+
debug.warn('preview', `❌ Tab not found: ${tabId}`);
|
|
268
|
+
return { success: false, newActiveTabId: null };
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
debug.log('preview', `🗑️ Closing tab: ${tabId}`);
|
|
272
|
+
|
|
273
|
+
const wasActive = tab.isActive;
|
|
274
|
+
|
|
275
|
+
// Auto-release MCP control if this tab is being controlled
|
|
276
|
+
browserMcpControl.autoReleaseForTab(tabId);
|
|
277
|
+
|
|
278
|
+
// IMMEDIATELY set destroyed flag and stop streaming
|
|
279
|
+
tab.isDestroyed = true;
|
|
280
|
+
tab.isStreaming = false;
|
|
281
|
+
|
|
282
|
+
// Clear all intervals immediately
|
|
283
|
+
if (tab.screenshotInterval) {
|
|
284
|
+
clearInterval(tab.screenshotInterval);
|
|
285
|
+
tab.screenshotInterval = undefined;
|
|
286
|
+
}
|
|
287
|
+
if (tab.streamingInterval) {
|
|
288
|
+
clearInterval(tab.streamingInterval);
|
|
289
|
+
tab.streamingInterval = undefined;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Wait a moment for streaming loop to detect the flags and stop
|
|
293
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
294
|
+
|
|
295
|
+
// Clean up the isolated context
|
|
296
|
+
await this.cleanupContext(tab);
|
|
297
|
+
|
|
298
|
+
// Remove from map
|
|
299
|
+
this.tabs.delete(tabId);
|
|
300
|
+
this.tabActivity.delete(tabId);
|
|
301
|
+
|
|
302
|
+
// If closing active tab, switch to another tab
|
|
303
|
+
let newActiveTabId: string | null = null;
|
|
304
|
+
if (wasActive && this.tabs.size > 0) {
|
|
305
|
+
// Get the first available tab
|
|
306
|
+
const nextTab = Array.from(this.tabs.values())[0];
|
|
307
|
+
if (nextTab) {
|
|
308
|
+
this.setActiveTab(nextTab.id);
|
|
309
|
+
newActiveTabId = nextTab.id;
|
|
310
|
+
} else {
|
|
311
|
+
this.activeTabId = null;
|
|
312
|
+
}
|
|
313
|
+
} else if (this.tabs.size === 0) {
|
|
314
|
+
this.activeTabId = null;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Emit tab closed event
|
|
318
|
+
this.emit('preview:browser-tab-closed', {
|
|
319
|
+
tabId,
|
|
320
|
+
newActiveTabId,
|
|
321
|
+
timestamp: Date.now()
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
debug.log('preview', `✅ Tab closed: ${tabId} (new active: ${newActiveTabId || 'none'})`);
|
|
325
|
+
|
|
326
|
+
// Log pool stats after cleanup
|
|
327
|
+
const stats = browserPool.getStats();
|
|
328
|
+
debug.log('preview', `📊 Pool stats after cleanup: ${stats.activeSessions}/${stats.maxConcurrency} tabs active`);
|
|
329
|
+
|
|
330
|
+
return { success: true, newActiveTabId };
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Switch to a specific tab
|
|
335
|
+
*/
|
|
336
|
+
setActiveTab(tabId: string): boolean {
|
|
337
|
+
const tab = this.tabs.get(tabId);
|
|
338
|
+
if (!tab) {
|
|
339
|
+
debug.warn('preview', `❌ Cannot switch to tab: ${tabId} (not found)`);
|
|
340
|
+
return false;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const previousTabId = this.activeTabId;
|
|
344
|
+
|
|
345
|
+
// Deactivate previous active tab
|
|
346
|
+
if (previousTabId && previousTabId !== tabId) {
|
|
347
|
+
const previousTab = this.tabs.get(previousTabId);
|
|
348
|
+
if (previousTab) {
|
|
349
|
+
previousTab.isActive = false;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// Activate new tab
|
|
354
|
+
tab.isActive = true;
|
|
355
|
+
tab.lastAccessedAt = Date.now();
|
|
356
|
+
this.activeTabId = tabId;
|
|
357
|
+
|
|
358
|
+
// Mark tab activity
|
|
359
|
+
this.markTabActivity(tabId);
|
|
360
|
+
|
|
361
|
+
// Emit tab switched event
|
|
362
|
+
if (previousTabId !== tabId) {
|
|
363
|
+
this.emit('preview:browser-tab-switched', {
|
|
364
|
+
previousTabId: previousTabId || '',
|
|
365
|
+
newTabId: tabId,
|
|
366
|
+
timestamp: Date.now()
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
debug.log('preview', `🔄 Switched tab: ${previousTabId || 'none'} → ${tabId}`);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
return true;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Get a tab by ID
|
|
377
|
+
*/
|
|
378
|
+
getTab(tabId: string): BrowserTab | null {
|
|
379
|
+
const tab = this.tabs.get(tabId);
|
|
380
|
+
if (!tab) {
|
|
381
|
+
return null;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// Validate tab before returning
|
|
385
|
+
if (!this.isValidTab(tabId)) {
|
|
386
|
+
return null;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
return tab;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Get the active tab
|
|
394
|
+
*/
|
|
395
|
+
getActiveTab(): BrowserTab | null {
|
|
396
|
+
if (!this.activeTabId) return null;
|
|
397
|
+
return this.getTab(this.activeTabId);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Change viewport settings (device size and rotation) for an existing tab
|
|
402
|
+
*/
|
|
403
|
+
async setViewport(tabId: string, deviceSize: DeviceSize, rotation: Rotation): Promise<boolean> {
|
|
404
|
+
const tab = this.tabs.get(tabId);
|
|
405
|
+
if (!tab) {
|
|
406
|
+
debug.warn('preview', `❌ Cannot set viewport: Tab ${tabId} not found`);
|
|
407
|
+
return false;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// Get new viewport dimensions
|
|
411
|
+
const { width: viewportWidth, height: viewportHeight } = getViewportDimensions(deviceSize, rotation);
|
|
412
|
+
|
|
413
|
+
try {
|
|
414
|
+
// Update viewport on the page
|
|
415
|
+
await tab.page.setViewport({ width: viewportWidth, height: viewportHeight });
|
|
416
|
+
|
|
417
|
+
// Update tab metadata
|
|
418
|
+
tab.deviceSize = deviceSize;
|
|
419
|
+
tab.rotation = rotation;
|
|
420
|
+
|
|
421
|
+
// Mark tab activity
|
|
422
|
+
this.markTabActivity(tabId);
|
|
423
|
+
|
|
424
|
+
// Emit viewport changed event
|
|
425
|
+
this.emit('preview:browser-viewport-changed', {
|
|
426
|
+
tabId,
|
|
427
|
+
deviceSize,
|
|
428
|
+
rotation,
|
|
429
|
+
width: viewportWidth,
|
|
430
|
+
height: viewportHeight,
|
|
431
|
+
timestamp: Date.now()
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
debug.log('preview', `📱 Viewport changed for tab ${tabId}: ${deviceSize} (${rotation}) - ${viewportWidth}x${viewportHeight}`);
|
|
435
|
+
|
|
436
|
+
return true;
|
|
437
|
+
} catch (error) {
|
|
438
|
+
debug.error('preview', `❌ Failed to set viewport for tab ${tabId}:`, error);
|
|
439
|
+
return false;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* Get all tabs
|
|
445
|
+
*/
|
|
446
|
+
getAllTabs(): BrowserTab[] {
|
|
447
|
+
return Array.from(this.tabs.values());
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* Get tab count
|
|
452
|
+
*/
|
|
453
|
+
getTabCount(): number {
|
|
454
|
+
return this.tabs.size;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* Check if a tab exists
|
|
459
|
+
*/
|
|
460
|
+
hasTab(tabId: string): boolean {
|
|
461
|
+
return this.tabs.has(tabId);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* Get active tab ID
|
|
466
|
+
*/
|
|
467
|
+
getActiveTabId(): string | null {
|
|
468
|
+
return this.activeTabId;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/**
|
|
472
|
+
* Get tab info
|
|
473
|
+
*/
|
|
474
|
+
getTabInfo(tabId: string): BrowserTabInfo | null {
|
|
475
|
+
const tab = this.getTab(tabId);
|
|
476
|
+
if (!tab) return null;
|
|
477
|
+
|
|
478
|
+
return {
|
|
479
|
+
id: tab.id,
|
|
480
|
+
url: tab.url,
|
|
481
|
+
title: tab.title,
|
|
482
|
+
quality: tab.quality,
|
|
483
|
+
isStreaming: tab.isStreaming,
|
|
484
|
+
deviceSize: tab.deviceSize,
|
|
485
|
+
rotation: tab.rotation,
|
|
486
|
+
isActive: tab.isActive
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/**
|
|
491
|
+
* Get all tabs info
|
|
492
|
+
*/
|
|
493
|
+
getAllTabsInfo(): BrowserTabInfo[] {
|
|
494
|
+
return Array.from(this.tabs.values()).map(tab => ({
|
|
495
|
+
id: tab.id,
|
|
496
|
+
url: tab.url,
|
|
497
|
+
title: tab.title,
|
|
498
|
+
quality: tab.quality,
|
|
499
|
+
isStreaming: tab.isStreaming,
|
|
500
|
+
deviceSize: tab.deviceSize,
|
|
501
|
+
rotation: tab.rotation,
|
|
502
|
+
isActive: tab.isActive
|
|
503
|
+
}));
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
/**
|
|
507
|
+
* Get tabs status (for admin/debugging)
|
|
508
|
+
*/
|
|
509
|
+
getTabsStatus() {
|
|
510
|
+
const tabs = Array.from(this.tabs.entries()).map(([id, tab]) => ({
|
|
511
|
+
id,
|
|
512
|
+
url: tab.url,
|
|
513
|
+
title: tab.title,
|
|
514
|
+
isStreaming: tab.isStreaming,
|
|
515
|
+
isDestroyed: tab.isDestroyed || false,
|
|
516
|
+
browserConnected: tab.browser?.connected || false,
|
|
517
|
+
pageClosed: tab.page?.isClosed() || true,
|
|
518
|
+
deviceSize: tab.deviceSize,
|
|
519
|
+
rotation: tab.rotation,
|
|
520
|
+
consoleLogs: tab.consoleLogs.length,
|
|
521
|
+
lastInteractionTime: tab.lastInteractionTime,
|
|
522
|
+
duplicateFrameCount: tab.duplicateFrameCount || 0,
|
|
523
|
+
isActive: tab.isActive,
|
|
524
|
+
createdAt: tab.createdAt,
|
|
525
|
+
lastAccessedAt: tab.lastAccessedAt
|
|
526
|
+
}));
|
|
527
|
+
|
|
528
|
+
return {
|
|
529
|
+
totalTabs: tabs.length,
|
|
530
|
+
activeTabs: tabs.filter(t => t.isStreaming && t.browserConnected && !t.pageClosed && !t.isDestroyed).length,
|
|
531
|
+
inactiveTabs: tabs.filter(t => t.isDestroyed || !t.browserConnected || t.pageClosed || !t.isStreaming).length,
|
|
532
|
+
tabs
|
|
533
|
+
};
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
/**
|
|
537
|
+
* Update tab title
|
|
538
|
+
*/
|
|
539
|
+
updateTabTitle(tabId: string, title: string): void {
|
|
540
|
+
const tab = this.tabs.get(tabId);
|
|
541
|
+
if (tab) {
|
|
542
|
+
tab.title = title;
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
/**
|
|
547
|
+
* Update tab title from URL
|
|
548
|
+
*/
|
|
549
|
+
updateTabTitleFromUrl(tabId: string, url: string): void {
|
|
550
|
+
const tab = this.tabs.get(tabId);
|
|
551
|
+
if (tab) {
|
|
552
|
+
tab.title = this.getTitleFromUrl(url);
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
/**
|
|
557
|
+
* Get project-scoped session ID for a tab
|
|
558
|
+
*/
|
|
559
|
+
private getSessionId(tabId: string): string {
|
|
560
|
+
return this.projectId ? `${this.projectId}:${tabId}` : tabId;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
/**
|
|
564
|
+
* Validate tab
|
|
565
|
+
*/
|
|
566
|
+
private isValidTab(tabId: string): boolean {
|
|
567
|
+
const tab = this.tabs.get(tabId);
|
|
568
|
+
if (!tab) {
|
|
569
|
+
return false;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
// Check if tab is already destroyed
|
|
573
|
+
if (tab.isDestroyed) {
|
|
574
|
+
debug.warn('preview', `⚠️ Tab ${tabId}: already destroyed`);
|
|
575
|
+
return false;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
// Check if browser is still connected (shared browser)
|
|
579
|
+
if (!tab.browser || !tab.browser.connected) {
|
|
580
|
+
debug.warn('preview', `⚠️ Tab ${tabId}: shared browser disconnected`);
|
|
581
|
+
this.closeTab(tabId).catch(console.error);
|
|
582
|
+
return false;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
// Check if session is still valid in the pool (use project-scoped sessionId)
|
|
586
|
+
const sessionId = this.getSessionId(tabId);
|
|
587
|
+
const isPoolValid = browserPool.isSessionValid(sessionId);
|
|
588
|
+
if (!isPoolValid) {
|
|
589
|
+
debug.warn('preview', `⚠️ Tab ${tabId}: session no longer valid in pool`);
|
|
590
|
+
this.closeTab(tabId).catch(console.error);
|
|
591
|
+
return false;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
// Check if page is still open
|
|
595
|
+
if (!tab.page || tab.page.isClosed()) {
|
|
596
|
+
debug.warn('preview', `⚠️ Tab ${tabId}: page closed`);
|
|
597
|
+
this.closeTab(tabId).catch(console.error);
|
|
598
|
+
return false;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
return true;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
/**
|
|
605
|
+
* Mark tab activity (prevent cleanup)
|
|
606
|
+
*/
|
|
607
|
+
markTabActivity(tabId: string): void {
|
|
608
|
+
const now = Date.now();
|
|
609
|
+
this.tabActivity.set(tabId, now);
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
/**
|
|
613
|
+
* Setup page (viewport, headers, injections)
|
|
614
|
+
*/
|
|
615
|
+
private async setupPage(page: Page, deviceSize: DeviceSize, rotation: Rotation) {
|
|
616
|
+
// Get viewport dimensions from config
|
|
617
|
+
const { width: viewportWidth, height: viewportHeight } = getViewportDimensions(deviceSize, rotation);
|
|
618
|
+
|
|
619
|
+
await page.setViewport({ width: viewportWidth, height: viewportHeight });
|
|
620
|
+
|
|
621
|
+
// Set page timeouts - more generous for stability
|
|
622
|
+
page.setDefaultTimeout(30000);
|
|
623
|
+
page.setDefaultNavigationTimeout(30000);
|
|
624
|
+
|
|
625
|
+
// Configure page for stability
|
|
626
|
+
await page.setExtraHTTPHeaders({
|
|
627
|
+
'Accept-Language': 'en-US,en;q=0.9',
|
|
628
|
+
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
|
|
629
|
+
});
|
|
630
|
+
|
|
631
|
+
// Optimize font loading to prevent screenshot timeouts
|
|
632
|
+
await page.evaluateOnNewDocument(() => {
|
|
633
|
+
// Disable font loading wait
|
|
634
|
+
Object.defineProperty(document, 'fonts', {
|
|
635
|
+
value: {
|
|
636
|
+
ready: Promise.resolve(),
|
|
637
|
+
load: () => Promise.resolve([]),
|
|
638
|
+
check: () => true,
|
|
639
|
+
addEventListener: () => {},
|
|
640
|
+
removeEventListener: () => {}
|
|
641
|
+
}
|
|
642
|
+
});
|
|
643
|
+
});
|
|
644
|
+
|
|
645
|
+
// Inject audio capture script BEFORE page loads to intercept AudioContext
|
|
646
|
+
await this.audioCapture.setupAudioCapture(page, DEFAULT_STREAMING_CONFIG.audio);
|
|
647
|
+
|
|
648
|
+
// Simplified cursor tracking for visual feedback only
|
|
649
|
+
await this.injectCursorTracking(page);
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
/**
|
|
653
|
+
* Inject cursor tracking script
|
|
654
|
+
*/
|
|
655
|
+
private async injectCursorTracking(page: Page) {
|
|
656
|
+
await page.evaluateOnNewDocument(cursorTrackingScript);
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
/**
|
|
660
|
+
* Navigate with retry
|
|
661
|
+
*/
|
|
662
|
+
private async navigateWithRetry(page: Page, url: string): Promise<string> {
|
|
663
|
+
let retries = 3;
|
|
664
|
+
let actualUrl = '';
|
|
665
|
+
|
|
666
|
+
while (retries > 0) {
|
|
667
|
+
try {
|
|
668
|
+
await page.goto(url, {
|
|
669
|
+
waitUntil: 'domcontentloaded',
|
|
670
|
+
timeout: 30000
|
|
671
|
+
});
|
|
672
|
+
actualUrl = page.url();
|
|
673
|
+
break;
|
|
674
|
+
} catch (error) {
|
|
675
|
+
retries--;
|
|
676
|
+
debug.warn('preview', `⚠️ Navigation failed, ${retries} retries left:`, error);
|
|
677
|
+
if (retries === 0) throw error;
|
|
678
|
+
|
|
679
|
+
// Wait before retry
|
|
680
|
+
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
return actualUrl;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
/**
|
|
688
|
+
* Setup browser event handlers
|
|
689
|
+
*/
|
|
690
|
+
private setupBrowserHandlers(tabId: string, browser: Browser, context: BrowserContext, page: Page) {
|
|
691
|
+
// Add error handlers for browser disconnection
|
|
692
|
+
// Note: With shared browser, we only clean up THIS tab, not close the browser
|
|
693
|
+
browser.on('disconnected', () => {
|
|
694
|
+
const tab = this.tabs.get(tabId);
|
|
695
|
+
if (tab && !tab.isDestroyed) {
|
|
696
|
+
debug.warn('preview', `⚠️ Shared browser disconnected, cleaning up tab ${tabId}`);
|
|
697
|
+
tab.isDestroyed = true;
|
|
698
|
+
this.closeTab(tabId).catch(console.error);
|
|
699
|
+
}
|
|
700
|
+
});
|
|
701
|
+
|
|
702
|
+
// Handle page errors
|
|
703
|
+
page.on('error', (error) => {
|
|
704
|
+
const tab = this.tabs.get(tabId);
|
|
705
|
+
if (tab && !tab.isDestroyed) {
|
|
706
|
+
debug.error('preview', `💥 Page error for tab ${tabId}: ${error.message}, cleaning up`);
|
|
707
|
+
tab.isDestroyed = true;
|
|
708
|
+
this.closeTab(tabId).catch(console.error);
|
|
709
|
+
}
|
|
710
|
+
});
|
|
711
|
+
|
|
712
|
+
// Track page close event
|
|
713
|
+
page.on('close', () => {
|
|
714
|
+
debug.warn('preview', `⚠️ Page close event for tab ${tabId}`);
|
|
715
|
+
});
|
|
716
|
+
|
|
717
|
+
// Handle popup/new window events within this context
|
|
718
|
+
context.on('targetcreated', async (target) => {
|
|
719
|
+
if (target.type() === 'page') {
|
|
720
|
+
const newPage = await target.page();
|
|
721
|
+
if (newPage && newPage !== page) {
|
|
722
|
+
const popupUrl = newPage.url();
|
|
723
|
+
|
|
724
|
+
// Emit event for frontend to handle
|
|
725
|
+
this.emit('new-window', {
|
|
726
|
+
tabId,
|
|
727
|
+
url: popupUrl,
|
|
728
|
+
timestamp: Date.now()
|
|
729
|
+
});
|
|
730
|
+
|
|
731
|
+
// Close the popup to prevent resource leak
|
|
732
|
+
try {
|
|
733
|
+
await newPage.close();
|
|
734
|
+
} catch (error) {
|
|
735
|
+
debug.warn('preview', 'Failed to close popup:', error);
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
});
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
/**
|
|
743
|
+
* Clean up the isolated context for a tab
|
|
744
|
+
*/
|
|
745
|
+
private async cleanupContext(tab: BrowserTab) {
|
|
746
|
+
try {
|
|
747
|
+
// Close the page first
|
|
748
|
+
if (tab.page && !tab.page.isClosed()) {
|
|
749
|
+
await tab.page.close().catch((error) =>
|
|
750
|
+
debug.warn('preview', `⚠️ Error closing page:`, error instanceof Error ? error.message : error)
|
|
751
|
+
);
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
// Destroy the isolated session via browser pool (use project-scoped sessionId)
|
|
755
|
+
const sessionId = this.getSessionId(tab.id);
|
|
756
|
+
await browserPool.destroySession(sessionId);
|
|
757
|
+
} catch (error) {
|
|
758
|
+
debug.warn('preview', `⚠️ Error during context cleanup for ${tab.id}:`, error instanceof Error ? error.message : error);
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
/**
|
|
763
|
+
* Helper: Get title from URL
|
|
764
|
+
*/
|
|
765
|
+
private getTitleFromUrl(url: string): string {
|
|
766
|
+
if (!url || url === 'about:blank') return 'New Tab';
|
|
767
|
+
try {
|
|
768
|
+
return new URL(url).hostname;
|
|
769
|
+
} catch {
|
|
770
|
+
return url.length > 30 ? url.slice(0, 30) + '...' : url;
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
/**
|
|
775
|
+
* Initialize periodic cleanup of inactive tabs
|
|
776
|
+
*/
|
|
777
|
+
private initializeCleanup(): void {
|
|
778
|
+
// Don't initialize twice
|
|
779
|
+
if (this.cleanupInterval) {
|
|
780
|
+
return;
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
// Start periodic cleanup
|
|
784
|
+
this.cleanupInterval = setInterval(() => {
|
|
785
|
+
this.performCleanup();
|
|
786
|
+
}, CLEANUP_INTERVAL);
|
|
787
|
+
|
|
788
|
+
// Cleanup on shutdown
|
|
789
|
+
const cleanup = () => {
|
|
790
|
+
if (this.cleanupInterval) clearInterval(this.cleanupInterval);
|
|
791
|
+
this.tabActivity.clear();
|
|
792
|
+
};
|
|
793
|
+
|
|
794
|
+
process.on('SIGTERM', cleanup);
|
|
795
|
+
process.on('SIGINT', cleanup);
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
/**
|
|
799
|
+
* Perform cleanup of inactive tabs
|
|
800
|
+
*/
|
|
801
|
+
private performCleanup(): void {
|
|
802
|
+
const now = Date.now();
|
|
803
|
+
|
|
804
|
+
for (const [tabId, tab] of this.tabs.entries()) {
|
|
805
|
+
const lastActivity = this.tabActivity.get(tabId);
|
|
806
|
+
|
|
807
|
+
// If no activity recorded, mark it as active now and skip cleanup
|
|
808
|
+
if (!lastActivity) {
|
|
809
|
+
this.tabActivity.set(tabId, now);
|
|
810
|
+
continue;
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
const inactiveTime = now - lastActivity;
|
|
814
|
+
|
|
815
|
+
// Skip if tab has recent activity
|
|
816
|
+
if (inactiveTime < INACTIVE_TIMEOUT) {
|
|
817
|
+
continue;
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
// Only cleanup if tab is truly orphaned
|
|
821
|
+
if (tab.isDestroyed || (tab.page?.isClosed() && !tab.browser?.connected)) {
|
|
822
|
+
debug.log('preview', `🧹 Auto-cleaning up inactive tab: ${tabId} (inactive for ${Math.round(inactiveTime / 1000)}s)`);
|
|
823
|
+
|
|
824
|
+
// Close tab
|
|
825
|
+
this.closeTab(tabId).catch(console.error);
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
/**
|
|
831
|
+
* Cleanup inactive tabs
|
|
832
|
+
*/
|
|
833
|
+
async cleanupInactiveTabs() {
|
|
834
|
+
const tabIds = Array.from(this.tabs.keys());
|
|
835
|
+
const inactiveTabs: string[] = [];
|
|
836
|
+
const activeTabs: string[] = [];
|
|
837
|
+
|
|
838
|
+
// Categorize tabs by activity
|
|
839
|
+
for (const tabId of tabIds) {
|
|
840
|
+
const tab = this.tabs.get(tabId);
|
|
841
|
+
if (!tab) {
|
|
842
|
+
inactiveTabs.push(tabId);
|
|
843
|
+
continue;
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
// Check if tab is truly inactive
|
|
847
|
+
const isInactive =
|
|
848
|
+
tab.isDestroyed ||
|
|
849
|
+
!tab.browser?.connected ||
|
|
850
|
+
tab.page?.isClosed() ||
|
|
851
|
+
!tab.isStreaming;
|
|
852
|
+
|
|
853
|
+
if (isInactive) {
|
|
854
|
+
inactiveTabs.push(tabId);
|
|
855
|
+
} else {
|
|
856
|
+
activeTabs.push(tabId);
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
// Only cleanup inactive tabs
|
|
861
|
+
if (inactiveTabs.length > 0) {
|
|
862
|
+
const cleanupPromises = inactiveTabs.map(tabId =>
|
|
863
|
+
this.closeTab(tabId).catch(error =>
|
|
864
|
+
debug.warn('preview', `⚠️ Error destroying inactive tab ${tabId}:`, error)
|
|
865
|
+
)
|
|
866
|
+
);
|
|
867
|
+
|
|
868
|
+
try {
|
|
869
|
+
await Promise.race([
|
|
870
|
+
Promise.all(cleanupPromises),
|
|
871
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error('Inactive tab cleanup timeout')), 10000))
|
|
872
|
+
]);
|
|
873
|
+
} catch (error) {
|
|
874
|
+
debug.warn('preview', '⚠️ Inactive tab cleanup timeout:', error);
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
return {
|
|
879
|
+
activeTabsCount: activeTabs.length,
|
|
880
|
+
inactiveTabsDestroyed: inactiveTabs.length,
|
|
881
|
+
activeTabs,
|
|
882
|
+
cleanedTabs: inactiveTabs
|
|
883
|
+
};
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
/**
|
|
887
|
+
* Cleanup all tabs
|
|
888
|
+
*/
|
|
889
|
+
async cleanup(): Promise<void> {
|
|
890
|
+
debug.log('preview', `🧹 Cleaning up ${this.tabs.size} tabs...`);
|
|
891
|
+
|
|
892
|
+
// Stop cleanup interval
|
|
893
|
+
if (this.cleanupInterval) {
|
|
894
|
+
clearInterval(this.cleanupInterval);
|
|
895
|
+
this.cleanupInterval = null;
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
const tabIds = Array.from(this.tabs.keys());
|
|
899
|
+
|
|
900
|
+
if (tabIds.length > 0) {
|
|
901
|
+
debug.log('preview', `🗑️ Destroying ${tabIds.length} tabs...`);
|
|
902
|
+
|
|
903
|
+
// Destroy all tabs in parallel
|
|
904
|
+
const cleanupPromises = tabIds.map((tabId) =>
|
|
905
|
+
this.closeTab(tabId).catch((error) => debug.warn('preview', `⚠️ Error destroying tab ${tabId}:`, error))
|
|
906
|
+
);
|
|
907
|
+
|
|
908
|
+
try {
|
|
909
|
+
await Promise.race([
|
|
910
|
+
Promise.all(cleanupPromises),
|
|
911
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error('Tab cleanup timeout')), 15000))
|
|
912
|
+
]);
|
|
913
|
+
} catch (error) {
|
|
914
|
+
debug.warn('preview', '⚠️ Tab cleanup timeout:', error);
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
// Force clear tabs map
|
|
919
|
+
this.tabs.clear();
|
|
920
|
+
this.activeTabId = null;
|
|
921
|
+
this.tabActivity.clear();
|
|
922
|
+
|
|
923
|
+
// Clean up the browser pool (closes all contexts and the shared browser)
|
|
924
|
+
await browserPool.cleanup();
|
|
925
|
+
|
|
926
|
+
debug.log('preview', '✅ All tabs cleaned up');
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
/**
|
|
930
|
+
* Get all tab IDs
|
|
931
|
+
*/
|
|
932
|
+
getAvailableTabIds(): string[] {
|
|
933
|
+
return Array.from(this.tabs.keys());
|
|
934
|
+
}
|
|
935
|
+
}
|