@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,541 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { sessionState } from '$frontend/lib/stores/core/sessions.svelte';
|
|
3
|
+
import { appState } from '$frontend/lib/stores/core/app.svelte';
|
|
4
|
+
import ChatMessage from './ChatMessage.svelte';
|
|
5
|
+
import DateSeparator from './DateSeparator.svelte';
|
|
6
|
+
import { onMount, tick } from 'svelte';
|
|
7
|
+
import { fade } from 'svelte/transition';
|
|
8
|
+
|
|
9
|
+
// Import utilities
|
|
10
|
+
import { shouldFilterMessage } from '$frontend/lib/utils/chat/message-processor';
|
|
11
|
+
import { groupMessages, embedToolResults } from '$frontend/lib/utils/chat/message-grouper';
|
|
12
|
+
import { addDateSeparators, type DateSeparatorItem } from '$frontend/lib/utils/chat/date-separator';
|
|
13
|
+
import { editModeState } from '$frontend/lib/stores/ui/edit-mode.svelte';
|
|
14
|
+
import { createVirtualScroll, VS_CONFIG } from '$frontend/lib/utils/chat/virtual-scroll.svelte';
|
|
15
|
+
|
|
16
|
+
interface Props {
|
|
17
|
+
scrollContainer?: HTMLElement | undefined;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const { scrollContainer }: Props = $props();
|
|
21
|
+
|
|
22
|
+
// Internal scroll element - this is the actual scrollable div
|
|
23
|
+
let messagesScrollEl: HTMLElement | undefined = $state();
|
|
24
|
+
|
|
25
|
+
let previousMessageCount = $state(0);
|
|
26
|
+
let isUserAtBottom = $state(true);
|
|
27
|
+
let lastPartialTextHash = $state('');
|
|
28
|
+
let hasInitiallyScrolled = $state(false);
|
|
29
|
+
let isContentReady = $state(false);
|
|
30
|
+
let lastToolResultsHash = $state('');
|
|
31
|
+
// Prevent scroll events from overriding isUserAtBottom during programmatic scroll
|
|
32
|
+
let scrollLockUntil = 0;
|
|
33
|
+
|
|
34
|
+
// ========================================
|
|
35
|
+
// VIRTUAL SCROLL
|
|
36
|
+
// ========================================
|
|
37
|
+
|
|
38
|
+
const vs = createVirtualScroll();
|
|
39
|
+
let topSentinelEl: HTMLElement | undefined = $state();
|
|
40
|
+
let bottomSentinelEl: HTMLElement | undefined = $state();
|
|
41
|
+
let topObserver: IntersectionObserver | null = null;
|
|
42
|
+
let bottomObserver: IntersectionObserver | null = null;
|
|
43
|
+
let isLoadingOlder = $state(false);
|
|
44
|
+
|
|
45
|
+
// ========================================
|
|
46
|
+
// MESSAGE PROCESSING (full pipeline on all messages)
|
|
47
|
+
// ========================================
|
|
48
|
+
|
|
49
|
+
// Process messages through grouping and embedding
|
|
50
|
+
const processedMessages = $derived.by(() => {
|
|
51
|
+
const { groups, toolUseMap } = groupMessages(sessionState.messages);
|
|
52
|
+
return embedToolResults(groups, toolUseMap);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// Filter out messages with empty content arrays
|
|
56
|
+
const filteredMessages = $derived(
|
|
57
|
+
processedMessages.filter(message => !shouldFilterMessage(message))
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
// Windowed slice for rendering
|
|
61
|
+
const windowedMessages = $derived.by(() => {
|
|
62
|
+
if (!vs.isActive) return filteredMessages;
|
|
63
|
+
return filteredMessages.slice(vs.windowStart, vs.windowEnd);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// Add date separators to windowed messages (with global index offset for stable keys)
|
|
67
|
+
const messagesWithDateSeparators = $derived.by((): DateSeparatorItem[] => {
|
|
68
|
+
return addDateSeparators(windowedMessages, vs.isActive ? vs.windowStart : 0);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// Get last user message ID for undo button logic
|
|
72
|
+
const lastUserMessageId = $derived.by(() => {
|
|
73
|
+
const userMessages = filteredMessages.filter(m => m.type === 'user');
|
|
74
|
+
if (userMessages.length === 0) return undefined;
|
|
75
|
+
const lastUserMsg = userMessages[userMessages.length - 1];
|
|
76
|
+
return lastUserMsg.metadata?.message_id;
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
// ========================================
|
|
80
|
+
// SCROLL HELPERS
|
|
81
|
+
// ========================================
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Resolve the best scroll container:
|
|
85
|
+
* - Use our own internal messagesScrollEl (which has overflow-y-auto)
|
|
86
|
+
* - Fall back to external scrollContainer from PageTemplate
|
|
87
|
+
*/
|
|
88
|
+
function getScrollEl(): HTMLElement | undefined {
|
|
89
|
+
return messagesScrollEl ?? scrollContainer;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Auto-scroll to bottom when new messages arrive
|
|
93
|
+
function scrollMessagesToBottom(smooth = true) {
|
|
94
|
+
const el = getScrollEl();
|
|
95
|
+
if (el) {
|
|
96
|
+
// During streaming, always use instant scroll to avoid jitter
|
|
97
|
+
// caused by smooth animation being outpaced by rapid DOM updates
|
|
98
|
+
const useSmooth = smooth && !appState.isLoading;
|
|
99
|
+
el.scrollTo({
|
|
100
|
+
top: el.scrollHeight,
|
|
101
|
+
behavior: useSmooth ? 'smooth' : 'auto'
|
|
102
|
+
});
|
|
103
|
+
// Immediately mark as at bottom after programmatic scroll
|
|
104
|
+
isUserAtBottom = true;
|
|
105
|
+
// Lock scroll detection briefly so scroll events fired during
|
|
106
|
+
// the programmatic scroll don't override isUserAtBottom
|
|
107
|
+
scrollLockUntil = Date.now() + 150;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Scroll detection with bottom position tracking
|
|
112
|
+
function handleMessagesScroll() {
|
|
113
|
+
// Don't override isUserAtBottom during/after a programmatic scroll
|
|
114
|
+
if (Date.now() < scrollLockUntil) return;
|
|
115
|
+
|
|
116
|
+
const el = getScrollEl();
|
|
117
|
+
if (el) {
|
|
118
|
+
const { scrollTop, scrollHeight, clientHeight } = el;
|
|
119
|
+
const threshold = 200;
|
|
120
|
+
isUserAtBottom = scrollTop + clientHeight >= scrollHeight - threshold;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// ========================================
|
|
125
|
+
// VIRTUAL SCROLL: SENTINELS & OBSERVERS
|
|
126
|
+
// ========================================
|
|
127
|
+
|
|
128
|
+
function onTopSentinelVisible() {
|
|
129
|
+
if (!vs.hasMoreAbove || isLoadingOlder) return;
|
|
130
|
+
isLoadingOlder = true;
|
|
131
|
+
|
|
132
|
+
const el = getScrollEl();
|
|
133
|
+
if (!el) { isLoadingOlder = false; return; }
|
|
134
|
+
|
|
135
|
+
// Capture scroll state before expanding window
|
|
136
|
+
const prevScrollHeight = el.scrollHeight;
|
|
137
|
+
const prevScrollTop = el.scrollTop;
|
|
138
|
+
|
|
139
|
+
const added = vs.expandUp();
|
|
140
|
+
if (added > 0) {
|
|
141
|
+
// After DOM updates, restore scroll position so content doesn't jump
|
|
142
|
+
tick().then(() => {
|
|
143
|
+
requestAnimationFrame(() => {
|
|
144
|
+
const newScrollHeight = el.scrollHeight;
|
|
145
|
+
const heightAdded = newScrollHeight - prevScrollHeight;
|
|
146
|
+
el.scrollTop = prevScrollTop + heightAdded;
|
|
147
|
+
|
|
148
|
+
setTimeout(() => { isLoadingOlder = false; }, 150);
|
|
149
|
+
|
|
150
|
+
// Trim bottom if window grew too large and not streaming
|
|
151
|
+
if (vs.windowEnd - vs.windowStart > VS_CONFIG.TRIM_THRESHOLD && !appState.isLoading) {
|
|
152
|
+
vs.trimBottom();
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
} else {
|
|
157
|
+
isLoadingOlder = false;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function onBottomSentinelVisible() {
|
|
162
|
+
if (!vs.hasMoreBelow) return;
|
|
163
|
+
|
|
164
|
+
vs.expandDown();
|
|
165
|
+
|
|
166
|
+
// Trim top if window grew too large
|
|
167
|
+
if (vs.windowEnd - vs.windowStart > VS_CONFIG.TRIM_THRESHOLD) {
|
|
168
|
+
vs.trimTop();
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function setupObservers() {
|
|
173
|
+
cleanupObservers();
|
|
174
|
+
|
|
175
|
+
if (!vs.isActive) return;
|
|
176
|
+
|
|
177
|
+
const root = getScrollEl();
|
|
178
|
+
if (!root) return;
|
|
179
|
+
|
|
180
|
+
topObserver = new IntersectionObserver(
|
|
181
|
+
(entries) => {
|
|
182
|
+
if (entries[0]?.isIntersecting) onTopSentinelVisible();
|
|
183
|
+
},
|
|
184
|
+
{ root, rootMargin: '200px 0px 0px 0px', threshold: 0 }
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
bottomObserver = new IntersectionObserver(
|
|
188
|
+
(entries) => {
|
|
189
|
+
if (entries[0]?.isIntersecting) onBottomSentinelVisible();
|
|
190
|
+
},
|
|
191
|
+
{ root, rootMargin: '0px 0px 200px 0px', threshold: 0 }
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
if (topSentinelEl) topObserver.observe(topSentinelEl);
|
|
195
|
+
if (bottomSentinelEl) bottomObserver.observe(bottomSentinelEl);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function cleanupObservers() {
|
|
199
|
+
topObserver?.disconnect();
|
|
200
|
+
bottomObserver?.disconnect();
|
|
201
|
+
topObserver = null;
|
|
202
|
+
bottomObserver = null;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Re-setup observers when elements or activation state changes
|
|
206
|
+
$effect(() => {
|
|
207
|
+
const active = vs.isActive;
|
|
208
|
+
topSentinelEl;
|
|
209
|
+
bottomSentinelEl;
|
|
210
|
+
messagesScrollEl;
|
|
211
|
+
|
|
212
|
+
if (active && messagesScrollEl) {
|
|
213
|
+
setupObservers();
|
|
214
|
+
} else {
|
|
215
|
+
cleanupObservers();
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
// ========================================
|
|
220
|
+
// AUTO-SCROLL & VIRTUAL SCROLL SYNC
|
|
221
|
+
// ========================================
|
|
222
|
+
|
|
223
|
+
// Auto-scroll: track message count and partial message updates
|
|
224
|
+
$effect(() => {
|
|
225
|
+
const currentMessageCount = filteredMessages.length;
|
|
226
|
+
const isNewMessage = currentMessageCount > previousMessageCount;
|
|
227
|
+
const isMessageCountDecreased = currentMessageCount < previousMessageCount;
|
|
228
|
+
|
|
229
|
+
// Sync virtual scroll with message count changes
|
|
230
|
+
if (hasInitiallyScrolled) {
|
|
231
|
+
if (isMessageCountDecreased) {
|
|
232
|
+
vs.reset(currentMessageCount);
|
|
233
|
+
} else if (isNewMessage) {
|
|
234
|
+
vs.sync(currentMessageCount, isUserAtBottom, appState.isLoading);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Track partial text changes across ALL streaming messages (not just the last)
|
|
239
|
+
// This handles reasoning + text streams both being active simultaneously
|
|
240
|
+
let partialTextChanged = false;
|
|
241
|
+
let hasStreamingMessage = false;
|
|
242
|
+
let currentPartialHash = '';
|
|
243
|
+
for (const msg of filteredMessages) {
|
|
244
|
+
if ('type' in msg && msg.type === 'stream_event' && 'partialText' in msg) {
|
|
245
|
+
hasStreamingMessage = true;
|
|
246
|
+
currentPartialHash += `${(msg as any).partialText?.length || 0}:`;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
if (currentPartialHash !== lastPartialTextHash) {
|
|
250
|
+
if (currentPartialHash !== '') partialTextChanged = true;
|
|
251
|
+
lastPartialTextHash = currentPartialHash;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Check for tool result updates ($result additions) across all messages
|
|
255
|
+
let toolResultChanged = false;
|
|
256
|
+
let currentToolResultsHash = '';
|
|
257
|
+
|
|
258
|
+
// Build a hash of all tool results to detect changes
|
|
259
|
+
for (const message of filteredMessages) {
|
|
260
|
+
if ('message' in message) {
|
|
261
|
+
const messageContent = (message as any).message?.content;
|
|
262
|
+
if (Array.isArray(messageContent)) {
|
|
263
|
+
for (const item of messageContent) {
|
|
264
|
+
if (item?.type === 'tool_use' && item.$result) {
|
|
265
|
+
currentToolResultsHash += `${item.id}:${JSON.stringify(item.$result).length}|`;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Check if tool results have changed
|
|
273
|
+
if (currentToolResultsHash !== lastToolResultsHash) {
|
|
274
|
+
toolResultChanged = true;
|
|
275
|
+
lastToolResultsHash = currentToolResultsHash;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// When message count decreases (e.g. after undo/reload), scroll to bottom
|
|
279
|
+
if (isMessageCountDecreased && hasInitiallyScrolled) {
|
|
280
|
+
requestAnimationFrame(() => {
|
|
281
|
+
scrollMessagesToBottom(false);
|
|
282
|
+
});
|
|
283
|
+
previousMessageCount = currentMessageCount;
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Scroll on new message, partial text changes, or tool result changes
|
|
288
|
+
if ((isNewMessage || partialTextChanged || toolResultChanged) && isUserAtBottom) {
|
|
289
|
+
requestAnimationFrame(() => {
|
|
290
|
+
if (editModeState.isEditing) return;
|
|
291
|
+
|
|
292
|
+
// During streaming, scroll immediately (instant via scrollMessagesToBottom)
|
|
293
|
+
// For non-streaming changes, add a small delay for smooth UX
|
|
294
|
+
if (partialTextChanged || hasStreamingMessage) {
|
|
295
|
+
scrollMessagesToBottom(false);
|
|
296
|
+
} else {
|
|
297
|
+
const delay = toolResultChanged ? 50 : 100;
|
|
298
|
+
setTimeout(() => {
|
|
299
|
+
if (!editModeState.isEditing) {
|
|
300
|
+
scrollMessagesToBottom(true);
|
|
301
|
+
}
|
|
302
|
+
}, delay);
|
|
303
|
+
}
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Reset partial text hash when not streaming
|
|
308
|
+
if (!hasStreamingMessage && lastPartialTextHash !== '') {
|
|
309
|
+
lastPartialTextHash = '';
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
previousMessageCount = currentMessageCount;
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
// ========================================
|
|
316
|
+
// TRANSITIONS & SCROLL LISTENERS
|
|
317
|
+
// ========================================
|
|
318
|
+
|
|
319
|
+
// Track if we should disable transitions (during restoration)
|
|
320
|
+
const disableTransitions = $derived(appState.isRestoring || (!hasInitiallyScrolled && filteredMessages.length > 5));
|
|
321
|
+
|
|
322
|
+
// Update scroll event listener when internal scroll element changes
|
|
323
|
+
let currentListenerEl: HTMLElement | null = null;
|
|
324
|
+
$effect(() => {
|
|
325
|
+
const el = messagesScrollEl ?? scrollContainer;
|
|
326
|
+
|
|
327
|
+
// Remove old listener if exists
|
|
328
|
+
if (currentListenerEl && currentListenerEl !== el) {
|
|
329
|
+
currentListenerEl.removeEventListener('scroll', handleMessagesScroll);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Add new listener
|
|
333
|
+
if (el && el !== currentListenerEl) {
|
|
334
|
+
el.addEventListener('scroll', handleMessagesScroll, { passive: true });
|
|
335
|
+
currentListenerEl = el;
|
|
336
|
+
}
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
// ========================================
|
|
340
|
+
// RESTORATION & INITIAL SCROLL
|
|
341
|
+
// ========================================
|
|
342
|
+
|
|
343
|
+
// Handle restoration and initial scroll
|
|
344
|
+
$effect(() => {
|
|
345
|
+
const currentMessages = filteredMessages.length;
|
|
346
|
+
const isRestoring = appState.isRestoring;
|
|
347
|
+
|
|
348
|
+
if (isRestoring) {
|
|
349
|
+
// Keep content hidden during restoration
|
|
350
|
+
isContentReady = false;
|
|
351
|
+
hasInitiallyScrolled = false;
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// Restoration complete
|
|
356
|
+
if (!isRestoring && !hasInitiallyScrolled) {
|
|
357
|
+
const el = getScrollEl();
|
|
358
|
+
if (currentMessages > 0 && el) {
|
|
359
|
+
// Mark as scrolled
|
|
360
|
+
hasInitiallyScrolled = true;
|
|
361
|
+
previousMessageCount = currentMessages;
|
|
362
|
+
|
|
363
|
+
// Initialize virtual scroll
|
|
364
|
+
vs.reset(currentMessages);
|
|
365
|
+
|
|
366
|
+
// Scroll to bottom immediately (while still hidden)
|
|
367
|
+
// Skip if edit mode is active - scroll-to-edit-message will handle positioning
|
|
368
|
+
if (!editModeState.isEditing) {
|
|
369
|
+
el.scrollTop = el.scrollHeight;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// Show content after a micro delay to ensure scroll is set
|
|
373
|
+
requestAnimationFrame(() => {
|
|
374
|
+
isContentReady = true;
|
|
375
|
+
});
|
|
376
|
+
} else if (currentMessages === 0) {
|
|
377
|
+
// No messages, show immediately
|
|
378
|
+
isContentReady = true;
|
|
379
|
+
hasInitiallyScrolled = true;
|
|
380
|
+
} else {
|
|
381
|
+
// Wait for container
|
|
382
|
+
setTimeout(() => {
|
|
383
|
+
const el2 = getScrollEl();
|
|
384
|
+
if (el2) {
|
|
385
|
+
vs.reset(filteredMessages.length);
|
|
386
|
+
el2.scrollTop = el2.scrollHeight;
|
|
387
|
+
hasInitiallyScrolled = true;
|
|
388
|
+
}
|
|
389
|
+
isContentReady = true;
|
|
390
|
+
}, 50);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
// ========================================
|
|
396
|
+
// EDIT MODE: SCROLL TO EDITED MESSAGE
|
|
397
|
+
// ========================================
|
|
398
|
+
|
|
399
|
+
// Auto-scroll to edited message when edit mode is activated (including refresh/project switch)
|
|
400
|
+
let lastEditMessageId: string | null = null;
|
|
401
|
+
|
|
402
|
+
function scrollToEditedMessage(messageId: string, retries = 10) {
|
|
403
|
+
if (!messagesScrollEl || !isContentReady) {
|
|
404
|
+
// Container not ready yet (refresh/project switch), retry
|
|
405
|
+
if (retries > 0) {
|
|
406
|
+
setTimeout(() => scrollToEditedMessage(messageId, retries - 1), 200);
|
|
407
|
+
}
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// Ensure message is within the virtual scroll window
|
|
412
|
+
if (vs.isActive) {
|
|
413
|
+
const msgIndex = filteredMessages.findIndex(m => m.metadata?.message_id === messageId);
|
|
414
|
+
if (msgIndex >= 0 && (msgIndex < vs.windowStart || msgIndex >= vs.windowEnd)) {
|
|
415
|
+
vs.ensureVisible(msgIndex);
|
|
416
|
+
// Wait for DOM to update after window change
|
|
417
|
+
tick().then(() => {
|
|
418
|
+
requestAnimationFrame(() => {
|
|
419
|
+
const el = messagesScrollEl?.querySelector(`[data-message-id="${messageId}"]`);
|
|
420
|
+
if (el) {
|
|
421
|
+
el.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
422
|
+
}
|
|
423
|
+
});
|
|
424
|
+
});
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
const el = messagesScrollEl.querySelector(`[data-message-id="${messageId}"]`);
|
|
430
|
+
if (el) {
|
|
431
|
+
el.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
432
|
+
} else if (retries > 0) {
|
|
433
|
+
// Messages might not be rendered yet, retry
|
|
434
|
+
setTimeout(() => scrollToEditedMessage(messageId, retries - 1), 200);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
$effect(() => {
|
|
439
|
+
const editMessageId = editModeState.isEditing ? editModeState.messageId : null;
|
|
440
|
+
|
|
441
|
+
if (editMessageId && editMessageId !== lastEditMessageId) {
|
|
442
|
+
// Wait for DOM to update then scroll to the edited message
|
|
443
|
+
requestAnimationFrame(() => {
|
|
444
|
+
scrollToEditedMessage(editMessageId);
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
lastEditMessageId = editMessageId;
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
// ========================================
|
|
452
|
+
// LIFECYCLE
|
|
453
|
+
// ========================================
|
|
454
|
+
|
|
455
|
+
onMount(() => {
|
|
456
|
+
// Reset flags on mount
|
|
457
|
+
hasInitiallyScrolled = false;
|
|
458
|
+
|
|
459
|
+
// Start with content hidden if restoring
|
|
460
|
+
if (appState.isRestoring) {
|
|
461
|
+
isContentReady = false;
|
|
462
|
+
} else {
|
|
463
|
+
// Not restoring, check if we need to scroll
|
|
464
|
+
const el = getScrollEl();
|
|
465
|
+
if (filteredMessages.length > 0 && el) {
|
|
466
|
+
// Initialize virtual scroll and scroll immediately
|
|
467
|
+
vs.reset(filteredMessages.length);
|
|
468
|
+
el.scrollTop = el.scrollHeight;
|
|
469
|
+
hasInitiallyScrolled = true;
|
|
470
|
+
isContentReady = true;
|
|
471
|
+
} else {
|
|
472
|
+
// No messages or no container yet, just show
|
|
473
|
+
isContentReady = true;
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// Cleanup on component destroy
|
|
478
|
+
return () => {
|
|
479
|
+
cleanupObservers();
|
|
480
|
+
if (currentListenerEl) {
|
|
481
|
+
currentListenerEl.removeEventListener('scroll', handleMessagesScroll);
|
|
482
|
+
}
|
|
483
|
+
hasInitiallyScrolled = false;
|
|
484
|
+
};
|
|
485
|
+
});
|
|
486
|
+
</script>
|
|
487
|
+
|
|
488
|
+
<div class="flex flex-col h-full" style="position: relative">
|
|
489
|
+
<!-- Messages container -->
|
|
490
|
+
<div
|
|
491
|
+
bind:this={messagesScrollEl}
|
|
492
|
+
class="flex-1 overflow-y-auto pt-3 pb-14 lg:pb-16 px-3 lg:px-4 {isContentReady ? '' : 'invisible'}"
|
|
493
|
+
style="{!isContentReady ? 'scroll-behavior: auto' : ''}">
|
|
494
|
+
|
|
495
|
+
<!-- Top sentinel for virtual scroll (always present, observed only when active) -->
|
|
496
|
+
<div bind:this={topSentinelEl} class="h-px w-full" aria-hidden="true"></div>
|
|
497
|
+
|
|
498
|
+
<!-- Loading older messages indicator -->
|
|
499
|
+
{#if isLoadingOlder}
|
|
500
|
+
<div class="flex justify-center py-3">
|
|
501
|
+
<div class="flex items-center gap-2 text-xs text-slate-400 dark:text-slate-500">
|
|
502
|
+
<div class="w-3 h-3 border-2 border-slate-300 dark:border-slate-600 border-t-transparent rounded-full animate-spin"></div>
|
|
503
|
+
<span>Loading older messages...</span>
|
|
504
|
+
</div>
|
|
505
|
+
</div>
|
|
506
|
+
{/if}
|
|
507
|
+
|
|
508
|
+
{#each messagesWithDateSeparators as item (item.key)}
|
|
509
|
+
{#if disableTransitions}
|
|
510
|
+
<!-- No transition during restoration or initial load with many messages -->
|
|
511
|
+
<div>
|
|
512
|
+
{#if item.type === 'date'}
|
|
513
|
+
<DateSeparator date={item.data} />
|
|
514
|
+
{:else if item.type === 'message'}
|
|
515
|
+
{@const messageId = item.data.metadata?.message_id}
|
|
516
|
+
{@const isLastUser = messageId === lastUserMessageId}
|
|
517
|
+
<div class="mb-2 lg:mb-4" data-message-id={messageId}>
|
|
518
|
+
<ChatMessage message={item.data} isLastUserMessage={isLastUser} />
|
|
519
|
+
</div>
|
|
520
|
+
{/if}
|
|
521
|
+
</div>
|
|
522
|
+
{:else}
|
|
523
|
+
<!-- Normal transition for new messages -->
|
|
524
|
+
<div in:fade={{ duration: 300, delay: 0 }} out:fade={{ duration: 200 }}>
|
|
525
|
+
{#if item.type === 'date'}
|
|
526
|
+
<DateSeparator date={item.data} />
|
|
527
|
+
{:else if item.type === 'message'}
|
|
528
|
+
{@const messageId = item.data.metadata?.message_id}
|
|
529
|
+
{@const isLastUser = messageId === lastUserMessageId}
|
|
530
|
+
<div class="mb-2 lg:mb-4" data-message-id={messageId}>
|
|
531
|
+
<ChatMessage message={item.data} isLastUserMessage={isLastUser} />
|
|
532
|
+
</div>
|
|
533
|
+
{/if}
|
|
534
|
+
</div>
|
|
535
|
+
{/if}
|
|
536
|
+
{/each}
|
|
537
|
+
|
|
538
|
+
<!-- Bottom sentinel for virtual scroll -->
|
|
539
|
+
<div bind:this={bottomSentinelEl} class="h-px w-full" aria-hidden="true"></div>
|
|
540
|
+
</div>
|
|
541
|
+
</div>
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
Date Separator Component
|
|
3
|
+
|
|
4
|
+
Features:
|
|
5
|
+
- WhatsApp-style date separator
|
|
6
|
+
- Center-aligned with subtle styling
|
|
7
|
+
- Shows dates in user-friendly format
|
|
8
|
+
- Responsive design
|
|
9
|
+
-->
|
|
10
|
+
|
|
11
|
+
<script lang="ts">
|
|
12
|
+
const { date }: { date: string } = $props();
|
|
13
|
+
|
|
14
|
+
// Format date for display
|
|
15
|
+
const formatDate = (dateString: string) => {
|
|
16
|
+
const messageDate = new Date(dateString);
|
|
17
|
+
const today = new Date();
|
|
18
|
+
const yesterday = new Date(today);
|
|
19
|
+
yesterday.setDate(yesterday.getDate() - 1);
|
|
20
|
+
|
|
21
|
+
// Check if it's today
|
|
22
|
+
if (messageDate.toDateString() === today.toDateString()) {
|
|
23
|
+
return 'Today';
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Check if it's yesterday
|
|
27
|
+
if (messageDate.toDateString() === yesterday.toDateString()) {
|
|
28
|
+
return 'Yesterday';
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// For older dates, show day and date
|
|
32
|
+
const dayNames = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
|
|
33
|
+
const monthNames = [
|
|
34
|
+
'January',
|
|
35
|
+
'February',
|
|
36
|
+
'March',
|
|
37
|
+
'April',
|
|
38
|
+
'May',
|
|
39
|
+
'June',
|
|
40
|
+
'July',
|
|
41
|
+
'August',
|
|
42
|
+
'September',
|
|
43
|
+
'October',
|
|
44
|
+
'November',
|
|
45
|
+
'December'
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
const dayName = dayNames[messageDate.getDay()];
|
|
49
|
+
const day = messageDate.getDate();
|
|
50
|
+
const month = monthNames[messageDate.getMonth()];
|
|
51
|
+
const year = messageDate.getFullYear();
|
|
52
|
+
|
|
53
|
+
// Show year only if it's not current year
|
|
54
|
+
if (year === today.getFullYear()) {
|
|
55
|
+
return `${dayName}, ${day} ${month}`;
|
|
56
|
+
} else {
|
|
57
|
+
return `${dayName}, ${day} ${month} ${year}`;
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const displayDate = $derived(formatDate(date));
|
|
62
|
+
</script>
|
|
63
|
+
|
|
64
|
+
<!-- Date Separator -->
|
|
65
|
+
<div class="flex items-center justify-center my-3 md:my-4 px-4 md:px-6 select-none">
|
|
66
|
+
<div class="relative flex items-center w-full max-w-md">
|
|
67
|
+
<!-- Left line -->
|
|
68
|
+
<div
|
|
69
|
+
class="flex-grow h-px bg-gradient-to-r from-transparent via-violet-500/10 to-violet-500/20"
|
|
70
|
+
></div>
|
|
71
|
+
|
|
72
|
+
<!-- Date badge - WhatsApp style -->
|
|
73
|
+
<div
|
|
74
|
+
class="px-3 py-1 mx-2.5 bg-slate-100/80 dark:bg-slate-600/20 backdrop-blur-sm rounded-lg border border-slate-200 dark:border-slate-800 shadow-sm"
|
|
75
|
+
>
|
|
76
|
+
<span class="text-xs font-medium text-slate-500 whitespace-nowrap tracking-wide">
|
|
77
|
+
{displayDate}
|
|
78
|
+
</span>
|
|
79
|
+
</div>
|
|
80
|
+
|
|
81
|
+
<!-- Right line -->
|
|
82
|
+
<div
|
|
83
|
+
class="flex-grow h-px bg-gradient-to-l from-transparent via-violet-500/10 to-violet-500/20"
|
|
84
|
+
></div>
|
|
85
|
+
</div>
|
|
86
|
+
</div>
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
Message Bubble Component
|
|
3
|
+
|
|
4
|
+
Features:
|
|
5
|
+
- Message content wrapper
|
|
6
|
+
- Card styling
|
|
7
|
+
- Header and content sections
|
|
8
|
+
- Hover effects
|
|
9
|
+
-->
|
|
10
|
+
|
|
11
|
+
<script lang="ts">
|
|
12
|
+
import type { SDKMessageFormatter } from '$shared/types/database/schema';
|
|
13
|
+
import type { IconName } from '$shared/types/ui/icons';
|
|
14
|
+
import Card from '$frontend/lib/components/common/Card.svelte';
|
|
15
|
+
import MessageFormatter from '../formatters/MessageFormatter.svelte';
|
|
16
|
+
import MessageHeader from './MessageHeader.svelte';
|
|
17
|
+
|
|
18
|
+
const {
|
|
19
|
+
message,
|
|
20
|
+
messageTimestamp,
|
|
21
|
+
isLastUserMessage = false,
|
|
22
|
+
roleConfig,
|
|
23
|
+
roleCategory,
|
|
24
|
+
agentStatus,
|
|
25
|
+
senderName,
|
|
26
|
+
hasTokenUsageData,
|
|
27
|
+
formatTime,
|
|
28
|
+
onCopy,
|
|
29
|
+
onRestore,
|
|
30
|
+
onEdit,
|
|
31
|
+
onShowTokenUsage,
|
|
32
|
+
onShowDebug
|
|
33
|
+
}: {
|
|
34
|
+
message: SDKMessageFormatter;
|
|
35
|
+
messageTimestamp: string;
|
|
36
|
+
isLastUserMessage?: boolean;
|
|
37
|
+
roleConfig: { gradient: string; icon: IconName; name: string };
|
|
38
|
+
roleCategory: 'user' | 'assistant' | 'agent' | string;
|
|
39
|
+
agentStatus: 'processing' | 'success' | 'error' | null;
|
|
40
|
+
senderName: string | null;
|
|
41
|
+
hasTokenUsageData: any;
|
|
42
|
+
formatTime: (timestamp?: string) => string;
|
|
43
|
+
onCopy: () => void;
|
|
44
|
+
onRestore: () => void;
|
|
45
|
+
onEdit: () => void;
|
|
46
|
+
onShowTokenUsage: () => void;
|
|
47
|
+
onShowDebug: () => void;
|
|
48
|
+
} = $props();
|
|
49
|
+
</script>
|
|
50
|
+
|
|
51
|
+
<div class="relative overflow-hidden">
|
|
52
|
+
<Card
|
|
53
|
+
variant="outlined"
|
|
54
|
+
padding="none"
|
|
55
|
+
class="bg-white dark:bg-slate-900 text-slate-900 dark:text-slate-100 border border-slate-200 dark:border-slate-700 overflow-hidden"
|
|
56
|
+
>
|
|
57
|
+
<!-- Message Header -->
|
|
58
|
+
<MessageHeader
|
|
59
|
+
{message}
|
|
60
|
+
{messageTimestamp}
|
|
61
|
+
{isLastUserMessage}
|
|
62
|
+
{roleConfig}
|
|
63
|
+
{roleCategory}
|
|
64
|
+
{agentStatus}
|
|
65
|
+
{senderName}
|
|
66
|
+
{hasTokenUsageData}
|
|
67
|
+
{formatTime}
|
|
68
|
+
{onCopy}
|
|
69
|
+
{onRestore}
|
|
70
|
+
{onEdit}
|
|
71
|
+
{onShowTokenUsage}
|
|
72
|
+
{onShowDebug}
|
|
73
|
+
/>
|
|
74
|
+
|
|
75
|
+
<!-- Message Content -->
|
|
76
|
+
<div class="p-3 md:p-4">
|
|
77
|
+
<div class="max-w-none">
|
|
78
|
+
<!-- Content rendering using MessageFormatter component -->
|
|
79
|
+
<MessageFormatter {message} />
|
|
80
|
+
</div>
|
|
81
|
+
</div>
|
|
82
|
+
</Card>
|
|
83
|
+
|
|
84
|
+
<!-- Hover glow effect -->
|
|
85
|
+
<div class="absolute inset-0 bg-gradient-to-r from-violet-500/5 to-violet-500/5 rounded-xl opacity-0 group-hover:opacity-100 transition-opacity -z-10 blur-xl"></div>
|
|
86
|
+
</div>
|