@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,953 @@
1
+ /**
2
+ * Terminal Project Manager
3
+ * Manages terminal sessions per project with automatic context switching
4
+ */
5
+
6
+ import { terminalSessionManager, type TerminalSessionState } from './session.service';
7
+ import { terminalPersistenceManager } from './persistence.service';
8
+ import { terminalStore } from '$frontend/lib/stores/features/terminal.svelte';
9
+ import type { TerminalSession } from '$shared/types/terminal';
10
+ import { terminalService } from './terminal.service';
11
+ import { debug } from '$shared/utils/logger';
12
+ interface ProjectTerminalContext {
13
+ projectId: string;
14
+ projectPath: string;
15
+ sessionIds: string[];
16
+ activeSessionId: string | null;
17
+ lastActiveAt: Date;
18
+ // Store terminal output per session for this project
19
+ sessionOutputs: Map<string, Array<{ content: string; type: string; timestamp: Date }>>;
20
+ // Store command history per session for this project
21
+ sessionCommandHistories: Map<string, string[]>;
22
+ }
23
+
24
+ class TerminalProjectManager {
25
+ private projectContexts = new Map<string, ProjectTerminalContext>();
26
+ private currentProjectId: string | null = null;
27
+
28
+ constructor() {
29
+ // Initialize will be called from outside to set up event listeners
30
+ }
31
+
32
+ /**
33
+ * Initialize or get terminal context for a project
34
+ */
35
+ getOrCreateProjectContext(projectId: string, projectPath: string): ProjectTerminalContext {
36
+ let context = this.projectContexts.get(projectId);
37
+
38
+ if (!context) {
39
+ context = {
40
+ projectId,
41
+ projectPath,
42
+ sessionIds: [],
43
+ activeSessionId: null,
44
+ lastActiveAt: new Date(),
45
+ sessionOutputs: new Map(),
46
+ sessionCommandHistories: new Map()
47
+ };
48
+ this.projectContexts.set(projectId, context);
49
+ this.persistContexts();
50
+ }
51
+
52
+ return context;
53
+ }
54
+
55
+ /**
56
+ * Switch to a different project's terminal context
57
+ */
58
+ async switchToProject(projectId: string, projectPath: string): Promise<void> {
59
+ // Switching to project
60
+
61
+ // Save current project's terminal state if switching away
62
+ if (this.currentProjectId && this.currentProjectId !== projectId) {
63
+ await this.saveCurrentProjectState();
64
+ // Hide current project's terminal sessions (but keep them running)
65
+ await this.hideProjectTerminalSessions(this.currentProjectId);
66
+
67
+ // Background output collection is handled by the server
68
+ }
69
+
70
+ // Get or create context for the new project
71
+ const context = this.getOrCreateProjectContext(projectId, projectPath);
72
+
73
+ // Context for project
74
+
75
+ // Clear any existing sessions in store to prevent cross-contamination
76
+ if (this.currentProjectId !== projectId) {
77
+ // Clear all sessions properly
78
+ terminalStore.clearAllSessions();
79
+ }
80
+
81
+ // Restore or create terminal sessions for this project
82
+ if (context.sessionIds.length === 0) {
83
+ // No sessions for project, creating initial session
84
+ await this.createProjectTerminalSessions(projectId, projectPath);
85
+ } else {
86
+ // Show existing terminal sessions for this project
87
+ await this.showProjectTerminalSessions(context);
88
+ }
89
+
90
+ // Update current project
91
+ this.currentProjectId = projectId;
92
+ context.lastActiveAt = new Date();
93
+ this.persistContexts();
94
+
95
+ // Check for active streams for this project after switching
96
+ await this.checkAndRestoreActiveStreams(projectId);
97
+ }
98
+
99
+ /**
100
+ * Create initial terminal sessions for a project
101
+ */
102
+ private async createProjectTerminalSessions(projectId: string, projectPath: string): Promise<void> {
103
+ // Creating terminal session for project
104
+
105
+ const context = this.getOrCreateProjectContext(projectId, projectPath);
106
+
107
+ // Create only 1 terminal session by default with correct project path and projectId
108
+ const sessionId = terminalStore.createNewSession(projectPath, projectPath, projectId);
109
+
110
+ // Update the session's directory to ensure it's correct
111
+ const session = terminalStore.getSession(sessionId);
112
+ if (session) {
113
+ session.directory = projectPath;
114
+ }
115
+
116
+ // Create a fresh session in terminalSessionManager with correct project association
117
+ terminalSessionManager.createSession(sessionId, projectId, projectPath, projectPath);
118
+
119
+ context.sessionIds.push(sessionId);
120
+ context.activeSessionId = sessionId;
121
+ terminalStore.switchToSession(sessionId);
122
+
123
+ this.persistContexts();
124
+ }
125
+
126
+ /**
127
+ * Hide terminal sessions for a project (keep them running in background)
128
+ */
129
+ private async hideProjectTerminalSessions(projectId: string): Promise<void> {
130
+ const context = this.projectContexts.get(projectId);
131
+ if (!context) return;
132
+
133
+ // Hiding sessions for project
134
+
135
+ // Save execution state before hiding (in-memory via persistence manager)
136
+ for (const sessionId of context.sessionIds) {
137
+ const session = terminalSessionManager.getSession(sessionId);
138
+ if (session && session.isExecuting && session.streamId) {
139
+ terminalPersistenceManager.saveActiveStream(
140
+ sessionId, session.streamId, session.lastCommand || '', projectId
141
+ );
142
+ }
143
+ }
144
+
145
+ // CRITICAL: Clean up WebSocket listeners for ALL sessions in this project
146
+ // This prevents stale listeners from accumulating when switching projects
147
+ // which would cause duplicate terminal output processing (input duplication bug)
148
+ for (const sessionId of context.sessionIds) {
149
+ terminalService.cleanupListeners(sessionId);
150
+ }
151
+
152
+ // Clear the terminal store to hide sessions from UI
153
+ // Sessions are preserved in terminalSessionManager
154
+ terminalStore.clearAllSessions();
155
+ }
156
+
157
+ /**
158
+ * Show terminal sessions for a project
159
+ */
160
+ private async showProjectTerminalSessions(context: ProjectTerminalContext): Promise<void> {
161
+ // Showing sessions for project
162
+
163
+ // Clear all sessions from store first to ensure clean slate
164
+ terminalStore.clearAllSessions();
165
+
166
+ // Import terminalPersistenceManager to check for active streams
167
+ const { terminalPersistenceManager } = await import('./persistence.service');
168
+ const allActiveStreams = terminalPersistenceManager.getAllActiveStreams();
169
+
170
+ // Restore all sessions for this project from saved state
171
+ if (context.sessionIds.length > 0) {
172
+ for (const sessionId of context.sessionIds) {
173
+ // Create a new terminal session in the store
174
+ // Extract terminal number from sessionId (format: projectId-terminal-N or terminal-N)
175
+ const sessionParts = sessionId.split('-');
176
+ const terminalNumber = sessionParts[sessionParts.length - 1] || '1';
177
+
178
+ const terminalSession: TerminalSession = {
179
+ id: sessionId,
180
+ name: `Terminal ${terminalNumber}`,
181
+ directory: context.projectPath, // Always use the project path as base directory
182
+ lines: [],
183
+ commandHistory: [],
184
+ isActive: false,
185
+ createdAt: new Date(),
186
+ lastUsedAt: new Date(),
187
+ shellType: 'Unknown',
188
+ terminalBuffer: undefined,
189
+ projectId: context.projectId,
190
+ projectPath: context.projectPath
191
+ };
192
+
193
+ // Check for active streams for this session first (before restoring output)
194
+ let hasActiveStream = false;
195
+ let activeStreamInfo: any = null;
196
+ for (const stream of allActiveStreams) {
197
+ // Match by projectId and terminal number
198
+ if (stream.projectId === context.projectId) {
199
+ // Extract terminal number from stream sessionId
200
+ const streamParts = stream.sessionId.split('-');
201
+ const streamTerminalNumber = streamParts[streamParts.length - 1];
202
+ if (streamTerminalNumber === terminalNumber) {
203
+ hasActiveStream = true;
204
+ activeStreamInfo = stream;
205
+ break;
206
+ }
207
+ }
208
+ }
209
+
210
+ // Restore saved output for this session
211
+ let baseOutput: any[] = [];
212
+ let backgroundOutput: any[] = [];
213
+
214
+ // First, restore base output from context (input/output sebelumnya)
215
+ if (context.sessionOutputs.has(sessionId)) {
216
+ const savedOutput = context.sessionOutputs.get(sessionId);
217
+ if (savedOutput) {
218
+ baseOutput = savedOutput.map(output => ({
219
+ content: output.content,
220
+ type: output.type as any,
221
+ timestamp: output.timestamp
222
+ }));
223
+ // Restored base output lines for session
224
+ }
225
+ }
226
+
227
+ // Second, get NEW output from server that was generated while we were away
228
+ // We need to track when we saved the output to know what's new
229
+ if (hasActiveStream && activeStreamInfo) {
230
+ try {
231
+ // Get the saved output count from context metadata
232
+ // This tells us how much output we had when we switched away
233
+ let savedOutputCount = 0;
234
+ const savedMetadata = context.sessionOutputs.get(`${sessionId}-metadata`);
235
+ if (savedMetadata && typeof savedMetadata === 'object' && 'outputCount' in savedMetadata) {
236
+ savedOutputCount = (savedMetadata as any).outputCount || 0;
237
+ } else {
238
+ // Fallback: count actual output lines in baseOutput
239
+ for (const line of baseOutput) {
240
+ if (line.type === 'output' || line.type === 'error') {
241
+ savedOutputCount++;
242
+ }
243
+ }
244
+ }
245
+
246
+ // Get only NEW output from server (skip what we already have)
247
+ const data = await terminalService.getMissedOutput(
248
+ sessionId,
249
+ activeStreamInfo.streamId,
250
+ savedOutputCount
251
+ );
252
+ if (data.success && data.output && data.output.length > 0) {
253
+ // Convert server output to terminal lines
254
+ backgroundOutput = data.output.map((content: string) => ({
255
+ content: content,
256
+ type: 'output',
257
+ timestamp: new Date()
258
+ }));
259
+ debug.log('terminal', `Restored ${backgroundOutput.length} new output lines for session ${sessionId}`);
260
+ }
261
+ } catch (error) {
262
+ debug.error('terminal', 'Failed to fetch missed output:', error);
263
+ }
264
+ }
265
+
266
+ // Combine base output with background output from server
267
+ // Base output contains previous input/output saved in context
268
+ // Background output contains new output from server (if any)
269
+ terminalSession.lines = [...baseOutput, ...backgroundOutput];
270
+
271
+ // Restore command history from context (persisted) rather than manager (temporary)
272
+ const savedCommandHistory = context.sessionCommandHistories.get(sessionId);
273
+ if (savedCommandHistory) {
274
+ terminalSession.commandHistory = savedCommandHistory;
275
+ // Restored command history items for session
276
+ }
277
+
278
+ // Check if session exists in manager for execution state
279
+ const managerSession = terminalSessionManager.getSession(sessionId);
280
+ if (managerSession) {
281
+ // Update manager's command history with persisted history
282
+ if (savedCommandHistory) {
283
+ managerSession.commandHistory = savedCommandHistory;
284
+ }
285
+ // Always use project path for directory when switching projects
286
+ // This ensures the visual directory is correct
287
+ terminalSession.directory = context.projectPath;
288
+
289
+ // IMPORTANT: Restore execution state to fix interrupt button visibility
290
+ // When switching back to a project with running processes, the execution
291
+ // state must be preserved for the interrupt button to show correctly
292
+ if (hasActiveStream || managerSession.isExecuting) {
293
+ // Restoring execution state for session
294
+ // Update the store's execution state for this session
295
+ terminalStore.setExecutingState(sessionId, true);
296
+
297
+ // Update manager session with stream info if available
298
+ if (activeStreamInfo) {
299
+ managerSession.isExecuting = true;
300
+ managerSession.streamId = activeStreamInfo.streamId;
301
+ managerSession.lastCommand = activeStreamInfo.command;
302
+ }
303
+ }
304
+ }
305
+
306
+ // Add session to store
307
+ terminalStore.addSession(terminalSession);
308
+
309
+ // Ensure session exists in manager with correct project association and directory
310
+ if (!managerSession || managerSession.projectId !== context.projectId) {
311
+ const newSession = terminalSessionManager.createSession(sessionId, context.projectId, context.projectPath, context.projectPath);
312
+ // If we have active stream info, update the new session
313
+ if (hasActiveStream && activeStreamInfo) {
314
+ newSession.isExecuting = true;
315
+ newSession.streamId = activeStreamInfo.streamId;
316
+ newSession.lastCommand = activeStreamInfo.command;
317
+ }
318
+ } else {
319
+ // Update existing session to ensure correct directory
320
+ terminalSessionManager.updateSession(sessionId, {
321
+ projectId: context.projectId,
322
+ projectPath: context.projectPath,
323
+ workingDirectory: context.projectPath
324
+ });
325
+ }
326
+ }
327
+ } else {
328
+ // No sessions exist, create a new one
329
+ // No sessions for project, creating new
330
+ const newSessionId = terminalStore.createNewSession(context.projectPath, context.projectPath, context.projectId);
331
+
332
+ // Create in manager
333
+ terminalSessionManager.createSession(newSessionId, context.projectId, context.projectPath, context.projectPath);
334
+
335
+ context.sessionIds.push(newSessionId);
336
+ context.activeSessionId = newSessionId;
337
+ }
338
+
339
+ // Switch to the active session for this project
340
+ if (context.activeSessionId && context.sessionIds.includes(context.activeSessionId)) {
341
+ terminalStore.switchToSession(context.activeSessionId);
342
+ } else if (context.sessionIds.length > 0) {
343
+ // Active session is invalid, use first available
344
+ context.activeSessionId = context.sessionIds[0];
345
+ terminalStore.switchToSession(context.activeSessionId);
346
+ }
347
+
348
+ this.persistContexts();
349
+ }
350
+
351
+ /**
352
+ * Restore terminal sessions for a project (deprecated - use showProjectTerminalSessions)
353
+ */
354
+ private async restoreProjectTerminalSessions(context: ProjectTerminalContext): Promise<void> {
355
+ // Restoring sessions for project
356
+
357
+ // Show sessions for this project
358
+ for (const sessionId of context.sessionIds) {
359
+ const storeSession = terminalStore.getSession(sessionId);
360
+ const managerSession = terminalSessionManager.getSession(sessionId);
361
+
362
+ if (storeSession) {
363
+ // Session exists in store, just make it visible
364
+ // Session already exists, making visible
365
+ } else if (managerSession) {
366
+ // Session exists in manager but not in store, restore it
367
+ // Restoring session from manager
368
+ this.restoreSessionToStore(managerSession);
369
+ } else {
370
+ // Session doesn't exist, create a new one
371
+ // Session not found, creating new
372
+ const newSessionId = terminalStore.createNewSession(context.projectPath, context.projectPath, context.projectId);
373
+
374
+ // Update context with new session ID
375
+ const index = context.sessionIds.indexOf(sessionId);
376
+ if (index !== -1) {
377
+ context.sessionIds[index] = newSessionId;
378
+ }
379
+
380
+ // Link to project
381
+ terminalSessionManager.updateSession(newSessionId, {
382
+ projectId: context.projectId,
383
+ projectPath: context.projectPath
384
+ });
385
+
386
+ // Update active session ID if needed
387
+ if (context.activeSessionId === sessionId) {
388
+ context.activeSessionId = newSessionId;
389
+ }
390
+ }
391
+ }
392
+
393
+ // Switch to the active session for this project
394
+ if (context.activeSessionId) {
395
+ terminalStore.switchToSession(context.activeSessionId);
396
+ } else if (context.sessionIds.length > 0) {
397
+ // No active session, use the first one
398
+ context.activeSessionId = context.sessionIds[0];
399
+ terminalStore.switchToSession(context.activeSessionId);
400
+ }
401
+ }
402
+
403
+ /**
404
+ * Restore a session from manager to store with project-specific output
405
+ */
406
+ private restoreSessionToStore(managerSession: TerminalSessionState, projectContext?: ProjectTerminalContext): void {
407
+ // Ensure the working directory is correct for this project
408
+ const correctDirectory = managerSession.projectPath || managerSession.workingDirectory;
409
+
410
+ // Extract terminal number from sessionId (format: projectId-terminal-N or terminal-N)
411
+ const sessionParts = managerSession.id.split('-');
412
+ const terminalNumber = sessionParts[sessionParts.length - 1] || '1';
413
+
414
+ const terminalSession: TerminalSession = {
415
+ id: managerSession.id,
416
+ name: `Terminal ${terminalNumber}`,
417
+ directory: correctDirectory,
418
+ lines: [],
419
+ commandHistory: managerSession.commandHistory || [],
420
+ isActive: false,
421
+ createdAt: managerSession.createdAt || new Date(),
422
+ lastUsedAt: managerSession.lastUsedAt || new Date(),
423
+ shellType: 'Unknown',
424
+ terminalBuffer: undefined,
425
+ projectId: managerSession.projectId,
426
+ projectPath: managerSession.projectPath
427
+ };
428
+
429
+ // Restore output lines from project context if available
430
+ if (projectContext && projectContext.sessionOutputs.has(managerSession.id)) {
431
+ const savedOutput = projectContext.sessionOutputs.get(managerSession.id);
432
+ if (savedOutput) {
433
+ terminalSession.lines = savedOutput.map(output => ({
434
+ content: output.content,
435
+ type: output.type as any,
436
+ timestamp: output.timestamp
437
+ }));
438
+ // Restored lines for session
439
+ }
440
+ }
441
+
442
+ terminalStore.addSession(terminalSession);
443
+ }
444
+
445
+
446
+ /**
447
+ * Save current project's terminal state
448
+ */
449
+ private async saveCurrentProjectState(): Promise<void> {
450
+ if (!this.currentProjectId) return;
451
+
452
+ const context = this.projectContexts.get(this.currentProjectId);
453
+ if (!context) return;
454
+
455
+ // Saving state for project
456
+
457
+ // Update active session ID
458
+ const activeSessionId = terminalStore.activeSessionId;
459
+ if (activeSessionId) {
460
+ context.activeSessionId = activeSessionId;
461
+ }
462
+
463
+ // Save session outputs and states
464
+ for (const sessionId of context.sessionIds) {
465
+ const session = terminalStore.getSession(sessionId);
466
+ if (session) {
467
+ // Save output lines for this specific project session
468
+ const outputLines = session.lines.map(line => ({
469
+ content: line.content,
470
+ type: line.type,
471
+ timestamp: line.timestamp || new Date()
472
+ }));
473
+ context.sessionOutputs.set(sessionId, outputLines);
474
+
475
+ // Save metadata about how many output lines we have
476
+ // This helps us know what's new when we come back
477
+ let outputCount = 0;
478
+ for (const line of session.lines) {
479
+ if (line.type === 'output' || line.type === 'error') {
480
+ outputCount++;
481
+ }
482
+ }
483
+ context.sessionOutputs.set(`${sessionId}-metadata`, { outputCount } as any);
484
+
485
+ // Save command history for this specific project session
486
+ if (session.commandHistory && session.commandHistory.length > 0) {
487
+ context.sessionCommandHistories.set(sessionId, session.commandHistory);
488
+ // Saved command history items for session
489
+ }
490
+
491
+ // Update session metadata in manager
492
+ terminalSessionManager.updateSession(sessionId, {
493
+ commandHistory: session.commandHistory,
494
+ workingDirectory: session.directory,
495
+ projectId: this.currentProjectId,
496
+ projectPath: context.projectPath
497
+ });
498
+ }
499
+ }
500
+
501
+ this.persistContexts();
502
+ }
503
+
504
+ /**
505
+ * Save all projects' terminal states (called before browser unload)
506
+ */
507
+ saveAllProjectStates(): void {
508
+ // Saving all project states before unload
509
+
510
+ // Save current project state first
511
+ if (this.currentProjectId) {
512
+ this.saveCurrentProjectState();
513
+ }
514
+
515
+ // Save command history from terminalSessionManager for all projects
516
+ for (const [projectId, context] of this.projectContexts.entries()) {
517
+ if (projectId === this.currentProjectId) {
518
+ // Already saved above
519
+ continue;
520
+ }
521
+
522
+ // Saving state for background project
523
+
524
+ // Update command history from session manager for all sessions
525
+ for (const sessionId of context.sessionIds) {
526
+ const managerSession = terminalSessionManager.getSession(sessionId);
527
+ if (managerSession && managerSession.commandHistory) {
528
+ // Save command history from manager
529
+ context.sessionCommandHistories.set(sessionId, managerSession.commandHistory);
530
+ // Saved command history items for background session
531
+ }
532
+ }
533
+ }
534
+
535
+ // Sync stream info to persistence manager
536
+ this.persistContexts();
537
+ }
538
+
539
+ /**
540
+ * Add a new terminal session to the current project
541
+ */
542
+ addTerminalToCurrentProject(forceProjectId?: string, forceProjectPath?: string): string | null {
543
+ // Use forced project ID if provided (for cases where currentProjectId isn't set yet)
544
+ const projectIdToUse = forceProjectId || this.currentProjectId;
545
+ if (!projectIdToUse) {
546
+ // Cannot add terminal: no project ID available
547
+ return null;
548
+ }
549
+
550
+ // Get or create context
551
+ let context = this.projectContexts.get(projectIdToUse);
552
+ if (!context && forceProjectPath) {
553
+ // Create context if it doesn't exist and we have a path
554
+ context = this.getOrCreateProjectContext(projectIdToUse, forceProjectPath);
555
+ }
556
+ if (!context) {
557
+ // Cannot add terminal: no context for project
558
+ return null;
559
+ }
560
+
561
+ // Update current project ID if we're forcing it
562
+ if (forceProjectId && forceProjectId !== this.currentProjectId) {
563
+ this.currentProjectId = forceProjectId;
564
+ }
565
+
566
+ // Adding new terminal to project
567
+
568
+ // Create new session with correct project path and projectId for proper isolation
569
+ const sessionId = terminalStore.createNewSession(context.projectPath, context.projectPath, projectIdToUse);
570
+
571
+ // Ensure the directory is set correctly
572
+ const session = terminalStore.getSession(sessionId);
573
+ if (session) {
574
+ session.directory = context.projectPath;
575
+ }
576
+
577
+ // Create corresponding session in manager with correct project association
578
+ terminalSessionManager.createSession(sessionId, projectIdToUse, context.projectPath, context.projectPath);
579
+
580
+ // Add to context
581
+ context.sessionIds.push(sessionId);
582
+ context.activeSessionId = sessionId;
583
+ this.persistContexts();
584
+
585
+ return sessionId;
586
+ }
587
+
588
+ /**
589
+ * Remove a terminal session from the current project
590
+ */
591
+ async removeTerminalFromCurrentProject(sessionId: string): Promise<boolean> {
592
+ if (!this.currentProjectId) return false;
593
+
594
+ const context = this.projectContexts.get(this.currentProjectId);
595
+ if (!context) return false;
596
+
597
+ // Don't allow removing the last terminal
598
+ if (context.sessionIds.length <= 1) return false;
599
+
600
+ // Remove from context
601
+ context.sessionIds = context.sessionIds.filter(id => id !== sessionId);
602
+
603
+ // Update active session if needed
604
+ if (context.activeSessionId === sessionId && context.sessionIds.length > 0) {
605
+ context.activeSessionId = context.sessionIds[0];
606
+ }
607
+
608
+ // Remove from stores
609
+ await terminalStore.closeSession(sessionId);
610
+ terminalSessionManager.removeSession(sessionId);
611
+
612
+ this.persistContexts();
613
+ return true;
614
+ }
615
+
616
+ /**
617
+ * Remove a session from project context (called when terminal tab is closed)
618
+ * This is different from removeTerminalFromCurrentProject - it doesn't call closeSession
619
+ * to avoid circular dependency
620
+ */
621
+ removeSessionFromContext(sessionId: string): void {
622
+ debug.log('terminal', `🗑️ [projectManager] Removing session from context: ${sessionId}`);
623
+
624
+ // Find which project this session belongs to
625
+ for (const [projectId, context] of this.projectContexts.entries()) {
626
+ const sessionIndex = context.sessionIds.indexOf(sessionId);
627
+ if (sessionIndex !== -1) {
628
+ debug.log('terminal', `🗑️ [projectManager] Found session in project: ${projectId}`);
629
+ debug.log('terminal', `🗑️ [projectManager] Sessions before: ${context.sessionIds.join(', ')}`);
630
+
631
+ // Remove from sessionIds array
632
+ context.sessionIds.splice(sessionIndex, 1);
633
+
634
+ // Clear session-related data
635
+ context.sessionOutputs.delete(sessionId);
636
+ context.sessionOutputs.delete(`${sessionId}-metadata`);
637
+ context.sessionCommandHistories.delete(sessionId);
638
+
639
+ // Update active session if needed
640
+ if (context.activeSessionId === sessionId) {
641
+ context.activeSessionId = context.sessionIds[0] || null;
642
+ debug.log('terminal', `🗑️ [projectManager] Updated activeSessionId to: ${context.activeSessionId}`);
643
+ }
644
+
645
+ debug.log('terminal', `🗑️ [projectManager] Sessions after: ${context.sessionIds.join(', ')}`);
646
+
647
+ // Also remove from terminalSessionManager
648
+ terminalSessionManager.removeSession(sessionId);
649
+
650
+ // Persist changes
651
+ this.persistContexts();
652
+ debug.log('terminal', `🗑️ [projectManager] Context persisted`);
653
+ return;
654
+ }
655
+ }
656
+
657
+ debug.log('terminal', `🗑️ [projectManager] Session not found in any project context`);
658
+ }
659
+
660
+ /**
661
+ * Get terminal sessions for a specific project
662
+ */
663
+ getProjectSessions(projectId: string): string[] {
664
+ const context = this.projectContexts.get(projectId);
665
+ return context?.sessionIds || [];
666
+ }
667
+
668
+ /**
669
+ * Check if a project has running terminal sessions
670
+ */
671
+ hasRunningTerminals(projectId: string): boolean {
672
+ const context = this.projectContexts.get(projectId);
673
+ if (!context) return false;
674
+
675
+ return context.sessionIds.some(sessionId => {
676
+ const session = terminalSessionManager.getSession(sessionId);
677
+ return session?.isExecuting || false;
678
+ });
679
+ }
680
+
681
+ /**
682
+ * Get count of terminal sessions for a project
683
+ */
684
+ getTerminalCount(projectId: string): number {
685
+ const context = this.projectContexts.get(projectId);
686
+ return context?.sessionIds.length || 0;
687
+ }
688
+
689
+ /**
690
+ * Sync active stream info to persistence manager (in-memory)
691
+ * Project contexts are already managed in-memory via this.projectContexts Map
692
+ */
693
+ private persistContexts(): void {
694
+ // Sync active stream info to persistence manager
695
+ for (const [projectId, context] of this.projectContexts.entries()) {
696
+ for (const sessionId of context.sessionIds) {
697
+ const session = terminalSessionManager.getSession(sessionId);
698
+ if (session && session.isExecuting && session.streamId) {
699
+ terminalPersistenceManager.saveActiveStream(
700
+ sessionId, session.streamId, session.lastCommand || '', projectId
701
+ );
702
+ }
703
+ }
704
+ }
705
+ }
706
+
707
+ /**
708
+ * Load persisted contexts (no-op - contexts are in-memory only)
709
+ * Project contexts are created on demand via switchToProject()
710
+ */
711
+ private loadPersistedContexts(): void {
712
+ // No-op: contexts are managed in-memory and rebuilt on demand
713
+ }
714
+
715
+ /**
716
+ * Clear all data (used when clearing application data)
717
+ */
718
+ clearAll(): void {
719
+ this.projectContexts.clear();
720
+ this.currentProjectId = null;
721
+ }
722
+
723
+ /**
724
+ * Initialize the manager and setup collaborative WebSocket listeners
725
+ * Project contexts are created on demand via switchToProject()
726
+ */
727
+ initialize(): void {
728
+ if (typeof window === 'undefined') return;
729
+ this.setupCollaborativeListeners();
730
+ }
731
+
732
+ /**
733
+ * Setup WebSocket listeners for collaborative terminal tab management.
734
+ * When another user creates/closes a terminal tab, all users in the project
735
+ * see the tab list update automatically.
736
+ */
737
+ private setupCollaborativeListeners(): void {
738
+ import('$frontend/lib/utils/ws').then(({ default: ws }) => {
739
+ // Listen for terminal tab created by another user
740
+ ws.on('terminal:tab-created', (data: {
741
+ sessionId: string;
742
+ streamId: string;
743
+ pid: number;
744
+ currentDirectory: string;
745
+ cols: number;
746
+ rows: number;
747
+ }) => {
748
+ debug.log('terminal', `📥 Received terminal:tab-created: ${data.sessionId}`);
749
+
750
+ if (!this.currentProjectId) return;
751
+ const context = this.projectContexts.get(this.currentProjectId);
752
+ if (!context) return;
753
+
754
+ // Skip if this session already exists locally (we created it)
755
+ if (context.sessionIds.includes(data.sessionId)) {
756
+ debug.log('terminal', `✓ Terminal tab ${data.sessionId} already exists locally, skipping`);
757
+ return;
758
+ }
759
+
760
+ // Also skip if it exists in the store already
761
+ const existingStoreSession = terminalStore.getSession(data.sessionId);
762
+ if (existingStoreSession) {
763
+ debug.log('terminal', `✓ Terminal tab ${data.sessionId} already in store, skipping`);
764
+ return;
765
+ }
766
+
767
+ // Add the new tab from another user
768
+ debug.log('terminal', `➕ Adding remote terminal tab: ${data.sessionId}`);
769
+
770
+ // Extract terminal number from sessionId
771
+ const sessionParts = data.sessionId.split('-');
772
+ const terminalNumber = sessionParts[sessionParts.length - 1] || '1';
773
+
774
+ const terminalSession: TerminalSession = {
775
+ id: data.sessionId,
776
+ name: `Terminal ${terminalNumber}`,
777
+ directory: data.currentDirectory,
778
+ lines: [],
779
+ commandHistory: [],
780
+ isActive: false,
781
+ createdAt: new Date(),
782
+ lastUsedAt: new Date(),
783
+ shellType: 'Unknown',
784
+ terminalBuffer: undefined,
785
+ projectId: this.currentProjectId,
786
+ projectPath: context.projectPath
787
+ };
788
+
789
+ // Add to store and context
790
+ terminalStore.addSession(terminalSession);
791
+ context.sessionIds.push(data.sessionId);
792
+
793
+ // Create in session manager
794
+ terminalSessionManager.createSession(
795
+ data.sessionId,
796
+ this.currentProjectId,
797
+ context.projectPath,
798
+ data.currentDirectory
799
+ );
800
+
801
+ // Update nextSessionId to avoid conflicts
802
+ const match = data.sessionId.match(/terminal-(\d+)/);
803
+ if (match) {
804
+ terminalStore.updateNextSessionId(parseInt(match[1], 10) + 1);
805
+ }
806
+ });
807
+
808
+ // Listen for terminal tab closed by another user
809
+ ws.on('terminal:tab-closed', (data: { sessionId: string }) => {
810
+ debug.log('terminal', `📥 Received terminal:tab-closed: ${data.sessionId}`);
811
+
812
+ if (!this.currentProjectId) return;
813
+ const context = this.projectContexts.get(this.currentProjectId);
814
+ if (!context) return;
815
+
816
+ // Skip if session doesn't exist locally (already removed or not ours)
817
+ if (!context.sessionIds.includes(data.sessionId)) {
818
+ debug.log('terminal', `✓ Terminal tab ${data.sessionId} not in our context, skipping`);
819
+ return;
820
+ }
821
+
822
+ // Check if session still exists in store (might already be removed locally)
823
+ const existingSession = terminalStore.getSession(data.sessionId);
824
+ if (!existingSession) {
825
+ // Already removed from store, just clean up context
826
+ this.removeSessionFromContext(data.sessionId);
827
+ return;
828
+ }
829
+
830
+ // Remove the tab from another user
831
+ debug.log('terminal', `➖ Removing remote terminal tab: ${data.sessionId}`);
832
+
833
+ // Remove from context (without calling closeSession to avoid double-kill)
834
+ this.removeSessionFromContext(data.sessionId);
835
+
836
+ // Remove from store silently (don't kill PTY - already killed by the user who closed it)
837
+ terminalStore.removeSessionFromStore(data.sessionId);
838
+ });
839
+
840
+ debug.log('terminal', '✅ Terminal collaborative listeners registered');
841
+ });
842
+ }
843
+
844
+ /**
845
+ * Get visual indicator data for a project
846
+ */
847
+ getProjectIndicator(projectId: string): { hasTerminals: boolean; runningCount: number; totalCount: number } {
848
+ const context = this.projectContexts.get(projectId);
849
+
850
+ if (!context) {
851
+ return { hasTerminals: false, runningCount: 0, totalCount: 0 };
852
+ }
853
+
854
+ let runningCount = 0;
855
+ const countedTerminals = new Set<string>();
856
+
857
+ // First check sessions in terminalSessionManager (for current project)
858
+ for (const sessionId of context.sessionIds) {
859
+ const session = terminalSessionManager.getSession(sessionId);
860
+ if (session?.isExecuting) {
861
+ // Extract terminal number to track
862
+ const parts = sessionId.split('-');
863
+ const terminalNumber = parts[parts.length - 1];
864
+ countedTerminals.add(terminalNumber);
865
+ runningCount++;
866
+ }
867
+ }
868
+
869
+ // Check persistence manager for active streams (cross-project scenarios)
870
+ const allActiveStreams = terminalPersistenceManager.getAllActiveStreams();
871
+ const processedStreamIds = new Set<string>();
872
+
873
+ for (const streamInfo of allActiveStreams) {
874
+ if (processedStreamIds.has(streamInfo.streamId)) continue;
875
+
876
+ if (streamInfo.projectId === projectId) {
877
+ const streamParts = streamInfo.sessionId.split('-');
878
+ const terminalNumber = streamParts[streamParts.length - 1];
879
+
880
+ if (!countedTerminals.has(terminalNumber)) {
881
+ const hasMatchingTerminal = context.sessionIds.some(sid => {
882
+ const parts = sid.split('-');
883
+ return parts[parts.length - 1] === terminalNumber;
884
+ });
885
+
886
+ if (hasMatchingTerminal) {
887
+ countedTerminals.add(terminalNumber);
888
+ runningCount++;
889
+ }
890
+ }
891
+
892
+ processedStreamIds.add(streamInfo.streamId);
893
+ }
894
+ }
895
+
896
+ return {
897
+ hasTerminals: context.sessionIds.length > 0,
898
+ runningCount,
899
+ totalCount: context.sessionIds.length
900
+ };
901
+ }
902
+
903
+ /**
904
+ * Check and restore active streams for a project
905
+ */
906
+ private async checkAndRestoreActiveStreams(projectId: string): Promise<void> {
907
+ if (typeof window === 'undefined') return;
908
+
909
+ const context = this.projectContexts.get(projectId);
910
+ if (!context) return;
911
+
912
+ // Import services dynamically to avoid circular dependency
913
+ const { terminalPersistenceManager } = await import('./persistence.service');
914
+ const { backgroundTerminalService } = await import('./background');
915
+
916
+ // Get all active streams from persistence manager
917
+ const allActiveStreams = terminalPersistenceManager.getAllActiveStreams();
918
+
919
+ // Check if any active streams belong to this project
920
+ for (const streamInfo of allActiveStreams) {
921
+ if (streamInfo.projectId === projectId) {
922
+ // Extract terminal number from stream's original sessionId
923
+ const streamParts = streamInfo.sessionId.split('-');
924
+ const terminalNumber = streamParts[streamParts.length - 1];
925
+
926
+ // Find matching session by terminal number in current context
927
+ for (const sessionId of context.sessionIds) {
928
+ const sessionParts = sessionId.split('-');
929
+ const sessionTerminalNumber = sessionParts[sessionParts.length - 1];
930
+
931
+ if (sessionTerminalNumber === terminalNumber) {
932
+ const session = terminalSessionManager.getSession(sessionId);
933
+ if (session) {
934
+ // Restore execution state for this session
935
+ terminalSessionManager.startExecution(
936
+ sessionId,
937
+ streamInfo.command,
938
+ streamInfo.streamId
939
+ );
940
+ terminalStore.setExecutingState(sessionId, true);
941
+
942
+ // WebSocket akan otomatis handle reconnection
943
+ break;
944
+ }
945
+ }
946
+ }
947
+ }
948
+ }
949
+ }
950
+ }
951
+
952
+ // Export singleton instance
953
+ export const terminalProjectManager = new TerminalProjectManager();