@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
@@ -26,6 +26,8 @@
26
26
  import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, test } from 'bun:test';
27
27
  import { Window } from 'happy-dom';
28
28
  import type { h as PreactH, render as PreactRender } from 'preact';
29
+ import type { ComponentChildren } from 'preact';
30
+ import type { ComponentType } from 'preact/compat';
29
31
  import type { act as PreactAct } from 'preact/test-utils';
30
32
  import type { useFileTabs as UseFileTabsHook } from '@/client/hooks/workspace/file-tabs';
31
33
  import type { OpenFileInput } from '@/client/hooks/workspace/file-tabs';
@@ -44,7 +46,11 @@ const SESSION = 'new-file-tabs';
44
46
  const nativeFetch = Bun.fetch;
45
47
 
46
48
  let container: HTMLElement;
49
+ /** The provider component, kept so a case can re-render with a different root. */
50
+ let SessionStateProvider: ComponentType<{ sessionId: string; children: ComponentChildren }>;
47
51
  let handleOpenFile: ((file: OpenFileInput) => void) | null = null;
52
+ /** The mounted probe, so a re-render can hand it another root. */
53
+ let Probe: ComponentType<{ rootPath: string | null }>;
48
54
  let openedFiles: OpenedFile[] = [];
49
55
  let activeFileId: number | string | null = null;
50
56
 
@@ -56,23 +62,35 @@ async function settle() {
56
62
  }
57
63
  }
58
64
 
65
+ /** Re-render the mounted tree with a different workspace root. */
66
+ async function setRoot(rootPath: string | null): Promise<void> {
67
+ await act(async () => {
68
+ render(
69
+ h(SessionStateProvider, { sessionId: SESSION, children: h(Probe, { rootPath }) }),
70
+ container,
71
+ );
72
+ });
73
+ await settle();
74
+ }
75
+
59
76
  /** Mount the hook inside the provider, with `stored` as the session's blob. */
60
- async function mount(stored: Record<string, unknown> = {}) {
77
+ async function mount(stored: Record<string, unknown> = {}, root: string | null = ROOT) {
61
78
  globalThis.fetch = (async () => ({
62
79
  ok: true,
63
80
  json: async () => ({ sessionId: SESSION, state: stored }),
64
81
  text: async () => JSON.stringify({ sessionId: SESSION, state: stored }),
65
82
  })) as unknown as typeof fetch;
66
- const { SessionStateProvider } = await import('@/client/components/common/session-state-provider/index');
67
- const Probe = () => {
68
- const tabs = useFileTabs(ROOT, SESSION);
83
+ const provider = await import('@/client/components/common/session-state-provider/index');
84
+ SessionStateProvider = provider.SessionStateProvider as typeof SessionStateProvider;
85
+ Probe = ({ rootPath }: { rootPath: string | null }) => {
86
+ const tabs = useFileTabs(rootPath, SESSION);
69
87
  handleOpenFile = tabs.handleOpenFile;
70
88
  openedFiles = tabs.openedFiles;
71
89
  activeFileId = tabs.activeFileId;
72
90
  return null;
73
91
  };
74
92
  await act(async () => {
75
- render(h(SessionStateProvider, { sessionId: SESSION, children: h(Probe, {}) }), container);
93
+ render(h(SessionStateProvider, { sessionId: SESSION, children: h(Probe, { rootPath: root }) }), container);
76
94
  });
77
95
  await settle();
78
96
  }
@@ -147,6 +165,45 @@ describe('useFileTabs open resolution', () => {
147
165
  expect(activeFileId).toBe(fileTabId('src/a.ts'));
148
166
  });
149
167
 
168
+ /**
169
+ * A root that is not known yet must not close the tabs.
170
+ *
171
+ * `activeProjectPath` is derived from the sidebar's folder list, which is empty
172
+ * for a beat on every load and again whenever the structure topic
173
+ * re-subscribes. Treating that `null` as a root CHANGE wiped the file the user
174
+ * had just clicked, and the editor drew "Select a file to open" over it — the
175
+ * rare e2e failure this pins.
176
+ */
177
+ test('a transiently unknown root does not wipe the open tabs', async () => {
178
+ await mount();
179
+ await act(async () => {
180
+ handleOpenFile?.(explorerPayload('src/a.ts'));
181
+ });
182
+ await settle();
183
+ expect(openedFiles).toHaveLength(1);
184
+
185
+ // The folder list drops out and comes back — the same workspace.
186
+ await setRoot(null);
187
+ await setRoot(ROOT);
188
+
189
+ expect(openedFiles).toHaveLength(1);
190
+ expect(activeFileId).toBe('src/a.ts');
191
+ });
192
+
193
+ test('a DIFFERENT root closes the previous workspace’s tabs', async () => {
194
+ await mount();
195
+ await act(async () => {
196
+ handleOpenFile?.(explorerPayload('src/a.ts'));
197
+ });
198
+ await settle();
199
+ expect(openedFiles).toHaveLength(1);
200
+
201
+ await setRoot('/other');
202
+
203
+ expect(openedFiles).toHaveLength(0);
204
+ expect(activeFileId).toBeNull();
205
+ });
206
+
150
207
  test('reopening under the computed id activates the tab rather than adding one', async () => {
151
208
  await mount();
152
209
 
@@ -52,24 +52,37 @@ export function useFileTabs(
52
52
  [sessionId],
53
53
  );
54
54
 
55
- const activeRootRef = useRef<string | null | undefined>(undefined);
55
+ /** The last root a wipe was performed FOR; `null` until one is known. */
56
+ const wipedRootRef = useRef<string | null>(null);
56
57
  const { ready: layoutReady } = useSessionStateContext();
57
- const wipedRootRef = useRef<string | null | undefined>(undefined);
58
58
 
59
59
  useEffect(() => {
60
- if (activeRootRef.current === activeProjectPath) return;
61
- activeRootRef.current = activeProjectPath;
62
- if (!layoutReady || wipedRootRef.current === activeProjectPath) return;
60
+ // Only a CHANGE from one known root to another closes the previous
61
+ // workspace's tabs.
62
+ //
63
+ // Two rules, and each was a bug first. A root that is not known yet is not a
64
+ // root that changed: the path is derived from the sidebar's folder list,
65
+ // which is empty for a beat on every load (and again whenever the structure
66
+ // topic re-subscribes), so treating `null` as a root wiped the tabs a user
67
+ // had just opened. And the FIRST known root is not a change either — it is
68
+ // the workspace the page was already showing, so wiping on it discarded the
69
+ // tabs the session blob had just restored (measured: the e2e specs had to
70
+ // gate every tree click on the `/state` response to survive it, and a click
71
+ // that lost that race left the editor reading "Select a file to open" over
72
+ // the file the user had opened).
73
+ if (!activeProjectPath || !layoutReady) return;
74
+ const previous = wipedRootRef.current;
63
75
  wipedRootRef.current = activeProjectPath;
76
+ if (previous === null || previous === activeProjectPath) return;
64
77
  // The tabs are dropped, but their scroll offsets deliberately are NOT: this
65
- // wipe is not a user closing a tab. It fires on every load (the root is null
66
- // until the folder resolves), so dropping here erased the offset the session
67
- // blob had just restored — measured: the offset persisted as `900`, and came
68
- // back as `{}` after a reload. The entry is keyed by the file's path hash, so
69
- // it is reused when that file is opened again, which is the point.
78
+ // wipe is not a user closing a tab. Dropping here erased the offset the
79
+ // session blob had just restored — measured: the offset persisted as `900`,
80
+ // and came back as `{}` after a reload. The entry is keyed by the file's
81
+ // path hash, so it is reused when that file is opened again, which is the
82
+ // point.
70
83
  setOpenedFiles([]);
71
84
  setActiveFileId(null);
72
- }, [activeProjectPath, sessionId, layoutReady, setOpenedFiles, setActiveFileId]);
85
+ }, [activeProjectPath, layoutReady, setOpenedFiles, setActiveFileId]);
73
86
 
74
87
  const handleOpenFile = useCallback((file: OpenFileInput) => {
75
88
  const fileEntry: OpenedFile = {
@@ -22,7 +22,7 @@ import type { FormEvent, RefObject } from 'preact/compat';
22
22
  import { useFetcher } from '@/client/lib/router/fetcher';
23
23
  import { toAbsolutePath } from '@/shared/lib/fs/paths';
24
24
  import { copyToClipboard } from '@/client/hooks/ui/clipboard';
25
- import { useFileCreate } from '@/client/hooks/workspace/file-create';
25
+ import { useFileCreate, type CreatedEntry } from '@/client/hooks/workspace/file-create';
26
26
  import type { FileClipboard } from '@/client/hooks/workspace/file-clipboard';
27
27
  import type { FsActionResponse } from '@/shared/types';
28
28
 
@@ -40,7 +40,17 @@ export interface FileActionDeps {
40
40
  /** Which diff to open, when the row is a changed file. */
41
41
  diff?: { staged?: boolean; status?: string };
42
42
  onOpenFile?: (file: { path: string; name: string; root?: string; repo?: string }) => void;
43
+ /** Re-read the tree — a DELETE has no local answer, so it re-reads. */
43
44
  onActionComplete: () => void;
45
+ /**
46
+ * A row the panel just created, placed locally.
47
+ *
48
+ * A create answers with the whole result, so the tree inserts it rather than
49
+ * re-reading the root plus every open folder to learn it back.
50
+ */
51
+ onCreated?: (entry: CreatedEntry) => void;
52
+ /** A row the panel just renamed, moved locally — same reason as `onCreated`. */
53
+ onRenamed?: (fromPath: string, toPath: string) => void;
44
54
  /** The panel's clipboard: Cut/Copy fill it, Paste reads it. */
45
55
  clipboard: FileClipboard;
46
56
  /** Whether this row is a folder — what Cut/Copy/Paste need to name their target. */
@@ -56,12 +66,16 @@ export interface FileActions {
56
66
  showDeleteModal: boolean;
57
67
  showHistoryModal: boolean;
58
68
  showCreateModal: boolean;
69
+ /** Whether the open create dialog makes a folder. */
70
+ createKind: 'file' | 'folder';
59
71
  /** The server's refusal for the create request, rendered inside its dialog. */
60
72
  createError: string | null;
61
73
  createName: string;
62
74
  setCreateName: (value: string) => void;
63
75
  renameValue: string;
64
76
  setRenameValue: (value: string) => void;
77
+ /** The server's refusal for the rename request, rendered inside its dialog. */
78
+ renameError: string | null;
65
79
  /** True while a request is in flight; every dialog's buttons read it. */
66
80
  busy: boolean;
67
81
  /** Label of the row's Paste action, or null when a paste cannot land here. */
@@ -79,7 +93,7 @@ export interface FileActions {
79
93
  closeHistory: () => void;
80
94
  }
81
95
 
82
- export function useFileActions({ file, diff, onOpenFile, onActionComplete, clipboard, isFolder }: FileActionDeps): FileActions {
96
+ export function useFileActions({ file, diff, onOpenFile, onActionComplete, onCreated, onRenamed, clipboard, isFolder }: FileActionDeps): FileActions {
83
97
  const actionFetcher = useFetcher<FsActionResponse>();
84
98
  const [contextMenu, setContextMenu] = useState<{ x: number; y: number } | null>(null);
85
99
  const contextMenuRef = useRef<HTMLDivElement>(null);
@@ -87,6 +101,7 @@ export function useFileActions({ file, diff, onOpenFile, onActionComplete, clipb
87
101
  const [showDeleteModal, setShowDeleteModal] = useState(false);
88
102
  const [showHistoryModal, setShowHistoryModal] = useState(false);
89
103
  const [renameValue, setRenameValue] = useState(file.path);
104
+ const [renameError, setRenameError] = useState<string | null>(null);
90
105
 
91
106
  const repo = file.repo;
92
107
  const rootPath = file.rootPath;
@@ -98,7 +113,10 @@ export function useFileActions({ file, diff, onOpenFile, onActionComplete, clipb
98
113
  folderPath: file.path,
99
114
  rootPath,
100
115
  repo,
101
- onCreated: onActionComplete,
116
+ // The tree places the entry itself and does NOT reload: the response names
117
+ // the whole result, and re-reading the root plus every open folder to learn
118
+ // it back is the reload this path exists to avoid.
119
+ onCreated: (entry) => (onCreated ? onCreated(entry) : onActionComplete()),
102
120
  onOpenCreated: (created) => onOpenFile?.({ ...created, root: rootPath, repo: repo || '.' }),
103
121
  });
104
122
 
@@ -124,7 +142,15 @@ export function useFileActions({ file, diff, onOpenFile, onActionComplete, clipb
124
142
  if (data.success && !data.type) {
125
143
  setShowRenameModal(false);
126
144
  setShowDeleteModal(false);
127
- onActionComplete();
145
+ setRenameError(null);
146
+ // A rename response names both ends, so the tree moves the row locally
147
+ // instead of re-reading every open folder to discover the move.
148
+ if (data.previousPath && data.path && onRenamed) onRenamed(data.previousPath, data.path);
149
+ else onActionComplete();
150
+ } else if (!data.success && data.error && showRenameModal) {
151
+ // A refusal keeps the dialog open with the server's own words beside the
152
+ // field: "already exists" is a message about what to type next.
153
+ setRenameError(data.error);
128
154
  }
129
155
  // eslint-disable-next-line react-hooks/exhaustive-deps
130
156
  }, [actionFetcher.state, actionFetcher.data]);
@@ -155,6 +181,7 @@ export function useFileActions({ file, diff, onOpenFile, onActionComplete, clipb
155
181
 
156
182
  const handleAction = (actionType: string) => {
157
183
  setContextMenu(null);
184
+ setRenameError(null);
158
185
  if (actionType === 'view') {
159
186
  openFile();
160
187
  } else if (actionType === 'diff') {
@@ -186,7 +213,9 @@ export function useFileActions({ file, diff, onOpenFile, onActionComplete, clipb
186
213
  setRenameValue(file.path);
187
214
  setShowRenameModal(true);
188
215
  } else if (actionType === 'new_file') {
189
- create.openDialog();
216
+ create.openDialog('file');
217
+ } else if (actionType === 'new_folder') {
218
+ create.openDialog('folder');
190
219
  } else if (actionType === 'delete') {
191
220
  setShowDeleteModal(true);
192
221
  } else if (actionType === 'cut') {
@@ -202,6 +231,7 @@ export function useFileActions({ file, diff, onOpenFile, onActionComplete, clipb
202
231
 
203
232
  const submitRename = (e: FormEvent) => {
204
233
  e.preventDefault();
234
+ setRenameError(null);
205
235
  const fd = new FormData();
206
236
  fd.append('actionType', 'rename');
207
237
  fd.append('path', file.path);
@@ -225,11 +255,13 @@ export function useFileActions({ file, diff, onOpenFile, onActionComplete, clipb
225
255
  showDeleteModal,
226
256
  showHistoryModal,
227
257
  showCreateModal: create.open,
258
+ createKind: create.kind,
228
259
  createError: create.error,
229
260
  createName: create.name,
230
261
  setCreateName: create.setName,
231
262
  renameValue,
232
263
  setRenameValue,
264
+ renameError,
233
265
  busy: actionFetcher.state !== 'idle' || create.busy || clipboard.busy,
234
266
  pasteLabel,
235
267
  fetcherState: actionFetcher.state,
@@ -0,0 +1,72 @@
1
+ /**
2
+ * @license
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ /**
7
+ * The navbar's session menu, resolved from the session ON SCREEN.
8
+ *
9
+ * A hook rather than three statements in the layout because the lookup has a
10
+ * rule of its own: the session's row may live in a folder that is not expanded
11
+ * in the sidebar (or not visible at all), so it is found by id across every
12
+ * folder instead of by walking the visible list. No row means no menu — a
13
+ * session the sidebar does not know (a chat running outside every registered
14
+ * workspace) gets nothing rather than dead items.
15
+ *
16
+ * The pending `new-…` refusal lives here too: such a chat has no transcript
17
+ * anywhere yet, so the server refuses rename and delete, and the menu hides
18
+ * them the way the sidebar does.
19
+ */
20
+
21
+ import { useMemo } from 'preact/hooks';
22
+ import { useSidebarData } from '@/client/hooks/chat/omp/session-list';
23
+ import { useSessionActions } from '@/client/hooks/workspace/session-actions';
24
+ import { useSessionDelete, type SessionDeleteActions } from '@/client/hooks/workspace/session-delete';
25
+ import type { WorkspaceFolderData } from '@/shared/types';
26
+
27
+ /** What `TopNavbar` needs to draw its title menu. */
28
+ export interface NavbarSessionMenu {
29
+ isArchived: boolean;
30
+ onRename?: (name: string) => void;
31
+ onRenameWithAi?: () => void;
32
+ onArchive: () => void;
33
+ onDelete?: () => void;
34
+ }
35
+
36
+ export interface NavbarSessionActions {
37
+ menu: NavbarSessionMenu | undefined;
38
+ /** The delete confirmation's own state machine, which the layout renders. */
39
+ sessionDelete: SessionDeleteActions;
40
+ }
41
+
42
+ export function useNavbarSessionActions(
43
+ folders: WorkspaceFolderData[],
44
+ sessionId: string | null,
45
+ report: (message: string, tone?: 'success' | 'error') => void,
46
+ ): NavbarSessionActions {
47
+ const sidebar = useSidebarData();
48
+ const actions = useSessionActions(sidebar.refresh, report);
49
+ const sessionDelete = useSessionDelete();
50
+
51
+ const activeSession = useMemo(() => {
52
+ if (!sessionId) return null;
53
+ for (const folder of folders) {
54
+ const found = (folder.sessions || []).find((s) => String(s.id) === String(sessionId));
55
+ if (found) return found;
56
+ }
57
+ return null;
58
+ }, [folders, sessionId]);
59
+
60
+ const pending = Boolean(sessionId && sessionId.startsWith('new-'));
61
+ const menu: NavbarSessionMenu | undefined = activeSession
62
+ ? {
63
+ isArchived: activeSession.is_archived === 1,
64
+ onRename: pending ? undefined : (name: string) => void actions.handleRename(activeSession, name),
65
+ onRenameWithAi: pending ? undefined : () => void actions.handleRenameWithAi(activeSession),
66
+ onArchive: () => actions.handleArchive(activeSession),
67
+ onDelete: pending ? undefined : () => sessionDelete.requestDelete(activeSession),
68
+ }
69
+ : undefined;
70
+
71
+ return { menu, sessionDelete };
72
+ }
@@ -0,0 +1,165 @@
1
+ /**
2
+ * @license
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ /**
7
+ * Client state for the worktree surfaces: the list, creation and removal.
8
+ *
9
+ * One module for all three because they share the same two facts — the project
10
+ * the request is scoped to and the failure shape the server answers with — and
11
+ * splitting them would put the fetch envelope in three places.
12
+ *
13
+ * Nothing here invents a path: the server resolves the destination from the
14
+ * dialog's NAME and reports back what it used, so the client cannot name a
15
+ * directory outside the project.
16
+ */
17
+
18
+ import { useCallback, useEffect, useState } from 'preact/hooks';
19
+ import type { WorktreeFailure, WorktreeInfo, WorktreeListPayload, WorktreeValidation } from '@/shared/lib/workspace/worktree/types';
20
+
21
+ export interface WorktreeRequestScope {
22
+ /** The project's `project_path`, sent as `?root=`. */
23
+ root: string | null;
24
+ /** Sent as `?folderId=` so an unbound-but-valid folder still resolves. */
25
+ folderId?: number | string | null;
26
+ }
27
+
28
+ function queryFor(scope: WorktreeRequestScope): string {
29
+ const params = new URLSearchParams();
30
+ if (scope.root) params.set('root', scope.root);
31
+ if (scope.folderId !== null && scope.folderId !== undefined) params.set('folderId', String(scope.folderId));
32
+ const query = params.toString();
33
+ return query ? `?${query}` : '';
34
+ }
35
+
36
+ /** A refusal the UI reports verbatim; `field` marks the input that caused it. */
37
+ export class WorktreeRequestError extends Error {
38
+ readonly failure: WorktreeFailure;
39
+
40
+ constructor(failure: WorktreeFailure) {
41
+ super(failure.message);
42
+ this.name = 'WorktreeRequestError';
43
+ this.failure = failure;
44
+ }
45
+ }
46
+
47
+ async function readFailure(response: Response): Promise<never> {
48
+ const body = (await response.json().catch(() => null)) as (WorktreeFailure & { error?: string }) | null;
49
+ throw new WorktreeRequestError({
50
+ code: body?.code ?? 'create_failed',
51
+ message: body?.message ?? body?.error ?? `HTTP ${response.status}`,
52
+ ...(body?.field ? { field: body.field } : {}),
53
+ ...(body?.count !== undefined ? { count: body.count } : {}),
54
+ });
55
+ }
56
+
57
+ /** The project's worktrees, with the facts each row shows. */
58
+ export function useWorktrees(scope: WorktreeRequestScope, enabled: boolean) {
59
+ const [payload, setPayload] = useState<WorktreeListPayload | null>(null);
60
+ const [loading, setLoading] = useState(false);
61
+ const [failure, setFailure] = useState<WorktreeFailure | null>(null);
62
+
63
+ const refresh = useCallback(async () => {
64
+ if (!enabled) return;
65
+ setLoading(true);
66
+ try {
67
+ const response = await fetch(`/api/worktrees${queryFor(scope)}`);
68
+ if (!response.ok) await readFailure(response);
69
+ setPayload((await response.json()) as WorktreeListPayload);
70
+ setFailure(null);
71
+ } catch (cause) {
72
+ // Kept apart from the payload: an empty list and a failed read are
73
+ // different answers, and only one of them means nothing is there. The
74
+ // CODE travels too, because "this folder is not a repository" and "git
75
+ // could not answer" are different states to draw — and the first one is
76
+ // how a caller knows to offer no worktree control at all.
77
+ setFailure(
78
+ cause instanceof WorktreeRequestError
79
+ ? cause.failure
80
+ : { code: 'create_failed', message: cause instanceof Error ? cause.message : String(cause) },
81
+ );
82
+ } finally {
83
+ setLoading(false);
84
+ }
85
+ }, [enabled, scope.root, scope.folderId]);
86
+
87
+ useEffect(() => {
88
+ void refresh();
89
+ }, [refresh]);
90
+
91
+ return {
92
+ payload,
93
+ worktrees: payload?.worktrees ?? ([] as WorktreeInfo[]),
94
+ /**
95
+ * Whether the project is a git repository at all. `null` while it is still
96
+ * unknown: a caller that draws a worktree control must wait for the answer
97
+ * rather than offer one a plain folder can never satisfy.
98
+ */
99
+ isRepo: payload ? payload.isRepo : null,
100
+ /** The resolved parent directory a create would use. */
101
+ base: payload?.base ?? '',
102
+ loading,
103
+ failure,
104
+ /** The failure's message, for the surfaces that only print one. */
105
+ error: failure?.message ?? null,
106
+ refresh,
107
+ };
108
+ }
109
+
110
+ export interface CreateWorktreeRequest {
111
+ mode: 'new' | 'existing';
112
+ branch?: string;
113
+ worktreeName?: string;
114
+ startRef?: string;
115
+ }
116
+
117
+ /** The plan a create would use, without creating anything. */
118
+ export async function validateWorktree(
119
+ scope: WorktreeRequestScope,
120
+ request: CreateWorktreeRequest,
121
+ ): Promise<WorktreeValidation> {
122
+ const response = await fetch(`/api/worktrees/validate${queryFor(scope)}`, {
123
+ method: 'POST',
124
+ headers: { 'content-type': 'application/json' },
125
+ body: JSON.stringify(request),
126
+ });
127
+ const body = (await response.json().catch(() => null)) as WorktreeValidation | null;
128
+ if (!body) return { ok: false, failures: [{ code: 'create_failed', message: `HTTP ${response.status}` }] };
129
+ return body;
130
+ }
131
+
132
+ export async function createWorktree(
133
+ scope: WorktreeRequestScope,
134
+ request: CreateWorktreeRequest,
135
+ ): Promise<WorktreeInfo> {
136
+ const response = await fetch(`/api/worktrees${queryFor(scope)}`, {
137
+ method: 'POST',
138
+ headers: { 'content-type': 'application/json' },
139
+ body: JSON.stringify(request),
140
+ });
141
+ if (!response.ok) await readFailure(response);
142
+ const body = (await response.json()) as { worktree: WorktreeInfo };
143
+ return body.worktree;
144
+ }
145
+
146
+ export interface RemoveWorktreeResult {
147
+ removed: true;
148
+ archived: string[];
149
+ branchDeleted: boolean;
150
+ branchKept: boolean;
151
+ }
152
+
153
+ export async function removeWorktree(
154
+ scope: WorktreeRequestScope,
155
+ worktreePath: string,
156
+ options: { deleteBranch?: boolean } = {},
157
+ ): Promise<RemoveWorktreeResult> {
158
+ const response = await fetch(`/api/worktrees${queryFor(scope)}`, {
159
+ method: 'DELETE',
160
+ headers: { 'content-type': 'application/json' },
161
+ body: JSON.stringify({ path: worktreePath, deleteBranch: options.deleteBranch === true }),
162
+ });
163
+ if (!response.ok) await readFailure(response);
164
+ return (await response.json()) as RemoveWorktreeResult;
165
+ }
@@ -30,6 +30,14 @@
30
30
  export interface DesktopInfo {
31
31
  platform: string;
32
32
  shellVersion: string;
33
+ /**
34
+ * The machine's architecture, in the canonical vocabulary (`arm64` / `x64`).
35
+ *
36
+ * Every desktop artifact is per-architecture, so without this the console can
37
+ * only offer the release page. Absent on a shell too old to report it, which
38
+ * degrades the offer to that page rather than breaking it.
39
+ */
40
+ arch?: string;
33
41
  /** True when the shell hides the native title bar and the page draws it. */
34
42
  overlayTitlebar: boolean;
35
43
  /**
@@ -156,3 +164,40 @@ export function windowToggleMaximize(): Promise<void> {
156
164
  export function windowClose(): Promise<void> {
157
165
  return call<void>('window_close');
158
166
  }
167
+
168
+ /**
169
+ * What the shell did with an update it was handed.
170
+ *
171
+ * The shell owns the whole run — download, verify, replace, relaunch — because
172
+ * only it can do any of those things: a page cannot replace the executable it is
173
+ * running inside, and a page's "download" leaves a file in a folder the user has
174
+ * to find. This is the report it sends back.
175
+ *
176
+ * `relaunching` is the honest terminal state on success: the shell is about to
177
+ * quit, so a caller that keeps a spinner going is describing a page that is
178
+ * already gone.
179
+ */
180
+ export interface ShellUpdateOutcome {
181
+ success: boolean;
182
+ /** The version the shell installed, when it could read one. */
183
+ version?: string | null;
184
+ /** A sentence for the user — the shell's own, never a generic failure. */
185
+ message: string;
186
+ relaunching?: boolean;
187
+ }
188
+
189
+ /**
190
+ * Ask the shell to install the build it just offered.
191
+ *
192
+ * `url` and `version` are the console's answer, passed through rather than
193
+ * re-derived: the shell has no release list of its own, and asking it to fetch
194
+ * one would be a second reader of the same GitHub endpoint.
195
+ *
196
+ * The shell VERIFIES the bytes before installing them (the platform's own check —
197
+ * a code signature, an archive shape, a version floor). A console that is
198
+ * compromised can therefore make the shell download, but not make it run
199
+ * something the platform will not vouch for.
200
+ */
201
+ export function applyShellUpdate(url: string, version: string): Promise<ShellUpdateOutcome> {
202
+ return call<ShellUpdateOutcome>('update_apply', { url, version });
203
+ }
@@ -23,7 +23,7 @@
23
23
  * editor can decide by equality whether the request is for the file it holds.
24
24
  */
25
25
 
26
- import type { FindOptions } from '@/shared/lib/code/editor/find';
26
+ import type { FindOptions } from '@/shared/lib/code/editor/find-scan';
27
27
 
28
28
  export interface SearchRevealRequest {
29
29
  /** Workspace-relative path of the file the match lives in. */
@@ -1,6 +1,7 @@
1
1
  import { hydrate } from 'preact';
2
2
  import { App } from '@/client/App';
3
3
  import { bootSyntax } from '@/shared/lib/code/highlighter';
4
+ import { warmSyntaxTokenizers } from '@/shared/lib/code/highlighter-warmup';
4
5
  import { primeKabmoSettings } from '@/shared/lib/settings/client';
5
6
  import { initDocumentTheme } from '@/client/hooks/ui/theme';
6
7
  import { primeWindowChrome } from '@/client/hooks/ui/window-chrome';
@@ -70,7 +71,10 @@ installFileLaunchHandler((result) => {
70
71
  });
71
72
 
72
73
  // Fire-and-forget: warms the Shiki highlighter in parallel with hydration.
73
- bootSyntax();
74
+ // Once the eager grammars land, each is tokenized once on a tiny sample during
75
+ // idle time — loading a grammar is not compiling it, and the compile used to be
76
+ // paid inside the first tool card the reader expanded.
77
+ void bootSyntax().then(() => warmSyntaxTokenizers());
74
78
 
75
79
  if (root) {
76
80
  hydrate(