@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,953 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Terminal Project Manager
|
|
3
|
+
* Manages terminal sessions per project with automatic context switching
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { terminalSessionManager, type TerminalSessionState } from './session.service';
|
|
7
|
+
import { terminalPersistenceManager } from './persistence.service';
|
|
8
|
+
import { terminalStore } from '$frontend/lib/stores/features/terminal.svelte';
|
|
9
|
+
import type { TerminalSession } from '$shared/types/terminal';
|
|
10
|
+
import { terminalService } from './terminal.service';
|
|
11
|
+
import { debug } from '$shared/utils/logger';
|
|
12
|
+
interface ProjectTerminalContext {
|
|
13
|
+
projectId: string;
|
|
14
|
+
projectPath: string;
|
|
15
|
+
sessionIds: string[];
|
|
16
|
+
activeSessionId: string | null;
|
|
17
|
+
lastActiveAt: Date;
|
|
18
|
+
// Store terminal output per session for this project
|
|
19
|
+
sessionOutputs: Map<string, Array<{ content: string; type: string; timestamp: Date }>>;
|
|
20
|
+
// Store command history per session for this project
|
|
21
|
+
sessionCommandHistories: Map<string, string[]>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
class TerminalProjectManager {
|
|
25
|
+
private projectContexts = new Map<string, ProjectTerminalContext>();
|
|
26
|
+
private currentProjectId: string | null = null;
|
|
27
|
+
|
|
28
|
+
constructor() {
|
|
29
|
+
// Initialize will be called from outside to set up event listeners
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Initialize or get terminal context for a project
|
|
34
|
+
*/
|
|
35
|
+
getOrCreateProjectContext(projectId: string, projectPath: string): ProjectTerminalContext {
|
|
36
|
+
let context = this.projectContexts.get(projectId);
|
|
37
|
+
|
|
38
|
+
if (!context) {
|
|
39
|
+
context = {
|
|
40
|
+
projectId,
|
|
41
|
+
projectPath,
|
|
42
|
+
sessionIds: [],
|
|
43
|
+
activeSessionId: null,
|
|
44
|
+
lastActiveAt: new Date(),
|
|
45
|
+
sessionOutputs: new Map(),
|
|
46
|
+
sessionCommandHistories: new Map()
|
|
47
|
+
};
|
|
48
|
+
this.projectContexts.set(projectId, context);
|
|
49
|
+
this.persistContexts();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return context;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Switch to a different project's terminal context
|
|
57
|
+
*/
|
|
58
|
+
async switchToProject(projectId: string, projectPath: string): Promise<void> {
|
|
59
|
+
// Switching to project
|
|
60
|
+
|
|
61
|
+
// Save current project's terminal state if switching away
|
|
62
|
+
if (this.currentProjectId && this.currentProjectId !== projectId) {
|
|
63
|
+
await this.saveCurrentProjectState();
|
|
64
|
+
// Hide current project's terminal sessions (but keep them running)
|
|
65
|
+
await this.hideProjectTerminalSessions(this.currentProjectId);
|
|
66
|
+
|
|
67
|
+
// Background output collection is handled by the server
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Get or create context for the new project
|
|
71
|
+
const context = this.getOrCreateProjectContext(projectId, projectPath);
|
|
72
|
+
|
|
73
|
+
// Context for project
|
|
74
|
+
|
|
75
|
+
// Clear any existing sessions in store to prevent cross-contamination
|
|
76
|
+
if (this.currentProjectId !== projectId) {
|
|
77
|
+
// Clear all sessions properly
|
|
78
|
+
terminalStore.clearAllSessions();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Restore or create terminal sessions for this project
|
|
82
|
+
if (context.sessionIds.length === 0) {
|
|
83
|
+
// No sessions for project, creating initial session
|
|
84
|
+
await this.createProjectTerminalSessions(projectId, projectPath);
|
|
85
|
+
} else {
|
|
86
|
+
// Show existing terminal sessions for this project
|
|
87
|
+
await this.showProjectTerminalSessions(context);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Update current project
|
|
91
|
+
this.currentProjectId = projectId;
|
|
92
|
+
context.lastActiveAt = new Date();
|
|
93
|
+
this.persistContexts();
|
|
94
|
+
|
|
95
|
+
// Check for active streams for this project after switching
|
|
96
|
+
await this.checkAndRestoreActiveStreams(projectId);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Create initial terminal sessions for a project
|
|
101
|
+
*/
|
|
102
|
+
private async createProjectTerminalSessions(projectId: string, projectPath: string): Promise<void> {
|
|
103
|
+
// Creating terminal session for project
|
|
104
|
+
|
|
105
|
+
const context = this.getOrCreateProjectContext(projectId, projectPath);
|
|
106
|
+
|
|
107
|
+
// Create only 1 terminal session by default with correct project path and projectId
|
|
108
|
+
const sessionId = terminalStore.createNewSession(projectPath, projectPath, projectId);
|
|
109
|
+
|
|
110
|
+
// Update the session's directory to ensure it's correct
|
|
111
|
+
const session = terminalStore.getSession(sessionId);
|
|
112
|
+
if (session) {
|
|
113
|
+
session.directory = projectPath;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Create a fresh session in terminalSessionManager with correct project association
|
|
117
|
+
terminalSessionManager.createSession(sessionId, projectId, projectPath, projectPath);
|
|
118
|
+
|
|
119
|
+
context.sessionIds.push(sessionId);
|
|
120
|
+
context.activeSessionId = sessionId;
|
|
121
|
+
terminalStore.switchToSession(sessionId);
|
|
122
|
+
|
|
123
|
+
this.persistContexts();
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Hide terminal sessions for a project (keep them running in background)
|
|
128
|
+
*/
|
|
129
|
+
private async hideProjectTerminalSessions(projectId: string): Promise<void> {
|
|
130
|
+
const context = this.projectContexts.get(projectId);
|
|
131
|
+
if (!context) return;
|
|
132
|
+
|
|
133
|
+
// Hiding sessions for project
|
|
134
|
+
|
|
135
|
+
// Save execution state before hiding (in-memory via persistence manager)
|
|
136
|
+
for (const sessionId of context.sessionIds) {
|
|
137
|
+
const session = terminalSessionManager.getSession(sessionId);
|
|
138
|
+
if (session && session.isExecuting && session.streamId) {
|
|
139
|
+
terminalPersistenceManager.saveActiveStream(
|
|
140
|
+
sessionId, session.streamId, session.lastCommand || '', projectId
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// CRITICAL: Clean up WebSocket listeners for ALL sessions in this project
|
|
146
|
+
// This prevents stale listeners from accumulating when switching projects
|
|
147
|
+
// which would cause duplicate terminal output processing (input duplication bug)
|
|
148
|
+
for (const sessionId of context.sessionIds) {
|
|
149
|
+
terminalService.cleanupListeners(sessionId);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Clear the terminal store to hide sessions from UI
|
|
153
|
+
// Sessions are preserved in terminalSessionManager
|
|
154
|
+
terminalStore.clearAllSessions();
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Show terminal sessions for a project
|
|
159
|
+
*/
|
|
160
|
+
private async showProjectTerminalSessions(context: ProjectTerminalContext): Promise<void> {
|
|
161
|
+
// Showing sessions for project
|
|
162
|
+
|
|
163
|
+
// Clear all sessions from store first to ensure clean slate
|
|
164
|
+
terminalStore.clearAllSessions();
|
|
165
|
+
|
|
166
|
+
// Import terminalPersistenceManager to check for active streams
|
|
167
|
+
const { terminalPersistenceManager } = await import('./persistence.service');
|
|
168
|
+
const allActiveStreams = terminalPersistenceManager.getAllActiveStreams();
|
|
169
|
+
|
|
170
|
+
// Restore all sessions for this project from saved state
|
|
171
|
+
if (context.sessionIds.length > 0) {
|
|
172
|
+
for (const sessionId of context.sessionIds) {
|
|
173
|
+
// Create a new terminal session in the store
|
|
174
|
+
// Extract terminal number from sessionId (format: projectId-terminal-N or terminal-N)
|
|
175
|
+
const sessionParts = sessionId.split('-');
|
|
176
|
+
const terminalNumber = sessionParts[sessionParts.length - 1] || '1';
|
|
177
|
+
|
|
178
|
+
const terminalSession: TerminalSession = {
|
|
179
|
+
id: sessionId,
|
|
180
|
+
name: `Terminal ${terminalNumber}`,
|
|
181
|
+
directory: context.projectPath, // Always use the project path as base directory
|
|
182
|
+
lines: [],
|
|
183
|
+
commandHistory: [],
|
|
184
|
+
isActive: false,
|
|
185
|
+
createdAt: new Date(),
|
|
186
|
+
lastUsedAt: new Date(),
|
|
187
|
+
shellType: 'Unknown',
|
|
188
|
+
terminalBuffer: undefined,
|
|
189
|
+
projectId: context.projectId,
|
|
190
|
+
projectPath: context.projectPath
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
// Check for active streams for this session first (before restoring output)
|
|
194
|
+
let hasActiveStream = false;
|
|
195
|
+
let activeStreamInfo: any = null;
|
|
196
|
+
for (const stream of allActiveStreams) {
|
|
197
|
+
// Match by projectId and terminal number
|
|
198
|
+
if (stream.projectId === context.projectId) {
|
|
199
|
+
// Extract terminal number from stream sessionId
|
|
200
|
+
const streamParts = stream.sessionId.split('-');
|
|
201
|
+
const streamTerminalNumber = streamParts[streamParts.length - 1];
|
|
202
|
+
if (streamTerminalNumber === terminalNumber) {
|
|
203
|
+
hasActiveStream = true;
|
|
204
|
+
activeStreamInfo = stream;
|
|
205
|
+
break;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Restore saved output for this session
|
|
211
|
+
let baseOutput: any[] = [];
|
|
212
|
+
let backgroundOutput: any[] = [];
|
|
213
|
+
|
|
214
|
+
// First, restore base output from context (input/output sebelumnya)
|
|
215
|
+
if (context.sessionOutputs.has(sessionId)) {
|
|
216
|
+
const savedOutput = context.sessionOutputs.get(sessionId);
|
|
217
|
+
if (savedOutput) {
|
|
218
|
+
baseOutput = savedOutput.map(output => ({
|
|
219
|
+
content: output.content,
|
|
220
|
+
type: output.type as any,
|
|
221
|
+
timestamp: output.timestamp
|
|
222
|
+
}));
|
|
223
|
+
// Restored base output lines for session
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Second, get NEW output from server that was generated while we were away
|
|
228
|
+
// We need to track when we saved the output to know what's new
|
|
229
|
+
if (hasActiveStream && activeStreamInfo) {
|
|
230
|
+
try {
|
|
231
|
+
// Get the saved output count from context metadata
|
|
232
|
+
// This tells us how much output we had when we switched away
|
|
233
|
+
let savedOutputCount = 0;
|
|
234
|
+
const savedMetadata = context.sessionOutputs.get(`${sessionId}-metadata`);
|
|
235
|
+
if (savedMetadata && typeof savedMetadata === 'object' && 'outputCount' in savedMetadata) {
|
|
236
|
+
savedOutputCount = (savedMetadata as any).outputCount || 0;
|
|
237
|
+
} else {
|
|
238
|
+
// Fallback: count actual output lines in baseOutput
|
|
239
|
+
for (const line of baseOutput) {
|
|
240
|
+
if (line.type === 'output' || line.type === 'error') {
|
|
241
|
+
savedOutputCount++;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Get only NEW output from server (skip what we already have)
|
|
247
|
+
const data = await terminalService.getMissedOutput(
|
|
248
|
+
sessionId,
|
|
249
|
+
activeStreamInfo.streamId,
|
|
250
|
+
savedOutputCount
|
|
251
|
+
);
|
|
252
|
+
if (data.success && data.output && data.output.length > 0) {
|
|
253
|
+
// Convert server output to terminal lines
|
|
254
|
+
backgroundOutput = data.output.map((content: string) => ({
|
|
255
|
+
content: content,
|
|
256
|
+
type: 'output',
|
|
257
|
+
timestamp: new Date()
|
|
258
|
+
}));
|
|
259
|
+
debug.log('terminal', `Restored ${backgroundOutput.length} new output lines for session ${sessionId}`);
|
|
260
|
+
}
|
|
261
|
+
} catch (error) {
|
|
262
|
+
debug.error('terminal', 'Failed to fetch missed output:', error);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Combine base output with background output from server
|
|
267
|
+
// Base output contains previous input/output saved in context
|
|
268
|
+
// Background output contains new output from server (if any)
|
|
269
|
+
terminalSession.lines = [...baseOutput, ...backgroundOutput];
|
|
270
|
+
|
|
271
|
+
// Restore command history from context (persisted) rather than manager (temporary)
|
|
272
|
+
const savedCommandHistory = context.sessionCommandHistories.get(sessionId);
|
|
273
|
+
if (savedCommandHistory) {
|
|
274
|
+
terminalSession.commandHistory = savedCommandHistory;
|
|
275
|
+
// Restored command history items for session
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Check if session exists in manager for execution state
|
|
279
|
+
const managerSession = terminalSessionManager.getSession(sessionId);
|
|
280
|
+
if (managerSession) {
|
|
281
|
+
// Update manager's command history with persisted history
|
|
282
|
+
if (savedCommandHistory) {
|
|
283
|
+
managerSession.commandHistory = savedCommandHistory;
|
|
284
|
+
}
|
|
285
|
+
// Always use project path for directory when switching projects
|
|
286
|
+
// This ensures the visual directory is correct
|
|
287
|
+
terminalSession.directory = context.projectPath;
|
|
288
|
+
|
|
289
|
+
// IMPORTANT: Restore execution state to fix interrupt button visibility
|
|
290
|
+
// When switching back to a project with running processes, the execution
|
|
291
|
+
// state must be preserved for the interrupt button to show correctly
|
|
292
|
+
if (hasActiveStream || managerSession.isExecuting) {
|
|
293
|
+
// Restoring execution state for session
|
|
294
|
+
// Update the store's execution state for this session
|
|
295
|
+
terminalStore.setExecutingState(sessionId, true);
|
|
296
|
+
|
|
297
|
+
// Update manager session with stream info if available
|
|
298
|
+
if (activeStreamInfo) {
|
|
299
|
+
managerSession.isExecuting = true;
|
|
300
|
+
managerSession.streamId = activeStreamInfo.streamId;
|
|
301
|
+
managerSession.lastCommand = activeStreamInfo.command;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Add session to store
|
|
307
|
+
terminalStore.addSession(terminalSession);
|
|
308
|
+
|
|
309
|
+
// Ensure session exists in manager with correct project association and directory
|
|
310
|
+
if (!managerSession || managerSession.projectId !== context.projectId) {
|
|
311
|
+
const newSession = terminalSessionManager.createSession(sessionId, context.projectId, context.projectPath, context.projectPath);
|
|
312
|
+
// If we have active stream info, update the new session
|
|
313
|
+
if (hasActiveStream && activeStreamInfo) {
|
|
314
|
+
newSession.isExecuting = true;
|
|
315
|
+
newSession.streamId = activeStreamInfo.streamId;
|
|
316
|
+
newSession.lastCommand = activeStreamInfo.command;
|
|
317
|
+
}
|
|
318
|
+
} else {
|
|
319
|
+
// Update existing session to ensure correct directory
|
|
320
|
+
terminalSessionManager.updateSession(sessionId, {
|
|
321
|
+
projectId: context.projectId,
|
|
322
|
+
projectPath: context.projectPath,
|
|
323
|
+
workingDirectory: context.projectPath
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
} else {
|
|
328
|
+
// No sessions exist, create a new one
|
|
329
|
+
// No sessions for project, creating new
|
|
330
|
+
const newSessionId = terminalStore.createNewSession(context.projectPath, context.projectPath, context.projectId);
|
|
331
|
+
|
|
332
|
+
// Create in manager
|
|
333
|
+
terminalSessionManager.createSession(newSessionId, context.projectId, context.projectPath, context.projectPath);
|
|
334
|
+
|
|
335
|
+
context.sessionIds.push(newSessionId);
|
|
336
|
+
context.activeSessionId = newSessionId;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// Switch to the active session for this project
|
|
340
|
+
if (context.activeSessionId && context.sessionIds.includes(context.activeSessionId)) {
|
|
341
|
+
terminalStore.switchToSession(context.activeSessionId);
|
|
342
|
+
} else if (context.sessionIds.length > 0) {
|
|
343
|
+
// Active session is invalid, use first available
|
|
344
|
+
context.activeSessionId = context.sessionIds[0];
|
|
345
|
+
terminalStore.switchToSession(context.activeSessionId);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
this.persistContexts();
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Restore terminal sessions for a project (deprecated - use showProjectTerminalSessions)
|
|
353
|
+
*/
|
|
354
|
+
private async restoreProjectTerminalSessions(context: ProjectTerminalContext): Promise<void> {
|
|
355
|
+
// Restoring sessions for project
|
|
356
|
+
|
|
357
|
+
// Show sessions for this project
|
|
358
|
+
for (const sessionId of context.sessionIds) {
|
|
359
|
+
const storeSession = terminalStore.getSession(sessionId);
|
|
360
|
+
const managerSession = terminalSessionManager.getSession(sessionId);
|
|
361
|
+
|
|
362
|
+
if (storeSession) {
|
|
363
|
+
// Session exists in store, just make it visible
|
|
364
|
+
// Session already exists, making visible
|
|
365
|
+
} else if (managerSession) {
|
|
366
|
+
// Session exists in manager but not in store, restore it
|
|
367
|
+
// Restoring session from manager
|
|
368
|
+
this.restoreSessionToStore(managerSession);
|
|
369
|
+
} else {
|
|
370
|
+
// Session doesn't exist, create a new one
|
|
371
|
+
// Session not found, creating new
|
|
372
|
+
const newSessionId = terminalStore.createNewSession(context.projectPath, context.projectPath, context.projectId);
|
|
373
|
+
|
|
374
|
+
// Update context with new session ID
|
|
375
|
+
const index = context.sessionIds.indexOf(sessionId);
|
|
376
|
+
if (index !== -1) {
|
|
377
|
+
context.sessionIds[index] = newSessionId;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// Link to project
|
|
381
|
+
terminalSessionManager.updateSession(newSessionId, {
|
|
382
|
+
projectId: context.projectId,
|
|
383
|
+
projectPath: context.projectPath
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
// Update active session ID if needed
|
|
387
|
+
if (context.activeSessionId === sessionId) {
|
|
388
|
+
context.activeSessionId = newSessionId;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// Switch to the active session for this project
|
|
394
|
+
if (context.activeSessionId) {
|
|
395
|
+
terminalStore.switchToSession(context.activeSessionId);
|
|
396
|
+
} else if (context.sessionIds.length > 0) {
|
|
397
|
+
// No active session, use the first one
|
|
398
|
+
context.activeSessionId = context.sessionIds[0];
|
|
399
|
+
terminalStore.switchToSession(context.activeSessionId);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Restore a session from manager to store with project-specific output
|
|
405
|
+
*/
|
|
406
|
+
private restoreSessionToStore(managerSession: TerminalSessionState, projectContext?: ProjectTerminalContext): void {
|
|
407
|
+
// Ensure the working directory is correct for this project
|
|
408
|
+
const correctDirectory = managerSession.projectPath || managerSession.workingDirectory;
|
|
409
|
+
|
|
410
|
+
// Extract terminal number from sessionId (format: projectId-terminal-N or terminal-N)
|
|
411
|
+
const sessionParts = managerSession.id.split('-');
|
|
412
|
+
const terminalNumber = sessionParts[sessionParts.length - 1] || '1';
|
|
413
|
+
|
|
414
|
+
const terminalSession: TerminalSession = {
|
|
415
|
+
id: managerSession.id,
|
|
416
|
+
name: `Terminal ${terminalNumber}`,
|
|
417
|
+
directory: correctDirectory,
|
|
418
|
+
lines: [],
|
|
419
|
+
commandHistory: managerSession.commandHistory || [],
|
|
420
|
+
isActive: false,
|
|
421
|
+
createdAt: managerSession.createdAt || new Date(),
|
|
422
|
+
lastUsedAt: managerSession.lastUsedAt || new Date(),
|
|
423
|
+
shellType: 'Unknown',
|
|
424
|
+
terminalBuffer: undefined,
|
|
425
|
+
projectId: managerSession.projectId,
|
|
426
|
+
projectPath: managerSession.projectPath
|
|
427
|
+
};
|
|
428
|
+
|
|
429
|
+
// Restore output lines from project context if available
|
|
430
|
+
if (projectContext && projectContext.sessionOutputs.has(managerSession.id)) {
|
|
431
|
+
const savedOutput = projectContext.sessionOutputs.get(managerSession.id);
|
|
432
|
+
if (savedOutput) {
|
|
433
|
+
terminalSession.lines = savedOutput.map(output => ({
|
|
434
|
+
content: output.content,
|
|
435
|
+
type: output.type as any,
|
|
436
|
+
timestamp: output.timestamp
|
|
437
|
+
}));
|
|
438
|
+
// Restored lines for session
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
terminalStore.addSession(terminalSession);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* Save current project's terminal state
|
|
448
|
+
*/
|
|
449
|
+
private async saveCurrentProjectState(): Promise<void> {
|
|
450
|
+
if (!this.currentProjectId) return;
|
|
451
|
+
|
|
452
|
+
const context = this.projectContexts.get(this.currentProjectId);
|
|
453
|
+
if (!context) return;
|
|
454
|
+
|
|
455
|
+
// Saving state for project
|
|
456
|
+
|
|
457
|
+
// Update active session ID
|
|
458
|
+
const activeSessionId = terminalStore.activeSessionId;
|
|
459
|
+
if (activeSessionId) {
|
|
460
|
+
context.activeSessionId = activeSessionId;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// Save session outputs and states
|
|
464
|
+
for (const sessionId of context.sessionIds) {
|
|
465
|
+
const session = terminalStore.getSession(sessionId);
|
|
466
|
+
if (session) {
|
|
467
|
+
// Save output lines for this specific project session
|
|
468
|
+
const outputLines = session.lines.map(line => ({
|
|
469
|
+
content: line.content,
|
|
470
|
+
type: line.type,
|
|
471
|
+
timestamp: line.timestamp || new Date()
|
|
472
|
+
}));
|
|
473
|
+
context.sessionOutputs.set(sessionId, outputLines);
|
|
474
|
+
|
|
475
|
+
// Save metadata about how many output lines we have
|
|
476
|
+
// This helps us know what's new when we come back
|
|
477
|
+
let outputCount = 0;
|
|
478
|
+
for (const line of session.lines) {
|
|
479
|
+
if (line.type === 'output' || line.type === 'error') {
|
|
480
|
+
outputCount++;
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
context.sessionOutputs.set(`${sessionId}-metadata`, { outputCount } as any);
|
|
484
|
+
|
|
485
|
+
// Save command history for this specific project session
|
|
486
|
+
if (session.commandHistory && session.commandHistory.length > 0) {
|
|
487
|
+
context.sessionCommandHistories.set(sessionId, session.commandHistory);
|
|
488
|
+
// Saved command history items for session
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// Update session metadata in manager
|
|
492
|
+
terminalSessionManager.updateSession(sessionId, {
|
|
493
|
+
commandHistory: session.commandHistory,
|
|
494
|
+
workingDirectory: session.directory,
|
|
495
|
+
projectId: this.currentProjectId,
|
|
496
|
+
projectPath: context.projectPath
|
|
497
|
+
});
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
this.persistContexts();
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* Save all projects' terminal states (called before browser unload)
|
|
506
|
+
*/
|
|
507
|
+
saveAllProjectStates(): void {
|
|
508
|
+
// Saving all project states before unload
|
|
509
|
+
|
|
510
|
+
// Save current project state first
|
|
511
|
+
if (this.currentProjectId) {
|
|
512
|
+
this.saveCurrentProjectState();
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// Save command history from terminalSessionManager for all projects
|
|
516
|
+
for (const [projectId, context] of this.projectContexts.entries()) {
|
|
517
|
+
if (projectId === this.currentProjectId) {
|
|
518
|
+
// Already saved above
|
|
519
|
+
continue;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// Saving state for background project
|
|
523
|
+
|
|
524
|
+
// Update command history from session manager for all sessions
|
|
525
|
+
for (const sessionId of context.sessionIds) {
|
|
526
|
+
const managerSession = terminalSessionManager.getSession(sessionId);
|
|
527
|
+
if (managerSession && managerSession.commandHistory) {
|
|
528
|
+
// Save command history from manager
|
|
529
|
+
context.sessionCommandHistories.set(sessionId, managerSession.commandHistory);
|
|
530
|
+
// Saved command history items for background session
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
// Sync stream info to persistence manager
|
|
536
|
+
this.persistContexts();
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
/**
|
|
540
|
+
* Add a new terminal session to the current project
|
|
541
|
+
*/
|
|
542
|
+
addTerminalToCurrentProject(forceProjectId?: string, forceProjectPath?: string): string | null {
|
|
543
|
+
// Use forced project ID if provided (for cases where currentProjectId isn't set yet)
|
|
544
|
+
const projectIdToUse = forceProjectId || this.currentProjectId;
|
|
545
|
+
if (!projectIdToUse) {
|
|
546
|
+
// Cannot add terminal: no project ID available
|
|
547
|
+
return null;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
// Get or create context
|
|
551
|
+
let context = this.projectContexts.get(projectIdToUse);
|
|
552
|
+
if (!context && forceProjectPath) {
|
|
553
|
+
// Create context if it doesn't exist and we have a path
|
|
554
|
+
context = this.getOrCreateProjectContext(projectIdToUse, forceProjectPath);
|
|
555
|
+
}
|
|
556
|
+
if (!context) {
|
|
557
|
+
// Cannot add terminal: no context for project
|
|
558
|
+
return null;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
// Update current project ID if we're forcing it
|
|
562
|
+
if (forceProjectId && forceProjectId !== this.currentProjectId) {
|
|
563
|
+
this.currentProjectId = forceProjectId;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
// Adding new terminal to project
|
|
567
|
+
|
|
568
|
+
// Create new session with correct project path and projectId for proper isolation
|
|
569
|
+
const sessionId = terminalStore.createNewSession(context.projectPath, context.projectPath, projectIdToUse);
|
|
570
|
+
|
|
571
|
+
// Ensure the directory is set correctly
|
|
572
|
+
const session = terminalStore.getSession(sessionId);
|
|
573
|
+
if (session) {
|
|
574
|
+
session.directory = context.projectPath;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
// Create corresponding session in manager with correct project association
|
|
578
|
+
terminalSessionManager.createSession(sessionId, projectIdToUse, context.projectPath, context.projectPath);
|
|
579
|
+
|
|
580
|
+
// Add to context
|
|
581
|
+
context.sessionIds.push(sessionId);
|
|
582
|
+
context.activeSessionId = sessionId;
|
|
583
|
+
this.persistContexts();
|
|
584
|
+
|
|
585
|
+
return sessionId;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
/**
|
|
589
|
+
* Remove a terminal session from the current project
|
|
590
|
+
*/
|
|
591
|
+
async removeTerminalFromCurrentProject(sessionId: string): Promise<boolean> {
|
|
592
|
+
if (!this.currentProjectId) return false;
|
|
593
|
+
|
|
594
|
+
const context = this.projectContexts.get(this.currentProjectId);
|
|
595
|
+
if (!context) return false;
|
|
596
|
+
|
|
597
|
+
// Don't allow removing the last terminal
|
|
598
|
+
if (context.sessionIds.length <= 1) return false;
|
|
599
|
+
|
|
600
|
+
// Remove from context
|
|
601
|
+
context.sessionIds = context.sessionIds.filter(id => id !== sessionId);
|
|
602
|
+
|
|
603
|
+
// Update active session if needed
|
|
604
|
+
if (context.activeSessionId === sessionId && context.sessionIds.length > 0) {
|
|
605
|
+
context.activeSessionId = context.sessionIds[0];
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
// Remove from stores
|
|
609
|
+
await terminalStore.closeSession(sessionId);
|
|
610
|
+
terminalSessionManager.removeSession(sessionId);
|
|
611
|
+
|
|
612
|
+
this.persistContexts();
|
|
613
|
+
return true;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
/**
|
|
617
|
+
* Remove a session from project context (called when terminal tab is closed)
|
|
618
|
+
* This is different from removeTerminalFromCurrentProject - it doesn't call closeSession
|
|
619
|
+
* to avoid circular dependency
|
|
620
|
+
*/
|
|
621
|
+
removeSessionFromContext(sessionId: string): void {
|
|
622
|
+
debug.log('terminal', `🗑️ [projectManager] Removing session from context: ${sessionId}`);
|
|
623
|
+
|
|
624
|
+
// Find which project this session belongs to
|
|
625
|
+
for (const [projectId, context] of this.projectContexts.entries()) {
|
|
626
|
+
const sessionIndex = context.sessionIds.indexOf(sessionId);
|
|
627
|
+
if (sessionIndex !== -1) {
|
|
628
|
+
debug.log('terminal', `🗑️ [projectManager] Found session in project: ${projectId}`);
|
|
629
|
+
debug.log('terminal', `🗑️ [projectManager] Sessions before: ${context.sessionIds.join(', ')}`);
|
|
630
|
+
|
|
631
|
+
// Remove from sessionIds array
|
|
632
|
+
context.sessionIds.splice(sessionIndex, 1);
|
|
633
|
+
|
|
634
|
+
// Clear session-related data
|
|
635
|
+
context.sessionOutputs.delete(sessionId);
|
|
636
|
+
context.sessionOutputs.delete(`${sessionId}-metadata`);
|
|
637
|
+
context.sessionCommandHistories.delete(sessionId);
|
|
638
|
+
|
|
639
|
+
// Update active session if needed
|
|
640
|
+
if (context.activeSessionId === sessionId) {
|
|
641
|
+
context.activeSessionId = context.sessionIds[0] || null;
|
|
642
|
+
debug.log('terminal', `🗑️ [projectManager] Updated activeSessionId to: ${context.activeSessionId}`);
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
debug.log('terminal', `🗑️ [projectManager] Sessions after: ${context.sessionIds.join(', ')}`);
|
|
646
|
+
|
|
647
|
+
// Also remove from terminalSessionManager
|
|
648
|
+
terminalSessionManager.removeSession(sessionId);
|
|
649
|
+
|
|
650
|
+
// Persist changes
|
|
651
|
+
this.persistContexts();
|
|
652
|
+
debug.log('terminal', `🗑️ [projectManager] Context persisted`);
|
|
653
|
+
return;
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
debug.log('terminal', `🗑️ [projectManager] Session not found in any project context`);
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
/**
|
|
661
|
+
* Get terminal sessions for a specific project
|
|
662
|
+
*/
|
|
663
|
+
getProjectSessions(projectId: string): string[] {
|
|
664
|
+
const context = this.projectContexts.get(projectId);
|
|
665
|
+
return context?.sessionIds || [];
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
/**
|
|
669
|
+
* Check if a project has running terminal sessions
|
|
670
|
+
*/
|
|
671
|
+
hasRunningTerminals(projectId: string): boolean {
|
|
672
|
+
const context = this.projectContexts.get(projectId);
|
|
673
|
+
if (!context) return false;
|
|
674
|
+
|
|
675
|
+
return context.sessionIds.some(sessionId => {
|
|
676
|
+
const session = terminalSessionManager.getSession(sessionId);
|
|
677
|
+
return session?.isExecuting || false;
|
|
678
|
+
});
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
/**
|
|
682
|
+
* Get count of terminal sessions for a project
|
|
683
|
+
*/
|
|
684
|
+
getTerminalCount(projectId: string): number {
|
|
685
|
+
const context = this.projectContexts.get(projectId);
|
|
686
|
+
return context?.sessionIds.length || 0;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
/**
|
|
690
|
+
* Sync active stream info to persistence manager (in-memory)
|
|
691
|
+
* Project contexts are already managed in-memory via this.projectContexts Map
|
|
692
|
+
*/
|
|
693
|
+
private persistContexts(): void {
|
|
694
|
+
// Sync active stream info to persistence manager
|
|
695
|
+
for (const [projectId, context] of this.projectContexts.entries()) {
|
|
696
|
+
for (const sessionId of context.sessionIds) {
|
|
697
|
+
const session = terminalSessionManager.getSession(sessionId);
|
|
698
|
+
if (session && session.isExecuting && session.streamId) {
|
|
699
|
+
terminalPersistenceManager.saveActiveStream(
|
|
700
|
+
sessionId, session.streamId, session.lastCommand || '', projectId
|
|
701
|
+
);
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
/**
|
|
708
|
+
* Load persisted contexts (no-op - contexts are in-memory only)
|
|
709
|
+
* Project contexts are created on demand via switchToProject()
|
|
710
|
+
*/
|
|
711
|
+
private loadPersistedContexts(): void {
|
|
712
|
+
// No-op: contexts are managed in-memory and rebuilt on demand
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
/**
|
|
716
|
+
* Clear all data (used when clearing application data)
|
|
717
|
+
*/
|
|
718
|
+
clearAll(): void {
|
|
719
|
+
this.projectContexts.clear();
|
|
720
|
+
this.currentProjectId = null;
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
/**
|
|
724
|
+
* Initialize the manager and setup collaborative WebSocket listeners
|
|
725
|
+
* Project contexts are created on demand via switchToProject()
|
|
726
|
+
*/
|
|
727
|
+
initialize(): void {
|
|
728
|
+
if (typeof window === 'undefined') return;
|
|
729
|
+
this.setupCollaborativeListeners();
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
/**
|
|
733
|
+
* Setup WebSocket listeners for collaborative terminal tab management.
|
|
734
|
+
* When another user creates/closes a terminal tab, all users in the project
|
|
735
|
+
* see the tab list update automatically.
|
|
736
|
+
*/
|
|
737
|
+
private setupCollaborativeListeners(): void {
|
|
738
|
+
import('$frontend/lib/utils/ws').then(({ default: ws }) => {
|
|
739
|
+
// Listen for terminal tab created by another user
|
|
740
|
+
ws.on('terminal:tab-created', (data: {
|
|
741
|
+
sessionId: string;
|
|
742
|
+
streamId: string;
|
|
743
|
+
pid: number;
|
|
744
|
+
currentDirectory: string;
|
|
745
|
+
cols: number;
|
|
746
|
+
rows: number;
|
|
747
|
+
}) => {
|
|
748
|
+
debug.log('terminal', `📥 Received terminal:tab-created: ${data.sessionId}`);
|
|
749
|
+
|
|
750
|
+
if (!this.currentProjectId) return;
|
|
751
|
+
const context = this.projectContexts.get(this.currentProjectId);
|
|
752
|
+
if (!context) return;
|
|
753
|
+
|
|
754
|
+
// Skip if this session already exists locally (we created it)
|
|
755
|
+
if (context.sessionIds.includes(data.sessionId)) {
|
|
756
|
+
debug.log('terminal', `✓ Terminal tab ${data.sessionId} already exists locally, skipping`);
|
|
757
|
+
return;
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
// Also skip if it exists in the store already
|
|
761
|
+
const existingStoreSession = terminalStore.getSession(data.sessionId);
|
|
762
|
+
if (existingStoreSession) {
|
|
763
|
+
debug.log('terminal', `✓ Terminal tab ${data.sessionId} already in store, skipping`);
|
|
764
|
+
return;
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
// Add the new tab from another user
|
|
768
|
+
debug.log('terminal', `➕ Adding remote terminal tab: ${data.sessionId}`);
|
|
769
|
+
|
|
770
|
+
// Extract terminal number from sessionId
|
|
771
|
+
const sessionParts = data.sessionId.split('-');
|
|
772
|
+
const terminalNumber = sessionParts[sessionParts.length - 1] || '1';
|
|
773
|
+
|
|
774
|
+
const terminalSession: TerminalSession = {
|
|
775
|
+
id: data.sessionId,
|
|
776
|
+
name: `Terminal ${terminalNumber}`,
|
|
777
|
+
directory: data.currentDirectory,
|
|
778
|
+
lines: [],
|
|
779
|
+
commandHistory: [],
|
|
780
|
+
isActive: false,
|
|
781
|
+
createdAt: new Date(),
|
|
782
|
+
lastUsedAt: new Date(),
|
|
783
|
+
shellType: 'Unknown',
|
|
784
|
+
terminalBuffer: undefined,
|
|
785
|
+
projectId: this.currentProjectId,
|
|
786
|
+
projectPath: context.projectPath
|
|
787
|
+
};
|
|
788
|
+
|
|
789
|
+
// Add to store and context
|
|
790
|
+
terminalStore.addSession(terminalSession);
|
|
791
|
+
context.sessionIds.push(data.sessionId);
|
|
792
|
+
|
|
793
|
+
// Create in session manager
|
|
794
|
+
terminalSessionManager.createSession(
|
|
795
|
+
data.sessionId,
|
|
796
|
+
this.currentProjectId,
|
|
797
|
+
context.projectPath,
|
|
798
|
+
data.currentDirectory
|
|
799
|
+
);
|
|
800
|
+
|
|
801
|
+
// Update nextSessionId to avoid conflicts
|
|
802
|
+
const match = data.sessionId.match(/terminal-(\d+)/);
|
|
803
|
+
if (match) {
|
|
804
|
+
terminalStore.updateNextSessionId(parseInt(match[1], 10) + 1);
|
|
805
|
+
}
|
|
806
|
+
});
|
|
807
|
+
|
|
808
|
+
// Listen for terminal tab closed by another user
|
|
809
|
+
ws.on('terminal:tab-closed', (data: { sessionId: string }) => {
|
|
810
|
+
debug.log('terminal', `📥 Received terminal:tab-closed: ${data.sessionId}`);
|
|
811
|
+
|
|
812
|
+
if (!this.currentProjectId) return;
|
|
813
|
+
const context = this.projectContexts.get(this.currentProjectId);
|
|
814
|
+
if (!context) return;
|
|
815
|
+
|
|
816
|
+
// Skip if session doesn't exist locally (already removed or not ours)
|
|
817
|
+
if (!context.sessionIds.includes(data.sessionId)) {
|
|
818
|
+
debug.log('terminal', `✓ Terminal tab ${data.sessionId} not in our context, skipping`);
|
|
819
|
+
return;
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
// Check if session still exists in store (might already be removed locally)
|
|
823
|
+
const existingSession = terminalStore.getSession(data.sessionId);
|
|
824
|
+
if (!existingSession) {
|
|
825
|
+
// Already removed from store, just clean up context
|
|
826
|
+
this.removeSessionFromContext(data.sessionId);
|
|
827
|
+
return;
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
// Remove the tab from another user
|
|
831
|
+
debug.log('terminal', `➖ Removing remote terminal tab: ${data.sessionId}`);
|
|
832
|
+
|
|
833
|
+
// Remove from context (without calling closeSession to avoid double-kill)
|
|
834
|
+
this.removeSessionFromContext(data.sessionId);
|
|
835
|
+
|
|
836
|
+
// Remove from store silently (don't kill PTY - already killed by the user who closed it)
|
|
837
|
+
terminalStore.removeSessionFromStore(data.sessionId);
|
|
838
|
+
});
|
|
839
|
+
|
|
840
|
+
debug.log('terminal', '✅ Terminal collaborative listeners registered');
|
|
841
|
+
});
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
/**
|
|
845
|
+
* Get visual indicator data for a project
|
|
846
|
+
*/
|
|
847
|
+
getProjectIndicator(projectId: string): { hasTerminals: boolean; runningCount: number; totalCount: number } {
|
|
848
|
+
const context = this.projectContexts.get(projectId);
|
|
849
|
+
|
|
850
|
+
if (!context) {
|
|
851
|
+
return { hasTerminals: false, runningCount: 0, totalCount: 0 };
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
let runningCount = 0;
|
|
855
|
+
const countedTerminals = new Set<string>();
|
|
856
|
+
|
|
857
|
+
// First check sessions in terminalSessionManager (for current project)
|
|
858
|
+
for (const sessionId of context.sessionIds) {
|
|
859
|
+
const session = terminalSessionManager.getSession(sessionId);
|
|
860
|
+
if (session?.isExecuting) {
|
|
861
|
+
// Extract terminal number to track
|
|
862
|
+
const parts = sessionId.split('-');
|
|
863
|
+
const terminalNumber = parts[parts.length - 1];
|
|
864
|
+
countedTerminals.add(terminalNumber);
|
|
865
|
+
runningCount++;
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
// Check persistence manager for active streams (cross-project scenarios)
|
|
870
|
+
const allActiveStreams = terminalPersistenceManager.getAllActiveStreams();
|
|
871
|
+
const processedStreamIds = new Set<string>();
|
|
872
|
+
|
|
873
|
+
for (const streamInfo of allActiveStreams) {
|
|
874
|
+
if (processedStreamIds.has(streamInfo.streamId)) continue;
|
|
875
|
+
|
|
876
|
+
if (streamInfo.projectId === projectId) {
|
|
877
|
+
const streamParts = streamInfo.sessionId.split('-');
|
|
878
|
+
const terminalNumber = streamParts[streamParts.length - 1];
|
|
879
|
+
|
|
880
|
+
if (!countedTerminals.has(terminalNumber)) {
|
|
881
|
+
const hasMatchingTerminal = context.sessionIds.some(sid => {
|
|
882
|
+
const parts = sid.split('-');
|
|
883
|
+
return parts[parts.length - 1] === terminalNumber;
|
|
884
|
+
});
|
|
885
|
+
|
|
886
|
+
if (hasMatchingTerminal) {
|
|
887
|
+
countedTerminals.add(terminalNumber);
|
|
888
|
+
runningCount++;
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
processedStreamIds.add(streamInfo.streamId);
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
return {
|
|
897
|
+
hasTerminals: context.sessionIds.length > 0,
|
|
898
|
+
runningCount,
|
|
899
|
+
totalCount: context.sessionIds.length
|
|
900
|
+
};
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
/**
|
|
904
|
+
* Check and restore active streams for a project
|
|
905
|
+
*/
|
|
906
|
+
private async checkAndRestoreActiveStreams(projectId: string): Promise<void> {
|
|
907
|
+
if (typeof window === 'undefined') return;
|
|
908
|
+
|
|
909
|
+
const context = this.projectContexts.get(projectId);
|
|
910
|
+
if (!context) return;
|
|
911
|
+
|
|
912
|
+
// Import services dynamically to avoid circular dependency
|
|
913
|
+
const { terminalPersistenceManager } = await import('./persistence.service');
|
|
914
|
+
const { backgroundTerminalService } = await import('./background');
|
|
915
|
+
|
|
916
|
+
// Get all active streams from persistence manager
|
|
917
|
+
const allActiveStreams = terminalPersistenceManager.getAllActiveStreams();
|
|
918
|
+
|
|
919
|
+
// Check if any active streams belong to this project
|
|
920
|
+
for (const streamInfo of allActiveStreams) {
|
|
921
|
+
if (streamInfo.projectId === projectId) {
|
|
922
|
+
// Extract terminal number from stream's original sessionId
|
|
923
|
+
const streamParts = streamInfo.sessionId.split('-');
|
|
924
|
+
const terminalNumber = streamParts[streamParts.length - 1];
|
|
925
|
+
|
|
926
|
+
// Find matching session by terminal number in current context
|
|
927
|
+
for (const sessionId of context.sessionIds) {
|
|
928
|
+
const sessionParts = sessionId.split('-');
|
|
929
|
+
const sessionTerminalNumber = sessionParts[sessionParts.length - 1];
|
|
930
|
+
|
|
931
|
+
if (sessionTerminalNumber === terminalNumber) {
|
|
932
|
+
const session = terminalSessionManager.getSession(sessionId);
|
|
933
|
+
if (session) {
|
|
934
|
+
// Restore execution state for this session
|
|
935
|
+
terminalSessionManager.startExecution(
|
|
936
|
+
sessionId,
|
|
937
|
+
streamInfo.command,
|
|
938
|
+
streamInfo.streamId
|
|
939
|
+
);
|
|
940
|
+
terminalStore.setExecutingState(sessionId, true);
|
|
941
|
+
|
|
942
|
+
// WebSocket akan otomatis handle reconnection
|
|
943
|
+
break;
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
// Export singleton instance
|
|
953
|
+
export const terminalProjectManager = new TerminalProjectManager();
|