@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
@@ -21,6 +21,7 @@ import {
21
21
  EDITOR_MIN_FONT_SIZE,
22
22
  } from '@/shared/lib/code/editor/typography';
23
23
  import { useEditorFontStack } from '@/client/hooks/editor/font';
24
+ import { useEditorTheme } from '@/client/hooks/editor/theme';
24
25
  import { MOBILE_SCROLL_POSITIONS_KEY } from '@/shared/lib/workspace/scroll-positions';
25
26
  import { ViewerSurface, hasViewerSurface } from '@/client/components/workspace/editor/viewers/dispatch';
26
27
  import { BINARY_VIEWERS, RENDERED_VIEWERS, VIEWER_LABELS, viewerKindFor } from '@/shared/lib/fs/document-kind';
@@ -49,6 +50,7 @@ export function MobileFullEditor({ file, missing = false, onClose, onFileSaved,
49
50
  const [fontSize, setFontSize] = useState(EDITOR_DEFAULT_FONT_SIZE);
50
51
  const [wordWrap, setWordWrap] = useState(true);
51
52
  const editorFontFamily = useEditorFontStack();
53
+ const editorTheme = useEditorTheme();
52
54
 
53
55
  const isMd = viewerKind === 'markdown';
54
56
  // A byte viewer (image, PDF, DOCX, workbook) owns the whole surface and has
@@ -76,6 +78,12 @@ export function MobileFullEditor({ file, missing = false, onClose, onFileSaved,
76
78
  missing,
77
79
  });
78
80
 
81
+ /**
82
+ * The code surface's scroller, which belongs to CodeMirror and appears only
83
+ * once its view mounts.
84
+ */
85
+ const [codeScroller, setCodeScroller] = useState<HTMLElement | null>(null);
86
+
79
87
  // Closing and reopening the same file is a remount, so the offset is restored
80
88
  // from the session's own state — the phone shows one file at a time, and its
81
89
  // key is that file's path (a rendered view keeps its own offset, as on the
@@ -88,6 +96,9 @@ export function MobileFullEditor({ file, missing = false, onClose, onFileSaved,
88
96
  // and the desktop drops entries when a tab closes — against ids that mean
89
97
  // nothing here.
90
98
  slot: MOBILE_SCROLL_POSITIONS_KEY,
99
+ // The code surface's scroller belongs to CodeMirror and appears only once
100
+ // its view mounts; the markdown preview owns its own container.
101
+ element: isMd && isPreview ? undefined : codeScroller,
91
102
  });
92
103
 
93
104
  // A search row on the phone opens this full-screen editor, so the hand-off
@@ -135,9 +146,15 @@ export function MobileFullEditor({ file, missing = false, onClose, onFileSaved,
135
146
  // The sheet's own list, from the LIVE buffer — the same derivation the
136
147
  // desktop section uses, so the two cannot disagree about what a state means.
137
148
  // A byte viewer has no text buffer to scan, which is its own sentence.
138
- const { symbols: outlineSymbols, notice: outlineNotice } = useOutlineView({
149
+ const {
150
+ symbols: outlineSymbols,
151
+ total: outlineTotal,
152
+ truncated: outlineTruncated,
153
+ notice: outlineNotice,
154
+ } = useOutlineView({
139
155
  content,
140
156
  language: lang,
157
+ path: file.path ?? null,
141
158
  state: byteViewer ? 'not-a-document' : editor.loadError ? 'error' : editor.loaded ? 'ready' : 'loading',
142
159
  note: byteViewer
143
160
  ? `${VIEWER_LABELS[viewerKind]} files have no text outline.`
@@ -148,6 +165,7 @@ export function MobileFullEditor({ file, missing = false, onClose, onFileSaved,
148
165
  <div
149
166
  className="fixed inset-x-0 top-0 h-dvh z-[60] bg-paper flex flex-col font-mono text-xs select-none"
150
167
  {...drop.dropProps}
168
+ {...drop.captureDropProps}
151
169
  >
152
170
  <EditorDropOverlay visible={drop.isDragging} />
153
171
 
@@ -208,7 +226,9 @@ export function MobileFullEditor({ file, missing = false, onClose, onFileSaved,
208
226
  content={editor.content}
209
227
  revision={editor.revision}
210
228
  />
211
- ) : (
229
+ ) : editor.isLoading || editor.unsupported || editor.loadError || (isMd && isPreview) ? (
230
+ // The states that draw their own scrolling content — and the markdown
231
+ // preview, which is a document, not an editor — share one scroller.
212
232
  <div
213
233
  ref={scroll.ref}
214
234
  onScroll={scroll.onScroll}
@@ -233,7 +253,7 @@ export function MobileFullEditor({ file, missing = false, onClose, onFileSaved,
233
253
  <span className="text-xs text-error font-sans">Could not read {file.name}</span>
234
254
  <span className="text-[11px] text-ink/50 font-mono break-all">{editor.loadError}</span>
235
255
  </div>
236
- ) : isMd && isPreview ? (
256
+ ) : (
237
257
  /* Markdown Preview Mode — styled by `.prose-content` (the chat
238
258
  timeline's system), see the desktop editor for why there is no
239
259
  `prose` wrapper. */
@@ -244,42 +264,41 @@ export function MobileFullEditor({ file, missing = false, onClose, onFileSaved,
244
264
  scope={{ path: file.path ?? file.name, root: file.root, repo: file.repo }}
245
265
  />
246
266
  </div>
247
- ) : (
248
- /* Code Editor with Line Numbers - Desktop styled: gutter bg-canvas border-r border-ink/10 */
249
- <CodeSurface
250
- ref={surfaceRef}
251
- value={content}
252
- onValueChange={editor.onChange}
253
- language={lang}
254
- wordWrap={wordWrap}
255
- marks={revealMarks}
256
- currentMark={currentMark}
257
- rootClassName="flex min-h-full bg-paper"
258
- // The gutter auto-sizes to its widest number rather than taking a
259
- // fixed width: the numbers scale with the zoom control, and a
260
- // pinned `w-10` clipped a 3-digit line number at 18px.
261
- gutterClassName="flex flex-col text-right pr-2 select-none text-ink/30 bg-canvas border-r border-ink/10 font-mono flex-shrink-0"
262
- gutterLineClassName="min-w-[1.5rem]"
263
- gutterStyle={{
264
- fontSize,
265
- paddingTop: 12,
266
- paddingBottom: 12,
267
- lineHeight: EDITOR_LINE_HEIGHT,
268
- fontFamily: editorFontFamily,
269
- }}
270
- editorWrapperClassName="flex-1 p-3 overflow-x-auto min-w-0 bg-paper text-ink code-surface"
271
- editorClassName="focus:outline-none"
272
- editorStyle={{
273
- fontFamily: editorFontFamily,
274
- fontSize,
275
- lineHeight: EDITOR_LINE_HEIGHT,
276
- backgroundColor: 'transparent',
277
- minHeight: '100%',
278
- color: 'var(--theme-ink)',
279
- }}
280
- />
281
267
  )}
282
268
  </div>
269
+ ) : (
270
+ /* The code surface owns its own scroller (CodeMirror's), so it is NOT
271
+ wrapped in one: an outer `overflow-auto` resolves the editor's
272
+ `height: 100%` against an auto-height box, so the editor grew to the
273
+ document's full height and the panel had nothing to scroll. */
274
+ <CodeSurface
275
+ ref={surfaceRef}
276
+ value={content}
277
+ onValueChange={editor.onChange}
278
+ language={lang}
279
+ wordWrap={wordWrap}
280
+ documentKey={fileKey}
281
+ marks={revealMarks}
282
+ currentMark={currentMark}
283
+ onScroller={setCodeScroller}
284
+ scrollTestId="editor-scroller"
285
+ // No `bg-paper`: an explicit editor theme paints its own surface, and
286
+ // the panel's colour under it would be a border of the wrong shade.
287
+ // `select-text` is the desktop pane's rule, and it matters more here:
288
+ // the sheet's own root is `select-none` (it is a header-and-status
289
+ // layout), and the editor is now a `contenteditable` whose selection
290
+ // and caret are the BROWSER's business — an inherited `user-select:
291
+ // none` is the one thing that can take that away on a phone.
292
+ className="flex-1 min-h-0 text-ink select-text"
293
+ themeVars={editorTheme.vars}
294
+ themes={editorTheme.themes}
295
+ padding={12}
296
+ style={{
297
+ fontFamily: editorFontFamily,
298
+ fontSize: `${fontSize}px`,
299
+ lineHeight: EDITOR_LINE_HEIGHT,
300
+ }}
301
+ />
283
302
  )}
284
303
 
285
304
  {/* The outline is a floating control rather than a panel section: on a
@@ -289,7 +308,13 @@ export function MobileFullEditor({ file, missing = false, onClose, onFileSaved,
289
308
  target over the content, list in a sheet. Hidden for a byte viewer,
290
309
  which has no text to outline. */}
291
310
  {!byteViewer && (
292
- <MobileOutlineButton path={file.path ?? null} symbols={outlineSymbols} emptyNotice={outlineNotice} />
311
+ <MobileOutlineButton
312
+ path={file.path ?? null}
313
+ symbols={outlineSymbols}
314
+ total={outlineTotal}
315
+ truncated={outlineTruncated}
316
+ emptyNotice={outlineNotice}
317
+ />
293
318
  )}
294
319
 
295
320
  <EditorStatusBar
@@ -0,0 +1,137 @@
1
+ /**
2
+ * @license
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ /**
7
+ * The phone's outline sheet.
8
+ *
9
+ * Same two facts the desktop section shows, on a surface that cannot share its
10
+ * markup (the drawer has no room for a section pinned to a floor, so this is a
11
+ * floating button and a modal). Both are pinned here because each fails
12
+ * silently:
13
+ *
14
+ * - **the button names the TOTAL**, so a truncated list cannot read as "this
15
+ * file has 500 symbols";
16
+ * - **a truncated list says what it is not showing**, in the sheet, where the
17
+ * reader is actually looking.
18
+ *
19
+ * Rendered with `h()` against happy-dom. `Modal` is the shared shell every
20
+ * dialog in the app uses, so opening one here is the same code path a phone
21
+ * takes.
22
+ */
23
+
24
+ import { afterAll, afterEach, beforeAll, describe, expect, test } from 'bun:test';
25
+ import { Window } from 'happy-dom';
26
+ import { h, render } from 'preact';
27
+ import { act } from 'preact/test-utils';
28
+ import { MobileOutlineButton } from '@/client/components/mobile/mobile-right-sidebar/OutlineButton';
29
+ import { MAX_OUTLINE_ROWS, outlineTruncatedNotice } from '@/shared/lib/code/outline/rows';
30
+ import type { OutlineSymbol } from '@/shared/lib/code/outline/types';
31
+ import { installDomGlobals, restoreDomGlobals } from '@/test-support/pristine-globals';
32
+
33
+ let container: HTMLElement;
34
+
35
+ beforeAll(() => {
36
+ installDomGlobals(new Window({ url: 'http://localhost' }));
37
+ });
38
+
39
+ afterEach(() => {
40
+ if (container) {
41
+ render(null, container);
42
+ container.remove();
43
+ }
44
+ });
45
+
46
+ afterAll(() => {
47
+ restoreDomGlobals();
48
+ });
49
+
50
+ function symbols(count: number): OutlineSymbol[] {
51
+ return Array.from({ length: count }, (_, i) => ({
52
+ name: `fn${i}`,
53
+ kind: 'function' as const,
54
+ line: i + 1,
55
+ column: 0,
56
+ depth: 0,
57
+ }));
58
+ }
59
+
60
+ async function mount(props: {
61
+ symbols: OutlineSymbol[];
62
+ total?: number;
63
+ truncated?: boolean;
64
+ path?: string | null;
65
+ emptyNotice?: string;
66
+ }): Promise<void> {
67
+ container = document.createElement('div');
68
+ document.body.appendChild(container);
69
+ await act(async () => {
70
+ render(
71
+ h(MobileOutlineButton, {
72
+ path: props.path === undefined ? 'src/a.ts' : props.path,
73
+ symbols: props.symbols,
74
+ total: props.total ?? props.symbols.length,
75
+ truncated: props.truncated ?? false,
76
+ emptyNotice: props.emptyNotice ?? 'No symbols found.',
77
+ }),
78
+ container,
79
+ );
80
+ });
81
+ }
82
+
83
+ function trigger(): HTMLButtonElement {
84
+ const el = container.querySelector('button[aria-label^="Outline"]');
85
+ if (!el) throw new Error('no outline trigger rendered');
86
+ return el as HTMLButtonElement;
87
+ }
88
+
89
+ async function open(): Promise<void> {
90
+ await act(async () => {
91
+ trigger().click();
92
+ });
93
+ for (let i = 0; i < 4; i += 1) await act(async () => {});
94
+ }
95
+
96
+ describe('MobileOutlineButton', () => {
97
+ test('no path means no control — an attachment chip has nothing to outline', async () => {
98
+ await mount({ symbols: symbols(3), path: null });
99
+
100
+ expect(container.querySelector('button[aria-label^="Outline"]')).toBeNull();
101
+ });
102
+
103
+ test('the trigger names the TOTAL, not the window', async () => {
104
+ await mount({ symbols: symbols(MAX_OUTLINE_ROWS), total: 63_618, truncated: true });
105
+
106
+ // The accessible name is what a screen reader announces and what a spec
107
+ // addresses; a window count here would describe the panel, not the file.
108
+ expect(trigger().getAttribute('aria-label')).toBe('Outline (63618 symbols)');
109
+ });
110
+
111
+ test('a truncated sheet renders the window and says what it is not showing', async () => {
112
+ await mount({ symbols: symbols(MAX_OUTLINE_ROWS), total: 700, truncated: true });
113
+ await open();
114
+
115
+ const rows = container.querySelectorAll('button[aria-label*=", line "]');
116
+ expect(rows).toHaveLength(MAX_OUTLINE_ROWS);
117
+ expect(container.textContent).toContain(outlineTruncatedNotice(700));
118
+ // The header count reads window/total, so it cannot be mistaken for the
119
+ // number of symbols the file has.
120
+ expect(container.textContent).toContain(`${MAX_OUTLINE_ROWS}/700`);
121
+ });
122
+
123
+ test('a complete list shows no truncation sentence', async () => {
124
+ await mount({ symbols: symbols(4) });
125
+ await open();
126
+
127
+ expect(container.querySelectorAll('button[aria-label*=", line "]')).toHaveLength(4);
128
+ expect(container.textContent).not.toContain('Showing the first');
129
+ });
130
+
131
+ test('an empty list prints the reason it is empty, not a blank sheet', async () => {
132
+ await mount({ symbols: [], emptyNotice: 'This tab has no outline.' });
133
+ await open();
134
+
135
+ expect(container.textContent).toContain('This tab has no outline.');
136
+ });
137
+ });
@@ -27,15 +27,21 @@ import { Modal } from '@/client/components/common/Modal';
27
27
  import { OutlineKindIcon, outlineKindLabel } from '@/client/components/common/outline-kind-icon';
28
28
  import { setLineReveal } from '@/client/lib/line-reveal';
29
29
  import type { OutlineSymbol } from '@/shared/lib/code/outline/types';
30
+ import { outlineTruncatedNotice } from '@/shared/lib/code/outline/rows';
30
31
 
31
32
  interface MobileOutlineButtonProps {
32
33
  path: string | null;
34
+ /** The rows to render — already bounded by the hook. */
33
35
  symbols: OutlineSymbol[];
36
+ /** Every symbol found, so the button can say when it is showing a window. */
37
+ total: number;
38
+ /** True when `symbols` is only the first `MAX_OUTLINE_ROWS`. */
39
+ truncated: boolean;
34
40
  /** Why there is nothing to list, when `symbols` is empty. */
35
41
  emptyNotice: string;
36
42
  }
37
43
 
38
- export function MobileOutlineButton({ path, symbols, emptyNotice }: MobileOutlineButtonProps) {
44
+ export function MobileOutlineButton({ path, symbols, total, truncated, emptyNotice }: MobileOutlineButtonProps) {
39
45
  const [open, setOpen] = useState(false);
40
46
  if (!path) return null;
41
47
 
@@ -52,7 +58,7 @@ export function MobileOutlineButton({ path, symbols, emptyNotice }: MobileOutlin
52
58
  <button
53
59
  type="button"
54
60
  onClick={() => setOpen(true)}
55
- aria-label={`Outline (${symbols.length} symbols)`}
61
+ aria-label={`Outline (${total} symbols)`}
56
62
  title="Outline"
57
63
  className="flex h-9 w-9 items-center justify-center rounded-full border border-ink/20 bg-paper text-ink/60 shadow-sm transition-all hover:bg-ink/5 hover:text-ink active:scale-95"
58
64
  >
@@ -69,7 +75,9 @@ export function MobileOutlineButton({ path, symbols, emptyNotice }: MobileOutlin
69
75
  <div className="flex min-w-0 items-center gap-2">
70
76
  <ListTree size={15} className="flex-shrink-0 text-ink/50" />
71
77
  <span className="text-sm font-medium">Outline</span>
72
- <span className="font-mono text-[11px] text-ink/40">{symbols.length}</span>
78
+ <span className="font-mono text-[11px] text-ink/40">
79
+ {truncated ? `${symbols.length}/${total}` : total}
80
+ </span>
73
81
  </div>
74
82
  }
75
83
  >
@@ -92,6 +100,9 @@ export function MobileOutlineButton({ path, symbols, emptyNotice }: MobileOutlin
92
100
  </button>
93
101
  ))
94
102
  )}
103
+ {truncated && (
104
+ <div className="px-4 py-2 text-[11px] italic text-ink/40">{outlineTruncatedNotice(total)}</div>
105
+ )}
95
106
  </div>
96
107
  </Modal>
97
108
  )}
@@ -1,10 +1,7 @@
1
- import { Download, Info, Monitor, Settings } from 'lucide-preact';
1
+ import { Info, Monitor, Settings } from 'lucide-preact';
2
2
  import packageJson from '@/../package.json';
3
- import {
4
- shellDownloadLabel,
5
- shellDownloadShortLabel,
6
- type ShellDownload,
7
- } from '@/client/hooks/ui/updates/shell-download';
3
+ import type { ShellDownload } from '@/client/hooks/ui/updates/shell-download';
4
+ import { ShellUpdateButton } from '@/client/components/common/shell-update-button';
8
5
 
9
6
  interface MobileSessionFooterProps {
10
7
  onSettings: () => void;
@@ -49,27 +46,18 @@ export function MobileSessionFooter({
49
46
  <span>About</span>
50
47
  </button>
51
48
  {shellDownload && (
52
- // Directly after About, matching the desktop footer. An `<a>`, not a
53
- // button: the download is the browser's (or the WebView's) job, and
54
- // `target="_blank"` hands it to the system browser so the console is
55
- // not replaced by a file download.
49
+ // Directly after About, matching the desktop footer.
56
50
  //
57
51
  // The visible word is the OFFER, not the file type: `APK` told a
58
52
  // reader what the bytes are but not what the control does, and on a
59
53
  // browser it is the only place the native shell is ever mentioned. An
60
54
  // iOS update is an IPA, so the word names the platform instead of a
61
55
  // file extension.
62
- <a
63
- href={shellDownload.url}
64
- target="_blank"
65
- rel="noopener noreferrer"
66
- title={shellDownloadLabel(shellDownload)}
67
- aria-label={shellDownloadLabel(shellDownload)}
68
- className="flex items-center space-x-1 hover:text-ink transition-colors"
69
- >
70
- <Download size={14} />
71
- <span>{shellDownloadShortLabel(shellDownload)}</span>
72
- </a>
56
+ //
57
+ // The shared control decides link-versus-button: a desktop shell that
58
+ // knows its architecture installs the artifact itself, and a link
59
+ // there would leave the file in Downloads.
60
+ <ShellUpdateButton download={shellDownload} variant="label" />
73
61
  )}
74
62
  {onDesktopToggle && (
75
63
  <button
@@ -1,17 +1,21 @@
1
1
  import { X } from 'lucide-preact';
2
2
  import { BrandWordmark } from '@/client/components/common/brand-wordmark';
3
3
  import { titlebarPaddingLeft, titlebarPaddingRight } from '@/shared/lib/ui/titlebar';
4
+ import { dragRegionProps, titlebarInsetLeft } from '@/client/lib/desktop/titlebar';
4
5
 
5
6
  export function MobileSessionHeader({ onClose, viewLabel = 'Sessions' }: { onClose: () => void; viewLabel?: string }) {
6
7
  return (
7
8
  <div
8
9
  className="border-b border-ink/10 flex items-center justify-between px-3 flex-shrink-0 bg-canvas titlebar-drag-region select-none"
10
+ {...dragRegionProps()}
9
11
  style={{
10
12
  height: 'calc(3.5rem + env(safe-area-inset-top, 0px))',
11
13
  // `px-3`, the toolbar's own inset: the wordmark above and the New
12
14
  // Session button below share one left edge, which the header's old
13
- // 14px did not.
14
- paddingLeft: titlebarPaddingLeft('0.75rem', true),
15
+ // 14px did not — and the SHELL's own inset, because a desktop shell
16
+ // window narrowed past the layout breakpoint shows this drawer, whose
17
+ // wordmark would otherwise sit under macOS's traffic lights.
18
+ paddingLeft: titlebarInsetLeft(titlebarPaddingLeft('0.75rem', true)),
15
19
  // The drawer is the full window width, so on a desktop showing the
16
20
  // mobile layout its close button sits under the window controls too.
17
21
  paddingRight: titlebarPaddingRight('0.75rem', true),
@@ -4,6 +4,7 @@ import { useSearchParams } from '@/client/lib/router/search-params';
4
4
  import { MobileSessionRow } from '@/client/components/mobile/mobile-session-sidebar/SessionRow';
5
5
  import { SubagentList } from '@/client/components/common/subagent-list';
6
6
  import { WorkspaceOptionsMenu } from '@/client/components/common/workspace-options-menu';
7
+ import { useWorktreeDialog } from '@/client/components/mobile/mobile-session-sidebar/WorktreeDialog';
7
8
  import { useShowMore } from '@/client/hooks/ui/show-more';
8
9
  import { useExpandedSessions } from '@/client/hooks/workspace/expanded-sessions';
9
10
  import { useWorkspaceFolderActions } from '@/client/hooks/workspace/workspace-folder-actions';
@@ -25,6 +26,8 @@ interface MobileSessionCategoryProps {
25
26
  activeSessionId: number | string | null;
26
27
  onSelectSession: (id: number | string) => void;
27
28
  onNewSessionForFolder: (folderId: number) => void;
29
+ /** Start a chat in a worktree made from this folder. */
30
+ onNewWorktreeSession?: (folderId: number, worktreePath: string) => void;
28
31
  isExpanded: boolean;
29
32
  onToggleExpand: () => void;
30
33
  showArchived?: boolean;
@@ -43,6 +46,7 @@ export function MobileSessionCategory({
43
46
  activeSessionId,
44
47
  onSelectSession,
45
48
  onNewSessionForFolder,
49
+ onNewWorktreeSession,
46
50
  isExpanded,
47
51
  onToggleExpand,
48
52
  showArchived = false,
@@ -53,6 +57,11 @@ export function MobileSessionCategory({
53
57
  onCancelWait,
54
58
  }: MobileSessionCategoryProps) {
55
59
  const { visibleCount, showMore, showLess, canShowLess } = useShowMore();
60
+ // The phone's folder row opens the same dialog the desktop one does. Its state
61
+ // lives in a hook so this row stays under the size ceiling.
62
+ const worktreeDialog = useWorktreeDialog(folder, (worktreePath) =>
63
+ onNewWorktreeSession?.(folder.id, worktreePath),
64
+ );
56
65
  const { refresh } = useSidebarData();
57
66
  const { expandedSessionIds, toggleSession } = useExpandedSessions();
58
67
  // The viewed transcript, so its session row dims while a roster entry is
@@ -223,6 +232,11 @@ export function MobileSessionCategory({
223
232
  variant="mobile"
224
233
  isPinned={folder.isPinned}
225
234
  onOpenFolder={onOpenFolder}
235
+ onNewWorktree={
236
+ onNewWorktreeSession && folder.project_path
237
+ ? () => { setShowMenu(false); worktreeDialog.open(); }
238
+ : undefined
239
+ }
226
240
  onSelectSessions={
227
241
  onStartSessionSelection
228
242
  ? () => { setShowMenu(false); onStartSessionSelection(folder.id); }
@@ -326,6 +340,8 @@ export function MobileSessionCategory({
326
340
  onClose={sessionDelete.cancelDelete}
327
341
  onConfirm={() => void sessionDelete.confirmDelete()}
328
342
  />
343
+
344
+ {worktreeDialog.dialog}
329
345
  </div>
330
346
  );
331
347
  }
@@ -10,6 +10,8 @@ interface MobileSessionListProps {
10
10
  sessionStatus: Record<string, 'stream' | 'finish' | 'abort'>;
11
11
  onSelectSession: (id: number | string) => void;
12
12
  onNewSessionForFolder: (folderId: number) => void;
13
+ /** Start a chat in a worktree made from this folder. */
14
+ onNewWorktreeSession?: (folderId: number, worktreePath: string) => void;
13
15
  onToggleFolder: (folderId: number) => void;
14
16
  /** Surfaces a refused folder action (the reveal endpoint's own reason). */
15
17
  onToast?: (message: string, type?: 'success' | 'error') => void;
@@ -28,6 +30,7 @@ export function MobileSessionList({
28
30
  sessionStatus,
29
31
  onSelectSession,
30
32
  onNewSessionForFolder,
33
+ onNewWorktreeSession,
31
34
  onToggleFolder,
32
35
  onToast,
33
36
  batch,
@@ -55,6 +58,7 @@ export function MobileSessionList({
55
58
  activeSessionId={activeSessionId}
56
59
  onSelectSession={onSelectSession}
57
60
  onNewSessionForFolder={onNewSessionForFolder}
61
+ onNewWorktreeSession={onNewWorktreeSession}
58
62
  isExpanded={expandedFolders[folder.id] ?? true}
59
63
  onToggleExpand={() => onToggleFolder(folder.id)}
60
64
  showArchived={showArchived}
@@ -4,6 +4,7 @@ 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';
7
8
 
8
9
  export interface MobileSessionRowProps {
9
10
  session: SessionItemData;
@@ -175,6 +176,10 @@ export function MobileSessionRow({
175
176
  waiting for {waitingFor?.title ?? 'another session'}
176
177
  </span>
177
178
  )}
179
+ {/* The same chip the desktop row draws, from the same field on the
180
+ row payload — one layout cannot be the only one that says where
181
+ the chat writes. */}
182
+ {session.worktree ? <WorktreeBadge worktree={session.worktree} /> : null}
178
183
  </span>
179
184
  </div>
180
185
 
@@ -0,0 +1,49 @@
1
+ /**
2
+ * @license
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ /**
7
+ * The New Worktree dialog as a phone folder row opens it.
8
+ *
9
+ * Split out because the row was at the file-size ceiling and because the
10
+ * dialog's state (open, and which folder) belongs to the dialog rather than to a
11
+ * row that also draws sessions, a `⋯` menu and a delete confirmation. The row
12
+ * calls the hook, hands `open` to its menu row and renders `dialog` at its tail —
13
+ * the menu row has to stay INSIDE the dropdown, which is why this returns the
14
+ * modal rather than the trigger.
15
+ */
16
+
17
+ import { useState } from 'preact/hooks';
18
+ import { NewWorktreeModal } from '@/client/components/workspace/worktree-modal/index';
19
+ import type { WorkspaceFolderData } from '@/shared/types';
20
+
21
+ export interface WorktreeDialogHandle {
22
+ open: () => void;
23
+ /** Rendered at the row's tail; null until the dialog has been opened once. */
24
+ dialog: preact.JSX.Element | null;
25
+ }
26
+
27
+ export function useWorktreeDialog(
28
+ folder: WorkspaceFolderData,
29
+ onCreated: (worktreePath: string) => void,
30
+ ): WorktreeDialogHandle {
31
+ const [isOpen, setIsOpen] = useState(false);
32
+
33
+ return {
34
+ open: () => setIsOpen(true),
35
+ dialog: isOpen ? (
36
+ <NewWorktreeModal
37
+ isOpen
38
+ onClose={() => setIsOpen(false)}
39
+ root={folder.project_path ?? null}
40
+ folderId={folder.id}
41
+ folderName={folder.name}
42
+ onCreated={(created) => {
43
+ setIsOpen(false);
44
+ onCreated(created.path);
45
+ }}
46
+ />
47
+ ) : null,
48
+ };
49
+ }
@@ -0,0 +1,51 @@
1
+ /**
2
+ * @license
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ /**
7
+ * Starting a chat inside a worktree, from either layout's sidebar.
8
+ *
9
+ * Desktop and phone do the same three things in the same order, and the order is
10
+ * what makes it work: prewarm the child in the WORKTREE cwd (not the project's),
11
+ * mint the pending session, write its target BEFORE the URL names it, then
12
+ * navigate. Writing the target afterwards would leave the composer drawing the
13
+ * folder while the send path already had its ref armed for something else — the
14
+ * one outcome the pick exists to prevent.
15
+ *
16
+ * Extracted so both sidebars call one function rather than repeating a sequence
17
+ * whose middle step is invisible when it is wrong.
18
+ */
19
+
20
+ import { startPendingSession } from '@/client/hooks/chat/omp/pending-sessions';
21
+ import { triggerSessionPrewarm } from '@/shared/lib/omp/session/prewarm';
22
+ import { setSessionKey } from '@/shared/lib/workspace/session-state/store';
23
+ import { SPAWN_TARGET_KEY } from '@/client/hooks/chat/timeline/spawn-target';
24
+
25
+ export interface WorktreeSessionDeps {
26
+ setSearchParams: (fn: (prev: URLSearchParams) => URLSearchParams, options?: { replace?: boolean }) => void;
27
+ /** Called after the navigation — the phone closes its drawer here. */
28
+ afterNavigate?: () => void;
29
+ }
30
+
31
+ /**
32
+ * Open a pending chat in `worktreePath`, scoped to `folderId`.
33
+ * Returns the pending session's id, so a caller can report or follow it.
34
+ */
35
+ export function startWorktreeSession(
36
+ deps: WorktreeSessionDeps,
37
+ folderId: number,
38
+ worktreePath: string,
39
+ ): string {
40
+ triggerSessionPrewarm(worktreePath);
41
+ const pendingId = startPendingSession(folderId);
42
+ setSessionKey(pendingId, SPAWN_TARGET_KEY, { folderId, worktreePath });
43
+ deps.setSearchParams((prev) => {
44
+ const next = new URLSearchParams(prev);
45
+ next.set('sessionId', pendingId);
46
+ next.set('folderId', folderId.toString());
47
+ return next;
48
+ }, { replace: true });
49
+ deps.afterNavigate?.();
50
+ return pendingId;
51
+ }
@@ -9,7 +9,9 @@ import { useRestartKabmo } from '@/client/hooks/settings/restart-kabmo';
9
9
  import { applyDocumentTheme } from '@/client/hooks/ui/theme';
10
10
  import { DEFAULT_THEME_ID } from '@/shared/lib/theme/catalog';
11
11
  import { EDITOR_DEFAULT_FONT_FAMILY } from '@/shared/lib/code/editor/typography';
12
+ import { EDITOR_DEFAULT_THEME_ID } from '@/shared/lib/code/editor/themes';
12
13
  import { EDITOR_FONT_CHANGED_EVENT } from '@/client/hooks/editor/font';
14
+ import { EDITOR_THEME_CHANGED_EVENT } from '@/client/hooks/editor/theme';
13
15
  import { useKabmoEvent } from '@/client/hooks/ui/window-event';
14
16
  import { SETTINGS_CATEGORIES, SettingsSidebar } from '@/client/components/settings/Sidebar';
15
17
  import { AppearanceSettings } from '@/client/components/settings/categories/AppearanceSettings';
@@ -41,6 +43,7 @@ export interface SettingsModalProps {
41
43
  const DEFAULT_SETTINGS: SettingsState = {
42
44
  theme: DEFAULT_THEME_ID,
43
45
  editorFont: EDITOR_DEFAULT_FONT_FAMILY,
46
+ editorTheme: EDITOR_DEFAULT_THEME_ID,
44
47
  autoSessionTitle: true,
45
48
  soundAlerts: true,
46
49
  chatCompletionSound: true,
@@ -120,6 +123,11 @@ export function SettingsModal({
120
123
  if (typeof window !== 'undefined' && next.editorFont !== prev.editorFont) {
121
124
  window.dispatchEvent(new CustomEvent(EDITOR_FONT_CHANGED_EVENT, { detail: next.editorFont }));
122
125
  }
126
+ // Same rule for the editor's colour theme: it is visual, and every open
127
+ // editor subscribes rather than reading the boot-time `appSettings`.
128
+ if (typeof window !== 'undefined' && next.editorTheme !== prev.editorTheme) {
129
+ window.dispatchEvent(new CustomEvent(EDITOR_THEME_CHANGED_EVENT, { detail: next.editorTheme }));
130
+ }
123
131
  // Only the keys this update actually touched are persisted. The modal
124
132
  // holds a full `SettingsState` snapshot taken when it opened, so writing
125
133
  // the whole object back would also republish every value it read at that