@kabmo-labs/kabmo 0.7.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (351) hide show
  1. package/README.md +7 -0
  2. package/dist/client/index.html +1 -1
  3. package/dist/client/index.js +403 -384
  4. package/dist/client/static/js/{index-0wvbca0b.js → index-bpptfkz2.js} +905 -875
  5. package/dist/client/static/js/index-k315p1e8.css +1 -0
  6. package/package.json +10 -1
  7. package/src/client/components/common/code-surface/documents.test.ts +120 -0
  8. package/src/client/components/common/code-surface/documents.ts +201 -0
  9. package/src/client/components/common/code-surface/extensions.test.ts +110 -0
  10. package/src/client/components/common/code-surface/extensions.ts +162 -0
  11. package/src/client/components/common/code-surface/handle.ts +102 -0
  12. package/src/client/components/common/code-surface/index.tsx +253 -314
  13. package/src/client/components/common/code-surface/keymap.test.ts +133 -0
  14. package/src/client/components/common/code-surface/keymap.ts +267 -0
  15. package/src/client/components/common/code-surface/marks.ts +74 -0
  16. package/src/client/components/common/code-surface/shiki.ts +234 -0
  17. package/src/client/components/common/code-surface/sync.test.ts +185 -0
  18. package/src/client/components/common/code-surface/sync.ts +58 -0
  19. package/src/client/components/common/code-surface/theme.test.ts +202 -0
  20. package/src/client/components/common/code-surface/theme.ts +111 -0
  21. package/src/client/components/common/editor-view-buttons/index.test.tsx +148 -0
  22. package/src/client/components/common/editor-view-buttons/index.tsx +109 -0
  23. package/src/client/components/common/header-controls/index.tsx +24 -1
  24. package/src/client/components/common/header-controls/shell-chrome.test.tsx +272 -0
  25. package/src/client/components/common/shell-update-button/Modal.tsx +163 -0
  26. package/src/client/components/common/shell-update-button/index.test.tsx +251 -0
  27. package/src/client/components/common/shell-update-button/index.tsx +125 -0
  28. package/src/client/components/common/toolbar-row/index.test.tsx +166 -0
  29. package/src/client/components/common/toolbar-row/index.tsx +189 -0
  30. package/src/client/components/common/workspace-options-menu/index.tsx +19 -1
  31. package/src/client/components/layout/desktop-layout/TopNavbar.tsx +5 -6
  32. package/src/client/components/layout/desktop-layout/WorkspacePanels.tsx +41 -78
  33. package/src/client/components/layout/desktop-layout/index.tsx +55 -69
  34. package/src/client/components/layout/desktop-layout/resizer/index.tsx +6 -3
  35. package/src/client/components/layout/session-sidebar/CategoryHeader.tsx +24 -0
  36. package/src/client/components/layout/session-sidebar/CategoryHeader.worktree.test.tsx +126 -0
  37. package/src/client/components/layout/session-sidebar/CategoryItem.tsx +4 -0
  38. package/src/client/components/layout/session-sidebar/Footer.tsx +8 -15
  39. package/src/client/components/layout/session-sidebar/SessionItem.tsx +12 -0
  40. package/src/client/components/layout/session-sidebar/SessionList.tsx +4 -0
  41. package/src/client/components/layout/session-sidebar/SessionsView.tsx +24 -0
  42. package/src/client/components/layout/session-sidebar/WorktreeBadge.test.ts +48 -0
  43. package/src/client/components/layout/session-sidebar/WorktreeBadge.tsx +55 -0
  44. package/src/client/components/mobile/LayoutWrapper.tsx +23 -16
  45. package/src/client/components/mobile/RightSidebar.tsx +21 -10
  46. package/src/client/components/mobile/SessionSidebar.tsx +8 -0
  47. package/src/client/components/mobile/header-titlebar.test.tsx +172 -0
  48. package/src/client/components/mobile/mobile-main-view/Header.tsx +10 -1
  49. package/src/client/components/mobile/mobile-right-sidebar/FullDiff.tsx +3 -0
  50. package/src/client/components/mobile/mobile-right-sidebar/FullEditor.tsx +63 -38
  51. package/src/client/components/mobile/mobile-right-sidebar/OutlineButton.test.tsx +137 -0
  52. package/src/client/components/mobile/mobile-right-sidebar/OutlineButton.tsx +14 -3
  53. package/src/client/components/mobile/mobile-session-sidebar/Footer.tsx +9 -21
  54. package/src/client/components/mobile/mobile-session-sidebar/Header.tsx +6 -2
  55. package/src/client/components/mobile/mobile-session-sidebar/Item.tsx +16 -0
  56. package/src/client/components/mobile/mobile-session-sidebar/List.tsx +4 -0
  57. package/src/client/components/mobile/mobile-session-sidebar/SessionRow.tsx +5 -0
  58. package/src/client/components/mobile/mobile-session-sidebar/WorktreeDialog.tsx +49 -0
  59. package/src/client/components/mobile/worktree-session.ts +51 -0
  60. package/src/client/components/settings/Modal.tsx +8 -0
  61. package/src/client/components/settings/categories/AppearanceSettings.tsx +3 -0
  62. package/src/client/components/settings/categories/ProjectSettings.tsx +22 -8
  63. package/src/client/components/settings/categories/appearance-settings/EditorThemeSection.test.tsx +167 -0
  64. package/src/client/components/settings/categories/appearance-settings/EditorThemeSection.tsx +184 -0
  65. package/src/client/components/settings/categories/project-settings/WorktreeSettings.tsx +146 -0
  66. package/src/client/components/settings/categories/project-settings/worktree-row.test.ts +83 -0
  67. package/src/client/components/settings/categories/project-settings/worktree-row.ts +47 -0
  68. package/src/client/components/workspace/chat-timeline/EmptyWorkspacePrompt.tsx +36 -55
  69. package/src/client/components/workspace/chat-timeline/PendingChatView.tsx +8 -0
  70. package/src/client/components/workspace/chat-timeline/SystemNotice.tsx +9 -4
  71. package/src/client/components/workspace/chat-timeline/TaskResultContent.tsx +9 -7
  72. package/src/client/components/workspace/chat-timeline/index.tsx +5 -0
  73. package/src/client/components/workspace/chat-timeline/target-picker/WorkspacePicker.test.tsx +132 -0
  74. package/src/client/components/workspace/chat-timeline/target-picker/WorkspacePicker.tsx +120 -0
  75. package/src/client/components/workspace/chat-timeline/target-picker/WorktreePicker.test.tsx +319 -0
  76. package/src/client/components/workspace/chat-timeline/target-picker/WorktreePicker.tsx +285 -0
  77. package/src/client/components/workspace/chat-timeline/tool-renderers/ask-panel/index.test.ts +2 -0
  78. package/src/client/components/workspace/chat-timeline/tool-renderers/hashline-patch/index.tsx +34 -29
  79. package/src/client/components/workspace/chat-timeline/tool-renderers/panels/AstEdit.tsx +11 -9
  80. package/src/client/components/workspace/chat-timeline/tool-renderers/panels/Bash.tsx +10 -7
  81. package/src/client/components/workspace/chat-timeline/tool-renderers/panels/Edit.tsx +25 -8
  82. package/src/client/components/workspace/chat-timeline/tool-renderers/panels/Eval.test.tsx +146 -0
  83. package/src/client/components/workspace/chat-timeline/tool-renderers/panels/Eval.tsx +32 -17
  84. package/src/client/components/workspace/chat-timeline/tool-renderers/panels/FileMatches.test.ts +114 -0
  85. package/src/client/components/workspace/chat-timeline/tool-renderers/panels/FileMatches.tsx +111 -0
  86. package/src/client/components/workspace/chat-timeline/tool-renderers/panels/Read.tsx +7 -7
  87. package/src/client/components/workspace/chat-timeline/tool-renderers/panels/SearchTool.tsx +9 -59
  88. package/src/client/components/workspace/chat-timeline/tool-renderers/shared/ArtifactBody.tsx +2 -7
  89. package/src/client/components/workspace/chat-timeline/tool-renderers/shared/DiffView.test.ts +27 -28
  90. package/src/client/components/workspace/chat-timeline/tool-renderers/shared/DiffView.tsx +39 -26
  91. package/src/client/components/workspace/chat-timeline/tool-renderers/shared/ExcerptCode.tsx +8 -8
  92. package/src/client/components/workspace/chat-timeline/tool-renderers/shared/HighlightedCode.tsx +35 -0
  93. package/src/client/components/workspace/chat-timeline/tool-renderers/shared/JsonCodeBlock.tsx +4 -4
  94. package/src/client/components/workspace/diff-panel/MergeDiffView.tsx +179 -0
  95. package/src/client/components/workspace/diff-panel/ThemedMergeDiffView.tsx +24 -0
  96. package/src/client/components/workspace/diff-panel/Toolbar.tsx +224 -186
  97. package/src/client/components/workspace/diff-panel/ToolbarSegments.tsx +98 -0
  98. package/src/client/components/workspace/diff-panel/index.tsx +76 -145
  99. package/src/client/components/workspace/diff-panel/merge.ts +315 -0
  100. package/src/client/components/workspace/editor/Body.tsx +4 -7
  101. package/src/client/components/workspace/editor/CommandPalette.tsx +2 -2
  102. package/src/client/components/workspace/editor/FileDocument.tsx +58 -63
  103. package/src/client/components/workspace/editor/FilePane.tsx +4 -7
  104. package/src/client/components/workspace/editor/FindWidget.tsx +7 -2
  105. package/src/client/components/workspace/editor/Toolbar.tsx +220 -123
  106. package/src/client/components/workspace/editor/index.tsx +7 -9
  107. package/src/client/components/workspace/file-explorer/ContextMenu.tsx +2 -1
  108. package/src/client/components/workspace/file-explorer/Modals.tsx +40 -12
  109. package/src/client/components/workspace/file-explorer/OutlineSection.test.tsx +18 -0
  110. package/src/client/components/workspace/file-explorer/OutlineSection.tsx +10 -3
  111. package/src/client/components/workspace/file-explorer/RootMenu.tsx +14 -5
  112. package/src/client/components/workspace/file-explorer/Toolbar.tsx +74 -0
  113. package/src/client/components/workspace/file-explorer/TreeItem.tsx +38 -3
  114. package/src/client/components/workspace/file-explorer/filter.ts +47 -0
  115. package/src/client/components/workspace/file-explorer/index.tsx +77 -64
  116. package/src/client/components/workspace/file-explorer/local-edit.test.ts +135 -0
  117. package/src/client/components/workspace/search-panel/Body.tsx +1 -1
  118. package/src/client/components/workspace/worktree-modal/index.test.tsx +118 -0
  119. package/src/client/components/workspace/worktree-modal/index.tsx +261 -0
  120. package/src/client/hooks/chat/omp/reconnect.test.ts +338 -0
  121. package/src/client/hooks/chat/omp/reconnect.ts +187 -0
  122. package/src/client/hooks/chat/omp/run-liveness.test.ts +71 -0
  123. package/src/client/hooks/chat/omp/run-liveness.ts +65 -0
  124. package/src/client/hooks/chat/omp/session-list.activity.test.ts +114 -0
  125. package/src/client/hooks/chat/omp/session-list.test-util.ts +164 -0
  126. package/src/client/hooks/chat/omp/session-list.test.ts +30 -150
  127. package/src/client/hooks/chat/omp/session-list.tsx +69 -9
  128. package/src/client/hooks/chat/timeline/generating.ts +19 -2
  129. package/src/client/hooks/chat/timeline/index.ts +27 -28
  130. package/src/client/hooks/chat/timeline/lost-run.test.ts +329 -0
  131. package/src/client/hooks/chat/timeline/lost-run.ts +177 -0
  132. package/src/client/hooks/chat/timeline/send-execute.test.ts +2 -0
  133. package/src/client/hooks/chat/timeline/send-steer.test.ts +2 -0
  134. package/src/client/hooks/chat/timeline/send.test.ts +2 -0
  135. package/src/client/hooks/chat/timeline/send.ts +11 -1
  136. package/src/client/hooks/chat/timeline/session-load.test-util.ts +205 -0
  137. package/src/client/hooks/chat/timeline/session-load.test.ts +61 -162
  138. package/src/client/hooks/chat/timeline/session-load.ts +69 -37
  139. package/src/client/hooks/chat/timeline/session-switch.test.ts +65 -0
  140. package/src/client/hooks/chat/timeline/session-switch.ts +63 -0
  141. package/src/client/hooks/chat/timeline/spawn-target.ts +52 -0
  142. package/src/client/hooks/chat/timeline/tool-open.test.ts +74 -0
  143. package/src/client/hooks/chat/timeline/tool-open.ts +52 -8
  144. package/src/client/hooks/code/deferred-highlight.test.ts +182 -0
  145. package/src/client/hooks/code/deferred-highlight.ts +129 -0
  146. package/src/client/hooks/editor/find-state.ts +1 -1
  147. package/src/client/hooks/editor/theme.test.ts +206 -0
  148. package/src/client/hooks/editor/theme.ts +82 -0
  149. package/src/client/hooks/editor/use-editor-drop.test.ts +101 -0
  150. package/src/client/hooks/editor/use-editor-drop.ts +67 -14
  151. package/src/client/hooks/editor/use-editor-find.ts +7 -7
  152. package/src/client/hooks/editor/use-outline-view.test.ts +182 -0
  153. package/src/client/hooks/editor/use-outline-view.ts +94 -7
  154. package/src/client/hooks/editor/use-scroll-restore.test.ts +55 -2
  155. package/src/client/hooks/editor/use-scroll-restore.ts +124 -4
  156. package/src/client/hooks/editor/use-search-reveal.ts +1 -1
  157. package/src/client/hooks/ui/long-press.test.ts +174 -0
  158. package/src/client/hooks/ui/long-press.ts +112 -0
  159. package/src/client/hooks/ui/settings-modal.ts +56 -0
  160. package/src/client/hooks/ui/updates/shell-download.test.ts +75 -0
  161. package/src/client/hooks/ui/updates/shell-download.ts +153 -28
  162. package/src/client/hooks/ui/updates/shell-offer.test.ts +213 -0
  163. package/src/client/hooks/ui/updates/shell-update.test.ts +140 -0
  164. package/src/client/hooks/ui/updates/shell-update.ts +119 -0
  165. package/src/client/hooks/workspace/diff-file.ts +29 -10
  166. package/src/client/hooks/workspace/diff-git-actions.ts +147 -0
  167. package/src/client/hooks/workspace/editor-view.ts +61 -0
  168. package/src/client/hooks/workspace/file-create.ts +41 -11
  169. package/src/client/hooks/workspace/file-listing.ts +56 -1
  170. package/src/client/hooks/workspace/file-tabs.test.ts +62 -5
  171. package/src/client/hooks/workspace/file-tabs.ts +24 -11
  172. package/src/client/hooks/workspace/file-tree-actions.ts +37 -5
  173. package/src/client/hooks/workspace/navbar-session-actions.ts +72 -0
  174. package/src/client/hooks/workspace/worktree/index.ts +165 -0
  175. package/src/client/lib/desktop/bridge.ts +45 -0
  176. package/src/client/lib/search-reveal.ts +1 -1
  177. package/src/client/main.tsx +5 -1
  178. package/src/client/tailwind.css +56 -8
  179. package/src/server/index.ts +7 -2
  180. package/src/server/lib/auth/guard.test.ts +28 -0
  181. package/src/server/lib/auth/guard.ts +10 -0
  182. package/src/server/lib/bundler/titlebar-css.test.ts +42 -1
  183. package/src/server/lib/db/schema.ts +4 -0
  184. package/src/server/lib/db/worktree-registry.server.ts +91 -0
  185. package/src/server/lib/fs/canonical.ts +52 -0
  186. package/src/server/lib/fs/relative-path.ts +48 -0
  187. package/src/server/lib/fs/root-worktree.test.ts +116 -0
  188. package/src/server/lib/fs/root.ts +39 -10
  189. package/src/server/lib/fs/worktree/carry.ts +134 -0
  190. package/src/server/lib/fs/worktree/create.test.ts +249 -0
  191. package/src/server/lib/fs/worktree/create.ts +262 -0
  192. package/src/server/lib/fs/worktree/errors.ts +43 -0
  193. package/src/server/lib/fs/worktree/list.test.ts +207 -0
  194. package/src/server/lib/fs/worktree/list.ts +142 -0
  195. package/src/server/lib/fs/worktree/location.test.ts +206 -0
  196. package/src/server/lib/fs/worktree/location.ts +114 -0
  197. package/src/server/lib/fs/worktree/project-id.ts +53 -0
  198. package/src/server/lib/fs/worktree/remove.test.ts +242 -0
  199. package/src/server/lib/fs/worktree/remove.ts +152 -0
  200. package/src/server/lib/fs/worktree/settings.server.ts +56 -0
  201. package/src/server/lib/fs/worktree/topology.ts +142 -0
  202. package/src/server/lib/lifecycle/claim-port.test.ts +156 -0
  203. package/src/server/lib/lifecycle/port-guard.test.ts +20 -164
  204. package/src/server/lib/lifecycle/port-guard.ts +39 -7
  205. package/src/server/lib/lifecycle/probe-health.test.ts +166 -0
  206. package/src/server/lib/lifecycle/probe.test.ts +77 -164
  207. package/src/server/lib/lifecycle/probe.ts +121 -13
  208. package/src/server/lib/omp/rpc/frame-fold-signals.test.ts +112 -0
  209. package/src/server/lib/omp/rpc/frame-fold.ts +10 -5
  210. package/src/server/lib/omp/session/sidebar-activity.server.test.ts +162 -0
  211. package/src/server/lib/omp/session/sidebar-activity.server.ts +70 -0
  212. package/src/server/lib/omp/session/sidebar-data.canonical.test.ts +155 -0
  213. package/src/server/lib/omp/session/sidebar-data.server.ts +28 -1
  214. package/src/server/lib/omp/session/telemetry/compute.ts +13 -1
  215. package/src/server/lib/omp/session/telemetry.test.ts +33 -0
  216. package/src/server/lib/panels/local-edits.server.ts +33 -0
  217. package/src/server/lib/panels/sync.fixture.test-util.ts +92 -0
  218. package/src/server/lib/panels/sync.local-edits.test.ts +150 -0
  219. package/src/server/lib/panels/sync.server.test.ts +125 -147
  220. package/src/server/lib/panels/sync.server.ts +153 -48
  221. package/src/server/lib/realtime/signals.server.ts +10 -0
  222. package/src/server/lib/realtime/topics/catalog.server.ts +4 -1
  223. package/src/server/lib/realtime/topics.server.ts +16 -0
  224. package/src/server/lib/updates/shell-assets.test.ts +114 -0
  225. package/src/server/lib/updates/shell-assets.ts +200 -0
  226. package/src/server/lib/updates/shell-cache.ts +105 -0
  227. package/src/server/lib/updates/shell-pages.test.ts +187 -0
  228. package/src/server/lib/updates/shell-pages.ts +157 -0
  229. package/src/server/lib/updates/shell-release-desktop.test.ts +166 -0
  230. package/src/server/lib/updates/shell-release.test.ts +59 -71
  231. package/src/server/lib/updates/shell-release.ts +178 -198
  232. package/src/server/routes/fs/action-create.test.ts +18 -13
  233. package/src/server/routes/fs/action-nested.test.ts +184 -0
  234. package/src/server/routes/fs/action.ts +47 -16
  235. package/src/server/routes/fs/git-merge.test.ts +11 -1
  236. package/src/server/routes/fs/read.test.ts +24 -7
  237. package/src/server/routes/fs/scope.test.ts +15 -10
  238. package/src/server/routes/fs/scope.ts +3 -14
  239. package/src/server/routes/index.ts +2 -0
  240. package/src/server/routes/updates/android.ts +6 -1
  241. package/src/server/routes/updates/desktop.ts +59 -0
  242. package/src/server/routes/updates/index.ts +2 -0
  243. package/src/server/routes/updates/ios.ts +5 -1
  244. package/src/server/routes/worktrees/create.ts +138 -0
  245. package/src/server/routes/worktrees/index.ts +25 -0
  246. package/src/server/routes/worktrees/list.ts +134 -0
  247. package/src/server/routes/worktrees/remove.ts +102 -0
  248. package/src/server/routes/worktrees/routes.test.ts +326 -0
  249. package/src/server/routes/worktrees/shared.ts +90 -0
  250. package/src/shared/lib/chat/timeline/omp-callbacks.test.ts +36 -0
  251. package/src/shared/lib/chat/timeline/omp-callbacks.ts +18 -0
  252. package/src/shared/lib/chat/timeline/run-signals.test.ts +2 -0
  253. package/src/shared/lib/code/editor/comment-syntax.test.ts +37 -0
  254. package/src/shared/lib/code/editor/comment-syntax.ts +55 -0
  255. package/src/shared/lib/code/editor/find-replace.test.ts +134 -0
  256. package/src/shared/lib/code/editor/{find.ts → find-replace.ts} +23 -107
  257. package/src/shared/lib/code/editor/{find.test.ts → find-scan.test.ts} +19 -73
  258. package/src/shared/lib/code/editor/find-scan.ts +177 -0
  259. package/src/shared/lib/code/editor/keymap.ts +3 -3
  260. package/src/shared/lib/code/editor/lines.test.ts +7 -55
  261. package/src/shared/lib/code/editor/lines.ts +8 -48
  262. package/src/shared/lib/code/editor/marks.test.ts +4 -5
  263. package/src/shared/lib/code/editor/marks.ts +13 -25
  264. package/src/shared/lib/code/editor/theme-loader.ts +97 -0
  265. package/src/shared/lib/code/editor/themes.generated.ts +156 -0
  266. package/src/shared/lib/code/editor/themes.test.ts +165 -0
  267. package/src/shared/lib/code/editor/themes.ts +165 -0
  268. package/src/shared/lib/code/highlight-memo.test.ts +103 -0
  269. package/src/shared/lib/code/highlight-memo.ts +149 -0
  270. package/src/shared/lib/code/highlighter-warmup.ts +112 -0
  271. package/src/shared/lib/code/highlighter.ts +3 -1
  272. package/src/shared/lib/code/idle.ts +37 -0
  273. package/src/shared/lib/code/outline/rows.test.ts +96 -0
  274. package/src/shared/lib/code/outline/rows.ts +94 -0
  275. package/src/shared/lib/code/syntax-highlight.test.ts +1 -79
  276. package/src/shared/lib/code/syntax-highlight.ts +2 -3
  277. package/src/shared/lib/fs/file-kind-classify.test.ts +25 -11
  278. package/src/shared/lib/fs/file-kind.ts +47 -13
  279. package/src/shared/lib/fs/file-tree.test.ts +183 -0
  280. package/src/shared/lib/fs/file-tree.ts +159 -0
  281. package/src/shared/lib/fs/file-url.test.ts +52 -0
  282. package/src/shared/lib/fs/file-url.ts +37 -0
  283. package/src/shared/lib/fs/patch-sides.test.ts +114 -0
  284. package/src/shared/lib/fs/patch-sides.ts +139 -0
  285. package/src/shared/lib/fs/paths.test.ts +24 -1
  286. package/src/shared/lib/fs/paths.ts +17 -0
  287. package/src/shared/lib/fs/worktree/settings-keys.ts +18 -0
  288. package/src/shared/lib/omp/session/prewarm.ts +7 -0
  289. package/src/shared/lib/realtime/protocol.ts +16 -0
  290. package/src/shared/lib/ui/toolbar-overflow.test.ts +83 -0
  291. package/src/shared/lib/ui/toolbar-overflow.ts +86 -0
  292. package/src/shared/lib/updates/shell-arch.ts +54 -0
  293. package/src/shared/lib/updates/shell-ua.ts +24 -4
  294. package/src/shared/lib/workspace/active-project.ts +23 -0
  295. package/src/shared/lib/workspace/editor-view-mode.test.ts +70 -0
  296. package/src/shared/lib/workspace/editor-view-mode.ts +48 -0
  297. package/src/shared/lib/workspace/panel-widths.test.ts +30 -0
  298. package/src/shared/lib/workspace/panel-widths.ts +25 -5
  299. package/src/shared/lib/workspace/session-state/cache.ts +159 -0
  300. package/src/shared/lib/workspace/session-state/harness.test-util.ts +55 -0
  301. package/src/shared/lib/workspace/session-state/merge.ts +100 -0
  302. package/src/shared/lib/workspace/session-state/store-load.test.ts +206 -0
  303. package/src/shared/lib/workspace/session-state/store.test.ts +17 -139
  304. package/src/shared/lib/workspace/session-state/store.ts +92 -130
  305. package/src/shared/lib/workspace/workspace-modules.test.ts +49 -1
  306. package/src/shared/lib/workspace/workspace-panel-widths.ts +124 -0
  307. package/src/shared/lib/workspace/worktree/branch-name.test.ts +95 -0
  308. package/src/shared/lib/workspace/worktree/branch-name.ts +115 -0
  309. package/src/shared/lib/workspace/worktree/types.ts +104 -0
  310. package/src/shared/types/fs.ts +10 -0
  311. package/src/shared/types/realtime.ts +29 -0
  312. package/src/shared/types/settings/state.ts +10 -0
  313. package/src/shared/types/workspace.ts +14 -0
  314. package/src/test-support/file-explorer-disk.test-util.ts +76 -0
  315. package/src/test-support/file-explorer.test-util.ts +71 -40
  316. package/src/test-support/loopback.ts +183 -0
  317. package/dist/client/static/js/index-nzyjf92m.css +0 -1
  318. package/src/client/components/common/code-editor/handler.test.ts +0 -274
  319. package/src/client/components/common/code-editor/handler.ts +0 -175
  320. package/src/client/components/common/code-editor/index.tsx +0 -314
  321. package/src/client/components/common/code-surface/measure.ts +0 -148
  322. package/src/client/components/workspace/diff-panel/SplitView.tsx +0 -209
  323. package/src/client/components/workspace/diff-panel/UnifiedView.tsx +0 -150
  324. package/src/client/hooks/editor/scroll-container.test.ts +0 -102
  325. package/src/client/hooks/editor/scroll-container.ts +0 -26
  326. package/src/client/hooks/editor/use-code-window.ts +0 -100
  327. package/src/client/hooks/editor/use-editor-history.test.ts +0 -216
  328. package/src/client/hooks/editor/use-editor-history.ts +0 -134
  329. package/src/client/hooks/editor/use-occurrences.test.ts +0 -236
  330. package/src/client/hooks/editor/use-occurrences.ts +0 -98
  331. package/src/shared/lib/code/editor/commands.test.ts +0 -155
  332. package/src/shared/lib/code/editor/commands.ts +0 -260
  333. package/src/shared/lib/code/editor/edits.test.ts +0 -100
  334. package/src/shared/lib/code/editor/edits.ts +0 -122
  335. package/src/shared/lib/code/editor/history.test.ts +0 -118
  336. package/src/shared/lib/code/editor/history.ts +0 -101
  337. package/src/shared/lib/code/editor/layers.test.ts +0 -77
  338. package/src/shared/lib/code/editor/layers.ts +0 -67
  339. package/src/shared/lib/code/editor/line-edits.test.ts +0 -131
  340. package/src/shared/lib/code/editor/line-edits.ts +0 -159
  341. package/src/shared/lib/code/editor/occurrences.test.ts +0 -117
  342. package/src/shared/lib/code/editor/occurrences.ts +0 -111
  343. package/src/shared/lib/code/editor/reset-css.test.ts +0 -35
  344. package/src/shared/lib/code/editor/reset-css.ts +0 -32
  345. package/src/shared/lib/code/editor/text-commands.test.ts +0 -178
  346. package/src/shared/lib/code/editor/text-commands.ts +0 -154
  347. package/src/shared/lib/code/lazy-window.test.ts +0 -161
  348. package/src/shared/lib/code/lazy-window.ts +0 -156
  349. package/src/shared/lib/code/windowed-highlight.ts +0 -114
  350. package/src/shared/lib/fs/diff-parser.test.ts +0 -116
  351. package/src/shared/lib/fs/diff-parser.ts +0 -213
@@ -0,0 +1,319 @@
1
+ /**
2
+ * @license
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ /**
7
+ * The composer's WORKTREE picker, rendered.
8
+ *
9
+ * These are the cases a reader would otherwise meet in a browser and nowhere
10
+ * else:
11
+ *
12
+ * - a workspace that is not a git repository gets NO control (a permanently
13
+ * disabled one in every notes folder is noise, and a click that can only
14
+ * refuse is worse);
15
+ * - the trigger names the BRANCH, which is why the list is read on mount and
16
+ * not only when the menu opens;
17
+ * - the menu's groups are the three the plan names, with creating ABOVE the
18
+ * list so a long list cannot hide it;
19
+ * - the project root row clears the worktree pick.
20
+ */
21
+
22
+ import { afterAll, afterEach, beforeAll, describe, expect, test } from 'bun:test';
23
+ import { Window } from 'happy-dom';
24
+ import { h, render } from 'preact';
25
+ import { act } from 'preact/test-utils';
26
+ import { installDomGlobals, restoreDomGlobals } from '@/test-support/pristine-globals';
27
+ import {
28
+ WorktreePicker,
29
+ selectedWorktree,
30
+ worktreeLabel,
31
+ } from '@/client/components/workspace/chat-timeline/target-picker/WorktreePicker';
32
+ import type { WorktreeInfo } from '@/shared/lib/workspace/worktree/types';
33
+ import type { WorkspaceFolderData } from '@/shared/types';
34
+
35
+ const folder: WorkspaceFolderData = {
36
+ id: 1,
37
+ name: 'kabmo',
38
+ project_path: '/repo',
39
+ isExpanded: true,
40
+ hasMore: false,
41
+ totalSessions: 0,
42
+ sessions: [],
43
+ };
44
+
45
+ const row = (overrides: Partial<WorktreeInfo>): WorktreeInfo => ({
46
+ path: '/data/worktrees/x/main',
47
+ branch: 'main',
48
+ head: 'abc1234',
49
+ detached: false,
50
+ missing: false,
51
+ isPrimary: true,
52
+ dirty: 0,
53
+ aheadOfStart: 0,
54
+ startRef: '',
55
+ sessions: 0,
56
+ ...overrides,
57
+ });
58
+
59
+ const primary = row({});
60
+ const made = row({
61
+ path: '/data/worktrees/x/cosmic-dolphin',
62
+ branch: 'wt/cosmic-dolphin',
63
+ isPrimary: false,
64
+ dirty: 2,
65
+ });
66
+
67
+ const nativeFetch = Bun.fetch;
68
+ const mounted: HTMLElement[] = [];
69
+
70
+ beforeAll(() => {
71
+ installDomGlobals(new Window({ url: 'http://localhost' }));
72
+ });
73
+
74
+ afterEach(() => {
75
+ for (const host of mounted.splice(0)) {
76
+ render(null, host);
77
+ host.remove();
78
+ }
79
+ (globalThis as unknown as Record<string, unknown>).fetch = nativeFetch;
80
+ });
81
+
82
+ afterAll(() => {
83
+ restoreDomGlobals();
84
+ });
85
+
86
+ async function mount(node: h.JSX.Element): Promise<HTMLElement> {
87
+ const host = document.createElement('div');
88
+ document.body.appendChild(host);
89
+ await act(async () => {
90
+ render(node, host);
91
+ });
92
+ mounted.push(host);
93
+ return host;
94
+ }
95
+
96
+ /** Answer the list read without a server. `isRepo` is the whole point here. */
97
+ function stubList(worktrees: WorktreeInfo[], isRepo = true): string[] {
98
+ const calls: string[] = [];
99
+ (globalThis as unknown as Record<string, unknown>).fetch = async (input: RequestInfo | URL) => {
100
+ calls.push(String(input));
101
+ const payload = { worktrees, base: '/data/worktrees/x', isRepo };
102
+ return { ok: true, json: async () => payload } as unknown as Response;
103
+ };
104
+ return calls;
105
+ }
106
+
107
+ /**
108
+ * A stub whose LIST grows once a create has answered — the state a real control
109
+ * is in the moment after it makes a worktree.
110
+ */
111
+ function stubCreate(listBefore: WorktreeInfo[], created: WorktreeInfo): string[] {
112
+ const calls: string[] = [];
113
+ let created_ = false;
114
+ (globalThis as unknown as Record<string, unknown>).fetch = async (input: RequestInfo | URL, init?: RequestInit) => {
115
+ calls.push(`${init?.method ?? 'GET'} ${String(input)}`);
116
+ if (init?.method === 'POST') {
117
+ created_ = true;
118
+ return { ok: true, json: async () => ({ worktree: created }) } as unknown as Response;
119
+ }
120
+ const worktrees = created_ ? [...listBefore, created] : listBefore;
121
+ return {
122
+ ok: true,
123
+ json: async () => ({ worktrees, base: '/data/worktrees/x', isRepo: true }),
124
+ } as unknown as Response;
125
+ };
126
+ return calls;
127
+ }
128
+
129
+ /** Drain the fetch promise chain; each empty `act` is one round. */
130
+ async function settle(): Promise<void> {
131
+ for (let round = 0; round < 10; round += 1) await act(async () => {});
132
+ }
133
+
134
+ function textOf(host: HTMLElement): string {
135
+ return (host.textContent ?? '').replace(/\s+/g, ' ').trim();
136
+ }
137
+
138
+ describe('worktreeLabel', () => {
139
+ test('the branch the project root holds, not the word "root"', () => {
140
+ expect(worktreeLabel([primary], null)).toBe('main');
141
+ });
142
+
143
+ test('the picked worktree’s branch', () => {
144
+ expect(worktreeLabel([primary, made], { folderId: 1, worktreePath: made.path })).toBe('wt/cosmic-dolphin');
145
+ });
146
+
147
+ test('a pick the list has not caught up with still names something', () => {
148
+ // The dialog selects the new worktree the moment it is created, and the
149
+ // list that answers the next render may predate it.
150
+ expect(worktreeLabel([primary], { folderId: 1, worktreePath: '/data/worktrees/x/just-made' })).toBe('just-made');
151
+ });
152
+
153
+ test('nothing read yet falls back to the words a reader can act on', () => {
154
+ expect(worktreeLabel([], null)).toBe('Project root');
155
+ });
156
+ });
157
+
158
+ describe('selectedWorktree', () => {
159
+ test('the row the pick names, and null for the root', () => {
160
+ expect(selectedWorktree([primary, made], { folderId: 1, worktreePath: made.path })?.branch).toBe('wt/cosmic-dolphin');
161
+ expect(selectedWorktree([primary, made], null)).toBeNull();
162
+ });
163
+ });
164
+
165
+ describe('WorktreePicker', () => {
166
+ test('a workspace that is not a git repository draws no control', async () => {
167
+ const calls = stubList([], false);
168
+ const host = await mount(
169
+ h(WorktreePicker, {
170
+ folders: [folder],
171
+ selectedFolderId: 1,
172
+ target: null,
173
+ setTarget: () => {},
174
+ }),
175
+ );
176
+ await settle();
177
+
178
+ // The read happened — the answer is what suppresses the control, not a
179
+ // guess about the folder's shape.
180
+ expect(calls.length).toBeGreaterThan(0);
181
+ expect(host.querySelector('button')).toBeNull();
182
+ });
183
+
184
+ test('the trigger names the root branch, read before the menu is opened', async () => {
185
+ stubList([primary, made]);
186
+ const host = await mount(
187
+ h(WorktreePicker, {
188
+ folders: [folder],
189
+ selectedFolderId: 1,
190
+ target: null,
191
+ setTarget: () => {},
192
+ }),
193
+ );
194
+ await settle();
195
+
196
+ const trigger = host.querySelector('button') as HTMLButtonElement | null;
197
+ expect(trigger?.textContent).toContain('main');
198
+ });
199
+
200
+ test('the menu offers the project root, both create paths, and the list', async () => {
201
+ stubList([primary, made]);
202
+ const host = await mount(
203
+ h(WorktreePicker, {
204
+ folders: [folder],
205
+ selectedFolderId: 1,
206
+ target: null,
207
+ setTarget: () => {},
208
+ }),
209
+ );
210
+ await settle();
211
+
212
+ await act(async () => {
213
+ (host.querySelector('button') as HTMLButtonElement).click();
214
+ });
215
+ await settle();
216
+
217
+ const text = textOf(host);
218
+ expect(text).toContain('Project root');
219
+ expect(text).toContain('Quick worktree');
220
+ expect(text).toContain('New worktree…');
221
+ expect(text).toContain('Worktrees');
222
+ expect(text).toContain('wt/cosmic-dolphin');
223
+ // Creating sits above the LIST: a project with many worktrees must not push
224
+ // the action that makes one out of sight.
225
+ expect(text.indexOf('Quick worktree')).toBeLessThan(text.indexOf('wt/cosmic-dolphin'));
226
+ });
227
+
228
+ test('the project root row clears the worktree pick', async () => {
229
+ stubList([primary, made]);
230
+ const picks: Array<{ folderId: number | null; worktreePath: string | null } | null> = [];
231
+ const host = await mount(
232
+ h(WorktreePicker, {
233
+ folders: [folder],
234
+ selectedFolderId: 1,
235
+ target: { folderId: 1, worktreePath: made.path },
236
+ setTarget: (next) => picks.push(next),
237
+ }),
238
+ );
239
+ await settle();
240
+
241
+ await act(async () => {
242
+ (host.querySelector('button') as HTMLButtonElement).click();
243
+ });
244
+ await settle();
245
+
246
+ const rows = [...host.querySelectorAll('[role="menuitemradio"]')] as HTMLButtonElement[];
247
+ // Root first, then the worktree.
248
+ expect(rows.length).toBe(2);
249
+ await act(async () => {
250
+ rows[0]?.click();
251
+ });
252
+
253
+ expect(picks).toEqual([{ folderId: 1, worktreePath: null }]);
254
+ });
255
+
256
+ test('a picked worktree marks its own row and names itself on the trigger', async () => {
257
+ stubList([primary, made]);
258
+ const host = await mount(
259
+ h(WorktreePicker, {
260
+ folders: [folder],
261
+ selectedFolderId: 1,
262
+ target: { folderId: 1, worktreePath: made.path },
263
+ setTarget: () => {},
264
+ }),
265
+ );
266
+ await settle();
267
+
268
+ expect((host.querySelector('button') as HTMLButtonElement).textContent).toContain('wt/cosmic-dolphin');
269
+ // The dirty count is on the trigger too: a chat about to run in a checkout
270
+ // with two changed files should say so before the first prompt.
271
+ expect(host.querySelector('[aria-label="2 uncommitted files in this checkout"]')).not.toBeNull();
272
+ });
273
+
274
+ test('a create re-reads the list, so the trigger names the branch not the folder', async () => {
275
+ // The pick names a path the list has never seen — exactly the state after a
276
+ // create. `worktreeLabel` falls back to the folder's last segment there, so
277
+ // the trigger read `groovy-yak` where the branch is `wt/groovy-yak`, and it
278
+ // kept reading that until the menu was opened again (measured in the
279
+ // browser). The list is what the label is derived from, so the create has to
280
+ // re-read it.
281
+ const fresh = row({
282
+ path: '/data/worktrees/x/groovy-yak',
283
+ branch: 'wt/groovy-yak',
284
+ isPrimary: false,
285
+ });
286
+ const calls = stubCreate([primary], fresh);
287
+ const picks: Array<{ folderId: number | null; worktreePath: string | null } | null> = [];
288
+ const host = await mount(
289
+ h(WorktreePicker, {
290
+ folders: [folder],
291
+ selectedFolderId: 1,
292
+ target: null,
293
+ setTarget: (next) => picks.push(next),
294
+ }),
295
+ );
296
+ await settle();
297
+
298
+ await act(async () => {
299
+ (host.querySelector('button') as HTMLButtonElement).click();
300
+ });
301
+ await settle();
302
+
303
+ const quick = [...host.querySelectorAll('[role="menuitem"]')].find((node) =>
304
+ (node.textContent ?? '').includes('Quick worktree'),
305
+ ) as HTMLButtonElement | undefined;
306
+ expect(quick).toBeDefined();
307
+ await act(async () => {
308
+ quick?.click();
309
+ });
310
+ await settle();
311
+
312
+ expect(picks).toEqual([{ folderId: 1, worktreePath: fresh.path }]);
313
+ // The read AFTER the POST is the one that matters; without it the label is
314
+ // `groovy-yak`.
315
+ const postIndex = calls.findIndex((call) => call.startsWith('POST'));
316
+ expect(postIndex).toBeGreaterThanOrEqual(0);
317
+ expect(calls.slice(postIndex + 1).some((call) => call.startsWith('GET'))).toBe(true);
318
+ });
319
+ });
@@ -0,0 +1,285 @@
1
+ /**
2
+ * @license
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ /**
7
+ * WHICH CHECKOUT of the workspace the chat runs in: the project root, or one of
8
+ * its git worktrees.
9
+ *
10
+ * Its own control, not a group inside the workspace picker. The two answer
11
+ * different questions — which project, and which checkout of it — and the merged
12
+ * menu made both harder: a user looking for another project scrolled through
13
+ * branch names, and the worktree rows had no trigger of their own to be found
14
+ * from. OpenChamber's composer draws the same pair, and its worktree `Select`
15
+ * carries the branch label on the trigger, which is what this one does too.
16
+ *
17
+ * The control is ABSENT for a workspace that is not a git repository: there is
18
+ * nothing to pick and nothing to create, so a permanently disabled control in
19
+ * every notes folder would be noise. `isRepo` comes from the list payload, so
20
+ * the decision is the server's, not a guess from the folder's shape.
21
+ *
22
+ * The pick is `{folderId, worktreePath}`; the folder is what the workspace
23
+ * picker chose, and this control only ever moves the second half.
24
+ */
25
+
26
+ import { useEffect, useMemo, useRef, useState } from 'preact/hooks';
27
+ import { AlertTriangle, Check, ChevronDown, GitBranch } from 'lucide-preact';
28
+ import { useOnClickOutside } from '@/client/hooks/ui/on-click-outside';
29
+ import { createWorktree, useWorktrees } from '@/client/hooks/workspace/worktree/index';
30
+ import { NewWorktreeModal } from '@/client/components/workspace/worktree-modal/index';
31
+ import type { SpawnTarget } from '@/client/hooks/chat/timeline/spawn-target';
32
+ import type { WorktreeInfo } from '@/shared/lib/workspace/worktree/types';
33
+ import { lastPathSegment } from '@/shared/lib/fs/paths';
34
+ import type { WorkspaceFolderData } from '@/shared/types';
35
+
36
+ export interface WorktreePickerProps {
37
+ folders: WorkspaceFolderData[];
38
+ selectedFolderId: number | null;
39
+ target: SpawnTarget | null;
40
+ setTarget: (next: SpawnTarget | null) => void;
41
+ /** Reports a completed create, or the refusal that stopped one. */
42
+ onReport?: (message: string) => void;
43
+ }
44
+
45
+ /** The worktree row the pick names, or null for the project root. */
46
+ export function selectedWorktree(worktrees: WorktreeInfo[], target: SpawnTarget | null): WorktreeInfo | null {
47
+ if (!target?.worktreePath) return null;
48
+ return worktrees.find((worktree) => worktree.path === target.worktreePath) ?? null;
49
+ }
50
+
51
+ /**
52
+ * The trigger's words: the branch of the checkout the chat will run in.
53
+ *
54
+ * The project root is named by the branch it holds rather than by the word
55
+ * "root", which is what OpenChamber shows and what a reader needs: the root of a
56
+ * repository sitting on `main` and one sitting on `wt/scratch` are different
57
+ * places to start a chat. `Project root` remains the fallback for a payload that
58
+ * has not arrived, or a repository git cannot read a branch from.
59
+ */
60
+ export function worktreeLabel(worktrees: WorktreeInfo[], target: SpawnTarget | null): string {
61
+ const picked = selectedWorktree(worktrees, target);
62
+ if (target?.worktreePath) {
63
+ if (!picked) return lastPathSegment(target.worktreePath) || 'Worktree';
64
+ return picked.branch || (picked.detached ? 'detached' : lastPathSegment(picked.path) || 'Worktree');
65
+ }
66
+ const primary = worktrees.find((worktree) => worktree.isPrimary);
67
+ return primary?.branch || 'Project root';
68
+ }
69
+
70
+ export function WorktreePicker({ folders, selectedFolderId, target, setTarget, onReport }: WorktreePickerProps) {
71
+ const [open, setOpen] = useState(false);
72
+ const [creating, setCreating] = useState(false);
73
+ const [dialogOpen, setDialogOpen] = useState(false);
74
+ const containerRef = useRef<HTMLDivElement>(null);
75
+ useOnClickOutside(containerRef, () => setOpen(false));
76
+
77
+ const folder = folders.find((candidate) => String(candidate.id) === String(selectedFolderId)) ?? null;
78
+ const scopeRoot = folder?.project_path ?? null;
79
+ const folderId = folder ? Number(folder.id) : null;
80
+
81
+ // Read on MOUNT, not only on open: the trigger's own label is the branch the
82
+ // project root holds, and whether this control exists at all is the payload's
83
+ // `isRepo`. Measured cost of the read on this repository: ~150 ms, one spawn
84
+ // per worktree for its dirty count.
85
+ const { worktrees, isRepo, error, refresh } = useWorktrees(
86
+ { root: scopeRoot, folderId },
87
+ Boolean(scopeRoot),
88
+ );
89
+
90
+ // Re-read when the menu opens, so a worktree made in another tab or by the
91
+ // dialog is in the list the user is looking at.
92
+ useEffect(() => {
93
+ if (open) void refresh();
94
+ }, [open, refresh]);
95
+
96
+ // A pick is cleared by the WORKSPACE picker's own choice, never by an effect
97
+ // comparing `target.folderId` with the selected folder: on a reload the target
98
+ // is restored from the session's UI state a beat before the folder resolves,
99
+ // and a mismatch check in that window wiped the very pick it was protecting.
100
+
101
+ const list = useMemo(() => worktrees.filter((worktree) => !worktree.isPrimary), [worktrees]);
102
+ const picked = selectedWorktree(worktrees, target);
103
+ const primary = worktrees.find((worktree) => worktree.isPrimary) ?? null;
104
+ const dirty = (picked ?? primary)?.dirty ?? 0;
105
+
106
+ const pickRoot = () => {
107
+ if (folderId !== null) setTarget({ folderId, worktreePath: null });
108
+ setOpen(false);
109
+ };
110
+
111
+ const pickWorktree = (worktreePath: string) => {
112
+ if (folderId !== null) setTarget({ folderId, worktreePath });
113
+ setOpen(false);
114
+ };
115
+
116
+ /**
117
+ * Quick worktree: a generated branch and folder, created and selected without
118
+ * leaving the draft. Reported rather than silent — the create takes a moment
119
+ * and a pick that changed with no word reads as a mis-click.
120
+ */
121
+ const quickWorktree = async () => {
122
+ if (!scopeRoot || folderId === null) return;
123
+ setCreating(true);
124
+ try {
125
+ const created = await createWorktree({ root: scopeRoot, folderId }, { mode: 'new' });
126
+ pickWorktree(created.path);
127
+ // The list this control reads is the one that was fetched when the menu
128
+ // opened — before the worktree existed. Without this read the pick names a
129
+ // path no row holds, `worktreeLabel` falls back to the folder's last
130
+ // segment, and the trigger says `groovy-yak` where the checkout's branch
131
+ // is `wt/groovy-yak` (measured in the browser, and for the whole time the
132
+ // menu stays closed, which is when the label matters).
133
+ await refresh();
134
+ onReport?.(`Worktree ${created.branch || lastPathSegment(created.path)} created`);
135
+ } catch (failure) {
136
+ onReport?.(failure instanceof Error ? failure.message : String(failure));
137
+ } finally {
138
+ setCreating(false);
139
+ }
140
+ };
141
+
142
+ // Not a repository (or not read yet): no control at all. Rendering one that
143
+ // can only refuse would promise a worktree a plain folder cannot hold.
144
+ if (!scopeRoot || isRepo !== true) return null;
145
+
146
+ return (
147
+ <div className="relative shrink-0" ref={containerRef}>
148
+ <button
149
+ type="button"
150
+ aria-label={`Worktree: ${worktreeLabel(worktrees, target)}`}
151
+ aria-haspopup="menu"
152
+ aria-expanded={open}
153
+ onClick={() => setOpen((value) => !value)}
154
+ className="group flex items-center space-x-1.5 px-1.5 py-1 rounded-md text-xs font-medium text-ink/75 hover:text-ink hover:bg-ink/5 focus-visible:bg-ink/5 transition-all outline-none cursor-pointer select-none"
155
+ >
156
+ {dirty > 0 ? (
157
+ <AlertTriangle
158
+ size={13}
159
+ className="text-warning flex-shrink-0"
160
+ aria-label={`${dirty} uncommitted file${dirty === 1 ? '' : 's'} in this checkout`}
161
+ />
162
+ ) : (
163
+ <GitBranch size={13} className="text-ink/50 group-hover:text-ink/80 transition-colors flex-shrink-0" />
164
+ )}
165
+ <span className="max-w-[240px] truncate tracking-tight">{worktreeLabel(worktrees, target)}</span>
166
+ <ChevronDown
167
+ size={13}
168
+ className={`text-ink/40 group-hover:text-ink/70 flex-shrink-0 transition-transform duration-150 ${open ? 'rotate-180 text-ink/70' : ''}`}
169
+ />
170
+ </button>
171
+
172
+ {open && (
173
+ <div
174
+ role="menu"
175
+ className="absolute bottom-full left-0 mb-1.5 w-72 bg-paper border border-ink/10 rounded-lg shadow-lg z-50 flex flex-col overflow-hidden text-xs py-1"
176
+ >
177
+ <div className="px-3 py-1.5 text-[10px] uppercase tracking-wider text-ink/40 font-semibold border-b border-ink/5">
178
+ Project root
179
+ </div>
180
+ <button
181
+ type="button"
182
+ role="menuitemradio"
183
+ aria-checked={!target?.worktreePath}
184
+ onClick={pickRoot}
185
+ title={primary?.path ?? scopeRoot}
186
+ className={`w-full text-left px-3 py-1.5 flex items-center justify-between transition-colors cursor-pointer ${
187
+ !target?.worktreePath ? 'bg-ink/10 text-ink font-semibold' : 'text-ink/80 hover:bg-ink/5 hover:text-ink'
188
+ }`}
189
+ >
190
+ <span className="flex items-center gap-2 truncate pr-2">
191
+ <GitBranch size={12} className="text-ink/50 flex-shrink-0" />
192
+ <span className="truncate font-medium">{primary?.branch || folder?.name || 'Project root'}</span>
193
+ {primary && primary.dirty > 0 && (
194
+ <span className="text-ink/40 shrink-0" title={`${primary.dirty} uncommitted files`}>
195
+ ●{primary.dirty}
196
+ </span>
197
+ )}
198
+ </span>
199
+ {!target?.worktreePath && <Check size={12} className="text-ink flex-shrink-0" />}
200
+ </button>
201
+
202
+ {/* Making one comes before the list, so a long list never hides it. */}
203
+ <div className="my-1 border-t border-ink/5" />
204
+ <button
205
+ type="button"
206
+ role="menuitem"
207
+ disabled={creating}
208
+ onClick={() => void quickWorktree()}
209
+ className="w-full text-left px-3 py-1.5 flex items-center gap-2 text-ink/80 hover:bg-ink/5 hover:text-ink disabled:opacity-50"
210
+ >
211
+ <span aria-hidden="true">⚡</span>
212
+ {creating ? 'Creating…' : 'Quick worktree'}
213
+ </button>
214
+ <button
215
+ type="button"
216
+ role="menuitem"
217
+ onClick={() => {
218
+ setOpen(false);
219
+ setDialogOpen(true);
220
+ }}
221
+ className="w-full text-left px-3 py-1.5 flex items-center gap-2 text-ink/80 hover:bg-ink/5 hover:text-ink"
222
+ >
223
+ <GitBranch size={12} className="text-ink/50" />
224
+ New worktree…
225
+ </button>
226
+
227
+ <div className="my-1 border-t border-ink/5" />
228
+ <div className="px-3 py-1.5 text-[10px] uppercase tracking-wider text-ink/40 font-semibold">Worktrees</div>
229
+ <div className="max-h-56 scrollbar-overlay-container py-1">
230
+ {list.length === 0 ? (
231
+ <div className="px-3 py-2 text-ink/40 italic">
232
+ {error ? 'Worktrees could not be read' : 'No worktrees yet'}
233
+ </div>
234
+ ) : (
235
+ list.map((worktree) => {
236
+ const active = target?.worktreePath === worktree.path;
237
+ return (
238
+ <button
239
+ key={worktree.path}
240
+ type="button"
241
+ role="menuitemradio"
242
+ aria-checked={active}
243
+ onClick={() => pickWorktree(worktree.path)}
244
+ title={worktree.path}
245
+ className={`w-full text-left px-3 py-1.5 flex items-center justify-between transition-colors cursor-pointer ${
246
+ active ? 'bg-ink/10 text-ink font-semibold' : 'text-ink/80 hover:bg-ink/5 hover:text-ink'
247
+ }`}
248
+ >
249
+ <span className="flex items-center gap-2 truncate pr-2">
250
+ <GitBranch size={12} className="text-ink/50 flex-shrink-0" />
251
+ <span className="truncate font-medium">{worktree.branch || 'detached'}</span>
252
+ {worktree.missing && <span className="text-warning shrink-0">missing</span>}
253
+ {worktree.dirty > 0 && (
254
+ <span className="text-ink/40 shrink-0" title={`${worktree.dirty} uncommitted files`}>
255
+ ●{worktree.dirty}
256
+ </span>
257
+ )}
258
+ </span>
259
+ {active && <Check size={12} className="text-ink flex-shrink-0" />}
260
+ </button>
261
+ );
262
+ })
263
+ )}
264
+ </div>
265
+ </div>
266
+ )}
267
+
268
+ <NewWorktreeModal
269
+ isOpen={dialogOpen}
270
+ onClose={() => setDialogOpen(false)}
271
+ root={scopeRoot}
272
+ folderId={folderId}
273
+ folderName={folder?.name}
274
+ onCreated={(created) => {
275
+ pickWorktree(created.path);
276
+ // Same reason as the quick path: the pick names a worktree the list
277
+ // this control holds has never seen, so the trigger would fall back to
278
+ // the folder name until the menu was opened again.
279
+ void refresh();
280
+ onReport?.(`Worktree ${created.branch} created`);
281
+ }}
282
+ />
283
+ </div>
284
+ );
285
+ }
@@ -40,6 +40,8 @@ function mountFromFrame(frame: IncomingExtensionUiRequest): ExtensionUiDialogReq
40
40
  persistMessages: () => {}, abortControllerRef: { current: null }, appSettings: {},
41
41
  enqueueExtensionDialog: (request) => queue.push(request),
42
42
  withdrawExtensionDialog: (targetId) => { const i = queue.findIndex((r) => r.id === targetId); if (i >= 0) queue.splice(i, 1); },
43
+ reloadMessages: () => {},
44
+ needsReloadRef: { current: false },
43
45
  } satisfies OmpAgentCallbacksDeps;
44
46
  createOmpAgentCallbacks(deps).onExtensionUiRequest?.(frame);
45
47