@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
@@ -1,8 +1,6 @@
1
- import { Download, Info, Settings } from 'lucide-preact';
2
- import {
3
- shellDownloadLabel,
4
- type ShellDownload,
5
- } from '@/client/hooks/ui/updates/shell-download';
1
+ import { Info, Settings } from 'lucide-preact';
2
+ import type { ShellDownload } from '@/client/hooks/ui/updates/shell-download';
3
+ import { ShellUpdateButton } from '@/client/components/common/shell-update-button';
6
4
 
7
5
  interface SessionSidebarFooterProps {
8
6
  onSettings: () => void;
@@ -50,16 +48,11 @@ export function SessionSidebarFooter({ onSettings, onInfo, updateAvailable, onUp
50
48
  // an installed shell that is behind (an update), or an Android browser
51
49
  // whose reader may not know the shell exists (an install). A desktop
52
50
  // browser and an iOS browser get neither — see `useShellDownload`.
53
- <a
54
- href={shellDownload.url}
55
- target="_blank"
56
- rel="noopener noreferrer"
57
- title={shellDownloadLabel(shellDownload)}
58
- aria-label={shellDownloadLabel(shellDownload)}
59
- className="p-1 rounded hover:text-ink hover:bg-ink/5 cursor-pointer transition-colors"
60
- >
61
- <Download size={16} />
62
- </a>
51
+ //
52
+ // A desktop shell that reported its architecture gets a BUTTON: the
53
+ // shell installs the artifact itself, and a link there would be a file
54
+ // in Downloads instead of an updated app.
55
+ <ShellUpdateButton download={shellDownload} variant="icon" />
63
56
  )}
64
57
  </div>
65
58
  {updateAvailable && (
@@ -4,6 +4,8 @@ import { useInlineRename } from '@/client/hooks/ui/inline-rename';
4
4
  import { useSessionRenaming } from '@/client/hooks/workspace/renaming-sessions';
5
5
  import { SessionActionsMenu, useRowMenu } from '@/client/components/common/session-actions-menu';
6
6
  import { SelectionCheckbox } from '@/client/components/common/batch-selection/SelectionCheckbox';
7
+ import { WorktreeBadge } from '@/client/components/layout/session-sidebar/WorktreeBadge';
8
+ import type { SessionWorktreeInfo } from '@/shared/lib/workspace/worktree/types';
7
9
 
8
10
  export interface SessionItemProps {
9
11
  /** The session this row draws; the key its in-flight rename is tracked under. */
@@ -38,6 +40,11 @@ export interface SessionItemProps {
38
40
  waitingFor?: { title: string | null };
39
41
  /** Drop this row's held prompt, from the row's own menu. */
40
42
  onCancelWait?: () => void;
43
+ /**
44
+ * The worktree this session runs in, when its cwd is a linked checkout. The
45
+ * server sends it with the row, so no layout and no plugin asks git.
46
+ */
47
+ worktree?: SessionWorktreeInfo;
41
48
  /** Draw a checkbox and let the row's click toggle the selection instead of
42
49
  * opening the session (batch mode). */
43
50
  selectable?: boolean;
@@ -64,6 +71,7 @@ export function SessionItem({
64
71
  onToggleExpand,
65
72
  waitingFor,
66
73
  onCancelWait,
74
+ worktree,
67
75
  selectable = false,
68
76
  selected = false,
69
77
  onToggleSelect,
@@ -224,6 +232,10 @@ export function SessionItem({
224
232
  waiting for {waitingFor?.title ?? 'another session'}
225
233
  </span>
226
234
  )}
235
+ {/* A worktree session runs in its own checkout. The chip is the only
236
+ thing on the row saying so — the folder above it is the project,
237
+ which is where a reader would otherwise assume the chat writes. */}
238
+ {worktree && !isEditing && <WorktreeBadge worktree={worktree} />}
227
239
  </span>
228
240
  )}
229
241
 
@@ -10,6 +10,8 @@ interface SessionSidebarSessionListProps {
10
10
  sessionStatus: Record<string, 'stream' | 'finish' | 'abort'>;
11
11
  onSelectSession: (id: number | string) => void;
12
12
  onNewSessionForFolder: (id: number) => void;
13
+ /** Start a chat in a worktree made from this folder. */
14
+ onNewWorktreeSession?: (folderId: number, worktreePath: string) => void;
13
15
  /** Surfaces a refused folder action (the reveal endpoint's own reason). */
14
16
  onToast?: (message: string, type?: 'success' | 'error') => void;
15
17
  /** Present only while a batch selection is active. */
@@ -27,6 +29,7 @@ export function SessionSidebarSessionList({
27
29
  sessionStatus,
28
30
  onSelectSession,
29
31
  onNewSessionForFolder,
32
+ onNewWorktreeSession,
30
33
  onToast,
31
34
  batch,
32
35
  onStartSessionSelection,
@@ -49,6 +52,7 @@ export function SessionSidebarSessionList({
49
52
  activeSessionId={activeSessionId}
50
53
  onSelectSession={onSelectSession}
51
54
  onNewSessionForFolder={onNewSessionForFolder}
55
+ onNewWorktreeSession={onNewWorktreeSession}
52
56
  forceExpanded={!!searchQuery}
53
57
  showArchived={showArchived}
54
58
  sessionStatus={sessionStatus}
@@ -12,6 +12,7 @@ import { useSessionSidebarController } from '@/client/hooks/chat/omp/session-sid
12
12
  import { useCancelWait } from '@/client/hooks/chat/waiting/cancel';
13
13
  import { useWaitingDispatch } from '@/client/hooks/chat/waiting/dispatch';
14
14
  import { startPendingSession } from '@/client/hooks/chat/omp/pending-sessions';
15
+ import { setSessionKey } from '@/shared/lib/workspace/session-state/store';
15
16
  import { useScheduledTaskCount } from '@/client/hooks/workspace/scheduled-tasks';
16
17
  import { cloneWorkspaceFolder, createWorkspaceFolder } from '@/client/hooks/workspace/new-workspace';
17
18
  import { useBatchSelection } from '@/client/hooks/workspace/batch-selection';
@@ -150,6 +151,28 @@ export function SessionsView({
150
151
  }, { replace: true });
151
152
  };
152
153
 
154
+ /**
155
+ * Start a chat in a worktree made from this folder.
156
+ *
157
+ * The pick is written into the PENDING session's own state before the URL
158
+ * names it, so the composer's target picker draws it and the send path reads
159
+ * it through its ref — the same key the picker writes. Starting the chat here
160
+ * rather than only reporting the folder is what makes the entry point useful:
161
+ * a worktree created from the sidebar with no chat in it is a folder the user
162
+ * then has to go and find.
163
+ */
164
+ const handleNewWorktreeSession = (folderId: number, worktreePath: string) => {
165
+ prewarmForNewSession(folderId);
166
+ const pendingId = startPendingSession(folderId);
167
+ setSessionKey(pendingId, 'chat.spawnTarget', { folderId, worktreePath });
168
+ setSearchParams(prev => {
169
+ const next = new URLSearchParams(prev);
170
+ next.set('sessionId', pendingId);
171
+ next.set('folderId', folderId.toString());
172
+ return next;
173
+ }, { replace: true });
174
+ };
175
+
153
176
  const handleNewSessionForFolder = (folderId: number) => {
154
177
  prewarmForNewSession(folderId);
155
178
  setSearchParams(prev => {
@@ -197,6 +220,7 @@ export function SessionsView({
197
220
  sessionStatus={sessionStatus}
198
221
  onSelectSession={handleSelectSession}
199
222
  onNewSessionForFolder={handleNewSessionForFolder}
223
+ onNewWorktreeSession={handleNewWorktreeSession}
200
224
  onToast={pushToast}
201
225
  batch={batchBinding}
202
226
  onStartSessionSelection={(folderId) => batch.begin('session', folderId)}
@@ -0,0 +1,48 @@
1
+ /**
2
+ * @license
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ /**
7
+ * The worktree chip on a session row.
8
+ *
9
+ * What it must say, and the one state that changes the words rather than only
10
+ * the colour: a worktree whose folder was deleted outside Kabmo keeps its row
11
+ * (that is how the transcript stays reachable) and must not read as an ordinary
12
+ * checkout.
13
+ */
14
+
15
+ import { describe, expect, test } from 'bun:test';
16
+ import {
17
+ WORKTREE_BADGE_MISSING_SUFFIX,
18
+ worktreeBadgeLabel,
19
+ worktreeBadgeTitle,
20
+ } from '@/client/components/layout/session-sidebar/WorktreeBadge';
21
+ import type { SessionWorktreeInfo } from '@/shared/lib/workspace/worktree/types';
22
+
23
+ const ready: SessionWorktreeInfo = {
24
+ branch: 'wt/cosmic-dolphin',
25
+ path: '/data/worktrees/abc/cosmic-dolphin',
26
+ primaryRoot: '/repo',
27
+ status: 'ready',
28
+ detached: false,
29
+ };
30
+
31
+ describe('worktree badge', () => {
32
+ test('names the branch', () => {
33
+ expect(worktreeBadgeLabel(ready)).toBe('wt/cosmic-dolphin');
34
+ expect(worktreeBadgeTitle(ready)).toContain('/data/worktrees/abc/cosmic-dolphin');
35
+ });
36
+
37
+ test('a missing worktree says so in the label, not only in the colour', () => {
38
+ const missing: SessionWorktreeInfo = { ...ready, status: 'missing' };
39
+ expect(worktreeBadgeLabel(missing)).toContain(WORKTREE_BADGE_MISSING_SUFFIX);
40
+ expect(worktreeBadgeTitle(missing)).toMatch(/missing/i);
41
+ });
42
+
43
+ test('a detached HEAD has a name instead of an empty chip', () => {
44
+ const detached: SessionWorktreeInfo = { ...ready, branch: '', detached: true };
45
+ expect(worktreeBadgeLabel(detached)).toBe('detached');
46
+ expect(worktreeBadgeTitle(detached)).toMatch(/detached/i);
47
+ });
48
+ });
@@ -0,0 +1,55 @@
1
+ /**
2
+ * @license
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ /**
7
+ * The `⑂ wt/branch` chip under a worktree session's title.
8
+ *
9
+ * It is a LABEL, not a control: the row's own click opens the chat, and the
10
+ * project's panels already follow the session's root, so a second click target
11
+ * here would only be a way to open the same thing twice. The absolute path lives
12
+ * in `title`, which is where a reader looks when they want to know where the
13
+ * chat actually writes.
14
+ *
15
+ * `missing` is the one state that changes the wording rather than the colour
16
+ * alone: a worktree whose directory was removed outside Kabmo keeps its row (so
17
+ * its transcript is reachable) but must not read as an ordinary checkout.
18
+ */
19
+
20
+ import type { SessionWorktreeInfo } from '@/shared/lib/workspace/worktree/types';
21
+
22
+ export const WORKTREE_BADGE_MISSING_SUFFIX = 'missing';
23
+
24
+ /** What the chip says, without the markup — exported so a test asserts the words. */
25
+ export function worktreeBadgeLabel(worktree: SessionWorktreeInfo): string {
26
+ const name = worktree.branch || (worktree.detached ? 'detached' : 'worktree');
27
+ return worktree.status === 'missing' ? `${name} · ${WORKTREE_BADGE_MISSING_SUFFIX}` : name;
28
+ }
29
+
30
+ export function worktreeBadgeTitle(worktree: SessionWorktreeInfo): string {
31
+ if (worktree.status === 'missing') {
32
+ return `Worktree folder is missing: ${worktree.path}`;
33
+ }
34
+ return worktree.detached
35
+ ? `Detached HEAD in worktree ${worktree.path}`
36
+ : `Runs in worktree ${worktree.path}`;
37
+ }
38
+
39
+ export function WorktreeBadge({ worktree }: { worktree: SessionWorktreeInfo }) {
40
+ const missing = worktree.status === 'missing';
41
+ return (
42
+ <span
43
+ className={`flex min-w-0 items-center gap-1 truncate text-[10px] font-normal ${
44
+ missing ? 'text-warning' : 'text-ink/45'
45
+ }`}
46
+ title={worktreeBadgeTitle(worktree)}
47
+ data-worktree-status={worktree.status}
48
+ >
49
+ <span aria-hidden="true" className="shrink-0 font-mono">
50
+ ⑂
51
+ </span>
52
+ <span className="truncate">{worktreeBadgeLabel(worktree)}</span>
53
+ </span>
54
+ );
55
+ }
@@ -9,7 +9,7 @@ import { MobileRightSidebar } from '@/client/components/mobile/RightSidebar';
9
9
  import { MobileFullEditor } from '@/client/components/mobile/mobile-right-sidebar/FullEditor';
10
10
  import { MobileFullDiff } from '@/client/components/mobile/mobile-right-sidebar/FullDiff';
11
11
  import { SettingsModal } from '@/client/components/settings/LazyModal';
12
- import { activeProjectForSession } from '@/shared/lib/workspace/active-project';
12
+ import { activeProjectForSession, effectiveRootFor } from '@/shared/lib/workspace/active-project';
13
13
  import { spawnCwdForNewSession, triggerSessionPrewarm } from '@/shared/lib/omp/session/prewarm';
14
14
  import { useSidebarData } from '@/client/hooks/chat/omp/session-list';
15
15
  import { useMissingFiles } from '@/client/hooks/workspace/missing-files';
@@ -23,6 +23,7 @@ import { mobileEditorFileFrom, type MobileEditorFile } from '@/client/components
23
23
  import { useLeftDock } from '@/client/hooks/workspace/left-dock';
24
24
  import { useSessionStateContext } from '@/client/hooks/workspace/session-state/context';
25
25
  import { setSessionKey } from '@/shared/lib/workspace/session-state/store';
26
+ import { startWorktreeSession } from '@/client/components/mobile/worktree-session';
26
27
  import { OPEN_PANEL_EVENT, phoneDrawerForPanel, type OpenPanelDetail } from '@/shared/lib/workspace/panel-open';
27
28
 
28
29
  interface MobileLayoutWrapperProps {
@@ -106,7 +107,9 @@ export function MobileLayoutWrapper({ onDesktopToggle, appSettings = {} }: Mobil
106
107
 
107
108
  const { folder: sessionFolder } = activeProjectForSession(folders, sessionId);
108
109
  const contextFolder = sessionFolder ?? (folderId ? folders.find(f => String(f.id) === String(folderId)) ?? null : null);
109
- const activeProjectPath = contextFolder?.project_path ?? null;
110
+ // A worktree session runs outside its folder's project path: the phone draws
111
+ // the same panels as the desktop, so it resolves the same root.
112
+ const activeProjectPath = effectiveRootFor(folders, sessionId) ?? contextFolder?.project_path ?? null;
110
113
  const hasContext = !!contextFolder;
111
114
 
112
115
  // A phone drop is rare but real (an iPad with a trackpad). The target is the
@@ -145,20 +148,13 @@ export function MobileLayoutWrapper({ onDesktopToggle, appSettings = {} }: Mobil
145
148
  useKabmoEvent('omp:view-subagent', () => setCurrentScreen('main'));
146
149
 
147
150
  /**
148
- * A plugin's navbar readout asking for its own panel.
149
- *
150
- * A phone has no rail, so "show this panel" is two decisions: which DRAWER can
151
- * reach it, and which chip inside that drawer is the view. Both are answered
152
- * from the placement state (`phoneDrawerForPanel`), never from the panel's
153
- * slot — a panel placed on the left is drawn by the RIGHT drawer while the
154
- * rail is off, and naming the drawer from the slot would open one that does
155
- * not list it.
156
- *
157
- * The tab is written through the session store rather than through a prop:
158
- * the drawer mounts on this screen change and reads the stored tab, so the
159
- * selection has to be in the store before it mounts. An `editor`-slot
160
- * takeover resolves to null — the phone has no editor column — and the click
161
- * then does nothing rather than opening a drawer that cannot show it.
151
+ * A plugin's navbar readout asking for its own panel. A phone has no rail, so
152
+ * "show this panel" is two decisions — which DRAWER reaches it and which chip
153
+ * inside it — both answered by `phoneDrawerForPanel` from the PLACEMENT, never
154
+ * from the panel's slot. The tab is written through the store because the
155
+ * drawer mounts on this screen change and reads the stored tab; an `editor`
156
+ * takeover resolves to null and the click does nothing rather than opening a
157
+ * drawer that cannot show it.
162
158
  */
163
159
  useKabmoEvent(OPEN_PANEL_EVENT, (event) => {
164
160
  const panelKey = (event as CustomEvent<OpenPanelDetail>).detail?.panelKey;
@@ -228,6 +224,15 @@ export function MobileLayoutWrapper({ onDesktopToggle, appSettings = {} }: Mobil
228
224
  setCurrentScreen('main');
229
225
  };
230
226
 
227
+ // Desktop parity: a worktree made from the phone's folder menu starts its chat
228
+ // in it. The sequence lives in one module so both layouts share it.
229
+ const handleNewWorktreeSession = (targetFolderId: number, worktreePath: string) =>
230
+ startWorktreeSession(
231
+ { setSearchParams, afterNavigate: () => setCurrentScreen('main') },
232
+ targetFolderId,
233
+ worktreePath,
234
+ );
235
+
231
236
  const handleOpenFile = useCallback((file: unknown) => {
232
237
  const next = mobileEditorFileFrom(file, activeProjectPath);
233
238
  if (next) setMobileEditorFile(next);
@@ -254,6 +259,7 @@ export function MobileLayoutWrapper({ onDesktopToggle, appSettings = {} }: Mobil
254
259
  <div
255
260
  className={`flex-1 min-h-0 w-full relative ${currentScreen === 'main' ? 'block' : 'hidden'}`}
256
261
  {...editorDrop.dropProps}
262
+ {...editorDrop.captureDropProps}
257
263
  >
258
264
  <EditorDropOverlay visible={editorDrop.isDragging} />
259
265
  <MobileMainView
@@ -272,6 +278,7 @@ export function MobileLayoutWrapper({ onDesktopToggle, appSettings = {} }: Mobil
272
278
  onSelectSession={handleSelectSession}
273
279
  onNewSession={handleNewSession}
274
280
  onNewSessionForFolder={handleNewSessionForFolder}
281
+ onNewWorktreeSession={handleNewWorktreeSession}
275
282
  onClose={() => setCurrentScreen('main')}
276
283
  onDesktopToggle={onDesktopToggle}
277
284
  appSettings={appSettings}
@@ -10,6 +10,7 @@ import { SESSIONS_VIEW_ID } from '@/shared/lib/workspace/left-dock';
10
10
  import { phoneRightPanels } from '@/shared/lib/workspace/mobile-dock';
11
11
  import { PanelChips } from '@/client/components/mobile/panel-chips';
12
12
  import { titlebarPaddingLeft, titlebarPaddingRight } from '@/shared/lib/ui/titlebar';
13
+ import { dragRegionProps, titlebarInsetLeft } from '@/client/lib/desktop/titlebar';
13
14
 
14
15
  interface MobileRightSidebarProps {
15
16
  enabled?: boolean;
@@ -89,28 +90,38 @@ export function MobileRightSidebar({
89
90
  return (
90
91
  <div className="flex flex-col h-full w-full bg-paper text-ink relative select-none">
91
92
 
92
- {/* Top Header & Tab Navigation Bar */}
93
+ {/* Top Header & Tab Navigation Bar. It is the window's title bar while
94
+ this drawer is the mounted screen: a desktop shell window narrowed
95
+ past the layout breakpoint shows it, and without `data-drag-region`
96
+ there is nothing left to move the window by. The chip strip inside is
97
+ the `titlebar-no-drag` cluster. */}
93
98
  <div
94
- className="border-b border-ink/10 flex items-center justify-between px-3 flex-shrink-0 bg-canvas"
99
+ className="border-b border-ink/10 flex items-center justify-between px-3 flex-shrink-0 bg-canvas titlebar-drag-region select-none"
100
+ {...dragRegionProps()}
95
101
  style={{
96
102
  height: 'calc(3.5rem + env(safe-area-inset-top, 0px))',
97
103
  paddingTop: 'env(safe-area-inset-top, 0px)',
98
- paddingLeft: titlebarPaddingLeft('0.75rem', true),
104
+ paddingLeft: titlebarInsetLeft(titlebarPaddingLeft('0.75rem', true)),
99
105
  paddingRight: titlebarPaddingRight('0.75rem', true),
100
106
  }}
101
107
  >
102
108
 
103
- <PanelChips
104
- panels={catalog}
105
- activeId={activePanel?.id ?? activeTab}
106
- onSelect={setActiveTab}
107
- hasGitChanges={hasGitChanges}
108
- />
109
+ {/* The header is now a drag region, so its controls must say so —
110
+ `app-region` inherits, and a control inside a drag region is a drag
111
+ handle that swallows its own click on Windows. */}
112
+ <div className="flex items-center min-w-0 titlebar-no-drag">
113
+ <PanelChips
114
+ panels={catalog}
115
+ activeId={activePanel?.id ?? activeTab}
116
+ onSelect={setActiveTab}
117
+ hasGitChanges={hasGitChanges}
118
+ />
119
+ </div>
109
120
 
110
121
  <button
111
122
  type="button"
112
123
  onClick={onClose}
113
- className="p-1.5 rounded-lg hover:bg-ink/5 active:bg-ink/10 text-ink transition-colors flex-shrink-0 ml-2"
124
+ className="p-1.5 rounded-lg hover:bg-ink/5 active:bg-ink/10 text-ink transition-colors flex-shrink-0 ml-2 titlebar-no-drag"
114
125
  title="Close right sidebar"
115
126
  aria-label="Close right sidebar"
116
127
  >
@@ -29,6 +29,8 @@ interface MobileSessionSidebarProps {
29
29
  onSelectSession: (id: number | string) => void;
30
30
  onNewSession: () => void;
31
31
  onNewSessionForFolder: (folderId: number) => void;
32
+ /** Start a chat in a worktree made from this folder. */
33
+ onNewWorktreeSession?: (folderId: number, worktreePath: string) => void;
32
34
  onClose: () => void;
33
35
  onDesktopToggle?: () => void;
34
36
  appSettings?: Record<string, any>;
@@ -45,6 +47,7 @@ export function MobileSessionSidebar({
45
47
  onSelectSession,
46
48
  onNewSession,
47
49
  onNewSessionForFolder,
50
+ onNewWorktreeSession,
48
51
  onClose,
49
52
  onDesktopToggle,
50
53
  appSettings = {},
@@ -264,6 +267,11 @@ export function MobileSessionSidebar({
264
267
  sessionStatus={sessionStatus}
265
268
  onSelectSession={handleSelectSession}
266
269
  onNewSessionForFolder={handleNewSessionForFolderAndClose}
270
+ onNewWorktreeSession={
271
+ onNewWorktreeSession
272
+ ? (id, path) => { onNewWorktreeSession(id, path); onClose(); }
273
+ : undefined
274
+ }
267
275
  onToggleFolder={toggleFolder}
268
276
  onToast={pushToast}
269
277
  batch={batchBinding}
@@ -0,0 +1,172 @@
1
+ /**
2
+ * @license
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ /**
7
+ * The phone's headers are title bars, and they say so.
8
+ *
9
+ * A desktop shell window may be narrower than `App`'s 768px layout breakpoint —
10
+ * macOS, Windows and Linux all allow a 720px window — so a narrowed shell
11
+ * window renders the PHONE layout, and whichever header is on screen becomes
12
+ * the window's top strip. Three things follow, and all three were missing:
13
+ *
14
+ * 1. **`data-drag-region`.** macOS and Linux decide a drag from that attribute,
15
+ * so a header without it is a strip the window cannot be moved by. The main
16
+ * screen had none, and the right drawer had none at all.
17
+ * 2. **The shell's own left inset.** `titlebarPaddingLeft` reads
18
+ * `env(titlebar-area-x)`, which a native WKWebView never defines, so the
19
+ * 76px macOS reserves for its traffic lights has to come from
20
+ * `titlebarInsetLeft` — without it the hamburger and the wordmark sat under
21
+ * the buttons.
22
+ * 3. **`titlebar-no-drag` on the controls.** `app-region` INHERITS, so marking
23
+ * a header as a drag region turns every control in it into a drag handle on
24
+ * Windows unless the control says otherwise — and a drag handle swallows its
25
+ * own click.
26
+ *
27
+ * Asserted on the RENDERED markup, because a header's class list and attributes
28
+ * are what the shells read; a source-level check would pass on a component that
29
+ * rendered something else.
30
+ */
31
+
32
+ import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, test } from 'bun:test';
33
+ import { Window } from 'happy-dom';
34
+ import { h, render } from 'preact';
35
+ import { act } from 'preact/test-utils';
36
+ import { MobileHeader } from '@/client/components/mobile/mobile-main-view/Header';
37
+ import { MobileSessionHeader } from '@/client/components/mobile/mobile-session-sidebar/Header';
38
+ import { MobileRightSidebar } from '@/client/components/mobile/RightSidebar';
39
+ import { SessionStateProvider } from '@/client/components/common/session-state-provider/index';
40
+ import { silenceRealtime, restoreRealtime } from '@/test-support/silent-socket';
41
+
42
+ const DOM_GLOBALS = [
43
+ 'window', 'document', 'navigator', 'Node', 'Element', 'HTMLElement',
44
+ 'Event', 'MouseEvent', 'KeyboardEvent',
45
+ ] as const;
46
+ const nativeGlobals: Partial<Record<(typeof DOM_GLOBALS)[number], unknown>> = {};
47
+
48
+ let win: Window;
49
+ let container: HTMLElement;
50
+
51
+ /** The shell's globals, which the DOM's `window` type does not declare. */
52
+ type Shell = {
53
+ __kabmoCall?: (method: string, params?: Record<string, unknown>) => Promise<unknown>;
54
+ __kabmoDesktop?: Record<string, unknown>;
55
+ };
56
+
57
+ beforeAll(() => {
58
+ win = new Window({ url: 'http://localhost' });
59
+ const target = globalThis as unknown as Record<string, unknown>;
60
+ for (const key of DOM_GLOBALS) {
61
+ if (!(key in nativeGlobals)) nativeGlobals[key] = target[key];
62
+ target[key] = (win as unknown as Record<string, unknown>)[key];
63
+ }
64
+ });
65
+
66
+ afterAll(() => {
67
+ const target = globalThis as unknown as Record<string, unknown>;
68
+ for (const key of DOM_GLOBALS) {
69
+ const value = nativeGlobals[key];
70
+ if (value === undefined) delete target[key];
71
+ else target[key] = value;
72
+ }
73
+ });
74
+
75
+ beforeEach(() => {
76
+ // The headers subscribe to the realtime channel on mount; this window's
77
+ // origin answers nothing, and a refused dial fails asynchronously — after
78
+ // this suite has restored the globals, which reports against the NEXT file.
79
+ silenceRealtime();
80
+ container = document.createElement('div');
81
+ document.body.appendChild(container);
82
+ });
83
+
84
+ afterEach(() => {
85
+ if (container) {
86
+ render(null, container);
87
+ container.remove();
88
+ }
89
+ restoreRealtime();
90
+ const shell = win as unknown as Shell;
91
+ delete shell.__kabmoDesktop;
92
+ delete shell.__kabmoCall;
93
+ });
94
+
95
+ /** Mount a header as a macOS shell window would see it. */
96
+ async function mountAsMacShell(node: () => unknown): Promise<void> {
97
+ const shell = win as unknown as Shell;
98
+ shell.__kabmoCall = async () => null;
99
+ shell.__kabmoDesktop = {
100
+ platform: 'macos', shellVersion: '0.1.0', overlayTitlebar: true, trafficLightInset: '5rem',
101
+ };
102
+ // The headers embed the shared control cluster, which reads the session's
103
+ // state (the editor/panel toggles and the plugin slots) — the real provider is
104
+ // what a shell window mounts them inside.
105
+ await act(async () => {
106
+ render(
107
+ h(SessionStateProvider, { sessionId: 'session-shell-header', children: node() as never }),
108
+ container,
109
+ );
110
+ });
111
+ }
112
+
113
+ describe('phone headers as a shell title bar', () => {
114
+ test('the main header carries the drag region and the shell inset', async () => {
115
+ await mountAsMacShell(() =>
116
+ h(MobileHeader, {
117
+ activeSessionTitle: 'Shell',
118
+ activeSessionId: 'session-shell-header',
119
+ onOpenSessionSidebar: () => {},
120
+ onOpenRightSidebar: () => {},
121
+ onNewSession: () => {},
122
+ onSelectSession: () => {},
123
+ }),
124
+ );
125
+ const header = container.querySelector('header') as HTMLElement;
126
+ expect(header).toBeTruthy();
127
+ expect(header.hasAttribute('data-drag-region')).toBe(true);
128
+ // `5rem` is what this shell reported; the caller's own 0.75rem would leave
129
+ // the hamburger under the traffic lights.
130
+ expect(header.style.paddingLeft).toContain('5rem');
131
+ // And the controls inside it must not inherit the drag region.
132
+ expect(header.querySelector('.titlebar-no-drag')).toBeTruthy();
133
+ });
134
+
135
+ test('the sessions drawer header carries the drag region and the shell inset', async () => {
136
+ await mountAsMacShell(() => h(MobileSessionHeader, { onClose: () => {} }));
137
+ const header = container.firstElementChild as HTMLElement;
138
+ expect(header.hasAttribute('data-drag-region')).toBe(true);
139
+ expect(header.style.paddingLeft).toContain('5rem');
140
+ expect(header.querySelector('.titlebar-no-drag')).toBeTruthy();
141
+ });
142
+
143
+ test('the right drawer header is a drag region whose controls are not', async () => {
144
+ await mountAsMacShell(() =>
145
+ h(MobileRightSidebar, { enabled: false, onClose: () => {} }),
146
+ );
147
+ const drag = container.querySelector('[data-drag-region]') as HTMLElement;
148
+ expect(drag).toBeTruthy();
149
+ expect(drag.style.paddingLeft).toContain('5rem');
150
+ const close = drag.querySelector('button[aria-label="Close right sidebar"]') as HTMLElement;
151
+ expect(close).toBeTruthy();
152
+ // The close button is a control in a drag region: without this it is a
153
+ // drag handle on Windows and its click never fires.
154
+ expect(close.className).toContain('titlebar-no-drag');
155
+ });
156
+
157
+ /** A browser tab has no shell, so nothing is reserved and nothing is marked. */
158
+ test('a browser marks no drag region and reserves nothing', async () => {
159
+ await act(async () => {
160
+ render(
161
+ h(SessionStateProvider, {
162
+ sessionId: 'session-shell-header',
163
+ children: h(MobileSessionHeader, { onClose: () => {} }),
164
+ }),
165
+ container,
166
+ );
167
+ });
168
+ const header = container.firstElementChild as HTMLElement;
169
+ expect(header.hasAttribute('data-drag-region')).toBe(false);
170
+ expect(header.style.paddingLeft).not.toContain('5rem');
171
+ });
172
+ });
@@ -9,6 +9,7 @@ import { StreamStatusDot } from '@/client/components/common/StreamStatusDot';
9
9
  import { BrandWordmark } from '@/client/components/common/brand-wordmark';
10
10
  import { HeaderControls } from '@/client/components/common/header-controls';
11
11
  import { titlebarPaddingLeft, titlebarPaddingRight } from '@/shared/lib/ui/titlebar';
12
+ import { dragRegionProps, titlebarInsetLeft } from '@/client/lib/desktop/titlebar';
12
13
 
13
14
  interface MobileHeaderProps {
14
15
  activeSessionTitle: string;
@@ -90,9 +91,17 @@ export function MobileHeader({
90
91
  return (
91
92
  <header
92
93
  className="flex-shrink-0 bg-canvas border-b border-ink/10 flex items-center justify-between gap-2 px-3 z-20 titlebar-drag-region select-none"
94
+ {...dragRegionProps()}
93
95
  style={{
94
96
  height: 'calc(3.5rem + env(safe-area-inset-top, 0px))',
95
- paddingLeft: titlebarPaddingLeft('0.75rem', true),
97
+ // The shell's own inset, not only the PWA's. `titlebarPaddingLeft` reads
98
+ // `env(titlebar-area-x)`, which a native WKWebView never defines — so on
99
+ // macOS this header's hamburger and wordmark were drawn UNDER the
100
+ // traffic lights, in the 76px the shell reserves for them. `titlebarInsetLeft`
101
+ // is what the desktop navbar and the sidebar header already use; a
102
+ // desktop shell window narrowed past the layout breakpoint lands on this
103
+ // header, which is why it needs the same treatment.
104
+ paddingLeft: titlebarInsetLeft(titlebarPaddingLeft('0.75rem', true)),
96
105
  paddingRight: titlebarPaddingRight('0.75rem', true),
97
106
  paddingTop: 'env(safe-area-inset-top, 0px)',
98
107
  }}
@@ -69,6 +69,9 @@ export function MobileFullDiff({ diff, onClose, onFileSaved }: MobileFullDiffPro
69
69
  <div className="flex-1 min-h-0">
70
70
  <DiffPanel
71
71
  className="h-full"
72
+ // A full-screen phone overlay has no chat column or sidebar beside
73
+ // it, so the width modes would widen into nothing.
74
+ viewModeButtons={false}
72
75
  filePath={diff.path}
73
76
  status={status}
74
77
  isStaged={isStaged}