@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
@@ -7,14 +7,11 @@
7
7
  * The Files panel's test harness: a fake disk, a real realtime server, and the
8
8
  * click helpers the panel's specs drive it with.
9
9
  *
10
- * Extracted because TWO spec files exercise the same panel — the listing
11
- * behaviours and the root context menu — and each was carrying its own copy of
12
- * the mount, the HTTP stub and the click helpers. A second copy is how the two
13
- * files drift into testing different setups.
14
- *
15
- * The HTTP stub is a real directory model: specs mutate `disk` between reads,
16
- * which is what makes a refresh assertion fail on a panel that answered from
17
- * its children cache instead of from the server.
10
+ * Extracted because several spec files exercise the same panel, and each was
11
+ * carrying its own copy of the mount, the HTTP stub and the helpers. The HTTP
12
+ * stub is a real directory model (see `file-explorer-disk.test-util`): specs
13
+ * mutate `disk` between reads, which is what makes a refresh assertion fail on a
14
+ * panel that answered from its children cache instead of from the server.
18
15
  */
19
16
 
20
17
  import { Window } from 'happy-dom';
@@ -29,14 +26,13 @@ import { startRealtimeTestServer, type RealtimeTestServer } from '@/test-support
29
26
  import { installDomGlobals, pristineWebSocket, restoreDomGlobals } from '@/test-support/pristine-globals';
30
27
  import type { TopicResolver } from '@/server/lib/realtime/hub.server';
31
28
  import type { GitStatusPayload } from '@/server/lib/fs/git-status-read';
29
+ import { diskHas, entriesOf, moveOnDisk, writeToDisk, type Disk } from '@/test-support/file-explorer-disk.test-util';
30
+
31
+ export type { Disk } from '@/test-support/file-explorer-disk.test-util';
32
32
 
33
33
  export const ROOT = '/ws';
34
34
  export const SESSION = 'new-files-panel';
35
35
  const TOPIC = fsTopic(`${ROOT}\u0000.`);
36
-
37
- /** The fake disk: `path -> child -> (null for a folder, 'file' for a file)`. */
38
- export type Disk = Record<string, Record<string, null | 'file'> | undefined>;
39
-
40
36
  const GIT_STATUS: GitStatusPayload = { changes: [], branch: 'main', branches: [], remoteBranches: [] };
41
37
 
42
38
  export interface FileExplorerHarness {
@@ -46,9 +42,9 @@ export interface FileExplorerHarness {
46
42
  dirReads: string[];
47
43
  /** Paths whose NEXT read answers a non-JSON 500 (a transport failure). */
48
44
  failNext: Set<string>;
49
- /** Every create the panel posted, as its form fields. */
45
+ /** Every create / rename / transfer the panel posted, as its form fields. */
50
46
  createRequests: Array<Record<string, string>>;
51
- /** Every duplicate/paste the panel posted, as its form fields. */
47
+ renameRequests: Array<Record<string, string>>;
52
48
  transferRequests: Array<Record<string, string>>;
53
49
  /** Files the panel asked the editor to open, in order. */
54
50
  openedFiles: Array<{ path: string; name: string }>;
@@ -66,44 +62,36 @@ export interface FileExplorerHarness {
66
62
  panelBackground: () => HTMLElement;
67
63
  /** Right-click the background, leaving its menu open. */
68
64
  openMenuOnBackground: () => Promise<void>;
69
- /** Right-click the background and click the menu's New File row. */
65
+ /** Right-click the background and click its New File / New Folder row. */
70
66
  openRootNewFile: () => Promise<void>;
67
+ openRootNewFolder: () => Promise<void>;
71
68
  /** Type a name into the open create dialog and submit it. */
72
69
  submitNewFile: (name: string) => Promise<void>;
70
+ /** Type a destination into the open rename dialog and submit it. */
71
+ submitRename: (path: string) => Promise<void>;
72
+ renameInput: () => HTMLInputElement | null;
73
+ /** Every row name the tree is currently drawing, in order. */
74
+ visibleNames: () => string[];
75
+ /** The whole rendered document's text — a dialog's inline refusal lives here. */
76
+ documentText: () => string;
73
77
  /** Every button label currently in the document — the menu assertions read it. */
74
78
  buttonLabels: () => string[];
75
79
  /** Click a button by its exact label (a context-menu row, a dialog action). */
76
80
  clickMenuRow: (label: string) => Promise<void>;
77
- /**
78
- * Drag the row named `from` onto the row named `to`, or onto the panel
79
- * background when `to` is null — the whole gesture, in the order the browser
80
- * raises it: dragstart on the source, dragover on the target (where a target
81
- * refuses), then drop.
82
- */
81
+ /** Drag `from` onto `to` (or the background when null): dragstart, dragover, drop. */
83
82
  dragRow: (from: string, to: string | null) => Promise<void>;
84
- /** Install the DOM globals (call from `beforeAll`). */
83
+ /** DOM globals for the whole file (call from beforeAll / afterAll). */
85
84
  install: () => void;
86
- /** Restore the runner's own globals (call from `afterAll`). */
87
85
  restore: () => void;
88
86
  }
89
87
 
90
- /** One directory's entries as the server emits them: folders with `children: null`. */
91
- function entriesOf(disk: Disk, path: string): unknown[] {
92
- const dir = disk[path] ?? {};
93
- return Object.keys(dir).map((child) => {
94
- const rel = path === '.' ? child : `${path}/${child}`;
95
- return dir[child] === null
96
- ? { id: rel, name: child, type: 'folder', path: rel, children: null, ignored: false }
97
- : { id: rel, name: child, type: 'file', path: rel, ignored: false };
98
- });
99
- }
100
-
101
88
  export function createFileExplorerHarness(): FileExplorerHarness {
102
89
  const harness: FileExplorerHarness = {
103
90
  disk: {},
104
91
  dirReads: [],
105
92
  failNext: new Set(),
106
93
  createRequests: [],
94
+ renameRequests: [],
107
95
  transferRequests: [],
108
96
  openedFiles: [],
109
97
  container: null as unknown as HTMLElement,
@@ -127,6 +115,7 @@ export function createFileExplorerHarness(): FileExplorerHarness {
127
115
  async mount() {
128
116
  harness.failNext = new Set();
129
117
  harness.createRequests = [];
118
+ harness.renameRequests = [];
130
119
  harness.transferRequests = [];
131
120
  harness.openedFiles = [];
132
121
  // A fresh mount is a fresh session: the panel's per-session UI state
@@ -144,8 +133,8 @@ export function createFileExplorerHarness(): FileExplorerHarness {
144
133
  target.WebSocket = pristineWebSocket;
145
134
  target.fetch = async (input: unknown, init?: RequestInit) => {
146
135
  const url = String(input);
147
- const json = (body: unknown) => new Response(JSON.stringify(body), {
148
- status: 200,
136
+ const json = (body: unknown, status = 200) => new Response(JSON.stringify(body), {
137
+ status,
149
138
  headers: { 'content-type': 'application/json' },
150
139
  });
151
140
  if (url.includes('/state')) return json({ sessionId: SESSION, state: {} });
@@ -161,11 +150,25 @@ export function createFileExplorerHarness(): FileExplorerHarness {
161
150
  harness.transferRequests.push(fields);
162
151
  return json({ success: true, path: `moved-${fields.actionType}` });
163
152
  }
153
+ if (fields.actionType === 'rename') {
154
+ harness.renameRequests.push(fields);
155
+ // The real route refuses an existing destination; the disk is the
156
+ // authority here too, so a refusal spec drives the same path.
157
+ const destination = fields.newPath;
158
+ if (destination !== fields.path && diskHas(harness.disk, destination)) {
159
+ return json({ error: `${destination} already exists` }, 409);
160
+ }
161
+ moveOnDisk(harness.disk, fields.path, destination);
162
+ return json({ success: true, path: destination, previousPath: fields.path });
163
+ }
164
164
  harness.createRequests.push(fields);
165
- // The create route answers with the new file's path relative to the
165
+ // The create route answers with the new entry's path relative to the
166
166
  // LISTED root; the folder is `.` for the panel's own background menu.
167
- const rel = fields.path === '.' ? fields.name : `${fields.path}/${fields.name}`;
168
- return json({ success: true, path: rel });
167
+ // `kind` rides back because the tree draws a folder differently from a
168
+ // file, and the request is the only place it is stated.
169
+ const kind = fields.kind === 'folder' ? 'folder' : 'file';
170
+ const rel = writeToDisk(harness.disk, fields.path, fields.name, kind);
171
+ return json({ success: true, kind, path: rel });
169
172
  }
170
173
  if (url.includes('/api/fs/dir')) {
171
174
  const path = new URL(url, 'http://127.0.0.1').searchParams.get('path') ?? '.';
@@ -241,8 +244,36 @@ export function createFileExplorerHarness(): FileExplorerHarness {
241
244
  await harness.openMenuOnBackground();
242
245
  await harness.clickMenuRow('New File');
243
246
  },
247
+ async openRootNewFolder() {
248
+ await harness.openMenuOnBackground();
249
+ await harness.clickMenuRow('New Folder');
250
+ },
251
+ async submitRename(path) {
252
+ const input = harness.renameInput();
253
+ if (!input) throw new Error('no rename dialog');
254
+ await act(async () => {
255
+ input.value = path;
256
+ input.dispatchEvent(new window.Event('input', { bubbles: true }));
257
+ });
258
+ for (let i = 0; i < 5; i += 1) await act(async () => {});
259
+ await harness.clickMenuRow('Rename');
260
+ for (let i = 0; i < 20; i += 1) await act(async () => {});
261
+ },
262
+ renameInput() {
263
+ return document.querySelector('input[aria-label^="Rename "]') as HTMLInputElement | null;
264
+ },
265
+ visibleNames() {
266
+ // A row's name span is the leaf that carries it; the row wrapper reads the
267
+ // same text, so the SPAN is what is collected (one per row).
268
+ return [...harness.container.querySelectorAll('span')]
269
+ .map((el) => el.textContent?.trim() ?? '')
270
+ .filter((text) => text.length > 0);
271
+ },
272
+ documentText() {
273
+ return document.body.textContent ?? '';
274
+ },
244
275
  async submitNewFile(name) {
245
- const input = document.querySelector('input[aria-label="New file name"]') as HTMLInputElement | null;
276
+ const input = document.querySelector('input[aria-label^="New "]') as HTMLInputElement | null;
246
277
  if (!input) throw new Error('no create dialog');
247
278
  await act(async () => {
248
279
  input.value = name;
@@ -0,0 +1,183 @@
1
+ /**
2
+ * @license
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ /**
7
+ * Real loopback listeners and real processes for the lifecycle suites.
8
+ *
9
+ * `probe`, `claimPort` and `findOccupant` all answer questions about sockets
10
+ * and PIDs, so a double would test the double: every case here drives actual
11
+ * listeners on OS-assigned ports and actual processes. The harness exists
12
+ * because those suites are split by subject (the address rules, the health
13
+ * probe, the lock, the claim) and each needs the same six fixtures — a copy per
14
+ * file is how one of them quietly stops covering both loopback families.
15
+ *
16
+ * Two properties are load-bearing:
17
+ *
18
+ * - `localhost` is TWO addresses. A fixture that holds one family and expects
19
+ * the other to read free must first find a port free on both, because the OS
20
+ * assigns per family.
21
+ * - A live PID that is not ours is what a test about ANOTHER instance needs:
22
+ * the guard reads a listener reporting `process.pid` as this very process
23
+ * re-binding (the `bun run --hot` case), which is the opposite of a refusal.
24
+ */
25
+
26
+ import { expect } from 'bun:test';
27
+ import type { Server } from 'bun';
28
+ import net from 'node:net';
29
+
30
+ import { getProcessState } from '@/server/lib/lifecycle/identity';
31
+ import { isPortAvailable, waitForPortFree } from '@/server/lib/lifecycle/probe';
32
+
33
+ /**
34
+ * The runner's own fetch, reached through `Bun` so a stub another file left on
35
+ * the global cannot be mistaken for it. Every case here talks to a real
36
+ * listener, and `findOccupant` asks one over `fetch`.
37
+ */
38
+ export const realFetch = Bun.fetch;
39
+
40
+ async function probeIpv6Loopback(): Promise<boolean> {
41
+ const { promise, resolve } = Promise.withResolvers<boolean>();
42
+ const probe = net.createServer();
43
+ probe.once('error', () => resolve(false));
44
+ probe.listen({ port: 0, host: '::1' }, () => {
45
+ probe.close(() => resolve(true));
46
+ });
47
+ return await promise;
48
+ }
49
+
50
+ /**
51
+ * Whether this host has a usable IPv6 loopback.
52
+ *
53
+ * A kernel booted without IPv6 answers `EADDRNOTAVAIL` for `::1`, where a
54
+ * case about the two addresses of `localhost` cannot observe anything at all.
55
+ * Skipped BY NAME rather than weakened: an assertion that silently holds on one
56
+ * family would pass while the bug was live.
57
+ */
58
+ export const ipv6Loopback = await probeIpv6Loopback();
59
+
60
+ export interface LoopbackHarness {
61
+ /** A live listener on an OS-assigned port, plus the number it got. */
62
+ occupy(
63
+ fetch: (request: Request) => Response | Promise<Response>,
64
+ hostname?: string,
65
+ ): { server: Server<undefined>; port: number };
66
+ /** A live listener on a specific port and address. */
67
+ occupyAt(port: number, hostname: string, fetch?: (request: Request) => Response): Server<undefined>;
68
+ /** An OS-assigned port, released and confirmed free. */
69
+ freePort(): Promise<number>;
70
+ /** An OS-assigned port free on BOTH loopback families. */
71
+ freePortBothFamilies(): Promise<number>;
72
+ /** A live process that is not this one; killed by `dispose`. */
73
+ livePid(): Promise<number>;
74
+ /** A real process whose argv says `kabmo`, i.e. `matched` to identity.ts. */
75
+ spawnChamberHolder(): Promise<Bun.Subprocess>;
76
+ /** A PID that has exited and been reaped, so it reads as `dead`. */
77
+ deadPid(): Promise<number>;
78
+ /** Stop every listener and kill every child this harness started. */
79
+ dispose(): Promise<void>;
80
+ }
81
+
82
+ export function createLoopbackHarness(): LoopbackHarness {
83
+ const servers: Server<undefined>[] = [];
84
+ const children: Bun.Subprocess[] = [];
85
+
86
+ function occupy(
87
+ fetch: (request: Request) => Response | Promise<Response>,
88
+ hostname = '127.0.0.1',
89
+ ): { server: Server<undefined>; port: number } {
90
+ const server = Bun.serve({ port: 0, hostname, fetch }) as Server<undefined>;
91
+ const port = server.port;
92
+ if (port === undefined) throw new Error('the OS did not assign an ephemeral port');
93
+ servers.push(server);
94
+ return { server, port };
95
+ }
96
+
97
+ function occupyAt(
98
+ port: number,
99
+ hostname: string,
100
+ fetch: (request: Request) => Response = () => new Response('held'),
101
+ ): Server<undefined> {
102
+ const server = Bun.serve({ port, hostname, fetch }) as Server<undefined>;
103
+ servers.push(server);
104
+ return server;
105
+ }
106
+
107
+ function forget(server: Server<undefined>): void {
108
+ const index = servers.indexOf(server);
109
+ if (index >= 0) servers.splice(index, 1);
110
+ }
111
+
112
+ async function freePort(): Promise<number> {
113
+ // Bun closes a stopped listener's socket on a later event-loop turn, so an
114
+ // immediate check would be a race, not a test.
115
+ const held = occupy(() => new Response('unused'));
116
+ held.server.stop(true);
117
+ forget(held.server);
118
+ if (!(await waitForPortFree(held.port, '127.0.0.1', 2_000, 25))) {
119
+ throw new Error(`ephemeral port ${held.port} never became free`);
120
+ }
121
+ return held.port;
122
+ }
123
+
124
+ async function freePortBothFamilies(): Promise<number> {
125
+ for (let attempt = 0; attempt < 10; attempt += 1) {
126
+ const candidate = await freePort();
127
+ if (await isPortAvailable(candidate, '::1')) return candidate;
128
+ }
129
+ throw new Error('the OS handed out no ephemeral port free on both loopback families');
130
+ }
131
+
132
+ async function livePid(): Promise<number> {
133
+ const child = Bun.spawn({
134
+ cmd: ['bun', '-e', 'await new Promise(() => {})'],
135
+ stdout: 'ignore',
136
+ stderr: 'ignore',
137
+ });
138
+ children.push(child);
139
+ return child.pid;
140
+ }
141
+
142
+ async function spawnChamberHolder(): Promise<Bun.Subprocess> {
143
+ // The child announces itself before it parks, and that line is what says the
144
+ // OS has finished `exec`: a freshly spawned process's command line is
145
+ // unreadable for the instant between fork and exec, so Linux's probe falls
146
+ // back to `comm` and identity answers `mismatched` for a process that IS
147
+ // ours. Production reads a PID out of a lock file written long before, so it
148
+ // never sees that window — no fixed delay to guess at here.
149
+ const child = Bun.spawn({
150
+ cmd: ['bun', '-e', 'console.log("ready"); await new Promise(() => {})', 'kabmo-holder'],
151
+ stdout: 'pipe',
152
+ stderr: 'ignore',
153
+ });
154
+ children.push(child);
155
+ const reader = (child.stdout as ReadableStream<Uint8Array>).getReader();
156
+ await reader.read();
157
+ reader.releaseLock();
158
+ expect(getProcessState(child.pid)).toBe('matched');
159
+ return child;
160
+ }
161
+
162
+ async function deadPid(): Promise<number> {
163
+ const child = Bun.spawn({ cmd: ['bun', '-e', 'process.exit(0)'], stdout: 'ignore', stderr: 'ignore' });
164
+ const pid = child.pid;
165
+ await child.exited;
166
+ expect(getProcessState(pid)).toBe('dead');
167
+ return pid;
168
+ }
169
+
170
+ async function dispose(): Promise<void> {
171
+ for (const child of children.splice(0)) {
172
+ try {
173
+ child.kill('SIGKILL');
174
+ await child.exited;
175
+ } catch {
176
+ // Already gone.
177
+ }
178
+ }
179
+ for (const server of servers.splice(0)) server.stop(true);
180
+ }
181
+
182
+ return { occupy, occupyAt, freePort, freePortBothFamilies, livePid, spawnChamberHolder, deadPid, dispose };
183
+ }