@kabmo-labs/kabmo 0.7.0 → 0.9.0

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 (351) hide show
  1. package/README.md +7 -0
  2. package/dist/client/index.html +1 -1
  3. package/dist/client/index.js +403 -384
  4. package/dist/client/static/js/{index-0wvbca0b.js → index-bpptfkz2.js} +905 -875
  5. package/dist/client/static/js/index-k315p1e8.css +1 -0
  6. package/package.json +10 -1
  7. package/src/client/components/common/code-surface/documents.test.ts +120 -0
  8. package/src/client/components/common/code-surface/documents.ts +201 -0
  9. package/src/client/components/common/code-surface/extensions.test.ts +110 -0
  10. package/src/client/components/common/code-surface/extensions.ts +162 -0
  11. package/src/client/components/common/code-surface/handle.ts +102 -0
  12. package/src/client/components/common/code-surface/index.tsx +253 -314
  13. package/src/client/components/common/code-surface/keymap.test.ts +133 -0
  14. package/src/client/components/common/code-surface/keymap.ts +267 -0
  15. package/src/client/components/common/code-surface/marks.ts +74 -0
  16. package/src/client/components/common/code-surface/shiki.ts +234 -0
  17. package/src/client/components/common/code-surface/sync.test.ts +185 -0
  18. package/src/client/components/common/code-surface/sync.ts +58 -0
  19. package/src/client/components/common/code-surface/theme.test.ts +202 -0
  20. package/src/client/components/common/code-surface/theme.ts +111 -0
  21. package/src/client/components/common/editor-view-buttons/index.test.tsx +148 -0
  22. package/src/client/components/common/editor-view-buttons/index.tsx +109 -0
  23. package/src/client/components/common/header-controls/index.tsx +24 -1
  24. package/src/client/components/common/header-controls/shell-chrome.test.tsx +272 -0
  25. package/src/client/components/common/shell-update-button/Modal.tsx +163 -0
  26. package/src/client/components/common/shell-update-button/index.test.tsx +251 -0
  27. package/src/client/components/common/shell-update-button/index.tsx +125 -0
  28. package/src/client/components/common/toolbar-row/index.test.tsx +166 -0
  29. package/src/client/components/common/toolbar-row/index.tsx +189 -0
  30. package/src/client/components/common/workspace-options-menu/index.tsx +19 -1
  31. package/src/client/components/layout/desktop-layout/TopNavbar.tsx +5 -6
  32. package/src/client/components/layout/desktop-layout/WorkspacePanels.tsx +41 -78
  33. package/src/client/components/layout/desktop-layout/index.tsx +55 -69
  34. package/src/client/components/layout/desktop-layout/resizer/index.tsx +6 -3
  35. package/src/client/components/layout/session-sidebar/CategoryHeader.tsx +24 -0
  36. package/src/client/components/layout/session-sidebar/CategoryHeader.worktree.test.tsx +126 -0
  37. package/src/client/components/layout/session-sidebar/CategoryItem.tsx +4 -0
  38. package/src/client/components/layout/session-sidebar/Footer.tsx +8 -15
  39. package/src/client/components/layout/session-sidebar/SessionItem.tsx +12 -0
  40. package/src/client/components/layout/session-sidebar/SessionList.tsx +4 -0
  41. package/src/client/components/layout/session-sidebar/SessionsView.tsx +24 -0
  42. package/src/client/components/layout/session-sidebar/WorktreeBadge.test.ts +48 -0
  43. package/src/client/components/layout/session-sidebar/WorktreeBadge.tsx +55 -0
  44. package/src/client/components/mobile/LayoutWrapper.tsx +23 -16
  45. package/src/client/components/mobile/RightSidebar.tsx +21 -10
  46. package/src/client/components/mobile/SessionSidebar.tsx +8 -0
  47. package/src/client/components/mobile/header-titlebar.test.tsx +172 -0
  48. package/src/client/components/mobile/mobile-main-view/Header.tsx +10 -1
  49. package/src/client/components/mobile/mobile-right-sidebar/FullDiff.tsx +3 -0
  50. package/src/client/components/mobile/mobile-right-sidebar/FullEditor.tsx +63 -38
  51. package/src/client/components/mobile/mobile-right-sidebar/OutlineButton.test.tsx +137 -0
  52. package/src/client/components/mobile/mobile-right-sidebar/OutlineButton.tsx +14 -3
  53. package/src/client/components/mobile/mobile-session-sidebar/Footer.tsx +9 -21
  54. package/src/client/components/mobile/mobile-session-sidebar/Header.tsx +6 -2
  55. package/src/client/components/mobile/mobile-session-sidebar/Item.tsx +16 -0
  56. package/src/client/components/mobile/mobile-session-sidebar/List.tsx +4 -0
  57. package/src/client/components/mobile/mobile-session-sidebar/SessionRow.tsx +5 -0
  58. package/src/client/components/mobile/mobile-session-sidebar/WorktreeDialog.tsx +49 -0
  59. package/src/client/components/mobile/worktree-session.ts +51 -0
  60. package/src/client/components/settings/Modal.tsx +8 -0
  61. package/src/client/components/settings/categories/AppearanceSettings.tsx +3 -0
  62. package/src/client/components/settings/categories/ProjectSettings.tsx +22 -8
  63. package/src/client/components/settings/categories/appearance-settings/EditorThemeSection.test.tsx +167 -0
  64. package/src/client/components/settings/categories/appearance-settings/EditorThemeSection.tsx +184 -0
  65. package/src/client/components/settings/categories/project-settings/WorktreeSettings.tsx +146 -0
  66. package/src/client/components/settings/categories/project-settings/worktree-row.test.ts +83 -0
  67. package/src/client/components/settings/categories/project-settings/worktree-row.ts +47 -0
  68. package/src/client/components/workspace/chat-timeline/EmptyWorkspacePrompt.tsx +36 -55
  69. package/src/client/components/workspace/chat-timeline/PendingChatView.tsx +8 -0
  70. package/src/client/components/workspace/chat-timeline/SystemNotice.tsx +9 -4
  71. package/src/client/components/workspace/chat-timeline/TaskResultContent.tsx +9 -7
  72. package/src/client/components/workspace/chat-timeline/index.tsx +5 -0
  73. package/src/client/components/workspace/chat-timeline/target-picker/WorkspacePicker.test.tsx +132 -0
  74. package/src/client/components/workspace/chat-timeline/target-picker/WorkspacePicker.tsx +120 -0
  75. package/src/client/components/workspace/chat-timeline/target-picker/WorktreePicker.test.tsx +319 -0
  76. package/src/client/components/workspace/chat-timeline/target-picker/WorktreePicker.tsx +285 -0
  77. package/src/client/components/workspace/chat-timeline/tool-renderers/ask-panel/index.test.ts +2 -0
  78. package/src/client/components/workspace/chat-timeline/tool-renderers/hashline-patch/index.tsx +34 -29
  79. package/src/client/components/workspace/chat-timeline/tool-renderers/panels/AstEdit.tsx +11 -9
  80. package/src/client/components/workspace/chat-timeline/tool-renderers/panels/Bash.tsx +10 -7
  81. package/src/client/components/workspace/chat-timeline/tool-renderers/panels/Edit.tsx +25 -8
  82. package/src/client/components/workspace/chat-timeline/tool-renderers/panels/Eval.test.tsx +146 -0
  83. package/src/client/components/workspace/chat-timeline/tool-renderers/panels/Eval.tsx +32 -17
  84. package/src/client/components/workspace/chat-timeline/tool-renderers/panels/FileMatches.test.ts +114 -0
  85. package/src/client/components/workspace/chat-timeline/tool-renderers/panels/FileMatches.tsx +111 -0
  86. package/src/client/components/workspace/chat-timeline/tool-renderers/panels/Read.tsx +7 -7
  87. package/src/client/components/workspace/chat-timeline/tool-renderers/panels/SearchTool.tsx +9 -59
  88. package/src/client/components/workspace/chat-timeline/tool-renderers/shared/ArtifactBody.tsx +2 -7
  89. package/src/client/components/workspace/chat-timeline/tool-renderers/shared/DiffView.test.ts +27 -28
  90. package/src/client/components/workspace/chat-timeline/tool-renderers/shared/DiffView.tsx +39 -26
  91. package/src/client/components/workspace/chat-timeline/tool-renderers/shared/ExcerptCode.tsx +8 -8
  92. package/src/client/components/workspace/chat-timeline/tool-renderers/shared/HighlightedCode.tsx +35 -0
  93. package/src/client/components/workspace/chat-timeline/tool-renderers/shared/JsonCodeBlock.tsx +4 -4
  94. package/src/client/components/workspace/diff-panel/MergeDiffView.tsx +179 -0
  95. package/src/client/components/workspace/diff-panel/ThemedMergeDiffView.tsx +24 -0
  96. package/src/client/components/workspace/diff-panel/Toolbar.tsx +224 -186
  97. package/src/client/components/workspace/diff-panel/ToolbarSegments.tsx +98 -0
  98. package/src/client/components/workspace/diff-panel/index.tsx +76 -145
  99. package/src/client/components/workspace/diff-panel/merge.ts +315 -0
  100. package/src/client/components/workspace/editor/Body.tsx +4 -7
  101. package/src/client/components/workspace/editor/CommandPalette.tsx +2 -2
  102. package/src/client/components/workspace/editor/FileDocument.tsx +58 -63
  103. package/src/client/components/workspace/editor/FilePane.tsx +4 -7
  104. package/src/client/components/workspace/editor/FindWidget.tsx +7 -2
  105. package/src/client/components/workspace/editor/Toolbar.tsx +220 -123
  106. package/src/client/components/workspace/editor/index.tsx +7 -9
  107. package/src/client/components/workspace/file-explorer/ContextMenu.tsx +2 -1
  108. package/src/client/components/workspace/file-explorer/Modals.tsx +40 -12
  109. package/src/client/components/workspace/file-explorer/OutlineSection.test.tsx +18 -0
  110. package/src/client/components/workspace/file-explorer/OutlineSection.tsx +10 -3
  111. package/src/client/components/workspace/file-explorer/RootMenu.tsx +14 -5
  112. package/src/client/components/workspace/file-explorer/Toolbar.tsx +74 -0
  113. package/src/client/components/workspace/file-explorer/TreeItem.tsx +38 -3
  114. package/src/client/components/workspace/file-explorer/filter.ts +47 -0
  115. package/src/client/components/workspace/file-explorer/index.tsx +77 -64
  116. package/src/client/components/workspace/file-explorer/local-edit.test.ts +135 -0
  117. package/src/client/components/workspace/search-panel/Body.tsx +1 -1
  118. package/src/client/components/workspace/worktree-modal/index.test.tsx +118 -0
  119. package/src/client/components/workspace/worktree-modal/index.tsx +261 -0
  120. package/src/client/hooks/chat/omp/reconnect.test.ts +338 -0
  121. package/src/client/hooks/chat/omp/reconnect.ts +187 -0
  122. package/src/client/hooks/chat/omp/run-liveness.test.ts +71 -0
  123. package/src/client/hooks/chat/omp/run-liveness.ts +65 -0
  124. package/src/client/hooks/chat/omp/session-list.activity.test.ts +114 -0
  125. package/src/client/hooks/chat/omp/session-list.test-util.ts +164 -0
  126. package/src/client/hooks/chat/omp/session-list.test.ts +30 -150
  127. package/src/client/hooks/chat/omp/session-list.tsx +69 -9
  128. package/src/client/hooks/chat/timeline/generating.ts +19 -2
  129. package/src/client/hooks/chat/timeline/index.ts +27 -28
  130. package/src/client/hooks/chat/timeline/lost-run.test.ts +329 -0
  131. package/src/client/hooks/chat/timeline/lost-run.ts +177 -0
  132. package/src/client/hooks/chat/timeline/send-execute.test.ts +2 -0
  133. package/src/client/hooks/chat/timeline/send-steer.test.ts +2 -0
  134. package/src/client/hooks/chat/timeline/send.test.ts +2 -0
  135. package/src/client/hooks/chat/timeline/send.ts +11 -1
  136. package/src/client/hooks/chat/timeline/session-load.test-util.ts +205 -0
  137. package/src/client/hooks/chat/timeline/session-load.test.ts +61 -162
  138. package/src/client/hooks/chat/timeline/session-load.ts +69 -37
  139. package/src/client/hooks/chat/timeline/session-switch.test.ts +65 -0
  140. package/src/client/hooks/chat/timeline/session-switch.ts +63 -0
  141. package/src/client/hooks/chat/timeline/spawn-target.ts +52 -0
  142. package/src/client/hooks/chat/timeline/tool-open.test.ts +74 -0
  143. package/src/client/hooks/chat/timeline/tool-open.ts +52 -8
  144. package/src/client/hooks/code/deferred-highlight.test.ts +182 -0
  145. package/src/client/hooks/code/deferred-highlight.ts +129 -0
  146. package/src/client/hooks/editor/find-state.ts +1 -1
  147. package/src/client/hooks/editor/theme.test.ts +206 -0
  148. package/src/client/hooks/editor/theme.ts +82 -0
  149. package/src/client/hooks/editor/use-editor-drop.test.ts +101 -0
  150. package/src/client/hooks/editor/use-editor-drop.ts +67 -14
  151. package/src/client/hooks/editor/use-editor-find.ts +7 -7
  152. package/src/client/hooks/editor/use-outline-view.test.ts +182 -0
  153. package/src/client/hooks/editor/use-outline-view.ts +94 -7
  154. package/src/client/hooks/editor/use-scroll-restore.test.ts +55 -2
  155. package/src/client/hooks/editor/use-scroll-restore.ts +124 -4
  156. package/src/client/hooks/editor/use-search-reveal.ts +1 -1
  157. package/src/client/hooks/ui/long-press.test.ts +174 -0
  158. package/src/client/hooks/ui/long-press.ts +112 -0
  159. package/src/client/hooks/ui/settings-modal.ts +56 -0
  160. package/src/client/hooks/ui/updates/shell-download.test.ts +75 -0
  161. package/src/client/hooks/ui/updates/shell-download.ts +153 -28
  162. package/src/client/hooks/ui/updates/shell-offer.test.ts +213 -0
  163. package/src/client/hooks/ui/updates/shell-update.test.ts +140 -0
  164. package/src/client/hooks/ui/updates/shell-update.ts +119 -0
  165. package/src/client/hooks/workspace/diff-file.ts +29 -10
  166. package/src/client/hooks/workspace/diff-git-actions.ts +147 -0
  167. package/src/client/hooks/workspace/editor-view.ts +61 -0
  168. package/src/client/hooks/workspace/file-create.ts +41 -11
  169. package/src/client/hooks/workspace/file-listing.ts +56 -1
  170. package/src/client/hooks/workspace/file-tabs.test.ts +62 -5
  171. package/src/client/hooks/workspace/file-tabs.ts +24 -11
  172. package/src/client/hooks/workspace/file-tree-actions.ts +37 -5
  173. package/src/client/hooks/workspace/navbar-session-actions.ts +72 -0
  174. package/src/client/hooks/workspace/worktree/index.ts +165 -0
  175. package/src/client/lib/desktop/bridge.ts +45 -0
  176. package/src/client/lib/search-reveal.ts +1 -1
  177. package/src/client/main.tsx +5 -1
  178. package/src/client/tailwind.css +56 -8
  179. package/src/server/index.ts +7 -2
  180. package/src/server/lib/auth/guard.test.ts +28 -0
  181. package/src/server/lib/auth/guard.ts +10 -0
  182. package/src/server/lib/bundler/titlebar-css.test.ts +42 -1
  183. package/src/server/lib/db/schema.ts +4 -0
  184. package/src/server/lib/db/worktree-registry.server.ts +91 -0
  185. package/src/server/lib/fs/canonical.ts +52 -0
  186. package/src/server/lib/fs/relative-path.ts +48 -0
  187. package/src/server/lib/fs/root-worktree.test.ts +116 -0
  188. package/src/server/lib/fs/root.ts +39 -10
  189. package/src/server/lib/fs/worktree/carry.ts +134 -0
  190. package/src/server/lib/fs/worktree/create.test.ts +249 -0
  191. package/src/server/lib/fs/worktree/create.ts +262 -0
  192. package/src/server/lib/fs/worktree/errors.ts +43 -0
  193. package/src/server/lib/fs/worktree/list.test.ts +207 -0
  194. package/src/server/lib/fs/worktree/list.ts +142 -0
  195. package/src/server/lib/fs/worktree/location.test.ts +206 -0
  196. package/src/server/lib/fs/worktree/location.ts +114 -0
  197. package/src/server/lib/fs/worktree/project-id.ts +53 -0
  198. package/src/server/lib/fs/worktree/remove.test.ts +242 -0
  199. package/src/server/lib/fs/worktree/remove.ts +152 -0
  200. package/src/server/lib/fs/worktree/settings.server.ts +56 -0
  201. package/src/server/lib/fs/worktree/topology.ts +142 -0
  202. package/src/server/lib/lifecycle/claim-port.test.ts +156 -0
  203. package/src/server/lib/lifecycle/port-guard.test.ts +20 -164
  204. package/src/server/lib/lifecycle/port-guard.ts +39 -7
  205. package/src/server/lib/lifecycle/probe-health.test.ts +166 -0
  206. package/src/server/lib/lifecycle/probe.test.ts +77 -164
  207. package/src/server/lib/lifecycle/probe.ts +121 -13
  208. package/src/server/lib/omp/rpc/frame-fold-signals.test.ts +112 -0
  209. package/src/server/lib/omp/rpc/frame-fold.ts +10 -5
  210. package/src/server/lib/omp/session/sidebar-activity.server.test.ts +162 -0
  211. package/src/server/lib/omp/session/sidebar-activity.server.ts +70 -0
  212. package/src/server/lib/omp/session/sidebar-data.canonical.test.ts +155 -0
  213. package/src/server/lib/omp/session/sidebar-data.server.ts +28 -1
  214. package/src/server/lib/omp/session/telemetry/compute.ts +13 -1
  215. package/src/server/lib/omp/session/telemetry.test.ts +33 -0
  216. package/src/server/lib/panels/local-edits.server.ts +33 -0
  217. package/src/server/lib/panels/sync.fixture.test-util.ts +92 -0
  218. package/src/server/lib/panels/sync.local-edits.test.ts +150 -0
  219. package/src/server/lib/panels/sync.server.test.ts +125 -147
  220. package/src/server/lib/panels/sync.server.ts +153 -48
  221. package/src/server/lib/realtime/signals.server.ts +10 -0
  222. package/src/server/lib/realtime/topics/catalog.server.ts +4 -1
  223. package/src/server/lib/realtime/topics.server.ts +16 -0
  224. package/src/server/lib/updates/shell-assets.test.ts +114 -0
  225. package/src/server/lib/updates/shell-assets.ts +200 -0
  226. package/src/server/lib/updates/shell-cache.ts +105 -0
  227. package/src/server/lib/updates/shell-pages.test.ts +187 -0
  228. package/src/server/lib/updates/shell-pages.ts +157 -0
  229. package/src/server/lib/updates/shell-release-desktop.test.ts +166 -0
  230. package/src/server/lib/updates/shell-release.test.ts +59 -71
  231. package/src/server/lib/updates/shell-release.ts +178 -198
  232. package/src/server/routes/fs/action-create.test.ts +18 -13
  233. package/src/server/routes/fs/action-nested.test.ts +184 -0
  234. package/src/server/routes/fs/action.ts +47 -16
  235. package/src/server/routes/fs/git-merge.test.ts +11 -1
  236. package/src/server/routes/fs/read.test.ts +24 -7
  237. package/src/server/routes/fs/scope.test.ts +15 -10
  238. package/src/server/routes/fs/scope.ts +3 -14
  239. package/src/server/routes/index.ts +2 -0
  240. package/src/server/routes/updates/android.ts +6 -1
  241. package/src/server/routes/updates/desktop.ts +59 -0
  242. package/src/server/routes/updates/index.ts +2 -0
  243. package/src/server/routes/updates/ios.ts +5 -1
  244. package/src/server/routes/worktrees/create.ts +138 -0
  245. package/src/server/routes/worktrees/index.ts +25 -0
  246. package/src/server/routes/worktrees/list.ts +134 -0
  247. package/src/server/routes/worktrees/remove.ts +102 -0
  248. package/src/server/routes/worktrees/routes.test.ts +326 -0
  249. package/src/server/routes/worktrees/shared.ts +90 -0
  250. package/src/shared/lib/chat/timeline/omp-callbacks.test.ts +36 -0
  251. package/src/shared/lib/chat/timeline/omp-callbacks.ts +18 -0
  252. package/src/shared/lib/chat/timeline/run-signals.test.ts +2 -0
  253. package/src/shared/lib/code/editor/comment-syntax.test.ts +37 -0
  254. package/src/shared/lib/code/editor/comment-syntax.ts +55 -0
  255. package/src/shared/lib/code/editor/find-replace.test.ts +134 -0
  256. package/src/shared/lib/code/editor/{find.ts → find-replace.ts} +23 -107
  257. package/src/shared/lib/code/editor/{find.test.ts → find-scan.test.ts} +19 -73
  258. package/src/shared/lib/code/editor/find-scan.ts +177 -0
  259. package/src/shared/lib/code/editor/keymap.ts +3 -3
  260. package/src/shared/lib/code/editor/lines.test.ts +7 -55
  261. package/src/shared/lib/code/editor/lines.ts +8 -48
  262. package/src/shared/lib/code/editor/marks.test.ts +4 -5
  263. package/src/shared/lib/code/editor/marks.ts +13 -25
  264. package/src/shared/lib/code/editor/theme-loader.ts +97 -0
  265. package/src/shared/lib/code/editor/themes.generated.ts +156 -0
  266. package/src/shared/lib/code/editor/themes.test.ts +165 -0
  267. package/src/shared/lib/code/editor/themes.ts +165 -0
  268. package/src/shared/lib/code/highlight-memo.test.ts +103 -0
  269. package/src/shared/lib/code/highlight-memo.ts +149 -0
  270. package/src/shared/lib/code/highlighter-warmup.ts +112 -0
  271. package/src/shared/lib/code/highlighter.ts +3 -1
  272. package/src/shared/lib/code/idle.ts +37 -0
  273. package/src/shared/lib/code/outline/rows.test.ts +96 -0
  274. package/src/shared/lib/code/outline/rows.ts +94 -0
  275. package/src/shared/lib/code/syntax-highlight.test.ts +1 -79
  276. package/src/shared/lib/code/syntax-highlight.ts +2 -3
  277. package/src/shared/lib/fs/file-kind-classify.test.ts +25 -11
  278. package/src/shared/lib/fs/file-kind.ts +47 -13
  279. package/src/shared/lib/fs/file-tree.test.ts +183 -0
  280. package/src/shared/lib/fs/file-tree.ts +159 -0
  281. package/src/shared/lib/fs/file-url.test.ts +52 -0
  282. package/src/shared/lib/fs/file-url.ts +37 -0
  283. package/src/shared/lib/fs/patch-sides.test.ts +114 -0
  284. package/src/shared/lib/fs/patch-sides.ts +139 -0
  285. package/src/shared/lib/fs/paths.test.ts +24 -1
  286. package/src/shared/lib/fs/paths.ts +17 -0
  287. package/src/shared/lib/fs/worktree/settings-keys.ts +18 -0
  288. package/src/shared/lib/omp/session/prewarm.ts +7 -0
  289. package/src/shared/lib/realtime/protocol.ts +16 -0
  290. package/src/shared/lib/ui/toolbar-overflow.test.ts +83 -0
  291. package/src/shared/lib/ui/toolbar-overflow.ts +86 -0
  292. package/src/shared/lib/updates/shell-arch.ts +54 -0
  293. package/src/shared/lib/updates/shell-ua.ts +24 -4
  294. package/src/shared/lib/workspace/active-project.ts +23 -0
  295. package/src/shared/lib/workspace/editor-view-mode.test.ts +70 -0
  296. package/src/shared/lib/workspace/editor-view-mode.ts +48 -0
  297. package/src/shared/lib/workspace/panel-widths.test.ts +30 -0
  298. package/src/shared/lib/workspace/panel-widths.ts +25 -5
  299. package/src/shared/lib/workspace/session-state/cache.ts +159 -0
  300. package/src/shared/lib/workspace/session-state/harness.test-util.ts +55 -0
  301. package/src/shared/lib/workspace/session-state/merge.ts +100 -0
  302. package/src/shared/lib/workspace/session-state/store-load.test.ts +206 -0
  303. package/src/shared/lib/workspace/session-state/store.test.ts +17 -139
  304. package/src/shared/lib/workspace/session-state/store.ts +92 -130
  305. package/src/shared/lib/workspace/workspace-modules.test.ts +49 -1
  306. package/src/shared/lib/workspace/workspace-panel-widths.ts +124 -0
  307. package/src/shared/lib/workspace/worktree/branch-name.test.ts +95 -0
  308. package/src/shared/lib/workspace/worktree/branch-name.ts +115 -0
  309. package/src/shared/lib/workspace/worktree/types.ts +104 -0
  310. package/src/shared/types/fs.ts +10 -0
  311. package/src/shared/types/realtime.ts +29 -0
  312. package/src/shared/types/settings/state.ts +10 -0
  313. package/src/shared/types/workspace.ts +14 -0
  314. package/src/test-support/file-explorer-disk.test-util.ts +76 -0
  315. package/src/test-support/file-explorer.test-util.ts +71 -40
  316. package/src/test-support/loopback.ts +183 -0
  317. package/dist/client/static/js/index-nzyjf92m.css +0 -1
  318. package/src/client/components/common/code-editor/handler.test.ts +0 -274
  319. package/src/client/components/common/code-editor/handler.ts +0 -175
  320. package/src/client/components/common/code-editor/index.tsx +0 -314
  321. package/src/client/components/common/code-surface/measure.ts +0 -148
  322. package/src/client/components/workspace/diff-panel/SplitView.tsx +0 -209
  323. package/src/client/components/workspace/diff-panel/UnifiedView.tsx +0 -150
  324. package/src/client/hooks/editor/scroll-container.test.ts +0 -102
  325. package/src/client/hooks/editor/scroll-container.ts +0 -26
  326. package/src/client/hooks/editor/use-code-window.ts +0 -100
  327. package/src/client/hooks/editor/use-editor-history.test.ts +0 -216
  328. package/src/client/hooks/editor/use-editor-history.ts +0 -134
  329. package/src/client/hooks/editor/use-occurrences.test.ts +0 -236
  330. package/src/client/hooks/editor/use-occurrences.ts +0 -98
  331. package/src/shared/lib/code/editor/commands.test.ts +0 -155
  332. package/src/shared/lib/code/editor/commands.ts +0 -260
  333. package/src/shared/lib/code/editor/edits.test.ts +0 -100
  334. package/src/shared/lib/code/editor/edits.ts +0 -122
  335. package/src/shared/lib/code/editor/history.test.ts +0 -118
  336. package/src/shared/lib/code/editor/history.ts +0 -101
  337. package/src/shared/lib/code/editor/layers.test.ts +0 -77
  338. package/src/shared/lib/code/editor/layers.ts +0 -67
  339. package/src/shared/lib/code/editor/line-edits.test.ts +0 -131
  340. package/src/shared/lib/code/editor/line-edits.ts +0 -159
  341. package/src/shared/lib/code/editor/occurrences.test.ts +0 -117
  342. package/src/shared/lib/code/editor/occurrences.ts +0 -111
  343. package/src/shared/lib/code/editor/reset-css.test.ts +0 -35
  344. package/src/shared/lib/code/editor/reset-css.ts +0 -32
  345. package/src/shared/lib/code/editor/text-commands.test.ts +0 -178
  346. package/src/shared/lib/code/editor/text-commands.ts +0 -154
  347. package/src/shared/lib/code/lazy-window.test.ts +0 -161
  348. package/src/shared/lib/code/lazy-window.ts +0 -156
  349. package/src/shared/lib/code/windowed-highlight.ts +0 -114
  350. package/src/shared/lib/fs/diff-parser.test.ts +0 -116
  351. package/src/shared/lib/fs/diff-parser.ts +0 -213
@@ -0,0 +1,326 @@
1
+ /**
2
+ * @license
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ /**
7
+ * The `/api/worktrees` routes, end to end over a real repository.
8
+ *
9
+ * What a unit test on the modules cannot show, and what these cases are for:
10
+ *
11
+ * - the LIST payload carries every fact a row draws, from one read;
12
+ * - a create answers with the worktree it made, and the next list shows it —
13
+ * the topology cache invalidation is what makes "next list" true;
14
+ * - a DELETE takes the path in its BODY (a path has separators) and answers the
15
+ * branch verdict rather than a bare success;
16
+ * - MOCK refuses, because a demo install has no repository to make one in.
17
+ */
18
+
19
+ import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, test } from 'bun:test';
20
+ import { mkdirSync, mkdtempSync, writeFileSync } from 'node:fs';
21
+ import { tmpdir } from 'node:os';
22
+ import { join } from 'node:path';
23
+ import { getDb } from '@/server/db.server';
24
+ import { loader as listLoader } from '@/server/routes/worktrees/list';
25
+ import { action as createAction, validateAction } from '@/server/routes/worktrees/create';
26
+ import { action as removeAction } from '@/server/routes/worktrees/remove';
27
+ import { invalidateWorktreeTopology } from '@/server/lib/fs/worktree/topology';
28
+ import { clearSessionFileCaches } from '@/server/lib/omp/session/files';
29
+ import { isolateDb, releaseDb } from '@/test-support/isolated-db';
30
+ import { removeDir } from '@/test-support/remove-dir';
31
+
32
+ const GIT_ENV = {
33
+ ...Bun.env,
34
+ GIT_EDITOR: 'true',
35
+ GIT_TERMINAL_PROMPT: '0',
36
+ GIT_AUTHOR_NAME: 'Test',
37
+ GIT_AUTHOR_EMAIL: 'test@example.invalid',
38
+ GIT_COMMITTER_NAME: 'Test',
39
+ GIT_COMMITTER_EMAIL: 'test@example.invalid',
40
+ };
41
+
42
+ function git(cwd: string, ...args: string[]): string {
43
+ return Bun.spawnSync({ cmd: ['git', ...args], cwd, env: GIT_ENV }).stdout.toString();
44
+ }
45
+
46
+ let workRoot: string;
47
+ let repo: string;
48
+ /** Where the session files live, so a count can be observed without omp. */
49
+ let sessionsRoot: string;
50
+ const saved = {
51
+ MOCK: Bun.env.MOCK,
52
+ SYNC_WORKSPACE: Bun.env.SYNC_WORKSPACE,
53
+ agentDir: Bun.env.PI_CODING_AGENT_DIR,
54
+ xdg: Bun.env.XDG_DATA_HOME,
55
+ dataDir: Bun.env.KABMO_DATA_DIR,
56
+ };
57
+
58
+ /**
59
+ * Call a route module the way the adapter does — with `params`, which every
60
+ * Remix-shaped handler declares even when its path has no parameter.
61
+ */
62
+ async function call(
63
+ handler: (args: { request: Request; params: Record<string, string> }) => unknown,
64
+ url: string,
65
+ init?: RequestInit,
66
+ ): Promise<{ status: number; body: Record<string, unknown> }> {
67
+ const response = (await handler({ request: new Request(url, init), params: {} })) as Response;
68
+ return { status: response.status, body: (await response.json()) as Record<string, unknown> };
69
+ }
70
+
71
+ /** Put an env var back, or unset it when the file started without one. */
72
+ function restoreEnv(name: string, value: string | undefined): void {
73
+ if (value === undefined) delete Bun.env[name];
74
+ else Bun.env[name] = value;
75
+ }
76
+
77
+ /** One session file whose cwd is `cwd`, as the agent would record it. */
78
+ function writeSession(id: string, cwd: string): void {
79
+ const lines = [
80
+ JSON.stringify({ type: 'session', id, cwd, timestamp: '2026-10-23T00:00:00.000Z' }),
81
+ JSON.stringify({
82
+ type: 'message',
83
+ id: `${id}-m1`,
84
+ parentId: null,
85
+ timestamp: '2026-10-23T00:00:01.000Z',
86
+ message: { role: 'user', content: 'where am I' },
87
+ }),
88
+ ];
89
+ writeFileSync(join(sessionsRoot, 'project', `${id}.jsonl`), `${lines.join('\n')}\n`);
90
+ }
91
+
92
+ /** The `worktrees` array of a list payload, narrowed once. */
93
+ function worktreesOf(body: Record<string, unknown>): Array<Record<string, unknown>> {
94
+ const list = body.worktrees;
95
+ return Array.isArray(list) ? (list as Array<Record<string, unknown>>) : [];
96
+ }
97
+
98
+ const base = (): string => `http://localhost/api/worktrees?root=${encodeURIComponent(repo)}`;
99
+
100
+ beforeAll(() => {
101
+ workRoot = mkdtempSync(join(tmpdir(), 'kabmo-worktree-routes-'));
102
+ repo = join(workRoot, 'repo');
103
+ mkdirSync(repo, { recursive: true });
104
+ writeFileSync(join(repo, '.gitignore'), 'node_modules\n');
105
+ mkdirSync(join(repo, 'node_modules', 'dep'), { recursive: true });
106
+ writeFileSync(join(repo, 'node_modules', 'dep', 'index.js'), 'module.exports = {};\n');
107
+ writeFileSync(join(repo, 'readme.md'), 'hello\n');
108
+ git(repo, 'init', '-q', '-b', 'main', '.');
109
+ git(repo, 'add', '-A');
110
+ git(repo, 'commit', '-q', '-m', 'base');
111
+
112
+ // A sessions root of this file's own: the worktree count reads it, and
113
+ // without the override it would scan the developer's real `~/.omp/agent`.
114
+ const agent = join(workRoot, 'agent');
115
+ sessionsRoot = join(agent, 'sessions');
116
+ mkdirSync(join(sessionsRoot, 'project'), { recursive: true });
117
+ Bun.env.PI_CODING_AGENT_DIR = agent;
118
+ // XDG would redirect the sessions dir to $XDG_DATA_HOME/omp when it exists.
119
+ delete Bun.env.XDG_DATA_HOME;
120
+ // A worktree's default location is `<data>/worktrees/<projectId>/<name>`, so
121
+ // without this the suite writes into the developer's own `~/.kabmo`.
122
+ Bun.env.KABMO_DATA_DIR = join(workRoot, 'data');
123
+ });
124
+
125
+ beforeEach(async () => {
126
+ await isolateDb();
127
+ Bun.env.MOCK = 'false';
128
+ Bun.env.SYNC_WORKSPACE = 'false';
129
+ const db = await getDb();
130
+ await db.run('INSERT INTO workspace_folders (name, project_path) VALUES (?, ?)', ['proj', repo]);
131
+ });
132
+
133
+ afterEach(async () => {
134
+ await invalidateWorktreeTopology();
135
+ // The session-file caches are keyed on `globalThis` and this file writes
136
+ // session files, so a case that leaves one behind would be counted by the next.
137
+ clearSessionFileCaches();
138
+ await releaseDb();
139
+ });
140
+
141
+ afterAll(() => {
142
+ removeDir(workRoot);
143
+ // Explicitly mapped: the record's keys are not the env names, and an
144
+ // `Object.entries` restore would silently leave `PI_CODING_AGENT_DIR` pointed
145
+ // at a directory this file just deleted.
146
+ restoreEnv('MOCK', saved.MOCK);
147
+ restoreEnv('SYNC_WORKSPACE', saved.SYNC_WORKSPACE);
148
+ restoreEnv('PI_CODING_AGENT_DIR', saved.agentDir);
149
+ restoreEnv('XDG_DATA_HOME', saved.xdg);
150
+ restoreEnv('KABMO_DATA_DIR', saved.dataDir);
151
+ });
152
+
153
+ describe('GET /api/worktrees', () => {
154
+ test('lists the checkout with the facts a row draws', async () => {
155
+ const { status, body } = await call(listLoader, base());
156
+ expect(status).toBe(200);
157
+ expect(body.isRepo).toBe(true);
158
+ expect(String(body.base)).toContain('worktrees');
159
+ const primary = worktreesOf(body)[0] as Record<string, unknown>;
160
+ expect(primary.isPrimary).toBe(true);
161
+ expect(primary.branch).toBe('main');
162
+ expect(String(primary.head).length).toBeGreaterThan(0);
163
+ expect(primary.dirty).toBe(0);
164
+ });
165
+
166
+ test('a folder that is not a repository answers isRepo:false, not a failure', async () => {
167
+ // A notes folder bound as a workspace: it can never hold a worktree, and
168
+ // the composer must read that as "no control here" rather than as a broken
169
+ // feature. The route used to 500 here, because the identity read throws for
170
+ // a directory with no root commit.
171
+ const plain = join(workRoot, 'plain-notes');
172
+ mkdirSync(plain, { recursive: true });
173
+ writeFileSync(join(plain, 'todo.md'), '- buy milk\n');
174
+ const db = await getDb();
175
+ await db.run('INSERT INTO workspace_folders (name, project_path) VALUES (?, ?)', ['plain', plain]);
176
+
177
+ const { status, body } = await call(listLoader, `http://localhost/api/worktrees?root=${encodeURIComponent(plain)}`);
178
+ expect(status).toBe(200);
179
+ expect(body.isRepo).toBe(false);
180
+ expect(worktreesOf(body)).toEqual([]);
181
+ });
182
+
183
+ test('counts a session running in a worktree that lives OUTSIDE the repository', async () => {
184
+ const created = await call(createAction, base(), {
185
+ method: 'POST',
186
+ headers: { 'content-type': 'application/json' },
187
+ body: JSON.stringify({ mode: 'new', branch: 'wt/route-counted' }),
188
+ });
189
+ const path = String((created.body.worktree as Record<string, unknown>).path);
190
+ // The default location is under the data directory, not inside the checkout
191
+ // — which is the case a `primaryRoot` prefix test counted zero for.
192
+ expect(path.startsWith(repo)).toBe(false);
193
+
194
+ writeSession('route-counted-session', path);
195
+
196
+ const listed = await call(listLoader, base());
197
+ const row = worktreesOf(listed.body).find((entry) => entry.path === path);
198
+ // The number the delete dialog warns with: a chat is running here.
199
+ expect(row?.sessions).toBe(1);
200
+ });
201
+
202
+ test('a folder with no project binding is refused, not answered for the app root', async () => {
203
+ const { status, body } = await call(listLoader, 'http://localhost/api/worktrees');
204
+ expect(status).toBe(400);
205
+ expect(body.code).toBe('not_a_repo');
206
+ });
207
+
208
+ test('MOCK refuses — a demo install has no repository to read', async () => {
209
+ Bun.env.MOCK = 'true';
210
+ const { status, body } = await call(listLoader, base());
211
+ expect(status).toBe(409);
212
+ expect(body.code).toBe('mock_mode');
213
+ });
214
+ });
215
+
216
+ describe('POST /api/worktrees', () => {
217
+ test('creates one, carries the ignored build folder, and the next list shows it', async () => {
218
+ const created = await call(createAction, base(), {
219
+ method: 'POST',
220
+ headers: { 'content-type': 'application/json' },
221
+ body: JSON.stringify({ mode: 'new', branch: 'wt/route-created' }),
222
+ });
223
+
224
+ expect(created.status).toBe(200);
225
+ const worktree = created.body.worktree as Record<string, unknown>;
226
+ expect(worktree.branch).toBe('wt/route-created');
227
+ // The carry is the one capability git does not provide; without it the new
228
+ // checkout cannot run the project.
229
+ const carried = worktree.carried as { copied: string[] };
230
+ expect(carried.copied).toContain('node_modules');
231
+
232
+ const listed = await call(listLoader, base());
233
+ const row = worktreesOf(listed.body).find((entry) => entry.branch === 'wt/route-created');
234
+ expect(row).toBeDefined();
235
+ expect(row?.isPrimary).toBe(false);
236
+ expect(row?.startRef).toBe('HEAD');
237
+ });
238
+
239
+ test('a refusal answers its code and field, so the dialog can mark the input', async () => {
240
+ const refused = await call(createAction, base(), {
241
+ method: 'POST',
242
+ headers: { 'content-type': 'application/json' },
243
+ body: JSON.stringify({ mode: 'existing', branch: 'main' }),
244
+ });
245
+ expect(refused.status).toBe(400);
246
+ expect(refused.body.code).toBe('branch_in_use');
247
+ expect(refused.body.field).toBe('branch');
248
+ });
249
+
250
+ test('validate answers the same refusal without creating anything', async () => {
251
+ const result = await call(validateAction, base(), {
252
+ method: 'POST',
253
+ headers: { 'content-type': 'application/json' },
254
+ body: JSON.stringify({ mode: 'new', branch: 'has space' }),
255
+ });
256
+ expect(result.status).toBe(400);
257
+ expect(result.body.ok).toBe(false);
258
+ const failures = result.body.failures as Array<{ code: string }>;
259
+ expect(failures[0]?.code).toBe('invalid_name');
260
+
261
+ // Nothing was created: the fixture repository is shared by this file, so the
262
+ // assertion names the branch rather than a count (a count would also pin how
263
+ // many earlier tests happened to leave behind).
264
+ const listed = await call(listLoader, base());
265
+ expect(worktreesOf(listed.body).some((entry) => entry.branch === 'has-space')).toBe(false);
266
+ expect(worktreesOf(listed.body).some((entry) => entry.branch === 'has space')).toBe(false);
267
+ });
268
+ });
269
+
270
+ describe('DELETE /api/worktrees', () => {
271
+ test('removes the worktree and its branch, and the list no longer shows it', async () => {
272
+ const created = await call(createAction, base(), {
273
+ method: 'POST',
274
+ headers: { 'content-type': 'application/json' },
275
+ body: JSON.stringify({ mode: 'new', branch: 'wt/route-remove' }),
276
+ });
277
+ const path = String((created.body.worktree as Record<string, unknown>).path);
278
+
279
+ const removed = await call(removeAction, base(), {
280
+ method: 'DELETE',
281
+ headers: { 'content-type': 'application/json' },
282
+ body: JSON.stringify({ path, deleteBranch: true }),
283
+ });
284
+ expect(removed.status).toBe(200);
285
+ expect(removed.body.removed).toBe(true);
286
+ expect(removed.body.branchDeleted).toBe(true);
287
+
288
+ const listed = await call(listLoader, base());
289
+ expect(worktreesOf(listed.body).some((entry) => entry.path === path)).toBe(false);
290
+ });
291
+
292
+ test('a missing path is refused by name, not by a crash', async () => {
293
+ const refused = await call(removeAction, base(), {
294
+ method: 'DELETE',
295
+ headers: { 'content-type': 'application/json' },
296
+ body: JSON.stringify({}),
297
+ });
298
+ expect(refused.status).toBe(400);
299
+ expect(refused.body.code).toBe('invalid_name');
300
+ });
301
+
302
+ test('a dirty worktree is refused with 409 and its count, so a caller can tell a retry from a bad request', async () => {
303
+ const created = await call(createAction, base(), {
304
+ method: 'POST',
305
+ headers: { 'content-type': 'application/json' },
306
+ body: JSON.stringify({ mode: 'new', branch: 'wt/route-dirty' }),
307
+ });
308
+ const path = String((created.body.worktree as Record<string, unknown>).path);
309
+ writeFileSync(join(path, 'scratch.txt'), 'not committed\n');
310
+
311
+ const refused = await call(removeAction, base(), {
312
+ method: 'DELETE',
313
+ headers: { 'content-type': 'application/json' },
314
+ body: JSON.stringify({ path, deleteBranch: true }),
315
+ });
316
+ // 409: the request is well-formed and the tree is the obstacle — the same
317
+ // distinction `mock_mode` draws. A 400 here reads as "fix your request".
318
+ expect(refused.status).toBe(409);
319
+ expect(refused.body.code).toBe('dirty');
320
+ expect(Number(refused.body.count)).toBeGreaterThan(0);
321
+
322
+ // A refusal is not a partial removal.
323
+ const listed = await call(listLoader, base());
324
+ expect(worktreesOf(listed.body).some((entry) => entry.path === path)).toBe(true);
325
+ });
326
+ });
@@ -0,0 +1,90 @@
1
+ /**
2
+ * @license
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ /**
7
+ * The worktree routes' shared plumbing: resolve the project, refuse in MOCK, and
8
+ * turn a `WorktreeError` into the JSON envelope the client already understands.
9
+ *
10
+ * Root resolution goes through `resolveRoot`, the security boundary every fs
11
+ * route uses — a client-supplied `root` is only honoured when it is the app
12
+ * root, a registered `project_path`, or (this feature) a worktree of one.
13
+ */
14
+
15
+ import { json } from '@/server/lib/remix-compat';
16
+ import { isMockMode } from '@/server/mock.server';
17
+ import { getDb } from '@/server/db.server';
18
+ import { resolveRoot } from '@/server/lib/fs/root';
19
+ import { asWorktreeFailure } from '@/server/lib/fs/worktree/errors';
20
+ import type { WorktreeFailure } from '@/shared/lib/workspace/worktree/types';
21
+
22
+ /** A registered workspace's `project_path`, or null when the folder is unbound. */
23
+ export async function projectPathForFolder(folderId: number | string | null): Promise<string | null> {
24
+ if (folderId === null || folderId === '') return null;
25
+ try {
26
+ const db = await getDb();
27
+ const row = await db.get<{ project_path: string | null }>(
28
+ 'SELECT project_path FROM workspace_folders WHERE id = ?',
29
+ [Number(folderId)],
30
+ );
31
+ return row?.project_path ?? null;
32
+ } catch {
33
+ return null;
34
+ }
35
+ }
36
+
37
+ /** The project a request names: its own `root`, else the folder's binding. */
38
+ export async function resolveWorktreeProject(
39
+ rawRoot: string | null | undefined,
40
+ folderId: string | null,
41
+ ): Promise<{ root: string | null; error?: Response }> {
42
+ const fromFolder = await projectPathForFolder(folderId);
43
+ const root = await resolveRoot(rawRoot || fromFolder, '');
44
+ if (!root) {
45
+ return {
46
+ root: null,
47
+ error: json(
48
+ {
49
+ error: 'Pick a workspace bound to a Git repository first.',
50
+ code: 'not_a_repo',
51
+ },
52
+ { status: 400 },
53
+ ),
54
+ };
55
+ }
56
+ return { root };
57
+ }
58
+
59
+ /**
60
+ * A worktree mutation in demo mode is refused rather than simulated: MOCK has no
61
+ * repository on disk, so a "successful" create would promise a folder that does
62
+ * not exist.
63
+ */
64
+ export function mockRefusal(): Response {
65
+ return json(
66
+ { error: 'Worktrees need a real Git repository; Kabmo is running in demo mode.', code: 'mock_mode' },
67
+ { status: 409 },
68
+ );
69
+ }
70
+
71
+ /**
72
+ * A refusal's HTTP status, from what the refusal MEANS.
73
+ *
74
+ * `dirty` is a conflict with the tree's current state rather than a malformed
75
+ * request — the same distinction `session_not_running` and `mock_mode` draw with
76
+ * 409 — and a caller that retries after committing needs to tell the two apart
77
+ * without parsing the message.
78
+ */
79
+ const REFUSAL_STATUS: Partial<Record<WorktreeFailure['code'], number>> = {
80
+ dirty: 409,
81
+ };
82
+
83
+ export function worktreeFailureResponse(error: unknown, status = 400): Response {
84
+ const failure = asWorktreeFailure(error);
85
+ return json({ error: failure.message, ...failure }, { status: REFUSAL_STATUS[failure.code] ?? status });
86
+ }
87
+
88
+ export function isMock(): boolean {
89
+ return isMockMode();
90
+ }
@@ -55,6 +55,8 @@ function makeCallbacks(overrides: Partial<OmpAgentCallbacksDeps> = {}): OmpAgent
55
55
  appSettings: {},
56
56
  enqueueExtensionDialog: () => {},
57
57
  withdrawExtensionDialog: () => {},
58
+ reloadMessages: () => {},
59
+ needsReloadRef: { current: false },
58
60
  ...overrides,
59
61
  };
60
62
  return createOmpAgentCallbacks(deps);
@@ -133,6 +135,40 @@ describe('createOmpAgentCallbacks run boundaries', () => {
133
135
  expect(verbs).toEqual(['Thinking']);
134
136
  });
135
137
 
138
+ test('a run that lost frames re-reads its transcript when it settles', () => {
139
+ // A reconnect while the run was STILL streaming cannot reload: its rows are
140
+ // the stream's, and replacing them mid-turn is the lagging-snapshot bug.
141
+ // But the segments that finished during the outage have no other way back —
142
+ // their `message_end` went into the dead socket, and `agent_end` only
143
+ // materializes turns that stopped abnormally. So the loss is remembered and
144
+ // the read happens at the settle.
145
+ const reads: number[] = [];
146
+ const needsReloadRef = { current: true };
147
+ const callbacks = recordingCallbacks({
148
+ reloadMessages: () => { reads.push(1); },
149
+ needsReloadRef,
150
+ });
151
+
152
+ callbacks.onAgentEnd?.({});
153
+
154
+ expect(reads.length).toBe(1);
155
+ // Consumed: a later run end must not re-read a transcript nothing lost.
156
+ expect(needsReloadRef.current).toBe(false);
157
+ callbacks.onAgentEnd?.({});
158
+ expect(reads.length).toBe(1);
159
+ });
160
+
161
+ test('an ordinary run end does NOT re-read the transcript', () => {
162
+ // The read is a full parse of the session JSONL; a run that never lost a
163
+ // frame must not pay it.
164
+ const reads: number[] = [];
165
+ const callbacks = recordingCallbacks({ reloadMessages: () => { reads.push(1); } });
166
+
167
+ callbacks.onAgentEnd?.({});
168
+
169
+ expect(reads).toEqual([]);
170
+ });
171
+
136
172
  test('a real omp frame sequence drives the boundary once per run', () => {
137
173
  const callbacks = recordingCallbacks();
138
174
  const deps = makeFoldDeps(callbacks);
@@ -51,6 +51,14 @@ export interface OmpAgentCallbacksDeps {
51
51
  enqueueExtensionDialog: (request: ExtensionUiDialogRequest) => void;
52
52
  /** Drop a queued request omp withdrew (`method: 'cancel'`). */
53
53
  withdrawExtensionDialog: (targetId: string) => void;
54
+ /** Re-read the committed transcript (see `lost-run.ts`). Used at `agent_end`
55
+ * when frames were lost mid-run: the segments that finished during the
56
+ * outage have no other way back. */
57
+ reloadMessages: () => void;
58
+ /** Set when a run lost frames while STILL STREAMING. Its earlier segments can
59
+ * only be recovered by a full read, which is safe only once the run is over —
60
+ * so the read happens here, at the settle. */
61
+ needsReloadRef: { current: boolean };
54
62
  }
55
63
 
56
64
  export function createOmpAgentCallbacks(deps: OmpAgentCallbacksDeps): OmpAgentCallbacks {
@@ -70,6 +78,8 @@ export function createOmpAgentCallbacks(deps: OmpAgentCallbacksDeps): OmpAgentCa
70
78
  appSettings,
71
79
  enqueueExtensionDialog,
72
80
  withdrawExtensionDialog,
81
+ reloadMessages,
82
+ needsReloadRef,
73
83
  } = deps;
74
84
 
75
85
  bindStreamingCoalescer(setLocalMessages, () => scrollToBottom('smooth'));
@@ -245,6 +255,14 @@ export function createOmpAgentCallbacks(deps: OmpAgentCallbacksDeps): OmpAgentCa
245
255
  // placeholder until the realtime scan surfaces the row.
246
256
  const sid = adoptedSessionIdRef.current ?? sessionIdRef.current;
247
257
  if (sid) refreshSessionMeta(sid);
258
+ // A run that lost frames while still streaming: its earlier segments (a
259
+ // tool card, a thinking block, an earlier answer) have no other way back.
260
+ // Read now — the run is over, so a full transcript read cannot clobber a
261
+ // live row — and `reloadMessages` retries while omp has not flushed.
262
+ if (needsReloadRef.current) {
263
+ needsReloadRef.current = false;
264
+ reloadMessages();
265
+ }
248
266
  },
249
267
  onModelChanged: () => {
250
268
  // omp's model_changed frame carries no payload — re-read the session
@@ -56,6 +56,8 @@ function deps(overrides: Partial<OmpAgentCallbacksDeps> = {}): OmpAgentCallbacks
56
56
  appSettings: {},
57
57
  enqueueExtensionDialog: () => {},
58
58
  withdrawExtensionDialog: () => {},
59
+ reloadMessages: () => {},
60
+ needsReloadRef: { current: false },
59
61
  ...overrides,
60
62
  };
61
63
  }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * @license
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ /**
7
+ * The comment tokens `toggleComment` reads out of `languageData`.
8
+ *
9
+ * A wrong token is not a cosmetic bug: `//` inserted into YAML is a syntax
10
+ * error, and a `//` inserted into CSS comments out nothing while breaking the
11
+ * rule it was added to. The three shapes are asserted, plus the default that
12
+ * every C-family grammar relies on.
13
+ */
14
+
15
+ import { describe, expect, test } from 'bun:test';
16
+
17
+ import { commentSyntaxFor } from '@/shared/lib/code/editor/comment-syntax';
18
+
19
+ describe('commentSyntaxFor', () => {
20
+ test('defaults to a line comment for the C-family', () => {
21
+ expect(commentSyntaxFor('typescript')).toEqual({ line: '//' });
22
+ expect(commentSyntaxFor('rust')).toEqual({ line: '//' });
23
+ expect(commentSyntaxFor('not-a-language')).toEqual({ line: '//' });
24
+ });
25
+
26
+ test('names the languages whose line comment is not `//`', () => {
27
+ expect(commentSyntaxFor('python')).toEqual({ line: '#' });
28
+ expect(commentSyntaxFor('yaml')).toEqual({ line: '#' });
29
+ expect(commentSyntaxFor('sql')).toEqual({ line: '--' });
30
+ });
31
+
32
+ test('wraps the languages that have only block comments', () => {
33
+ expect(commentSyntaxFor('css')).toEqual({ block: ['/*', '*/'] });
34
+ expect(commentSyntaxFor('html')).toEqual({ block: ['<!--', '-->'] });
35
+ expect(commentSyntaxFor('markdown')).toEqual({ block: ['<!--', '-->'] });
36
+ });
37
+ });
@@ -0,0 +1,55 @@
1
+ /**
2
+ * @license
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ /**
7
+ * How each language writes a line comment.
8
+ *
9
+ * The only thing the editor still needs from the old text-command layer:
10
+ * CodeMirror's `toggleComment` reads its tokens out of the `languageData` facet
11
+ * and needs no syntax tree, so this table IS the whole of the comment feature.
12
+ * Everything else the old module did (line moves, duplicates, deletes, inserts,
13
+ * indent, occurrences, edit replication) is a CM6 command now.
14
+ *
15
+ * The default is `//`, which is right for every C-family grammar. The exceptions
16
+ * are the ones a wrong token would visibly break — `#` in YAML is a syntax
17
+ * error, not a comment — and a language that has ONLY block comments gets the
18
+ * wrapper, because a `//` inserted into CSS comments out nothing and breaks the
19
+ * rule it was added to.
20
+ */
21
+
22
+ /** A line-comment syntax: a prefix per line, or a wrapper for block-only languages. */
23
+ export type CommentSyntax = { line: string } | { block: readonly [string, string] };
24
+
25
+ const COMMENT_SYNTAX: Record<string, CommentSyntax> = {
26
+ python: { line: '#' },
27
+ bash: { line: '#' },
28
+ yaml: { line: '#' },
29
+ toml: { line: '#' },
30
+ ruby: { line: '#' },
31
+ perl: { line: '#' },
32
+ r: { line: '#' },
33
+ makefile: { line: '#' },
34
+ dockerfile: { line: '#' },
35
+ ini: { line: '#' },
36
+ properties: { line: '#' },
37
+ elixir: { line: '#' },
38
+ graphql: { line: '#' },
39
+ nix: { line: '#' },
40
+ sql: { line: '--' },
41
+ lua: { line: '--' },
42
+ haskell: { line: '--' },
43
+ css: { block: ['/*', '*/'] },
44
+ scss: { block: ['/*', '*/'] },
45
+ less: { block: ['/*', '*/'] },
46
+ sass: { block: ['/*', '*/'] },
47
+ stylus: { block: ['/*', '*/'] },
48
+ html: { block: ['<!--', '-->'] },
49
+ xml: { block: ['<!--', '-->'] },
50
+ markdown: { block: ['<!--', '-->'] },
51
+ };
52
+
53
+ export function commentSyntaxFor(language: string): CommentSyntax {
54
+ return COMMENT_SYNTAX[language] ?? { line: '//' };
55
+ }