@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,317 @@
1
+ /**
2
+ * Projects Store
3
+ * All project-related state and functions
4
+ *
5
+ * State persistence: Server-side via user:save-state / user:restore-state
6
+ * No localStorage usage - server is single source of truth
7
+ */
8
+
9
+ import ws from '$frontend/lib/utils/ws';
10
+ import type { Project } from '$shared/types/database/schema';
11
+
12
+ import { debug } from '$shared/utils/logger';
13
+ interface ProjectState {
14
+ projects: Project[];
15
+ currentProject: Project | null;
16
+ recentProjects: Project[];
17
+ isLoading: boolean;
18
+ error: string | null;
19
+ }
20
+
21
+ // Project state using Svelte 5 runes
22
+ export const projectState = $state<ProjectState>({
23
+ projects: [],
24
+ currentProject: null,
25
+ recentProjects: [],
26
+ isLoading: false,
27
+ error: null
28
+ });
29
+
30
+ // ========================================
31
+ // DERIVED VALUES
32
+ // ========================================
33
+
34
+ export function hasProjects() {
35
+ return projectState.projects.length > 0;
36
+ }
37
+
38
+ export function currentProjectName() {
39
+ return projectState.currentProject?.name || '';
40
+ }
41
+
42
+ export function currentProjectPath() {
43
+ return projectState.currentProject?.path || '';
44
+ }
45
+
46
+ // ========================================
47
+ // PROJECT MANAGEMENT
48
+ // ========================================
49
+
50
+ export async function setCurrentProject(project: Project | null) {
51
+ // Only clear session if we're actually switching to a different project
52
+ const { sessionState, setCurrentSession } = await import('./sessions.svelte');
53
+ const { appState } = await import('./app.svelte');
54
+
55
+ // Sync project context with WebSocket (for room-based broadcasting)
56
+ // IMPORTANT: Must await to ensure server has context before other operations
57
+ await ws.setProject(project?.id ?? null);
58
+
59
+ // Check if we're switching to a different project
60
+ const currentProjectId = projectState.currentProject?.id;
61
+ const newProjectId = project?.id;
62
+
63
+ // Handle project status tracking and terminal context switching
64
+ if (typeof window !== 'undefined') {
65
+ try {
66
+ // Import services dynamically to avoid circular dependency
67
+ const { projectStatusService } = await import('$frontend/lib/services/project');
68
+ const { terminalProjectManager } = await import('$frontend/lib/services/terminal');
69
+
70
+ // Stop tracking previous project if switching
71
+ if (currentProjectId && currentProjectId !== newProjectId) {
72
+ await projectStatusService.stopTracking();
73
+
74
+ // Reset loading state when switching projects
75
+ appState.isLoading = false;
76
+ }
77
+
78
+ // Start tracking new project
79
+ if (project?.id) {
80
+ await projectStatusService.startTracking(project.id);
81
+ // Switch terminal context to the new project
82
+ await terminalProjectManager.switchToProject(project.id, project.path);
83
+ }
84
+ } catch (error) {
85
+ debug.error('project', 'Error updating project tracking:', error);
86
+ }
87
+ }
88
+
89
+ // If switching to a different project, handle session transition
90
+ if (currentProjectId !== newProjectId) {
91
+ // Set restoring flag FIRST - prevents reactive effects from syncing stale state
92
+ // to the new project during transition
93
+ if (project) {
94
+ appState.isRestoring = true;
95
+ }
96
+
97
+ // Clear edit mode state from previous project (server retains per-project state)
98
+ const { onProjectLeave, onProjectEnter } = await import('$frontend/lib/stores/ui/edit-mode.svelte');
99
+ onProjectLeave();
100
+
101
+ // Clear current session when switching projects
102
+ await setCurrentSession(null);
103
+
104
+ // If we have a new project, try to restore or create a session for it
105
+ if (project) {
106
+ try {
107
+ // Import session management functions
108
+ const { createSession, getSessionsForProject, reloadSessionsForProject } = await import('./sessions.svelte');
109
+
110
+ // Reload all sessions for this project from server
111
+ // (local state may only have sessions from the previous project)
112
+ await reloadSessionsForProject();
113
+
114
+ // Check if there's an existing session for this project
115
+ const existingSessions = getSessionsForProject(project.id);
116
+ const activeSessions = existingSessions
117
+ .filter(s => !s.ended_at)
118
+ .sort((a, b) => new Date(b.started_at).getTime() - new Date(a.started_at).getTime());
119
+ const activeSession = activeSessions[0] || null;
120
+
121
+ if (activeSession) {
122
+ // Restore the most recent active session for this project
123
+ debug.log('project', 'Restoring existing session for project:', activeSession.id);
124
+ await setCurrentSession(activeSession);
125
+ } else {
126
+ // Create a new session for this project
127
+ debug.log('project', 'Creating new session for project:', project.id);
128
+ const newSession = await createSession(project.id, 'Chat Session', false);
129
+ if (newSession) {
130
+ await setCurrentSession(newSession);
131
+ }
132
+ }
133
+
134
+ // Restore edit mode from server for the new project
135
+ // (ws.setProject already completed, so server returns correct project's state)
136
+ await onProjectEnter();
137
+
138
+ // Set projectState BEFORE clearing isRestoring
139
+ // This ensures reactive effects that fire on projectState change
140
+ // still see isRestoring=true and don't sync stale state to server
141
+ projectState.currentProject = project;
142
+ } finally {
143
+ // Clear restoring flag after project state is fully set
144
+ appState.isRestoring = false;
145
+ }
146
+ }
147
+ } else {
148
+ projectState.currentProject = project;
149
+ }
150
+
151
+ if (project) {
152
+ // Save current project ID to server
153
+ ws.http('user:save-state', { key: 'currentProjectId', value: project.id }).catch(err => {
154
+ debug.error('project', 'Error saving project state to server:', err);
155
+ });
156
+
157
+ try {
158
+ // Update last opened time via WebSocket
159
+ const updatedProject = await ws.http('projects:get', { id: project.id });
160
+
161
+ if (updatedProject) {
162
+ // Update in local state
163
+ const index = projectState.projects.findIndex(p => p.id === project.id);
164
+ if (index !== -1) {
165
+ projectState.projects[index] = updatedProject;
166
+ }
167
+ projectState.currentProject = updatedProject;
168
+ }
169
+ } catch (error) {
170
+ debug.error('project', 'Error updating project last opened:', error);
171
+ }
172
+ } else {
173
+ // Save null to server
174
+ ws.http('user:save-state', { key: 'currentProjectId', value: null }).catch(err => {
175
+ debug.error('project', 'Error clearing project state on server:', err);
176
+ });
177
+ debug.log('project', 'Project cleared');
178
+ }
179
+ }
180
+
181
+ export function addProject(project: Project) {
182
+ projectState.projects.push(project);
183
+ updateRecentProjects();
184
+ }
185
+
186
+ export function updateProject(updatedProject: Project) {
187
+ const index = projectState.projects.findIndex(p => p.id === updatedProject.id);
188
+ if (index !== -1) {
189
+ projectState.projects[index] = updatedProject;
190
+
191
+ // Update current project if it's the same
192
+ if (projectState.currentProject?.id === updatedProject.id) {
193
+ projectState.currentProject = updatedProject;
194
+ }
195
+ }
196
+ updateRecentProjects();
197
+ }
198
+
199
+ export function removeProject(projectId: string) {
200
+ projectState.projects = projectState.projects.filter(p => p.id !== projectId);
201
+
202
+ // Clear current project if it's being removed
203
+ if (projectState.currentProject?.id === projectId) {
204
+ projectState.currentProject = null;
205
+ }
206
+ updateRecentProjects();
207
+ }
208
+
209
+ // ========================================
210
+ // DATA LOADING
211
+ // ========================================
212
+
213
+ /**
214
+ * Load projects from server.
215
+ * Optionally restores current project from server-provided projectId.
216
+ */
217
+ export async function loadProjects(restoreProjectId?: string | null) {
218
+ projectState.isLoading = true;
219
+ projectState.error = null;
220
+
221
+ try {
222
+ // Load projects via WebSocket
223
+ const projects = await ws.http('projects:list');
224
+
225
+ if (projects) {
226
+ projectState.projects = projects;
227
+ updateRecentProjects();
228
+
229
+ // Restore current project from server-provided ID if not already set
230
+ if (!projectState.currentProject && restoreProjectId) {
231
+ const existingProject = projects.find(p => p.id === restoreProjectId);
232
+ if (existingProject) {
233
+ debug.log('project', 'Restoring project from server state:', existingProject.id);
234
+
235
+ // Sync project context with WebSocket FIRST (before setting reactive state)
236
+ // This ensures server knows the project before any reactive effects fire
237
+ await ws.setProject(existingProject.id);
238
+ debug.log('project', 'WebSocket context synced for restored project:', existingProject.id);
239
+
240
+ projectState.currentProject = existingProject;
241
+
242
+ // Start tracking the restored project
243
+ if (typeof window !== 'undefined') {
244
+ try {
245
+ const { projectStatusService } = await import('$frontend/lib/services/project');
246
+ await projectStatusService.startTracking(existingProject.id);
247
+ } catch (error) {
248
+ debug.error('project', 'Error starting tracking for restored project:', error);
249
+ }
250
+ }
251
+ } else {
252
+ debug.log('project', 'Saved project no longer exists on server');
253
+ }
254
+ } else if (!projectState.currentProject) {
255
+ debug.log('project', 'No saved project to restore');
256
+ } else {
257
+ debug.log('project', 'Project already set, skipping restoration');
258
+ }
259
+ } else {
260
+ projectState.error = 'Failed to load projects';
261
+ }
262
+ } catch (error) {
263
+ debug.error('project', 'Error loading projects:', error);
264
+ projectState.error = `Error loading projects: ${error}`;
265
+ } finally {
266
+ projectState.isLoading = false;
267
+ }
268
+ }
269
+
270
+ export async function createProject(projectData: Omit<Project, 'id' | 'created_at'>) {
271
+ try {
272
+ // Create project via WebSocket
273
+ const project = await ws.http('projects:create', {
274
+ name: projectData.name,
275
+ path: projectData.path
276
+ });
277
+
278
+ if (project) {
279
+ addProject(project);
280
+ return project;
281
+ } else {
282
+ projectState.error = 'Failed to create project';
283
+ return null;
284
+ }
285
+ } catch (error) {
286
+ debug.error('project', 'Error creating project:', error);
287
+ projectState.error = `Error creating project: ${error}`;
288
+ return null;
289
+ }
290
+ }
291
+
292
+ // ========================================
293
+ // UTILITY FUNCTIONS
294
+ // ========================================
295
+
296
+ function updateRecentProjects() {
297
+ projectState.recentProjects = projectState.projects
298
+ .filter(p => p.last_opened_at)
299
+ .sort((a, b) => new Date(b.last_opened_at!).getTime() - new Date(a.last_opened_at!).getTime())
300
+ .slice(0, 5);
301
+ }
302
+
303
+ export function searchProjects(query: string): Project[] {
304
+ const lowercaseQuery = query.toLowerCase();
305
+ return projectState.projects.filter(project =>
306
+ project.name.toLowerCase().includes(lowercaseQuery) ||
307
+ project.path.toLowerCase().includes(lowercaseQuery)
308
+ );
309
+ }
310
+
311
+ // ========================================
312
+ // INITIALIZATION
313
+ // ========================================
314
+
315
+ export async function initializeProjects(restoreProjectId?: string | null) {
316
+ await loadProjects(restoreProjectId);
317
+ }
@@ -0,0 +1,383 @@
1
+ /**
2
+ * Sessions Store
3
+ * All session and message-related state and functions
4
+ *
5
+ * State persistence: Server-side only
6
+ * Session is determined by the shared session per project (sessions:get-shared)
7
+ * No localStorage usage - server is single source of truth
8
+ */
9
+
10
+ import type { ChatSession, SDKMessageFormatter } from '$shared/types/database/schema';
11
+ import type { SDKMessage } from '$shared/types/messaging';
12
+ import { buildMetadataFromTransport } from '$shared/utils/message-formatter';
13
+ import ws from '$frontend/lib/utils/ws';
14
+ import { projectState } from './projects.svelte';
15
+ import { setupEditModeListener, restoreEditMode } from '$frontend/lib/stores/ui/edit-mode.svelte';
16
+ import { debug } from '$shared/utils/logger';
17
+
18
+ interface SessionState {
19
+ sessions: ChatSession[];
20
+ currentSession: ChatSession | null;
21
+ messages: SDKMessageFormatter[];
22
+ isLoading: boolean;
23
+ error: string | null;
24
+ }
25
+
26
+ // Session state using Svelte 5 runes
27
+ export const sessionState = $state<SessionState>({
28
+ sessions: [],
29
+ currentSession: null,
30
+ messages: [],
31
+ isLoading: false,
32
+ error: null
33
+ });
34
+
35
+ // ========================================
36
+ // DERIVED VALUES
37
+ // ========================================
38
+
39
+ export function hasSessions() {
40
+ return sessionState.sessions.length > 0;
41
+ }
42
+
43
+ export function currentSessionId() {
44
+ return sessionState.currentSession?.id || '';
45
+ }
46
+
47
+ export function messageCount() {
48
+ return sessionState.messages.length;
49
+ }
50
+
51
+ // ========================================
52
+ // SESSION MANAGEMENT
53
+ // ========================================
54
+
55
+ export async function setCurrentSession(session: ChatSession | null, skipLoadMessages: boolean = false) {
56
+ const previousSessionId = sessionState.currentSession?.id;
57
+ sessionState.currentSession = session;
58
+
59
+ // Leave previous chat session room
60
+ if (previousSessionId && previousSessionId !== session?.id) {
61
+ ws.emit('chat:leave-session', { chatSessionId: previousSessionId });
62
+ }
63
+
64
+ if (session) {
65
+ // Join new chat session room (receive session-scoped events)
66
+ ws.emit('chat:join-session', { chatSessionId: session.id });
67
+
68
+ // Persist current session to server for refresh restore (server is single source of truth)
69
+ ws.emit('sessions:set-current', { sessionId: session.id });
70
+
71
+ // Refresh session data from server to get latest engine/model
72
+ // (may have been set by another user's stream or not yet synced)
73
+ try {
74
+ const response = await ws.http('sessions:get', { id: session.id });
75
+ if (response?.session) {
76
+ const freshSession = response.session as ChatSession;
77
+ // Update local session list with fresh data
78
+ const idx = sessionState.sessions.findIndex(s => s.id === session.id);
79
+ if (idx !== -1) {
80
+ sessionState.sessions[idx] = freshSession;
81
+ }
82
+ sessionState.currentSession = freshSession;
83
+ }
84
+ } catch {
85
+ // Ignore - proceed with existing session data
86
+ }
87
+
88
+ // Load messages for this session (skip if we're restoring and already have messages)
89
+ if (!skipLoadMessages) {
90
+ await loadMessagesForSession(session.id);
91
+ }
92
+
93
+ debug.log('session', 'Session set:', session.id);
94
+ } else {
95
+ // Clear messages when no session
96
+ sessionState.messages = [];
97
+ debug.log('session', 'Session cleared');
98
+ }
99
+ }
100
+
101
+ export async function createSession(projectId: string, title: string, forceNew: boolean = false): Promise<ChatSession | null> {
102
+ try {
103
+ // For shared sessions, we want to get or create a shared session for the project
104
+ const session = await ws.http('sessions:get-shared', { forceNew });
105
+
106
+ // When forceNew is true, mark all other sessions for this project as ended in frontend state
107
+ // This ensures switching projects and back won't restore the old session
108
+ if (forceNew) {
109
+ const now = new Date().toISOString();
110
+ for (let i = 0; i < sessionState.sessions.length; i++) {
111
+ const s = sessionState.sessions[i];
112
+ if (s.project_id === projectId && s.id !== session.id && !s.ended_at) {
113
+ sessionState.sessions[i] = { ...s, ended_at: now };
114
+ }
115
+ }
116
+ }
117
+
118
+ // Check if session already exists in state
119
+ const existingIndex = sessionState.sessions.findIndex(s => s.id === session.id);
120
+ if (existingIndex === -1) {
121
+ sessionState.sessions.push(session);
122
+ } else {
123
+ // Update existing session
124
+ sessionState.sessions[existingIndex] = session;
125
+ }
126
+ return session;
127
+ } catch (error) {
128
+ debug.error('session', 'Error creating session:', error);
129
+ return null;
130
+ }
131
+ }
132
+
133
+ export async function createNewChatSession(projectId: string): Promise<ChatSession | null> {
134
+ // Force create a new session (ends current shared session if exists)
135
+ return createSession(projectId, 'New Chat Session', true);
136
+ }
137
+
138
+ export function updateSession(updatedSession: ChatSession) {
139
+ const index = sessionState.sessions.findIndex((s) => s.id === updatedSession.id);
140
+ if (index !== -1) {
141
+ sessionState.sessions[index] = updatedSession;
142
+
143
+ // Update current session if it's the same
144
+ if (sessionState.currentSession?.id === updatedSession.id) {
145
+ sessionState.currentSession = updatedSession;
146
+ }
147
+ }
148
+ }
149
+
150
+ export function removeSession(sessionId: string) {
151
+ // Use splice for granular Svelte 5 reactivity — only the removed element
152
+ // triggers DOM updates, preventing full-list re-render flicker.
153
+ const index = sessionState.sessions.findIndex((s) => s.id === sessionId);
154
+ if (index !== -1) {
155
+ sessionState.sessions.splice(index, 1);
156
+ }
157
+
158
+ // Clear current session if it's the one being removed
159
+ if (sessionState.currentSession?.id === sessionId) {
160
+ sessionState.currentSession = null;
161
+ sessionState.messages = [];
162
+ }
163
+ }
164
+
165
+ export async function endSession(sessionId: string) {
166
+ const session = sessionState.sessions.find((s) => s.id === sessionId);
167
+ if (session && !session.ended_at) {
168
+ try {
169
+ const updatedSession = await ws.http('sessions:update', {
170
+ id: sessionId,
171
+ end_session: true
172
+ });
173
+
174
+ updateSession(updatedSession);
175
+ } catch (error) {
176
+ debug.error('session', 'Error ending session:', error);
177
+ }
178
+ }
179
+ }
180
+
181
+ // ========================================
182
+ // MESSAGE MANAGEMENT
183
+ // ========================================
184
+
185
+ export function addMessage(message: SDKMessage | SDKMessageFormatter): void {
186
+ // Convert to SDKMessageFormatter if needed
187
+ const messageFormatter: SDKMessageFormatter = {
188
+ ...message,
189
+ metadata: ('metadata' in message && message.metadata)
190
+ ? message.metadata
191
+ : buildMetadataFromTransport({ timestamp: new Date().toISOString() })
192
+ };
193
+ // Update unified store - single source of truth
194
+ sessionState.messages.push(messageFormatter);
195
+ }
196
+
197
+ export function updateMessages(messages: SDKMessageFormatter[]) {
198
+ sessionState.messages = messages;
199
+ }
200
+
201
+ export function clearMessages() {
202
+ sessionState.messages = [];
203
+ }
204
+
205
+ export async function loadMessagesForSession(sessionId: string) {
206
+ try {
207
+ const response = await ws.http('messages:list', { session_id: sessionId });
208
+
209
+ if (response && Array.isArray(response)) {
210
+ // Messages from server already have correct SDKMessageFormatter shape with metadata
211
+ sessionState.messages = response as SDKMessageFormatter[];
212
+ } else {
213
+ sessionState.messages = [];
214
+ }
215
+ } catch (error) {
216
+ debug.error('session', 'Error loading messages:', error);
217
+ sessionState.messages = [];
218
+ }
219
+ }
220
+
221
+ // ========================================
222
+ // DATA LOADING
223
+ // ========================================
224
+
225
+ export async function loadSessions() {
226
+ sessionState.isLoading = true;
227
+ sessionState.error = null;
228
+
229
+ try {
230
+ const response = await ws.http('sessions:list');
231
+
232
+ if (response) {
233
+ const { sessions, currentSessionId } = response;
234
+ sessionState.sessions = sessions;
235
+
236
+ // Auto-restore: find the active session for the current project
237
+ if (!sessionState.currentSession) {
238
+ const currentProject = projectState.currentProject;
239
+ if (currentProject) {
240
+ const projectSessions = sessions.filter(
241
+ (s: ChatSession) => s.project_id === currentProject.id && !s.ended_at
242
+ );
243
+
244
+ if (projectSessions.length > 0) {
245
+ // Try server-saved session first (preserves user's session across refresh)
246
+ let targetSession: ChatSession | null = null;
247
+ if (currentSessionId) {
248
+ targetSession = projectSessions.find(
249
+ (s: ChatSession) => s.id === currentSessionId
250
+ ) || null;
251
+ }
252
+
253
+ // Fall back to most recent active session
254
+ if (!targetSession) {
255
+ targetSession = projectSessions.sort((a: ChatSession, b: ChatSession) =>
256
+ new Date(b.started_at).getTime() - new Date(a.started_at).getTime()
257
+ )[0];
258
+ }
259
+
260
+ debug.log('session', 'Auto-restoring session for project:', targetSession.id);
261
+ // Load messages BEFORE setting session to avoid race condition:
262
+ // Setting session triggers $effect → catchupActiveStream (async),
263
+ // but loadMessagesForSession replaces sessionState.messages entirely,
264
+ // wiping out any stream_event injected by catchup.
265
+ await loadMessagesForSession(targetSession.id);
266
+ sessionState.currentSession = targetSession;
267
+ // Join chat session room so we receive session-scoped events
268
+ // (stream, input sync, edit mode, model sync).
269
+ // Critical after refresh — without it, connection misses all events.
270
+ ws.emit('chat:join-session', { chatSessionId: targetSession.id });
271
+ }
272
+ }
273
+ }
274
+ } else {
275
+ sessionState.error = 'Failed to load sessions';
276
+ }
277
+ } catch (error) {
278
+ debug.error('session', 'Error loading sessions:', error);
279
+ sessionState.error = `Error loading sessions: ${error}`;
280
+ } finally {
281
+ sessionState.isLoading = false;
282
+ }
283
+ }
284
+
285
+ // ========================================
286
+ // UTILITY FUNCTIONS
287
+ // ========================================
288
+
289
+ export function getSessionsForProject(projectId: string): ChatSession[] {
290
+ return sessionState.sessions.filter((session) => session.project_id === projectId);
291
+ }
292
+
293
+ export function getRecentSessions(limit: number = 10): ChatSession[] {
294
+ return sessionState.sessions
295
+ .sort((a, b) => new Date(b.started_at).getTime() - new Date(a.started_at).getTime())
296
+ .slice(0, limit);
297
+ }
298
+
299
+ /**
300
+ * Reload sessions for the current project from the server.
301
+ * Called when the user switches projects so session list stays in sync.
302
+ */
303
+ export async function reloadSessionsForProject() {
304
+ try {
305
+ const response = await ws.http('sessions:list');
306
+ if (response) {
307
+ const { sessions } = response;
308
+ // Merge: keep sessions from other projects, replace sessions for current project
309
+ const currentProjectId = projectState.currentProject?.id;
310
+ if (currentProjectId) {
311
+ const otherProjectSessions = sessionState.sessions.filter(
312
+ (s: ChatSession) => s.project_id !== currentProjectId
313
+ );
314
+ sessionState.sessions = [...otherProjectSessions, ...sessions];
315
+ } else {
316
+ sessionState.sessions = sessions;
317
+ }
318
+ }
319
+ } catch (error) {
320
+ debug.error('session', 'Error reloading sessions:', error);
321
+ }
322
+ }
323
+
324
+ // ========================================
325
+ // COLLABORATIVE LISTENERS
326
+ // ========================================
327
+
328
+ /**
329
+ * Setup WebSocket listeners for collaborative session management.
330
+ * When another user creates a new chat session, all users in the project
331
+ * automatically switch to the new shared session.
332
+ */
333
+ function setupCollaborativeListeners() {
334
+ // Listen for new session available notifications from other users.
335
+ // Does NOT auto-switch — adds session to list and shows notification.
336
+ ws.on('sessions:session-available', async (data: { session: ChatSession }) => {
337
+ debug.log('session', 'New session available in project:', data.session.id);
338
+
339
+ const { session } = data;
340
+
341
+ // Add to sessions list (don't switch)
342
+ const existingIndex = sessionState.sessions.findIndex(s => s.id === session.id);
343
+ if (existingIndex === -1) {
344
+ sessionState.sessions.push(session);
345
+ } else {
346
+ sessionState.sessions[existingIndex] = session;
347
+ }
348
+ });
349
+
350
+ // Listen for session deletion broadcasts from other users
351
+ ws.on('sessions:session-deleted', (data: { sessionId: string; projectId: string }) => {
352
+ debug.log('session', 'Session deleted by another user:', data.sessionId);
353
+ removeSession(data.sessionId);
354
+ });
355
+
356
+ // Listen for messages-changed broadcasts (undo/redo/edit by another user)
357
+ ws.on('chat:messages-changed', async (data: { sessionId: string; reason: string; timestamp: string }) => {
358
+ debug.log('session', `Messages changed (${data.reason}) for session: ${data.sessionId}`);
359
+
360
+ // Reload messages if we're viewing the affected session
361
+ if (sessionState.currentSession?.id === data.sessionId) {
362
+ await loadMessagesForSession(data.sessionId);
363
+ }
364
+ });
365
+ }
366
+
367
+ // ========================================
368
+ // INITIALIZATION
369
+ // ========================================
370
+
371
+ export async function initializeSessions() {
372
+ // Setup sync listeners first (no await needed)
373
+ setupCollaborativeListeners();
374
+ setupEditModeListener();
375
+
376
+ // Load sessions and restore edit mode in parallel
377
+ // Both only need WS project context (already set by initializeProjects)
378
+ await Promise.all([
379
+ loadSessions(),
380
+ restoreEditMode()
381
+ ]);
382
+ debug.log('session', 'Sessions initialized');
383
+ }