@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,387 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Persistent PTY Session Manager
|
|
3
|
+
* Manages long-running interactive PTY sessions (one per terminal tab)
|
|
4
|
+
* Supports true TTY interaction with stdin/stdout streaming
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { IPty } from 'bun-pty';
|
|
8
|
+
import { spawn } from 'bun-pty';
|
|
9
|
+
import { debug } from '$shared/utils/logger';
|
|
10
|
+
|
|
11
|
+
export interface PtySession {
|
|
12
|
+
sessionId: string;
|
|
13
|
+
pty: IPty;
|
|
14
|
+
cwd: string;
|
|
15
|
+
projectId?: string;
|
|
16
|
+
createdAt: Date;
|
|
17
|
+
lastActivityAt: Date;
|
|
18
|
+
// Stream listeners
|
|
19
|
+
dataListeners: Set<(data: string) => void>;
|
|
20
|
+
exitListeners: Set<(event: { exitCode: number; signal?: number | string }) => void>;
|
|
21
|
+
// Output batching (performance optimization)
|
|
22
|
+
pendingOutput: string;
|
|
23
|
+
flushScheduled: boolean;
|
|
24
|
+
// Monotonically increasing sequence number for deduplication
|
|
25
|
+
outputSeq: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
class PtySessionManager {
|
|
29
|
+
private sessions = new Map<string, PtySession>();
|
|
30
|
+
private cleanupInterval: Timer | null = null;
|
|
31
|
+
|
|
32
|
+
constructor() {
|
|
33
|
+
// Start cleanup interval (remove sessions inactive for >1 hour)
|
|
34
|
+
this.startCleanupInterval();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Create or get existing PTY session
|
|
39
|
+
*/
|
|
40
|
+
async createSession(
|
|
41
|
+
sessionId: string,
|
|
42
|
+
cwd: string,
|
|
43
|
+
projectId?: string,
|
|
44
|
+
terminalSize?: { cols: number; rows: number }
|
|
45
|
+
): Promise<PtySession> {
|
|
46
|
+
// Return existing session if already created
|
|
47
|
+
const existing = this.sessions.get(sessionId);
|
|
48
|
+
if (existing && existing.pty) {
|
|
49
|
+
debug.log('terminal', `♻️ Reusing existing PTY session: ${sessionId}`);
|
|
50
|
+
existing.lastActivityAt = new Date();
|
|
51
|
+
return existing;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
debug.log('terminal', `🚀 Creating new interactive PTY session: ${sessionId}`);
|
|
55
|
+
|
|
56
|
+
// Determine shell based on platform
|
|
57
|
+
const isWindows = process.platform === 'win32';
|
|
58
|
+
let shell: string;
|
|
59
|
+
let shellArgs: string[] = [];
|
|
60
|
+
|
|
61
|
+
if (isWindows) {
|
|
62
|
+
// Windows: Use PowerShell in interactive mode
|
|
63
|
+
shell = 'powershell';
|
|
64
|
+
shellArgs = ['-NoLogo']; // Interactive mode, no -Command
|
|
65
|
+
} else {
|
|
66
|
+
// Unix: Use user's default shell or bash
|
|
67
|
+
shell = process.env.SHELL || '/bin/bash';
|
|
68
|
+
shellArgs = []; // Interactive mode, no -c
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Prepare environment
|
|
72
|
+
const ptyEnv: Record<string, string> = {};
|
|
73
|
+
for (const [key, value] of Object.entries(process.env)) {
|
|
74
|
+
if (value !== undefined) {
|
|
75
|
+
ptyEnv[key] = value;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Terminal size
|
|
80
|
+
const cols = terminalSize?.cols || 80;
|
|
81
|
+
const rows = terminalSize?.rows || 24;
|
|
82
|
+
|
|
83
|
+
// Add terminal-specific environment variables
|
|
84
|
+
Object.assign(ptyEnv, {
|
|
85
|
+
FORCE_COLOR: '1',
|
|
86
|
+
COLORTERM: 'truecolor',
|
|
87
|
+
TERM: 'xterm-256color',
|
|
88
|
+
COLUMNS: cols.toString(),
|
|
89
|
+
LINES: rows.toString(),
|
|
90
|
+
TERM_PROGRAM: 'xterm.js',
|
|
91
|
+
CLICOLOR: '1',
|
|
92
|
+
LC_ALL: 'en_US.UTF-8',
|
|
93
|
+
LANG: 'en_US.UTF-8'
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
// Spawn interactive PTY
|
|
97
|
+
const pty = spawn(shell, shellArgs, {
|
|
98
|
+
name: 'xterm-256color',
|
|
99
|
+
cols,
|
|
100
|
+
rows,
|
|
101
|
+
cwd,
|
|
102
|
+
env: ptyEnv
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
debug.log('terminal', `✅ PTY spawned with PID: ${pty.pid}`);
|
|
106
|
+
|
|
107
|
+
// CRITICAL: Send initial newline to trigger shell prompt display
|
|
108
|
+
// Without this, shell won't show prompt on first connection
|
|
109
|
+
setTimeout(() => {
|
|
110
|
+
try {
|
|
111
|
+
pty.write('\r');
|
|
112
|
+
debug.log('terminal', '📝 Sent initial newline to trigger prompt');
|
|
113
|
+
} catch (error) {
|
|
114
|
+
debug.error('terminal', 'Failed to send initial newline:', error);
|
|
115
|
+
}
|
|
116
|
+
}, 100);
|
|
117
|
+
|
|
118
|
+
// Create session
|
|
119
|
+
const session: PtySession = {
|
|
120
|
+
sessionId,
|
|
121
|
+
pty,
|
|
122
|
+
cwd,
|
|
123
|
+
projectId,
|
|
124
|
+
createdAt: new Date(),
|
|
125
|
+
lastActivityAt: new Date(),
|
|
126
|
+
dataListeners: new Set(),
|
|
127
|
+
exitListeners: new Set(),
|
|
128
|
+
// Initialize batching state
|
|
129
|
+
pendingOutput: '',
|
|
130
|
+
flushScheduled: false,
|
|
131
|
+
outputSeq: 0
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
// Setup PTY event handlers with micro-task batching
|
|
135
|
+
pty.onData((data: string) => {
|
|
136
|
+
session.lastActivityAt = new Date();
|
|
137
|
+
|
|
138
|
+
// IMPORTANT: Always persist output to stream manager FIRST
|
|
139
|
+
// This ensures output is saved even if there are no active listeners
|
|
140
|
+
try {
|
|
141
|
+
const { terminalStreamManager } = require('./stream-manager');
|
|
142
|
+
const stream = terminalStreamManager.getStreamBySession(sessionId);
|
|
143
|
+
if (stream) {
|
|
144
|
+
terminalStreamManager.addOutput(stream.streamId, data);
|
|
145
|
+
}
|
|
146
|
+
} catch (error) {
|
|
147
|
+
// Stream manager not available yet, skip buffering
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Batch output for high-frequency data (micro-task batching)
|
|
151
|
+
session.pendingOutput += data;
|
|
152
|
+
|
|
153
|
+
if (!session.flushScheduled) {
|
|
154
|
+
session.flushScheduled = true;
|
|
155
|
+
|
|
156
|
+
// Use queueMicrotask for minimal latency batching
|
|
157
|
+
queueMicrotask(() => {
|
|
158
|
+
const output = session.pendingOutput;
|
|
159
|
+
session.pendingOutput = '';
|
|
160
|
+
session.flushScheduled = false;
|
|
161
|
+
|
|
162
|
+
// Increment sequence number for deduplication
|
|
163
|
+
session.outputSeq++;
|
|
164
|
+
|
|
165
|
+
// Broadcast batched output to all listeners
|
|
166
|
+
session.dataListeners.forEach(listener => {
|
|
167
|
+
try {
|
|
168
|
+
listener(output);
|
|
169
|
+
} catch (error) {
|
|
170
|
+
debug.error('terminal', 'Error in data listener:', error);
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
pty.onExit((event: { exitCode: number; signal?: number | string }) => {
|
|
178
|
+
debug.log('terminal', `🏁 PTY session ${sessionId} exited with code: ${event.exitCode}`);
|
|
179
|
+
// Broadcast to all listeners
|
|
180
|
+
session.exitListeners.forEach(listener => {
|
|
181
|
+
try {
|
|
182
|
+
listener(event);
|
|
183
|
+
} catch (error) {
|
|
184
|
+
debug.error('terminal', 'Error in exit listener:', error);
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
// Remove session
|
|
188
|
+
this.sessions.delete(sessionId);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
// Store session
|
|
192
|
+
this.sessions.set(sessionId, session);
|
|
193
|
+
|
|
194
|
+
return session;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Get existing session
|
|
199
|
+
*/
|
|
200
|
+
getSession(sessionId: string): PtySession | undefined {
|
|
201
|
+
return this.sessions.get(sessionId);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Write data to PTY (stdin)
|
|
206
|
+
*/
|
|
207
|
+
write(sessionId: string, data: string): boolean {
|
|
208
|
+
const session = this.sessions.get(sessionId);
|
|
209
|
+
if (!session || !session.pty) {
|
|
210
|
+
debug.error('terminal', `❌ Cannot write to session ${sessionId}: session not found`);
|
|
211
|
+
return false;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
try {
|
|
215
|
+
session.pty.write(data);
|
|
216
|
+
session.lastActivityAt = new Date();
|
|
217
|
+
return true;
|
|
218
|
+
} catch (error) {
|
|
219
|
+
debug.error('terminal', `❌ Error writing to PTY ${sessionId}:`, error);
|
|
220
|
+
return false;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Resize PTY
|
|
226
|
+
*/
|
|
227
|
+
resize(sessionId: string, cols: number, rows: number): boolean {
|
|
228
|
+
const session = this.sessions.get(sessionId);
|
|
229
|
+
if (!session || !session.pty) {
|
|
230
|
+
return false;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
try {
|
|
234
|
+
session.pty.resize(cols, rows);
|
|
235
|
+
debug.log('terminal', `🔧 PTY ${sessionId} resized to ${cols}x${rows}`);
|
|
236
|
+
return true;
|
|
237
|
+
} catch (error) {
|
|
238
|
+
debug.error('terminal', `❌ Error resizing PTY ${sessionId}:`, error);
|
|
239
|
+
return false;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Kill PTY session
|
|
245
|
+
*/
|
|
246
|
+
killSession(sessionId: string, signal?: string): boolean {
|
|
247
|
+
const session = this.sessions.get(sessionId);
|
|
248
|
+
if (!session || !session.pty) {
|
|
249
|
+
return false;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
try {
|
|
253
|
+
debug.log('terminal', `💀 Killing PTY session ${sessionId}`);
|
|
254
|
+
|
|
255
|
+
if (signal === 'SIGKILL' || signal === '9') {
|
|
256
|
+
session.pty.kill('SIGKILL');
|
|
257
|
+
} else if (signal === 'SIGTERM' || signal === '15') {
|
|
258
|
+
session.pty.kill('SIGTERM');
|
|
259
|
+
} else {
|
|
260
|
+
// Send Ctrl+C first for graceful termination
|
|
261
|
+
session.pty.write('\x03');
|
|
262
|
+
|
|
263
|
+
// Follow up with SIGKILL after delay
|
|
264
|
+
setTimeout(() => {
|
|
265
|
+
if (this.sessions.has(sessionId)) {
|
|
266
|
+
try {
|
|
267
|
+
session.pty.kill('SIGKILL');
|
|
268
|
+
} catch {
|
|
269
|
+
// Already dead, ignore
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}, 1000);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
this.sessions.delete(sessionId);
|
|
276
|
+
return true;
|
|
277
|
+
} catch (error) {
|
|
278
|
+
debug.error('terminal', `❌ Error killing PTY ${sessionId}:`, error);
|
|
279
|
+
this.sessions.delete(sessionId); // Remove anyway
|
|
280
|
+
return false;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Add data listener
|
|
286
|
+
*/
|
|
287
|
+
addDataListener(sessionId: string, listener: (data: string) => void): boolean {
|
|
288
|
+
const session = this.sessions.get(sessionId);
|
|
289
|
+
if (!session) {
|
|
290
|
+
return false;
|
|
291
|
+
}
|
|
292
|
+
session.dataListeners.add(listener);
|
|
293
|
+
return true;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Remove data listener
|
|
298
|
+
*/
|
|
299
|
+
removeDataListener(sessionId: string, listener: (data: string) => void): boolean {
|
|
300
|
+
const session = this.sessions.get(sessionId);
|
|
301
|
+
if (!session) {
|
|
302
|
+
return false;
|
|
303
|
+
}
|
|
304
|
+
return session.dataListeners.delete(listener);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Add exit listener
|
|
309
|
+
*/
|
|
310
|
+
addExitListener(
|
|
311
|
+
sessionId: string,
|
|
312
|
+
listener: (event: { exitCode: number; signal?: number | string }) => void
|
|
313
|
+
): boolean {
|
|
314
|
+
const session = this.sessions.get(sessionId);
|
|
315
|
+
if (!session) {
|
|
316
|
+
return false;
|
|
317
|
+
}
|
|
318
|
+
session.exitListeners.add(listener);
|
|
319
|
+
return true;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Remove exit listener
|
|
324
|
+
*/
|
|
325
|
+
removeExitListener(
|
|
326
|
+
sessionId: string,
|
|
327
|
+
listener: (event: { exitCode: number; signal?: number | string }) => void
|
|
328
|
+
): boolean {
|
|
329
|
+
const session = this.sessions.get(sessionId);
|
|
330
|
+
if (!session) {
|
|
331
|
+
return false;
|
|
332
|
+
}
|
|
333
|
+
return session.exitListeners.delete(listener);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Get all active sessions
|
|
338
|
+
*/
|
|
339
|
+
getAllSessions(): PtySession[] {
|
|
340
|
+
return Array.from(this.sessions.values());
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Start cleanup interval
|
|
345
|
+
*/
|
|
346
|
+
private startCleanupInterval() {
|
|
347
|
+
// Run every 15 minutes
|
|
348
|
+
this.cleanupInterval = setInterval(() => {
|
|
349
|
+
this.cleanupInactiveSessions();
|
|
350
|
+
}, 15 * 60 * 1000);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Cleanup inactive sessions (>1 hour no activity)
|
|
355
|
+
*/
|
|
356
|
+
private cleanupInactiveSessions() {
|
|
357
|
+
const now = new Date();
|
|
358
|
+
const oneHourAgo = new Date(now.getTime() - 60 * 60 * 1000);
|
|
359
|
+
|
|
360
|
+
for (const [sessionId, session] of this.sessions.entries()) {
|
|
361
|
+
if (session.lastActivityAt < oneHourAgo) {
|
|
362
|
+
debug.log('terminal', `🧹 Cleaning up inactive session: ${sessionId}`);
|
|
363
|
+
this.killSession(sessionId);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* Cleanup all sessions (on shutdown)
|
|
370
|
+
*/
|
|
371
|
+
dispose() {
|
|
372
|
+
debug.log('terminal', '🧹 Disposing all PTY sessions');
|
|
373
|
+
|
|
374
|
+
if (this.cleanupInterval) {
|
|
375
|
+
clearInterval(this.cleanupInterval);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
for (const sessionId of this.sessions.keys()) {
|
|
379
|
+
this.killSession(sessionId, 'SIGKILL');
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
this.sessions.clear();
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// Export singleton instance
|
|
387
|
+
export const ptySessionManager = new PtySessionManager();
|
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shell utilities optimized for bun-pty
|
|
3
|
+
* Provides cross-platform shell detection and PTY creation
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { join } from 'path';
|
|
7
|
+
import { spawn, type IPty } from 'bun-pty';
|
|
8
|
+
|
|
9
|
+
import { debug } from '$shared/utils/logger';
|
|
10
|
+
// Platform detection
|
|
11
|
+
export const isWindows = process.platform === 'win32';
|
|
12
|
+
export const isMacOS = process.platform === 'darwin';
|
|
13
|
+
export const isLinux = process.platform === 'linux';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Find Git Bash executable on Windows
|
|
17
|
+
*/
|
|
18
|
+
export async function findGitBash(): Promise<string | null> {
|
|
19
|
+
if (!isWindows) return null;
|
|
20
|
+
|
|
21
|
+
const possiblePaths = [
|
|
22
|
+
'C:\\Program Files\\Git\\bin\\bash.exe',
|
|
23
|
+
'C:\\Program Files (x86)\\Git\\bin\\bash.exe',
|
|
24
|
+
process.env.PROGRAMFILES ? join(process.env.PROGRAMFILES, 'Git', 'bin', 'bash.exe') : null,
|
|
25
|
+
process.env.LOCALAPPDATA ? join(process.env.LOCALAPPDATA, 'Programs', 'Git', 'bin', 'bash.exe') : null,
|
|
26
|
+
].filter(Boolean) as string[];
|
|
27
|
+
|
|
28
|
+
for (const bashPath of possiblePaths) {
|
|
29
|
+
try {
|
|
30
|
+
if (await Bun.file(bashPath).exists()) {
|
|
31
|
+
debug.log('terminal', '✅ Found Git Bash at:', bashPath);
|
|
32
|
+
return bashPath;
|
|
33
|
+
}
|
|
34
|
+
} catch {
|
|
35
|
+
// Continue to next path
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Find PowerShell executable on Windows
|
|
44
|
+
*/
|
|
45
|
+
export async function findPowerShell(): Promise<string | null> {
|
|
46
|
+
if (!isWindows) return null;
|
|
47
|
+
|
|
48
|
+
const possiblePaths = [
|
|
49
|
+
'C:\\Program Files\\PowerShell\\7\\pwsh.exe',
|
|
50
|
+
'C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe',
|
|
51
|
+
process.env.SYSTEMROOT ? join(process.env.SYSTEMROOT, 'System32', 'WindowsPowerShell', 'v1.0', 'powershell.exe') : null,
|
|
52
|
+
].filter(Boolean) as string[];
|
|
53
|
+
|
|
54
|
+
for (const pwshPath of possiblePaths) {
|
|
55
|
+
try {
|
|
56
|
+
if (await Bun.file(pwshPath).exists()) {
|
|
57
|
+
debug.log('terminal', '✅ Found PowerShell at:', pwshPath);
|
|
58
|
+
return pwshPath;
|
|
59
|
+
}
|
|
60
|
+
} catch {
|
|
61
|
+
// Continue to next path
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Check if Git Bash is available
|
|
70
|
+
*/
|
|
71
|
+
export async function isGitBashAvailable(): Promise<boolean> {
|
|
72
|
+
if (!isWindows) return false;
|
|
73
|
+
return (await findGitBash()) !== null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Get Git Bash required message
|
|
78
|
+
*/
|
|
79
|
+
export function getGitBashRequiredMessage(command: string): string {
|
|
80
|
+
return `⚠️ Git Bash is required for Unix commands!
|
|
81
|
+
|
|
82
|
+
The command '${command}' requires Git Bash.
|
|
83
|
+
|
|
84
|
+
Please install Git for Windows:
|
|
85
|
+
• Download from: https://git-scm.com/download/win
|
|
86
|
+
• Or install via: winget install Git.Git
|
|
87
|
+
|
|
88
|
+
After installation, restart the application.`;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Get the appropriate shell configuration for the current platform
|
|
93
|
+
*/
|
|
94
|
+
export async function getShellConfig(preferGitBash = false): Promise<{
|
|
95
|
+
shell: string;
|
|
96
|
+
args: (command: string) => string[];
|
|
97
|
+
name: string;
|
|
98
|
+
isUnixLike: boolean;
|
|
99
|
+
}> {
|
|
100
|
+
if (isWindows) {
|
|
101
|
+
// For Windows, always use PowerShell as the primary shell
|
|
102
|
+
// PowerShell is available on all modern Windows systems
|
|
103
|
+
return {
|
|
104
|
+
shell: 'powershell',
|
|
105
|
+
args: (command: string) => ['-NoProfile', '-Command', command],
|
|
106
|
+
name: 'PowerShell',
|
|
107
|
+
isUnixLike: false
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// macOS
|
|
112
|
+
if (isMacOS) {
|
|
113
|
+
if (await Bun.file('/bin/zsh').exists()) {
|
|
114
|
+
return {
|
|
115
|
+
shell: '/bin/zsh',
|
|
116
|
+
args: (command: string) => ['-c', command],
|
|
117
|
+
name: 'Zsh',
|
|
118
|
+
isUnixLike: true
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
return {
|
|
122
|
+
shell: '/bin/bash',
|
|
123
|
+
args: (command: string) => ['-c', command],
|
|
124
|
+
name: 'Bash',
|
|
125
|
+
isUnixLike: true
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Linux
|
|
130
|
+
if (isLinux) {
|
|
131
|
+
const userShell = process.env.SHELL || '/bin/bash';
|
|
132
|
+
const shellName = userShell.split('/').pop() || 'Shell';
|
|
133
|
+
return {
|
|
134
|
+
shell: userShell,
|
|
135
|
+
args: (command: string) => ['-c', command],
|
|
136
|
+
name: shellName.charAt(0).toUpperCase() + shellName.slice(1),
|
|
137
|
+
isUnixLike: true
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Fallback
|
|
142
|
+
return {
|
|
143
|
+
shell: '/bin/sh',
|
|
144
|
+
args: (command: string) => ['-c', command],
|
|
145
|
+
name: 'Shell',
|
|
146
|
+
isUnixLike: true
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Create a PTY instance using bun-pty with xterm.js optimizations
|
|
152
|
+
*/
|
|
153
|
+
export function createPty(shell: string, args: string[], cwd: string, terminalSize?: { cols: number; rows: number }): IPty {
|
|
154
|
+
// Prepare environment
|
|
155
|
+
const ptyEnv: Record<string, string> = {};
|
|
156
|
+
|
|
157
|
+
// Copy defined environment variables
|
|
158
|
+
for (const [key, value] of Object.entries(process.env)) {
|
|
159
|
+
if (value !== undefined) {
|
|
160
|
+
ptyEnv[key] = value;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Get terminal dimensions from frontend or use defaults
|
|
165
|
+
const cols = terminalSize?.cols || 80;
|
|
166
|
+
const rows = terminalSize?.rows || 24;
|
|
167
|
+
|
|
168
|
+
// Add terminal-specific environment variables optimized for xterm.js
|
|
169
|
+
Object.assign(ptyEnv, {
|
|
170
|
+
FORCE_COLOR: '1',
|
|
171
|
+
COLORTERM: 'truecolor',
|
|
172
|
+
TERM: 'xterm-256color',
|
|
173
|
+
COLUMNS: cols.toString(),
|
|
174
|
+
LINES: rows.toString(),
|
|
175
|
+
// Enable proper ANSI support
|
|
176
|
+
TERM_PROGRAM: 'xterm.js',
|
|
177
|
+
// Optimize for interactive terminal
|
|
178
|
+
CLICOLOR: '1',
|
|
179
|
+
// Enable Unicode support
|
|
180
|
+
LC_ALL: 'en_US.UTF-8',
|
|
181
|
+
LANG: 'en_US.UTF-8'
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
// Extract the actual command from args
|
|
185
|
+
const actualCommand: string = '';
|
|
186
|
+
|
|
187
|
+
if (isWindows) {
|
|
188
|
+
// Windows: Always use PowerShell
|
|
189
|
+
if (shell === 'powershell') {
|
|
190
|
+
// Extract the actual command from args
|
|
191
|
+
let actualCommand = args.join(' ');
|
|
192
|
+
if (args.length >= 2 && (args[0] === '-Command' || args[0] === '-NoProfile')) {
|
|
193
|
+
// Find the actual command in the args
|
|
194
|
+
const commandIndex = args.findIndex(arg => arg === '-Command');
|
|
195
|
+
if (commandIndex !== -1 && commandIndex + 1 < args.length) {
|
|
196
|
+
actualCommand = args[commandIndex + 1];
|
|
197
|
+
} else if (args[args.length - 1] !== '-Command' && args[args.length - 1] !== '-NoProfile') {
|
|
198
|
+
actualCommand = args[args.length - 1];
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
return spawn('powershell', ['-NoProfile', '-NoLogo', '-Command', actualCommand], {
|
|
202
|
+
name: 'xterm-256color',
|
|
203
|
+
cols,
|
|
204
|
+
rows,
|
|
205
|
+
cwd: cwd,
|
|
206
|
+
env: ptyEnv
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Default to PowerShell if shell is not recognized
|
|
211
|
+
return spawn('powershell', ['-NoProfile', '-NoLogo', '-Command', args.join(' ') || 'Write-Host "Terminal ready"'], {
|
|
212
|
+
name: 'xterm-256color',
|
|
213
|
+
cols,
|
|
214
|
+
rows,
|
|
215
|
+
cwd: cwd,
|
|
216
|
+
env: ptyEnv
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Unix-like systems
|
|
221
|
+
return spawn(shell, args, {
|
|
222
|
+
name: 'xterm-256color',
|
|
223
|
+
cols,
|
|
224
|
+
rows,
|
|
225
|
+
cwd: cwd,
|
|
226
|
+
env: ptyEnv
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Create a PTY wrapper for process-manager compatibility with xterm.js support
|
|
232
|
+
*/
|
|
233
|
+
export function createPtyWrapper(pty: IPty, sessionId?: string): any {
|
|
234
|
+
let isKilled = false;
|
|
235
|
+
|
|
236
|
+
return {
|
|
237
|
+
pid: pty.pid,
|
|
238
|
+
pty: pty, // Expose the original PTY instance
|
|
239
|
+
|
|
240
|
+
// Expose PTY event handlers
|
|
241
|
+
onData: (callback: (data: string) => void) => {
|
|
242
|
+
return pty.onData(callback);
|
|
243
|
+
},
|
|
244
|
+
|
|
245
|
+
onExit: (callback: (event: { exitCode: number; signal?: number | string }) => void) => {
|
|
246
|
+
return pty.onExit((event) => {
|
|
247
|
+
isKilled = true;
|
|
248
|
+
debug.log('terminal', `🏁 PTY ${pty.pid} exited with code:`, event.exitCode);
|
|
249
|
+
callback({ exitCode: event.exitCode, signal: event.signal });
|
|
250
|
+
});
|
|
251
|
+
},
|
|
252
|
+
|
|
253
|
+
resize: (cols: number, rows: number) => {
|
|
254
|
+
try {
|
|
255
|
+
debug.log('terminal', `🔧 Resizing PTY ${pty.pid} to ${cols}x${rows}`);
|
|
256
|
+
pty.resize(cols, rows);
|
|
257
|
+
debug.log('terminal', `✅ PTY ${pty.pid} resized successfully`);
|
|
258
|
+
} catch (error) {
|
|
259
|
+
debug.error('terminal', `❌ Failed to resize PTY ${pty.pid}:`, error);
|
|
260
|
+
throw error;
|
|
261
|
+
}
|
|
262
|
+
},
|
|
263
|
+
|
|
264
|
+
write: (data: string) => {
|
|
265
|
+
return pty.write(data);
|
|
266
|
+
},
|
|
267
|
+
|
|
268
|
+
kill: (signal?: number | string) => {
|
|
269
|
+
if (isKilled) {
|
|
270
|
+
debug.log('terminal', '⚠️ PTY already killed, ignoring kill signal');
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
try {
|
|
275
|
+
isKilled = true;
|
|
276
|
+
debug.log('terminal', `💀 Killing PTY ${pty.pid} with signal:`, signal);
|
|
277
|
+
|
|
278
|
+
if (signal === 'SIGKILL' || signal === 9) {
|
|
279
|
+
pty.kill('SIGKILL');
|
|
280
|
+
} else if (signal === 'SIGTERM' || signal === 15) {
|
|
281
|
+
pty.kill('SIGTERM');
|
|
282
|
+
} else {
|
|
283
|
+
// Send Ctrl+C for graceful termination, then SIGKILL as fallback
|
|
284
|
+
debug.log('terminal', '⌨️ Sending Ctrl+C to PTY...');
|
|
285
|
+
pty.write('\x03');
|
|
286
|
+
|
|
287
|
+
// Give the process time to handle Ctrl+C gracefully
|
|
288
|
+
setTimeout(() => {
|
|
289
|
+
try {
|
|
290
|
+
if (pty.pid && !isKilled) {
|
|
291
|
+
debug.log('terminal', '⏰ Graceful termination timeout, sending SIGKILL...');
|
|
292
|
+
pty.kill('SIGKILL');
|
|
293
|
+
}
|
|
294
|
+
} catch (killError) {
|
|
295
|
+
// Process might already be dead, this is expected
|
|
296
|
+
debug.log('terminal', '💀 PTY process already terminated');
|
|
297
|
+
}
|
|
298
|
+
}, 1000);
|
|
299
|
+
}
|
|
300
|
+
} catch (e) {
|
|
301
|
+
debug.log('terminal', '⚠️ Error killing PTY (this may be normal during shutdown):', e instanceof Error ? e.message : e);
|
|
302
|
+
}
|
|
303
|
+
},
|
|
304
|
+
|
|
305
|
+
exited: new Promise<number>((resolve) => {
|
|
306
|
+
pty.onExit((event) => {
|
|
307
|
+
isKilled = true;
|
|
308
|
+
debug.log('terminal', `🏁 PTY ${pty.pid} exited with code:`, event.exitCode);
|
|
309
|
+
resolve(event.exitCode);
|
|
310
|
+
});
|
|
311
|
+
})
|
|
312
|
+
};
|
|
313
|
+
}
|