@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,755 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Open Code Engine Adapter
|
|
3
|
+
*
|
|
4
|
+
* Wraps the @opencode-ai/sdk into the AIEngine interface.
|
|
5
|
+
* Converts Open Code messages/events → SDKMessage (Claude format)
|
|
6
|
+
* so stream-manager and frontend remain unchanged.
|
|
7
|
+
*
|
|
8
|
+
* Server lifecycle is managed by ./server.ts (bun-pty spawn).
|
|
9
|
+
* This file only contains the OpenCodeEngine class (per-project instance).
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { SDKMessage, SDKUserMessage, EngineSDKMessage } from '$shared/types/messaging';
|
|
13
|
+
import type { AIEngine, EngineQueryOptions } from '../../types';
|
|
14
|
+
import type { EngineModel } from '$shared/types/engine';
|
|
15
|
+
import type {
|
|
16
|
+
Provider,
|
|
17
|
+
Model,
|
|
18
|
+
EventMessageUpdated,
|
|
19
|
+
EventMessagePartUpdated,
|
|
20
|
+
EventSessionIdle,
|
|
21
|
+
EventSessionStatus,
|
|
22
|
+
EventSessionError,
|
|
23
|
+
Part,
|
|
24
|
+
ToolPart,
|
|
25
|
+
Message as OCMessage,
|
|
26
|
+
} from '@opencode-ai/sdk';
|
|
27
|
+
import {
|
|
28
|
+
convertAssistantMessages,
|
|
29
|
+
convertResultMessage,
|
|
30
|
+
convertSystemInitMessage,
|
|
31
|
+
convertStreamStart,
|
|
32
|
+
convertPartialTextDelta,
|
|
33
|
+
convertStreamStop,
|
|
34
|
+
convertToolUseOnly,
|
|
35
|
+
convertToolResultOnly,
|
|
36
|
+
convertReasoningMessage,
|
|
37
|
+
convertPartialReasoningDelta,
|
|
38
|
+
convertReasoningStreamStart,
|
|
39
|
+
convertReasoningStreamStop,
|
|
40
|
+
getToolInput,
|
|
41
|
+
} from './message-converter';
|
|
42
|
+
import { ensureClient, getClient } from './server';
|
|
43
|
+
import { debug } from '$shared/utils/logger';
|
|
44
|
+
|
|
45
|
+
/** Map SDK Model.status to our category */
|
|
46
|
+
function mapStatusToCategory(status: Model['status']): EngineModel['category'] {
|
|
47
|
+
switch (status) {
|
|
48
|
+
case 'deprecated': return 'legacy';
|
|
49
|
+
case 'alpha':
|
|
50
|
+
case 'beta': return 'stable';
|
|
51
|
+
default: return 'latest';
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Build capability tags from SDK Model.capabilities */
|
|
56
|
+
function buildCapabilityTags(model: Model): string[] {
|
|
57
|
+
const tags: string[] = [];
|
|
58
|
+
const caps = model.capabilities;
|
|
59
|
+
|
|
60
|
+
if (caps.reasoning) tags.push('Reasoning');
|
|
61
|
+
if (caps.attachment) tags.push('Attachments');
|
|
62
|
+
|
|
63
|
+
return tags;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ============================================================================
|
|
67
|
+
// OpenCode Engine (per-project instance)
|
|
68
|
+
// ============================================================================
|
|
69
|
+
|
|
70
|
+
export class OpenCodeEngine implements AIEngine {
|
|
71
|
+
readonly name = 'opencode' as const;
|
|
72
|
+
private _isInitialized = false;
|
|
73
|
+
private _isActive = false;
|
|
74
|
+
private activeAbortController: AbortController | null = null;
|
|
75
|
+
private activeSessionId: string | null = null;
|
|
76
|
+
private activeProjectPath: string | null = null;
|
|
77
|
+
|
|
78
|
+
get isInitialized(): boolean {
|
|
79
|
+
return this._isInitialized;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
get isActive(): boolean {
|
|
83
|
+
return this._isActive;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Initialize this per-project engine instance.
|
|
88
|
+
* Delegates to the shared client singleton (concurrency-safe).
|
|
89
|
+
*/
|
|
90
|
+
async initialize(): Promise<void> {
|
|
91
|
+
if (this._isInitialized) return;
|
|
92
|
+
|
|
93
|
+
await ensureClient();
|
|
94
|
+
this._isInitialized = true;
|
|
95
|
+
debug.log('engine', 'Open Code engine instance initialized (shared client)');
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Cleanup this per-project instance.
|
|
100
|
+
* Does NOT dispose the shared client — that's handled by disposeOpenCodeClient().
|
|
101
|
+
*/
|
|
102
|
+
async dispose(): Promise<void> {
|
|
103
|
+
await this.cancel();
|
|
104
|
+
this._isInitialized = false;
|
|
105
|
+
debug.log('engine', 'Open Code engine instance disposed');
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async getAvailableModels(): Promise<EngineModel[]> {
|
|
109
|
+
const client = await ensureClient();
|
|
110
|
+
|
|
111
|
+
try {
|
|
112
|
+
// config.providers() returns { data: { providers: Provider[] } }
|
|
113
|
+
const response = await client.config.providers();
|
|
114
|
+
const providers: Provider[] = response.data?.providers ?? [];
|
|
115
|
+
const models: EngineModel[] = [];
|
|
116
|
+
|
|
117
|
+
for (const provider of providers) {
|
|
118
|
+
// Provider.models is Record<string, Model>
|
|
119
|
+
const providerModels: Record<string, Model> = provider.models ?? {};
|
|
120
|
+
|
|
121
|
+
for (const [modelKey, model] of Object.entries(providerModels)) {
|
|
122
|
+
const modelId = model.id || modelKey;
|
|
123
|
+
const compoundId = `opencode:${provider.id}/${modelId}`;
|
|
124
|
+
|
|
125
|
+
models.push({
|
|
126
|
+
id: compoundId,
|
|
127
|
+
engine: 'opencode',
|
|
128
|
+
modelId: `${provider.id}/${modelId}`,
|
|
129
|
+
name: model.name || modelId,
|
|
130
|
+
provider: provider.id,
|
|
131
|
+
description: `${model.name || modelId} via ${provider.name || provider.id}`,
|
|
132
|
+
capabilities: buildCapabilityTags(model),
|
|
133
|
+
contextWindow: model.limit.context,
|
|
134
|
+
category: mapStatusToCategory(model.status),
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
debug.log('engine', `Fetched ${models.length} models from ${providers.length} Open Code providers`);
|
|
140
|
+
return models;
|
|
141
|
+
} catch (error) {
|
|
142
|
+
debug.error('engine', 'Failed to fetch Open Code providers:', error);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return [];
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Stream a query through Open Code SDK, yielding SDKMessage (Claude format)
|
|
150
|
+
*
|
|
151
|
+
* Flow: subscribe to events FIRST, then send prompt asynchronously.
|
|
152
|
+
* This ensures no events are missed between sending and subscribing.
|
|
153
|
+
*/
|
|
154
|
+
async *streamQuery(options: EngineQueryOptions): AsyncGenerator<EngineSDKMessage, void, unknown> {
|
|
155
|
+
const client = await ensureClient();
|
|
156
|
+
|
|
157
|
+
const {
|
|
158
|
+
projectPath,
|
|
159
|
+
prompt,
|
|
160
|
+
resume,
|
|
161
|
+
model = 'claude-sonnet',
|
|
162
|
+
abortController
|
|
163
|
+
} = options;
|
|
164
|
+
|
|
165
|
+
this.activeAbortController = abortController || new AbortController();
|
|
166
|
+
this._isActive = true;
|
|
167
|
+
this.activeProjectPath = projectPath;
|
|
168
|
+
|
|
169
|
+
debug.log('chat', 'Open Code - Stream Query');
|
|
170
|
+
debug.log('chat', { prompt });
|
|
171
|
+
|
|
172
|
+
try {
|
|
173
|
+
const promptParts = this.extractPromptParts(prompt);
|
|
174
|
+
|
|
175
|
+
// Create or fork a session
|
|
176
|
+
// When resuming, fork the session to create a new branch
|
|
177
|
+
// (like Claude Code's forkSession: true) so each checkpoint
|
|
178
|
+
// gets its own conversation branch
|
|
179
|
+
let sessionId: string;
|
|
180
|
+
|
|
181
|
+
if (resume) {
|
|
182
|
+
try {
|
|
183
|
+
const forkResult = await client.session.fork({
|
|
184
|
+
path: { id: resume },
|
|
185
|
+
query: { directory: projectPath }
|
|
186
|
+
});
|
|
187
|
+
sessionId = forkResult.data?.id || resume;
|
|
188
|
+
debug.log('engine', `Forked Open Code session: ${resume} → ${sessionId}`);
|
|
189
|
+
} catch (forkError) {
|
|
190
|
+
// Fallback to resuming the same session if fork fails
|
|
191
|
+
debug.warn('engine', 'Failed to fork Open Code session, falling back to resume:', forkError);
|
|
192
|
+
sessionId = resume;
|
|
193
|
+
}
|
|
194
|
+
} else {
|
|
195
|
+
const sessionResult = await client.session.create({
|
|
196
|
+
query: { directory: projectPath }
|
|
197
|
+
});
|
|
198
|
+
sessionId = sessionResult.data?.id || crypto.randomUUID();
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
this.activeSessionId = sessionId;
|
|
202
|
+
|
|
203
|
+
yield convertSystemInitMessage(sessionId, model);
|
|
204
|
+
yield convertStreamStart(sessionId);
|
|
205
|
+
|
|
206
|
+
// 1. Subscribe to event stream FIRST (before sending prompt)
|
|
207
|
+
const eventResult = await client.event.subscribe({
|
|
208
|
+
query: { directory: projectPath },
|
|
209
|
+
signal: this.activeAbortController.signal
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
// 2. Send prompt asynchronously (non-blocking) — parse "providerId/modelId"
|
|
213
|
+
const [providerID, modelID] = model.includes('/') ? model.split('/', 2) : ['', model];
|
|
214
|
+
|
|
215
|
+
client.session.promptAsync({
|
|
216
|
+
path: { id: sessionId },
|
|
217
|
+
body: {
|
|
218
|
+
parts: promptParts as any,
|
|
219
|
+
...(providerID && modelID ? { model: { providerID, modelID } } : {}),
|
|
220
|
+
},
|
|
221
|
+
query: { directory: projectPath },
|
|
222
|
+
}).catch(error => {
|
|
223
|
+
debug.error('engine', 'Open Code promptAsync error:', error);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
// 3. Process event stream — emit messages progressively (like Claude Code)
|
|
227
|
+
// Each assistant message becomes its own bubble in the UI
|
|
228
|
+
const messageParts = new Map<string, Part[]>();
|
|
229
|
+
const assistantMessages = new Map<string, OCMessage>(); // All tracked assistant messages
|
|
230
|
+
const emittedMessageIds = new Set<string>(); // Already yielded message IDs
|
|
231
|
+
let currentAssistantId: string | null = null; // Currently active assistant message
|
|
232
|
+
let streamingText = '';
|
|
233
|
+
const emittedToolParts = new Set<string>(); // Tool parts already emitted as tool_use
|
|
234
|
+
const completedToolParts = new Set<string>(); // Tool parts whose tool_result was emitted
|
|
235
|
+
const emittedReasoningParts = new Set<string>(); // Reasoning parts already flushed
|
|
236
|
+
let reasoningStreamActive = false; // Whether reasoning is currently streaming
|
|
237
|
+
let reasoningText = ''; // Accumulated reasoning text
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Flush active reasoning stream: stop reasoning stream, emit final reasoning message.
|
|
241
|
+
*/
|
|
242
|
+
const flushReasoning = function* (msg: OCMessage) {
|
|
243
|
+
if (!reasoningStreamActive) return;
|
|
244
|
+
yield convertReasoningStreamStop(sessionId);
|
|
245
|
+
reasoningStreamActive = false;
|
|
246
|
+
if (reasoningText) {
|
|
247
|
+
yield convertReasoningMessage(reasoningText, msg, sessionId);
|
|
248
|
+
}
|
|
249
|
+
reasoningText = '';
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Finalize and yield an assistant message by ID
|
|
254
|
+
* Emits stop stream event, the assembled message, and restarts stream for next message
|
|
255
|
+
*/
|
|
256
|
+
const finalizeMessage = function* (msgId: string) {
|
|
257
|
+
const msg = assistantMessages.get(msgId);
|
|
258
|
+
if (!msg || emittedMessageIds.has(msgId)) return;
|
|
259
|
+
|
|
260
|
+
// Flush any active reasoning before finalizing
|
|
261
|
+
yield* flushReasoning(msg);
|
|
262
|
+
|
|
263
|
+
emittedMessageIds.add(msgId);
|
|
264
|
+
|
|
265
|
+
// Stop current stream
|
|
266
|
+
yield convertStreamStop(sessionId);
|
|
267
|
+
|
|
268
|
+
const parts = messageParts.get(msgId) || [];
|
|
269
|
+
// Filter out tool parts and reasoning parts already emitted
|
|
270
|
+
const remainingParts = parts.filter(p => {
|
|
271
|
+
if (p.type === 'tool') return !emittedToolParts.has(p.id);
|
|
272
|
+
if (p.type === 'reasoning') return false; // Already emitted as reasoning message
|
|
273
|
+
return true;
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
if (remainingParts.length > 0) {
|
|
277
|
+
const splitMessages = convertAssistantMessages(msg, remainingParts, sessionId);
|
|
278
|
+
for (const m of splitMessages) {
|
|
279
|
+
yield m;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Restart stream for the next message
|
|
284
|
+
yield convertStreamStart(sessionId);
|
|
285
|
+
|
|
286
|
+
// Reset streaming text for new message
|
|
287
|
+
streamingText = '';
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
// Track whether message.part.delta events are being received.
|
|
291
|
+
// When active, skip delta processing in message.part.updated to prevent duplication.
|
|
292
|
+
let receivedPartDelta = false;
|
|
293
|
+
|
|
294
|
+
if (eventResult?.stream) {
|
|
295
|
+
for await (const event of eventResult.stream) {
|
|
296
|
+
if (this.activeAbortController?.signal.aborted) break;
|
|
297
|
+
|
|
298
|
+
const evt = event as { type: string; properties: Record<string, unknown> };
|
|
299
|
+
debug.log('engine', `[OC] event: ${evt.type}`);
|
|
300
|
+
|
|
301
|
+
switch (evt.type) {
|
|
302
|
+
case 'message.updated': {
|
|
303
|
+
const { info } = (event as EventMessageUpdated).properties;
|
|
304
|
+
// Only track assistant messages for our session
|
|
305
|
+
if (info.role === 'assistant' && info.sessionID === sessionId) {
|
|
306
|
+
assistantMessages.set(info.id, info);
|
|
307
|
+
|
|
308
|
+
// If a NEW assistant message arrives and we had a previous one,
|
|
309
|
+
// finalize the previous message (emit it as a separate bubble)
|
|
310
|
+
if (currentAssistantId && currentAssistantId !== info.id) {
|
|
311
|
+
yield* finalizeMessage(currentAssistantId);
|
|
312
|
+
}
|
|
313
|
+
currentAssistantId = info.id;
|
|
314
|
+
}
|
|
315
|
+
break;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
case 'message.part.updated': {
|
|
319
|
+
const props = (event as EventMessagePartUpdated).properties;
|
|
320
|
+
const part = props.part;
|
|
321
|
+
|
|
322
|
+
debug.log('engine', `[OC] part.updated: type=${part.type}, partId=${part.id}, msgId=${part.messageID}, session=${part.sessionID === sessionId ? 'match' : 'skip'}`);
|
|
323
|
+
|
|
324
|
+
// Only process parts belonging to our session
|
|
325
|
+
if (part.sessionID !== sessionId) break;
|
|
326
|
+
|
|
327
|
+
// Only process parts for tracked assistant messages (skip user message parts)
|
|
328
|
+
if (!assistantMessages.has(part.messageID)) {
|
|
329
|
+
debug.log('engine', `[OC] part.updated: skipped — messageID not in assistantMessages`);
|
|
330
|
+
break;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Accumulate parts per message
|
|
334
|
+
const msgId = part.messageID;
|
|
335
|
+
if (!messageParts.has(msgId)) {
|
|
336
|
+
messageParts.set(msgId, []);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const parts = messageParts.get(msgId)!;
|
|
340
|
+
const existingIdx = parts.findIndex(p => p.id === part.id);
|
|
341
|
+
if (existingIdx >= 0) {
|
|
342
|
+
parts[existingIdx] = part;
|
|
343
|
+
} else {
|
|
344
|
+
parts.push(part);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// Progressive tool rendering: emit tool_use immediately, tool_result when done
|
|
348
|
+
if (part.type === 'tool') {
|
|
349
|
+
const toolPart = part as ToolPart;
|
|
350
|
+
const msg = assistantMessages.get(msgId);
|
|
351
|
+
|
|
352
|
+
// Flush reasoning before tool rendering to preserve order
|
|
353
|
+
if (msg && reasoningStreamActive) {
|
|
354
|
+
yield* flushReasoning(msg);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
if (msg && !emittedToolParts.has(part.id)) {
|
|
358
|
+
// Only emit tool_use when input is available
|
|
359
|
+
// Pending tools may have empty input ({}) — wait for next update
|
|
360
|
+
const resolvedInput = getToolInput(toolPart);
|
|
361
|
+
const hasInput = Object.keys(resolvedInput).length > 0
|
|
362
|
+
|| toolPart.state.status !== 'pending';
|
|
363
|
+
|
|
364
|
+
if (hasInput) {
|
|
365
|
+
emittedToolParts.add(part.id);
|
|
366
|
+
|
|
367
|
+
yield convertStreamStop(sessionId);
|
|
368
|
+
yield convertToolUseOnly(toolPart, msg, sessionId);
|
|
369
|
+
|
|
370
|
+
// If already completed on first sight, emit result immediately too
|
|
371
|
+
if (toolPart.state.status === 'completed' || toolPart.state.status === 'error') {
|
|
372
|
+
completedToolParts.add(part.id);
|
|
373
|
+
yield convertToolResultOnly(toolPart, sessionId);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
yield convertStreamStart(sessionId);
|
|
377
|
+
streamingText = '';
|
|
378
|
+
}
|
|
379
|
+
} else if (
|
|
380
|
+
(toolPart.state.status === 'completed' || toolPart.state.status === 'error')
|
|
381
|
+
&& !completedToolParts.has(part.id)
|
|
382
|
+
) {
|
|
383
|
+
// Tool completed later — emit tool_result
|
|
384
|
+
completedToolParts.add(part.id);
|
|
385
|
+
|
|
386
|
+
yield convertStreamStop(sessionId);
|
|
387
|
+
yield convertToolResultOnly(toolPart, sessionId);
|
|
388
|
+
yield convertStreamStart(sessionId);
|
|
389
|
+
streamingText = '';
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
break;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// Handle reasoning parts — start reasoning stream (only once per part)
|
|
396
|
+
if (part.type === 'reasoning') {
|
|
397
|
+
if (!reasoningStreamActive && !emittedReasoningParts.has(part.id)) {
|
|
398
|
+
reasoningStreamActive = true;
|
|
399
|
+
reasoningText = '';
|
|
400
|
+
emittedReasoningParts.add(part.id);
|
|
401
|
+
yield convertReasoningStreamStart(sessionId);
|
|
402
|
+
debug.log('engine', `[OC] reasoning stream started for part=${part.id}`);
|
|
403
|
+
}
|
|
404
|
+
break;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// When a text part appears and reasoning was active, flush reasoning first
|
|
408
|
+
if (part.type === 'text' && reasoningStreamActive) {
|
|
409
|
+
const msg = assistantMessages.get(msgId);
|
|
410
|
+
if (msg) {
|
|
411
|
+
yield* flushReasoning(msg);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// Skip non-text/non-reasoning parts (step-start, step-finish, etc.)
|
|
416
|
+
if (part.type !== 'text') {
|
|
417
|
+
debug.log('engine', `[OC] part.updated: skipped non-text type=${part.type}`);
|
|
418
|
+
break;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// Only stream text for the current active message
|
|
422
|
+
if (msgId !== currentAssistantId) {
|
|
423
|
+
debug.log('engine', `[OC] part.updated: text skipped — msgId=${msgId} !== currentAssistantId=${currentAssistantId}`);
|
|
424
|
+
break;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// Stream text deltas — skip if message.part.delta events handle it
|
|
428
|
+
// to prevent double-counting the same delta
|
|
429
|
+
if (receivedPartDelta) {
|
|
430
|
+
// message.part.delta handles text streaming — just update accumulated text
|
|
431
|
+
if ((part as any).text) {
|
|
432
|
+
// Sync streamingText with the authoritative accumulated text from the part
|
|
433
|
+
// This handles any drift without emitting duplicate deltas
|
|
434
|
+
streamingText = (part as any).text;
|
|
435
|
+
}
|
|
436
|
+
break;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
const hasDelta = !!(props as any).delta;
|
|
440
|
+
const hasText = !!(part as any).text;
|
|
441
|
+
debug.log('engine', `[OC] text streaming: hasDelta=${hasDelta}, hasText=${hasText}, textLen=${(part as any).text?.length || 0}, streamingTextLen=${streamingText.length}`);
|
|
442
|
+
|
|
443
|
+
if ((props as any).delta) {
|
|
444
|
+
streamingText += (props as any).delta;
|
|
445
|
+
yield convertPartialTextDelta((props as any).delta, sessionId);
|
|
446
|
+
} else if ((part as any).text) {
|
|
447
|
+
const newText = (part as any).text;
|
|
448
|
+
if (newText.length > streamingText.length) {
|
|
449
|
+
const diff = newText.slice(streamingText.length);
|
|
450
|
+
streamingText = newText;
|
|
451
|
+
yield convertPartialTextDelta(diff, sessionId);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
break;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
case 'session.idle': {
|
|
458
|
+
// Session finished — flush reasoning and emit the last assistant message
|
|
459
|
+
if (currentAssistantId && !emittedMessageIds.has(currentAssistantId)) {
|
|
460
|
+
const msg = assistantMessages.get(currentAssistantId);
|
|
461
|
+
if (msg) {
|
|
462
|
+
// Flush any active reasoning
|
|
463
|
+
yield* flushReasoning(msg);
|
|
464
|
+
}
|
|
465
|
+
yield convertStreamStop(sessionId);
|
|
466
|
+
if (msg) {
|
|
467
|
+
const parts = messageParts.get(currentAssistantId) || [];
|
|
468
|
+
// Filter out tool parts and reasoning parts already emitted
|
|
469
|
+
const remainingParts = parts.filter(p => {
|
|
470
|
+
if (p.type === 'tool') return !emittedToolParts.has(p.id);
|
|
471
|
+
if (p.type === 'reasoning') return false;
|
|
472
|
+
return true;
|
|
473
|
+
});
|
|
474
|
+
if (remainingParts.length > 0) {
|
|
475
|
+
const splitMsgs1 = convertAssistantMessages(msg, remainingParts, sessionId);
|
|
476
|
+
for (const m of splitMsgs1) {
|
|
477
|
+
yield m;
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
emittedMessageIds.add(currentAssistantId);
|
|
482
|
+
} else {
|
|
483
|
+
yield convertStreamStop(sessionId);
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// Emit result message with token usage from the last assistant message
|
|
487
|
+
if (currentAssistantId) {
|
|
488
|
+
const lastMsg = assistantMessages.get(currentAssistantId);
|
|
489
|
+
if (lastMsg && lastMsg.role === 'assistant') {
|
|
490
|
+
yield convertResultMessage(lastMsg, sessionId);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
return; // Done
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
case 'session.status': {
|
|
498
|
+
const { status } = (event as EventSessionStatus).properties;
|
|
499
|
+
if (status.type === 'idle') {
|
|
500
|
+
// Same as session.idle
|
|
501
|
+
if (currentAssistantId && !emittedMessageIds.has(currentAssistantId)) {
|
|
502
|
+
const msg = assistantMessages.get(currentAssistantId);
|
|
503
|
+
if (msg) {
|
|
504
|
+
yield* flushReasoning(msg);
|
|
505
|
+
}
|
|
506
|
+
yield convertStreamStop(sessionId);
|
|
507
|
+
if (msg) {
|
|
508
|
+
const parts = messageParts.get(currentAssistantId) || [];
|
|
509
|
+
const remainingParts = parts.filter(p => {
|
|
510
|
+
if (p.type === 'tool') return !emittedToolParts.has(p.id);
|
|
511
|
+
if (p.type === 'reasoning') return false;
|
|
512
|
+
return true;
|
|
513
|
+
});
|
|
514
|
+
if (remainingParts.length > 0) {
|
|
515
|
+
const splitMsgs2 = convertAssistantMessages(msg, remainingParts, sessionId);
|
|
516
|
+
for (const m of splitMsgs2) {
|
|
517
|
+
yield m;
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
} else {
|
|
522
|
+
yield convertStreamStop(sessionId);
|
|
523
|
+
}
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
526
|
+
break;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// Handle message.part.delta — newer OpenCode servers send text deltas
|
|
530
|
+
// through this event instead of (or alongside) message.part.updated
|
|
531
|
+
// When both fire for the same text, only this handler processes deltas
|
|
532
|
+
case 'message.part.delta': {
|
|
533
|
+
receivedPartDelta = true;
|
|
534
|
+
const deltaProps = evt.properties as {
|
|
535
|
+
sessionID?: string;
|
|
536
|
+
messageID?: string;
|
|
537
|
+
partID?: string;
|
|
538
|
+
field?: string;
|
|
539
|
+
delta?: string;
|
|
540
|
+
};
|
|
541
|
+
|
|
542
|
+
// Only process deltas for our session
|
|
543
|
+
if (deltaProps.sessionID !== sessionId) break;
|
|
544
|
+
// Only process deltas for tracked assistant messages
|
|
545
|
+
if (!deltaProps.messageID || !assistantMessages.has(deltaProps.messageID)) break;
|
|
546
|
+
// Only stream for the current active message
|
|
547
|
+
if (deltaProps.messageID !== currentAssistantId) break;
|
|
548
|
+
// Only stream text field deltas
|
|
549
|
+
if (deltaProps.field !== 'text') break;
|
|
550
|
+
|
|
551
|
+
// Handle reasoning part deltas — stream as reasoning instead of text
|
|
552
|
+
if (deltaProps.partID && deltaProps.messageID && messageParts.has(deltaProps.messageID)) {
|
|
553
|
+
const knownParts = messageParts.get(deltaProps.messageID)!;
|
|
554
|
+
const knownPart = knownParts.find(p => p.id === deltaProps.partID);
|
|
555
|
+
if (knownPart && knownPart.type === 'reasoning') {
|
|
556
|
+
if (deltaProps.delta) {
|
|
557
|
+
reasoningText += deltaProps.delta;
|
|
558
|
+
yield convertPartialReasoningDelta(deltaProps.delta, sessionId);
|
|
559
|
+
}
|
|
560
|
+
break;
|
|
561
|
+
}
|
|
562
|
+
// Skip other non-text parts (step-start, etc.)
|
|
563
|
+
if (knownPart && knownPart.type !== 'text') {
|
|
564
|
+
break;
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
if (deltaProps.delta) {
|
|
569
|
+
debug.log('engine', `[OC] part.delta: field=${deltaProps.field}, deltaLen=${deltaProps.delta.length}`);
|
|
570
|
+
streamingText += deltaProps.delta;
|
|
571
|
+
|
|
572
|
+
// Also update the accumulated text in the tracked part
|
|
573
|
+
if (deltaProps.partID && messageParts.has(deltaProps.messageID)) {
|
|
574
|
+
const existingParts = messageParts.get(deltaProps.messageID)!;
|
|
575
|
+
const textPart = existingParts.find(p => p.id === deltaProps.partID && p.type === 'text');
|
|
576
|
+
if (textPart && 'text' in textPart) {
|
|
577
|
+
(textPart as any).text = (textPart as any).text + deltaProps.delta;
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
yield convertPartialTextDelta(deltaProps.delta, sessionId);
|
|
582
|
+
}
|
|
583
|
+
break;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
case 'session.error': {
|
|
587
|
+
const { error } = (event as EventSessionError).properties;
|
|
588
|
+
// Extract a human-readable error message.
|
|
589
|
+
// OpenCode SDK errors follow: { name: string, data: { message, statusCode?, providerID?, responseBody? } }
|
|
590
|
+
// Don't prepend error class names (e.g. "APIError") — those are SDK
|
|
591
|
+
// implementation details, not useful for the end user.
|
|
592
|
+
let errorMsg = 'Unknown Open Code error';
|
|
593
|
+
if (error) {
|
|
594
|
+
const errObj = error as Record<string, any>;
|
|
595
|
+
const name = errObj.name || '';
|
|
596
|
+
const dataMsg = errObj.data?.message || '';
|
|
597
|
+
const statusCode = errObj.data?.statusCode;
|
|
598
|
+
const providerID = errObj.data?.providerID;
|
|
599
|
+
const responseBody = errObj.data?.responseBody;
|
|
600
|
+
|
|
601
|
+
if (dataMsg) {
|
|
602
|
+
errorMsg = dataMsg;
|
|
603
|
+
} else if (responseBody) {
|
|
604
|
+
// No data.message — try to parse responseBody for the actual error
|
|
605
|
+
try {
|
|
606
|
+
const body = typeof responseBody === 'string' ? JSON.parse(responseBody) : responseBody;
|
|
607
|
+
errorMsg = body?.error?.message || body?.message || String(responseBody);
|
|
608
|
+
} catch {
|
|
609
|
+
errorMsg = String(responseBody);
|
|
610
|
+
}
|
|
611
|
+
} else if (name) {
|
|
612
|
+
errorMsg = name;
|
|
613
|
+
} else if (typeof error === 'string') {
|
|
614
|
+
errorMsg = error;
|
|
615
|
+
} else {
|
|
616
|
+
errorMsg = JSON.stringify(error);
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
// Append status code
|
|
620
|
+
if (statusCode) {
|
|
621
|
+
errorMsg += ` (status ${statusCode})`;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
// Append provider ID to help identify which provider failed
|
|
625
|
+
if (providerID) {
|
|
626
|
+
errorMsg += ` [provider: ${providerID}]`;
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
debug.error('engine', '[OC] session.error:', errorMsg);
|
|
630
|
+
throw new Error(errorMsg);
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
} catch (error) {
|
|
637
|
+
if (error instanceof Error) {
|
|
638
|
+
if (error.name === 'AbortError' || error.message.includes('aborted')) {
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
throw error;
|
|
643
|
+
} finally {
|
|
644
|
+
this._isActive = false;
|
|
645
|
+
this.activeAbortController = null;
|
|
646
|
+
this.activeSessionId = null;
|
|
647
|
+
this.activeProjectPath = null;
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
async cancel(): Promise<void> {
|
|
652
|
+
// Abort the OpenCode session on the server so it stops processing
|
|
653
|
+
const client = getClient();
|
|
654
|
+
if (client && this.activeSessionId) {
|
|
655
|
+
try {
|
|
656
|
+
await client.session.abort({
|
|
657
|
+
path: { id: this.activeSessionId },
|
|
658
|
+
...(this.activeProjectPath && { query: { directory: this.activeProjectPath } }),
|
|
659
|
+
});
|
|
660
|
+
debug.log('engine', 'Open Code session aborted:', this.activeSessionId);
|
|
661
|
+
} catch (error) {
|
|
662
|
+
debug.warn('engine', 'Failed to abort Open Code session:', error);
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
if (this.activeAbortController) {
|
|
667
|
+
this.activeAbortController.abort();
|
|
668
|
+
this.activeAbortController = null;
|
|
669
|
+
}
|
|
670
|
+
this._isActive = false;
|
|
671
|
+
this.activeSessionId = null;
|
|
672
|
+
this.activeProjectPath = null;
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
/**
|
|
676
|
+
* Cancel a specific session on the OpenCode server.
|
|
677
|
+
* Used by stream-manager for per-project isolation (instead of global cancel).
|
|
678
|
+
*/
|
|
679
|
+
async cancelSession(sessionId: string, projectPath?: string): Promise<void> {
|
|
680
|
+
const client = getClient();
|
|
681
|
+
if (!client || !sessionId) return;
|
|
682
|
+
try {
|
|
683
|
+
await client.session.abort({
|
|
684
|
+
path: { id: sessionId },
|
|
685
|
+
...(projectPath && { query: { directory: projectPath } }),
|
|
686
|
+
});
|
|
687
|
+
debug.log('engine', 'Open Code session aborted (per-stream):', sessionId);
|
|
688
|
+
} catch (error) {
|
|
689
|
+
debug.warn('engine', 'Failed to abort Open Code session:', error);
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
async interrupt(): Promise<void> {
|
|
694
|
+
// Open Code SDK doesn't have a separate interrupt — use cancel
|
|
695
|
+
await this.cancel();
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
/**
|
|
699
|
+
* Extract prompt parts (text + file attachments) from SDKUserMessage.
|
|
700
|
+
* Converts Claude-format image/document blocks to OpenCode FilePartInput format.
|
|
701
|
+
*/
|
|
702
|
+
private extractPromptParts(prompt: SDKUserMessage): Array<
|
|
703
|
+
| { type: 'text'; text: string }
|
|
704
|
+
| { type: 'file'; mime: string; filename?: string; url: string }
|
|
705
|
+
> {
|
|
706
|
+
const msg = prompt as Record<string, unknown>;
|
|
707
|
+
const message = msg.message as Record<string, unknown> | undefined;
|
|
708
|
+
if (!message) return [{ type: 'text', text: '' }];
|
|
709
|
+
|
|
710
|
+
if (typeof message.content === 'string') {
|
|
711
|
+
return [{ type: 'text', text: message.content }];
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
if (Array.isArray(message.content)) {
|
|
715
|
+
const parts: Array<
|
|
716
|
+
| { type: 'text'; text: string }
|
|
717
|
+
| { type: 'file'; mime: string; filename?: string; url: string }
|
|
718
|
+
> = [];
|
|
719
|
+
|
|
720
|
+
for (const block of message.content as Array<Record<string, unknown>>) {
|
|
721
|
+
if (block.type === 'text') {
|
|
722
|
+
parts.push({ type: 'text', text: block.text as string });
|
|
723
|
+
} else if (block.type === 'image' && block.source) {
|
|
724
|
+
// Claude format: { type: 'image', source: { type: 'base64', media_type, data } }
|
|
725
|
+
const source = block.source as Record<string, unknown>;
|
|
726
|
+
if (source.type === 'base64' && source.data && source.media_type) {
|
|
727
|
+
parts.push({
|
|
728
|
+
type: 'file',
|
|
729
|
+
mime: source.media_type as string,
|
|
730
|
+
url: `data:${source.media_type};base64,${source.data}`,
|
|
731
|
+
});
|
|
732
|
+
}
|
|
733
|
+
} else if (block.type === 'document' && block.source) {
|
|
734
|
+
// Claude format: { type: 'document', source: { type: 'base64', media_type, data }, title }
|
|
735
|
+
const source = block.source as Record<string, unknown>;
|
|
736
|
+
if (source.type === 'base64' && source.data && source.media_type) {
|
|
737
|
+
parts.push({
|
|
738
|
+
type: 'file',
|
|
739
|
+
mime: source.media_type as string,
|
|
740
|
+
filename: (block.title as string) || undefined,
|
|
741
|
+
url: `data:${source.media_type};base64,${source.data}`,
|
|
742
|
+
});
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
if (parts.length === 0) {
|
|
748
|
+
parts.push({ type: 'text', text: '' });
|
|
749
|
+
}
|
|
750
|
+
return parts;
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
return [{ type: 'text', text: '' }];
|
|
754
|
+
}
|
|
755
|
+
}
|