@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,619 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { untrack } from 'svelte';
|
|
3
|
+
import { settings } from '$frontend/lib/stores/features/settings.svelte';
|
|
4
|
+
import { modelStore } from '$frontend/lib/stores/features/models.svelte';
|
|
5
|
+
import { sessionState } from '$frontend/lib/stores/core/sessions.svelte';
|
|
6
|
+
import { userStore } from '$frontend/lib/stores/features/user.svelte';
|
|
7
|
+
import { chatModelState, initChatModel, restoreChatModelFromSession } from '$frontend/lib/stores/ui/chat-model.svelte';
|
|
8
|
+
import { ENGINES } from '$shared/constants/engines';
|
|
9
|
+
import type { EngineType, EngineModel } from '$shared/types/engine';
|
|
10
|
+
import Icon from '$frontend/lib/components/common/Icon.svelte';
|
|
11
|
+
import { claudeAccountsStore, type ClaudeAccountItem } from '$frontend/lib/stores/features/claude-accounts.svelte';
|
|
12
|
+
import ws from '$frontend/lib/utils/ws';
|
|
13
|
+
import { debug } from '$shared/utils/logger';
|
|
14
|
+
|
|
15
|
+
// ════════════════════════════════════════════
|
|
16
|
+
// Claude Accounts (reads from shared store)
|
|
17
|
+
// ════════════════════════════════════════════
|
|
18
|
+
|
|
19
|
+
const claudeAccounts = $derived(claudeAccountsStore.accounts);
|
|
20
|
+
|
|
21
|
+
const currentAccount = $derived(
|
|
22
|
+
claudeAccounts.find(a => a.id === chatModelState.claudeAccountId) || null
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
const showAccountPicker = $derived(chatModelState.engine === 'claude-code');
|
|
26
|
+
const hasClaudeAccounts = $derived(claudeAccounts.length > 0);
|
|
27
|
+
|
|
28
|
+
// Fetch accounts when engine switches to claude-code
|
|
29
|
+
$effect(() => {
|
|
30
|
+
const engine = chatModelState.engine;
|
|
31
|
+
if (engine === 'claude-code') {
|
|
32
|
+
claudeAccountsStore.fetch();
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
// Auto-select active account when no account is set and accounts are loaded
|
|
37
|
+
$effect(() => {
|
|
38
|
+
const engine = chatModelState.engine;
|
|
39
|
+
const accounts = claudeAccounts;
|
|
40
|
+
const currentId = chatModelState.claudeAccountId;
|
|
41
|
+
|
|
42
|
+
if (engine === 'claude-code' && accounts.length > 0) {
|
|
43
|
+
untrack(() => {
|
|
44
|
+
// If no account set, or current account not found in list, use active account
|
|
45
|
+
const hasValidAccount = currentId !== null && accounts.some(a => a.id === currentId);
|
|
46
|
+
if (!hasValidAccount) {
|
|
47
|
+
const activeAccount = accounts.find(a => a.isActive);
|
|
48
|
+
if (activeAccount) {
|
|
49
|
+
chatModelState.claudeAccountId = activeAccount.id;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// Emit account changes to other users in the same chat session
|
|
57
|
+
let lastSyncedAccountId: number | null = null;
|
|
58
|
+
let ignoringRemoteAccountSync = false;
|
|
59
|
+
|
|
60
|
+
$effect(() => {
|
|
61
|
+
const accountId = chatModelState.claudeAccountId;
|
|
62
|
+
const engine = chatModelState.engine;
|
|
63
|
+
const chatSessionId = sessionState.currentSession?.id;
|
|
64
|
+
const senderId = userStore.currentUser?.id;
|
|
65
|
+
if (!chatSessionId || !senderId || ignoringRemoteAccountSync || engine !== 'claude-code') return;
|
|
66
|
+
if (accountId !== null && accountId !== lastSyncedAccountId) {
|
|
67
|
+
lastSyncedAccountId = accountId;
|
|
68
|
+
ws.emit('chat:account-sync', {
|
|
69
|
+
senderId,
|
|
70
|
+
chatSessionId,
|
|
71
|
+
claudeAccountId: accountId
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// Listen for remote account changes from other users
|
|
77
|
+
$effect(() => {
|
|
78
|
+
const unsub = ws.on('chat:account-sync', (data: { senderId: string; claudeAccountId: number | null }) => {
|
|
79
|
+
const currentUserId = userStore.currentUser?.id;
|
|
80
|
+
if (data.senderId === currentUserId) return;
|
|
81
|
+
debug.log('chat', 'Remote account sync:', data);
|
|
82
|
+
ignoringRemoteAccountSync = true;
|
|
83
|
+
chatModelState.claudeAccountId = data.claudeAccountId;
|
|
84
|
+
lastSyncedAccountId = data.claudeAccountId;
|
|
85
|
+
ignoringRemoteAccountSync = false;
|
|
86
|
+
|
|
87
|
+
// Also update session state so init $effect won't overwrite on re-render
|
|
88
|
+
if (sessionState.currentSession) {
|
|
89
|
+
sessionState.currentSession = {
|
|
90
|
+
...sessionState.currentSession,
|
|
91
|
+
claude_account_id: data.claudeAccountId ?? undefined
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
return unsub;
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
// Account dropdown state
|
|
99
|
+
let showAccountDropdown = $state(false);
|
|
100
|
+
let accountTriggerButton = $state<HTMLButtonElement>();
|
|
101
|
+
let accountDropdownStyle = $state('');
|
|
102
|
+
|
|
103
|
+
function toggleAccountDropdown() {
|
|
104
|
+
if (!showAccountDropdown && accountTriggerButton) {
|
|
105
|
+
const rect = accountTriggerButton.getBoundingClientRect();
|
|
106
|
+
accountDropdownStyle = `position: fixed; bottom: ${window.innerHeight - rect.top + 4}px; left: ${rect.left}px; z-index: 9999;`;
|
|
107
|
+
}
|
|
108
|
+
showAccountDropdown = !showAccountDropdown;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function closeAccountDropdown() {
|
|
112
|
+
showAccountDropdown = false;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function selectAccount(account: ClaudeAccountItem) {
|
|
116
|
+
chatModelState.claudeAccountId = account.id;
|
|
117
|
+
closeAccountDropdown();
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// ════════════════════════════════════════════
|
|
121
|
+
// Model Picker (existing logic)
|
|
122
|
+
// ════════════════════════════════════════════
|
|
123
|
+
|
|
124
|
+
// Track whether a chat has started (any user message in current session)
|
|
125
|
+
const hasStartedChat = $derived(
|
|
126
|
+
sessionState.messages.some(m => m.type === 'user')
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
// Engine lock: once chat starts, the engine is locked for this session.
|
|
130
|
+
const lockedEngine = $derived<EngineType | null>(
|
|
131
|
+
hasStartedChat ? chatModelState.engine : null
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
const engineLocked = $derived(lockedEngine !== null);
|
|
135
|
+
|
|
136
|
+
// Read from local chat model state (isolated from Settings)
|
|
137
|
+
const currentEngine = $derived(ENGINES.find(e => e.type === chatModelState.engine));
|
|
138
|
+
const currentModel = $derived(modelStore.getById(chatModelState.model));
|
|
139
|
+
const availableModels = $derived(modelStore.getByEngine(chatModelState.engine));
|
|
140
|
+
|
|
141
|
+
// Label shown in the trigger button
|
|
142
|
+
const triggerLabel = $derived.by(() => {
|
|
143
|
+
if (chatModelState.engine !== 'claude-code' && modelStore.loading) return 'Loading...';
|
|
144
|
+
if (!currentModel) return 'No model selected';
|
|
145
|
+
return currentModel.name;
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
// Initialize model picker based on session state:
|
|
149
|
+
// - New session (no messages): apply Settings defaults
|
|
150
|
+
// - Existing session (has messages): restore from session's persisted engine/model
|
|
151
|
+
// Reads are done outside untrack (tracked), writes inside untrack (not tracked)
|
|
152
|
+
// to prevent UpdatedAtError from circular chatModelState read-write.
|
|
153
|
+
$effect(() => {
|
|
154
|
+
const session = sessionState.currentSession;
|
|
155
|
+
const _sessionId = session?.id;
|
|
156
|
+
const started = hasStartedChat;
|
|
157
|
+
const sEngine = settings.selectedEngine;
|
|
158
|
+
const sModel = settings.selectedModel;
|
|
159
|
+
const sMemory = settings.engineModelMemory;
|
|
160
|
+
const sessionEngine = session?.engine;
|
|
161
|
+
const sessionModel = session?.model;
|
|
162
|
+
const sessionAccountId = session?.claude_account_id;
|
|
163
|
+
|
|
164
|
+
untrack(() => {
|
|
165
|
+
if (!started) {
|
|
166
|
+
// New session (no messages): apply Settings defaults
|
|
167
|
+
initChatModel(sEngine, sModel, sMemory || {});
|
|
168
|
+
} else if (sessionEngine && sessionModel) {
|
|
169
|
+
// Existing session with persisted engine/model: restore
|
|
170
|
+
restoreChatModelFromSession(sessionEngine, sessionModel, sessionAccountId);
|
|
171
|
+
} else {
|
|
172
|
+
// Existing session without engine/model (pre-migration or not yet set):
|
|
173
|
+
// fall back to Settings defaults
|
|
174
|
+
initChatModel(sEngine, sModel, sMemory || {});
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
// Pre-load models for the current engine whenever it changes.
|
|
180
|
+
// Ensures models are ready without waiting for the user to open the dropdown.
|
|
181
|
+
$effect(() => {
|
|
182
|
+
const engine = chatModelState.engine;
|
|
183
|
+
if (engine !== 'claude-code') {
|
|
184
|
+
modelStore.fetchModels(engine);
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
// Auto-select a model if no valid model is set for the current engine.
|
|
189
|
+
// Reads (engine, currentModel, availableModels) are tracked; writes use untrack
|
|
190
|
+
// to prevent circular chatModelState read-write (UpdatedAtError).
|
|
191
|
+
$effect(() => {
|
|
192
|
+
const engine = chatModelState.engine;
|
|
193
|
+
const modelValid = currentModel?.engine === engine;
|
|
194
|
+
const models = availableModels;
|
|
195
|
+
if (!modelValid && models.length > 0) {
|
|
196
|
+
untrack(() => {
|
|
197
|
+
const memory = chatModelState.engineModelMemory;
|
|
198
|
+
const remembered = memory[engine];
|
|
199
|
+
const target =
|
|
200
|
+
(remembered && models.find(m => m.id === remembered)) ||
|
|
201
|
+
models.find(m => m.recommended) ||
|
|
202
|
+
models[0];
|
|
203
|
+
if (target) {
|
|
204
|
+
chatModelState.model = target.id;
|
|
205
|
+
chatModelState.engineModelMemory = { ...memory, [engine]: target.id };
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
// Emit model changes to other users in the same chat session
|
|
212
|
+
let lastSyncedModel = '';
|
|
213
|
+
let lastSyncedEngine = '';
|
|
214
|
+
let ignoringRemoteSync = false;
|
|
215
|
+
|
|
216
|
+
$effect(() => {
|
|
217
|
+
const engine = chatModelState.engine;
|
|
218
|
+
const model = chatModelState.model;
|
|
219
|
+
const chatSessionId = sessionState.currentSession?.id;
|
|
220
|
+
const senderId = userStore.currentUser?.id;
|
|
221
|
+
if (!chatSessionId || !senderId || ignoringRemoteSync) return;
|
|
222
|
+
// Only emit if model actually changed (not on init)
|
|
223
|
+
if (model && (model !== lastSyncedModel || engine !== lastSyncedEngine)) {
|
|
224
|
+
lastSyncedModel = model;
|
|
225
|
+
lastSyncedEngine = engine;
|
|
226
|
+
ws.emit('chat:model-sync', {
|
|
227
|
+
senderId,
|
|
228
|
+
chatSessionId,
|
|
229
|
+
engine,
|
|
230
|
+
model
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
// Listen for remote model changes from other users
|
|
236
|
+
$effect(() => {
|
|
237
|
+
const unsub = ws.on('chat:model-sync', (data: { senderId: string; engine: string; model: string }) => {
|
|
238
|
+
const currentUserId = userStore.currentUser?.id;
|
|
239
|
+
if (data.senderId === currentUserId) return; // Ignore own events
|
|
240
|
+
debug.log('chat', 'Remote model sync:', data);
|
|
241
|
+
ignoringRemoteSync = true;
|
|
242
|
+
chatModelState.engine = data.engine as EngineType;
|
|
243
|
+
chatModelState.model = data.model;
|
|
244
|
+
lastSyncedModel = data.model;
|
|
245
|
+
lastSyncedEngine = data.engine;
|
|
246
|
+
ignoringRemoteSync = false;
|
|
247
|
+
|
|
248
|
+
// Also update session state so init $effect won't overwrite on re-render
|
|
249
|
+
if (sessionState.currentSession) {
|
|
250
|
+
sessionState.currentSession = {
|
|
251
|
+
...sessionState.currentSession,
|
|
252
|
+
engine: data.engine as EngineType,
|
|
253
|
+
model: data.model
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
});
|
|
257
|
+
return unsub;
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
// Search state
|
|
261
|
+
let searchQuery = $state('');
|
|
262
|
+
let refreshing = $state(false);
|
|
263
|
+
let collapsedProviders = $state<Set<string>>(new Set());
|
|
264
|
+
|
|
265
|
+
const filteredModels = $derived.by(() => {
|
|
266
|
+
if (!searchQuery.trim()) return availableModels;
|
|
267
|
+
const q = searchQuery.toLowerCase();
|
|
268
|
+
return availableModels.filter(m =>
|
|
269
|
+
m.name.toLowerCase().includes(q) ||
|
|
270
|
+
m.modelId.toLowerCase().includes(q) ||
|
|
271
|
+
m.provider.toLowerCase().includes(q) ||
|
|
272
|
+
m.capabilities.some(c => c.toLowerCase().includes(q))
|
|
273
|
+
);
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
// Group models by provider
|
|
277
|
+
const groupedModels = $derived.by(() => {
|
|
278
|
+
const groups = new Map<string, EngineModel[]>();
|
|
279
|
+
for (const model of filteredModels) {
|
|
280
|
+
const key = model.provider;
|
|
281
|
+
if (!groups.has(key)) groups.set(key, []);
|
|
282
|
+
groups.get(key)!.push(model);
|
|
283
|
+
}
|
|
284
|
+
return groups;
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
// Sync accordion: open all when searching, otherwise only open the provider with the selected model
|
|
288
|
+
$effect(() => {
|
|
289
|
+
if (searchQuery.trim()) {
|
|
290
|
+
collapsedProviders = new Set();
|
|
291
|
+
} else if (groupedModels.size > 0) {
|
|
292
|
+
const allProviders = [...groupedModels.keys()];
|
|
293
|
+
let selectedProvider: string | null = null;
|
|
294
|
+
for (const [provider, models] of groupedModels) {
|
|
295
|
+
if (models.some(m => m.id === chatModelState.model)) {
|
|
296
|
+
selectedProvider = provider;
|
|
297
|
+
break;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
const collapsed = new Set(allProviders);
|
|
301
|
+
if (selectedProvider) {
|
|
302
|
+
collapsed.delete(selectedProvider);
|
|
303
|
+
}
|
|
304
|
+
collapsedProviders = collapsed;
|
|
305
|
+
}
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
function toggleProvider(provider: string) {
|
|
309
|
+
const next = new Set(collapsedProviders);
|
|
310
|
+
if (next.has(provider)) {
|
|
311
|
+
next.delete(provider);
|
|
312
|
+
} else {
|
|
313
|
+
next.add(provider);
|
|
314
|
+
}
|
|
315
|
+
collapsedProviders = next;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function formatProvider(provider: string): string {
|
|
319
|
+
return provider
|
|
320
|
+
.split(/[-_]/)
|
|
321
|
+
.map(w => w.charAt(0).toUpperCase() + w.slice(1))
|
|
322
|
+
.join(' ');
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// Dropdown state
|
|
326
|
+
let showDropdown = $state(false);
|
|
327
|
+
let triggerButton: HTMLButtonElement;
|
|
328
|
+
let dropdownStyle = $state('');
|
|
329
|
+
|
|
330
|
+
function toggleDropdown() {
|
|
331
|
+
if (!showDropdown && triggerButton) {
|
|
332
|
+
const rect = triggerButton.getBoundingClientRect();
|
|
333
|
+
dropdownStyle = `position: fixed; bottom: ${window.innerHeight - rect.top + 4}px; left: ${rect.left}px; z-index: 9999;`;
|
|
334
|
+
}
|
|
335
|
+
showDropdown = !showDropdown;
|
|
336
|
+
if (!showDropdown) searchQuery = '';
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function closeDropdown() {
|
|
340
|
+
showDropdown = false;
|
|
341
|
+
searchQuery = '';
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
async function selectEngine(engineType: EngineType) {
|
|
345
|
+
if (engineLocked) return;
|
|
346
|
+
|
|
347
|
+
// Switch engine immediately so the active tab updates before model fetch
|
|
348
|
+
chatModelState.engine = engineType;
|
|
349
|
+
searchQuery = '';
|
|
350
|
+
|
|
351
|
+
// Fetch models if needed; clear model immediately so it shows null during loading
|
|
352
|
+
if (engineType !== 'claude-code') {
|
|
353
|
+
chatModelState.model = '';
|
|
354
|
+
await modelStore.fetchModels(engineType);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// After models are loaded, pick a model for this engine
|
|
358
|
+
const memory = chatModelState.engineModelMemory;
|
|
359
|
+
const remembered = memory[engineType];
|
|
360
|
+
const models = modelStore.getByEngine(engineType);
|
|
361
|
+
const target =
|
|
362
|
+
(remembered && models.find(m => m.id === remembered)) ||
|
|
363
|
+
models.find(m => m.recommended) ||
|
|
364
|
+
models[0];
|
|
365
|
+
|
|
366
|
+
if (target) {
|
|
367
|
+
chatModelState.model = target.id;
|
|
368
|
+
chatModelState.engineModelMemory = { ...memory, [engineType]: target.id };
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function selectModel(model: EngineModel) {
|
|
373
|
+
chatModelState.model = model.id;
|
|
374
|
+
chatModelState.engineModelMemory = {
|
|
375
|
+
...chatModelState.engineModelMemory,
|
|
376
|
+
[chatModelState.engine]: model.id
|
|
377
|
+
};
|
|
378
|
+
closeDropdown();
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
async function handleRefresh() {
|
|
382
|
+
refreshing = true;
|
|
383
|
+
try {
|
|
384
|
+
await modelStore.refreshModels(chatModelState.engine);
|
|
385
|
+
} finally {
|
|
386
|
+
refreshing = false;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
</script>
|
|
390
|
+
|
|
391
|
+
<div class="flex items-center gap-1.5 px-4 pt-2 pb-0.5 -mb-2">
|
|
392
|
+
<button
|
|
393
|
+
bind:this={triggerButton}
|
|
394
|
+
type="button"
|
|
395
|
+
class="flex items-center gap-1.5 px-2 py-1 text-xs rounded-lg transition-all duration-150
|
|
396
|
+
bg-slate-100 dark:bg-slate-800 hover:bg-slate-200 dark:hover:bg-slate-700
|
|
397
|
+
text-slate-600 dark:text-slate-400 border border-slate-200 dark:border-slate-700"
|
|
398
|
+
onclick={toggleDropdown}
|
|
399
|
+
>
|
|
400
|
+
{#if currentEngine}
|
|
401
|
+
<div class="flex dark:hidden items-center justify-center w-3.5 h-3.5 [&>svg]:w-full [&>svg]:h-full">{@html currentEngine.icon.light}</div>
|
|
402
|
+
<div class="hidden dark:flex items-center justify-center w-3.5 h-3.5 [&>svg]:w-full [&>svg]:h-full">{@html currentEngine.icon.dark}</div>
|
|
403
|
+
{/if}
|
|
404
|
+
<span class="font-medium">{triggerLabel}</span>
|
|
405
|
+
<Icon name="lucide:chevron-down" class="w-3 h-3" />
|
|
406
|
+
</button>
|
|
407
|
+
|
|
408
|
+
<!-- Account picker (always shown for Claude Code engine) -->
|
|
409
|
+
{#if showAccountPicker}
|
|
410
|
+
{#if hasClaudeAccounts}
|
|
411
|
+
<button
|
|
412
|
+
bind:this={accountTriggerButton}
|
|
413
|
+
type="button"
|
|
414
|
+
class="flex items-center gap-1.5 px-2 py-1 text-xs rounded-lg transition-all duration-150
|
|
415
|
+
bg-slate-100 dark:bg-slate-800 hover:bg-slate-200 dark:hover:bg-slate-700
|
|
416
|
+
text-slate-600 dark:text-slate-400 border border-slate-200 dark:border-slate-700"
|
|
417
|
+
onclick={toggleAccountDropdown}
|
|
418
|
+
>
|
|
419
|
+
<Icon name="lucide:user" class="w-3.5 h-3.5" />
|
|
420
|
+
<span class="font-medium max-w-24 truncate">{currentAccount?.name || 'Account'}</span>
|
|
421
|
+
<Icon name="lucide:chevron-down" class="w-3 h-3" />
|
|
422
|
+
</button>
|
|
423
|
+
{:else}
|
|
424
|
+
<div class="flex items-center gap-1.5 px-2 py-1 text-xs rounded-lg
|
|
425
|
+
bg-amber-50 dark:bg-amber-900/20 text-amber-600 dark:text-amber-400
|
|
426
|
+
border border-amber-200 dark:border-amber-700/50">
|
|
427
|
+
<Icon name="lucide:triangle-alert" class="w-3.5 h-3.5 flex-shrink-0" />
|
|
428
|
+
<span class="font-medium">No accounts connected</span>
|
|
429
|
+
</div>
|
|
430
|
+
{/if}
|
|
431
|
+
{/if}
|
|
432
|
+
</div>
|
|
433
|
+
|
|
434
|
+
<!-- Account dropdown -->
|
|
435
|
+
{#if showAccountDropdown}
|
|
436
|
+
<div class="fixed inset-0" style="z-index: 9998;" onclick={closeAccountDropdown}></div>
|
|
437
|
+
|
|
438
|
+
<div style={accountDropdownStyle} class="bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-lg shadow-xl overflow-hidden min-w-48 max-h-64 flex flex-col">
|
|
439
|
+
<div class="flex gap-1.5 px-3 py-2 border-b border-slate-200 dark:border-slate-700 flex-shrink-0">
|
|
440
|
+
<Icon name="lucide:user" class="w-3.5 h-3.5" />
|
|
441
|
+
<span class="text-xs font-medium text-slate-500 dark:text-slate-400 tracking-wide">Claude Account</span>
|
|
442
|
+
</div>
|
|
443
|
+
<div class="overflow-y-auto py-1">
|
|
444
|
+
{#each claudeAccounts as account (account.id)}
|
|
445
|
+
{@const isSelected = chatModelState.claudeAccountId === account.id}
|
|
446
|
+
<button
|
|
447
|
+
type="button"
|
|
448
|
+
class="flex items-center gap-2.5 w-full px-3 py-2 text-left transition-all duration-150
|
|
449
|
+
{isSelected
|
|
450
|
+
? 'bg-violet-50 dark:bg-violet-900/20 text-violet-600 dark:text-violet-400'
|
|
451
|
+
: 'text-slate-700 dark:text-slate-300 hover:bg-slate-50 dark:hover:bg-slate-700/50'}"
|
|
452
|
+
onclick={() => selectAccount(account)}
|
|
453
|
+
>
|
|
454
|
+
<!-- Radio indicator -->
|
|
455
|
+
<div class="flex-shrink-0 w-3.5 h-3.5 rounded-full border-2 flex items-center justify-center
|
|
456
|
+
{isSelected ? 'border-violet-600' : 'border-slate-300 dark:border-slate-600'}">
|
|
457
|
+
{#if isSelected}
|
|
458
|
+
<div class="w-1.5 h-1.5 rounded-full bg-violet-600"></div>
|
|
459
|
+
{/if}
|
|
460
|
+
</div>
|
|
461
|
+
|
|
462
|
+
<div class="flex items-center gap-2 min-w-0 flex-1">
|
|
463
|
+
<span class="font-medium text-xs truncate">{account.name}</span>
|
|
464
|
+
</div>
|
|
465
|
+
</button>
|
|
466
|
+
{/each}
|
|
467
|
+
</div>
|
|
468
|
+
</div>
|
|
469
|
+
{/if}
|
|
470
|
+
|
|
471
|
+
<!-- Model dropdown rendered as fixed portal to escape overflow-hidden parent -->
|
|
472
|
+
{#if showDropdown}
|
|
473
|
+
<div class="fixed inset-0" style="z-index: 9998;" onclick={closeDropdown}></div>
|
|
474
|
+
|
|
475
|
+
<div style={dropdownStyle} class="bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-lg shadow-xl overflow-hidden min-w-64 max-h-96 flex flex-col">
|
|
476
|
+
|
|
477
|
+
<!-- Engine tabs -->
|
|
478
|
+
<div class="flex border-b border-slate-200 dark:border-slate-700 flex-shrink-0">
|
|
479
|
+
{#each ENGINES as engine (engine.type)}
|
|
480
|
+
{@const isActive = chatModelState.engine === engine.type}
|
|
481
|
+
{@const isDisabled = engineLocked && engine.type !== lockedEngine}
|
|
482
|
+
<button
|
|
483
|
+
type="button"
|
|
484
|
+
class="flex-1 flex items-center justify-center gap-1.5 px-3 py-2 text-xs font-medium transition-all duration-150
|
|
485
|
+
{isActive
|
|
486
|
+
? 'bg-violet-50 dark:bg-violet-900/20 text-violet-600 dark:text-violet-400 border-b-2 border-violet-600'
|
|
487
|
+
: isDisabled
|
|
488
|
+
? 'text-slate-300 dark:text-slate-600 cursor-not-allowed'
|
|
489
|
+
: 'text-slate-600 dark:text-slate-400 hover:bg-slate-50 dark:hover:bg-slate-700/50'}"
|
|
490
|
+
onclick={() => !isDisabled && selectEngine(engine.type)}
|
|
491
|
+
disabled={isDisabled}
|
|
492
|
+
>
|
|
493
|
+
<div class="flex dark:hidden items-center justify-center w-3.5 h-3.5 [&>svg]:w-full [&>svg]:h-full">{@html engine.icon.light}</div>
|
|
494
|
+
<div class="hidden dark:flex items-center justify-center w-3.5 h-3.5 [&>svg]:w-full [&>svg]:h-full">{@html engine.icon.dark}</div>
|
|
495
|
+
{engine.name}
|
|
496
|
+
{#if isDisabled}
|
|
497
|
+
<Icon name="lucide:lock" class="w-3 h-3" />
|
|
498
|
+
{/if}
|
|
499
|
+
</button>
|
|
500
|
+
{/each}
|
|
501
|
+
</div>
|
|
502
|
+
|
|
503
|
+
<!-- Engine locked notice -->
|
|
504
|
+
{#if engineLocked}
|
|
505
|
+
<div class="px-3 py-1.5 bg-amber-50 dark:bg-amber-900/10 border-b border-slate-200 dark:border-slate-700 flex-shrink-0">
|
|
506
|
+
<div class="flex items-center gap-1.5 text-3xs text-amber-600 dark:text-amber-400">
|
|
507
|
+
<Icon name="lucide:info" class="w-3 h-3 flex-shrink-0" />
|
|
508
|
+
<span>Engine is locked for this session. You can still switch models.</span>
|
|
509
|
+
</div>
|
|
510
|
+
</div>
|
|
511
|
+
{/if}
|
|
512
|
+
|
|
513
|
+
<!-- Search + Refresh -->
|
|
514
|
+
<div class="px-2 py-2 border-b border-slate-200 dark:border-slate-700 flex-shrink-0">
|
|
515
|
+
<div class="flex items-center gap-1.5">
|
|
516
|
+
<div class="relative flex-1">
|
|
517
|
+
<Icon name="lucide:search" class="absolute left-2 top-1/2 -translate-y-1/2 w-3 h-3 text-slate-400 pointer-events-none" />
|
|
518
|
+
<input
|
|
519
|
+
type="text"
|
|
520
|
+
bind:value={searchQuery}
|
|
521
|
+
placeholder="Search models..."
|
|
522
|
+
class="w-full pl-6 pr-2 py-1 text-xs bg-slate-50 dark:bg-slate-700/50 border border-slate-200 dark:border-slate-600 rounded-md outline-none focus:ring-1 focus:ring-violet-500/40 focus:border-violet-500 transition-colors text-slate-800 dark:text-slate-200 placeholder-slate-400"
|
|
523
|
+
/>
|
|
524
|
+
</div>
|
|
525
|
+
<button
|
|
526
|
+
type="button"
|
|
527
|
+
class="flex items-center justify-center w-6 h-6 rounded-md text-slate-400 hover:text-violet-600 hover:bg-violet-500/10 dark:hover:text-violet-400 dark:hover:bg-violet-500/15 transition-colors disabled:opacity-40 disabled:cursor-not-allowed flex-shrink-0"
|
|
528
|
+
onclick={handleRefresh}
|
|
529
|
+
disabled={refreshing || modelStore.loading}
|
|
530
|
+
title="Refresh models"
|
|
531
|
+
>
|
|
532
|
+
<svg viewBox="0 0 24 24" fill="none" class="w-3.5 h-3.5 {refreshing ? 'animate-spin' : ''}" aria-hidden="true">
|
|
533
|
+
<path d="M21 12a9 9 0 11-2.636-6.364M21 3v5h-5" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
|
534
|
+
</svg>
|
|
535
|
+
</button>
|
|
536
|
+
</div>
|
|
537
|
+
</div>
|
|
538
|
+
|
|
539
|
+
<!-- Model list -->
|
|
540
|
+
<div class="overflow-y-auto py-1">
|
|
541
|
+
{#if modelStore.loading}
|
|
542
|
+
<div class="flex items-center justify-center gap-2 py-5 text-xs text-slate-400">
|
|
543
|
+
<div class="w-3.5 h-3.5 border-2 border-slate-300 border-t-violet-500 rounded-full animate-spin"></div>
|
|
544
|
+
<span>Loading models...</span>
|
|
545
|
+
</div>
|
|
546
|
+
{:else if filteredModels.length === 0}
|
|
547
|
+
<div class="px-3 py-4 text-xs text-slate-500 text-center">
|
|
548
|
+
{searchQuery ? 'No models matching your search.' : 'No models available.'}
|
|
549
|
+
</div>
|
|
550
|
+
{:else}
|
|
551
|
+
{#each [...groupedModels.entries()] as [provider, providerModels] (provider)}
|
|
552
|
+
{@const isCollapsed = collapsedProviders.has(provider)}
|
|
553
|
+
{@const hasSelectedModel = providerModels.some(m => m.id === chatModelState.model)}
|
|
554
|
+
|
|
555
|
+
<!-- Provider header -->
|
|
556
|
+
<button
|
|
557
|
+
type="button"
|
|
558
|
+
class="flex items-center gap-2 w-full px-3 py-1.5 text-left transition-colors
|
|
559
|
+
hover:bg-slate-50 dark:hover:bg-slate-700/50"
|
|
560
|
+
onclick={() => toggleProvider(provider)}
|
|
561
|
+
>
|
|
562
|
+
<svg viewBox="0 0 24 24" fill="none"
|
|
563
|
+
class="w-3 h-3 text-slate-400 transition-transform duration-200 flex-shrink-0
|
|
564
|
+
{isCollapsed ? '' : 'rotate-90'}"
|
|
565
|
+
aria-hidden="true">
|
|
566
|
+
<path d="M9 18l6-6-6-6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
|
567
|
+
</svg>
|
|
568
|
+
<span class="text-2xs font-semibold text-slate-500 dark:text-slate-400 uppercase tracking-wide">
|
|
569
|
+
{formatProvider(provider)}
|
|
570
|
+
</span>
|
|
571
|
+
<span class="text-4xs text-slate-400 dark:text-slate-500">
|
|
572
|
+
{providerModels.length}
|
|
573
|
+
</span>
|
|
574
|
+
{#if hasSelectedModel}
|
|
575
|
+
<div class="w-1.5 h-1.5 rounded-full bg-violet-500 ml-auto flex-shrink-0"></div>
|
|
576
|
+
{/if}
|
|
577
|
+
</button>
|
|
578
|
+
|
|
579
|
+
<!-- Provider models -->
|
|
580
|
+
{#if !isCollapsed}
|
|
581
|
+
{#each providerModels as model (model.id)}
|
|
582
|
+
{@const isSelected = chatModelState.model === model.id}
|
|
583
|
+
<button
|
|
584
|
+
type="button"
|
|
585
|
+
class="flex items-start gap-2.5 w-full pl-5 pr-3 py-2 text-left transition-all duration-150
|
|
586
|
+
{isSelected
|
|
587
|
+
? 'bg-violet-50 dark:bg-violet-900/20 text-violet-600 dark:text-violet-400'
|
|
588
|
+
: 'text-slate-700 dark:text-slate-300 hover:bg-slate-50 dark:hover:bg-slate-700/50'}"
|
|
589
|
+
onclick={() => selectModel(model)}
|
|
590
|
+
>
|
|
591
|
+
<!-- Radio indicator -->
|
|
592
|
+
<div class="flex-shrink-0 w-3.5 h-3.5 rounded-full border-2 flex items-center justify-center mt-0.5
|
|
593
|
+
{isSelected ? 'border-violet-600' : 'border-slate-300 dark:border-slate-600'}">
|
|
594
|
+
{#if isSelected}
|
|
595
|
+
<div class="w-1.5 h-1.5 rounded-full bg-violet-600"></div>
|
|
596
|
+
{/if}
|
|
597
|
+
</div>
|
|
598
|
+
|
|
599
|
+
<!-- Model info -->
|
|
600
|
+
<div class="flex-1 min-w-0">
|
|
601
|
+
<div class="font-medium text-xs">{model.name}</div>
|
|
602
|
+
{#if model.capabilities.length > 0}
|
|
603
|
+
<div class="flex flex-wrap gap-1 mt-1">
|
|
604
|
+
{#each model.capabilities as cap}
|
|
605
|
+
<span class="px-1.5 py-0.5 text-3xs rounded bg-slate-100 dark:bg-slate-700/50 text-slate-500 dark:text-slate-400 leading-none">
|
|
606
|
+
{cap}
|
|
607
|
+
</span>
|
|
608
|
+
{/each}
|
|
609
|
+
</div>
|
|
610
|
+
{/if}
|
|
611
|
+
</div>
|
|
612
|
+
</button>
|
|
613
|
+
{/each}
|
|
614
|
+
{/if}
|
|
615
|
+
{/each}
|
|
616
|
+
{/if}
|
|
617
|
+
</div>
|
|
618
|
+
</div>
|
|
619
|
+
{/if}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import Icon from '$frontend/lib/components/common/Icon.svelte';
|
|
3
|
+
import type { FileAttachment } from '../composables/use-file-handling.svelte';
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
attachedFiles: FileAttachment[];
|
|
7
|
+
onRemove: (id: string) => void;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const { attachedFiles, onRemove }: Props = $props();
|
|
11
|
+
</script>
|
|
12
|
+
|
|
13
|
+
{#if attachedFiles.length > 0}
|
|
14
|
+
<div class="mb-2 p-2 bg-slate-50 dark:bg-slate-800/50 rounded-lg border border-slate-200 dark:border-slate-700">
|
|
15
|
+
<div class="flex flex-wrap gap-2">
|
|
16
|
+
{#each attachedFiles as attachment (attachment.id)}
|
|
17
|
+
<div class="relative group">
|
|
18
|
+
<div class="flex items-center gap-2 px-3 py-2 bg-white dark:bg-slate-900 rounded-lg border border-slate-200 dark:border-slate-700">
|
|
19
|
+
{#if attachment.type === 'image' && attachment.previewUrl}
|
|
20
|
+
<img
|
|
21
|
+
src={attachment.previewUrl}
|
|
22
|
+
alt={attachment.file.name}
|
|
23
|
+
class="w-8 h-8 object-cover rounded"
|
|
24
|
+
/>
|
|
25
|
+
{:else if attachment.type === 'document'}
|
|
26
|
+
<Icon name="lucide:file-text" class="w-4 h-4 text-slate-500" />
|
|
27
|
+
{:else}
|
|
28
|
+
<Icon name="lucide:file" class="w-4 h-4 text-slate-500" />
|
|
29
|
+
{/if}
|
|
30
|
+
<span class="text-sm text-slate-700 dark:text-slate-300 max-w-37.5 truncate">
|
|
31
|
+
{attachment.file.name}
|
|
32
|
+
</span>
|
|
33
|
+
<span class="text-xs text-slate-500 dark:text-slate-400">
|
|
34
|
+
({(attachment.file.size / 1024).toFixed(1)}KB)
|
|
35
|
+
</span>
|
|
36
|
+
<button
|
|
37
|
+
onclick={() => onRemove(attachment.id)}
|
|
38
|
+
class="ml-1 p-0.5 hover:bg-slate-100 dark:hover:bg-slate-800 rounded transition-colors"
|
|
39
|
+
aria-label="Remove attachment"
|
|
40
|
+
>
|
|
41
|
+
<Icon name="lucide:x" class="w-3 h-3 text-slate-500" />
|
|
42
|
+
</button>
|
|
43
|
+
</div>
|
|
44
|
+
</div>
|
|
45
|
+
{/each}
|
|
46
|
+
</div>
|
|
47
|
+
</div>
|
|
48
|
+
{/if}
|