@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
@@ -13,18 +13,13 @@ import { ResizeHandle } from '@/client/components/layout/desktop-layout/ResizeHa
13
13
  import { useAvailableWidth } from '@/client/hooks/workspace/available-width';
14
14
  import { useSessionStateContext } from '@/client/hooks/workspace/session-state/context';
15
15
  import {
16
- DEFAULT_EDITOR_FRACTIONS,
17
- DEFAULT_EDITOR_WIDTHS,
18
16
  HANDLE_WIDTH,
19
17
  MIN_EDITOR_PANEL_WIDTH,
20
18
  fillerMinWidth,
21
- pairMaxSize,
22
- panelFraction,
23
- resolvePanelWidth,
24
19
  type EditorWidthMode,
25
20
  type PanelWidths,
26
21
  } from '@/shared/lib/workspace/panel-widths';
27
- import { DEFAULT_RIGHT_PANEL_FRACTION } from '@/shared/lib/workspace/right-panels';
22
+ import { resolveWorkspacePanelWidths } from '@/shared/lib/workspace/workspace-panel-widths';
28
23
 
29
24
  const Editor = lazy(() => import('@/client/components/workspace/editor/index').then((m) => ({ default: m.Editor })));
30
25
 
@@ -43,6 +38,13 @@ function PanelSuspense({ children }: { children: ReactNode }) {
43
38
  interface WorkspacePanelsProps {
44
39
  appSettings?: Record<string, any>;
45
40
  showEditor: boolean;
41
+ /**
42
+ * The chat column is collapsed: the editor's compact/full view took its
43
+ * width. The chat STAYS MOUNTED (it owns the live stream and the composer)
44
+ * but is sized to nothing and marked inert, so a run in progress is not
45
+ * thrown away by widening a document.
46
+ */
47
+ chatHidden?: boolean;
46
48
  editorPanelRef: RefObject<PanelImperativeHandle | null>;
47
49
  showRightPanel: boolean;
48
50
  /** A built-in view id or a plugin panel key (`plugin:<id>/<panel>`). */
@@ -78,6 +80,7 @@ export function WorkspacePanels(props: WorkspacePanelsProps) {
78
80
  const {
79
81
  appSettings,
80
82
  showEditor,
83
+ chatHidden = false,
81
84
  editorPanelRef,
82
85
  showRightPanel,
83
86
  activeRightPanel,
@@ -138,75 +141,27 @@ export function WorkspacePanels(props: WorkspacePanelsProps) {
138
141
  // one, which is why a Kabmo without one looks exactly as it did.
139
142
  const editorPlugins = usePanelSlotPlugins();
140
143
 
141
- // Separators present in this group, which also consume width.
142
- const handleCount = (showEditor ? 1 : 0) + (showRightPanel ? 1 : 0);
143
-
144
- // The active view's own floor, resolved once here rather than at each use, so
145
- // the right panel's floor cannot be computed from different values. A view
146
- // that is switched off (or a plugin that has not loaded) has no entry, and the
147
- // right panel falls back to the editor's own floor — it renders the notice,
148
- // not a view.
149
- const rightPanelMin = activePanel?.minWidth ?? MIN_EDITOR_PANEL_WIDTH;
150
-
151
- // The editor and the right panel are a COUPLED pair (see `panel-widths.ts`):
152
- // they share `PAIR_MAX_TOTAL_FRACTION` and each is capped by `PAIR_MAX_FRACTION`.
153
- // The right panel is resolved first, so it reserves the editor's WANTED share
154
- // (the editor has no result yet); the editor then reserves the right panel's
155
- // ACTUAL width. That asymmetry is deliberate: an editor asking for more than
156
- // its default makes the right panel give way first, and the pair still sums
157
- // to the total.
158
- const editorFraction = panelFraction(panelWidths[editorWidthMode], DEFAULT_EDITOR_FRACTIONS[editorWidthMode], available);
159
- const editorReservePx = available != null && available > 0 ? Math.round(editorFraction * available) : 0;
160
-
161
- // Resolve the RIGHT panel first, then the editor against the width it
162
- // actually got. The px order is the other half of the contract: each panel's
163
- // group budget is `available − chat floor − the sibling's reserved width −
164
- // handles`, so reserving only a sibling's FLOOR (the previous shape) let the
165
- // two declared widths sum past the group, and flexbox then shrank BOTH —
166
- // measured at 1440 with both open: the editor asked for 507 and rendered 477,
167
- // the right panel asked for 245 and rendered 230. The right panel is resolved
168
- // first because it is the smaller, view-specific slot: it keeps its own width
169
- // and the editor absorbs the remainder.
170
- const rightWidth = resolvePanelWidth({
171
- stored: panelWidths.right?.[activeRightPanel],
172
- defaultFraction: activePanel?.defaultFraction ?? DEFAULT_RIGHT_PANEL_FRACTION,
173
- defaultPx: activePanel?.defaultWidth ?? MIN_EDITOR_PANEL_WIDTH,
174
- available,
175
- min: rightPanelMin,
176
- // The pair cap: the right panel may not exceed `total − the editor's wanted share`.
177
- pairReservePx: showEditor ? editorReservePx : 0,
178
- // The editor renders before the right panel and has not been resolved yet,
179
- // so only its floor can be reserved here.
180
- siblingWidth: showEditor ? MIN_EDITOR_PANEL_WIDTH : 0,
181
- handleCount,
182
- });
183
-
184
- const editorWidth = resolvePanelWidth({
185
- stored: panelWidths[editorWidthMode],
186
- defaultFraction: DEFAULT_EDITOR_FRACTIONS[editorWidthMode],
187
- defaultPx: DEFAULT_EDITOR_WIDTHS[editorWidthMode],
188
- available,
189
- min: MIN_EDITOR_PANEL_WIDTH,
190
- // The pair cap: the editor may not exceed `total − the right panel's width`.
191
- pairReservePx: showRightPanel ? rightWidth : 0,
192
- // The right panel is resolved above; reserve the width it actually took so
193
- // the editor's group budget cannot push the pair past the group.
194
- siblingWidth: showRightPanel ? rightWidth : 0,
144
+ // The active view's own floor and defaults are read here and handed to the
145
+ // resolver: a view that is switched off (or a plugin that has not loaded) has
146
+ // no entry, and the right panel falls back to the editor's own floor — it
147
+ // renders the notice, not a view.
148
+ const {
149
+ editor: editorWidth,
150
+ right: rightWidth,
151
+ editorMax: editorMaxSize,
152
+ rightMax: rightMaxSize,
153
+ rightMin: rightPanelMin,
195
154
  handleCount,
196
- });
197
-
198
- /**
199
- * Pair-aware maxima for a DRAG. `resolvePanelWidth` already caps the widths
200
- * it hands to `defaultSize`, but a separator drag bypasses it — the resizer
201
- * transfers pixels between two panels clamped only by their own `maxSize`.
202
- * Without these, dragging the editor wide would push the pair past
203
- * `PAIR_MAX_TOTAL_FRACTION`. Each panel's max is its own cap and the pair
204
- * total minus the sibling's FLOOR (the sibling gives way down to its minimum
205
- * during the drag), so the pair cannot exceed 0.7 by dragging either way.
206
- * `undefined` before the group is measured (no cap yet).
207
- */
208
- const editorMaxSize = pairMaxSize({ available, siblingMin: rightPanelMin, siblingOpen: showRightPanel });
209
- const rightMaxSize = pairMaxSize({ available, siblingMin: MIN_EDITOR_PANEL_WIDTH, siblingOpen: showEditor });
155
+ } = resolveWorkspacePanelWidths({
156
+ available,
157
+ panelWidths,
158
+ editorWidthMode,
159
+ activeRightPanel,
160
+ activePanel,
161
+ showEditor,
162
+ showRightPanel,
163
+ chatHidden,
164
+ });
210
165
 
211
166
  /**
212
167
  * Widths of the fixed panels as they are right now, keyed by slot: the
@@ -222,7 +177,11 @@ export function WorkspacePanels(props: WorkspacePanelsProps) {
222
177
  const patch: PanelWidths = {};
223
178
  const editor = editorPanelRef.current?.getSize()?.inPixels;
224
179
  const right = rightPanelRef.current?.getSize()?.inPixels;
225
- if (editor != null && editor > 0) {
180
+ // With the chat hidden the editor's width is whatever is left over, not a
181
+ // choice — persisting it would overwrite the width the user set for the
182
+ // ordinary layout with the one this mode produced. A drag made here still
183
+ // moves the RIGHT panel, which is the width that was actually chosen.
184
+ if (!chatHidden && editor != null && editor > 0) {
226
185
  patch[editorWidthMode] = { px: Math.round(editor), ...(available ? { fraction: editor / available } : {}) };
227
186
  }
228
187
  if (right != null && right > 0) {
@@ -250,6 +209,10 @@ export function WorkspacePanels(props: WorkspacePanelsProps) {
250
209
  <Panel
251
210
  id="center-panel"
252
211
  filler
212
+ // Collapsed — not unmounted — in compact/full view: the chat holds the
213
+ // live run, the composer draft and the scroll position, and widening a
214
+ // document must not discard any of them.
215
+ collapsed={chatHidden}
253
216
  minSize={fillerMinWidth({
254
217
  available,
255
218
  reservedPx:
@@ -258,15 +221,15 @@ export function WorkspacePanels(props: WorkspacePanelsProps) {
258
221
  handleCount * HANDLE_WIDTH,
259
222
  })}
260
223
  >
261
- <div className="relative w-full h-full" {...drop.dropProps}>
224
+ <div className="relative w-full h-full" {...drop.dropProps} {...drop.captureDropProps} inert={chatHidden}>
262
225
  <EditorDropOverlay visible={drop.isDragging} />
263
226
  <ChatTimeline className="w-full h-full" appSettings={appSettings} onSessionTitle={onSessionTitle} />
264
227
  </div>
265
228
  </Panel>
266
229
 
267
- {showEditor && <ResizeHandle />}
230
+ {showEditor && !chatHidden && <ResizeHandle />}
268
231
  <Panel panelRef={editorPanelRef} id="editor-panel" defaultSize={editorWidth} minSize={MIN_EDITOR_PANEL_WIDTH} maxSize={editorMaxSize} collapsed={!showEditor}>
269
- <div className="flex w-full h-full" {...drop.dropProps}>
232
+ <div className="flex w-full h-full" {...drop.dropProps} {...drop.captureDropProps}>
270
233
  <EditorDropOverlay visible={drop.isDragging} />
271
234
  <EditorPluginRail
272
235
  activeKey={activeEditorPanelKey}
@@ -3,8 +3,7 @@ import { useSearchParams } from '@/client/lib/router/search-params';
3
3
  import { Group, Panel, type PanelImperativeHandle } from '@/client/components/layout/desktop-layout/resizer';
4
4
  import { LeftColumn } from '@/client/components/layout/desktop-layout/LeftColumn';
5
5
  import { SettingsModal } from '@/client/components/settings/LazyModal';
6
- import type { SettingsCategoryId } from '@/shared/types';
7
- import { activeProjectForSession } from '@/shared/lib/workspace/active-project';
6
+ import { activeProjectForSession, effectiveRootFor } from '@/shared/lib/workspace/active-project';
8
7
  import { useFileTabs } from '@/client/hooks/workspace/file-tabs';
9
8
  import { useSessionState } from '@/client/hooks/workspace/session-state';
10
9
  import { TopNavbar } from '@/client/components/layout/desktop-layout/TopNavbar';
@@ -18,17 +17,17 @@ import { MIN_LEFT_DOCK_PLUGIN_WIDTH } from '@/shared/lib/workspace/left-dock-wid
18
17
  import { SESSIONS_VIEW_ID } from '@/shared/lib/workspace/left-dock';
19
18
  import { useOpenPanelRequest } from '@/client/hooks/workspace/open-panel';
20
19
  import { useLeftDock, useLeftDockWidth } from '@/client/hooks/workspace/left-dock';
20
+ import { useEditorViewLayout } from '@/client/hooks/workspace/editor-view';
21
21
  import { useLeftDockView } from '@/client/components/workspace/plugin-panel/catalog';
22
22
  import type { EditorWidthMode } from '@/shared/lib/workspace/panel-widths';
23
- import { useKabmoEvent } from '@/client/hooks/ui/window-event';
23
+ import { useSettingsModal } from '@/client/hooks/ui/settings-modal';
24
24
  import { useWorkspaceKeybindings } from '@/client/hooks/workspace/keybindings';
25
25
  import { PluginBootstrap } from '@/client/lib/plugins/bootstrap';
26
26
  import { useToasts } from '@/client/hooks/ui/toasts';
27
27
  import type { EditorDropResult } from '@/client/hooks/editor/use-editor-drop';
28
28
  import { ToastStack } from '@/client/components/common/ToastStack';
29
29
  import { SessionDeleteModal } from '@/client/components/common/session-delete-modal';
30
- import { useSessionActions } from '@/client/hooks/workspace/session-actions';
31
- import { useSessionDelete } from '@/client/hooks/workspace/session-delete';
30
+ import { useNavbarSessionActions } from '@/client/hooks/workspace/navbar-session-actions';
32
31
 
33
32
  interface DesktopLayoutProps {
34
33
  sessionId: string | null;
@@ -71,7 +70,13 @@ export function DesktopLayout({ sessionId, onSwitchToMobile, appSettings = {} }:
71
70
  if (folderIdParam) return folders.find(f => String(f.id) === String(folderIdParam)) ?? null;
72
71
  return null;
73
72
  }, [folders, sessionId, folderIdParam]);
74
- const activeProjectPath = activeProject?.project_path ?? null;
73
+ // A worktree session runs OUTSIDE its folder's project path, so the panels
74
+ // follow the session's own root — the checkout it left would otherwise be what
75
+ // Files, Search, Git and the terminal read.
76
+ const activeProjectPath = useMemo(
77
+ () => effectiveRootFor(folders, sessionId || '') ?? activeProject?.project_path ?? null,
78
+ [folders, sessionId, activeProject],
79
+ );
75
80
  const hasActiveContext = !!activeProject;
76
81
 
77
82
  const handleOpenTab = useCallback(() => {
@@ -106,32 +111,30 @@ export function DesktopLayout({ sessionId, onSwitchToMobile, appSettings = {} }:
106
111
  [openedFiles, activeFileId],
107
112
  );
108
113
 
114
+ /**
115
+ * The editor's WIDTH mode (`compact` / `full`), entered from either toolbar.
116
+ *
117
+ * It collapses columns around the editor and is resolved here, not in the
118
+ * panels, because the sidebar is a sibling of the whole workspace group: a
119
+ * full view that left it standing would not be the width it promises.
120
+ *
121
+ * The mode is derived from `showEditor`, so closing the column returns the
122
+ * chat and the sidebar without writing anything — reopening it comes back to
123
+ * the width the user chose.
124
+ */
125
+ const editorView = useEditorViewLayout(showEditor);
126
+ const sidebarOpen = showLeftPanel && !editorView.sidebarHidden;
127
+
109
128
  // Width restoration is declarative: each panel's `defaultSize` comes from the
110
129
  // remembered width for its current slot and `Panel` adopts a change, so a
111
130
  // view switch needs no imperative push and no cached layout can go stale.
112
131
 
113
- const [settingsOpen, setSettingsOpen] = useState(false);
114
- const [settingsCategory, setSettingsCategory] = useState<SettingsCategoryId>('appearance');
115
- const [autoOpenAddProvider, setAutoOpenAddProvider] = useState(false);
116
132
  const [refreshKey, setRefreshKey] = useState(0);
117
133
  const handleRefreshWorkspace = () => setRefreshKey(k => k + 1);
118
134
  const [sessionTitle, setSessionTitle] = useState<string | null>(null);
119
135
  const streamStatus = useAgentStreamStatus();
136
+ const settings = useSettingsModal();
120
137
 
121
- // The navbar's title menu acts on the session on screen. Its row may live in
122
- // a folder that is not expanded in the sidebar, so the session is found by id
123
- // across every folder rather than by walking the visible list. No row means
124
- // nothing to act on — a session the sidebar does not know (a chat running
125
- // outside every registered workspace) gets no menu rather than dead items.
126
- const activeSession = useMemo(() => {
127
- if (!sessionId) return null;
128
- for (const folder of folders) {
129
- const found = (folder.sessions || []).find((s) => String(s.id) === String(sessionId));
130
- if (found) return found;
131
- }
132
- return null;
133
- }, [folders, sessionId]);
134
- const sidebar = useSidebarData();
135
138
  const { toasts, pushToast, dismissToast } = useToasts();
136
139
 
137
140
  /**
@@ -143,20 +146,19 @@ export function DesktopLayout({ sessionId, onSwitchToMobile, appSettings = {} }:
143
146
  (result: EditorDropResult) => pushToast(result.message, result.ok ? 'success' : 'error'),
144
147
  [pushToast],
145
148
  );
146
- const { handleArchive, handleRename, handleRenameWithAi } = useSessionActions(sidebar.refresh, pushToast);
147
- const sessionDelete = useSessionDelete();
148
- // A pending `new-…` chat has no transcript anywhere yet, so rename and delete
149
- // are refused by the server; the menu hides them the way the sidebar does.
150
- const isPendingSession = Boolean(sessionId && sessionId.startsWith('new-'));
151
- const sessionActions = activeSession
152
- ? {
153
- isArchived: activeSession.is_archived === 1,
154
- onRename: isPendingSession ? undefined : (name: string) => void handleRename(activeSession, name),
155
- onRenameWithAi: isPendingSession ? undefined : () => void handleRenameWithAi(activeSession),
156
- onArchive: () => handleArchive(activeSession),
157
- onDelete: isPendingSession ? undefined : () => sessionDelete.requestDelete(activeSession),
158
- }
159
- : undefined;
149
+ const { menu: sessionActions, sessionDelete } = useNavbarSessionActions(folders, sessionId, pushToast);
150
+
151
+ /**
152
+ * Ask for the sidebar (or put it away).
153
+ *
154
+ * Full view hides the sidebar, so asking for it must leave that mode —
155
+ * otherwise the toggle flips a stored flag the layout is ignoring and the
156
+ * click does nothing. `compact` is the mode that keeps the wider editor.
157
+ */
158
+ const handleToggleLeftPanel = (show: boolean) => {
159
+ if (show) editorView.revealSidebar();
160
+ setShowLeftPanel(show);
161
+ };
160
162
 
161
163
  // The workspace shortcuts, from the shared keymap so the toggles' tooltips
162
164
  // name the bound chord. `⌘S` is absent: the editor owns it, and binding it
@@ -164,26 +166,13 @@ export function DesktopLayout({ sessionId, onSwitchToMobile, appSettings = {} }:
164
166
  useWorkspaceKeybindings({
165
167
  showEditor,
166
168
  editorHasContent,
167
- toggleSidebar: () => setShowLeftPanel((previous) => !previous),
169
+ // Through the handler, not the raw flag: in full view the sidebar is hidden
170
+ // by the editor's WIDTH mode, so flipping `showLeftPanel` alone would change
171
+ // a value the layout is ignoring and the chord would do nothing.
172
+ toggleSidebar: () => handleToggleLeftPanel(!sidebarOpen),
168
173
  toggleEditor: () => setUserToggledEditor((previous) => !(previous ?? showEditor)),
169
174
  toggleRightPanel: () => setShowRightPanel((previous) => !previous),
170
- openSettings: () => {
171
- setSettingsCategory('appearance');
172
- setAutoOpenAddProvider(false);
173
- setSettingsOpen((previous) => !previous);
174
- },
175
- });
176
-
177
- // Global event listener for settings triggers
178
- useKabmoEvent('omp:open-settings', (e) => {
179
- const customEvent = e as CustomEvent<{ category?: SettingsCategoryId; autoOpenAdd?: boolean }>;
180
- if (customEvent.detail?.category) {
181
- setSettingsCategory(customEvent.detail.category);
182
- } else {
183
- setSettingsCategory('appearance');
184
- }
185
- setAutoOpenAddProvider(!!customEvent.detail?.autoOpenAdd);
186
- setSettingsOpen(true);
175
+ openSettings: () => (settings.isOpen ? settings.close() : settings.open()),
187
176
  });
188
177
 
189
178
  // Clicking the ACTIVE view on an open panel collapses that panel; any other
@@ -194,7 +183,6 @@ export function DesktopLayout({ sessionId, onSwitchToMobile, appSettings = {} }:
194
183
  if (!collapsing) setActiveRightPanel(panel);
195
184
  };
196
185
 
197
- const handleToggleLeftPanel = (show: boolean) => setShowLeftPanel(show);
198
186
  const handleToggleRightPanel = () => setShowRightPanel(!showRightPanel);
199
187
  const handleToggleEditor = () => setUserToggledEditor(!showEditor);
200
188
 
@@ -237,7 +225,7 @@ export function DesktopLayout({ sessionId, onSwitchToMobile, appSettings = {} }:
237
225
  >
238
226
  <LeftColumn
239
227
  panelRef={leftPanelRef}
240
- open={showLeftPanel}
228
+ open={sidebarOpen}
241
229
  railEnabled={dock.enabled}
242
230
  view={leftView}
243
231
  onSelectView={(panel) => {
@@ -245,7 +233,7 @@ export function DesktopLayout({ sessionId, onSwitchToMobile, appSettings = {} }:
245
233
  // toggle would hide the rail the user is aiming at, and the dock
246
234
  // has its own collapse control in the header.
247
235
  setActiveLeftView(panel);
248
- if (!showLeftPanel) handleToggleLeftPanel(true);
236
+ if (!sidebarOpen) handleToggleLeftPanel(true);
249
237
  }}
250
238
  onCollapse={() => handleToggleLeftPanel(false)}
251
239
  width={sidebarWidth}
@@ -266,20 +254,21 @@ export function DesktopLayout({ sessionId, onSwitchToMobile, appSettings = {} }:
266
254
  <TopNavbar
267
255
  sessionTitle={sessionTitle}
268
256
  sessionId={sessionId}
269
- showLeftPanel={showLeftPanel}
257
+ showLeftPanel={sidebarOpen}
270
258
  showEditor={showEditor}
271
259
  showRightPanel={showRightPanel}
272
260
  streamStatus={streamStatus}
273
261
  onSwitchToMobile={onSwitchToMobile}
274
262
  onToggleEditor={handleToggleEditor}
275
263
  onToggleRightPanel={handleToggleRightPanel}
276
- onToggleLeftPanel={() => handleToggleLeftPanel(!showLeftPanel)}
264
+ onToggleLeftPanel={() => handleToggleLeftPanel(!sidebarOpen)}
277
265
  sessionActions={sessionActions}
278
266
  />
279
267
 
280
268
  <WorkspacePanels
281
269
  appSettings={appSettings}
282
270
  showEditor={showEditor}
271
+ chatHidden={editorView.chatHidden}
283
272
  editorPanelRef={editorPanelRef}
284
273
  showRightPanel={showRightPanel}
285
274
  activeRightPanel={activeRightPanel}
@@ -315,15 +304,12 @@ export function DesktopLayout({ sessionId, onSwitchToMobile, appSettings = {} }:
315
304
  </div>
316
305
 
317
306
  {/* Global Desktop Settings Modal */}
318
- <SettingsModal
319
- isOpen={settingsOpen}
320
- onClose={() => {
321
- setSettingsOpen(false);
322
- setAutoOpenAddProvider(false);
323
- }}
324
- initialCategory={settingsCategory}
325
- autoOpenAddProvider={autoOpenAddProvider}
326
- appSettings={appSettings}
307
+ <SettingsModal
308
+ isOpen={settings.isOpen}
309
+ onClose={settings.close}
310
+ initialCategory={settings.category}
311
+ autoOpenAddProvider={settings.autoOpenAddProvider}
312
+ appSettings={appSettings}
327
313
  />
328
314
 
329
315
  {/* The navbar title menu's delete path and the rename-with-AI refusal
@@ -212,11 +212,14 @@ export function Panel(props: {
212
212
 
213
213
  const baseStyle: CSSProperties = filler
214
214
  ? {
215
- flex: '1 1 0%',
215
+ // A collapsed filler keeps its DOM (and therefore its state: the chat
216
+ // owns the live stream and the composer) but takes no width, so the
217
+ // sibling panels' own widths are the whole row.
218
+ flex: collapsed ? '0 0 0%' : '1 1 0%',
216
219
  // The filler keeps its floor on the group's axis (and none across it,
217
220
  // where it is sized by the container): see the group model note above.
218
- minWidth: orientation === 'horizontal' ? minPx : 0,
219
- minHeight: orientation === 'horizontal' ? 0 : minPx,
221
+ minWidth: orientation === 'horizontal' ? (collapsed ? 0 : minPx) : 0,
222
+ minHeight: orientation === 'horizontal' ? 0 : collapsed ? 0 : minPx,
220
223
  overflow: 'hidden',
221
224
  position: 'relative',
222
225
  }
@@ -14,7 +14,9 @@
14
14
  * props; this file only decides how the two states look.
15
15
  */
16
16
 
17
+ import { useState } from 'preact/hooks';
17
18
  import { ChevronDown, ChevronRight, MoreHorizontal, Pin, Plus } from 'lucide-preact';
19
+ import { NewWorktreeModal } from '@/client/components/workspace/worktree-modal/index';
18
20
  import { SelectionCheckbox } from '@/client/components/common/batch-selection/SelectionCheckbox';
19
21
  import { WorkspaceOptionsMenu } from '@/client/components/common/workspace-options-menu';
20
22
  import { getProjectIcon } from '@/shared/lib/workspace/project-icon';
@@ -39,6 +41,8 @@ export interface CategoryHeaderProps {
39
41
  showMenu: boolean;
40
42
  onToggleMenu: () => void;
41
43
  onNewSession: () => void;
44
+ /** Start a chat in a worktree made from this folder. */
45
+ onNewWorktreeSession?: (folderId: number, worktreePath: string) => void;
42
46
  onOpenFolder?: () => void;
43
47
  onSelectSessions?: () => void;
44
48
  confirmDelete: boolean;
@@ -58,6 +62,7 @@ export function CategoryHeader({
58
62
  showMenu,
59
63
  onToggleMenu,
60
64
  onNewSession,
65
+ onNewWorktreeSession,
61
66
  onOpenFolder,
62
67
  onSelectSessions,
63
68
  confirmDelete,
@@ -67,6 +72,10 @@ export function CategoryHeader({
67
72
  onCancelDelete,
68
73
  }: CategoryHeaderProps) {
69
74
  const ProjectIcon = getProjectIcon(folder.icon);
75
+ // The dialog lives here rather than in the page: the row that opens it is this
76
+ // one, and the folder it belongs to is what the dialog needs.
77
+ const [worktreeDialogOpen, setWorktreeDialogOpen] = useState(false);
78
+ const canCreateWorktree = Boolean(onNewWorktreeSession && folder.project_path);
70
79
 
71
80
  return (
72
81
  <div className="group flex items-center justify-between h-7 text-xs font-semibold text-ink px-2 hover:bg-ink/5 rounded-md transition-colors select-none">
@@ -159,6 +168,7 @@ export function CategoryHeader({
159
168
  isPinned={folder.isPinned}
160
169
  onOpenFolder={onOpenFolder}
161
170
  onSelectSessions={onSelectSessions}
171
+ onNewWorktree={canCreateWorktree ? () => { onToggleMenu(); setWorktreeDialogOpen(true); } : undefined}
162
172
  confirmDelete={confirmDelete}
163
173
  onPin={onPin}
164
174
  onDelete={onDelete}
@@ -170,6 +180,20 @@ export function CategoryHeader({
170
180
  </div>
171
181
  </div>
172
182
  )}
183
+
184
+ {canCreateWorktree && (
185
+ <NewWorktreeModal
186
+ isOpen={worktreeDialogOpen}
187
+ onClose={() => setWorktreeDialogOpen(false)}
188
+ root={folder.project_path ?? null}
189
+ folderId={folder.id}
190
+ folderName={folder.name}
191
+ onCreated={(created) => {
192
+ setWorktreeDialogOpen(false);
193
+ onNewWorktreeSession?.(folder.id, created.path);
194
+ }}
195
+ />
196
+ )}
173
197
  </div>
174
198
  );
175
199
  }
@@ -0,0 +1,126 @@
1
+ /**
2
+ * @license
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ /**
7
+ * The folder header's `New worktree…` entry.
8
+ *
9
+ * This row is the ONLY entry point to the dialog on the desktop, and it is
10
+ * reachable through four components (SessionsView → SessionList → Category →
11
+ * CategoryHeader). The prop is optional at every hop, so a hop that forgets to
12
+ * forward it fails silently: the typechecker is satisfied, the menu renders, and
13
+ * the row is simply absent. Measured — that is exactly how the desktop half
14
+ * shipped unreachable while the phone's worked.
15
+ *
16
+ * The header is mounted directly here, which is the half that can be asserted
17
+ * cheaply; the assembled path is what `tests/e2e/specs/worktree.e2e.ts` covers.
18
+ */
19
+
20
+ import { afterAll, afterEach, beforeAll, describe, expect, test } from 'bun:test';
21
+ import { Window } from 'happy-dom';
22
+ import { h, render } from 'preact';
23
+ import { act } from 'preact/test-utils';
24
+ import { CategoryHeader } from '@/client/components/layout/session-sidebar/CategoryHeader';
25
+ import { installDomGlobals, restoreDomGlobals } from '@/test-support/pristine-globals';
26
+ import type { WorkspaceFolderData } from '@/shared/types';
27
+
28
+ const folder: WorkspaceFolderData = {
29
+ id: 7,
30
+ name: 'kabmo',
31
+ project_path: '/Users/dev/kabmo',
32
+ isExpanded: true,
33
+ hasMore: false,
34
+ totalSessions: 0,
35
+ sessions: [],
36
+ };
37
+
38
+ let host: HTMLElement | null = null;
39
+
40
+ beforeAll(() => {
41
+ installDomGlobals(new Window({ url: 'http://localhost' }));
42
+ });
43
+
44
+ afterEach(() => {
45
+ if (host) {
46
+ render(null, host);
47
+ host.remove();
48
+ host = null;
49
+ }
50
+ });
51
+
52
+ afterAll(() => {
53
+ restoreDomGlobals();
54
+ });
55
+
56
+ /** Mount the header with its `⋯` menu already open. */
57
+ function mountHeader(overrides: Partial<Parameters<typeof CategoryHeader>[0]> = {}): HTMLElement {
58
+ host = document.createElement('div');
59
+ document.body.appendChild(host);
60
+ render(
61
+ h(CategoryHeader, {
62
+ folder,
63
+ isOpen: true,
64
+ onToggleFolder: () => {},
65
+ checkbox: null,
66
+ batchActive: false,
67
+ menuRef: { current: null },
68
+ showMenu: true,
69
+ onToggleMenu: () => {},
70
+ onNewSession: () => {},
71
+ confirmDelete: false,
72
+ onPin: () => {},
73
+ onDelete: () => {},
74
+ onRequestDelete: () => {},
75
+ onCancelDelete: () => {},
76
+ ...overrides,
77
+ }),
78
+ host,
79
+ );
80
+ return host;
81
+ }
82
+
83
+ /** The menu's own buttons, by their words. */
84
+ function menuRows(container: HTMLElement): string[] {
85
+ return [...container.querySelectorAll('button')].map((button) => (button.textContent ?? '').trim()).filter(Boolean);
86
+ }
87
+
88
+ describe('CategoryHeader worktree entry', () => {
89
+ test('the menu offers New worktree… when a handler is supplied', () => {
90
+ const container = mountHeader({ onNewWorktreeSession: () => {} });
91
+ expect(menuRows(container)).toContain('New worktree…');
92
+ });
93
+
94
+ test('no row without a handler — a folder that cannot create one does not offer it', () => {
95
+ const container = mountHeader();
96
+ expect(menuRows(container).some((row) => row.startsWith('New worktree'))).toBe(false);
97
+ });
98
+
99
+ test('a folder with no project path offers nothing: a worktree needs a repository', () => {
100
+ const container = mountHeader({
101
+ folder: { ...folder, project_path: null },
102
+ onNewWorktreeSession: () => {},
103
+ });
104
+ expect(menuRows(container).some((row) => row.startsWith('New worktree'))).toBe(false);
105
+ });
106
+
107
+ test('clicking it opens the dialog, closed on its own name field', async () => {
108
+ const container = mountHeader({ onNewWorktreeSession: () => {} });
109
+ const row = [...container.querySelectorAll('button')].find(
110
+ (button) => (button.textContent ?? '').trim() === 'New worktree…',
111
+ );
112
+ if (!row) throw new Error('no New worktree row rendered');
113
+
114
+ await act(async () => {
115
+ row.click();
116
+ });
117
+
118
+ // The dialog names the project it will branch from, and starts on a
119
+ // generated branch with its folder following it.
120
+ const text = (document.body.textContent ?? '').replace(/\s+/g, ' ');
121
+ expect(text).toContain('Start from');
122
+ expect(text).toContain('kabmo');
123
+ const branch = (document.body.querySelector('input') as HTMLInputElement | null)?.value ?? '';
124
+ expect(branch).toMatch(/^wt\/[a-z]+-[a-z]+$/);
125
+ });
126
+ });
@@ -24,6 +24,7 @@ export function Category({
24
24
  activeSessionId,
25
25
  onSelectSession,
26
26
  onNewSessionForFolder,
27
+ onNewWorktreeSession,
27
28
  forceExpanded = false,
28
29
  showArchived = false,
29
30
  sessionStatus = {},
@@ -36,6 +37,7 @@ export function Category({
36
37
  activeSessionId: number | string | null;
37
38
  onSelectSession: (id: number | string) => void;
38
39
  onNewSessionForFolder: (id: number) => void;
40
+ onNewWorktreeSession?: (folderId: number, worktreePath: string) => void;
39
41
  forceExpanded?: boolean;
40
42
  showArchived?: boolean;
41
43
  sessionStatus?: Record<string, 'stream' | 'finish' | 'abort'>;
@@ -172,6 +174,7 @@ export function Category({
172
174
  showMenu={showMenu}
173
175
  onToggleMenu={() => setShowMenu(!showMenu)}
174
176
  onNewSession={() => onNewSessionForFolder(folder.id)}
177
+ onNewWorktreeSession={onNewWorktreeSession}
175
178
  onOpenFolder={onOpenFolder}
176
179
  onSelectSessions={
177
180
  onStartSessionSelection
@@ -211,6 +214,7 @@ export function Category({
211
214
  isArchived={session.is_archived === 1}
212
215
  status={sessionStatus[sessionKey]}
213
216
  awaitingInput={Boolean(session.awaitingInput)}
217
+ worktree={session.worktree}
214
218
  timeAgo={relativeTimeAgo(session.updated_at ?? session.created_at)}
215
219
  onClick={() => onSelectSession(session.id)}
216
220
  onArchive={() => handleArchive(session)}