@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,704 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WebSocket-based Chat Service
|
|
3
|
+
*
|
|
4
|
+
* Modern WebSocket implementation for real-time chat communication
|
|
5
|
+
* Replaces the old SSE-based chat service
|
|
6
|
+
*
|
|
7
|
+
* Key features:
|
|
8
|
+
* - Sequence-based deduplication to prevent duplicate messages
|
|
9
|
+
* - Stream reconnection after browser refresh / project switch
|
|
10
|
+
* - Robust cancel that works even after refresh
|
|
11
|
+
* - Proper presence synchronization
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { appState } from '$frontend/lib/stores/core/app.svelte';
|
|
15
|
+
import { chatModelState } from '$frontend/lib/stores/ui/chat-model.svelte';
|
|
16
|
+
import { projectState } from '$frontend/lib/stores/core/projects.svelte';
|
|
17
|
+
import { sessionState, setCurrentSession, createSession, updateSession } from '$frontend/lib/stores/core/sessions.svelte';
|
|
18
|
+
import { addNotification } from '$frontend/lib/stores/ui/notification.svelte';
|
|
19
|
+
import { userStore } from '$frontend/lib/stores/features/user.svelte';
|
|
20
|
+
import { SDK_CONFIG, parseModelId } from '$shared/constants/engines';
|
|
21
|
+
import type { ChatServiceOptions } from '$shared/types/messaging';
|
|
22
|
+
import { buildMetadataFromTransport } from '$shared/utils/message-formatter';
|
|
23
|
+
import { debug } from '$shared/utils/logger';
|
|
24
|
+
import ws from '$frontend/lib/utils/ws';
|
|
25
|
+
|
|
26
|
+
class ChatService {
|
|
27
|
+
private activeProcessId: string | null = null;
|
|
28
|
+
private streamCompleted: boolean = false;
|
|
29
|
+
private currentSessionId: string | null = null;
|
|
30
|
+
private lastEventSeq = new Map<string, number>(); // Sequence-based deduplication
|
|
31
|
+
private cancelledProcessIds = new Set<string>(); // Track ALL cancelled streams to ignore late events
|
|
32
|
+
private reconnected: boolean = false; // Whether we've reconnected to an active stream
|
|
33
|
+
|
|
34
|
+
static loadingTexts: string[] = [
|
|
35
|
+
'thinking', 'processing', 'analyzing', 'calculating', 'computing',
|
|
36
|
+
'strategizing', 'learningpatterns', 'updatingweights', 'finetuning',
|
|
37
|
+
'adaptingmodels', 'trainingnetworks', 'evaluatingoptions', 'planningactions',
|
|
38
|
+
'executingplans', 'simulatingscenarios', 'predictingoutcomes', 'scanningenvironment',
|
|
39
|
+
'monitoringsignals', 'processinginputs', 'adjustingparameters', 'optimizing',
|
|
40
|
+
'generatingresponses', 'refininglogic', 'recognizingpatterns', 'synthesizinginformation',
|
|
41
|
+
'runninginference', 'validatingoutputs', 'modulatingresponse', 'updatingmemory',
|
|
42
|
+
'switchingcontext', 'resolvingconflicts', 'allocatingresources', 'prioritizingtasks',
|
|
43
|
+
'developingawareness', 'buildingstrategies', 'assessingscenarios', 'integratingdata',
|
|
44
|
+
'bootingreasoning', 'activatingmodules', 'triggeringaction', 'deployinglogic',
|
|
45
|
+
'maintainingstate', 'clearingcache', 'updating', 'reflecting', 'syncinglogic',
|
|
46
|
+
'connectingdots', 'compilingideas', 'brainstorming', 'schedulingtasks'
|
|
47
|
+
].map(text => text + '...');
|
|
48
|
+
|
|
49
|
+
static placeholderTexts: string[] = [
|
|
50
|
+
// Creating new projects
|
|
51
|
+
'Create a full-stack e-commerce platform with Next.js, Stripe, and PostgreSQL',
|
|
52
|
+
'Build a real-time chat application using Socket.io with room support and typing indicators',
|
|
53
|
+
'Create a SaaS dashboard with user management, billing, and analytics',
|
|
54
|
+
// ... (keep the same placeholder texts as before)
|
|
55
|
+
];
|
|
56
|
+
|
|
57
|
+
constructor() {
|
|
58
|
+
this.setupWebSocketHandlers();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Check if event should be skipped (sequence-based deduplication)
|
|
63
|
+
*/
|
|
64
|
+
private shouldSkipEvent(processId: string, seq: number | undefined): boolean {
|
|
65
|
+
if (seq === undefined || seq === null) return false;
|
|
66
|
+
|
|
67
|
+
const lastSeq = this.lastEventSeq.get(processId) || 0;
|
|
68
|
+
if (seq <= lastSeq) {
|
|
69
|
+
// Skip duplicate
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
this.lastEventSeq.set(processId, seq);
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Setup WebSocket event handlers
|
|
79
|
+
*/
|
|
80
|
+
private setupWebSocketHandlers(): void {
|
|
81
|
+
// Session available event - reset stream state if we were streaming from the old session.
|
|
82
|
+
// With session-scoped routing, this is mostly a safety measure.
|
|
83
|
+
ws.on('sessions:session-available', () => {
|
|
84
|
+
// No-op: with chat session rooms, events are already scoped.
|
|
85
|
+
// Users stay in their current session until they explicitly switch.
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// Connection event - received by ALL users in the project
|
|
89
|
+
ws.on('chat:connection', (data) => {
|
|
90
|
+
if (this.shouldSkipEvent(data.processId, data.seq)) return;
|
|
91
|
+
// Ignore events from a locally cancelled stream
|
|
92
|
+
if (data.processId && this.cancelledProcessIds.has(data.processId)) return;
|
|
93
|
+
|
|
94
|
+
this.activeProcessId = data.processId;
|
|
95
|
+
this.streamCompleted = false;
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
// Message event
|
|
99
|
+
ws.on('chat:message', (data) => {
|
|
100
|
+
if (this.shouldSkipEvent(data.processId, data.seq)) return;
|
|
101
|
+
// Ignore events from a locally cancelled stream
|
|
102
|
+
if (data.processId && this.cancelledProcessIds.has(data.processId)) return;
|
|
103
|
+
|
|
104
|
+
this.handleMessageEvent(data);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
// Partial message event (streaming)
|
|
108
|
+
ws.on('chat:partial', (data) => {
|
|
109
|
+
if (this.shouldSkipEvent(data.processId, data.seq)) return;
|
|
110
|
+
// Ignore events from a locally cancelled stream
|
|
111
|
+
if (data.processId && this.cancelledProcessIds.has(data.processId)) return;
|
|
112
|
+
|
|
113
|
+
this.handlePartialEvent(data);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
// Notification event
|
|
117
|
+
ws.on('chat:notification', (data) => {
|
|
118
|
+
// Notifications don't have processId, use a global key
|
|
119
|
+
if (this.shouldSkipEvent('notification', data.seq)) return;
|
|
120
|
+
|
|
121
|
+
if (data.notification) {
|
|
122
|
+
const notif = data.notification;
|
|
123
|
+
addNotification({
|
|
124
|
+
type: notif.type as any,
|
|
125
|
+
title: notif.title,
|
|
126
|
+
message: notif.message,
|
|
127
|
+
duration: notif.type === 'warning' ? 7000 : 5000
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
// Complete event
|
|
133
|
+
ws.on('chat:complete', async (data) => {
|
|
134
|
+
if (this.shouldSkipEvent(data.processId, data.seq)) return;
|
|
135
|
+
// Ignore late events from a locally cancelled stream
|
|
136
|
+
if (data.processId && this.cancelledProcessIds.has(data.processId)) return;
|
|
137
|
+
|
|
138
|
+
this.streamCompleted = true;
|
|
139
|
+
this.reconnected = false;
|
|
140
|
+
appState.isLoading = false;
|
|
141
|
+
appState.isCancelling = false;
|
|
142
|
+
|
|
143
|
+
// Stream completed successfully — all old cancelled streams' events
|
|
144
|
+
// have definitely been delivered by now, so clear the blacklist.
|
|
145
|
+
this.cancelledProcessIds.clear();
|
|
146
|
+
|
|
147
|
+
// Don't reload messages - they're already added via chat:message events
|
|
148
|
+
// Reloading would cause duplicates
|
|
149
|
+
|
|
150
|
+
// Notifications handled by GlobalStreamMonitor via chat:stream-finished
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
// Cancelled event - broadcast to ALL collaborators when any user cancels
|
|
154
|
+
ws.on('chat:cancelled', async (data) => {
|
|
155
|
+
// Track the cancelled processId so late-arriving events are blocked.
|
|
156
|
+
// This handles the case where a collaborator initiated the cancel
|
|
157
|
+
// (so our local cancelRequest was not called).
|
|
158
|
+
if (data.processId) {
|
|
159
|
+
this.cancelledProcessIds.add(data.processId);
|
|
160
|
+
}
|
|
161
|
+
this.streamCompleted = true;
|
|
162
|
+
this.reconnected = false;
|
|
163
|
+
this.activeProcessId = null;
|
|
164
|
+
appState.isLoading = false;
|
|
165
|
+
appState.isCancelling = false;
|
|
166
|
+
|
|
167
|
+
// Notifications handled by GlobalStreamMonitor via chat:stream-finished
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
// Error event
|
|
171
|
+
ws.on('chat:error', async (data) => {
|
|
172
|
+
if (this.shouldSkipEvent(data.processId, data.seq)) return;
|
|
173
|
+
if (this.streamCompleted) return;
|
|
174
|
+
// Ignore late error events from a locally cancelled stream
|
|
175
|
+
if (data.processId && this.cancelledProcessIds.has(data.processId)) return;
|
|
176
|
+
|
|
177
|
+
// Mark completed immediately to block any duplicate error events that may arrive
|
|
178
|
+
// (e.g. from multiple subscriptions or late-arriving events with different processId/seq)
|
|
179
|
+
this.streamCompleted = true;
|
|
180
|
+
this.reconnected = false;
|
|
181
|
+
appState.isLoading = false;
|
|
182
|
+
appState.isCancelling = false;
|
|
183
|
+
|
|
184
|
+
// Don't show notification for cancel-triggered errors
|
|
185
|
+
if (data.error === 'Stream cancelled') return;
|
|
186
|
+
|
|
187
|
+
// Remove any remaining stream_event messages (streaming placeholders that won't be finalized).
|
|
188
|
+
// The actual error bubble is now emitted as a chat:message from the backend and saved to DB,
|
|
189
|
+
// so it persists across browser refresh. No need to inject a synthetic bubble here.
|
|
190
|
+
for (let i = sessionState.messages.length - 1; i >= 0; i--) {
|
|
191
|
+
const msg = sessionState.messages[i] as any;
|
|
192
|
+
if (msg.type === 'stream_event') {
|
|
193
|
+
sessionState.messages.splice(i, 1);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
addNotification({
|
|
198
|
+
type: 'error',
|
|
199
|
+
title: 'AI Engine Error',
|
|
200
|
+
message: data.error,
|
|
201
|
+
duration: 5000
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Reconnect to an active stream after browser refresh or project switch.
|
|
208
|
+
* This re-subscribes the connection to receive live stream events.
|
|
209
|
+
* Called from catchupActiveStream in ChatInput.
|
|
210
|
+
*/
|
|
211
|
+
reconnectToStream(chatSessionId: string, processId: string): void {
|
|
212
|
+
debug.log('chat', 'Reconnecting to active stream:', { chatSessionId, processId });
|
|
213
|
+
|
|
214
|
+
// Set up local state so events are processed and cancel works
|
|
215
|
+
this.activeProcessId = processId;
|
|
216
|
+
this.currentSessionId = chatSessionId;
|
|
217
|
+
this.streamCompleted = false;
|
|
218
|
+
this.cancelledProcessIds.clear();
|
|
219
|
+
this.reconnected = true;
|
|
220
|
+
|
|
221
|
+
// Tell backend to re-subscribe this connection to the stream
|
|
222
|
+
ws.emit('chat:reconnect', {
|
|
223
|
+
chatSessionId
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Send a message using WebSocket
|
|
229
|
+
*/
|
|
230
|
+
async sendMessage(
|
|
231
|
+
message: string,
|
|
232
|
+
options: ChatServiceOptions = {}
|
|
233
|
+
): Promise<void> {
|
|
234
|
+
if ((!message.trim() && !options.attachedFiles?.length) || appState.isLoading) return;
|
|
235
|
+
|
|
236
|
+
// Check if project is selected
|
|
237
|
+
if (!projectState.currentProject) {
|
|
238
|
+
addNotification({
|
|
239
|
+
type: 'warning',
|
|
240
|
+
title: 'No Project Selected',
|
|
241
|
+
message: 'Please select a project from the sidebar before sending messages',
|
|
242
|
+
duration: 3000
|
|
243
|
+
});
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const userMessage = message.trim();
|
|
248
|
+
|
|
249
|
+
// Create a new session if none exists
|
|
250
|
+
if (!sessionState.currentSession) {
|
|
251
|
+
const newSession = await createSession(
|
|
252
|
+
projectState.currentProject.id,
|
|
253
|
+
'New Chat Session'
|
|
254
|
+
);
|
|
255
|
+
if (newSession) {
|
|
256
|
+
await setCurrentSession(newSession);
|
|
257
|
+
} else {
|
|
258
|
+
addNotification({
|
|
259
|
+
type: 'error',
|
|
260
|
+
title: 'Session Creation Failed',
|
|
261
|
+
message: 'Failed to create chat session. Please try again.',
|
|
262
|
+
duration: 5000
|
|
263
|
+
});
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Ensure we have a valid session before proceeding
|
|
269
|
+
if (!sessionState.currentSession?.id) {
|
|
270
|
+
addNotification({
|
|
271
|
+
type: 'error',
|
|
272
|
+
title: 'No Valid Session',
|
|
273
|
+
message: 'No valid chat session available. Please refresh and try again.',
|
|
274
|
+
duration: 5000
|
|
275
|
+
});
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Set loading state
|
|
280
|
+
appState.isLoading = true;
|
|
281
|
+
appState.isCancelling = false;
|
|
282
|
+
this.streamCompleted = false;
|
|
283
|
+
this.reconnected = false;
|
|
284
|
+
this.currentSessionId = sessionState.currentSession.id;
|
|
285
|
+
// DON'T clear cancelledProcessIds — late events from previously cancelled
|
|
286
|
+
// streams must still be blocked. The set is cleared on stream complete.
|
|
287
|
+
// Clear sequence tracking for new stream
|
|
288
|
+
this.lastEventSeq.clear();
|
|
289
|
+
|
|
290
|
+
// Clean up stale stream_events from any previous cancelled streams.
|
|
291
|
+
// These linger because cancel doesn't remove them, and they cause
|
|
292
|
+
// wrong insertion positions for new reasoning/text streams.
|
|
293
|
+
this.cleanupStreamEvents();
|
|
294
|
+
|
|
295
|
+
try {
|
|
296
|
+
// Build message content (text + optional file attachments)
|
|
297
|
+
let messageContent: any = userMessage;
|
|
298
|
+
if (options.attachedFiles && options.attachedFiles.length > 0) {
|
|
299
|
+
const contentBlocks: any[] = [];
|
|
300
|
+
// Add file attachments first
|
|
301
|
+
for (const file of options.attachedFiles) {
|
|
302
|
+
if (file.type === 'image') {
|
|
303
|
+
contentBlocks.push({
|
|
304
|
+
type: 'image',
|
|
305
|
+
source: { type: 'base64', media_type: file.mediaType, data: file.data }
|
|
306
|
+
});
|
|
307
|
+
} else {
|
|
308
|
+
contentBlocks.push({
|
|
309
|
+
type: 'document',
|
|
310
|
+
source: { type: 'base64', media_type: file.mediaType, data: file.data },
|
|
311
|
+
title: file.fileName
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
// Add text block
|
|
316
|
+
if (userMessage) {
|
|
317
|
+
contentBlocks.push({ type: 'text', text: userMessage });
|
|
318
|
+
}
|
|
319
|
+
messageContent = contentBlocks;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// Create SDKUserMessage format for prompt
|
|
323
|
+
const sdkUserMessage = {
|
|
324
|
+
type: 'user' as const,
|
|
325
|
+
uuid: crypto.randomUUID(),
|
|
326
|
+
session_id: sessionState.currentSession.id,
|
|
327
|
+
parent_tool_use_id: null,
|
|
328
|
+
message: {
|
|
329
|
+
role: 'user' as const,
|
|
330
|
+
content: messageContent
|
|
331
|
+
}
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
// Optimistic UI: show user message immediately (before server confirms)
|
|
335
|
+
const optimisticMessage = {
|
|
336
|
+
...sdkUserMessage,
|
|
337
|
+
_optimistic: true,
|
|
338
|
+
metadata: buildMetadataFromTransport({
|
|
339
|
+
timestamp: new Date().toISOString(),
|
|
340
|
+
sender_id: userStore.currentUser?.id || null,
|
|
341
|
+
sender_name: userStore.currentUser?.name || null,
|
|
342
|
+
})
|
|
343
|
+
};
|
|
344
|
+
(sessionState.messages as any[]).push(optimisticMessage);
|
|
345
|
+
|
|
346
|
+
// Parse engine and model from the local chat model state (isolated from Settings)
|
|
347
|
+
const { engine, modelId } = parseModelId(chatModelState.model);
|
|
348
|
+
|
|
349
|
+
// Capture selected engine/model/account before sending
|
|
350
|
+
const selectedEngine = chatModelState.engine || engine;
|
|
351
|
+
const selectedModel = chatModelState.model;
|
|
352
|
+
const selectedAccountId = chatModelState.claudeAccountId;
|
|
353
|
+
|
|
354
|
+
// Send WebSocket message to start streaming
|
|
355
|
+
ws.emit('chat:stream', {
|
|
356
|
+
sessionId: crypto.randomUUID(), // ephemeral session ID for this stream
|
|
357
|
+
chatSessionId: sessionState.currentSession.id,
|
|
358
|
+
projectPath: projectState.currentProject?.path || '',
|
|
359
|
+
prompt: sdkUserMessage,
|
|
360
|
+
messages: sessionState.messages.filter((msg: any) => !msg._optimistic).map(msg => {
|
|
361
|
+
// Convert SDKMessage to API format
|
|
362
|
+
if (msg.type === 'user' && 'message' in msg) {
|
|
363
|
+
return {
|
|
364
|
+
role: msg.message.role,
|
|
365
|
+
content: typeof msg.message.content === 'string' ? msg.message.content : JSON.stringify(msg.message.content)
|
|
366
|
+
};
|
|
367
|
+
} else if (msg.type === 'assistant' && 'message' in msg) {
|
|
368
|
+
return {
|
|
369
|
+
role: msg.message.role,
|
|
370
|
+
content: Array.isArray(msg.message.content)
|
|
371
|
+
? msg.message.content.map(c => c.type === 'text' ? c.text : JSON.stringify(c)).join(' ')
|
|
372
|
+
: JSON.stringify(msg.message.content)
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
return {
|
|
376
|
+
role: 'assistant',
|
|
377
|
+
content: ''
|
|
378
|
+
};
|
|
379
|
+
}).filter(msg => msg.content),
|
|
380
|
+
engine: selectedEngine,
|
|
381
|
+
model: modelId,
|
|
382
|
+
temperature: SDK_CONFIG.DEFAULT_TEMPERATURE,
|
|
383
|
+
senderId: userStore.currentUser?.id,
|
|
384
|
+
senderName: userStore.currentUser?.name,
|
|
385
|
+
...(selectedEngine === 'claude-code' && selectedAccountId !== null && { claudeAccountId: selectedAccountId }),
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
// Persist engine/model to frontend session state immediately.
|
|
389
|
+
// Backend also saves to DB (for refresh/project-switch restore),
|
|
390
|
+
// but we update the frontend state here so the $effect in
|
|
391
|
+
// EngineModelPicker can see it without a server round-trip.
|
|
392
|
+
// IMPORTANT: Use updateSession() to update BOTH sessionState.currentSession
|
|
393
|
+
// AND sessionState.sessions[] array. A direct spread on currentSession
|
|
394
|
+
// creates a new object, leaving sessions[] stale — causing the model
|
|
395
|
+
// picker to lose the selection when switching projects and back.
|
|
396
|
+
if (sessionState.currentSession) {
|
|
397
|
+
updateSession({
|
|
398
|
+
...sessionState.currentSession,
|
|
399
|
+
engine: selectedEngine,
|
|
400
|
+
model: selectedModel,
|
|
401
|
+
...(selectedEngine === 'claude-code' && selectedAccountId !== null && { claude_account_id: selectedAccountId }),
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
} catch (error) {
|
|
406
|
+
this.handleError(error as Error, options);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* Cancel the current request - works for ANY collaborator, not just the sender
|
|
412
|
+
* Also works after browser refresh since it uses sessionState as fallback
|
|
413
|
+
*/
|
|
414
|
+
cancelRequest(): void {
|
|
415
|
+
// Use currentSessionId (set by sender) OR sessionState (available to all collaborators)
|
|
416
|
+
const chatSessionId = this.currentSessionId || sessionState.currentSession?.id;
|
|
417
|
+
|
|
418
|
+
if (chatSessionId) {
|
|
419
|
+
ws.emit('chat:cancel', {
|
|
420
|
+
sessionId: crypto.randomUUID(),
|
|
421
|
+
chatSessionId
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// Track cancelled processId so late-arriving events are ignored.
|
|
426
|
+
// Use a Set to track ALL cancelled streams (not just the last one),
|
|
427
|
+
// preventing late events from any previously cancelled stream from leaking through.
|
|
428
|
+
if (this.activeProcessId) {
|
|
429
|
+
this.cancelledProcessIds.add(this.activeProcessId);
|
|
430
|
+
}
|
|
431
|
+
this.activeProcessId = null;
|
|
432
|
+
this.currentSessionId = null;
|
|
433
|
+
this.streamCompleted = true;
|
|
434
|
+
this.reconnected = false;
|
|
435
|
+
appState.isLoading = false;
|
|
436
|
+
// Prevent presence effect from re-enabling loading before server confirms cancel
|
|
437
|
+
appState.isCancelling = true;
|
|
438
|
+
|
|
439
|
+
// Clean up stale stream_events from the cancelled stream.
|
|
440
|
+
// Without this, stale stream_events remain in the messages array and cause
|
|
441
|
+
// wrong insertion positions when a new stream starts (e.g., reasoning inserted
|
|
442
|
+
// before a stale non-reasoning stream_event instead of at the end).
|
|
443
|
+
this.cleanupStreamEvents();
|
|
444
|
+
|
|
445
|
+
// Safety timeout: clear isCancelling after 10s if WS confirmation never arrives
|
|
446
|
+
// (e.g., network issues, dropped connection)
|
|
447
|
+
setTimeout(() => {
|
|
448
|
+
if (appState.isCancelling) {
|
|
449
|
+
appState.isCancelling = false;
|
|
450
|
+
}
|
|
451
|
+
}, 10000);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* Reset stream state when switching sessions (e.g. collaborator receiving new-chat).
|
|
456
|
+
* Blocks all stale events from the old stream.
|
|
457
|
+
*/
|
|
458
|
+
resetForSessionSwitch(): void {
|
|
459
|
+
if (this.activeProcessId) {
|
|
460
|
+
this.cancelledProcessIds.add(this.activeProcessId);
|
|
461
|
+
}
|
|
462
|
+
this.activeProcessId = null;
|
|
463
|
+
this.currentSessionId = null;
|
|
464
|
+
this.streamCompleted = true;
|
|
465
|
+
this.reconnected = false;
|
|
466
|
+
this.lastEventSeq.clear();
|
|
467
|
+
appState.isLoading = false;
|
|
468
|
+
appState.isCancelling = false;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/**
|
|
472
|
+
* Handle message events from stream
|
|
473
|
+
*/
|
|
474
|
+
private handleMessageEvent(data: any): void {
|
|
475
|
+
const sdkMessage = data.message;
|
|
476
|
+
|
|
477
|
+
// Early return if no message
|
|
478
|
+
if (!sdkMessage) return;
|
|
479
|
+
|
|
480
|
+
// Ignore messages from a completed/cancelled stream
|
|
481
|
+
if (this.streamCompleted) return;
|
|
482
|
+
|
|
483
|
+
// Early return if no valid session
|
|
484
|
+
if (!sessionState.currentSession?.id) {
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// If this is a user message from server, replace the optimistic message
|
|
489
|
+
if (sdkMessage.type === 'user' && sdkMessage.message?.role === 'user') {
|
|
490
|
+
const optimisticIndex = sessionState.messages.findIndex(
|
|
491
|
+
(m: any) => m._optimistic && m.type === 'user' && m.uuid === sdkMessage.uuid
|
|
492
|
+
);
|
|
493
|
+
if (optimisticIndex !== -1) {
|
|
494
|
+
// Replace optimistic with server-confirmed message
|
|
495
|
+
const confirmedMessage = {
|
|
496
|
+
...sdkMessage,
|
|
497
|
+
metadata: buildMetadataFromTransport(data)
|
|
498
|
+
};
|
|
499
|
+
sessionState.messages[optimisticIndex] = confirmedMessage;
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// If this is an assistant message, replace the matching streaming message
|
|
505
|
+
if (sdkMessage.message?.role === 'assistant') {
|
|
506
|
+
const isReasoning = sdkMessage.metadata?.reasoning === true;
|
|
507
|
+
if (isReasoning) {
|
|
508
|
+
// Replace reasoning stream_event IN PLACE to preserve message order
|
|
509
|
+
for (let i = sessionState.messages.length - 1; i >= 0; i--) {
|
|
510
|
+
const msg = sessionState.messages[i] as any;
|
|
511
|
+
if (msg.type === 'stream_event' && msg.metadata?.reasoning) {
|
|
512
|
+
const messageFormatter = {
|
|
513
|
+
...sdkMessage,
|
|
514
|
+
metadata: buildMetadataFromTransport({ ...data, reasoning: true })
|
|
515
|
+
};
|
|
516
|
+
sessionState.messages[i] = messageFormatter;
|
|
517
|
+
return; // Already replaced in-place, skip push below
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
// If no reasoning stream_event found, fall through to push at end
|
|
521
|
+
} else {
|
|
522
|
+
// Remove ALL regular (non-reasoning) stream_events, not just the last one
|
|
523
|
+
// This prevents stale stream_events from remaining when message order varies
|
|
524
|
+
for (let i = sessionState.messages.length - 1; i >= 0; i--) {
|
|
525
|
+
const msg = sessionState.messages[i] as any;
|
|
526
|
+
if (msg.type === 'stream_event' && !msg.metadata?.reasoning) {
|
|
527
|
+
sessionState.messages.splice(i, 1);
|
|
528
|
+
break; // Only remove the most recent one
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// Deduplicate: skip if a message with the same uuid already exists
|
|
535
|
+
if (sdkMessage.uuid) {
|
|
536
|
+
const alreadyExists = sessionState.messages.some(
|
|
537
|
+
(m: any) => m.uuid === sdkMessage.uuid && m.type === sdkMessage.type && !m._optimistic
|
|
538
|
+
);
|
|
539
|
+
if (alreadyExists) return;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
// Update UI state (message already saved to DB by server)
|
|
543
|
+
const isReasoning = sdkMessage.metadata?.reasoning === true;
|
|
544
|
+
const messageFormatter = {
|
|
545
|
+
...sdkMessage,
|
|
546
|
+
metadata: buildMetadataFromTransport({
|
|
547
|
+
...data,
|
|
548
|
+
...(isReasoning && { reasoning: true }),
|
|
549
|
+
})
|
|
550
|
+
};
|
|
551
|
+
|
|
552
|
+
// For reasoning messages that couldn't find a matching stream_event,
|
|
553
|
+
// insert BEFORE trailing non-reasoning assistant messages (tools/text)
|
|
554
|
+
// to preserve reasoning-before-tool ordering within the same turn.
|
|
555
|
+
if (isReasoning) {
|
|
556
|
+
let insertIdx = sessionState.messages.length;
|
|
557
|
+
for (let i = sessionState.messages.length - 1; i >= 0; i--) {
|
|
558
|
+
const msg = sessionState.messages[i] as any;
|
|
559
|
+
// Stop at user messages or other reasoning messages — they mark turn boundaries
|
|
560
|
+
if (msg.type === 'user' || (msg.type === 'assistant' && msg.metadata?.reasoning)) break;
|
|
561
|
+
// Insert before non-reasoning assistant messages and non-reasoning stream_events
|
|
562
|
+
if (msg.type === 'assistant' || (msg.type === 'stream_event' && !msg.metadata?.reasoning)) {
|
|
563
|
+
insertIdx = i;
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
(sessionState.messages as any[]).splice(insertIdx, 0, messageFormatter);
|
|
567
|
+
} else {
|
|
568
|
+
(sessionState.messages as any[]).push(messageFormatter);
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
/**
|
|
573
|
+
* Handle partial message events (streaming)
|
|
574
|
+
*/
|
|
575
|
+
private handlePartialEvent(data: any): void {
|
|
576
|
+
// Ignore partials from a completed/cancelled stream
|
|
577
|
+
if (this.streamCompleted) return;
|
|
578
|
+
|
|
579
|
+
// Early return if no valid session
|
|
580
|
+
if (!sessionState.currentSession?.id) {
|
|
581
|
+
return;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
const { eventType, partialText } = data;
|
|
585
|
+
const isReasoning = data.reasoning === true;
|
|
586
|
+
|
|
587
|
+
if (eventType === 'start') {
|
|
588
|
+
if (isReasoning) {
|
|
589
|
+
// Check if there's already a reasoning stream_event (from catchup)
|
|
590
|
+
const existingReasoning = sessionState.messages.find(
|
|
591
|
+
(m: any) => m.type === 'stream_event' && m.metadata?.reasoning && m.processId === data.processId
|
|
592
|
+
);
|
|
593
|
+
if (existingReasoning) {
|
|
594
|
+
// Already have one, just update it
|
|
595
|
+
(existingReasoning as any).partialText = partialText || '';
|
|
596
|
+
return;
|
|
597
|
+
}
|
|
598
|
+
} else {
|
|
599
|
+
// Check if there's already a regular stream_event for this process (from catchup)
|
|
600
|
+
const existingStream = sessionState.messages.find(
|
|
601
|
+
(m: any) => m.type === 'stream_event' && !m.metadata?.reasoning && m.processId === data.processId
|
|
602
|
+
);
|
|
603
|
+
if (existingStream) {
|
|
604
|
+
// Already have one, just update it
|
|
605
|
+
(existingStream as any).partialText = partialText || '';
|
|
606
|
+
return;
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
// Create new streaming message (reasoning or text)
|
|
611
|
+
const streamingMessage = {
|
|
612
|
+
type: 'stream_event' as const,
|
|
613
|
+
processId: data.processId,
|
|
614
|
+
partialText: partialText || '',
|
|
615
|
+
metadata: buildMetadataFromTransport({
|
|
616
|
+
timestamp: data.timestamp,
|
|
617
|
+
...(isReasoning && { reasoning: true }),
|
|
618
|
+
})
|
|
619
|
+
};
|
|
620
|
+
|
|
621
|
+
if (isReasoning) {
|
|
622
|
+
// Insert reasoning stream BEFORE any existing non-reasoning stream_event
|
|
623
|
+
// to preserve logical order (reasoning comes before text in the model's output)
|
|
624
|
+
const textStreamIdx = (sessionState.messages as any[]).findIndex(
|
|
625
|
+
(m: any) => m.type === 'stream_event' && !m.metadata?.reasoning
|
|
626
|
+
);
|
|
627
|
+
if (textStreamIdx >= 0) {
|
|
628
|
+
(sessionState.messages as any[]).splice(textStreamIdx, 0, streamingMessage);
|
|
629
|
+
} else {
|
|
630
|
+
(sessionState.messages as any[]).push(streamingMessage);
|
|
631
|
+
}
|
|
632
|
+
} else {
|
|
633
|
+
// Text stream always goes to the end (after any reasoning)
|
|
634
|
+
(sessionState.messages as any[]).push(streamingMessage);
|
|
635
|
+
}
|
|
636
|
+
} else if (eventType === 'update') {
|
|
637
|
+
if (isReasoning) {
|
|
638
|
+
// Update reasoning streaming message — find last reasoning stream_event
|
|
639
|
+
for (let i = sessionState.messages.length - 1; i >= 0; i--) {
|
|
640
|
+
const msg = sessionState.messages[i] as any;
|
|
641
|
+
if (msg.type === 'stream_event' && msg.metadata?.reasoning) {
|
|
642
|
+
msg.partialText = partialText || '';
|
|
643
|
+
break;
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
} else {
|
|
647
|
+
// Update regular text streaming message — find matching stream_event
|
|
648
|
+
// Search backwards to find the most recent non-reasoning stream_event
|
|
649
|
+
for (let i = sessionState.messages.length - 1; i >= 0; i--) {
|
|
650
|
+
const msg = sessionState.messages[i] as any;
|
|
651
|
+
if (msg.type === 'stream_event' && !msg.metadata?.reasoning) {
|
|
652
|
+
msg.partialText = partialText || '';
|
|
653
|
+
break;
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
// Note: 'end' event is not needed - streaming message will be replaced by final message in handleMessageEvent
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
/**
|
|
662
|
+
* Remove all stream_event messages from the messages array.
|
|
663
|
+
* Called on cancel and new message send to prevent stale streaming
|
|
664
|
+
* placeholders from causing wrong insertion positions.
|
|
665
|
+
*/
|
|
666
|
+
private cleanupStreamEvents(): void {
|
|
667
|
+
for (let i = sessionState.messages.length - 1; i >= 0; i--) {
|
|
668
|
+
if ((sessionState.messages[i] as any).type === 'stream_event') {
|
|
669
|
+
sessionState.messages.splice(i, 1);
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
/**
|
|
675
|
+
* Handle general errors
|
|
676
|
+
*/
|
|
677
|
+
private handleError(
|
|
678
|
+
error: Error,
|
|
679
|
+
options: ChatServiceOptions
|
|
680
|
+
): void {
|
|
681
|
+
let errorMessage = 'Failed to connect to AI engine';
|
|
682
|
+
if (error.message.includes('Project path')) {
|
|
683
|
+
errorMessage = error.message;
|
|
684
|
+
} else {
|
|
685
|
+
errorMessage = error.message;
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
addNotification({
|
|
689
|
+
type: 'error',
|
|
690
|
+
title: 'Chat Error',
|
|
691
|
+
message: errorMessage,
|
|
692
|
+
duration: 5000
|
|
693
|
+
});
|
|
694
|
+
|
|
695
|
+
options.onError?.(error);
|
|
696
|
+
appState.isLoading = false;
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
// Export singleton instance
|
|
701
|
+
export const chatService = new ChatService();
|
|
702
|
+
|
|
703
|
+
// Export class for static methods
|
|
704
|
+
export { ChatService };
|