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