@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.
Files changed (456) hide show
  1. package/.env.example +6 -0
  2. package/.github/workflows/release.yml +60 -0
  3. package/.github/workflows/test.yml +40 -0
  4. package/CONTRIBUTING.md +499 -0
  5. package/LICENSE +21 -0
  6. package/README.md +209 -0
  7. package/backend/index.ts +156 -0
  8. package/backend/lib/chat/helpers.ts +42 -0
  9. package/backend/lib/chat/index.ts +2 -0
  10. package/backend/lib/chat/stream-manager.ts +1126 -0
  11. package/backend/lib/database/README.md +77 -0
  12. package/backend/lib/database/index.ts +119 -0
  13. package/backend/lib/database/migrations/001_create_projects_table.ts +31 -0
  14. package/backend/lib/database/migrations/002_create_chat_sessions_table.ts +33 -0
  15. package/backend/lib/database/migrations/003_create_messages_table.ts +32 -0
  16. package/backend/lib/database/migrations/004_create_prompt_templates_table.ts +34 -0
  17. package/backend/lib/database/migrations/005_create_settings_table.ts +24 -0
  18. package/backend/lib/database/migrations/006_add_user_to_messages.ts +58 -0
  19. package/backend/lib/database/migrations/007_create_stream_states_table.ts +41 -0
  20. package/backend/lib/database/migrations/008_create_message_snapshots_table.ts +62 -0
  21. package/backend/lib/database/migrations/009_add_delta_snapshot_fields.ts +41 -0
  22. package/backend/lib/database/migrations/010_add_soft_delete_and_branch_support.ts +70 -0
  23. package/backend/lib/database/migrations/011_git_like_commit_graph.ts +156 -0
  24. package/backend/lib/database/migrations/012_add_file_change_statistics.ts +41 -0
  25. package/backend/lib/database/migrations/013_checkpoint_tree_state.ts +118 -0
  26. package/backend/lib/database/migrations/014_add_engine_to_sessions.ts +18 -0
  27. package/backend/lib/database/migrations/015_add_model_to_sessions.ts +18 -0
  28. package/backend/lib/database/migrations/016_create_user_projects_table.ts +34 -0
  29. package/backend/lib/database/migrations/017_add_current_session_to_user_projects.ts +32 -0
  30. package/backend/lib/database/migrations/018_create_claude_accounts_table.ts +24 -0
  31. package/backend/lib/database/migrations/019_add_claude_account_to_sessions.ts +18 -0
  32. package/backend/lib/database/migrations/020_add_snapshot_tree_hash.ts +32 -0
  33. package/backend/lib/database/migrations/021_drop_prompt_templates_table.ts +33 -0
  34. package/backend/lib/database/migrations/index.ts +154 -0
  35. package/backend/lib/database/queries/checkpoint-queries.ts +87 -0
  36. package/backend/lib/database/queries/engine-queries.ts +75 -0
  37. package/backend/lib/database/queries/index.ts +9 -0
  38. package/backend/lib/database/queries/message-queries.ts +472 -0
  39. package/backend/lib/database/queries/project-queries.ts +117 -0
  40. package/backend/lib/database/queries/session-queries.ts +271 -0
  41. package/backend/lib/database/queries/settings-queries.ts +34 -0
  42. package/backend/lib/database/queries/snapshot-queries.ts +326 -0
  43. package/backend/lib/database/queries/utils-queries.ts +59 -0
  44. package/backend/lib/database/seeders/index.ts +13 -0
  45. package/backend/lib/database/seeders/settings_seeder.ts +84 -0
  46. package/backend/lib/database/utils/connection.ts +174 -0
  47. package/backend/lib/database/utils/index.ts +4 -0
  48. package/backend/lib/database/utils/migration-runner.ts +118 -0
  49. package/backend/lib/database/utils/seeder-runner.ts +121 -0
  50. package/backend/lib/engine/adapters/claude/environment.ts +164 -0
  51. package/backend/lib/engine/adapters/claude/error-handler.ts +60 -0
  52. package/backend/lib/engine/adapters/claude/index.ts +1 -0
  53. package/backend/lib/engine/adapters/claude/path-utils.ts +38 -0
  54. package/backend/lib/engine/adapters/claude/stream.ts +177 -0
  55. package/backend/lib/engine/adapters/opencode/index.ts +2 -0
  56. package/backend/lib/engine/adapters/opencode/message-converter.ts +862 -0
  57. package/backend/lib/engine/adapters/opencode/server.ts +104 -0
  58. package/backend/lib/engine/adapters/opencode/stream.ts +755 -0
  59. package/backend/lib/engine/index.ts +196 -0
  60. package/backend/lib/engine/types.ts +58 -0
  61. package/backend/lib/files/file-operations.ts +478 -0
  62. package/backend/lib/files/file-reading.ts +308 -0
  63. package/backend/lib/files/file-watcher.ts +383 -0
  64. package/backend/lib/files/path-browsing.ts +382 -0
  65. package/backend/lib/git/git-executor.ts +88 -0
  66. package/backend/lib/git/git-parser.ts +411 -0
  67. package/backend/lib/git/git-service.ts +505 -0
  68. package/backend/lib/mcp/README.md +1144 -0
  69. package/backend/lib/mcp/config.ts +316 -0
  70. package/backend/lib/mcp/index.ts +35 -0
  71. package/backend/lib/mcp/project-context.ts +236 -0
  72. package/backend/lib/mcp/servers/browser-automation/actions.ts +156 -0
  73. package/backend/lib/mcp/servers/browser-automation/browser.ts +419 -0
  74. package/backend/lib/mcp/servers/browser-automation/index.ts +791 -0
  75. package/backend/lib/mcp/servers/browser-automation/inspection.ts +501 -0
  76. package/backend/lib/mcp/servers/helper.ts +143 -0
  77. package/backend/lib/mcp/servers/index.ts +45 -0
  78. package/backend/lib/mcp/servers/weather/get-temperature.ts +56 -0
  79. package/backend/lib/mcp/servers/weather/index.ts +31 -0
  80. package/backend/lib/mcp/stdio-server.ts +103 -0
  81. package/backend/lib/mcp/types.ts +65 -0
  82. package/backend/lib/preview/browser/browser-audio-capture.ts +86 -0
  83. package/backend/lib/preview/browser/browser-console-manager.ts +263 -0
  84. package/backend/lib/preview/browser/browser-dialog-handler.ts +222 -0
  85. package/backend/lib/preview/browser/browser-interaction-handler.ts +421 -0
  86. package/backend/lib/preview/browser/browser-mcp-control.ts +415 -0
  87. package/backend/lib/preview/browser/browser-native-ui-handler.ts +512 -0
  88. package/backend/lib/preview/browser/browser-navigation-tracker.ts +104 -0
  89. package/backend/lib/preview/browser/browser-pool.ts +357 -0
  90. package/backend/lib/preview/browser/browser-preview-service.ts +882 -0
  91. package/backend/lib/preview/browser/browser-tab-manager.ts +935 -0
  92. package/backend/lib/preview/browser/browser-video-capture.ts +695 -0
  93. package/backend/lib/preview/browser/scripts/audio-stream.ts +292 -0
  94. package/backend/lib/preview/browser/scripts/cursor-tracking.ts +85 -0
  95. package/backend/lib/preview/browser/scripts/video-stream.ts +438 -0
  96. package/backend/lib/preview/browser/types.ts +359 -0
  97. package/backend/lib/preview/index.ts +24 -0
  98. package/backend/lib/project/index.ts +2 -0
  99. package/backend/lib/project/status-manager.ts +182 -0
  100. package/backend/lib/shared/index.ts +2 -0
  101. package/backend/lib/shared/port-utils.ts +25 -0
  102. package/backend/lib/shared/process-manager.ts +281 -0
  103. package/backend/lib/snapshot/blob-store.ts +227 -0
  104. package/backend/lib/snapshot/gitignore.ts +307 -0
  105. package/backend/lib/snapshot/helpers.ts +397 -0
  106. package/backend/lib/snapshot/snapshot-service.ts +483 -0
  107. package/backend/lib/terminal/helpers.ts +14 -0
  108. package/backend/lib/terminal/index.ts +8 -0
  109. package/backend/lib/terminal/pty-manager.ts +4 -0
  110. package/backend/lib/terminal/pty-session-manager.ts +387 -0
  111. package/backend/lib/terminal/shell-utils.ts +313 -0
  112. package/backend/lib/terminal/stream-manager.ts +293 -0
  113. package/backend/lib/tunnel/global-tunnel-manager.ts +243 -0
  114. package/backend/lib/tunnel/project-tunnel-manager.ts +311 -0
  115. package/backend/lib/user/helpers.ts +87 -0
  116. package/backend/lib/utils/ws.ts +944 -0
  117. package/backend/lib/vite-dev.ts +353 -0
  118. package/backend/middleware/cors.ts +15 -0
  119. package/backend/middleware/error-handler.ts +49 -0
  120. package/backend/middleware/logger.ts +9 -0
  121. package/backend/types/api.ts +24 -0
  122. package/backend/ws/README.md +1505 -0
  123. package/backend/ws/chat/background.ts +198 -0
  124. package/backend/ws/chat/index.ts +21 -0
  125. package/backend/ws/chat/stream.ts +707 -0
  126. package/backend/ws/engine/claude/accounts.ts +401 -0
  127. package/backend/ws/engine/claude/index.ts +13 -0
  128. package/backend/ws/engine/claude/status.ts +43 -0
  129. package/backend/ws/engine/index.ts +14 -0
  130. package/backend/ws/engine/opencode/index.ts +11 -0
  131. package/backend/ws/engine/opencode/status.ts +30 -0
  132. package/backend/ws/engine/utils.ts +36 -0
  133. package/backend/ws/files/index.ts +30 -0
  134. package/backend/ws/files/read.ts +189 -0
  135. package/backend/ws/files/search.ts +453 -0
  136. package/backend/ws/files/watch.ts +124 -0
  137. package/backend/ws/files/write.ts +143 -0
  138. package/backend/ws/git/branch.ts +106 -0
  139. package/backend/ws/git/commit.ts +39 -0
  140. package/backend/ws/git/conflict.ts +68 -0
  141. package/backend/ws/git/diff.ts +69 -0
  142. package/backend/ws/git/index.ts +24 -0
  143. package/backend/ws/git/log.ts +41 -0
  144. package/backend/ws/git/remote.ts +214 -0
  145. package/backend/ws/git/staging.ts +84 -0
  146. package/backend/ws/git/status.ts +90 -0
  147. package/backend/ws/index.ts +69 -0
  148. package/backend/ws/mcp/index.ts +61 -0
  149. package/backend/ws/messages/crud.ts +74 -0
  150. package/backend/ws/messages/index.ts +14 -0
  151. package/backend/ws/preview/browser/cleanup.ts +129 -0
  152. package/backend/ws/preview/browser/console.ts +114 -0
  153. package/backend/ws/preview/browser/interact.ts +513 -0
  154. package/backend/ws/preview/browser/mcp.ts +129 -0
  155. package/backend/ws/preview/browser/native-ui.ts +235 -0
  156. package/backend/ws/preview/browser/stats.ts +55 -0
  157. package/backend/ws/preview/browser/tab-info.ts +126 -0
  158. package/backend/ws/preview/browser/tab.ts +166 -0
  159. package/backend/ws/preview/browser/webcodecs.ts +293 -0
  160. package/backend/ws/preview/index.ts +146 -0
  161. package/backend/ws/projects/crud.ts +113 -0
  162. package/backend/ws/projects/index.ts +25 -0
  163. package/backend/ws/projects/presence.ts +46 -0
  164. package/backend/ws/projects/status.ts +116 -0
  165. package/backend/ws/sessions/crud.ts +327 -0
  166. package/backend/ws/sessions/index.ts +33 -0
  167. package/backend/ws/settings/crud.ts +112 -0
  168. package/backend/ws/settings/index.ts +14 -0
  169. package/backend/ws/snapshot/index.ts +17 -0
  170. package/backend/ws/snapshot/restore.ts +173 -0
  171. package/backend/ws/snapshot/timeline.ts +141 -0
  172. package/backend/ws/system/index.ts +14 -0
  173. package/backend/ws/system/operations.ts +49 -0
  174. package/backend/ws/terminal/index.ts +40 -0
  175. package/backend/ws/terminal/persistence.ts +153 -0
  176. package/backend/ws/terminal/session.ts +382 -0
  177. package/backend/ws/terminal/stream.ts +79 -0
  178. package/backend/ws/tunnel/index.ts +14 -0
  179. package/backend/ws/tunnel/operations.ts +91 -0
  180. package/backend/ws/types.ts +20 -0
  181. package/backend/ws/user/crud.ts +156 -0
  182. package/backend/ws/user/index.ts +14 -0
  183. package/bin/clopen.ts +307 -0
  184. package/bun.lock +1352 -0
  185. package/frontend/App.svelte +34 -0
  186. package/frontend/app.css +313 -0
  187. package/frontend/lib/app-environment.ts +10 -0
  188. package/frontend/lib/components/chat/ChatInterface.svelte +407 -0
  189. package/frontend/lib/components/chat/formatters/ErrorMessage.svelte +57 -0
  190. package/frontend/lib/components/chat/formatters/MessageFormatter.svelte +224 -0
  191. package/frontend/lib/components/chat/formatters/TextMessage.svelte +395 -0
  192. package/frontend/lib/components/chat/formatters/Tools.svelte +70 -0
  193. package/frontend/lib/components/chat/formatters/index.ts +3 -0
  194. package/frontend/lib/components/chat/input/ChatInput.svelte +421 -0
  195. package/frontend/lib/components/chat/input/components/ChatInputActions.svelte +78 -0
  196. package/frontend/lib/components/chat/input/components/DragDropOverlay.svelte +30 -0
  197. package/frontend/lib/components/chat/input/components/EditModeIndicator.svelte +33 -0
  198. package/frontend/lib/components/chat/input/components/EngineModelPicker.svelte +619 -0
  199. package/frontend/lib/components/chat/input/components/FileAttachmentPreview.svelte +48 -0
  200. package/frontend/lib/components/chat/input/components/LoadingIndicator.svelte +31 -0
  201. package/frontend/lib/components/chat/input/composables/use-animations.svelte.ts +201 -0
  202. package/frontend/lib/components/chat/input/composables/use-chat-actions.svelte.ts +148 -0
  203. package/frontend/lib/components/chat/input/composables/use-file-handling.svelte.ts +216 -0
  204. package/frontend/lib/components/chat/input/composables/use-input-state.svelte.ts +357 -0
  205. package/frontend/lib/components/chat/input/composables/use-textarea-resize.svelte.ts +57 -0
  206. package/frontend/lib/components/chat/message/ChatMessage.svelte +478 -0
  207. package/frontend/lib/components/chat/message/ChatMessages.svelte +541 -0
  208. package/frontend/lib/components/chat/message/DateSeparator.svelte +86 -0
  209. package/frontend/lib/components/chat/message/MessageBubble.svelte +86 -0
  210. package/frontend/lib/components/chat/message/MessageHeader.svelte +157 -0
  211. package/frontend/lib/components/chat/modal/DebugModal.svelte +59 -0
  212. package/frontend/lib/components/chat/modal/TokenUsageModal.svelte +124 -0
  213. package/frontend/lib/components/chat/shared/index.ts +2 -0
  214. package/frontend/lib/components/chat/shared/utils.ts +116 -0
  215. package/frontend/lib/components/chat/tools/BashOutputTool.svelte +35 -0
  216. package/frontend/lib/components/chat/tools/BashTool.svelte +46 -0
  217. package/frontend/lib/components/chat/tools/CustomMcpTool.svelte +139 -0
  218. package/frontend/lib/components/chat/tools/EditTool.svelte +48 -0
  219. package/frontend/lib/components/chat/tools/ExitPlanModeTool.svelte +32 -0
  220. package/frontend/lib/components/chat/tools/GlobTool.svelte +51 -0
  221. package/frontend/lib/components/chat/tools/GrepTool.svelte +90 -0
  222. package/frontend/lib/components/chat/tools/KillShellTool.svelte +26 -0
  223. package/frontend/lib/components/chat/tools/ListMcpResourcesTool.svelte +31 -0
  224. package/frontend/lib/components/chat/tools/NotebookEditTool.svelte +38 -0
  225. package/frontend/lib/components/chat/tools/ReadMcpResourceTool.svelte +34 -0
  226. package/frontend/lib/components/chat/tools/ReadTool.svelte +41 -0
  227. package/frontend/lib/components/chat/tools/TaskTool.svelte +64 -0
  228. package/frontend/lib/components/chat/tools/TodoWriteTool.svelte +75 -0
  229. package/frontend/lib/components/chat/tools/WebFetchTool.svelte +35 -0
  230. package/frontend/lib/components/chat/tools/WebSearchTool.svelte +84 -0
  231. package/frontend/lib/components/chat/tools/WriteTool.svelte +33 -0
  232. package/frontend/lib/components/chat/tools/components/CodeBlock.svelte +79 -0
  233. package/frontend/lib/components/chat/tools/components/DiffBlock.svelte +408 -0
  234. package/frontend/lib/components/chat/tools/components/FileHeader.svelte +45 -0
  235. package/frontend/lib/components/chat/tools/components/InfoLine.svelte +19 -0
  236. package/frontend/lib/components/chat/tools/components/StatsBadges.svelte +27 -0
  237. package/frontend/lib/components/chat/tools/components/TerminalCommand.svelte +54 -0
  238. package/frontend/lib/components/chat/tools/components/index.ts +7 -0
  239. package/frontend/lib/components/chat/tools/index.ts +26 -0
  240. package/frontend/lib/components/chat/widgets/FloatingTodoList.svelte +249 -0
  241. package/frontend/lib/components/chat/widgets/TokenUsage.svelte +78 -0
  242. package/frontend/lib/components/checkpoint/TimelineModal.svelte +391 -0
  243. package/frontend/lib/components/checkpoint/timeline/TimelineEdge.svelte +26 -0
  244. package/frontend/lib/components/checkpoint/timeline/TimelineGraph.svelte +87 -0
  245. package/frontend/lib/components/checkpoint/timeline/TimelineNode.svelte +108 -0
  246. package/frontend/lib/components/checkpoint/timeline/TimelineVersionGroup.svelte +59 -0
  247. package/frontend/lib/components/checkpoint/timeline/animation.ts +168 -0
  248. package/frontend/lib/components/checkpoint/timeline/config.ts +44 -0
  249. package/frontend/lib/components/checkpoint/timeline/graph-builder.ts +304 -0
  250. package/frontend/lib/components/checkpoint/timeline/types.ts +65 -0
  251. package/frontend/lib/components/checkpoint/timeline/utils.ts +53 -0
  252. package/frontend/lib/components/common/Alert.svelte +139 -0
  253. package/frontend/lib/components/common/AvatarBubble.svelte +56 -0
  254. package/frontend/lib/components/common/Button.svelte +71 -0
  255. package/frontend/lib/components/common/Card.svelte +102 -0
  256. package/frontend/lib/components/common/Checkbox.svelte +48 -0
  257. package/frontend/lib/components/common/Dialog.svelte +249 -0
  258. package/frontend/lib/components/common/FolderBrowser.svelte +843 -0
  259. package/frontend/lib/components/common/Icon.svelte +58 -0
  260. package/frontend/lib/components/common/Input.svelte +72 -0
  261. package/frontend/lib/components/common/Lightbox.svelte +233 -0
  262. package/frontend/lib/components/common/LoadingScreen.svelte +52 -0
  263. package/frontend/lib/components/common/LoadingSpinner.svelte +48 -0
  264. package/frontend/lib/components/common/Modal.svelte +177 -0
  265. package/frontend/lib/components/common/ModalProvider.svelte +28 -0
  266. package/frontend/lib/components/common/ModelSelector.svelte +110 -0
  267. package/frontend/lib/components/common/MonacoEditor.svelte +569 -0
  268. package/frontend/lib/components/common/NotificationToast.svelte +113 -0
  269. package/frontend/lib/components/common/PageTemplate.svelte +76 -0
  270. package/frontend/lib/components/common/ProjectUserAvatars.svelte +79 -0
  271. package/frontend/lib/components/common/Select.svelte +98 -0
  272. package/frontend/lib/components/common/Textarea.svelte +80 -0
  273. package/frontend/lib/components/common/ThemeToggle.svelte +44 -0
  274. package/frontend/lib/components/common/lucide-icons.ts +1642 -0
  275. package/frontend/lib/components/common/material-icons.ts +1082 -0
  276. package/frontend/lib/components/common/xterm/XTerm.svelte +796 -0
  277. package/frontend/lib/components/common/xterm/index.ts +16 -0
  278. package/frontend/lib/components/common/xterm/terminal-config.ts +68 -0
  279. package/frontend/lib/components/common/xterm/types.ts +31 -0
  280. package/frontend/lib/components/common/xterm/xterm-service.ts +353 -0
  281. package/frontend/lib/components/files/FileNode.svelte +384 -0
  282. package/frontend/lib/components/files/FileTree.svelte +681 -0
  283. package/frontend/lib/components/files/FileViewer.svelte +728 -0
  284. package/frontend/lib/components/files/SearchResults.svelte +303 -0
  285. package/frontend/lib/components/git/BranchManager.svelte +458 -0
  286. package/frontend/lib/components/git/ChangesSection.svelte +107 -0
  287. package/frontend/lib/components/git/CommitForm.svelte +76 -0
  288. package/frontend/lib/components/git/ConflictResolver.svelte +158 -0
  289. package/frontend/lib/components/git/DiffViewer.svelte +364 -0
  290. package/frontend/lib/components/git/FileChangeItem.svelte +97 -0
  291. package/frontend/lib/components/git/GitButton.svelte +33 -0
  292. package/frontend/lib/components/git/GitLog.svelte +361 -0
  293. package/frontend/lib/components/git/GitModal.svelte +80 -0
  294. package/frontend/lib/components/history/HistoryModal.svelte +563 -0
  295. package/frontend/lib/components/history/HistoryView.svelte +615 -0
  296. package/frontend/lib/components/index.ts +34 -0
  297. package/frontend/lib/components/preview/browser/BrowserPreview.svelte +549 -0
  298. package/frontend/lib/components/preview/browser/components/Canvas.svelte +1058 -0
  299. package/frontend/lib/components/preview/browser/components/ConsolePanel.svelte +757 -0
  300. package/frontend/lib/components/preview/browser/components/Container.svelte +450 -0
  301. package/frontend/lib/components/preview/browser/components/ContextMenu.svelte +236 -0
  302. package/frontend/lib/components/preview/browser/components/SelectDropdown.svelte +224 -0
  303. package/frontend/lib/components/preview/browser/components/Toolbar.svelte +339 -0
  304. package/frontend/lib/components/preview/browser/components/VirtualCursor.svelte +36 -0
  305. package/frontend/lib/components/preview/browser/core/cleanup.svelte.ts +155 -0
  306. package/frontend/lib/components/preview/browser/core/coordinator.svelte.ts +837 -0
  307. package/frontend/lib/components/preview/browser/core/interactions.svelte.ts +113 -0
  308. package/frontend/lib/components/preview/browser/core/mcp-handlers.svelte.ts +296 -0
  309. package/frontend/lib/components/preview/browser/core/native-ui-handlers.svelte.ts +391 -0
  310. package/frontend/lib/components/preview/browser/core/stream-handler.svelte.ts +231 -0
  311. package/frontend/lib/components/preview/browser/core/tab-manager.svelte.ts +210 -0
  312. package/frontend/lib/components/preview/browser/core/tab-operations.svelte.ts +239 -0
  313. package/frontend/lib/components/preview/index.ts +2 -0
  314. package/frontend/lib/components/settings/SettingsModal.svelte +235 -0
  315. package/frontend/lib/components/settings/SettingsView.svelte +36 -0
  316. package/frontend/lib/components/settings/appearance/AppearanceSettings.svelte +51 -0
  317. package/frontend/lib/components/settings/appearance/LayoutPresetSettings.svelte +160 -0
  318. package/frontend/lib/components/settings/appearance/LayoutPreview.svelte +76 -0
  319. package/frontend/lib/components/settings/engines/AIEnginesSettings.svelte +917 -0
  320. package/frontend/lib/components/settings/general/AdvancedSettings.svelte +187 -0
  321. package/frontend/lib/components/settings/general/DataManagementSettings.svelte +203 -0
  322. package/frontend/lib/components/settings/general/GeneralSettings.svelte +10 -0
  323. package/frontend/lib/components/settings/model/ModelSettings.svelte +357 -0
  324. package/frontend/lib/components/settings/notifications/NotificationSettings.svelte +205 -0
  325. package/frontend/lib/components/settings/user/UserSettings.svelte +197 -0
  326. package/frontend/lib/components/terminal/Terminal.svelte +368 -0
  327. package/frontend/lib/components/terminal/TerminalTabs.svelte +87 -0
  328. package/frontend/lib/components/terminal/TerminalView.svelte +55 -0
  329. package/frontend/lib/components/tunnel/TunnelActive.svelte +142 -0
  330. package/frontend/lib/components/tunnel/TunnelButton.svelte +54 -0
  331. package/frontend/lib/components/tunnel/TunnelInactive.svelte +284 -0
  332. package/frontend/lib/components/tunnel/TunnelModal.svelte +47 -0
  333. package/frontend/lib/components/tunnel/TunnelQRCode.svelte +49 -0
  334. package/frontend/lib/components/workspace/DesktopNavigator.svelte +382 -0
  335. package/frontend/lib/components/workspace/MobileNavigator.svelte +403 -0
  336. package/frontend/lib/components/workspace/PanelContainer.svelte +100 -0
  337. package/frontend/lib/components/workspace/PanelHeader.svelte +505 -0
  338. package/frontend/lib/components/workspace/ViewMenu.svelte +162 -0
  339. package/frontend/lib/components/workspace/WorkspaceLayout.svelte +169 -0
  340. package/frontend/lib/components/workspace/layout/DesktopLayout.svelte +15 -0
  341. package/frontend/lib/components/workspace/layout/MobileLayout.svelte +17 -0
  342. package/frontend/lib/components/workspace/layout/split-pane/Container.svelte +42 -0
  343. package/frontend/lib/components/workspace/layout/split-pane/Handle.svelte +85 -0
  344. package/frontend/lib/components/workspace/layout/split-pane/Layout.svelte +37 -0
  345. package/frontend/lib/components/workspace/panels/ChatPanel.svelte +274 -0
  346. package/frontend/lib/components/workspace/panels/FilesPanel.svelte +1261 -0
  347. package/frontend/lib/components/workspace/panels/GitPanel.svelte +1560 -0
  348. package/frontend/lib/components/workspace/panels/PreviewPanel.svelte +150 -0
  349. package/frontend/lib/components/workspace/panels/TerminalPanel.svelte +73 -0
  350. package/frontend/lib/constants/preview.ts +45 -0
  351. package/frontend/lib/services/chat/chat.service.ts +704 -0
  352. package/frontend/lib/services/chat/index.ts +7 -0
  353. package/frontend/lib/services/notification/global-stream-monitor.ts +86 -0
  354. package/frontend/lib/services/notification/index.ts +8 -0
  355. package/frontend/lib/services/notification/push.service.ts +144 -0
  356. package/frontend/lib/services/notification/sound.service.ts +127 -0
  357. package/frontend/lib/services/preview/browser/browser-console.service.ts +61 -0
  358. package/frontend/lib/services/preview/browser/browser-webcodecs.service.ts +1499 -0
  359. package/frontend/lib/services/preview/browser/mcp-integration.svelte.ts +67 -0
  360. package/frontend/lib/services/preview/index.ts +23 -0
  361. package/frontend/lib/services/project/index.ts +8 -0
  362. package/frontend/lib/services/project/status.service.ts +159 -0
  363. package/frontend/lib/services/snapshot/snapshot.service.ts +47 -0
  364. package/frontend/lib/services/terminal/background/index.ts +130 -0
  365. package/frontend/lib/services/terminal/background/session-restore.ts +274 -0
  366. package/frontend/lib/services/terminal/background/stream-manager.ts +286 -0
  367. package/frontend/lib/services/terminal/index.ts +14 -0
  368. package/frontend/lib/services/terminal/persistence.service.ts +260 -0
  369. package/frontend/lib/services/terminal/project.service.ts +953 -0
  370. package/frontend/lib/services/terminal/session.service.ts +364 -0
  371. package/frontend/lib/services/terminal/terminal.service.ts +369 -0
  372. package/frontend/lib/stores/core/app.svelte.ts +117 -0
  373. package/frontend/lib/stores/core/files.svelte.ts +73 -0
  374. package/frontend/lib/stores/core/presence.svelte.ts +48 -0
  375. package/frontend/lib/stores/core/projects.svelte.ts +317 -0
  376. package/frontend/lib/stores/core/sessions.svelte.ts +383 -0
  377. package/frontend/lib/stores/features/claude-accounts.svelte.ts +58 -0
  378. package/frontend/lib/stores/features/models.svelte.ts +89 -0
  379. package/frontend/lib/stores/features/settings.svelte.ts +87 -0
  380. package/frontend/lib/stores/features/terminal.svelte.ts +701 -0
  381. package/frontend/lib/stores/features/tunnel.svelte.ts +161 -0
  382. package/frontend/lib/stores/features/user.svelte.ts +96 -0
  383. package/frontend/lib/stores/ui/chat-input.svelte.ts +57 -0
  384. package/frontend/lib/stores/ui/chat-model.svelte.ts +61 -0
  385. package/frontend/lib/stores/ui/dialog.svelte.ts +59 -0
  386. package/frontend/lib/stores/ui/edit-mode.svelte.ts +214 -0
  387. package/frontend/lib/stores/ui/notification.svelte.ts +166 -0
  388. package/frontend/lib/stores/ui/settings-modal.svelte.ts +88 -0
  389. package/frontend/lib/stores/ui/theme.svelte.ts +179 -0
  390. package/frontend/lib/stores/ui/workspace.svelte.ts +754 -0
  391. package/frontend/lib/types/native-ui.ts +73 -0
  392. package/frontend/lib/utils/chat/date-separator.ts +39 -0
  393. package/frontend/lib/utils/chat/message-grouper.ts +219 -0
  394. package/frontend/lib/utils/chat/message-processor.ts +135 -0
  395. package/frontend/lib/utils/chat/tool-handler.ts +161 -0
  396. package/frontend/lib/utils/chat/virtual-scroll.svelte.ts +142 -0
  397. package/frontend/lib/utils/click-outside.ts +20 -0
  398. package/frontend/lib/utils/context-manager.ts +257 -0
  399. package/frontend/lib/utils/file-icon-mappings.ts +769 -0
  400. package/frontend/lib/utils/folder-icon-mappings.ts +1030 -0
  401. package/frontend/lib/utils/git-status.ts +68 -0
  402. package/frontend/lib/utils/platform.ts +113 -0
  403. package/frontend/lib/utils/port-check.ts +65 -0
  404. package/frontend/lib/utils/terminalFormatter.ts +207 -0
  405. package/frontend/lib/utils/theme.ts +6 -0
  406. package/frontend/lib/utils/tree-visualizer.ts +320 -0
  407. package/frontend/lib/utils/ws.ts +44 -0
  408. package/frontend/main.ts +13 -0
  409. package/index.html +70 -0
  410. package/package.json +111 -0
  411. package/scripts/generate-icons.ts +87 -0
  412. package/scripts/pre-publish-check.sh +142 -0
  413. package/scripts/setup-hooks.sh +134 -0
  414. package/scripts/validate-branch-name.sh +47 -0
  415. package/scripts/validate-commit-msg.sh +42 -0
  416. package/shared/constants/engines.ts +134 -0
  417. package/shared/types/database/connection.ts +16 -0
  418. package/shared/types/database/index.ts +6 -0
  419. package/shared/types/database/schema.ts +141 -0
  420. package/shared/types/engine/index.ts +45 -0
  421. package/shared/types/filesystem/index.ts +22 -0
  422. package/shared/types/git.ts +171 -0
  423. package/shared/types/messaging/index.ts +239 -0
  424. package/shared/types/messaging/tool.ts +526 -0
  425. package/shared/types/network/api.ts +18 -0
  426. package/shared/types/network/index.ts +5 -0
  427. package/shared/types/stores/app.ts +23 -0
  428. package/shared/types/stores/dialog.ts +21 -0
  429. package/shared/types/stores/index.ts +3 -0
  430. package/shared/types/stores/settings.ts +15 -0
  431. package/shared/types/terminal/index.ts +44 -0
  432. package/shared/types/ui/components.ts +61 -0
  433. package/shared/types/ui/icons.ts +23 -0
  434. package/shared/types/ui/index.ts +22 -0
  435. package/shared/types/ui/notifications.ts +14 -0
  436. package/shared/types/ui/theme.ts +12 -0
  437. package/shared/types/websocket/index.ts +43 -0
  438. package/shared/types/window.d.ts +13 -0
  439. package/shared/utils/anonymous-user.ts +168 -0
  440. package/shared/utils/async.ts +10 -0
  441. package/shared/utils/diff-calculator.ts +184 -0
  442. package/shared/utils/file-type-detection.ts +166 -0
  443. package/shared/utils/logger.ts +158 -0
  444. package/shared/utils/message-formatter.ts +79 -0
  445. package/shared/utils/path.ts +47 -0
  446. package/shared/utils/ws-client.ts +768 -0
  447. package/shared/utils/ws-server.ts +660 -0
  448. package/static/audio/notification.ogg +0 -0
  449. package/static/favicon.svg +8 -0
  450. package/static/fonts/dm-sans/dm-sans-italic-latin-ext.woff2 +0 -0
  451. package/static/fonts/dm-sans/dm-sans-italic-latin.woff2 +0 -0
  452. package/static/fonts/dm-sans/dm-sans-normal-latin-ext.woff2 +0 -0
  453. package/static/fonts/dm-sans/dm-sans-normal-latin.woff2 +0 -0
  454. package/static/fonts/dm-sans.css +96 -0
  455. package/svelte.config.js +20 -0
  456. 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
+ }