@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,317 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Projects Store
|
|
3
|
+
* All project-related state and functions
|
|
4
|
+
*
|
|
5
|
+
* State persistence: Server-side via user:save-state / user:restore-state
|
|
6
|
+
* No localStorage usage - server is single source of truth
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import ws from '$frontend/lib/utils/ws';
|
|
10
|
+
import type { Project } from '$shared/types/database/schema';
|
|
11
|
+
|
|
12
|
+
import { debug } from '$shared/utils/logger';
|
|
13
|
+
interface ProjectState {
|
|
14
|
+
projects: Project[];
|
|
15
|
+
currentProject: Project | null;
|
|
16
|
+
recentProjects: Project[];
|
|
17
|
+
isLoading: boolean;
|
|
18
|
+
error: string | null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Project state using Svelte 5 runes
|
|
22
|
+
export const projectState = $state<ProjectState>({
|
|
23
|
+
projects: [],
|
|
24
|
+
currentProject: null,
|
|
25
|
+
recentProjects: [],
|
|
26
|
+
isLoading: false,
|
|
27
|
+
error: null
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
// ========================================
|
|
31
|
+
// DERIVED VALUES
|
|
32
|
+
// ========================================
|
|
33
|
+
|
|
34
|
+
export function hasProjects() {
|
|
35
|
+
return projectState.projects.length > 0;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function currentProjectName() {
|
|
39
|
+
return projectState.currentProject?.name || '';
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function currentProjectPath() {
|
|
43
|
+
return projectState.currentProject?.path || '';
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ========================================
|
|
47
|
+
// PROJECT MANAGEMENT
|
|
48
|
+
// ========================================
|
|
49
|
+
|
|
50
|
+
export async function setCurrentProject(project: Project | null) {
|
|
51
|
+
// Only clear session if we're actually switching to a different project
|
|
52
|
+
const { sessionState, setCurrentSession } = await import('./sessions.svelte');
|
|
53
|
+
const { appState } = await import('./app.svelte');
|
|
54
|
+
|
|
55
|
+
// Sync project context with WebSocket (for room-based broadcasting)
|
|
56
|
+
// IMPORTANT: Must await to ensure server has context before other operations
|
|
57
|
+
await ws.setProject(project?.id ?? null);
|
|
58
|
+
|
|
59
|
+
// Check if we're switching to a different project
|
|
60
|
+
const currentProjectId = projectState.currentProject?.id;
|
|
61
|
+
const newProjectId = project?.id;
|
|
62
|
+
|
|
63
|
+
// Handle project status tracking and terminal context switching
|
|
64
|
+
if (typeof window !== 'undefined') {
|
|
65
|
+
try {
|
|
66
|
+
// Import services dynamically to avoid circular dependency
|
|
67
|
+
const { projectStatusService } = await import('$frontend/lib/services/project');
|
|
68
|
+
const { terminalProjectManager } = await import('$frontend/lib/services/terminal');
|
|
69
|
+
|
|
70
|
+
// Stop tracking previous project if switching
|
|
71
|
+
if (currentProjectId && currentProjectId !== newProjectId) {
|
|
72
|
+
await projectStatusService.stopTracking();
|
|
73
|
+
|
|
74
|
+
// Reset loading state when switching projects
|
|
75
|
+
appState.isLoading = false;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Start tracking new project
|
|
79
|
+
if (project?.id) {
|
|
80
|
+
await projectStatusService.startTracking(project.id);
|
|
81
|
+
// Switch terminal context to the new project
|
|
82
|
+
await terminalProjectManager.switchToProject(project.id, project.path);
|
|
83
|
+
}
|
|
84
|
+
} catch (error) {
|
|
85
|
+
debug.error('project', 'Error updating project tracking:', error);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// If switching to a different project, handle session transition
|
|
90
|
+
if (currentProjectId !== newProjectId) {
|
|
91
|
+
// Set restoring flag FIRST - prevents reactive effects from syncing stale state
|
|
92
|
+
// to the new project during transition
|
|
93
|
+
if (project) {
|
|
94
|
+
appState.isRestoring = true;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Clear edit mode state from previous project (server retains per-project state)
|
|
98
|
+
const { onProjectLeave, onProjectEnter } = await import('$frontend/lib/stores/ui/edit-mode.svelte');
|
|
99
|
+
onProjectLeave();
|
|
100
|
+
|
|
101
|
+
// Clear current session when switching projects
|
|
102
|
+
await setCurrentSession(null);
|
|
103
|
+
|
|
104
|
+
// If we have a new project, try to restore or create a session for it
|
|
105
|
+
if (project) {
|
|
106
|
+
try {
|
|
107
|
+
// Import session management functions
|
|
108
|
+
const { createSession, getSessionsForProject, reloadSessionsForProject } = await import('./sessions.svelte');
|
|
109
|
+
|
|
110
|
+
// Reload all sessions for this project from server
|
|
111
|
+
// (local state may only have sessions from the previous project)
|
|
112
|
+
await reloadSessionsForProject();
|
|
113
|
+
|
|
114
|
+
// Check if there's an existing session for this project
|
|
115
|
+
const existingSessions = getSessionsForProject(project.id);
|
|
116
|
+
const activeSessions = existingSessions
|
|
117
|
+
.filter(s => !s.ended_at)
|
|
118
|
+
.sort((a, b) => new Date(b.started_at).getTime() - new Date(a.started_at).getTime());
|
|
119
|
+
const activeSession = activeSessions[0] || null;
|
|
120
|
+
|
|
121
|
+
if (activeSession) {
|
|
122
|
+
// Restore the most recent active session for this project
|
|
123
|
+
debug.log('project', 'Restoring existing session for project:', activeSession.id);
|
|
124
|
+
await setCurrentSession(activeSession);
|
|
125
|
+
} else {
|
|
126
|
+
// Create a new session for this project
|
|
127
|
+
debug.log('project', 'Creating new session for project:', project.id);
|
|
128
|
+
const newSession = await createSession(project.id, 'Chat Session', false);
|
|
129
|
+
if (newSession) {
|
|
130
|
+
await setCurrentSession(newSession);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Restore edit mode from server for the new project
|
|
135
|
+
// (ws.setProject already completed, so server returns correct project's state)
|
|
136
|
+
await onProjectEnter();
|
|
137
|
+
|
|
138
|
+
// Set projectState BEFORE clearing isRestoring
|
|
139
|
+
// This ensures reactive effects that fire on projectState change
|
|
140
|
+
// still see isRestoring=true and don't sync stale state to server
|
|
141
|
+
projectState.currentProject = project;
|
|
142
|
+
} finally {
|
|
143
|
+
// Clear restoring flag after project state is fully set
|
|
144
|
+
appState.isRestoring = false;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
} else {
|
|
148
|
+
projectState.currentProject = project;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (project) {
|
|
152
|
+
// Save current project ID to server
|
|
153
|
+
ws.http('user:save-state', { key: 'currentProjectId', value: project.id }).catch(err => {
|
|
154
|
+
debug.error('project', 'Error saving project state to server:', err);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
try {
|
|
158
|
+
// Update last opened time via WebSocket
|
|
159
|
+
const updatedProject = await ws.http('projects:get', { id: project.id });
|
|
160
|
+
|
|
161
|
+
if (updatedProject) {
|
|
162
|
+
// Update in local state
|
|
163
|
+
const index = projectState.projects.findIndex(p => p.id === project.id);
|
|
164
|
+
if (index !== -1) {
|
|
165
|
+
projectState.projects[index] = updatedProject;
|
|
166
|
+
}
|
|
167
|
+
projectState.currentProject = updatedProject;
|
|
168
|
+
}
|
|
169
|
+
} catch (error) {
|
|
170
|
+
debug.error('project', 'Error updating project last opened:', error);
|
|
171
|
+
}
|
|
172
|
+
} else {
|
|
173
|
+
// Save null to server
|
|
174
|
+
ws.http('user:save-state', { key: 'currentProjectId', value: null }).catch(err => {
|
|
175
|
+
debug.error('project', 'Error clearing project state on server:', err);
|
|
176
|
+
});
|
|
177
|
+
debug.log('project', 'Project cleared');
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export function addProject(project: Project) {
|
|
182
|
+
projectState.projects.push(project);
|
|
183
|
+
updateRecentProjects();
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export function updateProject(updatedProject: Project) {
|
|
187
|
+
const index = projectState.projects.findIndex(p => p.id === updatedProject.id);
|
|
188
|
+
if (index !== -1) {
|
|
189
|
+
projectState.projects[index] = updatedProject;
|
|
190
|
+
|
|
191
|
+
// Update current project if it's the same
|
|
192
|
+
if (projectState.currentProject?.id === updatedProject.id) {
|
|
193
|
+
projectState.currentProject = updatedProject;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
updateRecentProjects();
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export function removeProject(projectId: string) {
|
|
200
|
+
projectState.projects = projectState.projects.filter(p => p.id !== projectId);
|
|
201
|
+
|
|
202
|
+
// Clear current project if it's being removed
|
|
203
|
+
if (projectState.currentProject?.id === projectId) {
|
|
204
|
+
projectState.currentProject = null;
|
|
205
|
+
}
|
|
206
|
+
updateRecentProjects();
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// ========================================
|
|
210
|
+
// DATA LOADING
|
|
211
|
+
// ========================================
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Load projects from server.
|
|
215
|
+
* Optionally restores current project from server-provided projectId.
|
|
216
|
+
*/
|
|
217
|
+
export async function loadProjects(restoreProjectId?: string | null) {
|
|
218
|
+
projectState.isLoading = true;
|
|
219
|
+
projectState.error = null;
|
|
220
|
+
|
|
221
|
+
try {
|
|
222
|
+
// Load projects via WebSocket
|
|
223
|
+
const projects = await ws.http('projects:list');
|
|
224
|
+
|
|
225
|
+
if (projects) {
|
|
226
|
+
projectState.projects = projects;
|
|
227
|
+
updateRecentProjects();
|
|
228
|
+
|
|
229
|
+
// Restore current project from server-provided ID if not already set
|
|
230
|
+
if (!projectState.currentProject && restoreProjectId) {
|
|
231
|
+
const existingProject = projects.find(p => p.id === restoreProjectId);
|
|
232
|
+
if (existingProject) {
|
|
233
|
+
debug.log('project', 'Restoring project from server state:', existingProject.id);
|
|
234
|
+
|
|
235
|
+
// Sync project context with WebSocket FIRST (before setting reactive state)
|
|
236
|
+
// This ensures server knows the project before any reactive effects fire
|
|
237
|
+
await ws.setProject(existingProject.id);
|
|
238
|
+
debug.log('project', 'WebSocket context synced for restored project:', existingProject.id);
|
|
239
|
+
|
|
240
|
+
projectState.currentProject = existingProject;
|
|
241
|
+
|
|
242
|
+
// Start tracking the restored project
|
|
243
|
+
if (typeof window !== 'undefined') {
|
|
244
|
+
try {
|
|
245
|
+
const { projectStatusService } = await import('$frontend/lib/services/project');
|
|
246
|
+
await projectStatusService.startTracking(existingProject.id);
|
|
247
|
+
} catch (error) {
|
|
248
|
+
debug.error('project', 'Error starting tracking for restored project:', error);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
} else {
|
|
252
|
+
debug.log('project', 'Saved project no longer exists on server');
|
|
253
|
+
}
|
|
254
|
+
} else if (!projectState.currentProject) {
|
|
255
|
+
debug.log('project', 'No saved project to restore');
|
|
256
|
+
} else {
|
|
257
|
+
debug.log('project', 'Project already set, skipping restoration');
|
|
258
|
+
}
|
|
259
|
+
} else {
|
|
260
|
+
projectState.error = 'Failed to load projects';
|
|
261
|
+
}
|
|
262
|
+
} catch (error) {
|
|
263
|
+
debug.error('project', 'Error loading projects:', error);
|
|
264
|
+
projectState.error = `Error loading projects: ${error}`;
|
|
265
|
+
} finally {
|
|
266
|
+
projectState.isLoading = false;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
export async function createProject(projectData: Omit<Project, 'id' | 'created_at'>) {
|
|
271
|
+
try {
|
|
272
|
+
// Create project via WebSocket
|
|
273
|
+
const project = await ws.http('projects:create', {
|
|
274
|
+
name: projectData.name,
|
|
275
|
+
path: projectData.path
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
if (project) {
|
|
279
|
+
addProject(project);
|
|
280
|
+
return project;
|
|
281
|
+
} else {
|
|
282
|
+
projectState.error = 'Failed to create project';
|
|
283
|
+
return null;
|
|
284
|
+
}
|
|
285
|
+
} catch (error) {
|
|
286
|
+
debug.error('project', 'Error creating project:', error);
|
|
287
|
+
projectState.error = `Error creating project: ${error}`;
|
|
288
|
+
return null;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// ========================================
|
|
293
|
+
// UTILITY FUNCTIONS
|
|
294
|
+
// ========================================
|
|
295
|
+
|
|
296
|
+
function updateRecentProjects() {
|
|
297
|
+
projectState.recentProjects = projectState.projects
|
|
298
|
+
.filter(p => p.last_opened_at)
|
|
299
|
+
.sort((a, b) => new Date(b.last_opened_at!).getTime() - new Date(a.last_opened_at!).getTime())
|
|
300
|
+
.slice(0, 5);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
export function searchProjects(query: string): Project[] {
|
|
304
|
+
const lowercaseQuery = query.toLowerCase();
|
|
305
|
+
return projectState.projects.filter(project =>
|
|
306
|
+
project.name.toLowerCase().includes(lowercaseQuery) ||
|
|
307
|
+
project.path.toLowerCase().includes(lowercaseQuery)
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// ========================================
|
|
312
|
+
// INITIALIZATION
|
|
313
|
+
// ========================================
|
|
314
|
+
|
|
315
|
+
export async function initializeProjects(restoreProjectId?: string | null) {
|
|
316
|
+
await loadProjects(restoreProjectId);
|
|
317
|
+
}
|
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sessions Store
|
|
3
|
+
* All session and message-related state and functions
|
|
4
|
+
*
|
|
5
|
+
* State persistence: Server-side only
|
|
6
|
+
* Session is determined by the shared session per project (sessions:get-shared)
|
|
7
|
+
* No localStorage usage - server is single source of truth
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { ChatSession, SDKMessageFormatter } from '$shared/types/database/schema';
|
|
11
|
+
import type { SDKMessage } from '$shared/types/messaging';
|
|
12
|
+
import { buildMetadataFromTransport } from '$shared/utils/message-formatter';
|
|
13
|
+
import ws from '$frontend/lib/utils/ws';
|
|
14
|
+
import { projectState } from './projects.svelte';
|
|
15
|
+
import { setupEditModeListener, restoreEditMode } from '$frontend/lib/stores/ui/edit-mode.svelte';
|
|
16
|
+
import { debug } from '$shared/utils/logger';
|
|
17
|
+
|
|
18
|
+
interface SessionState {
|
|
19
|
+
sessions: ChatSession[];
|
|
20
|
+
currentSession: ChatSession | null;
|
|
21
|
+
messages: SDKMessageFormatter[];
|
|
22
|
+
isLoading: boolean;
|
|
23
|
+
error: string | null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Session state using Svelte 5 runes
|
|
27
|
+
export const sessionState = $state<SessionState>({
|
|
28
|
+
sessions: [],
|
|
29
|
+
currentSession: null,
|
|
30
|
+
messages: [],
|
|
31
|
+
isLoading: false,
|
|
32
|
+
error: null
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
// ========================================
|
|
36
|
+
// DERIVED VALUES
|
|
37
|
+
// ========================================
|
|
38
|
+
|
|
39
|
+
export function hasSessions() {
|
|
40
|
+
return sessionState.sessions.length > 0;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function currentSessionId() {
|
|
44
|
+
return sessionState.currentSession?.id || '';
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function messageCount() {
|
|
48
|
+
return sessionState.messages.length;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ========================================
|
|
52
|
+
// SESSION MANAGEMENT
|
|
53
|
+
// ========================================
|
|
54
|
+
|
|
55
|
+
export async function setCurrentSession(session: ChatSession | null, skipLoadMessages: boolean = false) {
|
|
56
|
+
const previousSessionId = sessionState.currentSession?.id;
|
|
57
|
+
sessionState.currentSession = session;
|
|
58
|
+
|
|
59
|
+
// Leave previous chat session room
|
|
60
|
+
if (previousSessionId && previousSessionId !== session?.id) {
|
|
61
|
+
ws.emit('chat:leave-session', { chatSessionId: previousSessionId });
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (session) {
|
|
65
|
+
// Join new chat session room (receive session-scoped events)
|
|
66
|
+
ws.emit('chat:join-session', { chatSessionId: session.id });
|
|
67
|
+
|
|
68
|
+
// Persist current session to server for refresh restore (server is single source of truth)
|
|
69
|
+
ws.emit('sessions:set-current', { sessionId: session.id });
|
|
70
|
+
|
|
71
|
+
// Refresh session data from server to get latest engine/model
|
|
72
|
+
// (may have been set by another user's stream or not yet synced)
|
|
73
|
+
try {
|
|
74
|
+
const response = await ws.http('sessions:get', { id: session.id });
|
|
75
|
+
if (response?.session) {
|
|
76
|
+
const freshSession = response.session as ChatSession;
|
|
77
|
+
// Update local session list with fresh data
|
|
78
|
+
const idx = sessionState.sessions.findIndex(s => s.id === session.id);
|
|
79
|
+
if (idx !== -1) {
|
|
80
|
+
sessionState.sessions[idx] = freshSession;
|
|
81
|
+
}
|
|
82
|
+
sessionState.currentSession = freshSession;
|
|
83
|
+
}
|
|
84
|
+
} catch {
|
|
85
|
+
// Ignore - proceed with existing session data
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Load messages for this session (skip if we're restoring and already have messages)
|
|
89
|
+
if (!skipLoadMessages) {
|
|
90
|
+
await loadMessagesForSession(session.id);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
debug.log('session', 'Session set:', session.id);
|
|
94
|
+
} else {
|
|
95
|
+
// Clear messages when no session
|
|
96
|
+
sessionState.messages = [];
|
|
97
|
+
debug.log('session', 'Session cleared');
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export async function createSession(projectId: string, title: string, forceNew: boolean = false): Promise<ChatSession | null> {
|
|
102
|
+
try {
|
|
103
|
+
// For shared sessions, we want to get or create a shared session for the project
|
|
104
|
+
const session = await ws.http('sessions:get-shared', { forceNew });
|
|
105
|
+
|
|
106
|
+
// When forceNew is true, mark all other sessions for this project as ended in frontend state
|
|
107
|
+
// This ensures switching projects and back won't restore the old session
|
|
108
|
+
if (forceNew) {
|
|
109
|
+
const now = new Date().toISOString();
|
|
110
|
+
for (let i = 0; i < sessionState.sessions.length; i++) {
|
|
111
|
+
const s = sessionState.sessions[i];
|
|
112
|
+
if (s.project_id === projectId && s.id !== session.id && !s.ended_at) {
|
|
113
|
+
sessionState.sessions[i] = { ...s, ended_at: now };
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Check if session already exists in state
|
|
119
|
+
const existingIndex = sessionState.sessions.findIndex(s => s.id === session.id);
|
|
120
|
+
if (existingIndex === -1) {
|
|
121
|
+
sessionState.sessions.push(session);
|
|
122
|
+
} else {
|
|
123
|
+
// Update existing session
|
|
124
|
+
sessionState.sessions[existingIndex] = session;
|
|
125
|
+
}
|
|
126
|
+
return session;
|
|
127
|
+
} catch (error) {
|
|
128
|
+
debug.error('session', 'Error creating session:', error);
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export async function createNewChatSession(projectId: string): Promise<ChatSession | null> {
|
|
134
|
+
// Force create a new session (ends current shared session if exists)
|
|
135
|
+
return createSession(projectId, 'New Chat Session', true);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function updateSession(updatedSession: ChatSession) {
|
|
139
|
+
const index = sessionState.sessions.findIndex((s) => s.id === updatedSession.id);
|
|
140
|
+
if (index !== -1) {
|
|
141
|
+
sessionState.sessions[index] = updatedSession;
|
|
142
|
+
|
|
143
|
+
// Update current session if it's the same
|
|
144
|
+
if (sessionState.currentSession?.id === updatedSession.id) {
|
|
145
|
+
sessionState.currentSession = updatedSession;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function removeSession(sessionId: string) {
|
|
151
|
+
// Use splice for granular Svelte 5 reactivity — only the removed element
|
|
152
|
+
// triggers DOM updates, preventing full-list re-render flicker.
|
|
153
|
+
const index = sessionState.sessions.findIndex((s) => s.id === sessionId);
|
|
154
|
+
if (index !== -1) {
|
|
155
|
+
sessionState.sessions.splice(index, 1);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Clear current session if it's the one being removed
|
|
159
|
+
if (sessionState.currentSession?.id === sessionId) {
|
|
160
|
+
sessionState.currentSession = null;
|
|
161
|
+
sessionState.messages = [];
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export async function endSession(sessionId: string) {
|
|
166
|
+
const session = sessionState.sessions.find((s) => s.id === sessionId);
|
|
167
|
+
if (session && !session.ended_at) {
|
|
168
|
+
try {
|
|
169
|
+
const updatedSession = await ws.http('sessions:update', {
|
|
170
|
+
id: sessionId,
|
|
171
|
+
end_session: true
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
updateSession(updatedSession);
|
|
175
|
+
} catch (error) {
|
|
176
|
+
debug.error('session', 'Error ending session:', error);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// ========================================
|
|
182
|
+
// MESSAGE MANAGEMENT
|
|
183
|
+
// ========================================
|
|
184
|
+
|
|
185
|
+
export function addMessage(message: SDKMessage | SDKMessageFormatter): void {
|
|
186
|
+
// Convert to SDKMessageFormatter if needed
|
|
187
|
+
const messageFormatter: SDKMessageFormatter = {
|
|
188
|
+
...message,
|
|
189
|
+
metadata: ('metadata' in message && message.metadata)
|
|
190
|
+
? message.metadata
|
|
191
|
+
: buildMetadataFromTransport({ timestamp: new Date().toISOString() })
|
|
192
|
+
};
|
|
193
|
+
// Update unified store - single source of truth
|
|
194
|
+
sessionState.messages.push(messageFormatter);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export function updateMessages(messages: SDKMessageFormatter[]) {
|
|
198
|
+
sessionState.messages = messages;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export function clearMessages() {
|
|
202
|
+
sessionState.messages = [];
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export async function loadMessagesForSession(sessionId: string) {
|
|
206
|
+
try {
|
|
207
|
+
const response = await ws.http('messages:list', { session_id: sessionId });
|
|
208
|
+
|
|
209
|
+
if (response && Array.isArray(response)) {
|
|
210
|
+
// Messages from server already have correct SDKMessageFormatter shape with metadata
|
|
211
|
+
sessionState.messages = response as SDKMessageFormatter[];
|
|
212
|
+
} else {
|
|
213
|
+
sessionState.messages = [];
|
|
214
|
+
}
|
|
215
|
+
} catch (error) {
|
|
216
|
+
debug.error('session', 'Error loading messages:', error);
|
|
217
|
+
sessionState.messages = [];
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// ========================================
|
|
222
|
+
// DATA LOADING
|
|
223
|
+
// ========================================
|
|
224
|
+
|
|
225
|
+
export async function loadSessions() {
|
|
226
|
+
sessionState.isLoading = true;
|
|
227
|
+
sessionState.error = null;
|
|
228
|
+
|
|
229
|
+
try {
|
|
230
|
+
const response = await ws.http('sessions:list');
|
|
231
|
+
|
|
232
|
+
if (response) {
|
|
233
|
+
const { sessions, currentSessionId } = response;
|
|
234
|
+
sessionState.sessions = sessions;
|
|
235
|
+
|
|
236
|
+
// Auto-restore: find the active session for the current project
|
|
237
|
+
if (!sessionState.currentSession) {
|
|
238
|
+
const currentProject = projectState.currentProject;
|
|
239
|
+
if (currentProject) {
|
|
240
|
+
const projectSessions = sessions.filter(
|
|
241
|
+
(s: ChatSession) => s.project_id === currentProject.id && !s.ended_at
|
|
242
|
+
);
|
|
243
|
+
|
|
244
|
+
if (projectSessions.length > 0) {
|
|
245
|
+
// Try server-saved session first (preserves user's session across refresh)
|
|
246
|
+
let targetSession: ChatSession | null = null;
|
|
247
|
+
if (currentSessionId) {
|
|
248
|
+
targetSession = projectSessions.find(
|
|
249
|
+
(s: ChatSession) => s.id === currentSessionId
|
|
250
|
+
) || null;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Fall back to most recent active session
|
|
254
|
+
if (!targetSession) {
|
|
255
|
+
targetSession = projectSessions.sort((a: ChatSession, b: ChatSession) =>
|
|
256
|
+
new Date(b.started_at).getTime() - new Date(a.started_at).getTime()
|
|
257
|
+
)[0];
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
debug.log('session', 'Auto-restoring session for project:', targetSession.id);
|
|
261
|
+
// Load messages BEFORE setting session to avoid race condition:
|
|
262
|
+
// Setting session triggers $effect → catchupActiveStream (async),
|
|
263
|
+
// but loadMessagesForSession replaces sessionState.messages entirely,
|
|
264
|
+
// wiping out any stream_event injected by catchup.
|
|
265
|
+
await loadMessagesForSession(targetSession.id);
|
|
266
|
+
sessionState.currentSession = targetSession;
|
|
267
|
+
// Join chat session room so we receive session-scoped events
|
|
268
|
+
// (stream, input sync, edit mode, model sync).
|
|
269
|
+
// Critical after refresh — without it, connection misses all events.
|
|
270
|
+
ws.emit('chat:join-session', { chatSessionId: targetSession.id });
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
} else {
|
|
275
|
+
sessionState.error = 'Failed to load sessions';
|
|
276
|
+
}
|
|
277
|
+
} catch (error) {
|
|
278
|
+
debug.error('session', 'Error loading sessions:', error);
|
|
279
|
+
sessionState.error = `Error loading sessions: ${error}`;
|
|
280
|
+
} finally {
|
|
281
|
+
sessionState.isLoading = false;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// ========================================
|
|
286
|
+
// UTILITY FUNCTIONS
|
|
287
|
+
// ========================================
|
|
288
|
+
|
|
289
|
+
export function getSessionsForProject(projectId: string): ChatSession[] {
|
|
290
|
+
return sessionState.sessions.filter((session) => session.project_id === projectId);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
export function getRecentSessions(limit: number = 10): ChatSession[] {
|
|
294
|
+
return sessionState.sessions
|
|
295
|
+
.sort((a, b) => new Date(b.started_at).getTime() - new Date(a.started_at).getTime())
|
|
296
|
+
.slice(0, limit);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Reload sessions for the current project from the server.
|
|
301
|
+
* Called when the user switches projects so session list stays in sync.
|
|
302
|
+
*/
|
|
303
|
+
export async function reloadSessionsForProject() {
|
|
304
|
+
try {
|
|
305
|
+
const response = await ws.http('sessions:list');
|
|
306
|
+
if (response) {
|
|
307
|
+
const { sessions } = response;
|
|
308
|
+
// Merge: keep sessions from other projects, replace sessions for current project
|
|
309
|
+
const currentProjectId = projectState.currentProject?.id;
|
|
310
|
+
if (currentProjectId) {
|
|
311
|
+
const otherProjectSessions = sessionState.sessions.filter(
|
|
312
|
+
(s: ChatSession) => s.project_id !== currentProjectId
|
|
313
|
+
);
|
|
314
|
+
sessionState.sessions = [...otherProjectSessions, ...sessions];
|
|
315
|
+
} else {
|
|
316
|
+
sessionState.sessions = sessions;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
} catch (error) {
|
|
320
|
+
debug.error('session', 'Error reloading sessions:', error);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// ========================================
|
|
325
|
+
// COLLABORATIVE LISTENERS
|
|
326
|
+
// ========================================
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Setup WebSocket listeners for collaborative session management.
|
|
330
|
+
* When another user creates a new chat session, all users in the project
|
|
331
|
+
* automatically switch to the new shared session.
|
|
332
|
+
*/
|
|
333
|
+
function setupCollaborativeListeners() {
|
|
334
|
+
// Listen for new session available notifications from other users.
|
|
335
|
+
// Does NOT auto-switch — adds session to list and shows notification.
|
|
336
|
+
ws.on('sessions:session-available', async (data: { session: ChatSession }) => {
|
|
337
|
+
debug.log('session', 'New session available in project:', data.session.id);
|
|
338
|
+
|
|
339
|
+
const { session } = data;
|
|
340
|
+
|
|
341
|
+
// Add to sessions list (don't switch)
|
|
342
|
+
const existingIndex = sessionState.sessions.findIndex(s => s.id === session.id);
|
|
343
|
+
if (existingIndex === -1) {
|
|
344
|
+
sessionState.sessions.push(session);
|
|
345
|
+
} else {
|
|
346
|
+
sessionState.sessions[existingIndex] = session;
|
|
347
|
+
}
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
// Listen for session deletion broadcasts from other users
|
|
351
|
+
ws.on('sessions:session-deleted', (data: { sessionId: string; projectId: string }) => {
|
|
352
|
+
debug.log('session', 'Session deleted by another user:', data.sessionId);
|
|
353
|
+
removeSession(data.sessionId);
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
// Listen for messages-changed broadcasts (undo/redo/edit by another user)
|
|
357
|
+
ws.on('chat:messages-changed', async (data: { sessionId: string; reason: string; timestamp: string }) => {
|
|
358
|
+
debug.log('session', `Messages changed (${data.reason}) for session: ${data.sessionId}`);
|
|
359
|
+
|
|
360
|
+
// Reload messages if we're viewing the affected session
|
|
361
|
+
if (sessionState.currentSession?.id === data.sessionId) {
|
|
362
|
+
await loadMessagesForSession(data.sessionId);
|
|
363
|
+
}
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// ========================================
|
|
368
|
+
// INITIALIZATION
|
|
369
|
+
// ========================================
|
|
370
|
+
|
|
371
|
+
export async function initializeSessions() {
|
|
372
|
+
// Setup sync listeners first (no await needed)
|
|
373
|
+
setupCollaborativeListeners();
|
|
374
|
+
setupEditModeListener();
|
|
375
|
+
|
|
376
|
+
// Load sessions and restore edit mode in parallel
|
|
377
|
+
// Both only need WS project context (already set by initializeProjects)
|
|
378
|
+
await Promise.all([
|
|
379
|
+
loadSessions(),
|
|
380
|
+
restoreEditMode()
|
|
381
|
+
]);
|
|
382
|
+
debug.log('session', 'Sessions initialized');
|
|
383
|
+
}
|