@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,307 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gitignore-aware file filtering
|
|
3
|
+
*
|
|
4
|
+
* Two strategies:
|
|
5
|
+
* 1. Git repo: Use `git ls-files -co --exclude-standard` (perfect accuracy)
|
|
6
|
+
* 2. Non-git repo: Parse .gitignore files manually (fallback)
|
|
7
|
+
*
|
|
8
|
+
* Handles nested .gitignore files in subdirectories with proper scoping.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import fs from 'fs/promises';
|
|
12
|
+
import path from 'path';
|
|
13
|
+
import { debug } from '$shared/utils/logger';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Safety-net directories to always exclude regardless of .gitignore.
|
|
17
|
+
* These are never useful in snapshots and could be massive.
|
|
18
|
+
*/
|
|
19
|
+
const ALWAYS_EXCLUDE_DIRS = new Set([
|
|
20
|
+
'.git',
|
|
21
|
+
'node_modules',
|
|
22
|
+
]);
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Get list of snapshot-eligible files using git (preferred) or manual scan.
|
|
26
|
+
* Returns full absolute paths.
|
|
27
|
+
*/
|
|
28
|
+
export async function getSnapshotFiles(projectPath: string): Promise<string[]> {
|
|
29
|
+
// Try git-based scan first (handles all .gitignore rules perfectly)
|
|
30
|
+
const gitFiles = await scanWithGit(projectPath);
|
|
31
|
+
if (gitFiles !== null) {
|
|
32
|
+
return gitFiles;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Fallback: manual scan with .gitignore parsing
|
|
36
|
+
return scanWithGitignoreParsing(projectPath);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// ============================================================================
|
|
40
|
+
// Strategy 1: Git-based scanning
|
|
41
|
+
// ============================================================================
|
|
42
|
+
|
|
43
|
+
async function scanWithGit(dirPath: string): Promise<string[] | null> {
|
|
44
|
+
// Check if this is a git repo
|
|
45
|
+
try {
|
|
46
|
+
await fs.access(path.join(dirPath, '.git'));
|
|
47
|
+
} catch {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
const proc = Bun.spawn(
|
|
53
|
+
['git', 'ls-files', '-co', '--exclude-standard'],
|
|
54
|
+
{ cwd: dirPath, stdout: 'pipe', stderr: 'pipe' }
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
const output = await new Response(proc.stdout).text();
|
|
58
|
+
const exitCode = await proc.exited;
|
|
59
|
+
|
|
60
|
+
if (exitCode !== 0) return null;
|
|
61
|
+
|
|
62
|
+
const files: string[] = [];
|
|
63
|
+
for (const line of output.split('\n')) {
|
|
64
|
+
const relativePath = line.trim();
|
|
65
|
+
if (!relativePath) continue;
|
|
66
|
+
|
|
67
|
+
// Skip always-excluded directories
|
|
68
|
+
const firstSegment = relativePath.split('/')[0];
|
|
69
|
+
if (ALWAYS_EXCLUDE_DIRS.has(firstSegment)) continue;
|
|
70
|
+
|
|
71
|
+
files.push(path.join(dirPath, relativePath));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
debug.log('snapshot', `Git scan found ${files.length} files`);
|
|
75
|
+
return files;
|
|
76
|
+
} catch (err) {
|
|
77
|
+
debug.warn('snapshot', 'git ls-files failed, falling back to manual scan:', err);
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ============================================================================
|
|
83
|
+
// Strategy 2: Manual scan with .gitignore parsing
|
|
84
|
+
// ============================================================================
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Rule from a .gitignore file.
|
|
88
|
+
* scope = relative directory containing the .gitignore ('' for root).
|
|
89
|
+
*/
|
|
90
|
+
interface IgnoreRule {
|
|
91
|
+
pattern: RegExp;
|
|
92
|
+
negate: boolean;
|
|
93
|
+
dirOnly: boolean; // pattern ends with /
|
|
94
|
+
scope: string; // relative dir of the .gitignore that defined this rule
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Parse a single .gitignore line into an IgnoreRule (or null if comment/blank).
|
|
99
|
+
*/
|
|
100
|
+
function parseGitignoreLine(line: string, scope: string): IgnoreRule | null {
|
|
101
|
+
// Strip trailing whitespace (unless escaped)
|
|
102
|
+
let trimmed = line.replace(/(?<!\\)\s+$/, '');
|
|
103
|
+
|
|
104
|
+
// Skip empty lines and comments
|
|
105
|
+
if (!trimmed || trimmed.startsWith('#')) return null;
|
|
106
|
+
|
|
107
|
+
// Check for negation
|
|
108
|
+
let negate = false;
|
|
109
|
+
if (trimmed.startsWith('!')) {
|
|
110
|
+
negate = true;
|
|
111
|
+
trimmed = trimmed.slice(1);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Check for directory-only marker
|
|
115
|
+
let dirOnly = false;
|
|
116
|
+
if (trimmed.endsWith('/')) {
|
|
117
|
+
dirOnly = true;
|
|
118
|
+
trimmed = trimmed.slice(0, -1);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Determine if pattern is anchored (contains / other than at end)
|
|
122
|
+
const anchored = trimmed.includes('/');
|
|
123
|
+
|
|
124
|
+
// Build regex from glob pattern
|
|
125
|
+
const regexStr = globToRegex(trimmed, anchored, scope);
|
|
126
|
+
try {
|
|
127
|
+
const pattern = new RegExp(regexStr);
|
|
128
|
+
return { pattern, negate, dirOnly, scope };
|
|
129
|
+
} catch {
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Convert a gitignore glob pattern to a regex string.
|
|
136
|
+
*
|
|
137
|
+
* Rules:
|
|
138
|
+
* - `*` matches anything except /
|
|
139
|
+
* - `**` matches everything (including /)
|
|
140
|
+
* - `?` matches any single char except /
|
|
141
|
+
* - If pattern contains / (anchored), match from scope root
|
|
142
|
+
* - If pattern has no / (unanchored), match basename anywhere
|
|
143
|
+
*/
|
|
144
|
+
function globToRegex(pattern: string, anchored: boolean, scope: string): string {
|
|
145
|
+
let result = '';
|
|
146
|
+
|
|
147
|
+
// Process pattern character by character
|
|
148
|
+
let i = 0;
|
|
149
|
+
while (i < pattern.length) {
|
|
150
|
+
const ch = pattern[i];
|
|
151
|
+
|
|
152
|
+
if (ch === '*') {
|
|
153
|
+
if (pattern[i + 1] === '*') {
|
|
154
|
+
// ** pattern
|
|
155
|
+
if (pattern[i + 2] === '/') {
|
|
156
|
+
// **/ = match zero or more directories
|
|
157
|
+
result += '(?:.*/)?';
|
|
158
|
+
i += 3;
|
|
159
|
+
} else if (i + 2 === pattern.length) {
|
|
160
|
+
// ** at end = match everything
|
|
161
|
+
result += '.*';
|
|
162
|
+
i += 2;
|
|
163
|
+
} else {
|
|
164
|
+
// ** followed by something else
|
|
165
|
+
result += '.*';
|
|
166
|
+
i += 2;
|
|
167
|
+
}
|
|
168
|
+
} else {
|
|
169
|
+
// single * = match anything except /
|
|
170
|
+
result += '[^/]*';
|
|
171
|
+
i++;
|
|
172
|
+
}
|
|
173
|
+
} else if (ch === '?') {
|
|
174
|
+
result += '[^/]';
|
|
175
|
+
i++;
|
|
176
|
+
} else if (ch === '[') {
|
|
177
|
+
// Character class - pass through
|
|
178
|
+
const end = pattern.indexOf(']', i + 1);
|
|
179
|
+
if (end !== -1) {
|
|
180
|
+
result += pattern.slice(i, end + 1);
|
|
181
|
+
i = end + 1;
|
|
182
|
+
} else {
|
|
183
|
+
result += '\\[';
|
|
184
|
+
i++;
|
|
185
|
+
}
|
|
186
|
+
} else if ('.+^${}()|\\'.includes(ch)) {
|
|
187
|
+
// Escape regex special chars
|
|
188
|
+
result += '\\' + ch;
|
|
189
|
+
i++;
|
|
190
|
+
} else if (ch === '/') {
|
|
191
|
+
result += '/';
|
|
192
|
+
i++;
|
|
193
|
+
} else {
|
|
194
|
+
result += ch;
|
|
195
|
+
i++;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Build final regex based on anchoring
|
|
200
|
+
if (anchored) {
|
|
201
|
+
// Pattern is relative to the .gitignore scope
|
|
202
|
+
const prefix = scope ? scope + '/' : '';
|
|
203
|
+
return '^' + escapeRegex(prefix) + result + '(?:/.*)?$';
|
|
204
|
+
} else {
|
|
205
|
+
// Unanchored: match basename anywhere, or as a path segment
|
|
206
|
+
return '(?:^|/)' + result + '(?:/.*)?$';
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function escapeRegex(str: string): string {
|
|
211
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Gitignore rule collector.
|
|
216
|
+
* Accumulates rules from multiple .gitignore files during directory traversal.
|
|
217
|
+
*/
|
|
218
|
+
class GitignoreFilter {
|
|
219
|
+
private rules: IgnoreRule[] = [];
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Load and parse a .gitignore file
|
|
223
|
+
*/
|
|
224
|
+
async loadFromFile(filepath: string, scope: string): Promise<void> {
|
|
225
|
+
try {
|
|
226
|
+
const content = await fs.readFile(filepath, 'utf-8');
|
|
227
|
+
for (const line of content.split('\n')) {
|
|
228
|
+
const rule = parseGitignoreLine(line, scope);
|
|
229
|
+
if (rule) this.rules.push(rule);
|
|
230
|
+
}
|
|
231
|
+
} catch {
|
|
232
|
+
// File doesn't exist or can't be read - no rules to add
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Check if a path should be ignored.
|
|
238
|
+
* @param relativePath - Path relative to project root (forward slashes)
|
|
239
|
+
* @param isDirectory - Whether the path is a directory
|
|
240
|
+
*/
|
|
241
|
+
isIgnored(relativePath: string, isDirectory: boolean): boolean {
|
|
242
|
+
let ignored = false;
|
|
243
|
+
|
|
244
|
+
for (const rule of this.rules) {
|
|
245
|
+
// Skip dir-only rules for files
|
|
246
|
+
if (rule.dirOnly && !isDirectory) continue;
|
|
247
|
+
|
|
248
|
+
// Check scope: rule only applies within its .gitignore directory
|
|
249
|
+
if (rule.scope && !relativePath.startsWith(rule.scope + '/') && relativePath !== rule.scope) {
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (rule.pattern.test(relativePath)) {
|
|
254
|
+
ignored = !rule.negate;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
return ignored;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Scan directory manually, parsing .gitignore files at each level.
|
|
264
|
+
*/
|
|
265
|
+
async function scanWithGitignoreParsing(projectPath: string): Promise<string[]> {
|
|
266
|
+
const files: string[] = [];
|
|
267
|
+
const filter = new GitignoreFilter();
|
|
268
|
+
|
|
269
|
+
// Load root .gitignore
|
|
270
|
+
await filter.loadFromFile(path.join(projectPath, '.gitignore'), '');
|
|
271
|
+
|
|
272
|
+
const scan = async (currentPath: string): Promise<void> => {
|
|
273
|
+
try {
|
|
274
|
+
const entries = await fs.readdir(currentPath, { withFileTypes: true });
|
|
275
|
+
const relativeDir = path.relative(projectPath, currentPath).replace(/\\/g, '/');
|
|
276
|
+
|
|
277
|
+
// Load .gitignore in this directory (if not root - root already loaded)
|
|
278
|
+
if (relativeDir) {
|
|
279
|
+
await filter.loadFromFile(path.join(currentPath, '.gitignore'), relativeDir);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
for (const entry of entries) {
|
|
283
|
+
const fullPath = path.join(currentPath, entry.name);
|
|
284
|
+
const relativePath = path.relative(projectPath, fullPath).replace(/\\/g, '/');
|
|
285
|
+
|
|
286
|
+
// Always exclude certain directories
|
|
287
|
+
if (ALWAYS_EXCLUDE_DIRS.has(entry.name)) continue;
|
|
288
|
+
|
|
289
|
+
if (entry.isDirectory()) {
|
|
290
|
+
if (!filter.isIgnored(relativePath, true)) {
|
|
291
|
+
await scan(fullPath);
|
|
292
|
+
}
|
|
293
|
+
} else if (entry.isFile()) {
|
|
294
|
+
if (!filter.isIgnored(relativePath, false)) {
|
|
295
|
+
files.push(fullPath);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
} catch (err) {
|
|
300
|
+
debug.warn('snapshot', `Could not read directory ${currentPath}:`, err);
|
|
301
|
+
}
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
await scan(projectPath);
|
|
305
|
+
debug.log('snapshot', `Manual scan found ${files.length} files`);
|
|
306
|
+
return files;
|
|
307
|
+
}
|
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
import { snapshotQueries, messageQueries, checkpointQueries } from '../database/queries';
|
|
2
|
+
import { debug } from '$shared/utils/logger';
|
|
3
|
+
import type { SDKMessage } from '$shared/types/messaging';
|
|
4
|
+
import type { DatabaseMessage } from '$shared/types/database/schema';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Snapshot domain helper functions
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export interface CheckpointNode {
|
|
11
|
+
id: string;
|
|
12
|
+
messageId: string;
|
|
13
|
+
parentId: string | null; // parent checkpoint ID in the tree
|
|
14
|
+
activeChildId: string | null; // which child continues straight
|
|
15
|
+
timestamp: string;
|
|
16
|
+
messageText: string;
|
|
17
|
+
isOnActivePath: boolean;
|
|
18
|
+
isOrphaned: boolean; // descendant of current active checkpoint
|
|
19
|
+
isCurrent: boolean; // this is the current active checkpoint
|
|
20
|
+
hasSnapshot: boolean;
|
|
21
|
+
senderName?: string | null;
|
|
22
|
+
// File change statistics (git-like)
|
|
23
|
+
filesChanged?: number;
|
|
24
|
+
insertions?: number;
|
|
25
|
+
deletions?: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface TimelineResponse {
|
|
29
|
+
nodes: CheckpointNode[];
|
|
30
|
+
currentHeadId: string | null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Check if a user message is an internal tool confirmation message
|
|
35
|
+
* Internal messages contain tool_result blocks, not regular text input
|
|
36
|
+
*/
|
|
37
|
+
export function isInternalToolMessage(sdkMessage: any): boolean {
|
|
38
|
+
if (sdkMessage.type !== 'user') return false;
|
|
39
|
+
|
|
40
|
+
const content = sdkMessage.message?.content;
|
|
41
|
+
if (!content) return false;
|
|
42
|
+
|
|
43
|
+
// Check if content is array and contains tool_result blocks
|
|
44
|
+
if (Array.isArray(content)) {
|
|
45
|
+
return content.some((block: any) => block.type === 'tool_result');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Extract user message text from SDK message
|
|
53
|
+
*/
|
|
54
|
+
export function extractMessageText(sdkMessage: SDKMessage): string {
|
|
55
|
+
if ('message' in sdkMessage && sdkMessage.message?.content) {
|
|
56
|
+
const content = sdkMessage.message.content;
|
|
57
|
+
if (typeof content === 'string') {
|
|
58
|
+
return content;
|
|
59
|
+
} else if (Array.isArray(content)) {
|
|
60
|
+
const textBlock = content.find(
|
|
61
|
+
(item: any) => typeof item === 'object' && 'text' in item
|
|
62
|
+
);
|
|
63
|
+
if (textBlock && 'text' in textBlock) {
|
|
64
|
+
return textBlock.text;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return '';
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Check if a database message is a checkpoint (real user message with text)
|
|
73
|
+
*/
|
|
74
|
+
export function isCheckpointMessage(msg: DatabaseMessage): boolean {
|
|
75
|
+
try {
|
|
76
|
+
const sdk = JSON.parse(msg.sdk_message) as SDKMessage;
|
|
77
|
+
if (sdk.type !== 'user') return false;
|
|
78
|
+
if (isInternalToolMessage(sdk)) return false;
|
|
79
|
+
const text = extractMessageText(sdk);
|
|
80
|
+
return text.trim() !== '';
|
|
81
|
+
} catch {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Build checkpoint tree from all messages in a session.
|
|
88
|
+
* Returns a map of checkpoint IDs to their parent checkpoint IDs.
|
|
89
|
+
*
|
|
90
|
+
* A "checkpoint" is a real user message (not tool confirmation) with text.
|
|
91
|
+
* The parent-child relationship is determined by the message parent chain.
|
|
92
|
+
*/
|
|
93
|
+
export function buildCheckpointTree(
|
|
94
|
+
allMessages: DatabaseMessage[]
|
|
95
|
+
): {
|
|
96
|
+
checkpoints: DatabaseMessage[];
|
|
97
|
+
parentMap: Map<string, string>; // childId -> parentId
|
|
98
|
+
childrenMap: Map<string, string[]>; // parentId -> [childIds]
|
|
99
|
+
} {
|
|
100
|
+
const msgMap = new Map<string, DatabaseMessage>();
|
|
101
|
+
for (const msg of allMessages) {
|
|
102
|
+
msgMap.set(msg.id, msg);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Identify all checkpoint messages
|
|
106
|
+
const checkpoints: DatabaseMessage[] = [];
|
|
107
|
+
const checkpointIdSet = new Set<string>();
|
|
108
|
+
|
|
109
|
+
for (const msg of allMessages) {
|
|
110
|
+
if (isCheckpointMessage(msg)) {
|
|
111
|
+
checkpoints.push(msg);
|
|
112
|
+
checkpointIdSet.add(msg.id);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// For each checkpoint, find its parent checkpoint
|
|
117
|
+
// by walking back through the message parent chain
|
|
118
|
+
const parentMap = new Map<string, string>(); // childCheckpoint -> parentCheckpoint
|
|
119
|
+
const childrenMap = new Map<string, string[]>();
|
|
120
|
+
|
|
121
|
+
for (const cp of checkpoints) {
|
|
122
|
+
let currentId = cp.parent_message_id;
|
|
123
|
+
while (currentId) {
|
|
124
|
+
if (checkpointIdSet.has(currentId)) {
|
|
125
|
+
parentMap.set(cp.id, currentId);
|
|
126
|
+
if (!childrenMap.has(currentId)) {
|
|
127
|
+
childrenMap.set(currentId, []);
|
|
128
|
+
}
|
|
129
|
+
childrenMap.get(currentId)!.push(cp.id);
|
|
130
|
+
break;
|
|
131
|
+
}
|
|
132
|
+
const parentMsg = msgMap.get(currentId);
|
|
133
|
+
if (!parentMsg) break;
|
|
134
|
+
currentId = parentMsg.parent_message_id || null;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return { checkpoints, parentMap, childrenMap };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Find the checkpoint path from root to a given checkpoint.
|
|
143
|
+
* Returns ordered list of checkpoint IDs from root to target.
|
|
144
|
+
*/
|
|
145
|
+
export function getCheckpointPathToRoot(
|
|
146
|
+
checkpointId: string,
|
|
147
|
+
parentMap: Map<string, string>
|
|
148
|
+
): string[] {
|
|
149
|
+
const path: string[] = [];
|
|
150
|
+
let currentId: string | null = checkpointId;
|
|
151
|
+
|
|
152
|
+
while (currentId) {
|
|
153
|
+
path.unshift(currentId);
|
|
154
|
+
currentId = parentMap.get(currentId) || null;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return path;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Find which checkpoint the current HEAD belongs to.
|
|
162
|
+
* Walks back from HEAD through message parents until finding a checkpoint.
|
|
163
|
+
*/
|
|
164
|
+
export function findCheckpointForHead(
|
|
165
|
+
headMessageId: string,
|
|
166
|
+
allMessages: DatabaseMessage[],
|
|
167
|
+
checkpointIdSet: Set<string>
|
|
168
|
+
): string | null {
|
|
169
|
+
const msgMap = new Map<string, DatabaseMessage>();
|
|
170
|
+
for (const msg of allMessages) {
|
|
171
|
+
msgMap.set(msg.id, msg);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
let currentId: string | null = headMessageId;
|
|
175
|
+
while (currentId) {
|
|
176
|
+
if (checkpointIdSet.has(currentId)) {
|
|
177
|
+
return currentId;
|
|
178
|
+
}
|
|
179
|
+
const msg = msgMap.get(currentId);
|
|
180
|
+
if (!msg) break;
|
|
181
|
+
currentId = msg.parent_message_id || null;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Find the session end for a checkpoint.
|
|
189
|
+
* This is the last message of the checkpoint's session
|
|
190
|
+
* (last assistant/tool response before the next real user message).
|
|
191
|
+
*
|
|
192
|
+
* Uses two approaches:
|
|
193
|
+
* 1. Parent-based: Walk forward through children from checkpoint
|
|
194
|
+
* 2. Timestamp-based fallback: If parent-based fails, use chronological order
|
|
195
|
+
*/
|
|
196
|
+
export function findSessionEnd(
|
|
197
|
+
checkpointMsg: DatabaseMessage,
|
|
198
|
+
allMessages: DatabaseMessage[]
|
|
199
|
+
): DatabaseMessage {
|
|
200
|
+
// Try parent-based approach first
|
|
201
|
+
const parentResult = findSessionEndByParent(checkpointMsg, allMessages);
|
|
202
|
+
|
|
203
|
+
// If parent-based approach found a session end beyond the checkpoint, use it
|
|
204
|
+
if (parentResult.id !== checkpointMsg.id) {
|
|
205
|
+
debug.log('snapshot', `findSessionEnd: parent-based → ${parentResult.id.slice(0, 8)}`);
|
|
206
|
+
return parentResult;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Fallback: timestamp-based approach
|
|
210
|
+
// Walk chronologically through messages after checkpoint until next real user message
|
|
211
|
+
debug.log('snapshot', `findSessionEnd: parent-based returned checkpoint itself, trying timestamp fallback`);
|
|
212
|
+
const timestampResult = findSessionEndByTimestamp(checkpointMsg, allMessages);
|
|
213
|
+
|
|
214
|
+
if (timestampResult.id !== checkpointMsg.id) {
|
|
215
|
+
debug.log('snapshot', `findSessionEnd: timestamp-based → ${timestampResult.id.slice(0, 8)}`);
|
|
216
|
+
return timestampResult;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
debug.log('snapshot', `findSessionEnd: no session continuation found, returning checkpoint ${checkpointMsg.id.slice(0, 8)}`);
|
|
220
|
+
return checkpointMsg;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Parent-based session end finder.
|
|
225
|
+
* Walks through childrenMap (parent_message_id relationships).
|
|
226
|
+
*/
|
|
227
|
+
function findSessionEndByParent(
|
|
228
|
+
checkpointMsg: DatabaseMessage,
|
|
229
|
+
allMessages: DatabaseMessage[]
|
|
230
|
+
): DatabaseMessage {
|
|
231
|
+
const childrenMap = new Map<string, DatabaseMessage[]>();
|
|
232
|
+
for (const msg of allMessages) {
|
|
233
|
+
if (msg.parent_message_id) {
|
|
234
|
+
if (!childrenMap.has(msg.parent_message_id)) {
|
|
235
|
+
childrenMap.set(msg.parent_message_id, []);
|
|
236
|
+
}
|
|
237
|
+
childrenMap.get(msg.parent_message_id)!.push(msg);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
let current = checkpointMsg;
|
|
242
|
+
let lastValidEnd = checkpointMsg;
|
|
243
|
+
|
|
244
|
+
while (true) {
|
|
245
|
+
const children = childrenMap.get(current.id) || [];
|
|
246
|
+
children.sort((a, b) => a.timestamp.localeCompare(b.timestamp));
|
|
247
|
+
|
|
248
|
+
let sessionContinuation: DatabaseMessage | null = null;
|
|
249
|
+
|
|
250
|
+
for (const child of children) {
|
|
251
|
+
try {
|
|
252
|
+
const sdk = JSON.parse(child.sdk_message);
|
|
253
|
+
if (sdk.type === 'assistant') {
|
|
254
|
+
sessionContinuation = child;
|
|
255
|
+
break;
|
|
256
|
+
}
|
|
257
|
+
if (sdk.type === 'user' && isInternalToolMessage(sdk)) {
|
|
258
|
+
sessionContinuation = child;
|
|
259
|
+
break;
|
|
260
|
+
}
|
|
261
|
+
} catch {
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if (!sessionContinuation) {
|
|
267
|
+
return lastValidEnd;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
current = sessionContinuation;
|
|
271
|
+
lastValidEnd = current;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Timestamp-based session end finder (fallback).
|
|
277
|
+
* Walks chronologically through messages after checkpoint
|
|
278
|
+
* until hitting the next real user message (checkpoint).
|
|
279
|
+
*/
|
|
280
|
+
function findSessionEndByTimestamp(
|
|
281
|
+
checkpointMsg: DatabaseMessage,
|
|
282
|
+
allMessages: DatabaseMessage[]
|
|
283
|
+
): DatabaseMessage {
|
|
284
|
+
const sorted = [...allMessages].sort((a, b) => a.timestamp.localeCompare(b.timestamp));
|
|
285
|
+
|
|
286
|
+
const checkpointIndex = sorted.findIndex(m => m.id === checkpointMsg.id);
|
|
287
|
+
if (checkpointIndex === -1) return checkpointMsg;
|
|
288
|
+
|
|
289
|
+
let lastValidEnd = checkpointMsg;
|
|
290
|
+
|
|
291
|
+
for (let i = checkpointIndex + 1; i < sorted.length; i++) {
|
|
292
|
+
const msg = sorted[i];
|
|
293
|
+
try {
|
|
294
|
+
const sdk = JSON.parse(msg.sdk_message);
|
|
295
|
+
|
|
296
|
+
if (sdk.type === 'assistant') {
|
|
297
|
+
lastValidEnd = msg;
|
|
298
|
+
} else if (sdk.type === 'user' && isInternalToolMessage(sdk)) {
|
|
299
|
+
lastValidEnd = msg;
|
|
300
|
+
} else if (sdk.type === 'user' && !isInternalToolMessage(sdk)) {
|
|
301
|
+
// Hit the next real user message (checkpoint) - stop
|
|
302
|
+
break;
|
|
303
|
+
}
|
|
304
|
+
} catch {
|
|
305
|
+
continue;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
return lastValidEnd;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Check if checkpointId is a descendant of ancestorId in the checkpoint tree
|
|
314
|
+
*/
|
|
315
|
+
export function isDescendant(
|
|
316
|
+
checkpointId: string,
|
|
317
|
+
ancestorId: string,
|
|
318
|
+
childrenMap: Map<string, string[]>
|
|
319
|
+
): boolean {
|
|
320
|
+
// IMPORTANT: Copy the array to avoid mutating the original childrenMap
|
|
321
|
+
const queue = [...(childrenMap.get(ancestorId) || [])];
|
|
322
|
+
const visited = new Set<string>();
|
|
323
|
+
|
|
324
|
+
while (queue.length > 0) {
|
|
325
|
+
const current = queue.shift()!;
|
|
326
|
+
if (current === checkpointId) return true;
|
|
327
|
+
if (visited.has(current)) continue;
|
|
328
|
+
visited.add(current);
|
|
329
|
+
|
|
330
|
+
const children = childrenMap.get(current) || [];
|
|
331
|
+
queue.push(...children);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
return false;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Get file change stats for a checkpoint by looking at snapshots
|
|
339
|
+
* between this checkpoint and the next.
|
|
340
|
+
*/
|
|
341
|
+
export function getCheckpointFileStats(
|
|
342
|
+
checkpointMsg: DatabaseMessage,
|
|
343
|
+
allMessages: DatabaseMessage[],
|
|
344
|
+
nextCheckpointTimestamp?: string
|
|
345
|
+
): { filesChanged: number; insertions: number; deletions: number } {
|
|
346
|
+
let filesChanged = 0;
|
|
347
|
+
let insertions = 0;
|
|
348
|
+
let deletions = 0;
|
|
349
|
+
|
|
350
|
+
const checkpointTimestamp = checkpointMsg.timestamp;
|
|
351
|
+
|
|
352
|
+
const laterMessages = allMessages
|
|
353
|
+
.filter(m => {
|
|
354
|
+
if (m.timestamp <= checkpointTimestamp) return false;
|
|
355
|
+
if (nextCheckpointTimestamp && m.timestamp >= nextCheckpointTimestamp) return false;
|
|
356
|
+
return true;
|
|
357
|
+
})
|
|
358
|
+
.sort((a, b) => a.timestamp.localeCompare(b.timestamp));
|
|
359
|
+
|
|
360
|
+
const allChangedFiles = new Set<string>();
|
|
361
|
+
const statsInRange: Array<{ files: number; ins: number; del: number }> = [];
|
|
362
|
+
|
|
363
|
+
for (const msg of laterMessages) {
|
|
364
|
+
try {
|
|
365
|
+
const sdkMsg = JSON.parse(msg.sdk_message) as SDKMessage;
|
|
366
|
+
if (sdkMsg.type !== 'user') continue;
|
|
367
|
+
|
|
368
|
+
const userSnapshot = snapshotQueries.getByMessageId(msg.id);
|
|
369
|
+
if (!userSnapshot) continue;
|
|
370
|
+
|
|
371
|
+
const fc = userSnapshot.files_changed || 0;
|
|
372
|
+
const ins = userSnapshot.insertions || 0;
|
|
373
|
+
const del = userSnapshot.deletions || 0;
|
|
374
|
+
|
|
375
|
+
if (fc > 0 || ins > 0 || del > 0) {
|
|
376
|
+
statsInRange.push({ files: fc, ins, del });
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
if (userSnapshot.delta_changes) {
|
|
380
|
+
try {
|
|
381
|
+
const delta = JSON.parse(userSnapshot.delta_changes);
|
|
382
|
+
if (delta.added) Object.keys(delta.added).forEach(f => allChangedFiles.add(f));
|
|
383
|
+
if (delta.modified) Object.keys(delta.modified).forEach(f => allChangedFiles.add(f));
|
|
384
|
+
if (delta.deleted && Array.isArray(delta.deleted)) delta.deleted.forEach((f: string) => allChangedFiles.add(f));
|
|
385
|
+
} catch { /* skip */ }
|
|
386
|
+
}
|
|
387
|
+
} catch { /* skip */ }
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
if (statsInRange.length > 0) {
|
|
391
|
+
filesChanged = allChangedFiles.size > 0 ? allChangedFiles.size : Math.max(...statsInRange.map(s => s.files));
|
|
392
|
+
insertions = statsInRange.reduce((sum, s) => sum + s.ins, 0);
|
|
393
|
+
deletions = statsInRange.reduce((sum, s) => sum + s.del, 0);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
return { filesChanged, insertions, deletions };
|
|
397
|
+
}
|